CURSUL 05

Sistemul de Întreruperi

Durată: 121 min de predare Nivel: licență, anul II - recomandat după Cursul 04 Disciplină: Microcontrollere și Microprocesoare Laborator asociat: Laboratorul 03 PDF: descarcă suportul EN English version

Programele de până acum au avut o structură previzibilă: inițializare, apoi o buclă infinită în care procesorul întreabă mereu aceleași lucruri. Structura funcționează atâta timp cât lumea din jur are răbdare să fie întrebată. Problema apare când nu mai are: un buton apăsat câteva zecimi de milisecundă, un octet care sosește pe linia serială înainte să fi fost citit precedentul, un impuls scurt de la un senzor de turație. Mecanismul care rezolvă exact această problemă se numește întrerupere - soneria de la ușă, în locul plimbărilor repetate până acolo ca să verificăm dacă a venit cineva.

1Obiectul și structura cursului6 min

Vom urmări întreruperea pas cu pas: ce se întâmplă mecanic în procesor când apare, cum se scrie corect o rutină de tratare, ce este tabelul vectorilor, ce surse are ATmega328P, cum funcționează prioritatea pe AVR și, cel mai important pentru practică, de ce comunicarea dintre o rutină de tratare și programul principal ascunde două capcane pe care le rezolvă doar disciplina, nu talentul.

Recapitulare din Cursul 04
  • Citirea unei intrări se face din PINx, niciodată din PORTx (Cursul 04)
  • Eșantionarea unui pin poate rata un impuls mai scurt decât durata unei bucle de interogare (Cursul 04) - exact problema pe care o rezolvă acest curs
  • Să explicați de ce interogarea ciclică poate pierde evenimente scurte, cu o cifră concretă
  • Să enumerați, în ordine, operațiile pe care procesorul le execută la acceptarea unei întreruperi
  • Să scrieți o rutină de tratare corectă, scurtă, fără întârzieri și fără operații lente
  • Să explicați ce înseamnă prioritate pe AVR și de ce nu există preempțiune între întreruperi
  • Să recunoașteți și să rezolvați o variabilă neprotejată cu volatile și o condiție de cursă

2Interogarea ciclică și alternativa întreruperilor12 min

Metoda clasică fără întreruperi se numește interogare ciclică (polling): în bucla principală, programul verifică periodic starea fiecărei surse care îl interesează.

while (1) {
    stare_noua = (PIND & (1<<PD2)) ? 1 : 0;
    if (stare_veche == 1 && stare_noua == 0) PORTB ^= (1<<PB5);
    stare_veche = stare_noua;
    citeste_senzor_temperatura();   /* dureaza cateva milisecunde */
    actualizeaza_afisajul();        /* dureaza si mai mult */
}

Programul are două defecte pe care nicio rescriere nu le poate corecta. Primul: risipa de timp - procesorul citește PIND de zeci de mii de ori pe secundă, iar în marea majoritate a cazurilor răspunsul e „nu s-a întâmplat nimic"; într-o aplicație pe baterie, un procesor care întreabă încontinuu nu poate fi trecut niciodată în regim de consum redus. Al doilea, mai grav: pierderea informației - între două verificări ale butonului, programul execută citirea senzorului și actualizarea afișajului; dacă acestea durează cinci milisecunde, un impuls mai scurt poate cădea în întregime în acel interval și dispare fără urmă.

Exercițiu rezolvat - poate fi detectat un impuls de 40 µs prin interogare?

Un senzor optic generează un impuls de 40 µs la fiecare rotație. Bucla principală, pe un ATmega328P la 16 MHz, are nevoie de 1200 de cicluri pentru un parcurs complet.

Vezi rezolvarea

Un ciclu: T = 1/16×10⁶ = 62,5 ns. Un parcurs complet al buclei: 1200×62,5 ns = 75 µs.

