MODULI: Esempio RTC CON INTERFACCIA I2C

Per provare il codice presentato in questa pagina occorre avere a disposizione un real time clock (RTC) come quello rappresentato in figura

Il modulo RTC (Real Time Clock) fornisce un’accurata misurazione del tempo. Tale modulo si basa sull'integrato DS1307 I2C RTC (vedi datasheet). Il modulo Tiny RTC è dotato di EEPROM (AT24C32 - erasable and programmable read only memory) con 32kbyte (vedi datasheet ) di memoria. Alcuni modelli (non il nostro) hanno un sensore di temperatura (DS18B20 vedi  datasheet).

È presente una batteria tampone ricaricabile LIR2032 al litio che consente di alimentare il dispositivo e garantisce un funzionamento a lungo termine anche quando è disconnesso da arduino. Il circuito risulta essere così totalmente indipendente: incrementa l’ora senza aver bisogno di un microcontrollore e con la batteria completamente carica è in grado di funzionare per 1 anno intero quando il sensore di temperatura risulta spento oppure non presente.

L'orario e il calendario sono salvati nei registri in formato BCD. In questo formato ogni cifra di un numero è rappresentata con un codice binario a quattro bit, il cui valore è compreso tra 0000 (0) e 1001 (9). Ad esempio il numero 127 in formato BCD viene registrato in questo modo: 0001 0010 0111.
Sebbene il BCD comporti un notevole spreco di bit (circa 1/6 di memoria inutilizzata in packed BCD), in alcuni casi è preferibile perché ha una diretta corrispondenza con il codice ASCII. È sufficiente infatti sostituire i primi quattro bit inutilizzati con 0011 per ottenere il corrispondente ASCII (0011 + BCD  => 0011 + 0000 => '0').
Il codice BCD è molto usato in elettronica, specialmente in circuiti digitali privi di microprocessore, perché facilita la visualizzazione di lunghe cifre su display a sette segmenti dove ad ogni display fisico corrisponde esattamente una cifra. Esistono appositi circuiti integrati che effettuano la conversione da BCD nella corrispondente sequenza di accensione dei segmenti. Anche l'esecuzione di semplici calcoli aritmetici è più semplice da effettuarsi su cifre BCD per circuiti logici combinatori.

I registri dell'RTC vanno dall'indirizzo 00h a 07h. I registri utilizzabili come ram vanno da 08h a 3Fh.

- Il Bit 7 del registro dei secondi (00h) è il bit di halt (CH) dell'orologio. Quando questo bit è posto a 1 l'oscillatore è spento mentre se posto a 0 l'oscillatore viene riattivato.
- I valori che corrispondono al giorno della settimana vengono definiti dall'utente (solitamente 1 = domenica, 2 Lunedì etc.). Allo scadere della mezzanotte questi vengono incrementati di uno (se vale 7 viene riportato a 1).
- Il Bit 6 del registro delle ore (02h) indica se l'orario è definito con modalità 12-ore (quando vale 1) oppure 24-ore (se vale 0). Nel caso si utilizzi la modalità a 12 ore il bit 5 (bit AM/PM) viene posto a 1 se è PM, 0 se è AM. Nel caso sia abilitata la modalità 24-ore il 5 bit è usato per la codifica delle decine delle ore. Se si cambia da 12 a 24 ore l'orario deve essere risettato.
- il registro di controllo (08h) è utilizzato per pilotare l'uscita SQ (che è un'onda quadra).Infatti il nostro modulo RTC ha la possibilità di generare un onda quadra di 1Hz, 4096KHz, 8192KHz, 32768KHz

Il modulo utilizza i pin analogici Arduino A4 e A5 per la comunicazione seriale "I2C" (impostazione predefinita per la libreria <wire.h>).

Il DS1307 supporta il protocollo I2C. Una device che invia i dati sul bus è definita trasmettitore mentre quella che li legge ricevitore. La device che controlla le altre, dette slave, è detta master e deve generare il segnale di clock, controllare l'accesso al bus e generare le condizioni di START e STOP. Il DS1307 opera come slave sul bus I2C.

Ecco lo schema del circuito utilizzato per gli esempi successivi

Codice Sorgente A

Obiettivo progetto: Utilizzando il circuito precedente mostrare sul serial monitor l'orario attuale dell' RTC

soluzione: Ecco il codice che risolve il nostro problema. La soluzione utilizza la libreria di sistema wire.h (vedi manuale online http://arduino.cc/en/Reference/Wire) che consente di utilizzare il bus I2C.

Nella soluzione sono state utilizzate le struct. L'idea è quella di unificare in un'unica variabile una serie di informazioni tra loro correlate.

In C quindi le due implementazioni sono equivalenti:

tipoA variabile1;
tipoB variabile2;
...
tipoK variabileK;
...
tipoR funzione(tipoA v1,... tipoK vk)
{
   ...
   v1=valore1;
   V2=valore2;
   ...
   VK=valoreK;
}
typedef struct
{
   tipoA campo1;
   tipoB campo2;
   ...
   tipoK campoK;
} TipoStruttura;
...
tipoR funzione(TipoStruttura V)
{
   ...
   V.campo1=valore1;
   V.campo2=valore2;
   ...
   V.campoK=valoreK;
}

E' possibile definire la struttura anche in questo modo:

struct TipoStruttura
{
   tipoA campo1;
   tipoB campo2;
   ...
   tipoK campoK;
};

Le strutture (struct) sono preferibili poichè posso gestire all'interno di un'unica variabile un insieme di informazioni che risultano tra loro correlate.

Contrariamente ai compilatori C++ il preprocessore dell'IDE di arduino, durante la compilazione, genera i prototype di forwarding prima dellla dichiarazioni delle strutture per cui non è possibile utilizzarle come tipo nei parametri passati ad una funzione (esempio tipoR funzione(TipoStruttura V)).

Per evitare il problema occorre seguire una di queste modalità:
1) mettere la dichiarazione in un namespace (vedi codice sorgente B)
2) inserire la dichiarazione in un opportuno header file .h
3) passare alla funzione un puntatore a void tipoR funzione(void *V) ed effettuare all'interno della funzione un cast alla struttura desiderata TipoStruttura D=(TipoStruttura *)V (vedi codice sorgente A)
4) implementare la funzione come macro in modo che l'IDE di arduino non la riconosca come tale.

/* ------------------------------------------------------- 
 * Lettura un RTC con DS1307
 * ------------------------------------------------------- */
#include "Wire.h"

#define DS1307_ADDRESS 0x68 // Indirizzo dell'integrato DS1307 
                            // come indicato nel datasheet
byte zero = 0x00;
struct tDataOra
{
  byte second;   //0-59
  byte minute;   //0-59
  byte hour;     //0-23
  byte weekDay;  //1-7
  byte monthDay; //1-31
  byte month;    //1-12
  byte year;     //0-99
};

// Converte dalla notazione BCD alla notazione in base 10
byte bcdToDec(byte val)  
{
  // val % 16 => corrisponde ai 4 bit a destra di un byte
  // (val - val % 16)/ 16 => corrisponde ai 4 bit a sinistra di un byte
  // ovvero a shiftare verso destra di 4 bit quindi 
  // (val-val%16)/16 equivale a val >> 4
  return ( (val-val%16)/16*10 + val%16 );
}

