Orologio/datario LCD con Arduino e RTC

Stampa
( 1 Vote ) 
Valutazione attuale:  / 1
ScarsoOttimo 
Categoria principale: Elettronica Categoria: Microcontrollori
Data pubblicazione
Scritto da MGuruDC Visite: 13736

Calendario perpetuo + regolazione touch + encoder molto particolare

Come abbiamo già spiegato Arduino oramai è molto usato e famoso. Poco tempo fa ho scoperto che viene usato anche nelle università italiane, specialmente nei corsi che non riguardano ingegneria o informatica, dove c'è bisogno di implementare modelli e progetti anche senza avere specifiche competenze in materia di elettronica o programmazione.
Personalmente resto dell'idea che anche con una piattaforma così semplice(ma potente) c'è comunque bisogno di una certa coscienza di ciò che si sta facendo... insomma senza conoscere l'elettronica e la programmazione non si può diventare un "power user" e sfruttare a pieno tutte le potenzialità della scheda.

Questa volta mi è venuta incontro la comunità open-source(e open hardware) che oramai ha sviluppato una quantità impressionante di librerie e moduli da usare con Arduino.

Tempo fa avevo deciso di comprare un integrato RTC per sperimentare, ma per parecchio tempo l'integrato è rimasto inerme nel cassetto del componenti.
Quando finalmente mi sono deciso a usarlo ho scaricato il datasheet per cominciare a documentarmi e a creare delle API per usare l'RTC. Per curiosità poi ho cercato su Google per vedere se qualcun altro prima di me avesse usato quel determinato integrato con Arduino e con una certa sorpresa ho scoperto che non solo l'integrato era stato già usato, ma che c'era una completa e funzionante libreria apposita.

Dopo aver testato il funzionamento dell'RTC e della sua libreria ho pensato di fargli stampare l'orario sul display LCD che ho usato nell'articolo precedente. Questa volta però volevo implementare una libreria come si deve per l'uso del display LCD. Come prima cosa sono andato a sbirciare nei codici della libreria "liquidCrystal" già integrata nell'SDK di Arduino, e confrontando il datasheet del display e le maschere preimpostate nella libreria ho verificato che almeno le principali sono identiche... niente da fare, non mi hanno fatto scrivere neanche una riga di codice a basso livello, era tutto già fatto!

Beh dopo aver stampato l'orario e la data sul display non restava che regolare l'orario. Per fare ciò servono almeno due pulsanti(ok e u/d)... che proprio non avevo. Mi sono ingegnato e ho trovato una soluzione abbastanza carina...
Per l'OK ho usato un corto spezzone di filo collegato a un ingresso analogico dell'Arduino. Basta toccare il filo e l'Arduino tramite la lettura di una soglia analogica può capire se è stato "premuto" il pulsante. Il nostro corpo infatti si comporta come una R da 1M collegata e terra, quindi in caso di contatto il valore che il pin legge "in aria" cala drasticamente.
Dopo un po' di prove però ho visto che questo metodo funziona ma non è molto affidabile, basta ad esempio collegare un filo troppo lungo per captare le interferenze che vengono dagli altri apparecchi elettronici, quindi per essere affidabile deve avere un'isteresi altissima.
Il pulsante U/D invece deve essere molto più affidabile, quindi il pulsante "touch" non va bene. Girando per casa in cerca di un soluzione mi sono trovato tra le mani un piccolo motorino DC, di quelli che si usano nei giocattoli... e l'ho usato come encoder XD.
Il motore DC a spazzole e magneti permanenti infatti è in grado di generare corrente elettrica quando il suo albero motore viene messo in moto da un agente esterno.
La corrente generata ha una direzione che dipende dal senso in cui si fa ruotare l'albero. Questo aspetto dipende solo ed esclusivamente dall'architettura interna del motore, quindi bisogna vedere caso per caso come si comporta.
Il piccolo motore che ho usato, messo in moto con due dita genera una tensione di circa 0.2V(misurata col tester digitale, quindi su un carico da 1M), più che sufficiente per essere rilevata dall'ADC dell'Arduino.
Vediamo nel dettaglio

        Elettronica