Impulsul de 40 µs e mai scurt decât intervalul dintre două citiri consecutive - poate începe și se poate termina complet între două priviri ale programului. Detecția nu e imposibilă, dar e aleatoare: turația măsurată va fi greșită imprevizibil. Pentru detecție sigură prin interogare, bucla ar trebui să se închidă în mai puțin de 640 de cicluri - foarte puțin loc pentru restul aplicației.

Acelasi impuls, vazut de scrutare si de intrerupere
Analogia soneriei Așteptăm un colet la o ușă fără sonerie: singura soluție e să mergem periodic să verificăm. Dacă mergem des, pierdem tot timpul pe hol; dacă mergem rar, curierul apucă să sune, să aștepte și să plece. Nu există o frecvență a plimbărilor care rezolvă ambele probleme simultan. Cu o sonerie, ne vedem liniștiți de treabă și reacționăm doar când cineva apasă butonul. Este exact ce face sistemul de întreruperi: procesorul nu monitorizează, ci reacționează doar când evenimentul apare - evenimentul e generat de un periferic sau de mediul exterior, procesorul doar primește anunțul.
Interogare sau întrerupere? Comparația numerică

3Caracteristici ale sistemului de întreruperi7 min

Întreruperea e generată ca răspuns la un efect fizic al unui periferic: schimbarea unui pin, încheierea unei perioade de timp, terminarea unei transmisii seriale, sfârșitul unei conversii analog-numerice. Caracterul lor comun e asincronismul - se produc la momente necorelate cu poziția curentă a programului. Tocmai de aici vine câștigul de eficiență: programul nu mai trebuie construit în jurul momentelor în care evenimentele s-ar putea produce.

Tratarea presupune o subrutină dedicată, rutina de tratare, invocată automat de hardware, niciodată explicit din program. Apariția evenimentului setează un bistabil numit indicator de întrerupere (flag) - exact ce rezolvă problema impulsurilor scurte: impulsul poate dispărea fizic, dar bitul care îl marchează rămâne setat până e tratat sau șters.

Cele trei condiții pentru acceptarea unei cereri
Indicatorul sursei e setat, ȘI activarea locală a sursei e pornită, ȘI bitul global I din SREG e pornit. Toate trei simultan - lipsa oricăreia blochează cererea, chiar dacă indicatorul rămâne setat, în așteptare.
Timp real înseamnă „garantat", nu „rapid" Un program bazat pe întreruperi poate oferi o garanție calculabilă asupra timpului scurs între eveniment și începutul tratării lui. Un program bazat pe interogare oferă doar o medie, cu abateri imprevizibile - diferența dintre „de regulă răspunde repede" și „răspunde întotdeauna într-un interval cunoscut".

4Principiul de funcționare, pas cu pas13 min

Punctul de plecare e programul principal (MPF), cu contorul de program (PC) conținând adresa instrucțiunii următoare. Presupunem că un periferic setează indicatorul și cele trei condiții sunt îndeplinite.

Succesiunea de operații

1. Procesorul termină instrucțiunea aflată în curs - o garanție de corectitudine, nu un detaliu: oprirea la jumătate ar lăsa o scriere parțială sau un rezultat necalculat. Întreruperea nu e luată în calcul niciodată la mijlocul unei instrucțiuni, doar la granița dintre două. O instrucțiune lungă adaugă întârziere la răspuns.

2. Salvează PC pe stivă (push(PC)) - adresa de revenire exactă. Pe ATmega328P, doi octeți, SP scade cu două. Dacă stiva e coruptă, revenirea duce programul într-un loc arbitrar din Flash.

3. Dezactivează automat întreruperile, ștergând bitul I din SREG - echivalentul de „a scoate din priză soneria" cât timp discutăm cu curierul. Fără acest pas, o a doua întrerupere ar sosi imediat, ar salva încă o adresă, ar fi întreruptă la rândul ei - stiva ar crește necontrolat.

4. Încarcă în PC adresa vectorului corespunzător, din tabelul vectorilor de întrerupere.

5. Execută rutina de tratare - programul principal e complet oprit, nu rulează în paralel, nu rulează mai încet, nu rulează deloc.

