PrettyPrint

domenica 1 agosto 2010

Timer AVR in 5min: 2) Il modo CTC

< 1)Timer AVR in 5min: 1) Partiamo da zero 2) Timer AVR in 5min: 3) VMLAB e linux >




Ultima revisione: 26/Apr/2012 


Supponiamo di avere un compito piuttosto semplice:

Accendere e spegnere un LED (collegato sul pin1 del PortB) ogni secondo, usando il Timer1 di un ATmega168. Il clock è quello di sistema del micro, che è di 4MHz.

Dividiamolo in due parti:

"...Accendere e spegnere un LED (collegato sul pin1 del PortB) ...":

Il programma dovrà provvedere a spegnere il LED se è acceso, viceversa ad accenderlo se è spento.

Per quanto riguarda l'hardware, possiamo collegare il catodo del LED sul pin1 di PortB, mediante una resistenza di limitazione opportunamente dimensionata:




in questo modo, quando l'uscita sarà a livello "1" (5V)  il LED sarà spento, viceversa, quando l'uscita sarà a livello "0" (0V), il LED sarà acceso:


Lasciamo in sospeso per il momento come implementare via software questa prima parte del compito e concentriamoci invece sulla seconda:

"... ogni secondo, usando il Timer1 di un ATmega168. Il clock è quello di sistema del micro, che è di 4MHz...":

Quindi per un tempo T di 1s  il LED dovrà essere acceso, per il successivo tempo di 1s dovrà essere spento e così via, periodicamente.

Dalla frequenza di clock fc ricaviamo il periodo del clock di sistema Tc



Ora, il Timer1 (16 bit), al massimo può contare fino a N = 65535  (2 16 -1) dunque, il massimo tempo Tmax che possiamo ottenere da questo timer è ricavabile dalla formula generale:



ossia 16,384 ms, un tempo molto inferiore a 1s

N.B.  c'è il "+1" perché il conteggio inizia dallo 0 e quindi anche per esso viene speso un ciclo di clock

Come fare per arrivare a 1s?

I bit non li possiamo aumentare, ma c'è la possibilità di applicare al timer un periodo più grande di quello di sistema Tc ossia applicare al timer una frequenza di clock fc più bassa di quella di sistema.

Internamente l'AVR ha un prescaler (divisore di frequenza) programmabile, che consente di pilotare il timer anche con una frequenza di clock inferiore a quella del sistema, di un fattore: 8, 64, 256 o 1024 (questo per il Timer0 e il Timer1) oppure, ragionando in termini di periodo ... che consente di pilotare il timer con un periodo di clock maggiore di quello di sistema, di un fattore: 8, 64, 256 o 1024

Dunque, il clock al quale lavora il timer usando i prescaler, non è più Tc, ma Tc *k. , ove k è il fattore di prescaler; cioè ci consente di avere durate di tempo maggiori di quelle massime ottenibili con la frequenza di clock pari a quella di sistema.

Pertanto la formula per il calcolo del tempo massimo T , che tiene conto anche del fattore di prescaler k diventa:



Siamo così in grado di calcolare il massimo tempo T, per ogni fattore di prescaler:



























k T
116,384ms
80,13s
64 1,05s
2564,19s
1024 16,7ss


Scartiamo ovviamente i fattori k=1 e k=8, perché entrambi non ci consentono di arrivare a 1s e quindi restringiamo la scelta fra k=64, 256 oppure 1024.

Il numero N del timer per avere un desiderato tempo T sarà dato dall'equazione:




che, per T=1s, per i soli k sopra individuati diventa:





k N
64 62.499
25615.624
1024 3.905,25


A questo punto dobbiamo prendere il considerazione solo fattori  k per che danno numeri N rappresentabili nel registro del timer; il timer da 16 bit può infatti rappresentare numeri interi compresi fra 0 e 65535.

Quindi possiamo scegliere solo k=64 oppure k=256 perché i rispettivi N=62499 e N=15624 sono rappresentabili, a differenza di k=1024 che dà un numero N non intero.

Ci rimane allora da scegliere k=64 oppure k=256.

Scegliamo k=64, perché, in definitiva:

1) ci dà un numero N rappresentabile (62499)
2) è il minimo fattore che ci consente di avere un tempo superiore a quello desiderato e quindi ci assicura una risoluzione migliore (cioè un periodo di clock più piccolo).

Resta ora solo un problema: far azzerare il timer dopo 62499 invece che dopo 65535.

Come fare?  

Configurare il timer per farlo lavorare in modo CTC (Clear Timer on Compare) e usare un interrupt che viene generato all'azzeramento dopo il numero desiderato.

Il timer che lavora in modo CTC non conta fino al massimo numero dato dal suo numero di bit, ma fino a un numero massimo definito in un particolare registro: OCR1A (per il Timer1) (1).

Un impulso di clock dopo il raggiungimento del numero massimo N,  il timer viene azzerato e riparte il conteggio dal numero zero.

Dunque, in fase di configurazione del timer1, si andrà a caricare il numero 62499 nel registro OCR1A, perché vogliamo che il nostro timer non conti più fino a 65535, ma fino a 62499.

Il pseudo codice per la sola configurazione del timer è:

  1. Imposta modo CTC (con il numero massimo definito in OCR1A);
  2. Carica il numero N in OCR1A;
  3. Attivare l'interrupt su OCR1A;
  4. Imposta il prescaler;
  5. Abilita tutti gli interrupt;

Vediamo come codificare i vari passaggi, sempre con avr-libc:

 1. Imposta modo CTC (con il numero massimo definito in OCR1A); 

Il timer1 dispone di tre registri di controllo: TCRR1A, TCCR1B e TCCR1C, i cui bit, opportunamente attivati, consentono di configurare diverse opzioni di funzionamento del timer1.

Fra questi, ci sono quattro bit che riguardano  la configurazione del timer come generatore di forme d'onda: si trovano "dislocati" fra il registro A e il registro B e sono identificati dall'etichetta WGM1x:



I bit WGM10 e WGM11 sono nel registro di controllo A:







mentre i restanti WGM12 e WGM13 si trovano nel registro di controllo B:





Tutti i 4 bit sono di lettura/scrittura e di default si trovano a 0.


La tabella 16.4 del datasheet dell'ATmega168/328 descrive come attivare/disattivare questi quattro bit per usare la modalità di generazione di forma d'onda desiderata. A noi interessa la modalità CTC e precisamente la modalità n. 4, per la quale il numero massimo N che vogliamo contare  (TOP secondo la convenzione del d/sheet) va caricato nel registro OCR1A:


Per il modo n.4, come si vede, occorre attivare solo il bit WGM12, che, come appena visto, si trova nel registro TCR11B, dunque il codice sarà:

TCCR1B |= (1 << WGM12); // imposta il Timer1 in modo CTC (su OCR1A) 


2) Carica il numero N in OCR1A;


E' bene fare uso di un'etichetta simbolica N, che verrà definita all'inizio del sorgente (v. sorgente), così da migliorare la manutentibilità del codice:
OCR1A = N; // Carica il numero N nel registro OCR1A
L'etichetta simbolica sarà dunque, nel nostro caso:
#define N 62499  // T=1s @fck=4MHz :64
E' sempre consigliabile commentare il numero N, indicando il tempo che si ricava in corrispondenza di esso e per quale valore di clock e prescaler.


3) Abilitare l'interrupt su OCR1A;

Il timer1 dispone di un registro per il mascheramento delle interruzioni (TIMSK1) che riguardano il suo funzionamento.

L'interruzione che dobbiamo attivare è quella relativa al confronto fra OCR1A e il valore del registro dati del timer TCNT1: quando questi due valori si uguagliano, al successivo impulso di clock, la normale esecuzione del programma deve essere interrotta per eseguire la relativa funzione di gestione, nel cui corpo andrà inserito il codice che vogliamo far eseguire all'occorrenza (v. avanti)







Per attivare questa interruzione, occorre attivare il solo bit OCIE1A del registro TIMSK1:

TIMSK1 = (1 <<  OCIE1A);  // abilita interrupt CTC su confronto con OCR1A



4) Impostare il prescaler;





I primi tre bit del registro di configurazione B, consentono di impostare il tipo di clock (se interno, se esterno, se assente) e il fattore di prescaling del clock interno, secondo la seguente tabella (Tabella 16.5, datasheet ATmega168/328):






Dal momento che vogliamo attivare il prescalere con un fattore :64, abbiamo dunque da attivare solo i bit CS11 e CS10:

TCCR1B |= (1 << CS11) | (1 << CS10) ; // abilita prescaler (fc/64)
Dopo l'istruzione di impostazione del prescaler, il timer si avvia partendo da 0, perciò questa deve essere sempre l'ultima della configurazione del timer.



5) Abilita tutti gli interrupt;

Dopo la configurazione vanno abilitati tutti gli interrupt, globalmente, diversamente verrà ignorato anche l'interrupt, precedemente attivato. Con avr-lib c, l'istruzione a tale scopo è:

sei();  // abilita tutti gli interrupt

Questa istruzione va inserita come ultima di ogni altra configurazione (es. configurazione dell' I/O)



Ricodarsi ovviamente di includere nel sorgente l'header per la gestione delle interruzioni, subito dopo quello dell'I/O:
#include <avr/io.h>
#include <avr/interrupt.h>


La scrittura della routine di gestione dell'interrupt


Infine, si scrive la routine di gestione dell'interrupt , nel cui corpo funzione andremo a mettere il compito che abbiamo "schedulato" ogni T secondi

In avr-libc, l'etichetta simbolica associata al nostro interrupt è TIMER1_COMPA_vect, dunque avremo:

// Viene eseguita ogni T sec

ISR( TIMER1_COMPA_vect )
{
  // Accendi o spegni il LED su PB1
}


Rimane il compito di "Accendere o spegnere il LED su PB1", il cui codice andrà nel corpo funzione della routine di servizio dell'interrupt.

Innanzitutto, bisogna configurare la porta PB1 come di uscita essendo il LED collegando su questa. Ciò viene fatto attivando il bit 1 del registro direzione dati del PortB, quindi del registro DDRB

Dunque bisognerà inserire anche questa istruzione di configurazione del pin PB1, insieme a quelle già viste per il timer1:

DDRB |= (1 << PB1);  // Imposta il pin PB1 come di uscita


Per quanto riguarda l'accensione e lo spegnimento, possiamo collegare il LED con il catodo sull'uscita e l'anodo al ramo di alimentazione positiva (con ovvia resistenza di limitazione) e quindi questo si accenderà quando il bit di uscita è 0 (0V) e si spegnere quando il bit in uscita è 1 (per esempio 5V)

La commutazione viene realizzata via software, mediante un'operazione di ex-or (bit a bit) del solo bit letto su PB1 con un bit "1" e dei restanti bit con un bit "0".

Il contenuto di PortB viene così aggiornato con il risultato dell'operazione:

PORTB <---- PORTB ex-or 00000010 //leggi PORTB, fanne l'ex-or con 10 e riponi in PORTB
Se PB1 è 0 viene commutato a 1, viceversa se è 1, viene commutato a 0; i restanti bit rimangono immutati. Esempio:

Caso  in cui PB1=1 (LED spento)


      PB7 PB6 PB5 PB4 PB3 PB2 PB1 PB0
PORTB  0   0   0   1   0   1   1   0         ^
       0   0   0   0   0   0   1   0         =
---------------------------------------------
PORTB  0   0   0   1   0   1   0   0         (1 diventa 0 => il LED si accende)



Caso  in cui PB1=0 (LED acceso)

      PB7 PB6 PB5 PB4 PB3 PB2 PB1 PB0
PORTB  0   0   0   1   0   1   0   0         ^
       0   0   0   0   0   0   1   0         =
---------------------------------------------
PORTB  0   0   0   1   0   1   1   0         (0 diventa 1 => il LED si spegne)


Con avr-libc (avr-gcc), l'istruzione è:
PORTB = PORTB ^ (0b00000010);  // ex-or con 1, del solo bit PB1
che in sintassi C più compatta e leggibile diventa:
PORTB ^=  (1 << PB1) ;  // Se PB1=0 allora PB1=1, se PB1=1 allora PB1=0
Questo sarà dunque, in definitiva, il codice che vogliamo eseguire ogni secondo.

Il sorgente, combinando fra loro tutte le istruzioni di configurazione e la routine di interrupt, diventa in definitiva:

#include <avr/io.h>
#include <avr/interrupt.h>

#define N 62499  // T=1s @fck=4MHz :64

int main (void)
{

   DDRB |= (1 << PB1);  // Imposta il pin PB1 come di uscita
   TCCR1B |= (1 << WGM12); // imposta il Timer1 in modo CTC (su OCR1A)
   OCR1A = N;  // carica N nel registro OCR1A
   TIMSK1 = (1 <<  OCIE1A);  // abilita interrupt CTC su confronto con OCR1A
   TCCR1B |= (1 << CS11) | (1 << CS10) ; // abilita prescaler (fc/64)

   // Da questo punto in poi il timer è avviato!

   sei();  // abilita tutti gli interrupt

   while(1)
   {
      /* non fa niente...aspetta l'interruzione */
   }
}


/* Routine gestione interrupt (viene eseguita ogni T=1s)*/

ISR( TIMER1_COMPA_vect )
{
   PORTB ^=  (1 << PB1) ;  // Se PB1=0 allora PB1=1, se PB1=1 allora PB1=0
}
---- Nota 1) E' possibile utilizzare, in alternativa al registro OCR1A, il registro ICR1 (v. datasheet , Tab. 16-4)


< 1)Timer AVR in 5min: 1) Partiamo da zero 2) Timer AVR in 5min: 3) VMLAB e linux >



I contenuti di questo documento, sono stati resi possibili grazie ai seguenti strumenti gratuiti:
- LaTex online equation editor
- FidoCADJ (disegno grafici)
- Google Code Prettify (sorgenti con sintassi evidenziata)
- gEDA (disegno schemi elettrici)



Nessun commento:

Posta un commento