PrettyPrint

domenica 5 settembre 2010

Timer AVR in 5min: 5) Gen. forme d'onda PWM (fast)

< Timer AVR in 5min: 4) Generare forme d'onda con CTC Timer AVR in 5min: 6) Generare forme d'onda PWM (a correzione fase e freq.) >


Ultima revisione: 26/Apr/2012 

pwm1.zip


La modalità CTC consente di generare forme d'onda rettangolari, ma soltanto simmetriche (tempo di on uguale a quello di off ossia duty cicle del 50%).

Forme d'onda rettangolari con duty cicle diverso dal 50% possono essere generate con un'altra modalità di funzionamento del timer: la PWM (Pulse Width Modulation: modulazione a larghezza d'impulso).

Sul Timer1 dell'ATmega8 si possono usare tre tipologie di PWM:

1) Fast
2) A correzione di fase
3) A correzione di fase e di frequenza.

La 1) è la più veloce, consente di raggiungere frequenze più alte delle restanti due; al variare del duty cicle il periodo (ovvero la frequenza) rimane costante, ma varia la fase;
la 2) e la 3), invece, non comportano una variazione della fase al variare del duty cicle e dunque si prestano per applicazioni quali il pilotaggio di motori, laddove non sono richieste elevate frequenze di funzionamento ma la fase deve mantenersi costante.

Un possibile compito, che vede impiegata la FastPWM, potrebbe essere il seguente:

Generare, con il timer1 da 16bit, il sequente segnale rettangolare (d.c.=25%) avente frequenza di 1 kHz, usando come clock quello di sistema (fc=4MHz):

Nella modalità Fast PWM, come già detto, il periodo Ts rimane costante al variare del duty cicle ed è determinato dalla risoluzione del timer.

La risoluzione può essere scelta fra alcuni valori fissi (8bit, 9 bit o 10 bit) oppure variabile, da 2 bit fino alla risoluzione massima del timer1: 16bit.

Nella risoluzione fissa, il timer dunque conta da 0 fino a un numero N pari a 2n-1 :

N=255 (8bit),
N=511 (9bit)
N=1023 (10bit)

La durata dell'intero periodo Ts dipende da questi valori, dal periodo di clock, dal prescaler, secondo la nota formula già vista:



Con la nostra frequenza di clock fc di 4MHz (Tc=0,25us), per i vari valori di prescaler, si ottengono i seguenti valori di Ts (espressi in ms) per le varie risoluzioni fisse del timer:


Come si può facilmente constatare, non abbiamo nessun valore di Ts=1ms.

Solo con N=511 (9bit) e prescaler :8 ci avviciniamo a 1ms (1,024ms). Se non è richiesta una precisione spinta, potremmo allora "accontentarci" di un valore praticamente uguale a quello assegnato e configurare il timer1 in modo FastPWM con risoluzione fissa da 9bit.

Supponendo, invece, di volere esattamente un valore del periodo del segnale pari a 1ms, dobbiamo allora ricorrere alla risoluzione variabile. Impostando il modo PWM con risoluzione variabile bisogna determinare una risoluzione compresa fra 2 bit (quindi il timer conta fino a N=3) e quella massima consentita dal timer: 16bit nel caso del Timer1 (e quindi conta fino a N=65535).

Quindi, in definitiva, se impostiamo la risoluzione variabile il valore N può essere compreso fra 3 e 65535.

Quale sia un possibile valore di N per avere Ts=1ms, lo ricaviamo dalla formula inversa:



Per k=1, 8, 64, 256, 1024, e con Ts=0,25 us (fc=4MHz)ricaviamo:








k N
1 3.999
8 499
64 61,5
256 14,63
1024 2,906



Sono utilizzabili solo i prescaler 1 e 8, perché danno come risultati dei numeri, come appena detto, compresi fra 3 e 65535.

Usiamo k=1 (nessun prescaler) e dunque N=3999

Tale numero N va caricato nel registro ICR1