6. Se încheie cu reti (return from interrupt) - extrage adresa de pe stivă în PC, apoi redeschide întreruperile, setând la loc bitul I. Diferența față de ret e exact această reactivare.

De ce o rutină nu se poate termina cu ret

Programul ar reveni corect la adresă, dar ar rula mai departe cu întreruperile stinse - sistemul ar părea „blocat" după prima întrerupere, fără niciun mesaj de eroare. E o greșeală de sintaxă imposibil de comis în C (macroul ISR generează întotdeauna reti), dar reală în asamblare.

Latența răspunsului

Timpul scurs între eveniment și prima instrucțiune utilă a rutinei se numește latență - nu e nulă, nici constantă. Componente: terminarea instrucțiunii curente, recunoașterea cererii și salvarea PC, saltul din tabelul vectorilor, prologul rutinei (salvarea contextului).

Exercițiu rezolvat - ordinul de mărime al latenței

ATmega328P la 16 MHz. Recunoașterea cererii durează 4 perioade de ceas, saltul din tabelul vectorilor 3 perioade. Estimați latența minimă.

Vezi rezolvarea

T = 1/16×10⁶ = 62,5 ns. Recunoaștere + salvare PC: 4×62,5 = 250 ns. Salt: 3×62,5 = 187,5 ns. Total: ~437,5 ns.

La acest interval se adaugă terminarea instrucțiunii în curs (până la patru perioade) și prologul generat de compilator (pe o rutină cu mai multe variabile, poate depăși douăzeci de perioade). Concluzie: latența reală e de ordinul unei microsecunde, nu al zecilor de nanosecunde, și variază de la o apariție la alta - pentru măsurători precise nu ne bazăm pe momentul intrării în rutină, ci pe o valoare capturată direct de hardware (captura pe temporizator).

Acceptarea unei întreruperi: puneți cei opt pași în ordine

5Rutina de tratare a întreruperii11 min

Rutina de tratare, ISR (Interrupt Service Routine), se scrie ca orice funcție, dar se comportă diferit.

Salvarea și restaurarea contextului

Hardware-ul salvează doar PC. Tot restul - registrul SREG cu indicatorii de condiție (zero, transport, semn, depășire) și registrele de lucru - rămâne la dispoziția rutinei.

De ce nesalvarea SREG produce erori imposibil de reprodus prin depanare

Programul principal execută o comparație, urmează un salt condiționat pe baza ei. Dacă între cele două apare o întrerupere, iar rutina execută orice operație aritmetică, indicatorii sunt rescriși - la revenire, saltul se ia pe baza calculului din rutină, nu pe baza comparației originale. Eroarea depinde de momentul exact al întreruperii, deci apare o dată la mii de execuții.

Ansamblul SREG + registrele folosite se numește context. În C, macroul ISR() din avr/interrupt.h generează automat prologul și epilogul: salvează SREG, salvează exact registrele pe care corpul le folosește efectiv, execută corpul, restaurează totul în ordine inversă și încheie cu reti.

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

ISR(INT0_vect)
{
    PORTB ^= (1 << PB5);   /* corpul rutinei: o singura operatie */
}
O greșeală de scriere a numelui vectorului nu produce eroare de compilare

INT0_vect nu e un parametru oarecare, ci numele simbolic care leagă funcția de poziția din tabel. Scris greșit, compilatorul acceptă codul fără plângere - rutina nu e apelată niciodată. La apariția evenimentului, programul sare într-o intrare netratată și se resetează (vezi secțiunea despre tabelul vectorilor).

Reguli de scriere

Pe durata rutinei, programul principal e oprit și, pe AVR, celelalte întreruperi sunt blocate. Orice ciclu consumat inutil e un ciclu în care aplicația nu răspunde la nimic altceva.

Regulile, tratate ca obligații Rutina trebuie scurtă - câteva zeci de instrucțiuni: setarea unui indicator, citirea unui registru, incrementarea unui numărător. Fără funcții de întârziere (_delay_ms, delay) - opresc întregul sistem. Fără operații lente - serial, EEPROM, virgulă mobilă, LCD, toate durează milisecunde. Nu returnează valori, nu primește parametri - nu există cine să le transmită sau să le preia, apelul vine din hardware, nu din program. Tiparul central: rutina doar constată evenimentul, programul principal îl prelucrează.

6Tabelul vectorilor de întrerupere8 min

Legătura dintre sursa fizică și rutina de tratare e o structură de date fixă în memorie: tabelul vectorilor de întrerupere. Pe AVR se află la începutul Flash-ului, de la adresa zero.

Pe ATmega328P (32 KB), fiecare intrare conține o instrucțiune jmp completă (două cuvinte), pentru că distanța până la rutină poate depăși raza unui salt relativ. Vectorii se află deci din două în două cuvinte: RESET la 0x0000, INT0 la 0x0002, INT1 la 0x0004.

Tabelul conține salturi, nu codul rutinelor Spațiul dintre doi vectori consecutivi e de numai două cuvinte, insuficient pentru orice rutină reală. Intrarea din tabel nu e destinația finală, ci un indicator către ea: procesorul sare în tabel, iar de acolo saltul îl duce oriunde în Flash a plasat compilatorul corpul funcției.
Prima intrare e vectorul de RESET, nu o întrerupere propriu-zisă

E punctul de pornire după alimentare, resetare externă sau expirarea watchdog-ului. Consecință utilă: dacă o întrerupere apare fără rutină scrisă pentru ea, intrarea implicită trimite execuția la vectorul de RESET, și programul repornește de la început. Un microcontroler care se resetează la intervale aparent aleatoare este, foarte des, unul cu o sursă de întrerupere activată și netratată.

AdresăSursăSemnificație
0x0000RESETalimentare, resetare externă, watchdog
0x0002INT0întrerupere externă 0 (PD2)
0x0004INT1întrerupere externă 1 (PD3)
0x0006-0x000APCINT0-2schimbare de nivel pe porturile B, C, D
0x0012, 0x001A, 0x0020TIMERn_OVFdepășire la numărătoarele 0-2
0x0024, 0x0028USART_RX, USART_TXrecepție / transmisie încheiată
0x002AADCconversie analog-numerică încheiată

Regularitatea tabelului nu e întâmplătoare: sursele care necesită reacție rapidă și sunt legate direct de exterior (întreruperile externe) se află în capul tabelului, cele lente (legate de memorie) la sfârșit - poziția în tabel are un rol suplimentar, discutat la priorități.

Potriviți vectorul cu evenimentul care îl declanșează

7Surse de întrerupere: externe și interne9 min

Sursele se împart în externe (evenimentul vine de pe un pin) și interne (evenimentul e produs de un modul din interiorul cipului).

Întreruperi externe

Detecția se face în două moduri. Activ pe nivel (level-triggered): cererea există în permanență cât timp linia e în starea activă - dacă rutina se termină și linia e încă jos, cererea reapare imediat, execuție la nesfârșit până sursa eliberează linia. Activ pe front (edge-triggered): cererea apare o singură dată, la tranziție (crescător, căzător, sau oricare) - modul folosit în majoritatea aplicațiilor, pentru că evenimentul e prin natura lui punctual.

ATmega328P are două întreruperi externe complete, INT0 (PD2) și INT1 (PD3), configurate în EICRA prin perechile ISC01/ISC00 și ISC11/ISC10: 00 nivel jos, 01 orice schimbare, 10 front căzător, 11 front crescător. Activare locală prin EIMSK, indicatori în EIFR (INTF0, INTF1).

PCINT - grosier, dar acoperă orice pin Întreruperile pin change acoperă practic toți pinii, dar nu permit alegerea frontului, detectează orice schimbare, iar toți pinii unui port partajează un singur vector - rutina trebuie să citească portul și să compare cu valoarea anterioară pentru a afla care pin s-a modificat. Rezolvă situația în care sunt necesare multe intrări cu capacitate de întrerupere și doar două întreruperi externe complete disponibile.

