cancel
Showing results for 
Search instead for 
Did you mean: 

How to use SNTP over LwIP and RTC with an STM32

B.Montanari
ST Employee

Summary

This article demonstrates how to implement an SNTP client using the LwIP stack in a callback-based application. The demonstration code is built using the NUCLEO-H723ZG development board but can be easily tailored to a different STM32. All implementation is done using the STM32CubeIDE v1.16.0.

Introduction

SNTP (Simple Network Time Protocol) is a subset of the Network Time Protocol and it’s used to synchronize the time between client and server, by exchanging packets regularly. As opposed to NTP (Network Time Protocol), SNTP is built from the ground up to work with memory and processing-constrained devices such as small computers and microcontrollers. It’s used when extreme time precision isn’t necessary.

After a client request on UDP port 123, the server will send down a packet containing the timestamp which translates to the precise time and date. The application is based on a TCP server application, using DHCP to provide an IP address to the board and UDP to communicate with the SNTP server. 

1. Development

1.1. Project configuration

To start creating the application, we first need to create a project in STM32CubeIDE using the NUCLEO-H723ZG board as the starting point. After setting a project name, click [Yes] to the pop-up message about starting the peripherals in default mode.

1.2. System Core Tab configuration

We must configure the memory protection unit on the CORTEX_M7 to prevent the speculation from interfering with the code execution. To accomplish that, enable both CPU caches, speculation default mode and set the MPU control mode as background region privileged access only + MPU disabled during hard fault, NMI, and FAUTMASK handlers.

MPU regions must be configured to specify the access to both Ethernet descriptors, such as in the picture below. More details about the MPU and Ethernet descriptors for the M7 core can be found here: Knowledge article: How to create a project for STM32H7 with Ethernet and LwIP stack working

 

BMontanari_0-1730159840046.png

1.3. Timer tab setup

In the Timer tab, we’ll set up the RTC (real-time clock) peripheral. We can store the translated timestamp into human readable values into the embedded calendar of the MCU. Given this is just a simple demo, it will proceed and use the fairly inaccurate LSI. However, for real applications we recommend configuring the LSE as the clock source for the RTC to achieve better precision.

 

BMontanari_1-1730159840048.png

1.4. Connectivity tab setup

In the Connectivity tab, we’ll have both the Ethernet and USART3 peripherals that are used. Ethernet will be the focus of this application, and the serial port will be used for debug purposes, such as application progress, error, and IP address handling.

RMII Ethernet Mode must be enabled, configure the Rx buffers to 1000 and the first descriptor addresses must be set as such:

BMontanari_2-1730159840055.png

Still, in the Ethernet tab, navigate to the [NVIC Settings] tab and check the [Ethernet global interrupt] enable box.

BMontanari_3-1730159840056.png

The USART3 peripheral pins are mapped in the pinout view as STLINK_VCP_TX and STLINK_VCP_RX, as they are configured as Virtual COM Port. Configure the peripheral in Asynchronous Mode and with the desired parameters: Baud rate, Word Length, Parity and Stop Bits. The default settings are:

  • Baud rate: 115200 Bits/s
  • Word Length: 8 Bits (including Parity)
  • Parity: None
  • Stop Bits: 1
BMontanari_4-1730159840057.png

 

1.5. LwIP middleware configuration

In the middleware tabs, the first step is to [Enable] the LwIP. Only then it's possible to move to the [Platform Settings] tab and proceed with the configuration, as shown in the image below:

BMontanari_5-1730159840060.png

Next, the [SNTP/SMTP] tab is accessed to configure the [Sanity Check] and [Round-Trip Delay Compensation]. Reduce the [SNTP Update Delay] for demonstration purposes only.

 

BMontanari_6-1730159840068.png

1.6. Clock configuration

To achieve the best possible performance, the M7 core must run at the highest frequency allowed. To do so, type 550 MHz into the [Clock Configuration] field as shown in the image below and press enter. This allows the auto clock solution to find the best PLL configurations to reach the necessary frequency.

 

BMontanari_7-1730159840069.png

 

Once all these configurations are complete, you can save the project and have the code snippet generated using the [Alt + K] shortcut.

2. Coding the project

Now that the project has all of its peripheral and middleware foundation configured, we’re ready to have the application code constructed. Several files are modified in the project tree such as [main.c], [main.h], and [STM32H723ZGTX_FLASH.ld]. The modified files are referred as a section and the code snippets can be placed by locating the USER CODE XYZ.

2.1. main.c

Start by adding the external netif interface in the USER CODE PTD section.

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
extern struct netif gnetif;
/* USER CODE END PTD */

To ease the debugging and communication with the STLINK’s VCOM, the printf function will be configured. The following code must be inserted in the USER CODE BEGIN PFP section.

int __io_putchar(int ch) {
       HAL_UART_Transmit(&huart3, (uint8_t*) &ch, 1, 0xFFFF);
       return ch;
}

In the USER CODE 2 section, add a few debug messages to allow tracking the current firmware state as it executes. These debug messages are added throughout the entire application.

  /* USER CODE BEGIN 2 */
               printf("SNTP Client\r\n");
               printf("NUCLEO-H723ZG board\r\n");
               printf("State: Ethernet Initialization ...\r\n");
  /* USER CODE END 2 */

In the USER CODE 3 section, placed inside an infinite while loop, add the LwIP process function. This will make sure the LwIP stack keeps running.

    /* USER CODE BEGIN 3 */
                 MX_LWIP_Process();
  }
  /* USER CODE END 3 */

The final modification in the [main.c] file is to make sure the RED LED turns on whenever an error occurs. This can be easily done by modifying the USER CODE Error_Handler_Debug section at the end of the file.

void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
               HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, GPIO_PIN_SET);
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

2.2. main.h

In this file, static IP address configurations are added, even though the intention is to use the DHCP protocol to have the server assign an IP address to the device.

A macro is defined so the application can access the timestamp received by the SNTP protocol.

We should also increase the size of the memory available to the LwIP stack.

/* Exported constants --------------------------------------------------------*/
/* USER CODE BEGIN EC */
#define MEM_SIZE 14 * 1024
#define DEST_IP_ADDR0   ((uint8_t)10U)
#define DEST_IP_ADDR1   ((uint8_t)157U)
#define DEST_IP_ADDR2   ((uint8_t)21U)
#define DEST_IP_ADDR3   ((uint8_t)79U)
#define DEST_PORT       ((uint16_t)7U)

/*Static IP ADDRESS: IP_ADDR0.IP_ADDR1.IP_ADDR2.IP_ADDR3 */
#define IP_ADDR0   ((uint8_t) 192U)
#define IP_ADDR1   ((uint8_t) 168U)
#define IP_ADDR2   ((uint8_t) 0U)
#define IP_ADDR3   ((uint8_t) 192U)

/*NETMASK*/
#define NETMASK_ADDR0   ((uint8_t) 255U)
#define NETMASK_ADDR1   ((uint8_t) 255U)
#define NETMASK_ADDR2   ((uint8_t) 255U)
#define NETMASK_ADDR3   ((uint8_t) 0U)

/*Gateway Address*/
#define GW_ADDR0   ((uint8_t) 192U)
#define GW_ADDR1   ((uint8_t) 168U)
#define GW_ADDR2   ((uint8_t) 0U)
#define GW_ADDR3   ((uint8_t) 1U)
/* USER CODE END EC */
/* Exported macro ------------------------------------------------------------*/
/* USER CODE BEGIN EM */
#define SNTP_SET_SYSTEM_TIME(sec) sntp_get_time(sec);
/* USER CODE END EM */

 

2.3. lwip.c

Here’s where most of the application code is located. Don’t worry, if you don’t want to go over the entire code, locate the [lwip.c] attached as an external file to the bottom of this article.

In the USER CODE 0 section, a few necessary libraries are included.

/* USER CODE BEGIN 0 */
#include "lwip/apps/sntp.h"
#include "time.h"
#include <string.h>
/* USER CODE END 0 */

In the USER CODE 1 section, we add the DHCP code and variables required by the SNTP application

/* USER CODE BEGIN 1 */
#if LWIP_DHCP
/* DHCP process states */
#define DHCP_OFF                   (uint8_t) 0
#define DHCP_START                 (uint8_t) 1
#define DHCP_WAIT_ADDRESS          (uint8_t) 2
#define DHCP_ADDRESS_ASSIGNED      (uint8_t) 3
#define DHCP_TIMEOUT               (uint8_t) 4
#define DHCP_LINK_DOWN             (uint8_t) 5
#define MAX_DHCP_TRIES  4
uint8_t DHCP_state = DHCP_OFF;
#endif

ip4_addr_t sntp_ip;                                                                        // SNTP server ip

struct tm timeinfo = {0};                                                        // Struct holding the human format time
extern RTC_HandleTypeDef hrtc;                                        // External RTC handler
time_t unix = 0;                                                                             // Unix time received from the sntp stack

/* USER CODE END 1 */

In the USER CODE 2 section, is where the application takes form. Start by creating the macro expression that will retrieve the data the SNTP protocol provides and convert it into human time

/* function called to receive the unix time from the sntp stack */
void sntp_get_time (uint32_t sec) {
          unix = sec;
          gmtime_r(&unix, &timeinfo); // Converts unix time to human format. Unix must be global
}

Follow-up with the SNTP process, created to initialize the protocol and print out the received information every 15 seconds. This is the rate at which the SNTP protocol will ask the server for an updated time stamp. 

void sntp_process (void) {
            RTC_TimeTypeDef sTime = {0};
            RTC_DateTypeDef sDate = {0};
          if (sntp_enabled() != 1 && DHCP_state == DHCP_ADDRESS_ASSIGNED){
                    printf("Initializing SNTP\n\r");
          ipaddr_aton("0xC8A007BA", &sntp_ip);            // Set SNTP server IP
          sntp_setoperatingmode(SNTP_OPMODE_POLL);      // Set SNTP operation mode to Polling
          sntp_setserver(0, &sntp_ip);                                      // Set server 0 as the supplied ip address
        sntp_init();                                                                          // Start SNTP process
          }
          if (unix != 0){
                    printf("SNTP value received from server\n\r");
                      sTime.Hours = timeinfo.tm_hour - 3;
                      sTime.Minutes = timeinfo.tm_min;
                      sTime.Seconds = timeinfo.tm_sec;
                      sTime.DayLightSaving = timeinfo.tm_isdst;
                      sTime.StoreOperation = RTC_STOREOPERATION_RESET;
                      if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)
                      {
                        Error_Handler();
                      }
                      sDate.WeekDay = timeinfo.tm_wday;
                      sDate.Month = timeinfo.tm_mon + 1;
                      sDate.Date = timeinfo.tm_mday;
                      sDate.Year = timeinfo.tm_year - 100;
                      if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK)
                      {
                        Error_Handler();
                      }
                    // Get's the time and dat from RTC
                    HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
                    HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
                    printf("SNTP process complete\r\n");
                    // Prints the RTC values
                    printf("Weekday %d. Day %d of month %d year 20%d\r\n", sDate.WeekDay, sDate.Date, sDate.Month, sDate.Year);
                    printf("%02d:%02d:%02d\r\n",sTime.Hours, sTime.Minutes,sTime.Seconds);
                    unix = 0; // Waits for next reception to reset
                    }
}

The next portion of code is related to the DHCP protocol processing. This is default to all LwIP applications using DHCP.

#if LWIP_DHCP
/**
 * @brief  DHCP_Process_Handle
 *   netif: the network interface
 * @retval None
 */
void DHCP_Process(struct netif *netif) {
          ip_addr_t ipaddr;
          ip_addr_t netmask;
          ip_addr_t gw;
          struct dhcp *dhcp;
          uint8_t iptxt[20];
          switch (DHCP_state) {
          case DHCP_START : {
                    printf("State: Looking for DHCP server ...\n");
                    HAL_GPIO_WritePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin, GPIO_PIN_RESET);
                    HAL_GPIO_WritePin(LED_YELLOW_GPIO_Port, LED_YELLOW_Pin, GPIO_PIN_RESET);
                    ip_addr_set_zero_ip4(&netif->ip_addr);
                    ip_addr_set_zero_ip4(&netif->netmask);
                    ip_addr_set_zero_ip4(&netif->gw);
                    dhcp_start(netif);
                    DHCP_state = DHCP_WAIT_ADDRESS;
          }
                    break;

          case DHCP_WAIT_ADDRESS : {
                    if (dhcp_supplied_address(netif)) {
                              DHCP_state = DHCP_ADDRESS_ASSIGNED;
                              sprintf((char*) iptxt, "%s", ip4addr_ntoa(netif_ip4_addr(netif)));
                              printf("IP address assigned by a DHCP server: %s\n", iptxt);
                              HAL_GPIO_WritePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin, GPIO_PIN_SET);
                              HAL_GPIO_WritePin(LED_YELLOW_GPIO_Port, LED_YELLOW_Pin,
                                                  GPIO_PIN_RESET);
                    } else {
                              dhcp = (struct dhcp*) netif_get_client_data(netif,
                                                  LWIP_NETIF_CLIENT_DATA_INDEX_DHCP);

                              /* DHCP timeout */
                              if (dhcp->tries > MAX_DHCP_TRIES) {
                                        DHCP_state = DHCP_TIMEOUT;

                                        /* Static address used */
                                        IP_ADDR4(&ipaddr, IP_ADDR0, IP_ADDR1, IP_ADDR2, IP_ADDR3);
                                        IP_ADDR4(&netmask, NETMASK_ADDR0, NETMASK_ADDR1, NETMASK_ADDR2, NETMASK_ADDR3);
                                        IP_ADDR4(&gw, GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3);
                                        netif_set_addr(netif, &ipaddr, &netmask, &gw);
                                        sprintf((char*) iptxt, "%s", ip4addr_ntoa(netif_ip4_addr(netif)));
                                        printf("DHCP Timeout !! \n");
                                        printf("Static IP address: %s\n", iptxt);
                                        HAL_GPIO_WritePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin,GPIO_PIN_SET);
                                        HAL_GPIO_WritePin(LED_YELLOW_GPIO_Port, LED_YELLOW_Pin,GPIO_PIN_RESET);
                              }
                    }
          }
                    break;
          case DHCP_LINK_DOWN : {
                    DHCP_state = DHCP_OFF;
                    HAL_GPIO_WritePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin, GPIO_PIN_RESET);
                    HAL_GPIO_WritePin(LED_YELLOW_GPIO_Port, LED_YELLOW_Pin, GPIO_PIN_SET);
          }
                    break;
          default:
                    break;
          }
}
/**
 * @brief  DHCP periodic check
 *   netif: the network interface
 * @retval None
 */
