ESP32 SNTP Module using ESP-IDF – Synchronize Time with NTP

In this ESP32 tutorial, we will learn to use LwIP SNTP module and time functions to get time from internet servers with ESP-IDF . SNTP also known as “Simple Network Time Protocol” is a protocol used to get the current time and date. LwIP SNTP library available in esp-idf is helpful to obtain the current time and synchronize ESP32 timer with an NTP server. This is quite helpful in data logger applications where it is useful to log data values along with correct timestamps after a specific time interval.

Before we move ahead, make sure you have the latest version of VS Code installed on your system with the ESP-IDF extension configured.

ESP32 SNTP ESP-IDF APIs

Now let us discuss the LwIP SNTP library APIs provided by ESP-IDF to obtain the current time.

This is the header file that is required to include the library:

#include "esp_sntp.h"

gettimeofday function is commonly used to access the current timestamp. Likewise, settimeofday function is used to set the time. This is the function that is used by the LwIP library to set current time when response from NTP server is received.

However, there are several more functions provided by the standard C library related to time. Some of them are mentioned below:

gettimeofday
time
asctime
clock
ctime
difftime
gmtime
localtime
mktime
strftime

The sntp_get_sync_status() is used to obtain the status of the time. If time is synchronized then it returns the response SNTP_SYNC_RETURN_COMPLETED. If the status is reset, then it returns SNTP_SYC_STATUS_RESET. Moreover, if smooth time synchronization is occurring, then it returns the response SNTP_SYNC_STATUS_IN_PROGRESS.

sntp_sync_status_t sntp_get_sync_status(void)

The sntp_set_sync_status() is used to set the status of time synchronization. It takes in a single parameter which is ‘sync_status’ the status of time synchronization.

void sntp_set_sync_status(sntp_sync_status_t sync_status)

Next we have the function, sntp_get_sync_mode(). It is used to get the time synchronization mode. This function returns the response, SNTP_SYNC_MODE_IMMED, if synchronization is set to update time immediately. Otherwise, it returns the response, SNTP_SYNC_MODE_SMOOTH, if smooth time updating is set.

sntp_sync_mode_t sntp_get_sync_mode(void)

The sntp_set_sync_mode() function is used to set the time synchronization mode. It takes in a single parameter ‘sync_mode’ which is the synchronization mode. The mode can either be SNTP_SYNC_MODE_IMMED or SNTP_SYNC_MODE_SMOOTH. When the selected mode is SNTP_SYNC_MODE_IMMED, the system time is updated right away when it is received from the SNTP server. When the selected mode is SNTP_SYNC_MODE_SMOOTH, smooth time updating is being set. The adjtime function is used to notify the time in this case.

void sntp_set_sync_mode(sntp_sync_mode_t sync_mode)

The sntp_set_time_sync_notification_cb() is used to set the callback function for time synchronization notification. It takes in a single parameter which is the callback function.

void sntp_set_time_sync_notification_cb(sntp_sync_time_cb_t callback)

ESP-IDF SNTP Example

In this section, we will build and test a an example using the LwIP SNTP APIs as described in the previously. We will use the SNTP example provided by ESP-IDF. Through this example we will show you how to obtain current time using SNTP.

Create Example Project

Open your VS Code and head over to View > Command Palette. Type ESP-IDF: New Project in the search bar and press enter.

Specify the project name and directory. We have named our project ‘ESP_IDF_SNTP.’ For the ESP-IDF board, we have chosen the custom board option. For ESP-IDF target, we have chosen ESP32 module. Click ‘Choose Template’ button to proceed forward.

ESP32 SNTP Example using ESP-IDF 1

In the Extension, select ESP-IDF option:

ESP-IDF in VS Code New Project 2

We will click the ‘sntp’ under the Protocols tab. Now click ‘Create project using template sntp.’

ESP32 SNTP Example using ESP-IDF 2

You will get a notification that the project has been created. To open the project in a new window, click ‘Yes.’

This opens our ESP_IDF_SNTP project that we created, inside the EXPLORER tab. There are several folders inside our project folder. This is the same for every project which you will create through ESP-IDF Explorer.

ESP-IDF SDK Configuration Editor

Lets first head over to the menuconfig. Click the icon shown below. It opens the ESP-IDF SDK Configuration Editor.

ESP32 SNTP Example using ESP-IDF menuconfig

Scroll down and open the Example Connection Configuration. Here we have to specify our Wi-Fi SSID and password. This is because in the sntp example, when the ESP32 first boots, it connects with the internet and uses SNTP to obtain the current time. We will specify the connection parameters. You also have an option of connecting using an Ethernet interface.

Next, scroll down to Example Configuration, and select the time synchronization method. This example provides three methods of time synchronization.

  1. Update Time immediately when received: This is the default method that is selected. In this case, the time is straight away updated when it is received from the SNTP server.
  2. Update Time with smooth method (adjtime): In this method, the adjtime function is used to notify the time.
  3. Custom Implementation: In this method, the in-built time synchronization is not used.

We are using the ‘Update Time immediately when received’ option.

ESP32 SNTP Example using ESP-IDF Project Configuration 1

Next, head over to ESP32-specific. Here make sure “RTC and high-resolution timer” or “RTC” option is selected for “Timers used for gettimeofday function”. This will ensure that after synchronization with the SNTP server, timekeeping is performed by these timers. When the ESP32 board is in deep sleep mode, the RTC built-in timer will be used to keep track of the time. Likewise, the high resolution timer FRC1 will ensure time accurate to microseconds while the board is in active mode.

ESP32 SNTP Example using ESP-IDF Project Configuration 2

ESP-IDF SNTP Example Code

Lets head over to the main.c file. The main folder contains the source code meaning the main.c file will be found here.

Now go to main > sntp_example_main.c and open it.

The following code opens up which is shown below. This example code will demonstrate the ESP32 board synchronizing with an SNTP server to obtain the current time. After the initial boot, the ESP32 board connects with the Wi-Fi, synchronizes with the SNTP server and acquires the current time. Then the ESP32 board goes into deep sleep mode. During this time, the RTC timer keeps tracks of the current time. After waking up, the ESP32 prints the current time without connecting to the Wi-Fi.

/* LwIP SNTP example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_attr.h"
#include "esp_sleep.h"
#include "nvs_flash.h"
#include "protocol_examples_common.h"
#include "esp_sntp.h"

static const char *TAG = "example";

/* Variable holding number of times ESP32 restarted since first boot.
 * It is placed into RTC memory using RTC_DATA_ATTR and
 * maintains its value when ESP32 wakes from deep sleep.
 */
RTC_DATA_ATTR static int boot_count = 0;

static void obtain_time(void);
static void initialize_sntp(void);

#ifdef CONFIG_SNTP_TIME_SYNC_METHOD_CUSTOM
void sntp_sync_time(struct timeval *tv)
{
   settimeofday(tv, NULL);
   ESP_LOGI(TAG, "Time is synchronized from custom code");
   sntp_set_sync_status(SNTP_SYNC_STATUS_COMPLETED);
}
#endif

void time_sync_notification_cb(struct timeval *tv)
{
    ESP_LOGI(TAG, "Notification of a time synchronization event");
}

void app_main(void)
{
    ++boot_count;
    ESP_LOGI(TAG, "Boot count: %d", boot_count);

    time_t now;
    struct tm timeinfo;
    time(&now);
    localtime_r(&now, &timeinfo);
    // Is time set? If not, tm_year will be (1970 - 1900).
    if (timeinfo.tm_year < (2016 - 1900)) {
        ESP_LOGI(TAG, "Time is not set yet. Connecting to WiFi and getting time over NTP.");
        obtain_time();
        // update 'now' variable with current time
        time(&now);
    }
#ifdef CONFIG_SNTP_TIME_SYNC_METHOD_SMOOTH
    else {
        // add 500 ms error to the current system time.
        // Only to demonstrate a work of adjusting method!
        {
            ESP_LOGI(TAG, "Add a error for test adjtime");
            struct timeval tv_now;
            gettimeofday(&tv_now, NULL);
            int64_t cpu_time = (int64_t)tv_now.tv_sec * 1000000L + (int64_t)tv_now.tv_usec;
            int64_t error_time = cpu_time + 500 * 1000L;
            struct timeval tv_error = { .tv_sec = error_time / 1000000L, .tv_usec = error_time % 1000000L };
            settimeofday(&tv_error, NULL);
        }

        ESP_LOGI(TAG, "Time was set, now just adjusting it. Use SMOOTH SYNC method.");
        obtain_time();
        // update 'now' variable with current time
        time(&now);
    }
#endif

    char strftime_buf[64];

    // Set timezone to Eastern Standard Time and print local time
    setenv("TZ", "EST5EDT,M3.2.0/2,M11.1.0", 1);
    tzset();
    localtime_r(&now, &timeinfo);
    strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
    ESP_LOGI(TAG, "The current date/time in New York is: %s", strftime_buf);

    // Set timezone to China Standard Time
    setenv("TZ", "CST-8", 1);
    tzset();
    localtime_r(&now, &timeinfo);
    strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
    ESP_LOGI(TAG, "The current date/time in Shanghai is: %s", strftime_buf);

    if (sntp_get_sync_mode() == SNTP_SYNC_MODE_SMOOTH) {
        struct timeval outdelta;
        while (sntp_get_sync_status() == SNTP_SYNC_STATUS_IN_PROGRESS) {
            adjtime(NULL, &outdelta);
            ESP_LOGI(TAG, "Waiting for adjusting time ... outdelta = %li sec: %li ms: %li us",
                        (long)outdelta.tv_sec,
                        outdelta.tv_usec/1000,
                        outdelta.tv_usec%1000);
            vTaskDelay(2000 / portTICK_PERIOD_MS);
        }
    }

    const int deep_sleep_sec = 10;
    ESP_LOGI(TAG, "Entering deep sleep for %d seconds", deep_sleep_sec);
    esp_deep_sleep(1000000LL * deep_sleep_sec);
}

static void obtain_time(void)
{
    ESP_ERROR_CHECK( nvs_flash_init() );
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK( esp_event_loop_create_default() );

    /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
     * Read "Establishing Wi-Fi or Ethernet Connection" section in
     * examples/protocols/README.md for more information about this function.
     */
    ESP_ERROR_CHECK(example_connect());

    initialize_sntp();

    // wait for time to be set
    time_t now = 0;
    struct tm timeinfo = { 0 };
    int retry = 0;
    const int retry_count = 10;
    while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET && ++retry < retry_count) {
        ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
        vTaskDelay(2000 / portTICK_PERIOD_MS);
    }
    time(&now);
    localtime_r(&now, &timeinfo);

    ESP_ERROR_CHECK( example_disconnect() );
}

static void initialize_sntp(void)
{
    ESP_LOGI(TAG, "Initializing SNTP");
    sntp_setoperatingmode(SNTP_OPMODE_POLL);
    sntp_setservername(0, "pool.ntp.org");
    sntp_set_time_sync_notification_cb(time_sync_notification_cb);
#ifdef CONFIG_SNTP_TIME_SYNC_METHOD_SMOOTH
    sntp_set_sync_mode(SNTP_SYNC_MODE_SMOOTH);
#endif
    sntp_init();
}

How the Code Works?

Firstly, we will start by including the necessary libraries that includes the FreeRTOS libraries to generate delays and create tasks, sys/time.h and esp_sntp.h for accessing the time, esp_log.h to debug and print informational logs, esp_sleep.h to put the ESP32 board into deep sleep etc.

#include <string.h>
#include <time.h>
#include <sys/time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_attr.h"
#include "esp_sleep.h"
#include "nvs_flash.h"
#include "protocol_examples_common.h"
#include "esp_sntp.h"

This code uses Informational Logging. The log function takes in two arguments. The first argument is the tag and the second argument is a formatted string. Therefore this global variable will be useful while calling the ESP_LOGI() functions. Here, “example” is the tag that will be used while logging.

static const char *TAG = "example";

Next, there is a variable boot_count that holds the number of times the ESP32 rebooted. RTC_DATA_ATTR is used to store this static int variable in the RTC memory. This will ensure that the boot count value holds while the ESP32 board goes to deep sleep and then wakes up.

RTC_DATA_ATTR static int boot_count = 0;

If custom time sync configuration method was selected by the user, then the following code will run. Here the sntp_sync_time() function is being called. The sntp_sync_time() is used to update the system time. It takes in a single parameter ‘tv’ which is the time received from the SNTP server. Inside this function, settimeofday() function is called. This function is from the sys/time.h header file. It takes in two parameters. The first parameter is the timeval datatype which is ‘tv’ in this case. The second parameter is the time zone datatype which is NULL in this case. At this point the terminal prints, “Time is synchronized from custom code.” Finally, the sntp_set_sync_status() is used to set the status of time synchronization as SNTP_SYNC_STATUS_COMPLETED.

#ifdef CONFIG_SNTP_TIME_SYNC_METHOD_CUSTOM
void sntp_sync_time(struct timeval *tv)
{
   settimeofday(tv, NULL);
   ESP_LOGI(TAG, "Time is synchronized from custom code");
   sntp_set_sync_status(SNTP_SYNC_STATUS_COMPLETED);
}
#endif

The time_sync_notification_cb() prints the message “Notification of a time synchronization event.”

void time_sync_notification_cb(struct timeval *tv)
{
    ESP_LOGI(TAG, "Notification of a time synchronization event");
}

Inside the app_main() function, firstly boot count is incremented after each reset. The boot count is printed in the terminal.

++boot_count;
ESP_LOGI(TAG, "Boot count: %d", boot_count);

Then, a time structure named timeinfo is created. This holds the information relating to the time e.g. the number of hours/minutes/seconds. The function localtime_r(&now, &timeinfo), converts the time in seconds since epoch time to local time.

If a correct time is not set yet then the obtain_time() function is called which gets the time from the SNTP server. The ‘now’ variable now contains the current time.

time_t now;
    struct tm timeinfo;
    time(&now);
    localtime_r(&now, &timeinfo);


    if (timeinfo.tm_year < (2016 - 1900)) {
        ESP_LOGI(TAG, "Time is not set yet. Connecting to WiFi and getting time over NTP.");
        obtain_time();
        time(&now);
    }

For the time sync, if smooth method was selected by the user, then the following code will run. Here, 500ms error is being added to the current system time. This is to test adjtime.

#ifdef CONFIG_SNTP_TIME_SYNC_METHOD_SMOOTH
    else {
        {
            ESP_LOGI(TAG, "Add a error for test adjtime");
            struct timeval tv_now;
            gettimeofday(&tv_now, NULL);
            int64_t cpu_time = (int64_t)tv_now.tv_sec * 1000000L + (int64_t)tv_now.tv_usec;
            int64_t error_time = cpu_time + 500 * 1000L;
            struct timeval tv_error = { .tv_sec = error_time / 1000000L, .tv_usec = error_time % 1000000L };
            settimeofday(&tv_error, NULL);
        }

        ESP_LOGI(TAG, "Time was set, now just adjusting it. Use SMOOTH SYNC method.");
        obtain_time();
        // update 'now' variable with current time
        time(&now);
    }
#endif

Next, the time zone is set to Eastern Standard Time using the following lines of code. The current time in New York will get printed in the terminal.

    char strftime_buf[64];

    // Set timezone to Eastern Standard Time and print local time
    setenv("TZ", "EST5EDT,M3.2.0/2,M11.1.0", 1);
    tzset();
    localtime_r(&now, &timeinfo);
    strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
    ESP_LOGI(TAG, "The current date/time in New York is: %s", strftime_buf);

Similarly, after setting the time zone to China Standard Time, the current time in Shanghai is printed in the terminal.

// Set timezone to China Standard Time
    setenv("TZ", "CST-8", 1);
    tzset();
    localtime_r(&now, &timeinfo);
    strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
    ESP_LOGI(TAG, "The current date/time in Shanghai is: %s", strftime_buf);

If the time sync mode is SNTP_SYNC_MODE_SMOOTH which means that update with smooth method is selected, then the following lines of code run.

    if (sntp_get_sync_mode() == SNTP_SYNC_MODE_SMOOTH) {
        struct timeval outdelta;
        while (sntp_get_sync_status() == SNTP_SYNC_STATUS_IN_PROGRESS) {
            adjtime(NULL, &outdelta);
            ESP_LOGI(TAG, "Waiting for adjusting time ... outdelta = %li sec: %li ms: %li us",
                        (long)outdelta.tv_sec,
                        outdelta.tv_usec/1000,
                        outdelta.tv_usec%1000);
            vTaskDelay(2000 / portTICK_PERIOD_MS);
        }
    }

Here, while the status of the time sync is SNTP_SYNC_STATUS_IN_PROGRESS which means that the time synchronization is still running then adjtime() function is called. This function takes in two parameters as shown below:

int adjtime(const struct timeval *delta, struct timeval *outdelta)

The ESP32 board then enters deep sleep mode for 10 seconds using esp_deep_sleep() function.

    const int deep_sleep_sec = 10;
    ESP_LOGI(TAG, "Entering deep sleep for %d seconds", deep_sleep_sec);
    esp_deep_sleep(1000000LL * deep_sleep_sec);

obtain_time()

The obtain_time() function is responsible for acquiring the current time after the ESP32 synchronizes with the SNTP server.

static void obtain_time(void)
{
    ESP_ERROR_CHECK( nvs_flash_init() );
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK( esp_event_loop_create_default() );

    /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
     * Read "Establishing Wi-Fi or Ethernet Connection" section in
     * examples/protocols/README.md for more information about this function.
     */
    ESP_ERROR_CHECK(example_connect());

    initialize_sntp();

    // wait for time to be set
    time_t now = 0;
    struct tm timeinfo = { 0 };
    int retry = 0;
    const int retry_count = 10;
    while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET && ++retry < retry_count) {
        ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
        vTaskDelay(2000 / portTICK_PERIOD_MS);
    }
    time(&now);
    localtime_r(&now, &timeinfo);

    ESP_ERROR_CHECK( example_disconnect() );
}

Firstly, the default NVS partition is initialized using nvs_flash_init(). Next, lwIP is initialized using esp_netif_init(). Then, the default event loop is created using esp_event_loop_create_default().

    ESP_ERROR_CHECK( nvs_flash_init() );
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK( esp_event_loop_create_default() );

Next, the ESP32 board connects with the Wi-Fi network whose credentials were specified by the user in menuconfig. Then, we call initialize_sntp() function to initialize the SNTP server.

ESP_ERROR_CHECK(example_connect());

initialize_sntp();

Then we wait for a few moments till the system time sets up. This is checked via obtaining the SNTP sync status using sntp_get_sync_status() and checking whether the status is SNTP_SYNC_STATUS_RESET and the number of retries are still present. Afterwards, the function localtime_r(&now, &timeinfo), converts the time in seconds since epoch time to local time. Then, the ESP32 disconnects from the network.

    time_t now = 0;
    struct tm timeinfo = { 0 };
    int retry = 0;
    const int retry_count = 10;
    while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET && ++retry < retry_count) {
        ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
        vTaskDelay(2000 / portTICK_PERIOD_MS);
    }
    time(&now);
    localtime_r(&now, &timeinfo);

    ESP_ERROR_CHECK( example_disconnect() );

initialize_sntp()

The initialze_sntp() will initialize the SNTP server and start the synchronization.

static void initialize_sntp(void)
{
    ESP_LOGI(TAG, "Initializing SNTP");
    sntp_setoperatingmode(SNTP_OPMODE_POLL);
    sntp_setservername(0, "pool.ntp.org");
    sntp_set_time_sync_notification_cb(time_sync_notification_cb);
#ifdef CONFIG_SNTP_TIME_SYNC_METHOD_SMOOTH
    sntp_set_sync_mode(SNTP_SYNC_MODE_SMOOTH);
#endif
    sntp_init();
}

The following three functions, start the ESP32 synchronization with SNTP. Here, “pool.ntp.org” is the SNTP server that is commonly used.


sntp_setoperatingmode(SNTP_OPMODE_POLL);
sntp_setservername(0, "pool.ntp.org");
sntp_init();

For the time sync, if smooth method was selected by the user, then the following code will run. The sntp_set_sync_mode() function will set the time synchronization mode as SNTP_SYNC_MODE_SMOOTH. In this case, smooth time updating is being set. The adjtime function is used to notify the time in this case.

#ifdef CONFIG_SNTP_TIME_SYNC_METHOD_SMOOTH
    sntp_set_sync_mode(SNTP_SYNC_MODE_SMOOTH);
#endif

Compiling the Sketch

To flash your chip, type the following command in the serial terminal. Remember to replace the COM port with the one through which your board is connected.

idf.py -p COMX flash monitor
ESP32 SNTP Example using ESP-IDF Flash Chip

After the code flashes successfully, you can view all the informational logs. The first time the ESP32 boots, we can view the current date and time in New York as well as for Shanghai. Then the ESP32 board goes into deep sleep mode for 10 seconds.

ESP32 SNTP Example using ESP-IDF Terminal 1

After 10 seconds are over, the ESP32 board wakes up and logs the current timestamp for both New York and Shanghai. Additionally, the boot count is also printed which denotes the number of times the ESP32 board restarted.

ESP32 SNTP Example using ESP-IDF Terminal 2

You may also like to read:

2 thoughts on “ESP32 SNTP Module using ESP-IDF – Synchronize Time with NTP”

Leave a Comment