Întreruperile permit microcontrolerului să reacționeze imediat la evenimente externe, fără a irosi timp verificând permanent starea pinilor. Sunt mecanismul care face diferența dintre un sistem care ratează evenimente și unul care răspunde garantat.
1Obiectivele lucrării
- Înțelegerea diferenței dintre interogare ciclică (polling) și întrerupere
- Configurarea întreruperilor externe INT0 și INT1 prin
EICRAșiEIMSK - Scrierea corectă a unei rutine de tratare (ISR)
- Folosirea calificatorului
volatileși înțelegerea motivului - Măsurarea latenței de răspuns în ambele abordări
2Materiale necesare
- 1 Arduino Uno
- 1 Breadboard
- 2 Butoane
- 2 LED-uri + rezistoare 220 Ω
- 6 Fire jumper
3De ce sunt necesare întreruperile
Într-un program clasic, verificarea unui buton se face în loop(). Dacă bucla conține și
alte operații care durează - o afișare pe LCD, o conversie ADC, un delay() - evenimentul
poate fi ratat complet.
loop() care conține delay(200)
verifică butonul de doar 5 ori pe secundă. O apăsare scurtă, de 80 ms, are aproximativ 60% șanse să
cadă complet în interiorul unei pauze - și să fie pierdută fără urmă.Întreruperea rezolvă problema la nivel hardware: la apariția evenimentului, execuția normală este suspendată imediat, se execută rutina de tratare, apoi programul continuă exact de unde a rămas.
4Mecanismul de tratare
- Apare evenimentul (front pe un pin, depășire de timer, terminarea unei conversii ADC etc.)
- Se setează automat fanionul de întrerupere corespunzător
- Dacă întreruperile sunt activate global (
sei()) și local, procesorul termină instrucțiunea curentă - Adresa de revenire se salvează pe stivă, iar întreruperile globale se dezactivează
- Se sare la adresa din tabela vectorilor de întrerupere
- Se execută ISR-ul; la
reti()contextul se restaurează și întreruperile se reactivează
5Polling vs. întrerupere
Apăsați butonul (sau direct pe grafic) pentru a genera un eveniment extern. Măriți durata sarcinii CPU și observați cum crește latența în varianta cu polling, în timp ce întreruperea răspunde instantaneu, indiferent de încărcare.
| Criteriu | Polling | Întrerupere |
|---|---|---|
| Latență tipică | cât durează o iterație a buclei | sub 5 µs |
| Latență în cel mai rău caz | durata întregii bucle | durata celui mai lung ISR activ |
| Consum de procesor | continuu, chiar și fără evenimente | doar la apariția evenimentului |
| Complexitatea codului | simplă | necesită volatile și atenție la secțiuni critice |
| Potrivit pentru | semnale lente, verificări periodice | evenimente scurte, rare, critice în timp |
6Sursele de întrerupere pe ATmega328P
| Sursă | Pin Arduino | Pin AVR | Vector ISR | Observații |
|---|---|---|---|---|
| INT0 | D2 | PD2 | INT0_vect | front configurabil |
| INT1 | D3 | PD3 | INT1_vect | front configurabil |
| PCINT0…7 | D8–D13 | portul B | PCINT0_vect | orice schimbare, grupat pe port |
| PCINT8…14 | A0–A5 | portul C | PCINT1_vect | orice schimbare |
| PCINT16…23 | D0–D7 | portul D | PCINT2_vect | orice schimbare |
| Timer1 compare A | - | - | TIMER1_COMPA_vect | vezi Laboratorul 4 |
| ADC complet | - | - | ADC_vect | vezi Laboratorul 5 |
| USART recepție | - | - | USART_RX_vect | vezi Laboratorul 6 |
7Configurarea regiștrilor
| ISC01 | ISC00 | Declanșare INT0 | Echivalent Arduino | Utilizare |
|---|---|---|---|---|
| 0 | 0 | nivel LOW (cât timp pinul e 0) | LOW | trezire din sleep; se redeclanșează continuu |
| 0 | 1 | orice schimbare de nivel | CHANGE | encodere, numărare de fronturi duble |
| 1 | 0 | front descendent (1 → 0) | FALLING | butoane cu pull-up |
| 1 | 1 | front crescător (0 → 1) | RISING | butoane cu pull-down, senzori activi pe HIGH |
Modificați biții de mai jos și urmăriți valoarea rezultată a regiștrilor. Configurația implicită corespunde detectării frontului descendent pe INT0, cu întreruperea activată.
EIFR |= (1 << INTF0);sei()
îl setează, cli() îl șterge. Hardware-ul îl șterge automat la intrarea într-un ISR și îl
restaurează la reti() - de aceea, implicit, un ISR nu poate fi întrerupt de altul.8De ce este obligatoriu volatile
Compilatorul optimizează codul presupunând că știe tot ce se întâmplă cu o variabilă. Un ISR se execută însă „din afara" fluxului normal, iar compilatorul nu poate anticipa acest lucru.
bool gata = false; // LIPSESTE volatile!
ISR(INT0_vect) { gata = true; }
void loop() {
while (!gata) { } // compilatorul vede ca "gata" nu se modifica
// in bucla, deci o incarca O DATA intr-un
// registru CPU si testeaza la infinit
// aceeasi copie -> BUCLA INFINITA
gata = false;
digitalWrite(13, HIGH); // nu se ajunge niciodata aici
}
-O2. Sunt exact tipul de erori care apar „doar pe placa reală"
și dispar la depanare. volatile forțează recitirea variabilei din memorie la
fiecare acces.Accesul atomic la variabile multi-octet
ATmega328P este un procesor pe 8 biți. Citirea unei variabile de 16 sau 32 de biți se face în mai mulți pași. Dacă întreruperea intervine între ei, rezultatul combină jumătăți din valori diferite:
volatile uint16_t contor = 0; // 2 octeti pe un procesor de 8 biti
ISR(INT0_vect) { contor++; }
uint16_t citesteSigur() {
uint16_t v;
uint8_t sreg = SREG; // salveaza starea intreruperilor
cli(); // sectiune critica: opreste intreruperile
v = contor; // citire atomica, in 2 pasi neintrerupti
SREG = sreg; // restaureaza starea anterioara
return v;
}
cli() urmat de sei()
ar activa întreruperile chiar dacă apelantul le avea deja dezactivate. Salvarea și restaurarea lui
SREG păstrează starea exactă.9Simulator: fluxul unui ISR
Urmăriți ce se întâmplă când evenimentul apare chiar în mijlocul unui
delay(). Observați că ISR-ul se execută imediat, iar programul principal reia exact de
unde a fost întrerupt.
loop(), unde durata nu
afectează sistemul de întreruperi.10Reguli obligatorii pentru un ISR
- ISR-ul trebuie să fie foarte scurt - de regulă doar setează un fanion sau incrementează un contor
- Nu folosiți
delay()- se bazează pe întreruperi de timer, care sunt dezactivate - Nu faceți nicio operație lentă de intrare-ieșire - de exemplu trimiterea de text către calculator, pe care o veți studia la lucrarea a șasea: durează mult și folosește ea însăși întreruperi
- Orice variabilă partajată cu programul principal trebuie declarată
volatile - Citirea variabilelor pe 16/32 de biți în programul principal trebuie protejată
millis()poate fi citit în ISR, dar nu avansează în timpul execuției lui
| Ce faceți în ISR | Ce faceți în loop() |
|---|---|
| incrementarea unui contor | afișarea pe serial |
| ridicarea unui fanion | calcule cu virgulă mobilă |
| citirea unui registru care se pierde | scrierea pe LCD sau pe card SD |
| salvarea unui moment de timp | orice durează peste câteva microsecunde |
11Cod sursă
Varianta Arduino
const uint8_t PIN_BUTON = 2; // pe Uno doar D2 si D3 au INT0/INT1
const uint8_t PIN_LED = 13;
volatile uint16_t nrApasari = 0; // volatile: modificate in ISR
volatile bool evenimentNou = false;
void trateazaApasare() { // rutina de intrerupere
nrApasari++; // scurt si rapid
evenimentNou = true;
}
void setup() {
pinMode(PIN_BUTON, INPUT_PULLUP);
pinMode(PIN_LED, OUTPUT);
DDRB |= 0b00001111; // pinii 8..11: numaratorul, in binar
attachInterrupt(digitalPinToInterrupt(PIN_BUTON), trateazaApasare, FALLING);
}
void loop() {
if (evenimentNou) { // prelucrarea se face AICI
evenimentNou = false;
digitalWrite(PIN_LED, !digitalRead(PIN_LED));
// aratam numaratorul in binar pe pinii 8..11
PORTB = (PORTB & 0xF0) | (nrApasari & 0x0F);
}
delay(200); // sarcina lunga - evenimentul NU se pierde
}
Varianta cu acces direct la regiștri
volatile uint16_t contor = 0;
ISR(INT0_vect) { // vectorul intreruperii externe 0
contor++;
PORTB ^= (1 << PB5); // comuta LED-ul de pe pinul 13
}
void setup() {
DDRB |= (1 << PB5); // pinul 13 iesire
DDRD &= ~(1 << PD2); // pinul 2 intrare
PORTD |= (1 << PD2); // pull-up intern
EICRA |= (1 << ISC01); // ISC01 = 1
EICRA &= ~(1 << ISC00); // ISC00 = 0 -> front descendent
EIMSK |= (1 << INT0); // activeaza INT0
sei(); // activeaza intreruperile globale
}
void loop() {
// programul principal ramane complet liber
}
Debounce în întrerupere
volatile unsigned long ultimaIntrerupere = 0;
volatile uint16_t nrApasariReale = 0;
void trateazaApasare() {
unsigned long acum = millis(); // millis() poate fi CITIT in ISR
if (acum - ultimaIntrerupere > 50) { // ignora tot ce vine in 50 ms
nrApasariReale++;
ultimaIntrerupere = acum;
}
}
void setup() {
pinMode(2, INPUT_PULLUP);
DDRB |= 0b00001111; // pinii 8..11: numaratorul, in binar
attachInterrupt(digitalPinToInterrupt(2), trateazaApasare, FALLING);
}
void loop() {
static uint16_t afisat = 0;
// citire protejata a variabilei pe 16 biti
uint8_t sreg = SREG;
cli();
uint16_t v = nrApasariReale;
SREG = sreg;
if (v != afisat) { // s-a schimbat: reimprospatam bara
afisat = v;
PORTB = (PORTB & 0xF0) | (v & 0x0F);
}
}
Măsurarea latenței
// Legati pinul 7 la pinul 2 cu un fir scurt.
// Pinul 7 genereaza impulsul, pinul 2 il detecteaza prin intrerupere.
// Latenta se MASOARA CU OSCILOSCOPUL, pe canalele 7 si 4:
// distanta dintre frontul cazator de pe 7 si frontul urcator de pe 4.
void laFront() {
PORTD |= (1 << PD4); // prima instructiune din ISR: ridica martorul
}
void setup() {
pinMode(7, OUTPUT);
digitalWrite(7, HIGH);
pinMode(4, OUTPUT); // martorul de intrare in ISR
pinMode(2, INPUT);
attachInterrupt(digitalPinToInterrupt(2), laFront, FALLING);
delay(100);
}
void loop() {
PORTD &= ~(1 << PD4); // coboara martorul inainte de masuratoare
digitalWrite(7, HIGH);
delay(50);
digitalWrite(7, LOW); // genereaza frontul descendent = momentul zero
delay(500);
}
attachInterrupt. Un ISR scris direct cu ISR(INT0_vect)
este cu 1–2 µs mai rapid, pentru că elimină un nivel de indirecție.12Montaj interactiv
Diferența dintre interogare și întrerupere se înțelege cel mai bine când vedeți o apăsare pierdută. Montajul de mai jos are două numărătoare afișate în binar pe LED-uri: unul actualizat prin interogare într-o buclă cu pauze lungi, celălalt dintr-o rutină de întrerupere. Apăsați butonul repede și priviți care dintre ele rămâne în urmă.
volatile îi spune compilatorului că variabila se poate schimba „pe la spatele lui”, dintr-o rutină de întrerupere. Fără el, compilatorul are dreptul să citească variabila o singură dată și să folosească mai departe copia din registru - iar bucla nu ar observa niciodată apăsările.13Sarcini de lucru
- Realizați montajul cu buton pe pinul 2 și LED pe pinul 13.
- Implementați numărarea apăsărilor prin polling, cu
delay(200)în buclă. Apăsați rapid și observați câte apăsări se pierd. - Rescrieți programul folosind întreruperi și repetați testul - comparați rezultatele.
- Ștergeți cuvântul
volatileși compilați cu optimizări; observați dacă programul se blochează. - Adăugați filtrare de vibrații în ISR și verificați că fiecare apăsare fizică produce exact o incrementare.
- Configurați al doilea buton pe INT1 (pinul 3) astfel încât să reseteze contorul.
- Modificați tipul de declanșare din
FALLINGînCHANGEși explicați de ce contorul crește de două ori mai repede. - Măsurați latența întreruperii cu programul de mai sus și comparați cu latența polling-ului.
14Aplicație de aprofundare
CHANGE, și determinați
atât numărul de pași, cât și sensul de rotație, comparând starea celor două semnale în momentul
frontului. Afișați poziția pe serial și aprindeți un LED diferit pentru fiecare sens.
Indiciu: la rotire în sens orar, semnalul A precede semnalul B cu 90°; la rotire inversă, relația se schimbă.