Parte tre: Fishino, l’NTP e la sua applicazione

Si apre ora la parte informatica del progetto. Vedremo in dettaglio l’acquisizione dell’orario da un server NTP e l’inserimento di questo nell’RTC onboard. 

Nel primo articolo avevo anticipato che l’orologio sarebbe stato gestito da un Arduino, per via della sua diffusione e della semplicità d’uso. Quello che non ho detto è che invece di un arduino “regolare”, il progetto sarà operato da un fishino uno. Il Fishino è una scheda derivata da arduino uno, che integra un ESP8266 per la connettività wifi, un RTC(real time clock), e un lettore di schede microSD. Il Fishino è un progetto italiano, distribuito da Futura Elettronica. Per altre informazioni vi invito a guardare il sito fishino.it.

Fishino uno - foto via qui
Fishino uno – foto via qui

Di tutte le features che integra il fishino, la più utile per questo progetto è senz’altro l’RTC, nel nostro caso un DS1307. Si tratta di un integrato che gestisce autonomamente lo scorrimento del tempo, in modo da non impegnare il microcontrollore principale. Il fishino integra inoltre una backup battery per l’rtc, in modo che anche quando l’orologio non è collegato, l’orario possa rimanere salvato. Il DS1307 fornirà l’orario su richiesta del microcontrollore, tramite il bus I2C. 

Un’altra caratteristica utile del fishino in questo progetto è il modulo wifi integrato. Tramite il WiFi saremo infatti in grado di connetterci alla rete internet, e ottenere l’orario esatto anche nel caso in cui l’rtc non sia molto preciso. Per far questo si usa il protocollo NTP(network time protocol), lo stesso protocollo che viene usato dai computer connessi a internet per mantenere il loro orario sincronizzato. Il protocollo NTP prevede che il client, in questo caso l’orologio, invii un pacchetto UDP a un server NTP, e che il server risponda compilando alcuni campi del pacchetto e restituendolo. Vediamo la struttura del pacchetto NTP che invieremo al server, in dettaglio:

Struttura di un pacchetto NTP - illustrazione via qui.
Struttura di un pacchetto NTP – illustrazione via qui.
  • LI(2 bit): è l’indicatore di un leap second, ovvero un secondo aggiunto o sottratto artificalmente per compensare un eventuale disallineamento con il naturale ciclo planetario. La precisione richiesta a un orologio da tavolo è tale da consentirci di ignorare questo campo, dato che l’eventuale secondo in più o in meno sarà recuperato alla successiva sincronizzazione. Imposteremo il campo a 11(non sincronizzato).
  • VN(3 bit): è il numero di versione in uso, attualmente la 4. Il campo sarà impostato a 100(4 nel sistema binario).
  • MODE(3 bit): è la nostra presentazione al server. Siamo infatti chiamati a identificarci. Segnaleremo al server che siamo un client, impostando il dato a 011(3 in binario).
  • STRATUM(8 bit): il sistema NTP è organizzato in modo gerarchico, secondo livelli detti stratum. I server nello stratum 1 sono detti primari, perchè ottengono l’orario direttamente dal sistema GPS o dai sistemi radio WWV e CDMA, le fonti primarie. I server dello strato primario sono indicati qui. I server dello strato 2 ricevono l’orario da quelli dello strato 1 e così via, fino allo strato 15. Noi non abbiamo bisogno di dichiarare il nostro stratum, imposteremo quindi il campo a 0(unspecified).
  • POLL INTERVAL(8 bit): il protocollo NTP è predisposto per ricevere interrogazioni cicliche da parte dei client. Qui è richiesto al client di specificare il periodo delle sue interrogazioni, espressa sotto forma di secondi in potenza di due. Impostando per esempio il campo a 10, il server si aspetterà di ricevere interrogazioni ogni 210=1024 secondi. Noi non useremo questo campo, perché le nostre interrogazioni saranno saltuarie. Impostiamo comunque il campo a 10, un valore comunemente accettato dai server. 
  • PRECISION(8 bit): Questo numero indica la precisione dell’orologio. Sarà più utile leggere questo valore nella risposta del server, che inviarlo come parametro, ma in ogni caso non ci sarà utile per questo progetto. Il numero comunque è espresso come 8 bit con segno, e rappresenta una potenza di 2. Dovendo indicare un valore, imposteremo questo numero a -10(0xF6), così che rappresenti 2-10≈0.001 secondi.
  • ROOT DELAY(32 bit): Anche questo parametro può essere letto nella risposta del server, e indica il ritardo tra il server e lo stratum primario. Non verrà comunque usato per questo progetto. Imposteremo tutti i 4 byte a zero.
  • ROOT DISPERSION(32 bit): Indica il massimo ritardo tra il server e lo stratum primario. Non verrà usato, e lo imposteremo a zero. 
  • REFERENCE IDENTIFIER(32 bit): Questo dato ci viene passato dal server e contiene indicazioni su quale sia la fonte dell’orario di quel server. La forma di questo parametro è molto flessibile. Nel caso in cui il parametro stratum sia (unspecified o primary) questo campo sarà una stringa di caratteri ascii che indicano la sorgente(ad esempio ATOM per gli orologi atomici, GPS per il sistema GPS ecc.). Nel caso in cui il server sia posizionato in uno stratum più basso conterrà l’indirizzo IP del server sorgente. Ad ogni modo non sarà utilizzato e lo imposteremo a zero.
  • REFERENCE TIMESTAMP(64 bit): Contiene l’ultimo orario impostato sull’orologio locale del client. È disposto secondo una particolare struttura chiamata NTP Timestamp, che approfondiremo in seguito. Lo imposteremo a zero, dato che per la nostra applicazione non è necessario conoscerlo.
  • ORIGINATE TIMESTAMP(64 bit): In questo campo il client scrive l’orario di invio della richiesta. Come tutti gli altri orari del protocollo NTP è impacchettato secondo il Timestamp NTP. Nella nostra richiesta verrà impostato a zero.
  • RECEIVE TIMESTAMP(64 bit): Qui il server scrive l’orario di ricezione della richiesta. Anche questo avrà valore zero nella nostra richiesta, e non lo leggeremo nella risposta. 
  • TRANSMIT TIMESTAMP(64 bit): Questo è l’orario del server al momento di invio della rispsota. Questo sarà l’orario che andremo a leggere dalla risposta, perché si tratta del più vicino all’orario reale. Nella nostra richiesta, comunque, sarà impostato a zero. 

Il pacchetto NTP consta quindi di 48 byte, se escludiamo i campi opzionali contenenti le informazioni per l’autenticazione, che non saranno usati, perché ci connetteremo solo a server pubblici ad accesso libero. La presenza di così tanti timestamp in un unico pacchetto è dovuto al fatto che sono necessari per calcolare le latenze del pacchetto. Per applicazioni che richiedono particolare precisione è infatti possibile calcolare e corregere queste latenze, ottenendo un orario più preciso. Si tratta comunque di poche decine di millisecondi, che non è necessario correggere nel nostro caso. 

Vediamo ora il timestamp NTP. Abbiamo detto che è costituito da 64 bit. Si tratta di un numero a virgola fissa, che esprime i secondi trascorsi dalla mezzanotte del 1 gennaio 1900. I primi 32 bit del timestamp esprimono i secondi interi, mentre i successivi 32 bit esprimono la parte frazionaria.

È da notare che il sistema di misura del tempo più usato in informatica, il tempo UNIX, rappresenta i secondi trascorsi dalla mezzanotte del 1 gennaio 1970. C’è quindi una differenza di 70 anni, o 2208988800 secondi, tra i due sistemi. La prima operazione che compiamo ottenuti i secondi NTP è di sottrarre 2208988800 dai secondi, in modo da ottenere il tempo UNIX. I passaggi successivi possono essere eseguiti indipendentemente da questa sottrazione, che invece ci potrebbe servire per espandere le funzioni wifi dell’orologio.

Eseguiamo una divisione tra interi, tra il numero di secondi attuali e il numero di secondi presenti in un giorno. Il resto di questa divisione, diviso a sua volta per 3600, rappresenta le ore trascorse dall’inizio della giornata. Inseriamo questo risultato nella prima cella di un array di 3 interi, che conterrà l’orario. Prendiamo i resti delle divisioni per 3600 e lo dividiamo per 60, per ottenere i minuti. Prendiamo il resto della divisione per 60 per avere i secondi. 

orario[0] = (secondiNTP % 86400L) / 3600;
orario[1] = (secondiNTP % 3600) / 60;
orario[2] =  secondiNTP % 60;

A questo punto non ci resta che salvare i dati nell’RTC. Imposteremo l’orario convertito in BCD(Binary coded decimal, approfondimento qui) e la data del primo gennaio 2016. Non ci servirà ricordarla, quindi lasceremo che l’orologio parta a contare dal primo gennaio, per non lascare i campi a zero, che potrebbero creare problemi.