Definito il numero necessario per avere l'intero periodo Ts, rimane ora da determinare il numero (compreso nel nostro caso fra 0 e 3999) in corrispondenza del quale si ha la commutazione del pin (OC1A oppure OC1B) quando Ton=0,25ms.

Se vogliamo generare la nostra forma d'onda usando il canale A, dunque segnale in uscita sul pin OC1A, caricheremo questo valore nel registro OCR1A; viceversa, se vogliamo usare il canale B, dunque forma d'onda sul pin OC1B, caricheremo questo valore nel registro OCR1B.

Supponendo di volere usare il canale A (pin OC1A), la situazione è raffigurata di seguito:



Finché il dato del Timer1 (ovvero il valore del registro TCNT1) è inferiore al numero NA, l'uscita OC1A assume valore alto (es. 5V). Allorquando il valore di TCNT1 eguaglia quello contenuto nel registro OCR1A, al successivo impulso di clock, si ha una commutazione del pin OC1A a 0V.

Infine, al raggiungimento del successivo valore N, il pin OC1A commuta a VH (es. 5V)

Il registro di conffronto (OCR1A nel nostro caso) viene sincronizzato a ogni successivo valore 0 del registro TCNT1, questo per evitare asimmetrie nella forma d'onda generata.

Da precisare che questa è la modalità non invertente. E' possibile configurare la generazione della forma d'onda anche in modalità invertente, per la quale, ovviamente, i livelli di commutazione sono invertiti e quindi è invertita anche la forma d'onda.


Resta da determinare il numero NA in corrispondenza del quale si ha che il tempo trascorso dall'ultimo reset del Timer1 è proprio Ton.

E' molto semplice.

Se per impiegare un tempo Ts, il timer deve contare fino a N, evidentemente per un tempo Ton più piccolo il timer dovrà contare fino a un numero NA < N:



Dunque, per calcolare Ton:


mentre per il periodo Ts, già si è visto che:



Facendo il  rapporto Ton/Ts ricaviamo così il duty cicle e da qui il numero NA:


Nel nostro caso, essendo d.c.=0,25 e N=3999, ricaviamo NA=999

Abbiamo tutti i dati per scrivere il software, partendo dal seguente pseudocodice:

  1. Carica NA=999 nel registro OCR1A;
  2. Imposta il modo FastPWM a risoluzione variabile (e valore N definito nel registro ICR1);
  3. Imposta forma d'onda non invertita;
  4. Carica N=3999 nel registro ICR1;
  5. Imposta prescaler :1;


1. Carica NA=999 nel registro OCR1A;


Per il caricamento di questo valore nel registro OCR1A, conviene definire precedentemente una label NA corrispondente al numero 999, con il solito commento indicante il dyty cicle/Ton che si ottiene in corrispondenza di esso:


#define NA 999  // d.c. 25% (Ton=.25ms) @fc=4MHz :1

OCR1A = NA;


2. Imposta il modo FastPWM a risoluzione variabile (e valore N definito nel registro ICR1)



Con l'ausilio del datasheet dell'ATmega168, si va a vedere su quali registri di configurazione del timer1 agire e quali bit settare, per ottenere la modalità PWM desiderata.

Dalla tabella 16-4 del datasheet, si legge che la modalità Fast PWM con risoluzione variabile e con valore N ("TOP" secondo la convenzione del datasheet) specificato in ICR1, viene attuata attivando i bit WGM13, WGM12 e WGM11 (modo 14):







I bit WGM13 e WGM12 sono nel  registro B di configurazione del timer1 (TCCR1B):




mentre il restante bit WGM11 si trova nel  registro A di configurazione (TCCR1A):






Dunque scriveremo:

TCCR1B |= (1 << WGM13) | (1 << WGM12); 
TCCR1A |= (1 << WGM11);



3. Imposta forma d'onda non invertita


Vogliamo che la nostra forma d'onda sia non invertita, cioè così come quella corrispondente alla definizione di duty cicle (livello di tensione alto durante Ton, e livello 0V durante Toff) e per questo bisogna guardare la tabella 16-2, che ci dice come impostare i bit COM1A1 e COM1A0 (ovvero COM1B1 e COM1B0 se ci riferiamo all'uscita OC1B) del registro TCCR1A, per ottenere il comportamento desiderato dell'uscita, nella modalità Fast PWM, quando il timer raggiunge il numero contenuto nel registro di confronto OCR1A:







Quindi, attiveremo il solo bit COM1A1:

TCCR1A |= (1 << COM1A1); 

4. Carica N=3999 nel registro ICR1;


Per questa istruzione seguiamo lo stesso metodo usato per caricare il registro OCR1A, con etichetta e commento relativo al tempo Ts che si ottiene in corrispondenza di esso:

#define N 3999 // Ts=1ms @fc=4MHz :1


5. Imposta prescaler :1


Infine, l'impostazione del prescaler, che è decisa dalla terna di bit CS12, CS11 e CS10 del registro TCCR1B così come definito nella Tab. 16-5 del d/sheet:





Per avere un prescaler :1 (nessun prescaler) bisogna dunque attivare il solo bit CS10:

TCCR1B |= (1 << CS10);



Il sorgente definitivo è dunque:

#include <avr/io.h>

#define N 3999 // Ts=1ms @ fc=4MHz:1
#define NA 999 //  d.c.=25% (Ton=.25ms) @ fc=4MHz:1

int main (void)

{

  DDRB |= (1 << PB1);  // imposta pin PB1(OC1A) come uscita
  OCR1A = NA;  // carica OCR1A con il valore NA
  TCCR1B |= (1 << WGM13) | (1 << WGM12); // FastPWM con ICR1=N (Tab.16-4)
  TCCR1A |= (1 << COM1A1) | (1 << WGM11) ;  // Non inv. (Tab.16-2) | FastPWM con ICR1=N (Tab.16-4)
  ICR1 = N; // carica ICR1 con il valore N
  TCCR1B |= (1 << CS10); // Nessun prescaler (Tab.15-5)

  while(1)
  {
   /* il compito viene svolto completamente via hardware dal Timer1.. */
  }

}


SIMULAZIONE CON VMLAB

I risultati della simulazione sono riportati di seguito. In allegato, lo zip con tutti i file del progetto VMLAB. Per usare VMLAB sotto Linux seguire il relativo tutorial

Misura di Ton (0,25 ms)


Misura di Ts (1ms)






< Timer AVR in 5min: 4) Generare forme d'onda con CTC Timer AVR in 5min: 6) Generare forme d'onda PWM (a correzione fase e freq.) >





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)
- Libreoffice Calc (calcolo valori prescaler ris. fissa)



Ultima revisione: 26/Apr/2012 

sabato 14 agosto 2010

Timer AVR in 5min: 4) Generare forme d'onda con CTC

< Timer AVR in 5min: 3) VMLAB e linux Timer AVR in 5min: 5) Gen. forme d'onda PWM (fast) >




Ultima revisione: 26/Apr/2012 


quadra.zip


Supponiamo di avere il seguente compito:

Generare, con il timer1 da 16bit, un segnale rettangolare simmetrico (DC=50%) e unipolare avente frequenza di 1 kHz, usando come clock quello di sistema fc=4MHz

La commutazione del livello logico di un pin di uscita, ad intervalli di tempo regolari T, può essere vista come un'onda rettangolare simmetrica e unipolare (livelli di tensione 0V/VH):


Il periodo dell'onda quadra, che chiamiamo Ts, è evidentemente sempre pari a 2*T, ove T è il nostro tempo generato con il timer.

Nel compito assegnato, dunque abbiamo Ts=1/1000=1ms da cui ricaviamo T=0,5ms

A questo punto, possiamo prendere il software che abbiamo già scritto per il lampeggio del LED cambiando semplicemente il numero N calcolandolo per avere un tempo di 0,5ms invece di 1s

C'è però una soluzione migliore.

Con gli AVR (così come con altri micro) è possibile effettuare la commutazione del livello di uscita di un pin, non solo via software come si è fatto nella I parte (mediante un interrupt e un'operazione di ex-or) ma anche completamente via hardware, con tutti i vantaggi del caso.

Se, quindi, il nostro compito è semplicemente quello di commutare il livello di un piedino di uscita a intervalli di tempo regolari ovvero, in altre parole, generare un'onda quadra simmetrica è preferibile utilizzare sempre il modo CTC già visto, ma con un meccanismo hardware invece di uno software.

Nel modo CTC con meccanismo hardware non ci sono interrupt e relative routine di gestione da scrivere nel software; il modo CTC fa sempre il suo lavoro: allorquando il dato contenuto nel registro del timer1 TCNT1 eguaglia quello del registro OCR1A (1), al successivo impulso di clock avviene l'azzeramento del timer e il conteggio riparte.

Quello che cambia è cosa fare all'occorrenza dell'azzeramento: non si usa più un interrupt, ma avviene (totalmente via hardware) una commutazione del livello logico di un particolare piedino d'uscita chiamato OC1A; oppure, se configuriamo diversamente il nostro software, come vedremo, l'azzeramento ( ovvero V(t)=0V) o l'attivazione dell'uscita (ossia V(t)=VH, tipicamente 5V).

A differenza del "meccanismo software", che usa un interrupt, dove si è liberi di scegliere non solo cosa fare ogni T secondi, ma anche il piedino sul quale avere la commutazione, usando il modo CTC con il meccanismo hardware si è però vincolati a usare un preciso pin, riportato nel datasheet con il riferimento OC1A (che guardacaso negli ATmegax8 corrisponde proprio con PB1):



Il periodo Ts del segnale è



e quindi il tempo T del nostro timer, come già detto, dovrà essere pari a:



Determiniamo il numero N da caricare nel registro OCR1A per i diversi valori di prescaling: 1, 8, 64, 256 o 1024 tramite la nota formula:





ove il periodo Tc è immediatamente noto come reciproco della frequenza:



Calcoliamo per ogni valore di k, il relativo numero N:







k N
11.999
8249
64 30,25
2566,81
1024 0,95



A questo punto prendiamo in considerazione solo fattori di prescaler per i quali abbiamo numeri N rappresentabili dal timer a 16 bit (numeri interi e non superiori a 65535), quindi scartiamo da subito k=64, k=256 e k=1024.

Rimangono infine k=1 e k=8, dei due preferiamo il più piccolo (nessun prescaler) in corrispondenza del quale N=1999.

Scriviamo il pseudocodice:

  1. Imposta modo CTC (con confronto sul registro OCR1A) sul Timer1;
  2. Carica N nel registro OCR1A;
  3. Imposta la commutazione hardware del pin OC1A successivamente all'azzeramento del timer
  4. Imposta prescaler a 1 (nessun prescaler);


Vediamo le istruzioni, passo passo:

1. Imposta modo CTC (con confronto sul registro OCR1A) sul Timer1

E' la stessa configurazione già vista per il modo CTC con interrupt; bisogna attivare il solo bit WGM12 che si trova nel registro TCCR1B:

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



2. Carica N nel registro OCR1A;


L'istruzione di caricamento è immediata:
OCR1A = N;
ove N è la solita etichetta simbolica, che va definita all'inizio, con il consueto commento esplicativo sul tempo generato in corrispondenza del numero N:

#define N 1999 // T=0,5ms @4MHz :1



3. Imposta la commutazione hardware del pin OC1A successivamente all'azzeramento del timer


L'impostazione della commutazione hardware, avviene impostando i bit COM1A1 e COM1A0 del registro TCCR1A, che definiscono "cosa fare" sull'azzeramento del timer: 1) commutare l'uscita (toogling), 2) azzerare l'uscita, 3) settare l'uscita, come riportato nella tabella successiva:











che in codice avr-libc si traduce come:

TCCR1A |= (1 << COM1A0);  // Imposta toogle su OC1A




4. Imposta prescaler a 1 (nessun prescaler);

 
Dalla tabella 16-5 riportata nel datasheet dell'Atmega168, ricaviamo che bisogna attivare il solo bit CS10 del registro TCCR1B:




TCCR1B |= (1 << CS10) ; // nessun prescaling (k=1)




Combinando i vari frammenti di codice, siamo ora in grado di scrivere il software definitivo:



#include <avr/io.h>

#define N 1999  // T=0,5ms @4MHz :1

int main (void)

{


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

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

   OCR1A = N;  // carica N nel registro OCR1A

   TCCR1A |= (1 << COM1A0);  // Imposta funzione di commutazione su OC1A

   TCCR1B |= (1 << CS10) ; // nessun prescaling (k=1)



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



   while(1)

      {

        /* non fa niente...*/
        /* sul reset del timer (dopo il numero 1999) */
        /* avverrà la commutazione hardware del pin OC1A */

      }

Simuliamo il nostro software con vmlab (come già spiegato precedentemente) e il risultato è appunto un'onda quadra di 1kHz.




Per misurare la frequenza, cliccare sulla traccia e quindi sul pulsante Analyze.

Nello zip allegato, oltre al sorgente, c'è anche il file progetto (QUADRA.PRJ) per VMLAB.

----
Nota 1) E' possibile utilizzare, in alternativa al registro OCR1A, il registro ICR1 (v. datasheet per la diversa configurazione)

< Timer AVR in 5min: 3) VMLAB e linux Timer AVR in 5min: 5) Gen. forme d'onda PWM (fast) >





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)



mercoledì 11 agosto 2010

Timer AVR in 5min: 3) VMLAB e linux

< Timer AVR in 5min: 2) Il modo CTC Timer AVR in 5min: 4) Generare forme d'onda con CTC >




Ultima revisione: 26/Apr/2012 


timer1.zip