Perifericele de comunicație (USART, SPI, I2C) generează întreruperi la date gata de transfer sau primite. La 115200 b/s, un caracter sosește la ~87 µs - fără întrerupere, programul ar trebui să verifice registrul de stare cu o frecvență cel puțin dublă, imposibil de conciliat cu orice altă activitate.

Întreruperi interne

Numărătoarele generează depășire (overflow, perioadă fixă, dictată de dimensiune și divizor) sau comparare (perioadă aleasă liber, prin registrul de comparare). ADC-ul generează o întrerupere la finalul conversiei - soluția firească pentru achiziții: se pornește conversia, procesorul face altceva zeci de microsecunde, rutina preia rezultatul.

Ștergerea indicatorului nu e uniformă între surse

La majoritatea surselor, indicatorul se șterge automat de hardware la intrarea în rutină. Dacă tratăm prin interogare, fără rutină, ștergerea manuală se face scriind unu în bit, nu zero. La alte surse (recepția USART), indicatorul se șterge singur la citirea registrului de date - dacă rutina uită să citească UDR0, indicatorul rămâne setat și întreruperea reapare la nesfârșit.

8Priorități și imbricarea întreruperilor9 min

Prioritatea decide ordinea de tratare când apar mai multe cereri simultan, sau când o cerere nouă sosește în timpul tratării alteia. Poate fi hardware (fixată prin construcție) sau software (aleasă de programator, prin registru de configurare) - arhitecturile pe 32 de biți oferă de regulă un controler cu nivele programabile.

Un mecanism comun tuturor arhitecturilor e masca de întrerupere: dezactivarea temporară a unor surse, indiferent de prioritate. O sursă mascată nu dispare - indicatorul ei continuă să fie setat, iar la ridicarea măștii, cererea acumulată e tratată imediat.

Cum stau lucrurile pe AVR

Prioritatea e stabilită exclusiv de poziția vectorului în tabel O sursă mai aproape de adresa zero are prioritate mai mare. INT0 > INT1 > întreruperile de numărător > USART sau ADC - ordine fixă, nemodificabilă prin niciun registru.

Prioritatea contează numai când două sau mai multe cereri sunt în așteptare simultan, în momentul în care procesorul e gata să accepte o întrerupere - atunci se tratează întâi cea cu vectorul mai mic, cealaltă rămâne în așteptare și e tratată imediat după. Nicio cerere nu se pierde.

Pe AVR nu există preempțiune între întreruperi

Odată intrată în rutină, cu bitul I șters automat, nicio altă întrerupere nu o poate întrerupe, oricât de mare i-ar fi prioritatea. O cerere INT0 sosită în timpul tratării unei întreruperi ADC nu scoate procesorul din rutina ADC - așteaptă cuminte până la reti. Consecință practică: pe AVR prioritatea contează mult mai puțin decât durata rutinelor - cea mai bună garanție de răspuns rapid pentru o sursă critică nu e poziția ei în tabel, ci faptul că toate celelalte rutine sunt scurte.

Reactivarea manuală a întreruperilor în interiorul unei rutine (sei() sau ISR_NOBLOCK) produce imbricare - rutine care se pot întrerupe reciproc. Aproape întotdeauna o idee proastă, din trei motive: consumul de stivă crește cu fiecare nivel de imbricare, pe o memorie de 2 KB; riscul de recursivitate, dacă indicatorul propriei surse nu e tratat înainte de reactivare; și complexitatea rezultată face imposibil de verificat toate întrețeserile posibile ale execuției. Soluția corectă la o rutină prea lungă nu e imbricarea, ci scurtarea ei.

9Variabile partajate și condiții de cursă11 min

Comunicarea cu programul principal se face exclusiv prin variabile globale - și ascunde două capcane distincte.

Optimizarea compilatorului și volatile

Dacă vede o variabilă globală citită într-o buclă, dar niciodată modificată acolo sau în funcțiile apelate din ea, compilatorul conclude că valoarea nu se schimbă - o citește o singură dată, o păstrează într-un registru, folosește registrul la toate iterațiile. Raționament corect pentru codul vizibil, greșit în realitate, pentru că rutina de tratare o modifică fără ca vreun apel să apară în program.

volatile uint8_t eveniment = 0;   /* fara volatile, bucla nu se termina niciodata */

ISR(INT0_vect) { eveniment = 1; }              /* rutina doar constata */

int main(void) {
    configureaza_intreruperea(); sei();
    while (1) {
        if (eveniment) {                       /* programul principal il prelucreaza */
            eveniment = 0;
            PORTB ^= (1 << PB5);
        }
    }
}
Regula practică Orice variabilă globală scrisă într-o rutină de tratare și citită în programul principal, sau invers, trebuie declarată volatile. Costul e o mică pierdere de viteză, nesemnificativă față de riscul unei bucle care așteaptă la nesfârșit un indicator deja setat.

Condiția de cursă

Nu se rezolvă cu volatile. Registrele AVR sunt de opt biți - o variabilă pe 16 biți cere două citiri succesive, iar între ele există o granița de instrucțiune unde o întrerupere poate fi acceptată.

Exercițiu rezolvat - citirea neatomică a unei variabile pe 16 biți

impulsuri are valoarea 255 = 0x00FF. Programul citește octetul inferior (0xFF). Exact atunci sosește un impuls, rutina incrementează variabila la 256 = 0x0100. Programul citește octetul superior (0x01).

Vezi rezolvarea

Valoarea recompusă: 0x01FF = 511. Numărul 511 nu a existat niciodată - variabila a avut 255, apoi 256, dar niciodată 511. Programul a citit jumătate dintr-o valoare veche și jumătate dintr-una nouă - o eroare de peste 100%, apărută o dată la câteva mii de citiri, care trece de toate testele și se manifestă la client.

Soluție: citire atomică, prin dezactivarea temporară a întreruperilor, cu restaurarea stării anterioare (nu reactivare necondiționată):

#include <util/atomic.h>
uint16_t citeste_impulsuri(void) {
    uint16_t copie;
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { copie = impulsuri; }
    return copie;
}
O secțiune critică se protejează scurtă, se copiază, nu se prelucrează în ea

Pe durata secțiunii critice sistemul e surd la orice eveniment - o secțiune lungă produce exact aceleași probleme ca o rutină de tratare lungă. Se copiază valoarea în interior, se prelucrează după ieșire. Notă utilă: pe AVR, citirea/scrierea unei variabile de opt biți e atomică prin construcție (o singură instrucțiune) - dar contor++ nu e atomic nici pe opt biți, fiind citire+incrementare+scriere, trei instrucțiuni separate.

10Aplicație completă cu sistemul de întreruperi11 min

Aplicație completă: LED-ul comută starea la fiecare apăsare a unui buton, folosind INT0, pe frontul crescător.

Montajul și inițializarea

Butonul pe PD2 (singurul pin cu INT0), legat între pin și VCC, cu pull-down extern de ~10 kΩ - repaus la nivel jos, apăsare la nivel înalt (front crescător). LED-ul pe PB5, cu rezistență de limitare calculată ca în Cursul 04.

static void init_porturi(void) {
    DDRB = (1 << DDB5); PORTB = 0x00;      /* PB5 iesire, LED stins */
    DDRD = 0x00; PORTD = 0x00;              /* PD2 intrare, fara pull-up (extern) */
}

static void init_intrerupere(void) {
    EICRA |= (1 << ISC01) | (1 << ISC00);   /* front crescator pe INT0 */
    EIFR  |= (1 << INTF0);                  /* sterge un eventual indicator ramas */
    EIMSK |= (1 << INT0);                   /* activare locala */
}

ISR(INT0_vect) { PORTB ^= (1 << PB5); }      /* comuta LED-ul; nimic altceva */