boolean getDateTime(void *V)
{
  tDataOra *D=(tDataOra *)V;
  // inizializzo la trasmissione partendo
  // dall'indirizzo 0x00
  Wire.beginTransmission(DS1307_ADDRESS);
  Wire.write(zero);
  Wire.endTransmission();

  // requestFrom e' utilizzato dal master per richiedere 
  // n byte alle device slave. In questo caso ne richiede
  // 7 bytes alla device con l'indirizzo indicato 
  // il DS1307 utilizza 56 bit (7byte) per registrare la data/ora
  Wire.requestFrom(DS1307_ADDRESS, 7);
  if (Wire.available())
  {
      D->second = bcdToDec(Wire.read());
      D->minute = bcdToDec(Wire.read());
      D->hour = bcdToDec(Wire.read() & 0b111111); // modo 24 ore
                                                  // considero i primi 6 bit
      D->weekDay = Wire.read(); // non mi serve convertire (Range da 1 a 7 => 3 bit) 
      D->monthDay = bcdToDec(Wire.read());
      D->month = bcdToDec(Wire.read());
      D->year = bcdToDec(Wire.read());
      return true;
  }
  return false;
}

void printShortDateTime(void *V)
{
  char buffer[25]={'\0'};
  tDataOra *D=(tDataOra *)V;
  sprintf(buffer, "%02d/%02d/20%02d %02d:%02d:%02d", 
                  D->monthDay,D->month,D->year,
                  D->hour, D->minute, D->second);
  Serial.println(buffer);
}

void printLongDateTime(void *V)
{
   const char* days[] =  { "Domenica", "Lunedi'", "Martedi'", "Mercoledi'", 
                           "Giovedi'", "Venerdi'", "Sabato" };
   const char* months[] = { "Gennaio", "Febbraio", "Marzo", "Aprile", 
                            "Maggio", "Giugno", "Luglio", "Agosto",
                            "Settembre", "Ottobre", "Novembre", "Dicembre"};
  char buffer[40]={'\0'};
  tDataOra *D=(tDataOra *)V;

  sprintf(buffer, "%s, %u %s 20%2u %02u:%02u:%02u", 
                  days[D->weekDay-1], D->monthDay, months[D->month-1],D->year,
                  D->hour, D->minute, D->second);
  Serial.println(buffer);
}

void setup()
{
  Wire.begin(); // Si connette al bus i2c
  Serial.begin(9600);
}

void loop()
{
  tDataOra *D = new tDataOra();
  if (getDateTime(D))
  {
      printLongDateTime(D);
      printShortDateTime(D);
  }
  else
     Serial.println("Errore di lettura Data/ora");
  delay(1000);
}

Si consiglia ora di consultare alcune note sintattiche, relative all'esempio appena analizzato, accessibili cliccando qui.  Aggiungiamo le seguenti conclusioni:

>> Sulla base delle note appena citate una variabile dichiarata come tDataOra D e una come  tDataOra *P verranno allocate in RAM  in una modalità similare a questa:

Poichè ogni campo della struttura tDataOra occupa un byte, la memoria complessiva necessaria per gestire una variabile di questo tipo sarà di 7 byte. La variabile puntatore è sempre lunga 2 byte poichè deve gestire un indirizzo di memoria. E' possibile verificare la dimensione in byte di una variabile utilizzando la funzione di sistema sizeof().

struct tDataOra
{
  byte second;   //0-59
  byte minute;   //0-59
  byte hour;     //0-23
  byte weekDay;  //1-7
  byte monthDay; //1-31
  byte month;    //1-12
  byte year;     //0-99
};
void setup() 
{
  tDataOra D, *P;
  P=&D;
  Serial.begin(9600);
  Serial.println("Byte tDataOra  = " +String(sizeof(D)));
  Serial.println("Byte *tDataOra = " +String(sizeof(P)));
}

void loop() {}

L'output prodotto sul serial monitor è:

>> Per accedere alla singola voce di una variabile strutturata (come D) occorre aggiungere al nome della variabile il nome del campo specifico separato da un punto. Nel caso la variabile sia un puntatore (come P) come occorre utilizzare -> al posto del punto.

struct tDataOra D, *P;
...
D.second=20;
...
P=&D; // Indirizzo (posizione in RAM) di D
P->Year = 2015;
...

>> La seguente istruzione

struct tDataOra *P;
...
P=new tDataOra();
...

permetta di allocare, durante l'esecuzione del nostro programma (si dice dinamicamente), una quantità di memoria pari a quella richiesta dalla struttura indicata dopo la parola chiave new. L'area allocata ha un indirizzo di inizio che viene salvato nella variabile di tipo puntatore P. La dimensione di tale area viene fissata nella dichiarazione indicando il tipo di dato puntato:

struct tDataOra *P;

In ram P=new tDataOra(); determina una situazione di questo tipo:

>> La sequenza

tDataOra *D = new tDataOra();
if (getDateTime(D))
{
    printLongDateTime(D);
    printShortDateTime(D);
}

poteva essere scritta anche in questo modo:

tDataOra D;
if (getDateTime(&D))
{
    printLongDateTime(&D);
    printShortDateTime(&D);
}

In questo caso lo spazio di memoria non viene assegnato durante l'esecuzione  ma staticamente durante la fase di load della funzione setup();

>> L'istruzione boolean getDateTime(void *V) nella seguente sequenza

boolean getDateTime(void *V)
{
   tDataOra *D=(tDataOra *)V;
   ...
   D->second = bcdToDec(Wire.read());
   ...
}
....
void loop()
{
   tDataOra D;
   if (getDateTime(&D))
      ...
}

permette il passaggio di un puntatore senza specificarne il tipo. Questa tecnica è necessaria poichè il compilatore di Arduino non ammette come parametri di funzione tipi strutturati e neppure puntatori a strutture. { poichè genera i prototipi di forwarding prima dellla dichiarazioni delle strutture per cui non riconosce le struct come tipo nella dichiarazione dei parametri passati ad una funzione (esempio tipoR funzione(TipoStruttura V)) } .  All'interno della funzione getDateTime() è necessario effettuare un'operazione di cast (implementata con la seguente istruzione: tDataOra *D=(tDataOra *)V;) poichè per manipolare il contenuto di un puntatore non basta sapere l'indirizzo ma anche la dimensione della variabile puntata.

Codice Sorgente B

Obiettivo progetto: Utilizzando il circuito precedente impostare l'ora e la data attuale dell'RTC mediante il serial monitor come mostrato in figura.

soluzione: Ecco il codice che risolve il nostro problema. In questo esempio il problema di Arduino, visto in precedenza, che non ammette il passaggio di parametri strutturati o puntatori ad essi (esempio tipoR funzione(TipoStruttura V)) è stato aggirato utilizzando i namespace.

namespace NomeNS
{
    // dichiarazione strutture
    ...
    // dichiarazione variabili
    ...
    // dichiarazione funzione
    ...
}

Per accedere ad una variabile o ad una struttura definita all'interno di un namespace è necessario anteporre al nome della variabile/tipo strutturato il nome del namespace seguito da ::.

NomeNS::nometipo;
...
NomeNS::nomevariabile;
...

/* ------------------------------------------------------- 
 * Lettura ed impostazione di un RTC con DS1307
 * ------------------------------------------------------- */
#include "Wire.h"

#define DS1307_ADDRESS 0x68 // Indirizzo dell'integrato DS1307 
                            // come indicato nel datasheet
byte zero = 0x00;

// Trucco per riuscire a far passare una struttura a una funzione
namespace RTC_ns 
{
    typedef struct
    {
      byte second;   //0-59
      byte minute;   //0-59
      byte hour;     //0-23
      byte weekDay;  //1-7
      byte monthDay; //1-31
      byte month;    //1-12
      byte year;     //0-99
    } tDataOra;
}

byte readByte() 
{
   while (!Serial.available()) delay(10);
   byte reading = 0;
   byte incomingByte = Serial.read();
   while (incomingByte != '\n') 
   {
     if (incomingByte >= '0' && incomingByte <= '9')
         reading = reading * 10 + (incomingByte - '0');
     else;
         incomingByte = Serial.read();
   }
   Serial.flush();
   return reading;
}
    
// Converte dalla notazione in base 10 alla notazione BCD
byte decToBcd(byte val)
{
    // val % 10 => corrisponde alla notazione BCD 
    //             messa nei 4 bit a destra di un byte
    // (val-(val%10))/10*16 => corrisponde lla notazione BCD 
    //                         messa nei 4 bit a sinistra di un byte 
    return ( (val-val%10)/10*16 + val%10 );
}

// Converte dalla notazione BCD alla notazione in base 10
byte bcdToDec(byte val)  
{
    // val % 16 => corrisponde ai 4 bit a destra di un byte
    // (val - val % 16)/ 16 => corrisponde ai 4 bit a sinistra di un byte 
    return ( (val-val%16)/16*10 + val%16 );
}
    
void printShortDateTime(RTC_ns::tDataOra D)
{
  char buffer[25];
  sprintf(buffer, "%02u/%02u/%u %02u:%02u:%02u", 
                  D.monthDay,D.month,D.year+2000,
                  D.hour, D.minute, D.second);
  Serial.println(buffer);
}
void LeggiDataOra(RTC_ns::tDataOra *D)
{
     Serial.println("Cambiare su 'A capo (NL)' l'impostazione \nin basso a destra nel Serial Monitor");
     Serial.println("Digita il giorno del mese (1..31):");
     D->monthDay = readByte();
     Serial.println("Digita il mese (1-12):");
     D->month = readByte();
     Serial.println("Digita l'anno (0-99):");
     D->year = readByte();
     Serial.println("Digita il giorno della settimana (1 - Dom ... 7 - Sab):");
     D->weekDay = readByte();
     Serial.println("Digita l'ora (0-23):");
     D->hour = readByte();
     Serial.println("Digita i minuti (0-59):");
     D->minute = readByte();
     D->second = 0;
     Serial.print("Hai digitato la data:");
     printShortDateTime(*D);
}

void setDateTime(RTC_ns::tDataOra D)
{
    Wire.beginTransmission(DS1307_ADDRESS);
    // il primo byte inviato stabilisce il registro
    // iniziale su cui scrivere
    Wire.write(zero);
    Wire.write(decToBcd(D.second));
    Wire.write(decToBcd(D.minute));
    Wire.write(decToBcd(D.hour));
    Wire.write(decToBcd(D.weekDay));
    Wire.write(decToBcd(D.monthDay));
    Wire.write(decToBcd(D.month));
    Wire.write(decToBcd(D.year));
    Wire.write(zero);  
    Wire.endTransmission();
}

boolean getDateTime(RTC_ns::tDataOra *D)
{
  // inizializzo la trasmissione partendo
  // dall'indirizzo 0x00
  Wire.beginTransmission(DS1307_ADDRESS);
  Wire.write(zero);
  Wire.endTransmission();
  // richiede 7 bytes alla device con l'indirizzo indicato 
  // il DS1307 utilizza 56 bit per registrare la data/ora
  Wire.requestFrom(DS1307_ADDRESS, 7);
  if (Wire.available())
  {
      D->second = bcdToDec(Wire.read());
      D->minute = bcdToDec(Wire.read());
      D->hour = bcdToDec(Wire.read() & 0b111111); // modo 24 ore
                                                  // considero i primi 6 bit
      D->weekDay = Wire.read(); // non mi serve convertire (Range da 1 a 7 => 3 bit) 
      D->monthDay = bcdToDec(Wire.read());
      D->month = bcdToDec(Wire.read());
      D->year = bcdToDec(Wire.read());
      return true;
  }
  return false;
}

void printLongDateTime(RTC_ns::tDataOra D)
{
   const char* days[] =  { "Domenica", "Lunedi'", "Martedi'", "Mercoledi'", 
                           "Giovedi'", "Venerdi'", "Sabato" };
   const char* months[] = { "Gennaio", "Febbraio", "Marzo", "Aprile", 
                            "Maggio", "Giugno", "Luglio", "Agosto",
                            "Settembre", "Ottobre", "Novembre", "Dicembre"};
  char buffer[40];
  sprintf(buffer, "%s, %u %s %u %02u:%02u:%02u", 
                  days[D.weekDay-1], D.monthDay, months[D.month-1],D.year+2000,
                  D.hour, D.minute, D.second);
  Serial.println(buffer);
}

void setup()
{
  Wire.begin(); // Si connette al bus i2c
  Serial.begin(9600);
  RTC_ns::tDataOra D;
  LeggiDataOra(&D);
  setDateTime(D);
  // Leggo una sola volta la data/ora per verifica
  if (getDateTime(&D))
  {
      printLongDateTime(D);
      printShortDateTime(D);
  }
  else
     Serial.println("Errore di lettura Data/ora");
}

void loop() {}

Codice Sorgente C

Obiettivo progetto: Utilizzando le librerie RTCLib ed il circuito precedente impostare l'ora e la data attuale dell'RTC.

soluzione:

Occorre prima di tutto scaricare la libreria RTCLib . Per utilizzarla basta aggiungere il file zip come libreria di arduino mediante il menu "Add Library", all'interno dell'IDE.

Aggiunta libreria

Al termine dell'importazione i files vengono ricopiati nella cartella delle librerie  C:\Users\<User>\Documents\Arduino\libraries. Se ci sono problemi nell'installazione basta scompattare il file zip e porre manualmente la cartella appena generata all'interno della cartella delle librerie appena citata.

Ecco il codice che risolve il nostro problema

#include "Wire.h"
#include "RTClib.h"

RTC_DS1307 RTC;

void setup () 
{
    Serial.begin(9600);
    Wire.begin();
    RTC.begin();
    // Se l'ora e' errata la aggiorna 
    // con quella del computer
    DateTime OraRTC = RTC.now();
    DateTime OraPC = DateTime(__DATE__, __TIME__);
    if (OraRTC.unixtime() < OraPC.unixtime())
       Serial.println("L'RTC e' indietro! Lo aggiorno con quello del PC!");
    RTC.adjust(OraPC);
    if (! RTC.isrunning())
        Serial.println("RTC is NOT running!");
}

void printShortDateTime(DateTime now)
{
  char buffer[25]={'\0'};
  sprintf(buffer, "%02u/%02u/%u %02u:%02u:%02u", 
                  now.day(),now.month(),now.year(),
                  now.hour(), now.minute(), now.second());
  Serial.println(buffer);
}

void printLongDateTime(DateTime now)
{
   const char* days[] =  { "Domenica", "Lunedi'", "Martedi'", "Mercoledi'", 
                           "Giovedi'", "Venerdi'", "Sabato" };
   const char* months[] = { "Gennaio", "Febbraio", "Marzo", "Aprile", 
                            "Maggio", "Giugno", "Luglio", "Agosto",
                            "Settembre", "Ottobre", "Novembre", "Dicembre"};
  char buffer[40];

  sprintf(buffer, "%s, %u %s %u %02u:%02u:%02u", 
                  days[now.dayOfWeek()-1], now.day(), months[now.month()-1],now.year(),
                  now.hour(), now.minute(), now.second());
  Serial.println(buffer);
}

void loop () 
{
    printLongDateTime(RTC.now());
    printShortDateTime(RTC.now());
    delay(1000);
}

Esercitazione pratica 1:

Obiettivo progetto: Implementare un circuito che utilizzando un keypad, un display LCD 20x4 e un modo RTC consenta di:
- visualizzare la data e l'ora corrente sul display LCD
- Impostare l'ora mediante il tasto * e confermarlo mediante la pressione ulteriore di *. Usare il tasto # per annullare la modalità "modifica".
- Impostare la data mediante il tasto # e confermare mediante la pressione di un ulteriore #. Usare il tasto * per disattivare la modalità "modifica".
Impostare opportuni controlli per  impedire di settare ore e date assurde.

soluzione:

Occorre prima costruire il seguente circuito.

Prima di iniziare a scrivere il codice immaginiamo il contenuto del display a secondo delle diverse situazioni:

VIDEATE STANDARD
Visualizzazione data e ora Digitazione della nuova data
MESSAGGI DI ERRORE
 
Avviso quando digito un'ora errata Avviso quando invio una data errata

MESSAGGI DI AVVENUTO SETTAGGIO
Conferma settaggio dell'ora Conferma settaggio della data

Ecco il codice che risolve il nostro problema

/* --------------------------------------------------------------------------
 Imposto l'orario su un modulo RTC (con interfaccia seriale I2C) tramite il 
 keypad (# per settare la data e * per settare l'orario) e poi lo mostro su
 un display LCD 20x4 con interfaccia seriale I2C "YwRobot Arduino LCM1602 
 IIC V1". Il pin SDA di I2C è connesso alla pin analogico A4 mentre SCL e' 
 connesso alla pin analogico A5
 -------------------------------------------------------------------------- */
#include "Wire.h"  // E' richiesta da I2CIO.cpp

// *************************************************************
// IMPOSTAZIONE PER L'OROLOGIO
// *************************************************************
#include "RTClib.h"
RTC_DS1307 RTC;

// *************************************************************
// IMPOSTAZIONE PER IL KEYPAD
// *************************************************************
#include "Keypad.h"

const byte ROWS = 4; //4 righe
const byte COLS = 3; //3 colonne
char keys[ROWS][COLS] = 
{
  {'1','2','3'},
  {'4','5','6'},
  {'7','8','9'},
  {'*','0','#'}
};
byte rowPins[ROWS] = {8, 7, 6, 5}; // Pin di arduino connessi ai pin 1,2,3 e 4 delle righe del keypad
byte colPins[COLS] = {4, 3, 2};    // Pin di arduino connessi ai pin 5,6,7 delle righe del keypad
Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );

// *************************************************************
// IMPOSTAZIONI PER IL DISPLAY LCD
// *************************************************************
#include "LiquidCrystal_I2C.h" // Libreria LCD I2C
// Imposta i pin usati sul chip I2C per le connessioni 
// con il display LCD: addr, en, rw ,rs ,d4 ,d5 ,d6 ,d7 ,bl ,blpol
// ed inoltre pone l'indirizzo del display a 0x27
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);

// -------------------------------------------------------------
// PROGRAMMA VERO E PROPRIO
// -------------------------------------------------------------
// Variabili globali necessarie al nostro progetto.
#define StringaVuota "                    ";
String cifre="";
boolean AttivaModificaData;
boolean AttivaModificaOra;

void ScriviRiga3e4(String Frase3, String Frase4)
{
   if (Frase3!="")
   {
      lcd.setCursor(0, 2);  // 1° colonna - 3° riga
      lcd.print(Frase3); 
   }
   if (Frase4!="")
   {
      lcd.setCursor(0, 3);  // 1° colonna - 4° riga
      lcd.print(Frase4);
   } 
}

void Pulisci3e4Riga()
{
    String Vuota="                    ";
    ScriviRiga3e4(Vuota,Vuota);
}

void Pulisci4Riga()
{
    String Vuota="                    ";
    ScriviRiga3e4("",Vuota);
}

void MessaggioAvviso(String Frase1, String Frase2)
{
    String Frase3="...:: "+Frase1+" ::...";
    String Frase4=">> "+Frase2;
    ScriviRiga3e4(Frase3,Frase4);
}

void printLCD_DateTime(DateTime now)
{
  char buffer[25]={'\0'};
  sprintf(buffer, "DATA: %02u/%02u/%u",now.day(),now.month(),now.year());
  lcd.setCursor(0, 0);  // 1° colonna - 1° riga
  lcd.print(buffer); 
  sprintf(buffer, "ORA:  %02u:%02u:%02u",now.hour(), now.minute(), now.second());
  lcd.setCursor(0, 1);  // 1° colonna - 2° riga
  lcd.print(buffer); 
}