L'RTC che ho scelto è il DS1302 prodotto dalla Maxim. Questo integrato non offre particolari prestazioni rispetto a quelli della concorrenza e in realtà ha anche un prezzo un po' troppo alto(3,20€), ma quando l'ho ordinato era l'unico in catalogo ad avere un contenitore DIL adatto per gli esperimenti su breadboard.
Questo integrato come la maggior parte degli RTC supporta la doppia alimentazione, memorizza in differenti registri la data già formattata(valori che esprimono anno-mese-giorno-ora anziché un timestamp) ed ha un'interfaccia seriale, quindi può essere controllato con solo 3 fili e per funzionare richiede solo un quarzo da 32,768kHz.
Il display è quello del precedente articolo ma questa volta ho usato la modalità 4bit, poiché usare 10pin solo per il display stavolta avrebbe creato troppo disordine sulla breadboard.
Ecco lo schema elettrico



L'immagine è stata ridimensionata. Clicca qui per vedere l'immagine ingrandita. Le dimensioni originali sono 800x605

L'immagine è stata ridimensionata. Clicca qui per vedere l'immagine ingrandita. Le dimensioni originali sono 1200x375

Come potete vedere il motore è collegato a un partitore resistivo che dimezza la tensione di alimentazione. L'altro polo invece entra nell'ingresso analogico 5 dell'Arduino tramite una R di limitazione da 10k(ci vorrebbe anche uno zener di protezione ma non ce l'ho... attenzione a no girare troppo forte XD). Ecco cosa succede:
- Se il motore è fermo... è come se non ci fosse, i 2,5V del partitore passano nell'avvolgimento del motore e arrivano invariati all'Arduino;
- Se il (mio)motore gira in senso orario genera una corrente che va dal polo collegato al partitore all'Arduino, questa corrente si somma a quella che scorre normalmente dal partitore all'Arduino, quindi l'ADC leggerà una tensione leggermente più alta di 2,5V... 2,5 + x, non ci importa molto quanto sia di preciso.
- Se il (mio)motore gira in senso antiorario genera una corrente inversa a quella che scorre dal partitore all'Arduino che verrà ridotta, quindi il convertitore leggerà una tensione di 2.5 - x.
Leggendo il valore sulla porta analogica possiamo quindi capire se il motore è fermo e in che senso sta girando.

        Software

La lettura dei dati dall'RTC e la scrittura sul display è relativamente semplice, tutte le funzione a basso livello svolgono egregiamente il loro lavoro.
La parte difficile riguarda la regolazione della data e il controllo di validità della data. Diamo un'occhiata al codice.

Definizioni e macro 

#include 
#include 
#include 
#include 

#define OK    analogRead(4) < 200
#define CURD  analogRead(5) > 542
#define CURS  analogRead(5) < 478

#define ok  1
#define sx  2
#define dx  3

#define I  true
#define D  false


Nel primo blocco richiamiamo le librerie necessarie. Oltre a "LiquidCrystal" per l'LCD e "DS1302" per la gestione dell'RTC ci servono anche "stdio" e "string" per gestire i dati.
Nel secondo blocco dichiariamo tre macro che ci servono per la lettura del pulsante touch e del motore. Sono semplicemente le condizioni che sono vere quando c'è un input. I numeri espressi dopo l'operatore sono critici per la sensibilità, in base al filo o al motore che usiamo questi numeri vanno cambiati per avere un comportamento corretto. Per l'"OK" bisogna prendere una soglia abbastanza alta poiché a volte il nostro corpo fa anche un "effetto condensatore", quindi ci mette un  po' di tempo prima di portare il pin a un valore prossimo allo 0.
Per il motore, dobbiamo ricordare che la tensione in ingresso quando il motore è fermo è di 2,5V, quindi l'ADC(a 10bit, non dimentichiamo) leggerà un valore che si aggira intorno ai 512. Scegliendo una soglia in positivo e in negativo potremo discriminare il  movimento del motore. Nel mio caso la soglia scelta è 20, quindi la tensione sul pin A5 deve variare di circa 100mV perché il software intercetti il movimento del motore.
I due blocchi successivi definiscono delle costanti che servono alle procedure successive.

Costruttori e variabili globali
LiquidCrystal lcd(6, 7, 5, 4, 3, 2);
DS1302 rtc(11, 12, 13);

char buf[17];
Time t;
const char giorni[][4] = {"Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab"};
const char mesi[][4] = {"Gen", "Feb", "Mar", "Apr", "Mag", "Giu", "Lug", "Ago", "Set", "Ott", "Nov", "Dic"};


Ecco i costruttori per avviare le librerie LiquidCrystal e DS1302.
I valori in ingresso sono semplicemente i pin usati, e per LiquidCrystal esprimono rispettivamente E, RS, DB7, DB6, DB5, DB4, per DS1302 invece sono SCK, SDA, CE.
"buf" è una stringa che useremo tra poco per formattare il testo da inviare al display;
"t" è una struttura globale(umm... in realtà è un oggetto) che useremo per fare tutte le operazioni sull'orario e sulla data. Ecco come è composta:
uint16_t yr
uint8_t mon
uint8_t date
uint8_t hr
uint8_t min
uint8_t sec
uint8_t day

Gli array "giorni" e "mesi" contengono i nomi abbreviati dei giorni della settimana e dei mesi dell'anno e verranno usati sempre per creare il testo da stampare sull'LCD.
Chi è pratico di programmazione saprà che le variabili globali andrebbero usate il meno possibile, tuttavia in questo caso andiamo ad usare registri abbastanza grandi per il µC, quindi è meglio allocarli una volta sola.

Setup
void setup() {
  rtc.write_protect(false);
  rtc.halt(false);
  Time t(2010, 11, 1, 0, 0, 0, 1);
  rtc.time(t);
  lcd.begin(16, 2);
  }


Ecco le istruzioni di inizializzazione. Togliamo la protezione di scrittura all'RTC, Lo avviamo e gli scriviamo dentro la data di inizio standard(ho scelto 2010-11-1 0:0:0 Lun). Per scrivere la data prima di tutto memorizziamo i vari parametri nella struttura t, poi li inviamo all'RTC con la funzione "rtc.time()". Fatto ciò inizializziamo anche il display e siamo pronti.

Loop
void loop() {
  if(OK) {
    delay(500);
    regola();
    }
  t = rtc.time();
  print_timeD(t);
  print_timeH(t);
  delay(1000);
  }


Molto semplice...
Come prima cosa controlliamo se è stato "premuto" il tasto OK, se così fosse diamo una pausa di mezzo secondo come isteresi e invochiamo la procedura per regolare l'ora.
Fatto ciò, memorizziamo l'ora nella struttura t con rtc.time(), stampiamo la data sulla prima riga del display con "print_timeD", poi l'orario sulla seconda riga con "print_timeH" e poi va tutto in pausa per 1 secondo.

Funzioni e procedure
void regola() {

  print_timeD(t);
  print_timeH(t);

  regStep('d', 4, 0);
  regStep('M', 7, 0);
  regStep('y', 11, 0);
  regStep('h', 0, 1);
  regStep('m', 3, 1);
  regStep('s', 6, 1);

  t.yr = 2000 + (t.yr % 100);
  rtc.time(t);
  }


Questa funzione per prima cosa aggiorna i dati sul display e poi chiama la procedura "regStep()" che si occupa della regolazione dei vari campi.
Cambiano l'ordine delle chiamate a regStep cambiamo l'ordine con cui vengono regolati i vari parametri della data. Ho scelto di regolare giorno - mese - anno - ora - minuti - secondi, penso si possa anche scegliere una regolazione più comoda, oppure implementare una routine per renderla più facile.
Dopo aver acquisito i dati bisogna scriverli sull'RTC, ma prima di farlo vado a troncare gli anni in modo da avere sempre un valore compreso tra 2000 e 2100, l'RTC infatti memorizza valori BDC compresi un quest'intervallo. In pratica durante la regolazione non abbiamo limiti per l'anno, poi quando andiamo a memorizzare i dati dobbiamo fare questo troncamento.
I caratteri che prende in ingresso la funzione regStep sono degli identificatori che indicano il campo da regolare, ecco la legenda:

