LABORATORUL 03

Sistemul de întreruperi

Durată: 2 ore Suport: Capitolul 3 Regiștri: EICRA · EIMSK · SREG PDF îndrumar EN English version

Î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 și EIMSK
  • 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.

Exemplu concretUn 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

program principal rutina ISR eveniment extern (front pe pin) 1. se setează fanionul 2. se salvează PC pe stivă 3. întreruperi dezactivate 5. reti() restaurează PC 6. întreruperi reactivate 4. se execută corpul ISR (trebuie să fie FOARTE scurt) execuție normală continuă exact de unde a rămas
Fig. 1 - Secvența completă de tratare a unei întreruperi. Programul principal nu „știe" că a fost suspendat: contextul este salvat și restaurat automat.
  1. Apare evenimentul (front pe un pin, depășire de timer, terminarea unei conversii ADC etc.)
  2. Se setează automat fanionul de întrerupere corespunzător
  3. Dacă întreruperile sunt activate global (sei()) și local, procesorul termină instrucțiunea curentă
  4. Adresa de revenire se salvează pe stivă, iar întreruperile globale se dezactivează
  5. Se sare la adresa din tabela vectorilor de întrerupere
  6. 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.

Cronogramă interactivă - latența de răspuns
CriteriuPollingÎntrerupere
Latență tipicăcât durează o iterație a bucleisub 5 µs
Latență în cel mai rău cazdurata întregii bucledurata celui mai lung ISR activ
Consum de procesorcontinuu, chiar și fără evenimentedoar la apariția evenimentului
Complexitatea coduluisimplănecesită volatile și atenție la secțiuni critice
Potrivit pentrusemnale lente, verificări periodiceevenimente scurte, rare, critice în timp

6Sursele de întrerupere pe ATmega328P

SursăPin ArduinoPin AVRVector ISRObservații
INT0D2PD2INT0_vectfront configurabil
INT1D3PD3INT1_vectfront configurabil
PCINT0…7D8–D13portul BPCINT0_vectorice schimbare, grupat pe port
PCINT8…14A0–A5portul CPCINT1_vectorice schimbare
PCINT16…23D0–D7portul DPCINT2_vectorice schimbare
Timer1 compare A--TIMER1_COMPA_vectvezi Laboratorul 4
ADC complet--ADC_vectvezi Laboratorul 5
USART recepție--USART_RX_vectvezi Laboratorul 6
INT0/INT1 față de PCINTÎntreruperile INT0 și INT1 pot detecta selectiv frontul crescător sau descendent și au fiecare propriul vector. Întreruperile PCINT se declanșează la orice schimbare și sunt grupate pe port - în ISR trebuie determinat prin cod care pin s-a modificat și în ce direcție.
Prioritatea întreruperilorOrdinea din tabela vectorilor determină prioritatea: cu cât adresa este mai mică, cu atât prioritatea este mai mare. RESET are prioritatea maximă, urmat de INT0, apoi INT1. Prioritatea contează doar când două întreruperi devin pendinte simultan - nu produce întreruperea unui ISR în curs de execuție.

7Configurarea regiștrilor

ISC01ISC00Declanșare INT0Echivalent ArduinoUtilizare
00nivel LOW (cât timp pinul e 0)LOWtrezire din sleep; se redeclanșează continuu
01orice schimbare de nivelCHANGEencodere, numărare de fronturi duble
10front descendent (1 → 0)FALLINGbutoane cu pull-up
11front crescător (0 → 1)RISINGbutoane 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ă.

EICRA și EIMSK - configurarea întreruperilor externe
Registrul EIMSK cu biții INT1 și INT0
Fig. 2 - Registrul EIMSK (External Interrupt Mask Register): doar biții 1 și 0 sunt folosiți, pentru activarea individuală a INT1 și INT0. Fig. 3.2 din îndrumar
Registrul EIFR cu fanioanele INTF1 și INTF0
Fig. 3 - Registrul EIFR (External Interrupt Flag Register): fanioanele se setează automat de hardware la apariția întreruperii și se șterg singure la intrarea în ISR. Fig. 3.3 din îndrumar
Ștergerea manuală a unui fanionFanioanele din EIFR se șterg scriind un 1 logic (nu 0!) în poziția respectivă. Este o convenție întâlnită la majoritatea regiștrilor de fanioane, pe toate microcontrolerele: EIFR |= (1 << INTF0);
Bitul I din SREGEste comutatorul global al întreruperilor. 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.

ce se întâmplă fără volatile
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
}
Efectul optimizăriiCodul funcționează perfect compilat fără optimizări și se blochează complet cu -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:

citire protejată
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;
}
De ce se salvează SREGUn simplu 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.

