Laboratorul 3: Timere. PWM

Acest laborator are ca scop familiarizarea voastră cu lucrul cu întreruperile hardware generate de timer-ele prezente în microcontroller-ul Atmega328p. Vom folosi timer-ele pentru a număra intervale de timp și pentru a genera semnale periodice. Folosind semnale PWM vom controla intensitatea luminoasă a unui LED și poziția unui servomotor.

1. Timere

1.1. Principiul de funcționare al unui Timer

Timer-ul/Counter-ul oferă facilitatea de a măsura intervale fixe de timp și de a genera întreruperi la expirarea intervalului măsurat. Un timer, odată inițializat va funcționa independent de unitatea centrală (UCP). Acest lucru permite eliminarea buclelor de delay din programul principal.

Componentele și funcționarea unui timer pentru ATmega328p pot fi descrise în linii mari de cele trei unități din figura de mai jos:

Componentele și funcționarea unui timer pentru ATmega324

  1. Registrul numărător (Timer Counter, TCNT) - măsoară efectiv intervalele de timp și este incrementat automat cu o frecvență dată.
  2. Prescaler-ul - are rolul de a diviza în funcție de necesitățile aplicației frecvența de ceas.
  3. La fiecare incrementare a TCNT are loc o comparație între acest registru și o valoare stocată în registrul OCRn. Această valoare poate fi încarcată de către programator prin scrierea registrului OCRn. Dacă are loc egalitatea se generează o întrerupere, în caz contrar incrementarea continuă.

Timer-ele sunt prevăzute cu mai multe canale astfel încât se pot desfășura diferite număratori în paralel. ATmega328p este prevăzut cu 3 unități de timer: două de pe 8 biți și una de 16 biți.

Timer-ele pot funcționa și în moduri PWM, astfel încat să genereze pe un pin de ieșire un semnal de comandă cu tensiune (medie) variabilă.

1.2. Moduri de funcționare

Timer-ele oferă mai multe moduri de funcționare (cu mici diferențe între Timer/Counter0, Timer/Counter1 și Timer/Counter2), ce se diferențiaza prin:

  • valorile până la care se face incrementarea contorului
  • modul în care se numără (doar crescător, sau alternativ crescător/descrescător)
  • momentul când se face resetarea contorului

În următorul tabel sunt prezentate două moduri pe care le vom folosi în acest laborator.

Mod Descriere Imagine contor Caracteristici
Normal pornește de la 0
numără până la 0xFFFF
Modul Normal frecvența este fixată la câteva valori predefinite, date de frecvența ceasului și de prescaler
CTC
Clear Timer on Compare
pornește de la 0
numără până când se atinge un prag (OCRnA pentru Timer 0/2, OCR1A sau ICR1 pentru Timer 1)
Modul CTC frecvența este variabilă, determinată de valoarea pragului, frecvența ceasului și de prescaler

Definiții care apar în datasheet:

  • BOTTOM: capătul inferior din intervalul de numărare
  • TOP: capătul superior al intervalului de numărare
  • MAX: limita superioară a numărării, 255 (0xff) pentru 8 biți, 65535 (0xffff) pentru 16 biți. TOP poate fi MAX în anumite moduri de funcționare (ex: Normal).

1.3. Registre

Timer Registre Rol
Timer0
8 biți
TCNT0 Registrul contor al timer-ului 0 (cel care numără)
TCCR0A, TCCR0B Registre de control ale timer-ului 0 (diverși biți pentru configurarea timer-ului)
OCR0A, OCR0B Registre prag pentru timer-ul 0 (prag al numărătorii la care se poate declansa intreruperea, în funcție de configurare)
TIMSK0, TIFR0 Registre cu biți de activare întreruperi timer 0 / flag-uri de activare (activați întreruperile)
Timer1
16 biți
TCNT1 Registrul contor al timer-ului 1 (la fel ca la timer0, doar că pe 16 biți)
TCCR1A
TCCR1B
TCCR1C
Registre control ale timer-ului 1 (la fel ca la timer0)
OCR1A , OCR1B Registre prag pe 16 biți ale timer-ului 1 (la fel ca la timer0)
TIMSK1, TIFR1 (la fel ca la timer0)
ICR1 Registru folosit pentru a reține valoarea contorului la apariția unui eveniment extern pe pin-ul ICP sau ca prag pentru unul din modurile CTC
Timer2
8 biți
aceleași registre ca la Timer0 Diferența față de Timer-ul 0 este posibilitatea folosirii unui oscilator extern separat pentru Timer-ul 2,
pe pinii TOSC1 și TOSC2
ASSR, GTCCR Registre ce țin de funcționarea asicronă a acestui timer față de restul sistemului

Întreruperile sunt activate doar dacă bitul I din SREG este setat (întreruperile sunt activate global)

1.4. Lucrul cu Timer-ul

Setarea modului de funcționare

Pentru a configura un mod de funcționare, vom seta:

  • biții WGM din timer-ul respectiv (care se găsesc în registrele TCCRnA din datasheet, la secțiunile aferente timerelor, “Register Description”)
  • pragul de numărare

De exemplu, ca să setăm timer-ul 0 să numere până la 5, ne vom uita în datasheet la capitolul 14 (8-bit Timer/Counter0 with PWM) - secțiunea Register DescriptionTCCR0A

  1. găsim modul CTC ca având biții 0 1 0 pe biții WGM2..0
  2. aflăm că modul CTC numără până la OCRA

Presupunând că plecăm de la un registru complet neinițializat (0 este valoarea default pentru majoritatea biților), avem următorul cod:

TCCR0A |= (1 << WGM01);
OCR0A = 5;

Setarea prescaler-ului

Pentru setarea prescaler-ului se vor modifica biții tip CS.. din registrul TCCRnB al timer-ului respectiv.

De exemplu, pentru setarea valorii de prescaler 256 pentru timer-ul 2, vom urmări în datasheet capitolul 17 (8-bit Timer/Counter2 with PWM) - secțiunea Register DescriptionTCCR2B

  1. găsim tabelul pentru valorile CS..
  2. aflăm că prescaler-ul 256 corespunde biților 1 1 0 pentru biții CS22 CS21 CS20

Presupunând că plecăm de la un registru complet neinițializat (0 este valoarea default pentru majoritatea biților), avem următorul cod:

TCCR2B |= (1 << CS22) | (1 << CS21);

Configurarea întreruperilor

// exemplu de configurare pentru Timer 1 în mod CTC, care va genera întreruperi cu frecvența de 2Hz
OCR1A = 31249;            // compare match register 16 MHz/256/2 Hz - 1
TCCR1B |= (1 << WGM12);   // CTC mode
TCCR1B |= (1 << CS12);    // 256 prescaler 

Scriem rutina de tratare a întreruperii

Rutinele de tratare a întreruperii trebuie menționate în memorie la anumite adrese fixe (în vectorul de întreruperi). Pentru a face acest lucru folosim macro-ul de ISR(), care primește ca parametru tipul de întrerupere ce este tratată.

De exemplu, pentru întreruperile generate de Timer 1 în mod CTC, codul va arăta în felul următor:

// implementare rutină de tratare a întreruperii TIMER1_COMPA
ISR(TIMER1_COMPA_vect) {
  // cod întrerupere de la Timer1
}

Activăm întreruperea și testăm programul

// activare întrerupere TIMER1_COMPA
TIMSK1 |= (1 << OCIE1A);
// activare întreruperi la nivel global
sei();

Pentru un timer deja configurat, pentru a activa întreruperile trebuie doar să activăm bitul corespunzător din TIMSKx

De exemplu, pentru pragul A al timer-ului 1 vom scrie:

ISR(TIMER1_COMPA_vect) {
  // cod întrerupere 
}
 
void configure_timer1() {
  // exemplu de configurare pentru Timer 1 în mod CTC
  // care va genera întreruperi cu frecvența de 2Hz
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1 = 0;
  OCR1A = 31249;            // compare match register 16MHz/256/2Hz-1
  TCCR1B |= (1 << WGM12);   // CTC mode
  TCCR1B |= (1 << CS12);    // 256 prescaler 
}
 
void init_timer1() {
  TIMSK1 |= (1 << OCIE1A);  // enable timer compare interrupt
}
 
void setup() {
  // dezactivăm întreruperile globale
  cli();  
  configure_timer1();
  init_timer1();
  // activăm întreruperile globale
  sei();
}
 
void loop() {
 
}

1.5. Calculator

În general, pentru configurarea unui Timer, este necesară alegerea unui prescaler și a unei limite de numărare, în funcție de perioada dorită și de frecvența de lucru a procesorului (e.g. 16 MHz pentru Arduino UNO) și de modul de funcționare ales. Un exemplu de calcul este prezentat mai jos:

  • calcul frecvență întreruperi în funcție de frecvența ceasului și a pragului de numărare:

f_int = f_clk / (prescaler * (tc + 1))

  • calcul prag de numărare al timer-ului pentru a obține frecvența dorită:

tc = f_clk / (prescaler * f_int) - 1

Observăm că trebuie aleasă o valoare convenabilă pentru prescaler (din cele disponibile, ex. 8, 64, 256, 1024) și un prag de numărare (0-255 pentru timer-e pe 8 biți, 0-65535 pentru timer-e pe 16 biți) astfel încât să se obțină frecvența exactă.

Există calculatoare care pot fi utile pentru determinarea rapidă a valorilor pentru registrele de configurare ale unui Timer:

Calculatorul indică valoarea corespunzătoare intervalului de numărare. Pentru a configura valoarea de prag a timer-ului, din această valoare se scade o unitate.

2. PWM (Pulse-width Modulation)

PWM (Pulse Width Modulation) este o tehnică folosită pentru a varia în mod controlat tensiunea dată unui dispozitiv electronic. Având la dispoziție doar două niveluri de tensiune (0V și 5V), putem aproxima alte valori comutând foarte rapid între cele două niveluri.

Tensiunea primită de un dispozitiv reprezintă media acestor pulsuri și este raportul dintre perioada de timp corespunzătoare valorii ON și perioada totală dintr-un ciclu ON-OFF. Aceast raport se numește factor de umplere (duty cycle). Astfel, se pot controla circuite analogice din domeniul digital. Practic, asta înseamnă că un LED acționat astfel se va putea aprinde / stinge gradual, iar în cazul unui motor acesta se va învârti mai repede sau mai încet.

2.1. Principiul de funcționare

Factorul de umplere se exprimă în procente și reprezintă cât la sută din perioada unui semnal acesta va fi pe nivelul ON. În figura de mai jos, se pot observa semnale PWM cu factori de umplere diferiți. Astfel, se poate deduce foarte ușor formula pentru a obține valoarea factorului de umplere (D):

$D[\%] = \frac{t\_on}{t\_on + t\_off} \cdot 100 = \frac{pulse\_width}{period} \cdot 100$

Astfel, tensiunea medie care ajunge la dispozitiv este dată de relația: D * Vcc.

Figura 8. Semnal PWM cu diferiți factori de umplere

Modularea folosește variația factorului de umplere a unui semnal dreptunghiular pentru a genera la ieșire o tensiune analogică. Considerând o formă de undă dreptunghiulară f(t) cu o valoare minimă ymin=0 și o valoare maximă ymax și factorul de umplere D (ca în figură) valoarea medie a formei de undă e dată de relația:

$\bar{y} = D \cdot ymax$

Multe circuite digitale pot genera semnale PWM. Majoritatea microcontroller-elor oferă această facilitate, pe care o implementează folosind un numărător care este incrementat periodic (conectat direct sau indirect la o unitate de ceas) și care este resetat la sfârșitul fiecărei perioade a PWM-ului. Când valoarea numărătorului este mai mare decât valoarea de referință, ieșirea PWM (output-ul) trece din starea HIGH în starea LOW (sau invers).

Detalii

Detalii

Semnalul PWM poate fi generat și software prin mai multe metode: bit-banging, timer cu ISR. Practic orice metodă prin care se poate genera software secvența:

  • Set Pin High
  • Wait for T_on
  • Set Pin Low
  • Wait For T_off

Totuși, în multe situații se dorește o frecvență de ordinul kHz sau zeci de kHz a semnalului PWM (ex. controlul motoarelor DC sau Brushless DC), iar o astfel de metodă nu este cea mai eficientă, fiind necesară intervenția procesorului pentru tratarea întreruperilor frecvente.

2.2. Lucrul cu PWM-ul

Lucrul cu PWM-ul presupune inițializarea unui timer și configurarea output-ului pe pini. Fiecare timer are doi pini pe care poate genera ca output un astfel de semnal (cele două canale): Timer0 are OC0A și 0C0B, Timer1 are OC1A și OC1B etc.

Atmega328p dispune de 6 canale de PWM distribuite astfel:

  • Timer0 - OCR0A, OCR0B respectiv Timer2 - OCR2A, OCR2B - în total 4 canale pe 8 biți
  • Timer1 - OCR1A, OCR1B în total 2 canale pe 16 biți

Controller-ul dispune de 6 canale de PWM în cazul în care acestea sunt configurate în moduri ce au ca TOP valorile 0xFF respectiv 0xFFFF. Alternativ, putem configura fiecare timer (atât cele de 8 biți cât și cele de 16 biți) în moduri ce au ca TOP o valoare specificată într-unul din registrele OCRnA sau OCRnB. Un timer configurat astfel are doar un canal ce genereaza output așa cum ne-am aștepta. Și canalul corespunzător registrului ce conține valoare TOP va genera semnal, însa nu în modul dorit.

Pe Atmega328p există 3 tipuri de PWM care pot fi generate:

  • Fast PWM
  • Phase Correct PWM
  • Phase and Frequency Correct PWM

Ne vom ocupa în principal de modul Fast PWM care este utilizat pentru majoritatea aplicațiilor, mai puțin cele în care este nevoie de un control precis (de exemplu motoare BLDC, audio).

Fast PWM

Numărarea se face doar pe frontul crescător al semnalului de ceas. În modul Fast PWM, modificarea factorului de umplere se realizează instant, în schimb semnalul nu este centrat (este defazat, practic apare un “glitch” la modificarea semnificativă a factorului de umplere). Există mai multe moduri de Fast PWM oferite de către microcontroller. De exemplu, pentru Timer-ul 1 avem:

  • Fast PWM pe 8 biți, cu valoarea de TOP = 0x00FF.
  • Fast PWM pe 9 biți, cu valoarea de TOP = 0x01FF.
  • Fast PWM pe 10 biți, cu valoarea de TOP = 0x03FF.
  • Fast PWM cu valoarea de TOP în ICR
  • Fast PWM cu valoarea de TOP în OCRnA

Frecvența semnalului PWM depinde de prescaler și de frecvența oscilatorului. Din secțiunea 15.9.3 (pag 102), formula de calcul a frecvenței în modul Fast PWM 8-bit este:

\begin{equation} f_{OCnX}=\frac{f_{clk}}{N \cdot (TOP + 1)}=\frac{f_{clk}}{N \cdot 256} \end{equation}

În acest laborator vom folosi doar modul Fast PWM. Pentru variantele de Fast PWM oferite de către celelalte timere (0 și 2) și pentru celelalte moduri PWM, consultați datasheet-ul.

De exemplu, presupunem că avem Timer1 configurat pe modul de funcționare Fast PWM (modul de funcționare nu trebuie neapărat să conțină cuvântul 'PWM' ca să poată fi folosit pentru generarea unui semnal). Fast PWM este caracterizat de o frecvență fixă și un prag stabilit de programator, ce poate fi modificat în timpul execuției.

  • Modul 1 0 pentru biții COM1A1 COM1A0 va lăsa semnalul de pe pinul OC1A pe 1 în timpul numărătorii (până la atingerea pragului) și va pune semnalul pe 0 de la atingerea pragului până la capătul unui ciclu (numărare completă până la TOP).
  • Pentru a obține un factor de umplere x%, aici vom seta OCR1A = x * TOP / 100 (pentru Fast PWM, TOP poate fi 0xFF, 0x1FF sau 0x3FF, în funcție de configurația aleasă)

Exemplu de inițializare a Timer1 in modul Fast PWM 8-bit non-inverting, cu Prescaler la valoarea 1024:

CCR1A = 0;
TCCR1B = 0;
TCNT1  = 0;
//PB1 output - OC1A este PB1
DDRB |= (1 << PB1);
//pentru modul Fast PWM 8-bit, biții WGMn0 si WGMn2 au valoarea 1
TCCR1A |= (1 << WGM10);
//TCCR1A conține doar biții WGM10 si WGM11, WGM12 și WGM13 se găsesc in TCCR1B
TCCR1B |= (1 << WGM12);
//pentru modul non-inverting, COM1A1 = 1 și COM1A0 = 0
TCCR1A |= (1 << COM1A1);
//pentru Prescaler de 1024 scriem 1 pe CS12 si CS10
TCCR1B |= (1 << CS12) | (1 << CS10);
//Pragul la care se schimbă semnalul pentru a obține un factor de umplere de 0.5
//Deoarece în acest mod TOP este 0xFF, OCR1A va fi 50 * 255 / 100 = 127 
OCR1A = 127;
sei();

3. Aplicații cu PWM în Arduino

Pentru a controla un LED folosind un semnal PWM, se poate conecta la fel ca în cazul on/off, folosind o rezistență de limitare a curentului. Dacă avem un LED de putere (ex. LED-uri auto, LED-uri pentru iluminare) este necesar un driver specializat.

În cazul unui motor, căruia i se aplică un semnal PWM cu factor de umplere de 0%, viteza de rotație a acestuia va fi egală cu 0 RPM. Un factor de umplere de 100% va duce la o turație maximă a acestuia. Pentru astfel de cazuri în care avem sarcină inductivă și curenți mari (releuri, motoare, inductoare, electrovalve, etc.) este necesară utilizarea unui driver (element de comutație/tranzistor, driver motor, etc.)

Detalii

Detalii

Pentru astfel de cazuri în care avem sarcină inductivă și curenți mari, este necesară și o diodă flyback (Wikipedia Flyback diode). Semnalul PWM generat de microcontroller va comanda driver-ul (ex. baza tranzistorului printr-o rezistență) iar dioda flyback va prelua spike-urile de tensiune generate de motor/sarcina inductivă la închiderea tranzistorului (care altfel pot duce la distrugerea tranzistorului).

O altă utilizare a semnalelor PWM este de a genera semnal analogic cu aplicații în amplificare audio (de clasă D), încărcătoare de baterii, etc. Prin modificarea periodică a factorului de umplere, se pot obține semnale ce aproximează un semnal analogic (ex. o sinusoidă).

Generarea unui semnal sinusoidal folosind PWM:

Semnalele PWM pot avea și alte utilizări, precum trimiterea de comenzi către un dispozitiv de acționare. Un astfel de exemplu este servomotorul, care primește comenzi determinate de factorul de umplere al semnalului PWM pentru a controla poziția. Servo control

În continuare, ne vom folosi de biblioteca Arduino pentru a realiza niște aplicații ale semnalelor PWM: controlul unui LED RGB și controlul poziției unui servomotor hobby. Alternativ, pentru cei care doresc să aprofundeze modul de programare la nivel de registre, se pot realiza aplicațiile folosind timere în modul Fast PWM și/sau CTC în loc de bibliotecile din Arduino (analogWrite, Servo).

AnalogWrite

Funcția analogWrite din Arduino, configurează de fapt un timer în modul Fast PWM pe 8 biți și poate genera semnal PWM (doar) pe pinii asociați unuia dintre timere. analogWrite(pin_arduino, value_0_255) De exemplu, pe Arduino/Atmega328p avem următorii pini care pot genera semnal PWM folosind funcția analogWrite:

Pin Arduino Pin Atmega328p Timer output Frecvența PWM (implicit)
5 PD5 OC0B (Timer0) 980 Hz
6 PD6 OC0A (Timer0) 980 Hz
9 PB1 OC1A (Timer1) 490 Hz
10 PB2 OC1B (Timer1) 490 Hz
11 PB3 OC2A (Timer2) 490 Hz
3 PD3 OC2B (Timer2) 490 Hz

Este posibil să avem nevoie să modificăm frecvența semnalului PWM, caz în care trebuie să configurăm explicit Timer-ul (ex. prescaler, TOP) folosind registre. SecretsOfArduinoPWM

4. Exerciții

4.1. Task 0

Folosind Timer 1 de pe Arduino și operațiile cu regiștri, afișați pe interfața serială un mesaj la alegerea voastră o dată la 5 secunde.

Timerul de pe Arduino nu poate cronometra perioade atât de lungi. Veți fi nevoiți să afișați mesajul după un anumit număr de perioade, de lungime aleasă de voi. Ex: perioadă de 0.5s (2Hz), afișăm mesajul la fiecare a doua intrare în funcția asociată întreruperii de timer; perioadă de 0.1s (10Hz), afișăm mesajul la fiecare a 10-a intrare.

Alegerea parametrilor pentru timer pentru setările registrelor:

  OCR1A = 31249; // compare match register 16MHz/256/2Hz-1 => 2Hz
  TCCR1B |= (1 << WGM12);   // CTC mode
  TCCR1B |= (1 << CS12);    // 256 prescaler 
  TIMSK1 |= (1 << OCIE1A);  // enable timer compare interrupt

4.2. Task 1

Folosind codul de la exercițiul anterior, la fiecare 5 secunde veți alterna între două stări de redare a unei melodii pe buzzer-ul din montaj:

  • viteză normală și octavă normală
  • viteză dublă (noteDuration / 2) și octavă superioară

Creșterea unei octave se realizează dublând frecvența notelor. Pentru redarea unei note veți folosi funcția tone(). Urmăriți exemplele de pe acest repository arduino-songs și alegeți o melodie.

Realizați redarea melodiei în funcția loop() pentru a putea folosi întreruperile generate de timer și pentru a reda continuu melodia.

4.3. Task 2

Folosind Timer 0, ciclați led-ul RGB între mai multe culori și mișcați servo-motorul câte 10° la fiecare x secunde (alegeți voi o perioadă potrivită pentru urmărirea schimbărilor). Nu uitați de sugestia de la primul task (va trebui să numărați un număr de perioade mult mai scurte decât cea dorită).

Atenție la limitele de unghi ale servo-motorului. Depășirea acestora poate avaria servomotorul. Verificați ca unghiul să nu depășească intervalul [0°, 180°] (sau [0, pi] pentru cei ce își amintesc cu plăcere de trigonometrie :-P).

La acest task veți afișa diverse culori cu ajutorul PWM-ului și veți controla un servo-motor:

  • Cu funcția analogWrite veți seta factorul de umplere PWM pentru fiecare culoare
  • Exemplu pentru controlul unui servo-motor ce oscilează în intervalul [0°, 180°]
#include <Servo.h>

Servo myservo;  // create servo object to control a servo
// twelve servo objects can be created on most boards

int pos = 0;    // variable to store the servo position

void setup() {
  myservo.attach(3);  // attaches the servo on pin 3 to the servo object
  // test led
  DDRD |= (1 << PD7);
  PORTD &= ~(1 << PD7);
}

void loop() {
  for (pos = 0; pos <= 180; pos += 1) { // goes from 0 degrees to 180 degrees
    // in steps of 1 degree
    myservo.write(pos);              // tell servo to go to position in variable 'pos'
    delay(15);                       // waits 15ms for the servo to reach the position
  }
  for (pos = 180; pos >= 0; pos -= 1) { // goes from 180 degrees to 0 degrees
    myservo.write(pos);              // tell servo to go to position in variable 'pos'
    delay(15);                       // waits 15ms for the servo to reach the position
  }
}

Bonus : modificați programul folosind functia setLedColorHSV()

  • Funcția setLedColorHSV permite modificarea culorii folosind reprezentarea alternativă HSV (Hue Saturation Value), fiind mai ușor apoi de modificat culoarea, saturația și intensitatea luminoasă. setLedColorHSV
  • Setați valorile pentru saturație (s) și intensitate (v) pe 1 și modificați culoarea (h) în intervalul 0-360
  • Pentru a urmări corespondența dintre cele 2 reprezentări (RGB și HSV) există selectoare de culori precum Online Color Picker

5. Resurse

6. Linkuri utile

pm/lab/lab3-2023.txt · Last modified: 2023/03/27 11:26 by narcis_florin.caroi
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0