Tag: blog

  • Display Time on the Blackpill using UART

    Display Time on the Blackpill using UART

    Goal

    My goal for this project was to display the RTC timer from my STM32 to my computer via Putty. Nothing fancy, just displaying the time. Accuracy is not critical for this so the internal oscillator is fine. My final goal is to have a RTC timer running on a STM32 and have it sync via the PPS from my Spectracom Netclock. But before we run I need to learn how to crawl. I’m using this as the opportunity to learn how to interact and retrieve RTC then send it visually through a serial port. The process I followed in this post is based on a tutorial I found on YouTube linked here. I found this channel very helpful for putting things in simple terms and keeping configurations simple. I highly recommend for anyone trying to learn how to use an STM32.

    Configured Pins for USART Connections

    STM32CubeMX Configuration

    The first thing I had to do was start a project is STM32CubeMX. I opened a new project and chose my board (STM32F411CEUx6.) This opens a generic base configuration with nothing selected. The first thing I needed to enable was RTC of course. I was able to set this in in the RTC section listed under Timers. The settings pictured below are all that need to be set.

    RTC Configuration Settings

    The next thing to be configured is the UART connection. This is needed so that you are actually able to interface with the computer and view the time in Putty. To configure this I used USART2 in the “Connectivity” section. All I had to change in this section was setting the mode to Asynchronous and not the Baud rate which is 115200. This will be needed later on.

    UART Configuration Settings

    Once configured the pins PA2 and PA3 both turned green and were labeled Rx and Tx respectively. These correspond to the two GPIO headers I needed to connected to. (A2,A3) After this I was able to save my project and generate code.

    Code Generation

    Now that the code is generated I actually need to configure it so that the STM knows what I want it to do. Currently its just enable functions to put it simply.

    The first section I modified was for User Includes. This Include will import the stdio C library which is used later for snprintf to convert binary to a string.

    /* USER CODE BEGIN Includes */
    #include <stdio.h>
    /* USER CODE END Includes */

    Next I added the character buffer as a User private variable. This where the strings will be stored once they are generated.

    /* USER CODE BEGIN PV */
    char buffer[100];  // Buffer to hold time and date strings
    /* USER CODE END PV */

    To make the code cleaner and easier to read the sDate and sTime variables and synced them to their respective functions.

    /* USER CODE BEGIN 2 */
    RTC_TimeTypeDef sTime;
    RTC_DateTypeDef sDate;
    /* USER CODE END 2 */

    The final step is the while loop setup. This is where the meat and potatoes happen. The first part of the while loop calls to RTC to get the time and date. These are used to add values to sTime and sDate. At this point they are stored in binary. Snprintf is then called so that we can format our binary strings into readable text and format them the way we want. Now that the string has been manipulated from binary to a formatted string its ready to be transmitted. That is where the final HAL function comes in. It takes all of our information and puts it together and sends it. This is what shows in Putty. It gets updated once per second thanks to the HAL_Delay

    /* USER CODE BEGIN WHILE */
      while (1)
      {
        /* USER CODE END WHILE */
    
    /* USER CODE BEGIN 3 */
      // Get the current time and date from RTC
      HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
      HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
    
      // Format time and date into a string
      snprintf(buffer, sizeof(buffer), "Time: %02d:%02d:%02d, Date: %02d-%02d-%02d\r\n",
               sTime.Hours, sTime.Minutes, sTime.Seconds,
               sDate.Date, sDate.Month, sDate.Year + 2000);
    
      // Transmit the formatted string over USART
      HAL_UART_Transmit(&huart2, (uint8_t*)buffer, strlen(buffer), HAL_MAX_DELAY);
    
      HAL_Delay(1000);  // Update every second
      }
      /* USER CODE END 3 */
    View Full Code Here
    /* USER CODE BEGIN Header */
    /**
      ******************************************************************************
      * @file           : main.c
      * @brief          : Main program body
      ******************************************************************************
      * @attention
      *
      * Copyright (c) 2025 STMicroelectronics.
      * All rights reserved.
      *
      * This software is licensed under terms that can be found in the LICENSE file
      * in the root directory of this software component.
      * If no LICENSE file comes with this software, it is provided AS-IS.
      *
      ******************************************************************************
      */
    /* USER CODE END Header */
    /* Includes ------------------------------------------------------------------*/
    #include "main.h"
    
    /* Private includes ----------------------------------------------------------*/
    /* USER CODE BEGIN Includes */
    #include <stdio.h>
    /* USER CODE END Includes */
    
    /* Private typedef -----------------------------------------------------------*/
    /* USER CODE BEGIN PTD */
    
    /* USER CODE END PTD */
    
    /* Private define ------------------------------------------------------------*/
    /* USER CODE BEGIN PD */
    
    /* USER CODE END PD */
    
    /* Private macro -------------------------------------------------------------*/
    /* USER CODE BEGIN PM */
    
    /* USER CODE END PM */
    
    /* Private variables ---------------------------------------------------------*/
    RTC_HandleTypeDef hrtc;
    
    UART_HandleTypeDef huart2;
    
    /* USER CODE BEGIN PV */
    char buffer[100];  // Buffer to hold time and date strings
    /* USER CODE END PV */
    
    /* Private function prototypes -----------------------------------------------*/
    void SystemClock_Config(void);
    static void MX_GPIO_Init(void);
    static void MX_RTC_Init(void);
    static void MX_USART2_UART_Init(void);
    /* USER CODE BEGIN PFP */
    
    /* USER CODE END PFP */
    
    /* Private user code ---------------------------------------------------------*/
    /* USER CODE BEGIN 0 */
    
    /* USER CODE END 0 */
    
    /**
      * @brief  The application entry point.
      * @retval int
      */
    int main(void)
    {
    
      /* USER CODE BEGIN 1 */
    
      /* USER CODE END 1 */
    
      /* MCU Configuration--------------------------------------------------------*/
    
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
    
      /* USER CODE BEGIN Init */
    
      /* USER CODE END Init */
    
      /* Configure the system clock */
      SystemClock_Config();
    
      /* USER CODE BEGIN SysInit */
    
      /* USER CODE END SysInit */
    
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_RTC_Init();
      MX_USART2_UART_Init();
      /* USER CODE BEGIN 2 */
      RTC_TimeTypeDef sTime;
      RTC_DateTypeDef sDate;
      /* USER CODE END 2 */
    
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
    	  // Get the current time and date from RTC
    	  HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
    	  HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
    
    	  // Format time and date into a string
    	  snprintf(buffer, sizeof(buffer), "Time: %02d:%02d:%02d, Date: %02d-%02d-%02d\r\n",
    	           sTime.Hours, sTime.Minutes, sTime.Seconds,
    	           sDate.Date, sDate.Month, sDate.Year + 2000);
    
    	  // Transmit the formatted string over USART
    	  HAL_UART_Transmit(&huart2, (uint8_t*)buffer, strlen(buffer), HAL_MAX_DELAY);
    
    	  HAL_Delay(1000);  // Update every second
      }
      /* USER CODE END 3 */
    }
    
    /**
      * @brief System Clock Configuration
      * @retval None
      */
    void SystemClock_Config(void)
    {
      RCC_OscInitTypeDef RCC_OscInitStruct = {0};
      RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
    
      /** Configure the main internal regulator output voltage
      */
      __HAL_RCC_PWR_CLK_ENABLE();
      __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
    
      /** Initializes the RCC Oscillators according to the specified parameters
      * in the RCC_OscInitTypeDef structure.
      */
      RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_LSI;
      RCC_OscInitStruct.HSIState = RCC_HSI_ON;
      RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
      RCC_OscInitStruct.LSIState = RCC_LSI_ON;
      RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
      if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
      {
        Error_Handler();
      }
    
      /** Initializes the CPU, AHB and APB buses clocks
      */
      RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                                  |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
      RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
      RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
      RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
      RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
    
      if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
      {
        Error_Handler();
      }
    }
    
    /**
      * @brief RTC Initialization Function
      * @param None
      * @retval None
      */
    static void MX_RTC_Init(void)
    {
    
      /* USER CODE BEGIN RTC_Init 0 */
    
      /* USER CODE END RTC_Init 0 */
    
      RTC_TimeTypeDef sTime = {0};
      RTC_DateTypeDef sDate = {0};
    
      /* USER CODE BEGIN RTC_Init 1 */
    
      /* USER CODE END RTC_Init 1 */
    
      /** Initialize RTC Only
      */
      hrtc.Instance = RTC;
      hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
      hrtc.Init.AsynchPrediv = 127;
      hrtc.Init.SynchPrediv = 255;
      hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
      hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
      hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
      if (HAL_RTC_Init(&hrtc) != HAL_OK)
      {
        Error_Handler();
      }
    
      /* USER CODE BEGIN Check_RTC_BKUP */
    
      /* USER CODE END Check_RTC_BKUP */
    
      /** Initialize RTC and set the Time and Date
      */
      sTime.Hours = 12;
      sTime.Minutes = 30;
      sTime.Seconds = 0;
      sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
      sTime.StoreOperation = RTC_STOREOPERATION_RESET;
      if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)
      {
        Error_Handler();
      }
      sDate.WeekDay = RTC_WEEKDAY_MONDAY;
      sDate.Month = RTC_MONTH_NOVEMBER;
      sDate.Date = 10;
      sDate.Year = 25;
    
      if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK)
      {
        Error_Handler();
      }
      /* USER CODE BEGIN RTC_Init 2 */
    
      /* USER CODE END RTC_Init 2 */
    
    }
    
    /**
      * @brief USART2 Initialization Function
      * @param None
      * @retval None
      */
    static void MX_USART2_UART_Init(void)
    {
    
      /* USER CODE BEGIN USART2_Init 0 */
    
      /* USER CODE END USART2_Init 0 */
    
      /* USER CODE BEGIN USART2_Init 1 */
    
      /* USER CODE END USART2_Init 1 */
      huart2.Instance = USART2;
      huart2.Init.BaudRate = 115200;
      huart2.Init.WordLength = UART_WORDLENGTH_8B;
      huart2.Init.StopBits = UART_STOPBITS_1;
      huart2.Init.Parity = UART_PARITY_NONE;
      huart2.Init.Mode = UART_MODE_TX_RX;
      huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
      huart2.Init.OverSampling = UART_OVERSAMPLING_16;
      if (HAL_UART_Init(&huart2) != HAL_OK)
      {
        Error_Handler();
      }
      /* USER CODE BEGIN USART2_Init 2 */
    
      /* USER CODE END USART2_Init 2 */
    
    }
    
    /**
      * @brief GPIO Initialization Function
      * @param None
      * @retval None
      */
    static void MX_GPIO_Init(void)
    {
      /* USER CODE BEGIN MX_GPIO_Init_1 */
    
      /* USER CODE END MX_GPIO_Init_1 */
    
      /* GPIO Ports Clock Enable */
      __HAL_RCC_GPIOA_CLK_ENABLE();
    
      /* USER CODE BEGIN MX_GPIO_Init_2 */
    
      /* USER CODE END MX_GPIO_Init_2 */
    }
    
    /* USER CODE BEGIN 4 */
    
    /* USER CODE END 4 */
    
    /**
      * @brief  This function is executed in case of error occurrence.
      * @retval None
      */
    void Error_Handler(void)
    {
      /* USER CODE BEGIN Error_Handler_Debug */
      /* User can add his own implementation to report the HAL error return state */
      __disable_irq();
      while (1)
      {
      }
      /* USER CODE END Error_Handler_Debug */
    }
    #ifdef USE_FULL_ASSERT
    /**
      * @brief  Reports the name of the source file and the source line number
      *         where the assert_param error has occurred.
      * @param  file: pointer to the source file name
      * @param  line: assert_param error line source number
      * @retval None
      */
    void assert_failed(uint8_t *file, uint32_t line)
    {
      /* USER CODE BEGIN 6 */
      /* User can add his own implementation to report the file name and line number,
         ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
      /* USER CODE END 6 */
    }
    #endif /* USE_FULL_ASSERT */
    Code Source

    CircuitGatorHQ Github Post

    Wiring

    The wiring was pretty simple to do. I just need to supply power to the MCU and connect to the UART pins. I also added a small CR2032 battery chip so that it can be unplugged and still powered to keep time.

    Conclusion

    I had a lot of fun getting this figured out. It helped teach me the basics of how the code works for STM32. It also showed me how to deal with RTC and getting the information from binary and then send that information to a buffer with snprintf. This project is also going to be used as the base for a future project where I use the Blackpill to display time and date with i2c to an LCD.

  • Selfhosted GPS NTP – Spectracom Netclock 9483

    Selfhosted GPS NTP – Spectracom Netclock 9483

    Introduction

    While working in one of the datacenters at my organization I come across many interesting pieces of enterprise hardware. Most of which I couldn’t dream of running at home due to noise and of course, I don’t own a power station. But one server in particular that caught my interest was a Netclock 9483 managed by one of our vendors. Immediately it drew my attention. I knew that one day I had to have one of my own. Their is no practical reason for this other that I genuinely like the look that it gives my rack. Plus what who doesn’t want to brag about having your own private sub ~100ns time source.

    Acquiring Hardware

    Sadly, we wont be decommissioning our Netclock anytime soon, so I had to find the next best source, eBay! I kept my eye out for a few weeks, my goal was to get one is good operable condition with a OCXO for around $250USD or less. At the time of searching most fully functional 9483s were listed for $800USD+. I didn’t want to spend this much so I kept researching and came across a listing for $400, only issue was the LED display was noted as faulty, otherwise it would boot and was in physically good condition. Because of the issues I sent a lowball offer of $200. The seller immediately accepted. In hindsight it makes me wish I sent an even lower offer but alas I did not. The seller was Canadian and the shipping went smooth. FedEx hung up the tracking for a while due to weather but eventually it arrived with no damage.

    Internal picture after delivery while looking over the hardware

    First Boot!

    Now came the time for first boot. Before attempting to boot I did a look over the hardware to check for any obvious issues or missing components. Everything looked pretty normal to me although I noticed a suspicious Compact Flash slot that would later come to haunt me. Once satisfied I plugged it in, flipped the switch and crossed my fingers. Immediately the fan bearing started grinding, thankfully not a big deal. After about 5s the front LED panel powered up. The display was blank but the backlight turned on. Then the time display on the side powered up and began to count up. At this point I was hopeful that the device was “on” and that the LED display was blank like the eBay listing said.

    Troubleshooting

    Once I booted it up I let the clock idle for a few minutes and nothing changed. I figured that since the screen indeed was blank I should move onto accessing the web GUI. Needless to say that came with its own issues. I plugged it into my 3560X switch and waited a bit for the activity lights to go green. Once green I connected to my Omada GUI and checked DHCP for what reservation it picked up. This turned up nothing, I couldn’t find the IP. I tried checked the IP table on the switch to no avail as well. At this point I became very skeptical of the CF slot being empty but figured I would give it one more go to get the IP. I plugged it into my spare laptop while running a PCAP to see if I could find a ARP advertisement to get the IP…nothing, time to start Googling!

    This is where I ran across a old forum post from someone in a similar situation in 2021. (Located here) They were having similar issues although they bricked the BIOS. Not sure if they were ever able to get it fixed but the information they shared was enough to convince me the CF slot indeed was where a OS would be installed. This makes sense in hindsight since these devices are marketed to law enforcement. They must have removed it before recycling the device. Now I had to source a useable image to flash. Luckily the stars aligned and someone posted multiple different images for a 9483 on archive.org! With this .img I was able to use rufus and flash the image easily.

    Now for the moment of truth…Its alive!

    Picture of first boot

    I was ecstatic, the LED display was functional! The eBay listing was incorrect and the screen was functional as you can see above. The only fault is the lower dot on the colon for the 9 digit display but I will save that for a future project. Once plugged in it pulled an IP and displayed it on the front panel. With this I was able to access the GUI and login with the default credentials. Whew after some of the other reading I was doing I was worried that I was going to finally get the .img to work just to get smacked with a license prompt and be out of luck.

    GPS Configuration

    Now that I was able to get in the GUI and everything looked to be functional it was finally time to get it setup and running. This overall was pretty simple. My goal is to setup the antenna and successfully pull GPS time and have NTP backup references if needed. For my antenna I didn’t get anything fancy, I went with a Bingfu SMA antenna from Amazon and a type N to SMA adapter. I’m keeping an eye out for a Spectracom 8230 to popup on eBay to complete the ‘official’ setup but for now a Bingfu works.

    Parts List:

    Once installed the Netclock automatically acknowledged the antenna and began a satellite survey. This took a few tries to get right, but once I got the antenna placed in a good spot where it would get signal it took about 3hours. After the survey it automatically pulled time and started ticking. The time was a bit off in the beginning, but after adjusting a few settings I got it to keep consistent time. Currently it is able to keep an estimated time error(ETE) of 10ns < ETE <= 100ns. I can now say that I maintain my own stratum 1 time server, time to check that off the bucket list.

    GPS Disciplined Desktop Clock?

    I hope in the future to use this for more than just a rack decoration and simple time keeping. Currently I only have 2 clients referencing it. (My Desktop and my home server) Since it has a 1PPS output I want to try and build a clock that will run using that signal to maintain time. So instead of running a 555 clock circuit or something similar it will work off the raw PPS signal for when to increment the time. Is this possible, I have no idea but it sounds like a fun project!

    Hope you found this interesting, thanks for reading!