boolean EUnaDataCorretta(uint8_t dd, uint8_t mm, uint16_t yy)
{
  uint8_t GGMese[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
  if ((mm<1) || (mm>12)) return false; // non e' un mese!
  if (yy % 4 == 0) GGMese[1]=29; // Bisestile
  if (dd<1 || dd>GGMese[mm-1]) return false;
  return true;
}

boolean EUnOraCorretta(uint8_t hh, uint8_t mm, uint8_t ss)
{
  if ( (hh>23) || (mm>59) || (ss>59) ) return false;
  return true;
}


void AnnullaSet()
{
   AttivaModificaData=false;
   AttivaModificaOra=false;
   cifre="";
}

void Registra()
{
   DateTime DataOraRTC = RTC.now();
   DateTime DataOraNuova;
   uint8_t dd, mm, hh, ss;
   uint16_t yy;
   if (cifre.length()!=6) return; // ignoro # o * prima delle 6 cifre
   if (AttivaModificaOra==true)
   {
       hh=(uint8_t)cifre.substring(0,2).toInt();
       mm=(uint8_t)cifre.substring(2,4).toInt();
       ss=(uint8_t)cifre.substring(4,6).toInt();
       if ( EUnOraCorretta(hh,mm,ss) )
       {
         DataOraNuova=DateTime(DataOraRTC.year(),DataOraRTC.month(),
                               DataOraRTC.day(),hh,mm,ss);
         RTC.adjust(DataOraNuova);
         MessaggioAvviso("SET ORA ","Orario settato!");
       }
       else
         MessaggioAvviso("SET ORA ",cifre.substring(0,2)+":"
                                   +cifre.substring(2,4)+":"
                                   +cifre.substring(4,6)+" ERRATA!");
   }
   else if (AttivaModificaData==true)
   {
       dd=(uint8_t)cifre.substring(0,2).toInt();
       mm=(uint8_t)cifre.substring(2,4).toInt();
       yy=(uint16_t)(2000+cifre.substring(4,6).toInt());
       if ( EUnaDataCorretta(dd,mm,yy) )
       {
           DataOraNuova=DateTime(yy,mm,dd,DataOraRTC.hour(),
                                 DataOraRTC.minute(),DataOraRTC.second());
           RTC.adjust(DataOraNuova);
           MessaggioAvviso("SET DATA","Data settata!");
       }
       else
         MessaggioAvviso("SET DATA",cifre.substring(0,2)+"/"
                                   +cifre.substring(2,4)+"/"
                                   +cifre.substring(4,6)+" ERRATA!");
   }
   AnnullaSet();
}

void StampaConSeparatore(String cifr)
{
    int i;
    char sep=(AttivaModificaData) ? '/' : ':';
    Pulisci4Riga();
    lcd.setCursor(0,3);
    lcd.print(">> IN: ");
    for (i=0; i registro
           Registra();
         else if (AttivaModificaOra)
         {
           AnnullaSet();  // Azzero l'impostazione della data
           Pulisci3e4Riga();
         }
         else
         {
           AttivaModificaData=true;
           cifre="";
           MessaggioAvviso("SET DATA","IN: GG:MM:AA");
         }
      }
      else if (key=='*') // Imposta orario
      {
         if (AttivaModificaOra) // 2° * => registro
           Registra();
         else if (AttivaModificaData)
         {
           AnnullaSet();  // Azzero l'impostazione dell'ora
           Pulisci3e4Riga();
         }
         else
         {
           AttivaModificaOra=true;
           cifre="";
           MessaggioAvviso("SET ORA ","IN: HH:MM:SS");
         }
      }
      else if (AttivaModificaOra || AttivaModificaData)
      {
        if (cifre.length()<6) // non vado oltre le 6 cifre
        {
            cifre=cifre+String(key);
            StampaConSeparatore(cifre);
        }
      }
    }
    delay(20);
}