int main(void) {
    init_porturi(); init_intrerupere();
    sei();                                   /* activare globala, dupa toata initializarea */
    while (1) { /* liber pentru orice altceva */ }
}
De ce ordinea celor trei pași contează, și de ce sei() vine ultimul Modul de declanșare se scrie înainte de ștergerea indicatorului, pentru că schimbarea modului poate seta el însuși indicatorul - fără curățare, rutina s-ar executa o dată nejustificat imediat după activarea globală. Iar sei() se apelează întotdeauna la sfârșitul inițializării: dacă întreruperile ar fi active mai devreme, o rutină s-ar putea executa înainte ca structurile de date pe care le folosește să fi fost inițializate.

Varianta Arduino

volatile bool cerere_comutare = false;

void trateaza_buton() { cerere_comutare = true; }   /* doar marcheaza */

void setup() {
    pinMode(PIN_LED, OUTPUT);
    pinMode(PIN_BUTON, INPUT);
    attachInterrupt(digitalPinToInterrupt(PIN_BUTON), trateaza_buton, RISING);
}
void loop() {
    if (cerere_comutare) {
        cerere_comutare = false;
        digitalWrite(PIN_LED, !digitalRead(PIN_LED));
    }
}

Diferența nu e doar de sintaxă: aici rutina nu comandă direct ieșirea, doar ridică un indicator, iar comutarea se face în loop() - pentru că digitalRead/ digitalWrite nu sunt operații elementare (traduc pin în port+mască, verifică PWM activ), durează câteva microsecunde, mult pentru o rutină de tratare. Același tipar: rutina constată, bucla prelucrează.

11Problema oscilațiilor de contact6 min

Programul de mai sus funcționează perfect în simulare. Pe un montaj real, LED-ul va comuta uneori o dată, alteori de trei sau cinci ori la o singură apăsare - oscilația de contact (Cursul 04), văzută de sistemul de întreruperi la fel de fidel ca orice alt front.

Filtrarea oscilațiilor direct în rutina de tratare
const unsigned long INTERVAL_MIN = 40;   /* milisecunde */
volatile bool cerere_comutare = false;
volatile unsigned long ultimul_moment = 0;

void trateaza_buton() {
    unsigned long acum = millis();       /* poate fi citita, nu avanseaza in rutina */
    if (acum - ultimul_moment >= INTERVAL_MIN) {
        cerere_comutare = true;
        ultimul_moment = acum;
    }
}
De ce comparația e scrisă ca „diferență ≥ interval", nu ca „acum ≥ moment + interval"

Prima formă rămâne corectă și atunci când numărătorul de milisecunde depășește valoarea maximă și revine la zero - scăderea în aritmetică fără semn dă oricum diferența corectă. A doua formă produce, la momentul depășirii, un buton care nu mai răspunde pentru un interval lung.

Alternativa hardware, un filtru RC eventual urmat de trigger Schmitt, nu consumă timp de procesor, dar cere o componentă în plus pentru fiecare buton - decizia depinde de câte butoane are montajul și cât de critic e timpul de procesor.

Observație despre millis() în interiorul unei rutine Pe platforma Arduino, millis() se bazează pe o întrerupere de numărător. Deoarece întreruperile sunt dezactivate în interiorul unei rutine de tratare, valoarea returnată de millis() nu avansează pe durata execuției acesteia - poate fi citită (utilă pentru a marca momentul evenimentului), dar nu poate măsura o durată în interiorul rutinei, și cu atât mai puțin poate fi folosită pentru așteptare. delay() plasat într-o rutină de tratare nu se termină niciodată.

