Ciao a tutto il forum,
per una precisa esigenza progettuale mi occorreva far generare un interrupt a un ATMega328, che richiamasse ogni tot millisecondi una routine in modo indipendente da eventuali delay() o vari altri "ritardi". Naturalmente sulla rete ci sono innumerevoli esempi per realizzare funzioni come quella da me cercata, ma sono quasi sempre scritti in maniera alquanto "criptica"...e io invece, avevo voglia di qualcosa di più "didattico" di un semplice "copia/incolla", insomma, volevo capirci qualcosa ![]()
Ho preferito, quindi, "sporcarmi" personalmente le mani con i timer e il datasheet, anche in considerazione del fatto che l'hardware dei timer è praticamente identico in molti micro Atmel (Tiny compresi), quindi saperne qualcosa in più diventa un utile "know how" da impiegare anche in altri contesti.
Voglio sottolineare che la mia conoscenza sui timer è ancora BASILARE, ma ho pensato di condividere con la comunità del forum i piccoli risultati delle mie ricerche (in pratica quello che ho capito !) in modo che possa essere di aiuto a quanti abbiano bisogno di realizzare una funzione simile e vogliano contemporaneamente capirci qualcosa. Ovviamente ogni suggerimento/precisazione degli esperti, per me è oro colato! ![]()
Come esempio, descriverò il codice necessario a richiamare una routine di interrupt ogni secondo.
L'ATMega328 ha tre timer, usati normalmente per la generazione el PWM. Teoricamente si potrebbe usare uno qualsiasi di questi timer, io ho scelto il timer1 perchè è l'unico (con i suoi 16 bit di risoluzione) a consentire un intervallo cosi grande (1s).
Ecco il codice nudo e crudo:
unsigned volatile long test;
void setup() {
Serial.begin(9600);
TIMSK1 &= ~(1<<TOIE1);
TCCR1A &= ~((1<<WGM11) | (1<<WGM10));
TCCR1B &= ~((1<<WGM12) | (1<<WGM13));
TIMSK1 &= ~(1<<OCIE1A);
TCCR1B |= (1<<CS12) | (1<<CS10);
TCCR1B &= ~(1<<CS11);
TCNT1H = 0xC2;
TCNT1L = 0xF7;
TIMSK1 |= (1<<TOIE1);
}
ISR(TIMER1_OVF_vect) {
TCNT1H = 0xC2;
TCNT1L = 0xF7;
/*
* Istruzioni da inserire nella routine...
*/
test++;
}
void loop() {
// Il delay non ferma gli interrupt :-)
delay(1000);
Serial.println(test);
}
Abbastanza incomprensibile, vero? Ecco una descrizione dei vari passaggi (non esaustiva, ovviamente):
- Innanzitutto, dato che dovremo modificare dei registri che interessano gli interrupt del timer1, occorre prima disabilitarli!
Questo si ottiene impostando a 0 il bit meno significativo (0) del registro TIMSK1.
Quando si saranno impostati tutti i valori si riabiliterà nuovamente il flag, per attivare gli interrupt (vedi più avanti).
TIMSK1 &= ~(1<<TOIE1);
N.B.Le costanti relative ai registri e ai nomi dei singoli bit sono già caricate nell'ambiente di programmazione; la notazione
(1<<TOIE1) rappresenta un "1" nella posizione rappresentata da TOIE1 (shift a sx) che unito all'operazione di and (&) biwise significa: "azzera il bit TOIE1 e lascia inalterati gli altri:
- Ora si configura Timer1 in modalità "Normal" (cioè nessun PWM o CTC o fast PWM...ma solo "contatore").
Per fare questo si interviene su DUE registri denominati TCCR1A e TCCR1B.
Quattro bit (WGM10,WGM11 di TCCR1A e WGM12, WGM13 di TCCR1B) controllano la modalità di
generazione della forma d'onda (Normale, PWM, CTC, etc.)
La combinazione WGM10 = WGM11 = WGM12 = WGM13 = 0 corrisponde al modo "Normal" del timer (vedi pagina 137 del datasheet).
TCCR1A &= ~((1<<WGM11) | (1<<WGM10));
TCCR1B &= ~((1<<WGM12) | (1<<WGM13));
Notare che si può scrivere la stessa cosa usando una macro di avr di nome _BL(bit), che setta a "1" il bit specificato:
nell'esempio, la prima riga può essere scritta come:
TCCR1A &= ~((_BL(1WGM11) | _BL(WGM10));
- Si specifica, adesso, che il timer 1 debba generare un interrupt solo in caso di overflow, nessuna altra modalità. (pag. 140 datasheet)
TIMSK1 &= ~(1<<OCIE1A);
- Qui si confiura il Prescaler, un divisore posto a monte del timer, che divide il clock della CPU per un particolare valore. In questo modo il timer andrà più...lentamente.
I registri CS10, CS11 e CS12 controllano il valore del prescaler, essi sono i 3 bit meno significativi del registro TCCR1B (vedi pag. 138 del datasheet).
I valori possibili sono:
===================================================
| CS22 | CS21 | CS20 | Descrizione
|================================================
| 0 | 0 | 0 | Noclock. Timer fermo
| 0 | 0 | 1 | CLK (Nessun prescaling)
| 0 | 1 | 0 | CLK / 8
| 0 | 1 | 1 | CLK / 64
| 1 | 0 | 0 | CLK / 256
| 1 | 0 | 1 | CLK / 1024 (*)
Il valore scelto, nel nostro caso, è 1024 (in binario: Bxxxxx101)
TCCR1B |= (1<<CS12) | (1<<CS10); // setta "1" nei bit CS10 e CS12
TCCR1B &= ~(1<<CS11); // setta "0" nel bit CS11
- Eccoci al "clou"

Calcolo del valore di partenza appropriato da inserire nel contatore del TIMER 2:
-
Trovare la frequenza di clock del timer1 divisa dal prescaler:
(CPU frequency) / (prescaler value) = (16MHz / 1024) = 15.625Hz
Ottenere il periodo (1/f):
1/15625 = 64uS -
Ora, dovendo nel nostro specifico caso, programmare un interrupt ogni 1S, si calcola:
(periodo_richiesto / 64uS) = (1/64E-6) = 15625.
Il numero trovato rappresenta il valore che vogliamo far contare al timer (infatti contando uno step ogni 64us, 15625 step sono contati dal timer esattamente nel tempo di un secondo). -
Dato che il timer1 ha 16 bit di risoluzione, dobbiamo fare in modo che esso conti solo gli ULTIMI 15625 step, trascorsi i quali vogliamo che lanci la routine interrupt di overflow denominata TIMER1_OVF_vect.
In altre parole il conteggio non deve iniziare da zero ma da:
(risoluzione_timer) - 15625. Dove risoluzione_timer vale 2^16 = 65536.
Nel nostro caso:
(risoluzione_timer) - 15625 = (2^16) - 15625 = 49911;
Il timer, quindi, dopo ogni overflow, dovrà iniziare il conteggio sempre da questo numero.
Inseriremo questo valore di partenza nei due registri a 8 bit di cui dispone Timer 1, essi sono denominati TCNT1H e TCNT1L rispettivamente per la parte più significativa e meno significativa del numero.
Il valore 49911 in esadecimale è 0xC2F7, sarà sufficiente, dunque inserire C2 in TCNT1H e F7 in TCNT1L
TCNT1H = 0xC2;
TCNT1L = 0xF7;
-
Ora si possono riattivare gli interrupt di overflow per timer 1
TIMSK1 |= (1<<TOIE1); -
Ad ogni overflow del timer (e quindi a ogni secondo), viene chiamata la routine:
ISR(TIMER1_OVF_vect) {
TCNT1H = 0xC2;
TCNT1L = 0xF7;
}
E' importante notare che all'interno è necessario ricaricare nel timer il valore di partenza.
Tutto qui...!
Ovviamente lo stesso procedimento può essere usato per i timer 0 e 2 (a 8 bit) per generare gli intervalli più disparati, basta fare un pò di pratica con i valori dei prescaler per trovare i massimi (e minimi) intervalli di tempo ottenibili per ogni configurazione. Una agevolazione è data dal fatto che i nomi dei registri sono gli stessi per ogni timer, hanno solo, all'interno del nome, il numero di appartenenza (es: TCCRxB, TCNTx, TIMSKx... dove x è il numero del timer 0, 1 o 2).
N.B. Voglio sottolineare che usando questo sistema si interrompe il funzionamento del PWM controllato dal timer associato.
I timer controllano i seguenti piedini (pin Arduino):
Timer 0 (8 bit): pin 6 and 5
Timer 1 (16 bit): pin 9, 10
Timer 2 (8 bit): pin 11, 3
Inoltre il timer0 gestisce le funzioni delay(), millis(), etc. quindi se si usa questo timer molto probabilmente (a seconda del valore di prescaler scelto) sarà alterato anche il funzionamento di queste routine, che non daranno più valori corretti.
Spero di non essere stato troppo noioso con le descrizione... ![]()