Generare un Interrupt ogni secondo.

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 :slight_smile:
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! :slight_smile:

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” :slight_smile:
    Calcolo del valore di partenza appropriato da inserire nel contatore del TIMER 2:
  1. 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

  2. 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).

  3. 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… :slight_smile:

Complimenti per la descrizione e il tempo dedicato, non ho mai avuto occasione di vedere il timer sotto questa forma, me lo studio volentieri Grazie per la condivisione

ciao

Hai visto la libreria di Leo: swRTC? http://arduino.cc/forum/index.php/topic,73496.0.html

No, nei progetti che bisognavano di RTC ho sempre usato un DS1307... ormai sono affezionato :)

Gli ho dato un'occhiata adesso...davvero un bel lavoro. Viene impiegato un interrupt overflow di un timer a 8 bit per l'aggiornamento dei valori dell'orologio, fondamentalmente la metodologia è la stessa (ma del resto i registri dei timer sono quelli!).

Capisco benissimo i problemi che hai incontrato perché manipolare i timer non è semplice. Fanno veramente impazzire.
Ed impazzisci ancor di più se pensi che quasi ogni micro ha i suoi registri per cui scrivere una cosa portabile su più HW richiede un grosso sbattimento.

Leo hai proprio ragione...e infatti ti faccio i miei complimenti per il tuo non facile lavoro svolto su swRTC.

Il problema, molte volte, è che non è affatto facile trovare delle guide dettagliate che spieghino con semplicità i concetti basilari che regolano il funzionamento di queste piccole "bestie" che però hanno qualcosa di affascinante... E quando mi capita di apprendere e "comprendere" qualcosa, quando capisco che un piccolo passettino è stato fatto (spesso con "mezzi propri"), sono solito scrivere degli appunti completi sull'argomento proprio perchè ho la certezza che passata qualche settimana e dedicandomi ad altro, non ricorderei più niente di ciò che avevo faticosamente imparato. Così ho preso l'abitudine di perdere un po di tempo a fissare le idee a vantaggio di una consultazione futura. Oggi, ho pensato che l'argomento potesse essere di interesse generale...

dalubar: Oggi, ho pensato che l'argomento potesse essere di interesse generale...

Sicuramente