Come gestire il tempo con Arduino

I microcontrollori funzionano grazie ad una sorgente di clock che permette di temporizzare e sincronizzare tutti i moduli che lo compongono. Il clock è un segnale con una frequenza fissa e stabile tipicamente prodotta da un oscillatore al quarzo. Questo segnale è la base per tutte quelle applicazioni che devono gestire date e orari. In effetti esistono molte applicazioni in cui la gestione del tempo è il fattore principale, pensiamo a tutti quegli automatismi che vengono attivati in funzione dell’ora e della data corrente o che devono compiere delle azioni cicliche.
La gestione del tempo viene affidata a librerie esterne all’Arduino IDE, on line troviamo decine di progetti, quella che io utilizzo è la libreria Arduino Time scritta da Michael Margolis. Questa libreria comprende le classi Time, TimeAlarms e DS1307RTC, con cui è possibile avere un controllo pressoché completo sulle data e sull’orario.

La sorgente di clock alla base della generazione del dato di tempo può essere quella interna al microcontrollore oppure quella gestita da un circuito esterno, tipicamente chiamato Real Time Clock(RTC) come l’itegrato DS1307.
La differenza sostanziale è che usando un RTC avremmo sempre a disposizione il tempo e la data correnti poiché questo circuito tiene in memoria il conteggio impiegando una batteria tampone. Quando la base dei tempi è il clock interno del mirocontrollore, avremo il vantaggio di risparmiare sul costo dell’hardware esterno, ma purtroppo in caso di reset o mancata alimentazione del circuito, la perdita del conteggio. Quindi quando il nostro progetto ha bisogno di avere una fonte temporale sicura è consigliato l’uso di un circuito RTC. Per tutte le applicazioni di test possiamo invece utilizzare il conteggio interno del microcontrollore.

Libreria Time

E’ la libreria principale che permette di creare e manipolare date e orari. Internamente la libreria Time utilizza la funzione millis() di Arduino per tenere traccia del tempo trascorso.
La libreria espone diversi metodi che permettono di impostare ed ottenere informazioni sulla data e sul tempo.
La prima operazione da effettuare è quella di impostare la data e l’orario tramite la funzione setTime():

setTime(h,m,s,D,M,H)Imposta l’ora e la data corrente

Per recuperare informazioni di data e di tempo possiamo usare le seguenti funzioni:

second()Restituisce il numero di secondi dell’orario corrente (da 0 a 59)
minute()Restituisce il numero di minuti dell’orario corrente (da 0 a 59)
hour()Restituisce il numero di ore dell’orario corrente (da 0 a 23)
day()Restituisce il giorno della data corrente (da 1 a 31)
month()Restituisce il mese della data corrente (da 1 a 12)
year()Restituisce l’anno della data corrente

Nel codice di esempio seguente imposto un orario e una data per poi leggerne il valore ed inviarlo al pc tramite serial monitor:

//includo la libreria time
#include <Time.h>

void setup() {

  //init Seriale
  Serial.begin(9600);
  delay(100);

  //imposto l'ora e la data
  //ora 23:59:55
  //data 31/12/2015
  setTime(23,59,55,31,12,2015);
}

void loop() {
  //leggo l'ora e la data
  //e la spedisco sul serial monitor

  Serial.print("Tempo= ");
  Serial.print(hour());
  Serial.print(":");
  Serial.print(minute());
  Serial.print(":");
  Serial.print(second());
  Serial.println("");

  Serial.print("Data= ");
  Serial.print(day());
  Serial.print("/");
  Serial.print(month());
  Serial.print("/");
  Serial.print(year());
  Serial.println("");

  //esegui ogni secondo
  delay(1000);
}

Nel codice precedente l’orario e la data sono impostati qualche secondo prima del 1 gennaio 2016, in questo modo potete osservare che la il dato viene incrementato correttamente passando dal 31 dicembre al 1 gennaio.

Altra funzione utile è la adjustTime() che permette di aggiungere o sottrarre dei valori alla data/ora corrente. Inserendo un numero di secondi positivo, incrementeremmo il valore di data ora, viceversa inserendo un valore in secondi negativo decrementeremo il valore di data e ora.
Nell’esempio seguente incremento di un giorno il valore impostato:

//includo la libreria time
#include <Time.h>

void setup() {
  //init Seriale
  Serial.begin(9600);
  delay(100);

  //imposto l'ora e la data
  //ora 23:59:55
  //data 31/12/2015
  setTime(23,59,55,31,12,2015);
}

void loop() {
  //leggo l'ora e la data
  //e la spedisco sul serial monitor

  Serial.print("Tempo= ");
  Serial.print(hour());
  Serial.print(":");
  Serial.print(minute());
  Serial.print(":");
  Serial.print(second());
  Serial.println("");

  Serial.print("Data= ");
  Serial.print(day());
  Serial.print("/");
  Serial.print(month());
  Serial.print("/");
  Serial.print(year());
  Serial.println("");

  //esegui ogni secondo
  delay(1000);
  //aggiungi un giorno alla data corrente
//il valore del giorno è espresso in secondi
//60*60*24
  adjustTime(86400);
}

Utilizzare time_t

La variabile time_t è tipica dei sistemi Unix e rappresenta il numero di secondi trascorsi dal 1 gennaio 1970. La funzione now() restituisce il valore in time_t dei secondi trascorsi tra il 1/1/1970 rispetto alla data corrente impostata con la funzione setTime(). In pratica now() esegue una sottrazione tra la data e ora corrente e il valore di default.

//includo la libreria time
#include <Time.h>

time_t tm = 0;

void setup() {
  //init Seriale
  Serial.begin(9600);
  delay(100);

  //imposto l'ora e la data
  //ora 23:59:55
  //data 31/12/2015
  setTime(23,59,55,31,12,2015);
}

void loop() {
  //recupero i secondi trascorsi dal 1/1/1970
  //rispetto alla data impostata con setTime
  tm = now();

  Serial.println(tm);

  //esegui ogni secondo
  delay(1000);

}

Se escludiamo dal codice la funzione setTime() la data e l’ora corrente verrà automaticamente impostata al 1/1/1970 00:00 e now() inizierà il conteggio partendo da zero.

//includo la libreria time
#include <Time.h>

time_t tm=0;

void setup() {
  //init Seriale
  Serial.begin(9600);
  delay(100);
}

void loop() {
  //recupero i secondi trascorsi dal 1/1/1970
  tm = now();

  Serial.println(tm);

  //esegui ogni secondo
  delay(1000);

}

 

E’ possibile settare una data ed un orario usando i secondi trascorsi dal 1/1/1970 ma questo è alquanto scomodo, perciò è disponibile la struttura TimeElements in abbinamento con la funzione makeTime e setTime():

//includo la libreria time
#include <Time.h>

time_t tm ;
//struttura timeelements
TimeElements te;

void setup() {
  //init Seriale
  Serial.begin(9600);
  delay(100);

//riempio la struttura con l'orario e la data
//rispettando i correttamente i campi
  te.Second = 0;
  te.Minute = 10;
  te.Hour = 10;
  //te.wday = 0; //giorno della settimana da 0 a 6 (non necessario per la makeTime)
  te.Day = 8;
  te.Month = 4;
  te.Year = 2015 - 1970; //accetta valori da 0 a 99

  //converte la data e l'ora in secondi trascorsi
  //dal 1/1/1970 00:00
  tm = makeTime(te);

  //setta la data e l'ora
  setTime(tm);
}

void loop() {
  //visualizza l'ora corrente e la data
  Serial.print("Tempo= ");
  Serial.print(hour());
  Serial.print(":");
  Serial.print(minute());
  Serial.print(":");
  Serial.print(second());
  Serial.println("");

  Serial.print("Data= ");
  Serial.print(day());
  Serial.print("/");
  Serial.print(month());
  Serial.print("/");
  Serial.print(year());
  Serial.println("");

  //esegui ogni secondo
  delay(1000);
}

Usare TimeAlarms

TimeAlarms permette di eseguire funzioni in corrispondenza di orari preimpostati o anche ad intervalli regolari.

Per eseguire una funzione in uno specifico orario utilizzeremmo il metodo alarmRepeat() inserendo come parametro data, orario e il nome della funzione da richiamare. Per eseguire in modo corretto la funzione alarmRepeat() è necessario inserire nel blocco loop la funzione Alarm.delay() che crea un ritardo e verifica quando richiamare la funzione inserita nell’alarmRepeat():

//includo la libreria time e TimeAlarms
#include <Time.h>
#include <TimeAlarms.h>

void setup() {
  //init Seriale
  Serial.begin(9600);
  delay(100);

  pinMode(13, OUTPUT);

  //imposto l'ora e la data
  //ora 15:29:30
  //data 08/04/2015
  setTime(15,29,30,8,4,2015);

  //creo
  Alarm.alarmRepeat(15,30,0, ledBlink);
}

void loop() {
  //visualizza l'ora corrente
  Serial.print("Tempo= ");
  Serial.print(hour());
  Serial.print(":");
  Serial.print(minute());
  Serial.print(":");
  Serial.print(second());
  Serial.println("");

  //esegui ogni secondo
  //e richiama la funzione Alarm
  Alarm.delay(1000);
}

void ledBlink()
{
  //blinka il led onboard
  digitalWrite(13, HIGH);
  delay(500);
  digitalWrite(13, LOW);
  delay(500);
  digitalWrite(13, HIGH);
  delay(500);
  digitalWrite(13, LOW);
  delay(500);
}

 

Nel codice precedente la funzione ledBlink() viene richiamata non appena l’orario corrente corrisponde a quello specificato nella alarmRepeat().

La funzione ledBlink viene richiamata ogni giorno alle 15:30:00. Volendo possiamo specificare nella funzione alarmRepeat() solo un giorno della settimana utilizzando uno dei parametri seguenti:

dowSundayDomenica
dowMondayLunedi
dowTuesdayMartedì
dowWednesdayMercoledì
dowThursdayGiovedì
dowFridayVenerdì
dowSaturdaySabato

ad esempio per eseguire la funzione ledBlink solo il lunedì il codice dovrà essere modificato in questo modo:

//includo la libreria time e TimeAlarms
#include <Time.h>
#include <TimeAlarms.h>

void setup() {
  //init Seriale
  Serial.begin(9600);
  delay(100);

  pinMode(13, OUTPUT);

  //imposto l'ora e la data
  //ora 15:29:30
  //data 08/04/2015
  setTime(15,29,30,8,4,2015);

  //creo un allarme per ogni lunedi alle 15:30:00
  Alarm.alarmRepeat(dowMonday,15,30,0, ledBlink);
}

void loop() {
  //visualizza l'ora corrente
  Serial.print("Tempo= ");
  Serial.print(hour());
  Serial.print(":");
  Serial.print(minute());
  Serial.print(":");
  Serial.print(second());
  Serial.println("");

  //esegui ogni secondo
  //e richiama la funzione Alarm
  Alarm.delay(1000);
}

void ledBlink()
{
  //blinka il led onboard
  digitalWrite(13, HIGH);
  delay(500);
  digitalWrite(13, LOW);
  delay(500);
  digitalWrite(13, HIGH);
  delay(500);
  digitalWrite(13, LOW);
  delay(500);
}

 

La libreria permette anche di eseguire la funzione specificata solamente una sola volta utilizzando il metodo alarmOnce(). Anche in questo caso è possibile specificare sia giorno della settima sia solo l’orario.

//includo la libreria time e TimeAlarms
#include <Time.h>
#include <TimeAlarms.h>

void setup() {
  //init Seriale
  Serial.begin(9600);
  delay(100);

  pinMode(13, OUTPUT);

  //imposto l'ora e la data
  //ora 15:29:30
  //data 08/04/2015
  setTime(15,29,30,8,4,2015);

  //creo un allarme ed eseguilo solo una volta
  Alarm.alarmOnce(15,30,0, ledBlink);
}

