on
2024-10-29
07:30 AM
- edited on
2024-10-29
07:43 AM
by
Laurids_PETERSE
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.
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.
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.
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
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.
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:
Still, in the Ethernet tab, navigate to the [NVIC Settings] tab and check the [Ethernet global interrupt] enable box.
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:
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:
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.
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.
Once all these configurations are complete, you can save the project and have the code snippet generated using the [Alt + K] shortcut.
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.
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 */
}
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 */
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 */
}
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.
In STM32CubeIDE, left click the debug button or press the [F11] shortcut.
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.
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.
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.
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.
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!
Here are some useful links that can help you in your developments using our ST peripherals: