În acest laborator vom înțelege cum funcționează întreruperile și vom implementa un sistem minimal de întreruperi pentru procesorul nostru AVR. Vom demonstra funcționalitatea lor folosind o rutină de tratare a întreruperilor de timer.
Întreruperile sunt evenimente generate asincron față de codul nostru care trebuie să capteze atenția procesorului pentru a fi rezolvate imediat. Altfel spus, în timpul execuției unui program, dacă este generată o întrerupere, procesorul va întrerupe firul normal de execuție după terminarea instrucțiunii curente și va trata întreruperea.
Întreruperile sunt folosite pentru a evita polling-ul și reprezintă fundamentul multitasking-ului preemptiv pe un sistem cu un singur procesor și un singur set de resurse hardware (unitate de execuție, registre, program counter, spațiu de adrese). În lipsa lor, singurul tip de execuție concurențială posibilă pe un astfel de sistem este multitasking-ul cooperativ.
Pentru a determina dacă trebuie să trateze o întrerupere, procesorul verifică, după fiecare instrucțiune, linia de cereri de întreruperi (interrupt request sau IRQ). Dacă această linie este activă atunci, după terminarea execuției instrucțiunii curente, starea curentă a procesorului este salvată pe stivă și se transferă execuția către o rutină de tratare a întreruperii respective, după un mecanism foarte asemănător instrucțiunii CALL
.
Tratarea întreruperilor se face de către funcții dedicate, numite rutine de tratare a întreruperilor (Interrupt Service Routine - ISR). Acestea sunt rutinele care sunt apelate la apariția unei întreruperi. Ceea ce caracterizează o rutină de tratare a întreruperilor față de o rutină normală este că ea se încheie întotdeauna cu instrucțiunea RETI
(echivalentul IRET
de pe x86).
Prin vector de întrerupere ne referim la adresa de memorie la care se afla începutul unei astfel de rutine. Rutinele de tratare pentru toate întreruperile pe care știe să le trateze un procesor sunt înscrise într-o tabelă a vectorilor de întrerupere, o zonă dedicată a memoriei de instrucțiuni. În această zonă procesorul este programat să caute rutine de tratare, creând astfel asocierea dintre un request hardware și o rutină software de tratare a lui. Numărul unei întreruperi (IRQNO) reprezintă indexul în tabela vectorilor de întrerupere a rutinei sale de tratare.
Pentru a evita tratarea aceleiași întreruperi imediat dupa terminarea rutinei sale, rutina trebuie să informeze sursa că întreruperea a fost tratată. Acest proces se numește acknowledge - ACK. Este responsabilitatea programului să facă ACK unei întreruperi astfel încât sursa să își retragă cererea.
În sistemele cu procesor x86 convenția este ca întreruperea cu numărul 2 să fie asociată cu evenimente de la tastatură. Asta înseamnă că toate tastaturile, atunci când vor să trimită informații către sistem, vor genera o cerere de întrerupere (IRQ), și vor transmite numărul întreruperii (IRQNO) ca fiind 2. Programatorii, știind această convenție, vor înregistra în tabela vectorilor de întrerupere, la intrarea numărul 2, o rutină care va citi caracterele trimise de către tastatură și le va prelucra.
Nu tot timpul este de dorit ca un program să poată fi întrerupt. De aceea, întreruperile pot fi deactivate sau mascate. Chiar mai mult, în general procesoarele își încep execuția după reset cu întreruperile dezactivate.
A activa/dezactiva întreruperile pe un procesor înseamnă a permite/nu permite, la nivel global, tratarea lor. Activarea/Dezactivarea întreruperilor are loc doar la nivel global procesorului, deci fie toate întreruperile sunt active, fie niciuna nu este.
A demasca/masca întreruperi pe un procesor înseamnă a permite/nu permite, la nivel individual, tratarea unei întreruperi. Demascarea/Mascarea întreruperilor are loc doar la nivel individual, deci mascarea unei întreruperi nu afectează nicio altă întrerupere.
Există întreruperi ce nu pot fi nici mascate, nici dezactivate. Aceste întreruperi se numesc întreruperi non-mascabile (Non-Mascable Interrupts - NMI).
În arhitectura AVR sunt mai multe registre I/O dedicate întreruperilor. Ele se grupează, în principiu, în:
Pentru ca un procesor AVR sa trateze o cerere de întrerupere (Mascable Interrupts) trebuie să fie adevărate 3 condiții:
I
din registrul SREG
este 1În arhitectura AVR, tabela vectorilor de întrerupere se află întotdeauna la adresa 0. Fiecare intrare în tabelă reprezintă prima instrucțiune din rutina de tratare a acelei întreruperi.
În cod, dacă vrem să folosim întreruperi, înseamnă că programul nostru trebuie să urmeze următoarea structură:
rjmp main ; Adresa 0x0000 rjmp INT0_ISR ; Adresa 0x0001 rjmp PCINT0_ISR ; Adresa 0x0002 rjmp PCINT1_ISR ; Adresa 0x0003 rjmp WDT_ISR ; Adresa 0x0004 rjmp TIM1_CAPT_ISR ; Adresa 0x0005 rjmp TIM1_COMPA_ISR ; Adresa 0x0006 rjmp TIM1_COMPB_ISR ; Adresa 0x0007 rjmp TIM1_OVF_ISR ; Adresa 0x0008 rjmp TIM0_COMPA_ISR ; Adresa 0x0009 rjmp TIM0_COMPB_ISR ; Adresa 0x000A rjmp TIM0_OVF_ISR ; Adresa 0x000B rjmp ANA_COMP_ISR ; Adresa 0x000C rjmp ADC_ISR ; Adresa 0x000D rjmp TWI_SLAVE_ISR ; Adresa 0x000E rjmp SPI_ISR ; Adresa 0x000F rjmp QTRIP_ISR ; Adresa 0x0010 main: <instr> ; Adresa 0x0011, prima instrucțiune a programului ...
main
nu este, așadar, nimic altceva decât rutina de tratare a întreruperii de reset.
Cu cât o rutină se află la o adresă mai mică, cu atât este mai prioritară!
RETI
, pentru a încheia rutina. Acesta este cazul comun.
În cazul în care vrem să executăm rutina foo
, atunci acea instrucțiune va fi RJMP foo
. Acesta ar trebui să fie mereu cazul pentru prima rutină, cea a întreruperii de reset.
Bitul I
din SREG indică faptul că întreruperile sunt activate/dezactivate la nivel global. Dacă bitul este 1, înseamnă că întreruperile sunt activate la nivel global. Dacă bitul este 0, înseamnă că sunt dezactivate la nivel global. Întreruperile pot fi activate la nivel global folosind instrucțiunea SEI
(Set Interrupt). Ele pot fi dezactivate folosind instrucțiunea CLI
(Clear Interrupt).
CALL_ISR
) întreruperile sunt dezactivate. La sfârșitul tratării unei întreruperi (deci la execuția instrucțiunii RETI
) întreruperile sunt activate.
Mascarea/demascarea întreruperilor se face în registre specifice fiecărui periferic. Pentru perifericul de timer, acel registru este TIMSK.
Vom configura Timer/Counter0 în modul de funcționare normal, cu prescaler 1. Vom configura timer-ul să genereze întreruperea de overflow. Vom configura rutina TIM0_OVF_ISR
astfel încât să fie executată atunci când apare o astfel de întrerupere. Această rutină va încărca valoarea 42
în R31
. La final, vom activa global întreruperile.
I
din SREG
este 1)
Task 01 (3p) Implementați logica controller-ului de întreruperi pentru întreruperile TIM0_OVF
, TIM0_COMPA
si TIM0_COMPB
. Urmăriți comentariile marcate TODO 1
din interrupt_controller.v
Hint: Valorile ce trebuie puse în vector
sunt definite în fișierul defines.vh
.
Task 02 (2.5p) Implementați instrucțiunile SEI
și CLI
. Urmăriți comentariile marcate TODO 2
din decode_unit.v
și control_unit.v
.
Task 03 (2.5p) Implementați instrucțiunea virtuală CALL_ISR
și instrucțiunea RETI
. Urmăriți comentariile marcate TODO 3
din decode_unit.v
, signal_generation_unit.v
și control_unit.v
.
Task 04 (2p) Plecând de la următorul schelet de cod, creați un program care schimbă starea pinului PA0 folosindu-se de o întrerupere (o dată pe secundă), cât timp bucla principală este într-o buclă infinită. Simulați programul. Încărcați programul pe FPGA.