void loop() {
  //recupero i secondi trascorsi dal 1/1/1970
  //rispetto alla data impostata con setTime
  Serial.print("Tempo= ");
  Serial.print(hour());
  Serial.print(":");
  Serial.print(minute());
  Serial.print(":");
  Serial.print(second());
  Serial.println("");

  //esegui ogni secondo
  //e richiama la funzione Alarm
  Alarm.delay(1000);
}

void ledBlink()
{
  //blinka il led onboard
  digitalWrite(13, HIGH);
  delay(500);
  digitalWrite(13, LOW);
  delay(500);
  digitalWrite(13, HIGH);
  delay(500);
  digitalWrite(13, LOW);
  delay(500);
}

 

Come scritto precedentemente è anche possibile eseguire un timer che ripete costantemente una particolare funzione. Il metodo da usare è timerRepeat(). Ad esempio il codice seguente permette di eseguire ogni 30 secondi la funzione Allarme()

//includo la libreria time e TimeAlarms
#include <Time.h>
#include <TimeAlarms.h>

void setup() {
  //init Seriale
  Serial.begin(9600);
  delay(100);

  //imposto l'ora e la data
  //ora 15:30:00
  //data 08/04/2015
  setTime(15,30,00,8,4,2015);

  //cesegui la funzione ogni 30 secondi
  Alarm.timerRepeat(30, Allarme);
}

void loop() {
  //recupero i secondi trascorsi dal 1/1/1970
  //rispetto alla data impostata con setTime
  Serial.print("Tempo= ");
  Serial.print(hour());
  Serial.print(":");
  Serial.print(minute());
  Serial.print(":");
  Serial.print(second());
  Serial.println("");

  //esegui ogni secondo
  //e richiama la funzione Alarm
  Alarm.delay(1000);
}

void Allarme()
{
  //allarme!!!!
  Serial.println("Allarme!!");
}

 

è possibile anche creare un timer che viene eseguito solo una volta impiegando la funzione timerOnce():

//includo la libreria time e TimeAlarms
#include <Time.h>
#include <TimeAlarms.h>

void setup() {
  //init Seriale
  Serial.begin(9600);
  delay(100);

  //imposto l'ora e la data
  //ora 15:30:00
  //data 08/04/2015
  setTime(15,30,00,8,4,2015);

  //cesegui la funzione dopo 30 secondi
  //la funzione non viene più eseguita fino al reset
  //della scheda
  Alarm.timerOnce(30, Allarme);
}

void loop() {
  //recupero i secondi trascorsi dal 1/1/1970
  //rispetto alla data impostata con setTime
  Serial.print("Tempo= ");
  Serial.print(hour());
  Serial.print(":");
  Serial.print(minute());
  Serial.print(":");
  Serial.print(second());
  Serial.println("");

  //esegui ogni secondo
  //e richiama la funzione Alarm
  Alarm.delay(1000);
}

void Allarme()
{
  //genera l'allarme
  Serial.println("Allarme!!");
}

 

Time e TimeAlarms sono due librerie molto utili che prima o poi dovremmo utilizzare per creare applicazioni che devono essere sincronizzate con la data e l’ora. Gli esempi visti fino ad ora sono si funzionali ma non permettono di tenere traccia del conteggio del tempo anche quando si verifica un reset della scheda. La soluzione per evitare questo problema è l’impiego di un circuito esterno chiamato RTC (Real Time Clock).

La libreria DS1307RTC

Questa libreria viene utilizzata in abbinamento ad un circuito RTC esterno. Questi circuiti sono composti da un integrato (nel nostro caso il DS1307) da un quarzo e da una batteria tampone che garantisce il conteggio del tempo anche quando avviene un reset del microcontrollore o una mancanza di energia.
Il loro costo è contenuto, sono disponibili in commercio diversi modelli che soddisfano molte tipologie di installazione. Quello in mio possesso è prodotto da SeedStudio e potete visualizzare le sue caratteristiche seguendo questo link.

Questo circuito si collega all’Arduino UNO usando il bus I2C. La libreria DS1307RTC ha quindi bisogno di interagire con questo bus richiedendo l’inserimento nel codice della libreria Wire.

L’RTC deve essere dotato di batteria tampone CR2032 per poter garantire il conteggio del tempo anche in assenza di alimentazione (proveniente da Arduino). Inoltre abbiamo bisogno, la prima volta che usiamo l’RTC, di impostare la data e l’ora correnti. Lo sketch seguente mostra come fare questo settaggio:

#include <Wire.h>
#include <Time.h>
#include <DS1307RTC.h>

time_t tm ;
//struttura timeelements
TimeElements te;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  delay(1000);

  //imposto la struttura con ora
  //e data
  te.Hour = 10;
  te.Minute = 10;
  te.Second = 10;
  //tm.Wday = 2
  te.Day = 7;
  te.Month = 4;
  te.Year = 2015 - 1970;

  //converte la data e l'ora in secondi trascorsi
  //dal 1/1/1970 00:00
  tm = makeTime(te);

  //setta la data e l'ora
  //questa operazione agiste sul chip
  //DS1307 tramite bus I2C
  RTC.set(tm);

}

void loop() {
  // put your main code here, to run repeatedly:

}

 

Possiamo usare anche un altro metodo per settare data e ora, piuttosto che impiegare il metodo set() possiamo utilizzare il metodo write() in abbinamento con la struttura TimeElement:

#include <Wire.h>
#include <Time.h>
#include <DS1307RTC.h>

//struttura timeelements
TimeElements te;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  delay(1000);

  te.Hour = 10;
  te.Minute = 10;
  te.Second = 10;

  te.Day = 10;
  te.Month = 4;
  te.Year = 2015 - 1970;

  RTC.write(te);
}

void loop() {
  ;
}

 

A questo punto l’RTC incrementerà autonomamente il valore di data e ora . Il codice di esempio seguente va infatti a leggere il valore di data e ora, precedentemente impostato, e lo invia sul serial monitor (il valore è espresso in secondi trascorsi dal 1/1/1970):

#include <Wire.h>
#include <Time.h>
#include <DS1307RTC.h>

void setup() {
  Serial.begin(9600);
  delay(1000);
}

void loop() {
  //legge del modulo RTC
  Serial.println(RTC.get());

  //leggi ogni secondo
  delay(1000);
}

 

Il codice precedente non è di certo esplicativo, possiamo quindi convertire il dato (secondi trascorsi dal 1/1/1970) in valori comprensibili utilizzando il metodo get() ed elaborando il valore restituito con le funzioni della libreria Time:

#include <Wire.h>
#include <Time.h>
#include <DS1307RTC.h>

time_t tm ;
//struttura time elements
TimeElements te;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  delay(1000);

  //legge del modulo RTC
  tm = RTC.get();
  //setta la data e l'ora
  //in riferimento alla
  //libreria Time
  setTime(tm);
}

void loop() {
  Serial.print("Tempo RTC = ");
  Serial.print(hour());
  Serial.write(':');
  Serial.print(minute());
  Serial.write(':');
  Serial.print(second());
  Serial.println();

  Serial.print("Data RTC = ");
  Serial.print(day());
  Serial.write('/');
  Serial.print(month());
  Serial.write('/');
  Serial.print(year());
  Serial.println();

  //leggi ogni secondo
  delay(1000);
}

 

Oppure possiamo utilizzare il metodo read() per popolare la struttura TimeElements ed ottenere lo stesso risultato ma con meno righe di codice.

#include <Wire.h>
#include <Time.h>
#include <DS1307RTC.h>

//struttura time elements
TimeElements te;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  delay(1000);
}

void loop() {
  //legge del modulo RTC
  if (RTC.read(te))
  {
    Serial.print("Tempo RTC = ");
    Serial.print(te.Hour);
    Serial.write(':');
    Serial.print(te.Minute);
    Serial.write(':');
    Serial.print(te.Second);
    Serial.println();

    Serial.print("Data RTC = ");
    Serial.print(te.Day);
    Serial.write('/');
    Serial.print(te.Month);
    Serial.write('/');
    Serial.print(te.Year + 1970);
    Serial.println();
  }

  //leggi ogni secondo
  delay(1000);
}

 

Utilizzando le librerie di questo tutorial avrete pieno controllo su data ed ora per realizzare progetti dove il riferimento temporale è il requisito principale.