mappa della memoria del DS1307 - dal datasheet ufficiale
mappa della memoria del DS1307 – dal datasheet ufficiale
Wire.beginTransmission(0x68);
Wire.write(byte(0));
Wire.write(decToBcd(second));
Wire.write(decToBcd(minute));
Wire.write(decToBcd(hour)); 
[...]
Wire.endTransmission();

La trasmissione all’RTC inizia scrivendo l’indirizzo del DS1307(si trova nel datasheet) sul bus I2C. Successivamente scriviamo la posizione da cui iniziamo a scrivere(il byte 0) e iniziamo a mettere i dati nel registro. Al termine chiudiamo la trasmissione.

Per capire meglio facciamo un esempio:

DA FB B8 25 DA B8 30 FD

Questo è il transmit timestamp che ci ha inviato come risposta un server NTP. Prendiamo solo i primi 32 bit (4 byte), che indicano gli interi dei secondi. Convertito in binario è 11011010 11111011 10111000 00100101, che in decimale è 3673929765. Questi sono i secondi trascorsi dal 1 gennaio 1900 nel sistema UTC.

ore=\frac{3673929765\mod86400}{3600}=8.045333..
minuti=\frac{3673929765\mod3600}{60}=2.75
secondi=(3673929765\mod60)=45

Troncati i decimali, sono le ore 08:02:45.

Uno dei grandi vantaggi che ci porta l’uso di un RTC DS1307 risiede nel fatto che in questo particolare modello i dati sull’orario sono conservati e accessibili sotto forma di numeri BCD. Il BCD è un sistema di numerazione binaria comunemente usato in informatica. In questo sistema i numeri sono conservati in blocchi da 4 bit, ciascuno dei quali rappresenta una cifra da zero a nove. Un numero a due cifre(come ore, minuti o secondi) saranno quindi rappresentati da un byte, cioè 8 bit. Con delle semplici operazioni potremo separare le cifre e conservarle in un array di 6 valori, contenenti le nostre 6 cifre.

void readTime() {
  
  Wire.beginTransmission(DS1307);
  Wire.write(0b0);
  Wire.endTransmission();
  
  Wire.requestFrom(DS1307, 3);
  second = Wire.read();
  minute = Wire.read();
  hour   = Wire.read();

  digits[0]= second >> 4;
  digits[1]= second & 0b00001111;
  digits[2]= minute >> 4;
  digits[3]= minute & 0b00001111;
  digits[4]= hour   >> 4;
  digits[5]= hour   & 0b00001111;
}

Per prima cosa dovremo comunicare con l’RTC scrivendo sul bus I2c il suo indirizzo(che abbiamo definito all’inizio del programma con l’istruzione static const int DS1307 = 0x68;). Scriveremo poi il valore zero sul bus(il prefisso 0b indica al compilatore che il numero che segue è in sistema binario, mentre ad esempio il prefisso 0x indica che si tratta di un numero esadecimale), che rappresenta la posizione del registro da cui vogliamo iniziare la nostra lettura, e terminiamo l’operazione di scrittura. Iniziamo ora l’operazione di lettura indicando indirizzo e numero di campi successivi che vogliamo leggere, nel nostro caso tre. Immagazziniamo i valori letti in tre variabili: hour, minute second. 

Ora inseriamo nell’array digits[] i valori delle cifre, in quest’ordine:

  1. decina dei secondi
  2. unità dei secondi
  3. decina dei minuti
  4. unità dei minuti
  5. decina delle ore
  6. unità delle ore

Per ottenere le decine delle ore eseguiamo uno scorrimento dei bit a destra di 4 posizioni. Per ottenere le unità eseguiamo un’operazione logica AND con il numero 00001111. In questo modo le prime 4 cifre saranno eliminate, e saranno conservate le ultime quattro. 

Per chiarire facciamo l’esempio di voler scrivere il numero 57 nella posizione dei secondi. Il numero letto in BCD sarà 01010111. Facendo scorrere i bit ci troveremo col numero 00000101, ovvero l’espressione binaria del numero 5. Eseguendo un AND con il numero 00001111, ci troveremo ad avere il numero 00000111, ovvero l’espressione binaria del numero 7.

Ora dovrebbe essere chiaro il sistema di sincronizzazione NTP e quello di immagazzinamento e lettura dati dal chip RTC. Nei prossimi articoli affronteremo il loop e le funzioni che regolano la frequenza di aggiornamento dei dati.

CC BY-NC-SA 4.0 This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Un commento su “Parte tre: Fishino, l’NTP e la sua applicazione”

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *