Arduino timers

Arduino dispone di parecchi timer interni, che l'API pero' non valorizza adeguatamente. Per tale ragione si finisce sempre per leggere codice criptico basato sull'hacking dei registri ATMega, spesso con spiegazioni sommarie.

Scrivo questo articolo perche' sembra oramai  impossibile trovare spiegazioni chiare e decenti su questo argomento,  senza che siano zeppe di pubblicita', anche se qualche sito si salva.

Come sapete, su Gioorgi.com l'unica pubblicita' che vedrete sono i rari box dei prodotti Amazon, che pubblicizzo piu' che altro per fornirvi un 'servizio' che per guadagnarci qualche cosa  con le relative commissioni, e i link ai mei pochi libri/prodotti.
Andiamo ad iniziare.

Arduino uno e i timer: intro

NB: Le seguenti informazioni sono valide solo per Arduino Uno basato su ATMega (per cui R1-R3 e NON R4, per cui ci riserviamo un articolo quando sara' sufficientemente diffuso).
Gli interrupt sono delle 'interruzioni' che esistono quasi da quando esistono i chip (!) e servono per gestire eventi che richiedono una pronta risposta, siano essi episodici o periodici.

Nell' ATMega328p ci sono diversi modi per generare interrupt hardware: possono anche essere generati dal cambio di tensione di alcuni pin (es 2,3) da un timer speciale chiamato Watchdog (WDT), dalle porte UART/SPI/I2C/ADC ecc... insomma non ve ne sono solo paio. come nei vecchi computer a 8bit :)

Il chip ATMega328P su cui e' basato ArduinoUno, dispone di tre timer hardware, chiamati

  • Timer0 a 8bit
  • Timer1 a 16 bit (e difatti composto da due registri a 8bit)
  • Timer2 a 8 bit

Un timer sostanzialmente e' un contatore che puo' essere impostato in modo che quando raggiunga o superi un valore,  venga generato un interrupt che chiami una funzioni personalizzata.

L'API base di Arduino non espone un API per programmare cosi' a basso livello i timer, per cui vi sono diversi tutorial su Internet che spiegano in dettaglio la cosa... come questo articolo :)

L'API di Arduino uso i timer nel seguente modo:

  • Timer0 e' usato per gestire il tempo, e cioe' le funzioni delay(), millis()
  • Timer1 e' usato dalla libreria servo per i motori passo-passo
  • Timer2 e' usato dalla funzione tone()

Di norma si sceglie di lavorare sui Timer1 o Timer2, perche' le funzioni del Timer0 servono quasi sempre.

Se il progetto lo consente Timer1 e' la scelta piu' logica e di seguito lo useremo come riferimento, anche perche' e' l'unico a 16bit. Tale timer e' anche usato da NilRTOS, che vi suggerisco di andare a guardare se ritenete di sapere gia' tutto sui timer e vi state annoiando :)

Come sempre accade l'Arduino Mega ha piu' timer, piu' pin, piu' tutto, per cui se dovete mettere in piedi un progetto complesso, andate in quella direzione, e procuratevelo.

Prescaler

Come e' scandito il tempo del timer? Allora, Arduino Uno gira a 16Mhz, cioe' il chip e' in grado di eseguire 16 milioni di istruzioni 'semplici' al secondo. Lo stesso oscillatore e' disponibile al timer. Al fine di rendere piu' comodo l'uso del timer, questi 16Mhz possono essere divisi da un dispositivo chiamato prescaler.

Il prescaler puo' dividere solo per alcuni numeri fissi, che sono: 1024, 256,64,8,1.
Per cui se impostate il prescaler a 1024, vuol dire che il vostro contatore sara' incrementato 16Mhz/1024=16000000/1024=15625 volte al secondo.

Se mettete il divisore a 1, verra' incrementato 16 milioni di volte al secondo, che e' la precisione massima ottenibile (in realta' poi c'e' il tempo di esecuzione dell'interrupt, ma ci siamo capiti).

Registri di gestione del timer: TCCRxA e TCCRxB

Per ognuno dei tre timer (x in {0,1,2})  ci sono diversi registri configurabili, pieni di bit (come con il C/64!)

Potete trovare la documentazione completa scaricando il pdf del chip ATmega328p direttamente dalla product page di Arduino

Register\\Bits76543210
TCCR1A
TCCR1BCS12CS11CS10
TIMSK1RRICIE1RROCIE1BOCIE1ATOIE1
Registri timer Arduino: indicati solo i bit che ci interessano. R= Riservati, ignorare

Per configurare il prescaler, si usano i bit 0-2 del registro B, con la seguente frizzante semantica:

CS12CS11CS10USE
000No Clock Timer STOP
001CLCK i/o /1 No Prescaling
010CLK i/o /8 (From Prescaler)
011CLK i/o /64 (From Prescaler)
100CLK i/o /256 (From Prescaler)
101CLK i/o /1024 (From Prescaler)
110External Clock source on T1 Pin. Clock on falling edge  
111External Clock source on T1 pin. Clock on rising edge.

Timer/Counter Register (TCNTn)

Questo registro (a 16 bit solo per il timer 1) indica il valore al quale far scattare il timer. Il timer contera' a partire da zero fino a 65535.
Nell'esempio di prima, per farlo scattare ogni secondo con un prescale di 1024 basta impostarlo a

TCNT1=65535-15625=49910

In questo modo fara' solo gli ultimi 15625 cicli

Infine la funziona da chiamare allo scadere del timer (callback) si definisce con la Interrupt Service Rotudine che e' una macro del tipo:

ISR(TIMERx_OVF_vect)
{
// codice qui
}

Questo handler viene chiamato quando si verifica un OVerFlow del timer, cioe' quando il contatore supera il valore impostato.

Nel registro TIMSK1, il bit TIOE1 sta per Overflow Interrupt Enabler ed abilita il timer assieme al flag TOV1 del registro TIFR1.

Proviamo un esempio completo:

const uint8_t ledPin=13;
const float cicli_secondo=15625;
const float intervallo_sec=0.25; // intervallo
const int cicli_necessari=((int)(cicli_secondo*intervallo_sec));
const uint16_t  timer_deadline=65535- cicli_necessari;

void setup()
{
  Serial.begin(9600);
  pinMode(ledPin, OUTPUT);
  Serial.print("Intervallo in secondi:");
  Serial.print(intervallo_sec);
  Serial.print(" cicli necessari:");
  Serial.println(cicli_necessari);
  Serial.print("Valore di partenza del timer:");
  Serial.print(timer_deadline);
  // Configurar Timer1
  TCCR1A = 0;                        //Abilita il timer 0, disattiva le modalita' di controllo basate su pin esterni
  TCCR1B = 0;                        //
  TCCR1B |= (1<<CS10)|(1 << CS12);   //Configura il prescale ponendo a 1 i soli bit CS10 e CS12 (vedi tabella sopra)
  
  
  TCNT1 = timer_deadline;            //Imposta il valore calcolato in precedenza                            
   
  TIMSK1 |= (1 << TOIE1);           // Abilita il timer 1 usando il registro TIMSK1

}
void loop()
{
}
ISR(TIMER1_OVF_vect)                              // Interrupt service rotudine
{
  TCNT1 = timer_deadline;                         // Reinit del timer
  digitalWrite(ledPin, !digitalRead(ledPin));     // Inverte il pin
}

L'output del terminale sara'

Intervallo in secondi:0.25 cicli necessari:3906
Valore di partenza del timer:61629

La sintassi ( 1<<TOIE1) e' un modo carino per impostare la maschera di bit con un solo 1 per la posizione indicata dalla costante 'TOIE1' .

Per ragioni di chiarezza, l'esempio calcola da solo tutti i valori: dovete semplicemente impostare l'intervallo in secondi come costante, e lasciar fare tutti i conti al bravo compilatore, che fornira' poi i valori costanti.

Oltre al TCTN1 ci sono i registri che scattano quando si raggiunge un valore dato (Output compare register) che sono due: OCR1A, OCR1B

Per cui in generali ci sono 3 macro eventi per i nostri tre timer:

TIMERx_COMPA, TIMERx_COMPB e TIEMERx_OVF

Il Timer 1 e' l'unico ad avere un evento extra chiamato Capture Event, di cui parleremo in un altro articolo (forse).

FastPWM

I contatori possono anche essere usati per generare segnali i modulazione di frequenza (PWM). Si tratta di una modalita' avanzata, e potete trovare qui una spiegazione dettgliata. E' usata dalla libreria di StereoSID per generare in output il segnale che generebbe il chip dopo opportuni calcoli.

Il cane da guardia: il WDT

Interessante infine e' il watchdog timer (WDT, capitolo 11.8 del manuale del chip) con uno scopo ben preciso e un oscillatore separato.

La modalita' d'uso standard e' che se questo contatore a ritroso non e' fatto ripartire (tramite un 'reset'), provoca un riavvio completo del microcontrollore. Si tratta di un meccanismo di sicurezza tipico dei sistemi embedded: si imposta un valore limite oltre il quale, se il sistema non ha reagito in modo previsto, resettando il cane da guardia, viene riavviato.

Il WDT ha solo 10 valori possibili che vanno da circa 16ms a 8 secondi.

Un suo uso molto comodo e' per forzare un "risveglio" quando il sistema va in risparmio energetico, e qui trovate quale codice di esempio. Infine in questo articolo trovate librerie e risorse extra come la libreria TimerOne che consente di nascondere molta della complessita' illustrata finora, rendendola anche un po' indipendente dal microcontrollore.