Generiamo un’onda quadra di frequenza 1hz

In questo articolo vedremo come impostare i registri del timer di Arduino per ottenere un’onda quadra con frequenza di un hertz, che può tornarvi utile ad esempio per creare un temporizzatore, per delle luci, per un eventuale bromografo, o un orologio.

Scegliere il timer

Arduino Uno dispone di tre timer, il timer0 e il timer2 sono a 8 bit, mentre il timer1 è a 16 bit. Ma che significa???
Significa che un timer a 8 bit può contare da 0 a 255, va in overflow e a seconda della modalità di lavoro o conta all’indietro o si azzera, mentre un timer a 16 bit può contare da 0 a 65535.

A loro volta questi 3 timer dispongono di 3 modalità di lavoro. Più precisamente il timer1 ne ha a disposizione 4.

Per questo progetto utilizzeremo la modalità CTC (Clear timer on compare match) al fine di avere un controllo preciso sulla frequenza.

La scelta del timer da utilizzare ricade anche sulle funzioni che avrete intenzione di utilizzare nel vostro codice.

Arduino Uno ad esempio utilizza il Timer0 per le funzioni delay(), millis() e il pwm ovvero analogWrite(), sui pin 5 e 6. Il Timer1 viene utilizzato dalla libreria Servo e il pwm sui pin 9 e 10. Infine il Timer2 è utilizzato dalla libreria tone e per generare il pwm sui pin 3 e 11.

Questo per quanto riguarda le funzioni predefinite, se utilizzate qualche altra libreria che utilizza un timer, per sapere quale, vi conviene andare a leggere il file.h.

Va da se che se avrete intenzione di usare delay() il timer0 è escluso a priori.

A noi al momento non interessa nessuna delle funzioni citate, quindi prendiamo buona la possibilità di utilizzarli tutti e tre… Ma quale???

Innanzitutto vediamo come funziona la modalità scelta, ovvero CTC.

Modalità CTC
Modalità di funzionamento CTC

Come possiamo vedere, il valore di TCNTn non viene azzerato quando raggiunge il valore di overflow, che come abbiamo visto, nei timer a 8 bit equivale a 255, mentre in quelli a 16 a 65535, ma quando raggiunge il valore che abbiamo impostato in OCnx.
E sulla base del valore da inserire in questo registro decideremo se ci basta un registro a 8 bit o dovremmo usare quello a 16.

OCnx = (16.000.000/2*prescaler*(freq desiderata))-1 Al risultato sottraiamo 1 perchè il conteggio parte da 0.

Proviamo a partire con un prescaler di 8

OCnx = (16.000.000/2*8*(1))-1 = 999.999 decisamente eccessivo dato che il massimo è 255

OCnx = (16.000.000/2*32+(1))-1 = 249.999 passiamo al prescaler successivo

OCnx = (16.000.000/2*64*(1))-1 = 124.999 sempre troppo

OCnx = (16.000.000/2*128*(1))-1 = 62.499 troppo per un timer a 8 bit ma ok per quello a 16

OCnx = (16.000.000/2*256*(1))-1 = 31.249

OCnx = (16.000.000/2*1024*(1))-1 = 7.811 nulla da fare. Un hertz è una frequenza troppo bassa per un timer a 8 bit.

Dobbiamo per forza orientarci sul timer1 essendo l’unico a 16 bit.

Impostiamo i registri del timer

Se guardiamo a pagina 109 del datasheet possiamo vedere che per abilitare la modalità di funzionamento CTC l’unico bit che bisogna settare a 1 è il WGM12 che appartiene a TCCRB, quindi WGM10, WGM11 e WGM13 vanno settati a 0.

Mode numero 4
Registro TCCR1A
Registro TCCR1B

TCCR1A &= ~((1<<WGM10) | (1<<WGM11)); così settiamo a 0 i due bit di controllo di TCCR1A.

TCCR1B |= (1<<WGM12); settiamo a 1 WGM12

TCCR1B &= ~(1<<WGM13); Setto a 0 WGM13, l’ultimo bit che mancava per impostare la modalità CTC.

Adesso per avere la nostra onda quadra in uscita dall’arduino dobbiamo abilitare il Compare Output Mode affinché non appena TCNT1 raggiunge il valore di OCR1A venga effettuato il toggle sul relativo pin. La seguente immagine ci aiuta a capire quale.

Pinout Arduino Uno

Possiamo vedere dall’immagine che OC1A corrisponde al pin 9.

Vediamo dal datasheet come vanno impostati i bit.

Dobbiamo guardare Toggle OC1A/OC1B on compare match

Per eseguire il toggle di OC1A quindi dobbiamo settare a 1 il bit COM1A0 e a 0 il bit COM1A1, lo facciamo con la seguente riga.

