In embedded applications it’s often necessary a clock, as accurate as possible. For example, imagine a device that has to activate an output at a given time or a logger that has to store values (temperature…) at precise intervals.
A solution widely adopted is the use of an RTC (Real Time Clock) chip. The problem becomes how you configure the RTC chip with the current time and how you keep it synchronized. Some time ago, I published a project (RTCSetup) to configure an RTC connected to an Arduino using the time of your PC; today I’ll show you how to use an Internet time service with the esp32 chip.
SNTP
SNTP (Simple Network Time Protocol) is a protocol designed to synchronize the clock of devices connected to the Internet. The latest version of the protocol (SNTPv4) is defined in the RFC 4330. The word “simple” in its name is because of SNTP is a reduced and simplified version of the NTP (Network Time Protocol) protocol; protocol which, to be able to guarantee high accuracy, is quite complex.
The basic working principle of SNTP is as follows:
– the client device connects to the server using the UDP protocol on port 123
– the server answers with different fields, among which is the timestamp value, 64bit long, where:
- the first 32 bits are the number of seconds from 01/01/1990
- the remaining 32 bits are the fraction of seconds
esp-idf
In a previous post I’ve already introduced the lwip library, chosen by the esp-idf framework to implement network communcation. The library includes an app (sntp.c) which implements a SNTP client.
Let’s see how to use it in your program (the full source of the test program is available in my Github repository).
First you have to include its header file:
#include "apps/sntp/sntp.h" |
Then configure the SNTP client in poll mode, to query the servers every n seconds:
sntp_setoperatingmode(SNTP_OPMODE_POLL); |
Alternatively, you can configure the client in listen only mode (SNTP_OPMODE_LISTENONLY). In this configuration, the client will listen for broadcast updates, without actively query any servers.
Now you have to tell the SNTP client which server it has to use. A common choice is to use the cluster of servers from pool.ntp.org. Alternatively, because of I live in Italy, I normally use the SNTP server offered by the Istituto Nazionale di Ricerca Meterologica (ntp1.inrim.it) that has an atomic clock as its time source.
In the example provided with this post, the server name can be configured via menuconfig:
sntp_setservername(0, CONFIG_SNTP_SERVER); |
After having configured the client, you can start it with:
sntp_init(); |
the client will be executed in parallel with the main program.
To get the actual time from the system clock, use the time() method which updates a time_t variable:
time_t now; time(&now); |
The time_t variable usually represents the time as the number of seconds from a date called Epoch (01/01/1970). To split the variable into the different time values (year, month, day…) you can use the localtime_r method which updates a tm struct:
struct tm timeinfo; localtime_r(&now, &timeinfo); |
The fields of the tm struct are defined in the time.h file:
We can verify if the first sync with the SNTP server was perfomed with success for example checking the year field:
while(timeinfo.tm_year < (2016 - 1900)) { printf("Time not set, waiting...\n"); vTaskDelay(5000 / portTICK_PERIOD_MS); time(&now); localtime_r(&now, &timeinfo); } |
If the struct tm is available, you can then use the strftime() method to format a string with the pattern you prefer. Many placeholders are available: they are described in the manpage of the method. Here are some examples:
char buffer[100]; // es. 08/05/2017 15:10:34 strftime(buffer, sizeof(buffer), "%d/%m/%Y %H:%M:%S", &timeinfo); // es. Monday, 08 May 2017 strftime(buffer, sizeof(buffer), "%A, %d %B %Y", &timeinfo); // es. Today is day 128 of year 2017 strftime(buffer, sizeof(buffer), "Today is day %j %Y", &timeinfo); |
Time zone
SNTP servers keep the time with the UTC (Coordinated universal time) time zone. You can change the time zone modifying the TZ environmental value with the method:
int setenv(const char *name, const char *value, int overwrite); |
name is the name of the variable and value the value for it. If overwrite is > 0 the method updates the variable if already exists.
Once the value for the TZ variable is set, you can initialize the timezone conversion routine with the tzset() command.
The TZ variable can have two different kind of values, depending on whether you want to handle daylight saving time or not.
In the simplest form (no daylight saving time) the format is std offset, where std is the name for the time zone (3 or more characters long) and offset specifies the time value you must add to the local time to get a UTC value. For example Italy is in the CET (Central European Time) timezone, which is one hour ahead from UTC (therefore offset -1). The command to use in Italy is:
setenv("TZ", "CET-1", 1); |
If you want to handle the daylight saving time, you can use the format std offset dst [offset],start[/time],end[/time]. std and the first offset have the same meaning as above. dst and the second offset specify the name and offset for the corresponding daylight saving time zone (if the second offset is omitted, the default is 1 hour ahead of standard time). Finally, start and end are the day and time when daylight saving time starts and ends.
For Italy, in the daylight saving time we use the CEST (Central European Summer Time) time zone, active (direttiva 2008/84/CE del Parlamento Europeo) from 02:00 of the last Sunday of March to 03:00 of the last Sunday of October.
The command is therefore:
setenv("TZ", "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00", 1); |
M3.5.0 specify March (3), last week (5), Sunday (0).
On the Internet you can find websites that lists the TZ values for different countries.
In conclusion, if you want to convert the time to a given timezone the steps are:
[checklist]
- configure the TZ variable (setenv())
- inizialize the timezone conversion routine (tzset())
- update the tm struct (localtime_r())
[/checklist]
In the example program:
// print the actual time in Italy printf("Actual time in Italy:\n"); localtime_r(&now, &timeinfo); strftime(buffer, sizeof(buffer), "%d/%m/%Y %H:%M:%S", &timeinfo); printf("%s\n", buffer); |
Hi Luca,
I’m new to the ESP32 and I’ve used the example SNTP code to synchronise the local time on an ESP32 with the time on a Raspberry Pi, which I’ve set up as an NTP server (this is because I need to synchronise multiple ESPs’ clocks offline). In order to test if the ESP clock remains in sync with the Pi, I’ve changed SNTP_UPDATE_DELAY to its minimum of 15 seconds, and manually adjusted the time on the Pi (i.e. added 30 mins to the current time), however I see no change in the local time on the ESP, even after waiting a while. After rebooting the ESP it syncs with the new time. Do you know how I could get it to re-synchronise on its own? Is there a particular function I can run to do this after the SNTP client has initially been set up?
Thanks!
Hi Daniel… it should work without any explicit call to the sntp “module”… you may run a tcpdump on the Raspberry to “see” if the esp32 chip correctly request the time every 15 seconds, otherwise it might be a bug of the framework
Thanks for wonderful tutorials,
Just found small typo in this tutorial
Then configure the SNTP client in pool mode, to query the servers every n seconds:
pool -> poll
thanks!
Like Daniel above, I have found that re-synchronisation is not happening at the expected interval, despite what all the ESP32 documentation says. This is true even if I set
sntp_set_sync_mode(SNTP_SYNC_MODE_IMMED);
A workaround is to call sntp_restart(); in a task which runs at the desired interval.
However, it would be good to know why the documented re-synchronisation does not happen.