This is an old revision of the document!
Acest laborator are ca scop familiarizarea voastră cu lucrul cu întreruperile hardware și cu 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.
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:
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ă. Mai multe detalii veți afla în laboratoarele următoare.
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:
În următorul tabel sunt prezentate cele două moduri pe care le vom folosi în acest laborator.
Definiții care apar în datasheet:
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 |
I
din SREG
este setat (întreruperile sunt activate global)
Pentru a configura un mod de funcționare, vom seta:
WGM
din timer-ul respectiv (care se găsesc în registrele TCCRnA
din datasheet, la secțiunile aferente timerelor, “Register Description”)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 Description → TCCR0A
CTC
ca având biții 0 1 0
pe biții WGM2..0
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;
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 Description → TCCR2B
CS..
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);
// 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
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 }
// 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() { }
Î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:
f_int = f_clk / (prescaler * (tc + 1))
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 precum:
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.
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.
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).
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.
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.) și a unei diode 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 cadrul laboratorului trecut am văzut că microcontroller-ul Atmega328 are 3 timere: Timer0 pe 8 biți, Timer1 pe 16 biți și Timer2 pe 8 biți.
Fiecare dintre aceste timere putea fi configurat în diferite moduri. În cazul timer-ului 1 configurarea se realiza prin intermediul registrelor TCCR1A și TCCR1B cu ajutorul biților WGM13 - WGM10
. Printre aceste moduri se numărau:
Atmega328p dispune de 6 canale de PWM distribuite astfel:
Din punct de vedere al microcontroller-ului Atmega328p există 3 tipuri de 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). Se utilizează pentru majoritatea aplicațiilor, mai puțin cele în care este nevoie de un control precis (de exemplu motoare BLDC, audio). Există mai multe moduri de Fast PWM oferite de către microcontroller. De exemplu, pentru Timer-ul 1 avem:
\begin{equation} f_{OCnX}=\frac{f_{clk}}{N \cdot (TOP + 1)}=\frac{f_{clk}}{N \cdot 256} \end{equation}
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.
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.
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).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();
Î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).
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 (default) |
---|---|---|---|
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
Un LED RGB este compus din 3 diode care emit culori diferite: una roșie, una verde și una culoare albastră. Cu acest LED se poate obține orice culoare printr-o combinație de intensități pe fiecare diodă în parte.
În funcție de LED-ul RGB folosit, LED-urile individuale se pot conecta în modul catod comun/“active-high” (LED-ul este aprins atunci cand pinul aferent este HIGH, si stins atunci cand pinul este LOW) sau anod comun/“active-low” (LED-ul este aprins atunci cand pinul aferent este LOW, si stins atunci cand pinul este HIGH). În ambele cazuri, se folosește câte o rezistență de limitare a curentului pentru fiecare culoare.
Servomotoarele sunt construite pe baza unui motor DC sau Brushless DC, au angrenaje integrate și un arbore (ax) care poate fi controlat cu precizie. Servo-urile standard permit poziționarea arborelui la diferite unghiuri, de obicei între 0 și 180 de grade. Există și variante cu rotație continuă ce permit modificarea precisă a vitezei de rotație a arborelui.
Servomotoarele hobby (Servo) sunt în mod uzual comandate printr-un semnal PWM cu frecvența de 50 Hz și perioada semnalului T_on în intervalul 1-2 ms, unde 1.5 ms reprezintă poziția de centru. Practic, semnalul PWM este folosit pentru a codifica și transmite poziția dorită către microcontroller-ul integrat care se ocupă de poziționarea efectivă servomotorului.
În Arduino, biblioteca Servo permite controlul servomotoarelor.
#include<Servo.h> Servo myservo; // creates the servo object void setup() { myservo.attach(9); // attaches the servo on pin 9 to the servo object } 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 } }
Alegerea parametrilor pentru timer pentru setările registrelor:
OCR1A = 31249; //counter TCCR1B |= (1 << WGM12); // CTC mode TCCR1B |= (1 << CS12); // 256 prescaler TIMSK1 |= (1 << OCIE1A); // enable timer compare interrupt
Dacă frecvența de bază a procesorului este de 16 MHz și prescalerul 256 atunci timerul are frecvența de 62500 Hz deci se va incrementa cu 1 la fiecare 1/62500 Hz = 16 μs. O întrerupere va fi generată când valoarea din timer va fi egală cu cea din registrul OCR1A (31249). 16μs×31250 = 0.5s
Task 1 (1p) Rulați exemplul din scheletul de laborator. Ce mod de funcționare folosește timer-ul? Dar prescaler-ul?
Task 2 (2p) Dorim să vizualizăm efectele prescaler-ului asupra frecvenței cu care se incrementează numărul din display-ul cu 7 segmente. Vom folosi butonul legat la PD3 pentru a cicla prin valorile posibile ale prescaler-ului.
Task 3 (3p) Realizați un cronometru de tip “countdown timer”. La finalul numărătorii, LED-ul va trebui să rămână aprins iar la apăsarea butonului se va reseta cronometrul la valoarea maximă. Valoarea maximă a cronometrului va fi de 10 secunde și se va afișa pe display valoarea corespunzătoare (9..0).
Bonus (1p): Aprindeți LED-ul pentru 200 ms la fiecare secundă cât timp cronometrul numără descrescător.
Task 4 (4p) Conectați un buzzer la pinul PD2. Folosind notele muzicale definite în repo-ul arduino-songs sau biblioteca Melody, implementați un cântec la alegere. Atunci când butonul va fi apăsat, notele vor fi ridicate cu o octavă (vor reveni la normal atunci când butonul nu mai este apăsat).
Bonus (1p) Pe măsură ce cronometrul se apropie de zero, viteza de redare va fi încetinită liniar astfel încât atunci când cronometrul ajunge la 0 viteza va fi 0.1 din valoarea normală.
Observați ceva special în montajul de mai sus modul de conectare al led-ului RGB ? (Hint: diagrama PINOUT, PWM)
La acest task veți cicla cu ajutorul PWM-ului prin toate culorile led-ului RGB.
Bonus : modificați programul folosind functia setLedColorHSV()
Rulați exemplul standard din Arduino de mai jos, prin care se modifică în mod “continuu” poziția (0-180).
Modificați programul astfel încât servomotorul să se oprească atunci când ajunge în oricare din capete. Cu ajutorul interfeței seriale, se va modifica sensul de rotație.
#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(9); // attaches the servo on pin 9 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 } }
Folosind o întrerupere de tip PCINT (Pin Change INTerrupt), modificați incremental poziția servomotorului (0-180 și 180-0) cu ajutorul butonului.