VMLAB è un software di simulazione di micro (AVR e ST62) per Windows, completamente gratuito, che si appoggia su WinAVR (l'ambiente di sviluppo gratuito per AVR su Windows).

La sua peculiarità è quella di avere anche un oscilloscopio, mediante il quale è possibile fare delle misure di tempo sulle forme d'onda generate dal micro.

VMLAB si installa e funziona bene anche sotto Linux (grazie a Wine).
 
In ambiente Linux non essendoci WinAVR bisognerà provvedere manualmente alla compilazione, al collegamento ecc. dei vari file.

Dopo una rapida occhiata su Google, ho proceduto in questo modo:

1. Scaricare e installare (tramite Wine) VMLAB, disponibile all'indirizzo: http://amctools.com/download.htm

2. Successivamente scaricare e copiare nella propria home dir. il nuovo modello per l'ATmega168B già modificato per Linux  (curato da ThVortex)

Quindi decomprimerlo nella cartella di installazione di VMLAB. Da  bash shell:

$cd
~$ unzip -o useravr-0.6.zip -d ~/.wine/drive_c/VMLAB


N.B. Senza la modifica del punto 2, la simulazione del Timer1, in alcuni contesti di utilizzo, non avviene correttamente (e questo vale anche sotto Windows).

3.  assicurarci di avere installato sul nostro sistema il toolchain per lo sviluppo AVR con gcc. Con Debian il comando è:

# apt-get install gcc-avr binutils-avr avr-libc gdb-avr avrdude


(Con Ubuntu ricordarsi che c'è sudo)

Installato il necessario, si passa alla preparazione del makefile.

Per la generazione dei file necessari mi sono affidato all'ottimo Makefile di Winavr; l'ho solo modificato un poco per generare anche il file COFF con l'all, e per "spostare" tutta la parte di configurazione (micro, frequenza di clock, file, directory) in un altro file (che ho chiamato config) così da non mettere mani nel Makefile.

Il mio suggerimento è innanzitutto quello di crearsi una directory in cui mettere i propri lavori di Vmlab. Ogni progetto avrà dunque la propria directory nella quale ci sono tutti i file sorgenti, header, le librerie ecc. necessarie.

Creiamo allora una cartella nella nostra home: vmlabproj:

~$ mkdir vmlabproj


e una dir. in cui mettiamo il nostro primo progettino, di nome timer1:

~$ cd vmlabproj
~/vmlabproj$ mkdir timer1


In questa dir. ci copiamo dunque il file sorgente principale, quello dove c'è la main(), che chiamiamo sempre main.c e i file makefile e config. Il makefile non va modificato, il config, sì, secondo le proprie esigenze.

Il main.c è il nostro software per accendere e spegnere il LED ogni sec. che avevamo già scritto:


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

#define N 62499 // 1s @ 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 OCR1A
  TCCR1B |= (1 << CS11) | (1 << CS10) ; // abilita prescaler (fc/64)

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

  sei();  // abilita gli interrupt

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


/* Routine gestione interrupt (viene eseguita ogni secondo)*/

ISR( TIMER1_COMPA_vect )
{
   PORTB ^=  (1 << PB1) ;  // Se PB1=0 allora PB1=1, se PB1=1 allora PB1=0
}


Il file config è riportato di seguito:

# MCU name

MCU = atmega168

# Processor frequency (in Hz)

F_CPU = 4000000

# Target file name (without extension)

TARGET = main

# Object files directory
#     To put object files in current directory, use a dot (.), do NOT make
#     this an empty or blank macro!

OBJDIR = .

# List C source files here. (C dependencies are automatically generated.)
# The first one must be TARGET above defined, the other ones
# must be separated by a space. Examples:
# SRC = $(TARGET).c lcd.c i2c.

SRC = $(TARGET).c


# List C++ source files here. (C dependencies are automatically generated.) spaced

CPPSRC =

# List Assembler source files here.
#     Make them always end in a capital .S.  Files ending in a lowercase .s
#     will not be considered source files but generated files (assembler
#     output from the compiler), and will be deleted upon "make clean"!
#     Even though the DOS/Win* filesystem matches both .s and .S the same,
#     it will preserve the spelling of the filenames, and gcc itself does
#     care about how the name is spelled on its command-line.

ASRC =



# List any extra directories to look for include files here.
#     Each directory must be seperated by a space.
#     Use forward slashes for directory separators.
#     For a directory that has spaces, enclose it in quotes.

EXTRAINCDIRS =

# List any extra directories to look for libraries here.
#     Each directory must be seperated by a space.
#     Use forward slashes for directory separators.
#     For a directory that has spaces, enclose it in quotes.

EXTRALIBDIRS =




In MCU si definirà il tipo di micro (atmega168, atmega328, attiny2313 ecc.),

in F_CPU la frequenza di lavoro in Hz (nel nostro caso 4000000 Hz, cioè 4MHz).

TARGET è il solo nome (senza estensione) del nostro file principale.

OBJDIR lasciamo il punto (i file oggetto verranno copiati nella stessa directory).

In SRC, se abbiamo più file sorgenti (es. lcd.c, i2c.c, serial.c ecc.) li specificheremo dopo il target, separandoli con uno spazio.

Stesso discorso per CPPSRC, che riguarda i file C++. Le altre definizioni le useremo se i nostri file si trovano in altre directory, se abbiamo anche file asm ecc.

Con il config preparato, portiamoci nella dir. del nostro progetto e lanciamo un make

~$ cd vmlabproj/timer1
~/vmlabproj/timer1$ make


Dopo questo comando dovremmo trovarci nella nostra directory di lavoro alcuni file, fra i quali quelli che interessano a VMLAB sono:

main.cof
main.elf


Nel caso volessimo cancellare i file generati, basterà il classico clean:

~/vmlabproj/timer1$ make clean


Fatto questo creiamo il file progetto per VMLAB, con un editor di testi, TIMER1.PRJ

.MICRO     "ATmega168b" "CKSEL=0000 CKDIV8=1"
.CLOCK     4MEG
.TOOLCHAIN "GENERIC"
.TARGET    "main.hex"
.COFF      "main.cof"
.STORE 5   ; Time stop della simulazione

; ------------------------------------------------------------

D1 VDD K        ; D1 (LED), morsetto 1: A del LED (obbligatoriamente su VDD), morsetto 2: catodo
R1 K PB1 0.62K  ; R1 (620Ohm), morsetto 1 sul K del LED, morsetto 2 su PB1
.PLOT V(PB1)

Il ";" inizia un commento e quindi tutto quello che cìè scritto dopo è ignorato da VMLAB.

Nelle prime direttive si vanno a definire il micro, la frequenza di clock in MHz, i file che abbiamo generato e il tempo fino al quale vogliamo far eseguire i calcoli di simulazione. Dal momento che il nostro timer genera un periodo di 2 secondi ci si può fermare a po' più del doppio del periodo.

La seconda parte, invece, è quella che riguarda il nostro LED collegato all'uscita PB1

VMLAB mette a disposizione diversi componenti (fino a 8 LED, slide, tastierino ecc.), che possono essere richiamati dall'utente con una particolare sintassi (vedi help).

La sintassi è molto simile a quella di Spice.

Per il LED, la sintassi è:

Dx VDD nomecatodo


Con x che va da 1 a 8, a secondo del LED che vogliamo usare.

L'anodo del LED (il morsetto n.1) deve essere sempre collegato all'alimentazione VDD. Il secondo morsetto del LED (il catodo) lo chiamiamo, per esempio, "K" e lo colleghiamo all'uscita PB1, mediante la resistenza R1 di 620 Ohm.

Questo perché VMLAB impone di collegare +5V sull'anodo e dunque il nostro LED (come già avevamo scelto in partenza) si accenderà sul livello logico 0.

Lo schema quindi è:




L'ultima direttiva, chiede a VMLAB di visualizzare sull'oscilloscopio la tensione sul piedino PB1.

Avviamo VMLAB dal menù Wine...e apriamo il file progetto, dalla barrà dei menù con:
Project -> Open.

Visualizziamo sia il "Panel control" (dove ci sono tutte le periferiche di I/O e quindi anche i LED) e l'oscilloscopio con i comandi, con:

View -> Scope
View -> Control Panel


e lanciamo un build...che in effetti è solo un build per VMLAB, in quanto i file già sono stati "buildati" manualmente con make...

Project -> Project Build

A questo punto lanciamo il Run (premendo F5) e osserviamo il segnale PB1 sull'oscilloscopio.

Notiamo subito che la simulazione di VMLAB non è in tempo reale: il LED non rimane acceso/spento per 1s, ma i tempi computati sull'oscilloscopio sono invece esatti.

Considerati i tempi in gioco, conviene impostare l'oscilloscopio su un Time/DIV di 50ms/DIV e su "Full View".

Ci sono due cursori a disposizione per la misura dei tempi, il primo viene attivato cliccando sul bottone 1 e ha lo stile di una linea continua, il secondo viene attivato cliccando sul bottone 2 e ha lo stile di una linea tratteggiata. Sotto questi bottoni, viene riportata la differenza di tempo: t2-t1

La misura dei tempi avverrà per il tempo in cui l'uscita è bassa e per il tempo in cui l'uscita è alta.

Ovviamente occorrerà fermare la simulazione a un istante in cui si visualizza tutto il semiperiodo, fare la misura e poi riavviarla fino a visualizzare per intero il successivo semiperiodo.

I risultati della simulazione sono riportati di seguito e come si può notare i tempi risultano perfettamente uguali a 1s.

Tempo "on" - LED acceso (PB1=0V):



Tempo "off" - LED spento (PB1=5V)



Buon divertimento!



timer1.zip


< Timer AVR in 5min: 2) Il modo CTC Timer AVR in 5min: 4) Generare forme d'onda con CTC >



I contenuti di questo documento, sono stati resi possibili grazie ai seguenti strumenti gratuiti:
- FidoCADJ (disegno grafici)
- Google Code Prettify (sorgenti con sintassi evidenziata)
- gEDA (disegno schemi elettrici)
- Forum VMLAB: http://www.amctools.com/cgi-bin/yabb2/YaBB.pl?board=news
- Tutorial VMLAB: http://www.scienceprog.com/using-vmlab-as-virtual-oscilloscope/



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)