void DHCP_Periodic_Handle(struct netif *netif) {
          /* Fine DHCP periodic process every 500ms */
          if (HAL_GetTick() - DHCPfineTimer >= DHCP_FINE_TIMER_MSECS) {
                    DHCPfineTimer = HAL_GetTick();
                    /* process DHCP state machine */
                    DHCP_Process(netif);
          }
}

#endif

With USER CODE 2 section done, the next steps are to ensure that the DHCP protocol works correctly all the way through, including cable disconnections. In USER CODE 3 section add the following:

/* USER CODE BEGIN 3 */
          dhcp_stop(&gnetif);
          ethernet_link_status_updated(&gnetif);
/* USER CODE END 3 */

In USER CODE 4_3 section, add our periodic handles so they are called indefinitely by the LwIP process.

/* USER CODE BEGIN 4_3 */
#if LWIP_DHCP
          DHCP_Periodic_Handle(&gnetif);
#endif
          sntp_process();
/* USER CODE END 4_3 */

USER CODE 5 and 6 sections are modified to accommodate for cable disconnections as follows:

/* USER CODE BEGIN 5 */
#if LWIP_DHCP
                    /* Update DHCP state machine */
                    DHCP_state = DHCP_START;
#endif /* LWIP_DHCP */
/* USER CODE END 5 */
  }
  else /* netif is down */
  {
/* USER CODE BEGIN 6 */
#if LWIP_DHCP
                    /* Update DHCP state machine */
                    DHCP_state = DHCP_LINK_DOWN;
#endif /* LWIP_DHCP */
/* USER CODE END 6 */
  }

2.4. STM32H723ZGTX_FLASH.ld

The last file to be modified is the flash linker descriptor. Being responsible for creating a few memory locations to hold both descriptors and memory pool sections, the following code are pasted in line 168 (or around to this location):

  .lwip_sec (NOLOAD) : {
    . = ABSOLUTE(0x30000000);
    *(.RxDecripSection)
    . = ABSOLUTE(0x30000080);
    *(.TxDecripSection)
    . = ABSOLUTE(0x30000100);
    *(.Rx_PoolSection)
  } >RAM_D2 AT> FLASH

With the linker descriptor modification, all coding is complete. The code is compiled using the [CTRL + B] shortcut. It may take a few minutes to complete building, but it should finish with 0 errors.

3. Deployment

3.1. Code download

In STM32CubeIDE, left click the debug button or press the [F11] shortcut.

BMontanari_8-1730159840070.png

A pop-up will appear to configure the debug session, as the default settings are recommended simply press [OK] on the bottom right corner.

The perspective should be switched to debug and the code is ready to run.

3.2. Code run and web page opening

A terminal interface is required for IP address checking and code flow, as well as specific debug. You can either use your preferred terminal software or set up the built-in terminal in STM32CubeIDE. For more details on how to configure that, you can refer to this quick knowledge article:

Make sure to connect the board using an RJ45 cable to a router or switch then allow the code to run by pressing the resume button or the [F8] shortcut.

BMontanari_9-1730159840071.png

On the terminal, the following messages should display if all steps are done correctly, the IP address varying based on your network’s addressing and architecture.

 

BMontanari_10-1730159840071.png

This message output updates every 15 seconds with a new timestamp being received from the server. In the meantime, the value is saved into the internal RTC. It can be used by the application in a precise way, especially if the RTC was configured with the LSE. Given that a new update is received every 15 seconds, it can be used to adjust the time if a larger deviation is observed.

To reduce CPU load, it’s recommended to increase the SNTP delay to a full hour or more. This depends on the precision that you want to achieve with the application and the clock source used for the RTC.

Conclusion

With the content presented here, you should be able to setup both the underlaying SNTP application for more complex use cases. This includes calendar logging using the RTC peripheral and the LwIP middleware on the STM32H7.

We hope you enjoyed this article!

Related links

Here are some useful links that can help you in your developments using our ST peripherals:

Version history
Last update:
‎2024-10-29 07:43 AM
Updated by: