Table of Contents

Laboratorul 08 - Întreruperi

Î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.

Descriere generală

Î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.

Tratarea întreruperilor

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.

Să luăm un exemplu. La sfârșitul execuției unei instrucțiuni, procesorul verifică linia de cereri de întreruperi (IRQ) și vede că ea este activă. Asta înseamnă că există o întrerupere ce trebuie tratată. Mai departe, se uită la numărul întreruperii (IRQNO), pentru a determina cum să trateze întreruperea. Odată ce știe numărul, în tabela vectorilor de întrerupere la indexul respectiv găsește rutina de tratare a întreruperii respective, și o execută.

Î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.

Mascarea întreruperilor

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).

Întreruperi pe AVR

În arhitectura AVR sunt mai multe registre I/O dedicate întreruperilor. Ele se grupează, în principiu, în:

  • Registre de status - Conțin flag-uri de întrerupere, biți care indică dacă o întrerupere a fost sau nu generată
  • Registre de control - Conțin măști de întrerupere, biți care indică dacă o întrerupere a fost sau nu demascată

Pentru ca un procesor AVR sa trateze o cerere de întrerupere (Mascable Interrupts) trebuie să fie adevărate 3 condiții:

  • Întreruperile sunt activate la nivel global
    • Bitul I din registrul SREG este 1
  • Întreruperea respectivă nu este mascată
    • Bitul corespunzător ei din registrul ei de control este 1, deci masca ei este 1
  • Întreruperea respectivă este activă
    • Bitul corespunzător ei din registrul ei de stare este 1, deci flag-ul ei este 1

Tratarea întreruperilor pe AVR

Î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.

Tabela vectorilor de întrerupere - ATtiny20

Tabela vectorilor de întrerupere - ATtiny20

În datasheetul ATtiny20, în capitolul 9. Interrupts, pagina 36, este prezentata tabela vectorilor de întrerupere a acestui procesor:

Vector No. Program Address Label Interrupt Source
1 0x0000 RESET External Pin, Power-on Reset, Brown-Out Reset, Watchdog Reset
2 0x0001 INT0 External Interrupt Request 0
3 0x0002 PCINT0 Pin Change Interrupt Request 0
4 0x0003 PCINT1 Pin Change Interrupt Request 1
5 0x0004 WDT Watchdog Time-out
6 0x0005 TIM1_CAPT Timer/Counter1 Input Capture
7 0x0006 TIM1_COMPA Timer/Counter1 Compare Match A
8 0x0007 TIM1_COMPB Timer/Counter1 Compare Match B
9 0x0008 TIM1_OVF Timer/Counter1 Overflow
10 0x0009 TIM0_COMPA Timer/Counter0 Compare Match A
11 0x000A TIM0_COMPB Timer/Counter0 Compare Match B
12 0x000B TIM0_OVF Timer/Counter0 Overflow
13 0x000C ANA_COMP Analog Comparator
14 0x000D ADC ADC Conversion Complete
15 0x000E TWI_SLAVE Two-Wire Interface
16 0x000F SPI Serial Peripheral Interface
17 0x0010 QTRIP Touch Sensing

Cu cât o rutină se află mai sus în acest tabel (are o adresă mai mică), cu atât este mai prioritară! De aceea, întreruperea de RESET are prioritatea cea mai mare.


Î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
    ...

Faptul că procesorul este programat ca, la pornire, să înceapă să execute cod de la adresa 0x0000 coincide cu faptul că la acea adresa se afla vectorul de întrerupere pentru reset. Deci funcția noastră 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ă!

În cazul în care nu vrem să facem nimic la întâlnirea unei întreruperi, acea instrucțiune va fi 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.

Exemplu tratare întrerupere Timer

Exemplu tratare întrerupere Timer

Pentru un anumit tip de întrerupere, procesorul primește de la controller-ul de întreruperi un anumit vector (e.g. pentru întreruperea de overflow a Timer/Counter0 - TOV0 controllerul trimite procesorului vectorul TIM0_OVF_ISR, care este definit ca 0x000B). Procesorul execută un CALL virtual către vectorul primit de la controller-ul de întreruperi, ajungând astfel să execute codul de la adresa 0x000B. La această adresă procesorul găsește o valoare. Printre lucrurile plauzibile pe care le poate găsi aici sunt:

  • O instrucțiune RJMP către rutina efectivă de tratare, dacă aceasta este implementată
  • O instrucțiune RETI dacă aceasta nu este implementată
  • Altceva - dacă tabela vectorilor de întrerupere conține orice altă valoarea, ea va fi pur și simplu executată ca o instrucțiune

Activarea întreruperilor pe AVR

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).

La începutul tratării unei întreruperi (deci la execuția instrucțiunii virtuale 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.

Exemplu de utilizare a întreruperilor

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.

timer_interrupt_demo.asm

timer_interrupt_demo.asm

    TCCR0A      equ 0x19
    TCCR0B      equ 0x18
    TIMSK       equ 0x26
 
    rjmp        main            ; Adresa 0x0000
    reti                        ; Adresa 0x0001
    reti                        ; Adresa 0x0002
    reti                        ; Adresa 0x0003
    reti                        ; Adresa 0x0004
    reti                        ; Adresa 0x0005
    reti                        ; Adresa 0x0006
    reti                        ; Adresa 0x0007
    reti                        ; Adresa 0x0008
    reti                        ; Adresa 0x0009
    reti                        ; Adresa 0x000A
    rjmp        TIM0_OVF_ISR    ; Adresa 0x000B
    reti                        ; Adresa 0x000C
    reti                        ; Adresa 0x000D
    reti                        ; Adresa 0x000E
    reti                        ; Adresa 0x000F
    reti                        ; Adresa 0x0010
 
TIM0_OVF_ISR:
    ; Rutina doar încarcă valoarea 42 în R31.
    ldi         R31, 0x2A
    reti
 
main:
    ; Pornim Timer/Counter0.
    ldi         R16, 0b00000000 ; COM0A = 0 (normal port operation, OC0A disconnected)
                                ; COM0B = 0 (normal port operation, OC0B disconnected)
                                ; WGM0[1:0] = 0 (normal mode operation)
    out         TCCR0A, R16
 
    ldi         R16, 0b00000001 ; WGM0[2] = 0 (normal mode operation)
                                ; CS0 = 1 (clkT = clkIO/1, no prescaling)
    out         TCCR0B, R16
 
    ; Activăm întreruperea de overflow pentru Timer/Counter0.
    ldi         R16, 0b00000001 ; TOIE0 = 1 (Timer/Counter0 overflow interrupt enabled)
    out         TIMSK, R16
 
    ; Activăm întreruperile global.
    sei
 
    loop:
        rjmp loop

TL;DR

  • Întreruperile sunt eveniment asincrone execuției programului
  • Dacă apare o întrerupere, procesorul oprește execuția normală a programului după instrucțiunea curentă și execută o rutină de tratare
    • Tabela vectorilor de întreruperi conține câte o intrare pentru fiecare întrerupere care duce la rutina ei de tratare
    • În cadrul rutinei de tratare trebuie să dăm acknowledge la întrerupere
  • Întreruperile pot fi activate/dezactivate la nivel global și demascate/mascate la nivel individual
    • Excepție fac NMI
  • Pe AVR
    • Activarea/Dezactivarea la nivel global se face cu instrucțiunile SEI/CLI
    • Demascarea/Mascarea se face prin registrele de control
      • E.g. TIMSK
    • Tabela vectorilor de întreruperi se află la adresa 0
      • La intrarea într-o rutină de tratare întreruperile sunt dezactivate la nivel global
      • La ieșirea dintr-o rutină de tratare întreruperile sunt activate la nivel global
    • O întrerupere este tratată dacă
      • Întreruperile sunt activate la nivel global (bitul I din SREG este 1)
      • Întreruperea este demascată la nivel individual (masca ei este 1)
      • Întreruperea este activă (flag-ul ei este activ)

Exerciții

Rulați simularea după ce au fost implementate exercițiile 1-3. Pentru a primi punctaj pe aceste exerciții, trebuie prezentată simularea cu semnalele relevante pentru a explica comportamentul codului din rom.v.

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.

task04_skel.asm

task04_skel.asm

    DDRA        equ 0x01
    PORTA       equ 0x02
    TCCR0A      equ 0x19
    TCCR0B      equ 0x18
    TIMSK       equ 0x26
    OCR0A       equ 0x16
 
    rjmp        main            ; Adresa 0x0000
    reti                        ; Adresa 0x0001
    reti                        ; Adresa 0x0002
    reti                        ; Adresa 0x0003
    reti                        ; Adresa 0x0004
    reti                        ; Adresa 0x0005
    reti                        ; Adresa 0x0006
    reti                        ; Adresa 0x0007
    reti                        ; Adresa 0x0008
    rjmp        TIM0_COMPA_ISR  ; Adresa 0x0009
    reti                        ; Adresa 0x000A
    reti                        ; Adresa 0x000B
    reti                        ; Adresa 0x000C
    reti                        ; Adresa 0x000D
    reti                        ; Adresa 0x000E
    reti                        ; Adresa 0x000F
    reti                        ; Adresa 0x0010
 
TIM0_COMPA_ISR:
    ; TODO 4: Schimbați (toggle) starea pinului PA0.
 
main:
    ; TODO 4: Configurați pinul PA0 ca și ieșire.
 
    ; TODO 4: Porniți un timer care să genereze o întrerupere de comparație pe canalul A. Puneți
    ; valoarea 36 ca valoare de comparație pentru canalul A.
    ; Căutați în datasheet după "Timer/Counter0 Output Compare Match A Interrupt Enable"
 
    ; TODO 4: Activați întreruperile global.
 
    ; Deși programul pare să stea într-o buclă infinită, ar trebui să vedem că totuși starea
    ; pinului PA0 se schimbă.
    loop:
        rjmp loop

Resurse