PrettyPrint

Visualizzazione post con etichetta timer. Mostra tutti i post
Visualizzazione post con etichetta timer. Mostra tutti i post

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)



lunedì 19 luglio 2010

Timer AVR in 5min: 1) Partiamo da zero





Ultima revisione: 29/Apr/2012 



Il timer di un microcontrollore è una periferica hardware interna impiegata per la generazione, la misura, di tempi ed eventi, che, lavorando separatamente dall'esecuzione del programma, consente di ottenere una notevole efficienza e precisione rispetto alle tecniche software basate su cicli di ritardo.

Un timer è un registro a n bit, il cui dato viene incrementato di un'unità a ogni impulso di clock.

Il timer inizia così a contare da 0 e arriva fino a un numero massimo 2n-1, per poi ricominciare da zero e così via di nuovo fino al massimo numero (tutti bit a 1), continuamente.

Ad esempio, un elementare timer da 2bit conterà continuamente, in sequenza, quattro numeri:





0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3....


ossia in codice binario:




00, 01, 10, 11, 00, 01, 10, 11, 00, 01, 10, 11....


In questo caso, diremo anche che la risoluzione del timer è di 2bit.

La direzione del conteggio può anche essere verso il "basso", cioè dal numero più grande al numero più piccolo.

Un numero resta memorizzato nel registro per una durata di tempo pari a quella di un periodo di clock (Tc).

Per esempio, se Tc=0,25s, partendo dal primo zero e finquando il registro del timer conterrà il primo numero 3 sarà trascorso un tempo pari a 4 * Tc sec ossia 1s.

Dal successivo zero e fino a tutto il secondo numero tre verrà di nuovo impiegato 1s e così via, per il terzo tre...il comportamento è dunque periodico di periodo T

Il timer va in overflow, quando passa dal massimo numero rappresentable (registro con tutti bit a 1) allo zero.

Una possibile raffigurazione dell'esempio è riportata di seguito, nella quale ogni cella rappresenta il valore dell'ipotetico registro dati del timer che memorizza il numero:



In generale, il numero N al quale corrisponde un intervallo di tempo T, dato il tempo di clock Tc è dato da:




N.B. si aggiunge 1, perché anche lo zero "impegna" un ciclo di clock.

Possiamo sfruttare questo periodo di 1s per il nostro programma: per esempio, far compiere ogni secondo un'operazione: accendere o spegnere un LED oppure realizzare un orologio, per esempio ricorrendo a un'interruzione software.

Esempio:

Ogni sec:
{
   Accendi il LED sul pin PortB.2 se è spento, viceversa spegnilo se è acceso
}


oppure:

Ogni sec:
{
   secondi = secondi + 1;

   se (secondi==60) allora:
   {
      minuti = minuti + 1;
      secondi = 0;
   }

   se (minuti==60) allora:
   {
      ora = ora + 1;
      minuti = 0;
      secondi = 0;
   }

   se (ore==24) allora:
   {
      ore=0;
      minuti=0;
      secondi=0;
   }  
}


Un timer può essere usato non solo per schedulare eventi in uscita, come gli esempi fatti sopra, ma anche per contare eventi d'ingresso: quante volte è stato premuto un pulsante, quanto tempo è trascorso fra uno start e uno stop di un cronometro, la frequenza di un segnale, la durata di un impulso oppure il duty cicle di un segnale rettangolare.

In tal caso, cioè quando l'impulso di clock proviene dal mondo esterno, il nome più appropriato è quello di contatore. Perciò, nella letteratura si parla indistintamente di timer/contatore a secondo del particolare contesto d'utilizzo, se per creare tempi (out) o per contare eventi (in)


 I registri dell'AVR ATmega168

Un AVR ATmegax8 (48, 88, 168, 328) contiene tre timer:

Timer0, da 8 bit
Timer1, da 16 bit
Timer2, da 8 bit



Il registro dati del timer a 16bit è costituito dall'unione di due registri a 8bit: il primo che memorizza il byte inferiore e il secondo quello superiore.

Quindi il registro dati (che contiene il numero) del Timer1 sarà composto da due registri: TNCT1L e TCNT1H.

L'intero registro dati, in alcuni linguaggi, può comunque essere direttamente accessibile mediante l'etichetta TCNT1:

Una descrizione grafica molto usata per i timer è quella di rappresentare l'andamento nel tempo del numero contenuto nel registro dati

Nel caso del timer1 (TCNT1), che conti da 0 fino al suo valore massimo, il grafico assume un andamento "a rampa crescente":

Quando il timer conta da 0 fino al massimo valore dato dal numero di bit del suo registro dati, si dice che opera in modo normale o naturale; tale modalità è in effetti poco flessibile in quanto "rigidamente" legata al numero di bit e alla frequenza di clock; esistono invece diverse modalità e tecniche che consentono di controllare in modo preciso e affidabile i tempi generati, come si vedrà più avanti.






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