12Erori frecvente4 min

  • „O rutină de tratare mai lungă nu strică, doar face ceva mai mult." Fals - pe durata ei, programul principal e complet oprit și, pe AVR, toate celelalte întreruperi sunt blocate. Orice ciclu inutil e un ciclu în care aplicația nu răspunde la nimic altceva, inclusiv la evenimente critice. Rutina doar constată evenimentul (indicator, citire, incrementare); prelucrarea se face în bucla principală.
  • „O variabilă globală citită și în întrerupere, și în bucla principală nu are nevoie de nimic special dacă e mică." Fals - fără volatile, compilatorul poate păstra o copie învechită într-un registru, iar bucla nu vede niciodată actualizarea. Pentru variabile mai mari de un octet, chiar cu volatile, citirea nu e atomică și poate recompune o valoare care nu a existat niciodată. Declarați volatile orice variabilă partajată; protejați citirile pe mai mult de un octet cu ATOMIC_BLOCK.
  • „Dacă o sursă are prioritate mai mare, poate întrerupe orice altă rutină în curs." Fals pe AVR - nu există preempțiune. Odată intrată într-o rutină, nicio altă întrerupere nu o poate întrerupe, indiferent de prioritate; ea așteaptă cuminte până la reti. Nu vă bazați pe prioritate pentru răspuns rapid - garanția reală vine din faptul că toate rutinele sunt scurte.

13Rezumat și glosar5 min

Interogarea ciclică poate pierde evenimente mai scurte decât durata unei iterații a buclei; întreruperea rezolvă asta printr-un indicator hardware care memorează evenimentul chiar dacă semnalul fizic a dispărut deja. La acceptarea unei cereri, procesorul termină instrucțiunea curentă, salvează PC pe stivă, dezactivează global întreruperile, sare la vectorul din tabel, execută rutina și revine cu reti, care reactivează întreruperile - spre deosebire de ret. O rutină de tratare trebuie să fie scurtă, fără întârzieri și fără operații lente: constată evenimentul, lasă prelucrarea pentru bucla principală. Pe AVR, prioritatea e dată exclusiv de poziția vectorului în tabel și contează doar între cereri simultane în așteptare - nu există preempțiune între rutine în curs de execuție. Comunicarea prin variabile globale cere volatile pentru corectitudinea vizibilității și, pentru variabile mai mari de un octet, secțiuni critice pentru citiri atomice - altfel apar condiții de cursă, greu de reprodus și devastatoare când apar.

ISR
Interrupt Service Routine - rutina de tratare, invocată automat de hardware.
Flag (indicator)
bistabil care memorează apariția unui eveniment de întrerupere.
Tabelul vectorilor
zonă fixă de Flash cu adresa fiecărei rutine de tratare (prin salt).
Latență
timpul dintre eveniment și prima instrucțiune utilă a rutinei.
Context
SREG plus registrele de lucru folosite - salvat la intrarea în rutină, restaurat la ieșire.
Condiție de cursă
eroare dependentă de momentul exact al unei întreruperi, la citirea neatomică a unei variabile partajate.

14Întrebări de verificare7 min

  1. Explicați de ce interogarea ciclică nu poate garanta detecția unui eveniment scurt, chiar dacă bucla e optimizată. Ce mărime trebuie comparată cu durata evenimentului?
  2. Enumerați, în ordine, operațiile procesorului AVR între acceptarea unei cereri și prima instrucțiune a rutinei. Care sunt hardware și care generate de compilator?
  3. Care e diferența dintre ret și reti? Ce se întâmplă dacă o rutină se încheie din greșeală cu ret?
  4. Ce se înțelege prin contextul procesorului, și de ce trebuie salvat la intrarea în rutină? Dați un exemplu concret de eroare produsă de nesalvarea SREG.
  5. Un ATmega328P se resetează la intervale neregulate, deși programul nu conține nicio instrucțiune de resetare, iar watchdog-ul e dezactivat. Formulați o explicație plauzibilă.
  6. Pe AVR, INT0 are prioritate mai mare decât recepția USART. O cerere INT0 sosește în timp ce se execută rutina de recepție. Ce se întâmplă, și de ce?
  7. Un numărător de 16 biți e incrementat într-o rutină și citit în programul principal. Construiți o secvență de evenimente în care programul obține o valoare care nu a existat niciodată.

15Direcții de aprofundare2 min

Cursul următor folosește exact mecanismul de întreruperi discutat aici, aplicat unui periferic specific: modulele de tip temporizator/numărător - baza timpului, generarea de PWM și captura evenimentelor externe cu precizie de ciclu de ceas.

Montajul, inițializarea și rutina de tratare din acest curs devin montaj real în Laboratorul 03.