d giorno del mese
M mese dell'anno
y anno
h ora
m minuto
s secondo
Il secondo e il terzo parametro esprimono la posizione del valore sul display, servono solo a posizionare il cursore.


void regStep(char campo, short x, short y) {
  delay(1000);
nReg:
  lcd.setCursor(x, y);
  switch(input()) {
    case ok:
      return;
    case dx:
      regDate(campo, I);
      print_timeD(t);
      print_timeH(t);
      delay(100);
      goto nReg;
    case sx:
      regDate(campo, D);
      print_timeD(t);
      print_timeH(t);

      delay(100);
      goto nReg;
    }
  }


La procedura "regStep()" come prima cosa posiziona il cursore al punto giusto del display, dopodiché dà il polling degli input e sceglie l'operazione da fare:
- con l'ok la funzione esce;
- se viene girato il motore viene chiamata la funzione "regDate" con il campo scelto e l'operazione, con "I" incrementa il valore, con "D" decrementa, dopodiché aggiorna il display. Dopo una pausa di 100mS la funzione torna all'inizio per ascoltare il nuovo input.
Qualcuno con quel "goto" storcerà il naso, ma a un'attenta analisi è la soluzione migliore dato che la ricorsione è meglio lasciarla ai processori con parecchi MB di RAM.
short input() {
  boolean uno, due, tre, out;
  lcd.blink();
  while(!((uno = OK) || (due = CURS) || (tre = CURD))) {
    }
  if(uno) {
    out = ok;
    }
  else if(tre) {
    out = dx;
    }
  else if(due) {
    out = sx;
    }
  lcd.noBlink();
  return out;
  }


La funzione input fa il polling dei possibili input e quando ne trova uno lo restituisce. Forse si può ottimizzare ancora il codice...
void regDate(char campo, boolean op) {
  switch(campo){
    case 'd':
      adj(&t.date, NULL, 1, lDay(), op);
      break;
    case 'M':
      adj(&t.mon, NULL, 1, 12, op);
      if(t.date > lDay()) {
        t.date = lDay();
        }
      break;
    case 'y':
      adj(NULL, &t.yr, 0, 65535, op);
      break;
    case 'h':
      adj(&t.hr, NULL, 0, 23, op);
      break;
    case 'm':
      adj(&t.min, NULL, 0, 59, op);
      break;
    case 's':
      adj(&t.sec, NULL, 0, 59, op);
      }

  short tab[2][12] = {0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5, 6, 2, 4, 6, 1, 4, 6, 2, 5, 0, 3, 5};
  int M = tab[!(t.yr % 4)][t.mon - 1];
  int C = 6 - (2 * ((int)(t.yr / 100) % 4));
  t.day = (t.date % 7 + M + (t.yr % 100) % 28 + (int)((((t.yr % 100) % 28) - 1)/4) + C) % 7;
  }


"regDate" è la routine che controlla la validità della data e in più calcola il giorno della settimana tramite l'algoritmo del calendario perpetuo. Selezionando il campo da regolare viene chiamata la proceduta "adj" a cui vengono passati il campo da regolare(questa volta il vero e proprio campo, non un riferimento), i valori minimi e massimi che più assumere e il tipo di operazione richiesta.
Purtroppo all'interno della funzione adj ho avuto un problema di cast tra puntatori, quindi sono stato costretto a dividere in due il parametro per il campo da regolare, il primo è per i valori a 8bit, il secondo per quelli a 16bit(solo l'anno in questo caso). In pratica se vogliamo regolare un campo a 8bit il secondo valore deve essere "NULL", se invece regoliamo un campo a 16bit il primo valore deve essere nullo e il secondo invece sarà il puntatore al campo.
Da notare che quando regoliamo il mese dobbiamo anche ricontrollare la data, per impedire l'inserimento di date tipo il 31 Aprile o il 30 Febbraio.
Dopo la regolazione dei campo andiamo a calcolare il giorno della settimana con l'algoritmo del calendario perpetuo. "tab" contiene una tabella di valori che cambiano in base al mese e all'anno bisestile, la struttura dell'array ci permette un accesso veloce al valore giusto. Il calcolo è ridotto quasi all'osso(e si può ridurre ancora), quindi per chi volesse capire come funziona... meglio cercare altrove XD.
short lDay() {
  short ms[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  if(!(t.yr %4)) {
    ms[1] = 29;
    }
  return ms[t.mon - 1];
  }


"lDay" è una semplicissima funzione che restituisce il massimo numero di giorni del mese attualmente memorizzato nella struttura t. Per farlo semplicemente interroga un array, ma prima di farlo si accerta che l'anno non sia bisestile, in questo caso il valore corrispondente a Febbraio viene corretto.
void adj(uint8_t *campo, uint16_t *campoG, int min, int max, boolean op) {
  if(campoG == NULL) {
    if(op){
      if((*campo)== max) {
        (*campo) = min;
        }
      else {
        (*campo) ++;
        }
      }
    else{
      if((*campo) == min) {
        (*campo) = max;
        }
      else {
        (*campo) --;
        }
      }
    }
  else {
    if(op){
      if((*campoG)== max) {
        (*campoG) = min;
        }
      else {
        (*campoG) ++;
        }
      }
    else{
      if((*campoG) == min) {
        (*campoG) = max;
        }
      else {
        (*campoG) --;
        }
      }
    }
  }


"adj" è la funzione che va effettivamente ad alterare i valori della struttura t. Come ho detto prima il casting tra i puntatori "uint8_t" e "uint16_t" ha un comportamento fuori standard in questa implementazione del C(AVR C appunto), quindi sono stato a malincuore costretto a sdoppiare la funzione  e far eseguire uno dei due blocchi a condizione di quale puntatore sia stato passato e quale no.
A ogni modo l'algoritmo è semplice:
- se il campo da alterare è pari al suo valore massimo e vuole essere incrementato viene riportato al suo minimo;
- se il campo non è uguale al suo massimo viene incrementato di 1 unità;
Viceversa se si chiede di decrementare.
void print_timeD(Time t) {
  lcd.setCursor(0, 0);
  sprintf(buf, "%s %02d %s %04d", giorni[t.day], t.date, mesi[t.mon - 1], t.yr);
  lcd.print(buf);
  }


void print_timeH(Time t) {
  lcd.setCursor(0, 1);
  sprintf(buf, "%02d:%02d:%02d",t.hr, t.min, t.sec);
  lcd.print(buf);
  }


Queste due funzioni si occupano di stampare sull'LCD i dati. Nelle prime versioni dello sketch mi faceva comodo dividere l'aggiornamento di data e ora, allo stato attuale è inutile ma... è rimasto così...
Le due funzioni prima di tutto posizionano il cursore sull'LCD, poi tramite "sprintf" scrivono nel buffer la stringa da stampare secondo la formattazione desiderata, poi inviano la stringa buf al display...il quale finalmente la visualizza.

Fine!
Le prime versioni del codice superavano abbondantemente le 300 righe, dopo una serie di ottimizzazioni sono riuscito a scendere al di sotto delle 190, tuttavia adesso l'interpretazione del codice potrebbe risultare un po' più difficile. In ogni caso meglio ottimizzare, dato che con questa versione ho ridotto le dimensioni dell'eseguibile a 8KB... Sul nostro Arduino2009 quindi ci sono ancora 24KB disponibili.

        Immagini

Che giorno sarà il 22 Marzo 2087? Chiedetelo all'Arduino, sarà Domenica...
L'immagine è stata ridimensionata. Clicca qui per vedere l'immagine ingrandita. Le dimensioni originali sono 800x247


Ecco un video del normale funzionamento(lettura e visualizzazione dell'ora e della data).
Ecco un video della regolazione della data e dell'ora con il calcolo del giorno della settimana, da notare il troncamento del dato anno che avviene alla memorizzazione(da 1991 passa automaticamente a 2091).

È tutto, spero vi sia piaciuto.
Alla prossima
Guru
Joomla 1.7 Templates designed by College Jacke