Come implicito nel nome stesso, un interrupt e' un evento
che interrompe la normale esecuzione di un programma.
Come gia' precedentemente affermato, il flusso di un programma e sempre
di tipo sequenziale e puo' essere alterato da particolari istruzioni che
volutamente intendono deviare il flusso del programma stesso. Gli interrupt
forniscono invece un meccanismo di "congelamento" del flusso del programma
in corso, eseguono una subroutine (sottoprogramma) e ripristinano il normale
funzionamento del programma come se nulla fosse accaduto. Questa subroutine,denominate
"interrupt handler" (gestore di interrupt) viene eseguita soltanto quando
si verifica un determinato evento che attiva l'interrupt. L'evento puo'
essere: il timer che va in overflow, la ricezione di un byte dalla seriale,
la fine della trasmissione di un byte dalla seriale e uno o due eventi
esterni. L'8051 puo' essere configurato per gestire un interrupt handler
per ognuno di questi eventi.
La capacita' di interrompere la normale esecuzione di un programma quando un particolare evento si verifica, rende il programma stesso molto piu' efficiente nel gestire processi asincroni. Qualora non disponessimo degli interrupt e volessimo gestire piu' eventi dal programma principale, saremmo costretti a scrivere un codice poco elegante e difficile da capire. Allo stesso tempo il nostro programma, in particolari condizioni, diventerebbe inefficiente, dal momento che perderemmo del tempo prezioso ad aspettare un evento che non avviene molto frequentemente.
Per esempio, supponiamo di avere una grande memoria programma di 16k che esegue molte subroutine per eseguire molti lavori. Supponiamo inoltre che il nostro programma debba automaticamente far cambiare lo stato del pin P3.0 ogni volta che il timer 0 va in overflow.
Il codice per effettuare tale azione non e' tanto difficile:
Per fortuna, questo non serve. Gli interrupt ci permettono di dimenticarci di controllare la condizione che scatena l'interrupt stesso. Il microcontrollore eseguira' il controllo automaticamente e quando la condizione sara' vera, richiamera' la subroutine (chiamata interrupt handler), eseguira' il codice in esso contenuta e ritornera'. In questo caso la la nostra subroutine dovrebbe ridursi a:
Dovete sempre terminare un interrupt handler con RETI.
Tornando all'esempio precedente, ogni 65536 cicli d'istruzione eseguiremo
le istruzioni CPL e RETI che richiedono solo 3 cicli. Come risultato finale
il nostro codice e' 437 volte piu' efficiente del precedente senza
contare che e' piu' facile da leggere e da capire. Dobbiamo solo predisporre
l'interrupt e dimenticarcene, fiduciosi che l'8051 eseguira' il nostro
codice quando serve.
Lo stesso concetto si applica alla ricezione dei dati dalla porta seriale.
Una possibilita' e' quella di controllare continuamente lo stato del flag
RI in un loop infinito. Oppure noi potremmo controllare il flag all'interno
del piu' grande loop del programma principale. Nel peggiore dei casi potremmo
correre il rischio di perdere il carattere ricevuto. Se un carattere
viene ricevuto subito dopo che e' stato effettuato il controllo del flag
RI, nella prosecuzione del resto del programma, prima di controllare di
nuovo RI, un nuovo carattere potrebbe arrivare e noi avremo perso il precedente.
Usando l'interrupt, l'8051 ferma il programma e richiama l'interrupt handler
per servire la ricezione del carattere. Cosi' non saremo costretti a scrivere
un antiestetico controllo nel nostro codice e nemmeno correremo il rischio
di perdere i caratteri ricevuti.
Quali eventi possono attivare un interrupt
Possiamo configuarare l'8051 affinche' ognuno di questi eventi possa causare un interrupt:
Interrupt | Flag | Indirizzo dell'Interrupt Handler |
INT 0 | IE0 | 0003h |
Timer 0 | TF0 | 000Bh |
INT 1 | IE1 | 0013h |
Timer 1 | TF1 | 001Bh |
Seriale | RI/TI | 0023h |
Se consultiamo la tabella, possiamo affermare che, se il timer 0 va
in overflow, il programma principale sospendera' momentanemante il programma
e saltera' all'indirizzo 0003H dove avremmo dovuto scrivere l'interrupt
handler opportuno.
Al power up, per default, tutti gli interrupt sono disabilitati. Questo
significa che, anche se per esempio il flag TF0 e' attivo, l'interrupt
non verra' accettato. Il vostro programma deve specificatamente dire all'8051
che intende abilitare la gestione degli interrupt e indicare quali
interrupt sono abilitati ad essere serviti.
Il vostro programma puo' abilitare o disabilitare gli interrupt mediante il registro IE (A8h):
Bit | Nome | Indirizzo a Bit | Funzione |
7 | EA | AFh | Abilita globalmente gli inetrrupt |
6 | - | AEh | Non definito |
5 | - | ADh | Non definito |
4 | ES | ACh | Abilita l'interrupt della seriale |
3 | ET1 | ABh | Abilita l'interrupt del Timer 1 |
2 | EX1 | AAh | Abilita l'interrupt esterno INT 1 |
1 | ET0 | A9h | Abilita l'interrupt del Timer 0 |
0 | EX0 | A8h | Abilita l'interrupt esterno INT 0 |
Come potete notare, ciascun interrupt dell'8051 dispone si un suo bit nel registro IE. Potete abilitare un determinato interrupt, settando il corrispondente bit. Per esempio, se desiderate abilitare l'interrupt del Timer 1, potreste usare ambedue le seguenti istruzioni:
Questo bit e' utile durante l'esecuzione di un programma che abbia una
porzione di codice con criticita' temporali. Allora, per evitare che quella
parte di codice possa essere interrotta da un qualsiasi interrupt, sara'
sufficiente resettare il bit 7 di IE e settarlo di nuovo quando la sezione
critica e' terminata.
Durante ogni istruzione, l'8051 verifica che non vi sia un interrupt
da servire. Durante tale controllo, esso segue il seguente ordine:
L'8051 dispone di due livelli di priorita' degli interrupt: alto e basso. Usando tali priorita' potete cambiare l'ordine con il quale gli interrupt vengono serviti.
Per esempio, se avete abilitato sia l'interrupt del Timer 1 che quello della porta seriale e ritenete che la ricezione di un carattere sia molto piu' importante della gestione del timer, potreste assegnare alla seriale un priorita' alta, in maniera da cambiare l'ordine standard con il quale il micro serve normalmente gli interrupt.
La priorita' degli interrupt e' controllata dal registro IP (B8h).
Esso ha il seguente formato:
Bit | Nome | Indirizzo a Bit | Funzione |
7 | - | - | Non definito |
6 | - | - | Non definito |
5 | - | - | Non definito |
4 | PS | BCh | Priorita' Interrupt Seriale |
3 | PT1 | BBh | Priorita' Interrupt Timer 1 |
2 | PX1 | BAh | Priorita' Interrupt esterno INT 1 |
1 | PT0 | B9h | Priorita' Interrupt Timer 0 |
PX0 | B8h | Priorita' Interrupt esterno INT 0 |
Quando consideriamo la priorita' degli interrupt, vanno applicate le
seguenti regole:
Cosa succede quanto si verifica
una richiesta d'interrupt?
Al momento che un interrupt viene richiesto, il microcontrollore automaticamente esegue le seguenti operazioni:
Che succede quando un interrupt
finisce?
Un interrupt termina quando viene eseguita l'istruzione RETI (Return from Interrupt). A quel punto il microcontrollore esegue i seguenti passi:
Gli interrupt seriali sono leggermente diversi da tutti gli altri. Cio' e' dovuto al fatto che ci sono due flag di interrupt: RI e TI. Se uno dei due e' attivo allora anche l'interrupt della seriale diventera' attivo. Questo vuol dire che, al momento di una richiesta di interrupt sulla seriale, non conosciamo quali dei due o tutti e due i flag sono attivi. Allora, l'interrupt handler deve verificare lo stato dei flag e comportarsi di conseguenza. Inoltre deve anche cancellare i flag poiche' l'8051 volutamente non lo fa in maniera automatica.
Un breve esempio di codice vi chiarira' il tutto:
INT_SERIAL: | JNB RI,CHECK_TI | ; Se il flag RI non e' attivo vai a CHECK_TI |
MOV A,SBUF | ; leggi il buffer di ricezione | |
CLR RI | ; Cancella il flag RI | |
CHECK_TI: | JNB TI,EXIT_INT | ; Se il flag TI non e' attivo vai a EXIT_TI |
CLR TI | ; Cancella il flag TI prima di inviare un nuovo carattere | |
MOV SBUF,#?A? | ; Copia il nuovo carattere da inviare nel buffer di trasmissione. | |
EXIT_INT: | RETI |
Una Importante
Considerazione sugli interrupt: Preservare il valore di un Registro
Una regola importante deve essere applicata da tutti gli interrupt handler: Ogni interrupt deve lasciare il processore nel medesimo stato che esso aveva al momento di iniziare l'interrupt stesso.
Il programma principale non tiene conto del fatto che i vari interrupt sono eseguiti di nascosto.
Prendiamo in considerazione la seguente porzione di codice:
Ma cosa accadrebbe se appena conclusa l'istruzione MOV viene servito un interrupt e questo cambia sia il carry che il valore dell'accumulatore ponendolo ad uno? Quando l'interrupt restituisce il controllo al programma principale, l'istruzione ADDC addizionera' 10h a 40h e aggiungera' 1h perche' trovera' il bit di carry settato. In questo caso l' accumulatore conterra' il valore 51h invece di 35h.
Quello che e' successo nella relata', e' che l'interrupt handler non ha preservato il valore del registro che ha usato. La regola pero' dice : Ogni interrupt deve lasciare il processore nel medesimo stato che esso aveva al momento di iniziare l'interrupt stesso.
Cosa vuol dire l'affermazione precedente? Vuol dire che, se l'interrupt usa l'accumulatore, deve assicurare che il valore di esso rimanga inalterato; cio' viene ottenuto con delle operazioni di PUSH e POP. Per esempio:
Il programma principale trovera' la situazione dei suoi registri immutata
e quindi non si accorgera' minimamente dell' interruzione.
In generale, la routine di interrupt deve preservare il contenuto dei
seguenti registri:
Notate che l'assembler non vi permette di eseguire la seguente istruzione:
Percio', se state usando un registro "R" all'interno della routine di interrupt, effettuate l'operazione di push facendo riferimento all'indirizzo assoluto del registro stesso.
Per esempio, invece di usare PUSH R0, dovreste usare:
Problemi Comuni
con l'uso degli Interrupt
Gli interrupt sono degli strumenti molto potenti, ma se usati in maniera
non corretta, sono una fonte di ore spese a trovare problemi nel programma.
Gli errori sulle routine di interrupt sono spesso molto difficili da
diagnosticare e correggere.
Se state utlizzando degli interrupt e il vostro programma va in crash oppure ha dei comportamenti strani con risultati randomici, ricordatevi di verificare che le regole appena esposte non siano state violate.
In generale i problemi piu' comuni derivano da: