In applicazioni embedded è spesso necessario avere a disposizione un orologio quanto più preciso possibile. Pensiamo ad esempio a dispositivi che si devono attivare in un preciso istante o a logger che devono memorizzare letture di una grandezza (ad esempio la temperatura esterna) a precisi intervalli.
La soluzione spesso adottata è l’utilizzo di un chip RTC (Real Time Clock). Il problema diviene quindi come configurare il chip RTC con l’orario corrente e come tenerlo allineato. Qualche tempo fa ho pubblicato un progetto (RTCSetup) per configurare un RTC collegato ad Arduino usando l’orario del proprio PC; oggi vedremo invece come utilizzare un servizio Internet con il chip esp32.
SNTP
SNTP (Simple Network Time Protocol) è un protocollo pensato per sincronizzare l’orario dei dispositivi connessi ad Internet. L’ultima versione del protocollo (SNTPv4) è definita nella RFC 4330. Il nome “simple” (semplice) è dovuto al fatto che SNTP è una versione ridotta e semplificata del protocollo NTP (Network Time Protocol); tale protocollo infatti – per consentire elevata accuratezza – ha un funzionamento molto complesso.
Il funzionamento base di SNTP è il seguente:
– il dispositivo client contatta un server usando il protocollo UDP sulla porta 123
– il server restituisce un valore di timestamp di 64bit, dove:
- i primi 32 bit rappresentano il numero di secondi trascorsi dal giorno 01/01/1990
- i rimanenti 32 bit rappresentano la frazione di secondi trascorsi
esp-idf
In un precedente articolo vi ho parlato della libreria lwip, scelta dal framework esp-idf per gestire le comunicazioni di rete. Tale libreria include una app (sntp.c) che implementa un client SNTP.
Vediamo come utilizzarla in un nostro programma (il sorgente completo del programma è disponibile nel mio repository Github).
Per prima cosa dobbiamo includere il relativo header file:
#include "apps/sntp/sntp.h" |
Quindi configuriamo il client SNTP in modo che interroghi il server (pool mode) ogni tot secondi:
sntp_setoperatingmode(SNTP_OPMODE_POLL); |
In alternativa possiamo configurare il client in modalità listen only (SNTP_OPMODE_LISTENONLY). In tale modalità il client SNTP rimarrà in attesa di ricevere aggiornamenti via broadcast, senza interrogare attivamente alcun server.
Dobbiamo ora indicare al client SNTP quale server utilizzare. Una delle scelte più comuni è affidarsi al cluster di server pool.ntp.org. In alternativa io, vivendo in Italia, normalmente utilizzo il server SNTP messo a disposizione dall’Istituto Nazionale di Ricerca Meterologica (ntp1.inrim.it) e sincronizzato con un orologio atomico.
Nell’esempio che ho preparato, il nome del server può essere configurato via menuconfig:
sntp_setservername(0, CONFIG_SNTP_SERVER); |
Terminata la configurazione, possiamo attivare il client con:
sntp_init(); |
Il client sarà eseguito in maniera autonoma rispetto al programma principale.
Per ottenere l’orario attuale del sistema, utilizziamo il metodo time() che aggiorna un puntatore ad una variabile di tipo time_t:
time_t now; time(&now); |
La variabile time_t rappresenta normalmente il numero di secondi trascorsi dalla data chiamata Epoch (01/01/1970). Per ottenere i vari campi (anno, mese, giorno…) possiamo utilizzare il metodo localtime_r che aggiorna una struct tm:
struct tm timeinfo; localtime_r(&now, &timeinfo); |
I campi che compongono la struct tm sono dettagliati nel file time.h:
Possiamo quindi capire se la prima sincronizzazione ha avuto successo verificando ad esempio l’anno:
while(timeinfo.tm_year < (2016 - 1900)) { printf("Time not set, waiting...\n"); vTaskDelay(5000 / portTICK_PERIOD_MS); time(&now); localtime_r(&now, &timeinfo); } |
Avendo a disposizione la struct tm, possiamo utilizzare il metodo strftime() per formattare una stringa secondo un formato scelto. I placeholders disponibili sono molti e descritti nella manpage di tale metodo. Di seguito alcuni esempi:
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); |
Fuso orario
I server SNTP restituiscono l’orario secondo il fuso orario UTC (Tempo coordinato universale). E’ possibile applicare un proprio fuso orario modificando la variabile di ambiente TZ, con il metodo:
int setenv(const char *name, const char *value, int overwrite); |
name indica il nome della variabile mentre value il valore che tale variabile deve assumere. Se overwrite è > 0 il metodo setenv aggiorna la variabile se già esiste.
Una volta aggiornato il valore della variabile TZ, è possibile inizializzare la funzionalità di timezone con il comando tzset().
La variabile TZ può assumere due tipi di valori, a seconda che si voglia gestire o meno l’ora legale.
Nel caso più semplice (nessuna gestione dell’ora legale), il formato è std offset, dove std indica il nome del fuso orario (3 o più caratteri), mentre offset indica il tempo da aggiungere a quello locale per arrivare a UTC. Ad esempio l’Italia è nel fuso orario CET (Central European Time) che è di un’ora avanti rispetto a UTC (quindi offset -1); il comando da utilizzare è:
setenv("TZ", "CET-1", 1); |
Se si vuole gestire anche l’ora legale, è possibile utilizzare il formato std offset dst [offset],start[/time],end[/time]. std e offset iniziali hanno lo stesso significato descritto in precedenza. dst e il secondo offset indicano il nome del fuso orario durante l’ora legale e il relativo offset (se questo secondo offset è omesso, il default è +1 rispetto all’orario normale). Infine start e end indicano giorno e ora di inizio e fine dell’ora legale.
Sempre nel caso dell’Italia, durante l’ora legale si utilizza il fuso orario CEST (Central European Summer Time) che è attiva (direttiva 2008/84/CE del Parlamento Europeo) dalle 02:00 dell’ultima domenica di Marzo fino alle 03:00 dell’ultima domenica di Ottobre.
Il comando da utilizzare in questo caso è quindi:
setenv("TZ", "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00", 1); |
M3.5.0 indica infatti Marzo (3), ultima settimana (5), giorno domenica (0).
Su Internet si trovano siti che raccolgono i valori di TZ per i differenti fusi orari.
Gli step per formattare l’orario secondo un fuso orario sono quindi:
[checklist]
- configurare la variabile TZ (setenv())
- inizializzare la funzionalità di conversione orario (tzset())
- aggiornare la struct tm (localtime_r())
[/checklist]
Nel programma di esempio:
// 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.