TCCR1A |= (1<<COM1A0); settiamo così a 1 il bit COM1A0 in TCCR1A abilitando il toggle del pin 9 connesso a OC1A.

Non ci resta che abilitare il pin 9 come OUTPUT o scrivendo pinMode(9, output);o agendo direttamente sul registro DDRD. Guardando il pinout possiamo vedere che il pin 9 è il primo bit del registro PORTB, quindi scriveremo DDRB = B00000010;

Ora andiamo a settare il prescaler. Dai calcoli che abbiamo fatto precedentemente la nostra scelta ricadrà su 256. Il timer1 non dispone del prescaler a 128.
A pagina 110 del datasheet possiamo vedere che per impostare il prescaler 256 dobbiamo settare i bit CS11 e CS10 a 0, mentre CS12 a 1. Lo facciamo con le seguenti righe di codice.

TCCR1B &= ~((1<<CS11) | (1<<CS10)); setto a 0 i bit CS11 e CS10
TCCR1B |= (1<<CS12); setto a 1 il bit CS12

Ok, ora il prescaler è impostato a 256.

Come ultima cosa inseriamo all’interno del registro OCR1A il valore precedentemente calcolato per 1 hz e prescaler a 256, ovvero 31.249.

Come possiamo notare dall’immagine seguente il registro OCR1A è diviso in OCR1AH (che contiene gli 8 bit più significativi, e OCR1AL che contiene gli 8 bit meno significativi.

Quindi convertiamo 31.249 in esadecimale, ovvero 7A11 e lo inseriamo nel registro al seguente modo.

OCR1AH = 0x7A;
OCR1AL = 0x11;

Adesso possiamo caricare lo sketch e collegando un led con una resistenza da 1K al pin 9 lo vedremo lampeggiare con la frequenza di una volta al secondo.

Onda quadra con periodo di 1 secondo. Frequenza 1 hertz

Di seguito lo sketch completo.

void setup() {
  DDRB = B00000010; //imposto il pin 9 (PB1) corrispondente a OC1A come output
  TCCR1A &= ~((1<<WGM10) | (1<<WGM11));
  TCCR1A |= (1<<COM1A0);
  TCCR1B |= (1<<WGM12);
  TCCR1B &= ~(1<<WGM13);
  TCCR1B &= ~((1<<CS11) | (1<<CS10));
  TCCR1B |= (1<<CS12);
  OCR1AH = 0x7A;
  OCR1AL = 0x11;
}

void loop() {
  // put your main code here, to run repeatedly:
}

Ottenere lo stesso risultato con un timer a 8 bit

Volendo possiamo ottenere lo stesso utilizzando un timer a 8 bit… Ma come???

Invece di generare un’onda quadra avente un periodo di un secondo, genereremo un interrupt ogni millisecondo, che andrà ad incrementare una variabile ms, che una volta raggiunto il valore di 1000 si resetterà incrementando una variabile sec e facendo il toggle su di un pin.

Essendo il timer0 dedicato a funzioni dell’ambiente Arduino utilizzeremo il timer2 sempre in modalità CTC.

Calcoliamo il valore che inseriremo in OCR2A scegliendo un prescaler di 64:

16.000.000/128=250.000 Questa è la frequenza a cui viene aggiornato TCNT2

1/250.000=0.000004 Dopo aver trovato la frequenza determiniamo il periodo. Ora sappiamo che ogni 0.000004 sec TCNT2 incrementa di 1.

0.001/0.000004=250-1 valore da inserire in OCR2A, ho sottratto 1 perchè il conteggio parte da 0. Questo nella modalità CTC determina un incremento di TCNT2 ogni 0.000004 secondi sino a che raggiunge il valore di 249, momento in cui verrà scatenato l’interrupt. Il tempo trascorso sarà di 0.001 sec, ovvero 1 ms.

Ricapitolando:

Modalità di lavoro –> CTC

Prescaler –> 64

OCR2A –> 249

Impostiamo i registri

Innanzitutto impostiamo i registri di controllo TCCR2A e TCCR2B

TCCR2A
TCCR2B
Mode numero 2

Dalla tabella soprastante possiamo vedere che per abilitare la modalità CTC dobbiamo settare a 0 Il bit WGM20 e a 1 il bit WGM21 entrambi appartenenti a TCCR2A, e a 0 il bit WGM22 appartenente invece a TCCR2B. Quindi nel setup scriviamo:

TCCR2A &= ~(1<<WGM20); //Setto a 0 il bit 0 del registro TCCRA
TCCR2A |= ((1<<WGM21)); //Setto a 1 il bit 1 del registro TCCR2A
TCCR2B &= ~(1<<WGM22);//Setto a 0 il bit 3 del registro TCCR2B

Ora il nostro timer è impostato per lavorare in modalità CTC, settiamo il prescaler a 64. Come lo vediamo dalla seguente immagine.

La tabella ci dice che per impostare il prescaler a 64 dobbiamo settare i bit CS20, CS21 a 0, mentre il bit CS22 va impostato a 1. Tutti e tre questi bit appartengono al registro TCCRB, quindi scriviamo:

TCCR2B &= ~((1<<CS21) | (1<<CS20));//Settiamo a 0 i bit CS20 e CS21
TCCR2B |= (1<<CS22);//Settiamo a 1 il bit CS22

Ora abbiamo il prescaler impostato su un fattore di divisione di 64.

Non ci resta altro da fare che settare OCR2A, abilitare gli interrupt e scrivere la relativa ISR (interrupt service routine).

OCR2A = 249; //imposto il valore di OCR2A a 249 come precedentemente calcolato

L’immagine seguente ci dice quale bit del registro TIMSK2 abilitare per attivare l’interrupt che ci interessa.

Registro TIMSK2

Per attivare l’interrupt dobbiamo settare a 1 il bit OCIE2A (A perchè la comparazione viene effettuata con il registro OCR2A), ovvero il bit 1, quindi al codice aggiungiamo:

TIMSK2 |= (1<<OCIE2A); //attivo l’interrupt TIMER2_COMPA_vect

Ultima cosa da fare è scrivere la ISR. Ma cosa vogliamo che faccia???

Per generare l’onda quadra e avere un riscontro visivo di ciò che accade aggiungiamo anche il blink di un led. Ho scelto il pin 2 appartenente al bit 2 del registro PIND.
Quindi nel setup lo setteremo come uscita scrivendo DDRD = B00000100;.
Siccome il periodo è dato dalla somma della semionda a livello basso con quella a livello alto e l’interrupt viene richiamato ogni millisecondo, per avere il periodo di un secondo il toggle del pin deve avvenire ogni mezzo secondo, ovvero 500 ms.
Perciò dovremo avere una variabile ms da incrementare per tener traccia del tempo passato.

Dobbiamo dichiararla all’inizio.

Siccome è una una variabile che viene modificata all’interno di una ISR va dichiarata come volatile, prima del setup scrivendo volatile int ms=0;

Con un if verifichiamo se sono trascorsi 500 ms, se così fosse, con l’istruzione PIND = B00000100; effettiamo il toggle del pin2.

Proseguendo con un else if verifichiamo se sono passati ulteriori 500 ms controllando se ms ha raggiunto il valore 1000. Se così fosse eseguiamo nuovamente il toggle del pin2 e al fine di tenere un conteggio del tempo passato possiamo incrementare una variabile sec che andremo a dichiarare sotto ms con volatile int sec = 0;. Sempre come volatile perchè anche questa è incrementata dentro ISR.

//ISR TIMER2 overflow
ISR(TIMER2_COMPA_vect){
  ms++;
  if(ms==500){
    PIND = B00000100;
  }
  else if(ms==1000){
    PIND = B00000100;
    sec++;
    ms=0;
  }
}

Una volta caricato lo sketch avremo in uscita al pin2 un’onda quadra con frequenza di un hertz come la precedente. E una variabile sec che potremo utilizzare per realizzare un temporizzatore.

Vediamolo con un’immagine.

Onda quadra con frequenza di 1 hertz in uscita sul pin2.

Nell’immagine sotto a paragone le due forme d’onda. Quella generata con il timer1 in uscita da OC1A (in rosso) e quella generata con il timer2 contando il valore di ms incrementato da un interrupt generato ogni ms. Come potete vedere sono uguali identiche.

Le due onde quadre a confronto.

Di seguito lo sketch completo:

volatile int ms = 0;
volatile int sec = 0;

void setup() {
  DDRD = B00000100; //imposto i pin dal 2 al 7 come uscite
  PORTD = B00000000; //imposto tutti i pin di PORTD a 0
  Serial.begin(9600);
  TCCR2A |= ((1<<WGM21)); //Setto a 1 i bit 0 e 1 del registro TCCR2A abilitando la modalità CTC
  TCCR2A &= ~(1<<WGM20);
  TCCR2B &= ~((1<<WGM22) | (1<<CS21) | (1<<CS20)); //con questo comando settiamo i bit CS21 e CS20 a 0
  TCCR2B |= (1<<CS22); //Settiamo il bit CS22 a 1
  OCR2A = 249;
  TIMSK2 |= (1<<OCIE2A); //attivo l'overflow interrupt
  
}

//ISR TIMER2 overflow
ISR(TIMER2_COMPA_vect){
  ms++;
  if(ms==500){
    PIND = B00000100;
  }
  else if(ms==1000){
    PIND = B00000100;
    sec++;
    ms=0;
  }
}

void loop() {
  // put your main code here, to run repeatedly:
}