Întreruperea unui delay() în curs
Tiparul corectISR-ul doar ridică un fanion și incrementează un contor. Toată prelucrarea - afișare, calcule, comunicație - se face în loop(), unde durata nu afectează sistemul de întreruperi.

10Reguli obligatorii pentru un ISR

Restricții
  • 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 ISRCe faceți în loop()
incrementarea unui contorafișarea pe serial
ridicarea unui fanioncalcule cu virgulă mobilă
citirea unui registru care se pierdescrierea pe LCD sau pe card SD
salvarea unui moment de timporice durează peste câteva microsecunde

11Cod sursă

Varianta Arduino

intrerupere_arduino.ino
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

intrerupere_registri.ino
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
}
Montaj practic cu două butoane pentru aplicația cu întreruperi
Fig. 4 - Montajul practic al aplicației cu întreruperi: două butoane conectate pe pinii cu INT0 și INT1. Fig. 3.4 din îndrumar

Debounce în întrerupere

intrerupere_debounce.ino
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

latenta.ino
// 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);
}
Ce veți măsuraPuneți osciloscopul pe cele două canale, cu baza de timp de 1 µs/div, și declanșați pe frontul căzător al pinului 7. Valoarea tipică este de 4–8 µs. Ea include recunoașterea frontului, terminarea instrucțiunii curente, salvarea contextului, saltul la vector și apelul funcției înregistrate prin 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ă.

Interogare vs. întrerupere - apăsați repede și comparați
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

/* Fiecare numarator este aratat in binar pe cate 4 LED-uri:
   pinii  8..11 = apasarile prinse prin INTRERUPERE
   pinii  3..6  = apasarile prinse prin INTEROGARE      */

volatile unsigned char prinIntrerupere = 0;
unsigned char prinInterogare = 0;

ISR(INT0_vect)                  // se executa oricand, imediat
{
    prinIntrerupere++;
    PORTB = (PORTB & 0xF0) | (prinIntrerupere & 0x0F);
}

int main(void)
{
    DDRB  |= 0x0F;                       // pinii 8..11, iesiri
    DDRD  |= (1 << PD3) | (1 << PD4) | (1 << PD5) | (1 << PD6);
    DDRD  &= ~(1 << PD2);                // pinul 2, intrare
    PORTD |=  (1 << PD2);                // pull-up intern

    EICRA |= (1 << ISC01);      // front cazator
    EIMSK |= (1 << INT0);
    sei();

    while (1) {
        /* bucla principala este ocupata 400 ms cu "altceva" */
        _delay_ms(400);

        if (!(PIND & (1 << PD2))) {      // interogare: prinde doar ce nimereste
            prinInterogare++;
            /* bitii 3..6 din PORTD, fara sa stricam pull-up-ul de pe PD2 */
            PORTD = (PORTD & 0x87) | ((prinInterogare & 0x0F) << 3);
        }
    }
}
De încercatApăsați butonul de zece ori, rapid. Sub fiecare bară vi se arată numărul în binar și în zecimal. Numărătorul întrerupere le prinde pe toate, pentru că hardware-ul întrerupe execuția indiferent unde se afla programul. Numărătorul interogare rămâne mult în urmă: el vede butonul doar în clipa în care ajunge la linia respectivă, o dată la 400 ms. Diferența dintre cele două numere este exact numărul de apăsări pierdute.
Exercițiu - de ce este obligatoriu volatile
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

volatile unsigned char apasari = 0;

ISR(INT0_vect)
{
    /* Regula: un ISR trebuie sa fie foarte scurt.
       Aici doar numaram - restul se face in bucla principala. */
    apasari++;
}

int main(void)
{
    DDRB  |= (1 << PB5) | 0x0F;   // LED-ul de prag + bara de 4 LED-uri
    DDRD  &= ~(1 << PD2);
    PORTD |=  (1 << PD2);

    EICRA |= (1 << ISC01);
    EIMSK |= (1 << INT0);
    sei();

    while (1) {
        /* Completati:
           1. Cititi in siguranta variabila "apasari" intr-o copie locala.
              Pentru asta opriti intreruperile cu cli(), copiati, apoi sei().
           2. Aratati copia in binar pe bara:
                 PORTB = (PORTB & 0xF0) | (copie & 0x0F);
           3. Daca s-au strans 5 apasari, aprindeti LED-ul de pe pinul 13.
           4. La 10 apasari, aduceti numaratorul la 0 si stingeti LED-ul. */

        _delay_ms(100);
    }
}
De încercatCuvântul 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 în CHANGE ș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

Extindere - encoder rotativConectați un encoder rotativ incremental la pinii D2 și D3. Folosiți întreruperi pe ambele canale, cu declanșare 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ă.

15Întrebări de verificare

16Resurse