La gestione degli interrupt sui microcontrollori PIC ad 8 e 16bit

La gestione degli interrupt è una cosa forse complicata ma che consente sicuramente di realizzare applicazioni molto complesse ed efficienti e spesso impensabili da eseguire tramite polling. Ho pensato quindi di fare cosa gradita eseguendo una comparazione tra le modalità di funzionamento e di gestione degli interrupt sulle varie fasce di pic. Questo, pertanto, vuole essere sia un articolo di approfondimento sugli interrupt che una terza raccolta di appunti dedicati all’esplorazione e alla migrazione verso i pic a 16 bit.

Riassumo brevemente cos’è un interrupt (o “interruzione” in italiano): è una condizione particolare, interna o esterna al pic, che consente di mettere in pausa il nostro programma principale e fare in modo che la CPU si occupi piuttosto di eseguire una porzione di codice associata alla particolare condizione che ha causato l’interruzione. Quando questo codice è stato eseguito (ovvero: l’interrupt è stato servito), la CPU può tornare ad eseguire il programma principale. Sfruttando opportunamente gli interrupt è possibile realizzare, verosimilmente, programmi multitasking, ovvero che eseguono più operazioni contemporaneamente.

Le differenze nella gestione degli interrupt variano, ovviamente, a seconda dell’architettura del pic e di conseguenza anche il codice da scrivere in C è differente. Per questa trattazione farò riferimento ai seguenti linguaggi:

  • PICC compiler (Hitec-C) per i pic12 e pic16 (fornito di serie con MPLAB)
  • MPLAB C18 per i pic18 (download)
  • MPLAB C30 per i pic24 e dsPic (download)

Se usate compilatori diversi da questi elencati, alcune cose, a livello di scrittura del codice, potrebbero essere diverse.

Ricordo qui alcuni concetti fondamentali riguardanti gli interrupt e da tenere bene a mente quando andremo ad affrontare la loro gestione. Le routine di gestione degli interrupt devono sempre avere i seguenti requisiti per tutte le tipologie di MCU:

  1. Non accettano parametri in ingresso e non restituiscono nulla in uscita (utilizzano sempre il tipo VOID)
  2. Non possono essere richiamate dal programma principale
  3. Devono essere più corte possibile. Qualora si richieda di eseguire istruzioni laboriose è sempre bene nell’ISR settare un flag che verrà poi controllato nel main per eseguire le operazioni associate all’evento di interruzione.
  4. Non devono richiamare altre funzioni

I punti 1 e 2 sono obbligatori, pena la mancata compilazione del programma, i punti 3 e 4 sono invece fortemente consigliati e ritenuti “moralmente” obbligatori.

Quando si verifica un interrupt, lo stato dei registri principali (che variano da pic a pic) viene generalmente salvato per poi essere ripristinato al termine della ISR (Interrupt Service Routine: ovvero la serie di istruzioni da eseguire nel momento in cui si verifica un’interruzione) in maniera tale da poter riprendere il programma nell’esatto punto e nell’esatto stato in cui era stato interrotto.

Al verificarsi di un interrupt, quindi, non viene eseguita subito la prima istruzione dell’ISR ma trascorre un certo lasso di tempo durante il quale vengono appunto eseguite queste operazioni di salvataggio di stato. Il tempo che intercorre tra il verificarsi dell’interrupt e la prima istruzione dell’ISR eseguita è detto tempo di latenza. I pic di fascia medio/alta hanno delle funzionalità hardware che permettono di diminuire i tempi di latenza velocizzando le operazioni di salvataggio di stato.

Ma come fa il pic a capire quali istruzioni deve eseguire al verificarsi di un interrupt e a quale punto tornare dopo che ne ha terminato l’esecuzione?

Il pic tiene conto delle istruzioni da eseguire nel nostro programma mediante un registro particolare chiamato PC (Program Counter – diviso in due parti). In tale registro la CPU carica la locazione di memoria nella quale è contenuta la successiva istruzione da eseguire: man mano che il programma va avanti, il PC viene incrementato.

Nel momento in cui si verifica un interrupt, la CPU deve mettere in pausa il programma principale e dedicarsi quindi ad eseguire le istruzioni definite nella nostra ISR: questo viene realizzato in automatico caricando nel PC il cosiddetto interrupt vector ovvero quella particolare locazione di memoria nella quale partono le istruzioni dell’ISR. In pratica la CPU viene dirottata in un’altra posizione per poter eseguire un codice diverso e poi ritornare al punto in cui si era fermata in precedenza.

E’ facile capire, quindi, che il PC può anche essere alterato via software ma le applicazioni che fanno uso di questa tecnica richiedono conoscenze molto approfondite.

La CPU per poi poter ritornare al punto in cui il programma è stato interrotto utilizza una serie di registri chiamati stack. In tali registri vengono memorizzate le locazioni di memoria a cui tornare dopo un’interruzione o dopo che è stato eseguito un salto qualsiasi (istruzioni GOTO o chiamate a funzioni esterne).

I registri dello stack sono sequenziali dall’alto verso il basso: ogni volta che si esegue un salto, l’ultima locazione di memoria a cui ritornare viene memorizzata verso il basso e a tal scopo c’è anche un registro aggiuntivo chiamato Stack Pointer (STKPTR) che permette di capire in che punto dello stack ritornare (o in quale punto dello stack memorizzare il prossimo ritorno). Si hanno quindi più livelli (più registri) di stack a seconda dell’architettura. Sui pic12 e pic16 di fascia bassa, ad esempio, ci sono 8 livelli di stack il che significa che possiamo annidare massimo 8 “salti”. Quando si annidano funzioni (una funzione che ne richiama un’altra, la quale ne richiama un’altra ancora e così via) si fa appunto utilizzo di salti e quindi dello stack. Nel momento in cui i salti diventano troppi e quindi si supera il livello massimo di stack offerto dalla MCU che stiamo usando, si verifica uno stack overflow che causa comportamenti imprevisti. Il compilatore in genere segnala questa possibilità con un warning durante la compilazione (possible stack overflow).

Alcuni pic utilizzano un set particolare di registri, chiamati registri shadow (o fast stack), che servono a memorizzare in maniera rapida lo stato di alcuni registri principali per poterli riportare alle condizioni precedenti l’interrupt una volta che questo è terminato. Non tutti i pic hanno questi registri e quelli che ce li hanno ne hanno uno per ogni registro da memorizzare durante un interrupt. La presenza di questa funzionalità rende le ISR più veloci (nel senso che hanno una latenza più bassa).

Le modalità con le quali i pic salvano il proprio stato al verificarsi di un interrupt sono sempre riportate sul datasheet alla voce Context saving during interrupts o Automatic contest saving (generalmente nel capitolo Special features of CPU o incluse nel capitolo riguardante gli interrupts.)

Dopo fatte queste premesse, analizzerò qui come i vari pic (e di conseguenza i loro compilatori) gestiscono gli interrupt.

Dispositivi Base-Line

Questa categoria non ha la funzionalità degli interrupt e ho incluso questo paragrafo giusto per spirito di completezza. A questa categoria appartengono i PIC10, che sono pic in formato SMD a 6 pin (o anche in formato DIP a 8 pin dei quali 2 però non sono connessi). Quindi se intendete utilizzare un pic10 per la vostra applicazione, sappiate che non avete a disposizione nessun tipo di interrupt.

Gestione degli interrupt sui PIC12 e PIC16

I pic12 e pic16 hanno un unico interrupt vector posizionato alla locazione 0x04. I dispositivi Midrange (PIC12 e PIC16Fxxx) hanno 8 livelli di stack, i dispositivi Midrange Enhanced (pic16F1xxx) hanno 16 livelli di stack.

Essendoci dopo la locazione 0x04 tutto il resto della memoria programma, le routine di interrupt possono essere anche molto lunghe purchè ovviamente il loro tempo di esecuzione non superi il lasso di tempo tra un interrupt e il successivo. Ricordo difatti che le routine di interrupt devono sempre essere il più corte possibile e, anche se abbiamo a disposizione tanto spazio in cui scrivere le nostre routine e una frequenza di clock molto elevata (che ci consente, cioè, di eseguire le istruzioni molto rapidamente), bisogna abituarsi a scrivere solo l’essenziale e a scriverlo in maniera efficiente.

I pic della serie MidRange al verificarsi di un interrupt, salvano in automatico il contenuto del Program Counter nello Stack e quello dei registri W e STATUS in registri temporanei (W_TEMP e STATUS_TEMP). I PIC dichiarati obsoleti e fuori produzione (come il 16F84 e il 16F877)  , ma anche altri di fasci bassa, salvano soltanto il Program Counter e il salvataggio di W e STATUS e/o altri registri importanti deve eventualmente essere realizzato via software.

Per questi pic, che registrano da sè solo il PC, l’Hitech-C esegue un controllo dell’ISR e delle funzioni eventualmente richiamate da questa per capire se è il caso o meno di eseguire il salvataggio di altri registri importanti. In caso affermativo aggiunge in automatico parti di codice per consentire il ripristino dei registri alla fine dell’Interrupt.

I pic della serie Midrange Enhanced (es.: il PIC16F1934 o il PIC16F1827 per citarne alcuni), oltre al program counter, salvano i registri W, STATUS, BSR, FSR, PCLATH nei registri SHADOW.

Il PICC (hitec-C) gestisce le routine di interrupt mediante l’attributo interrupt da anteporre al nome della funzione che fungerà da gestore interrupt:

void interrupt mia_isr(void)
{
// routine di gestione interrupt
}

Sui pic12/16 ogni interrupt ha un bit di abilitazione (suffisso -IE) che permette all’interrupt di essere catturato dall’ISR e un flag di avvenuto interrupt (suffisso -IF) che ci permette di capire se un interrupt si è verificato o meno. Dato che su questi pic qualsiasi interrupt causa il salto all’unico vettore di interruzione (e quindi il richiamo della nostra ISR), nell’ISR dovremo capire quale interrupt è scattato per poterlo servire e questo viene fatto testando, con una serie di IF, i flag di avvenuto interrupt che ci interessano.

Gli interrupt sui PIC12 e PIC16 hanno una sorta di interruttore generale: il bit GIE (Global Interrupt Enable) che consente di mascherare tutte le sorgenti di interrupt, ovvero di non far richiamare l’ISR anche se i bit di abilitazione sono settati. Vi è inoltre un altro bit di abilitazione per gli interrupt di periferica: PIE (Peripheral Interrupt Enable) che serve per mascherare tutti gli interrupt i cui flag di abilitazione si trovano nei registri PIEx. GIE maschera comunque anche i bit gestiti da PIE.

Gestione degli interrupt sui PIC18

Sui pic18 abbiamo due interrupt vector: quello ad alta priorità (posizionato alla locazione di memoria programma 0x08) e quello a bassa priorità (locazione 0x18). Ogni interrupt può quindi essere impostato per essere gestito in alta o in bassa priorità. A cosa serve la priorità?

Se il processore sta servendo un interrupt a bassa priorità e durante queste operazioni si verifica un interrupt ad alta priorità, l’ISR che gestisce quello a bassa priorità viene messa in pausa per consentire alla CPU di servire quello ad alta priorità. Quando la CPU avrà finito di servire l’ISR ad alta priorità, riprenderà l’esecuzione dell’ISR a bassa priorità e alla fine di quest’ultima riprenderà il programma principale.

L’utilizzo di tale caratteristica viene effettuato settando il bit IPEN del registro RCON. Quando IPEN=1 ci sono due bit che abilitano gli interrupt ad alta priorità (GIEH, bit 7 del registro INTCON) e bassa priorità (GIEL, bit 6 del registro INTCON). Il processore, al verificarsi di un interrupt, andrà a cercarsi le istruzioni da eseguire nelle locazioni 0x08 o 0x18 a seconda della priorità che è stata assegnata all’interrupt che ha causato l’interruzione del flusso del programma principale.

Quando si verifica un interrupt ad alta priorità, oltre a salvare il PC nello stack, il contenuto dei registri WREG, BSR e STATUS viene salvato nei registri shadow (Fast Register Stack), cosa, quest’ultima, che non avviene per gli interrupt a bassa priorità.

I pic18 possono gestire gli interrupt anche in maniera compatibile con i pic16: se poniamo IPEN=0 (condizione di default) non avremo distinzione di priorità. In questo caso abbiamo che il bit 7 del registro INTCON viene indicato come GIE ed abilita la gestione globale degli interrupt, mentre il bit 6 del registro INTCON prende il nome di PIE e serve per abilitare gli interrupt di periferica. In modalità compatibile, tutti gli interrupt vengono dirottati alla locazione 0x08 e non essendoci distinzioni di priorità verranno serviti in sequenza. In altre parole in modalità compatibile tutti gli interrupt sono sempre gestiti come se fossero ad alta priorità e quindi fanno anche uso del Fast Register Stack. Per memorizzare i salti i pic18 hanno ben 31 livelli di stack.

Gli interrupt sui PIC18, per poter essere gestiti, hanno anch’essi un bit di abilitazione (identificato dal suffisso -IE) che consente loro di essere rilevati e un flag che indica l’avvenuto interrupt (suffisso -IF) il quale viene settato anche se il relativo bit di abilitazione è posto a zero. In aggiunta qui abbiamo, per ogni interrupt, anche un bit di priorità (suffisso -IP) che permette di impostare per ogni interrupt una priorità bassa (bit posto a zero) o alta (bit a 1).

Il numero di locazioni tra l’interrupt ad alta priorità e quello a bassa priorità (0x18 – 0x08 = 0x10) non consente di scrivere delle ISR più o meno lunghe per l’interrupt ad alta priorità, per cui generalmente in queste locazioni di memoria si inserisce un salto (GOTO) ad un’altra routine che effettivamente gestirà l’interrupt. Per poter gestire quindi gli interrupt sui PIC18, il codice da scrivere è piuttosto laborioso (anche più di quello necessario rispetto ai pic a 16 bit come vedremo tra poco) e forse anche un po’ brutto perchè il C18 fa utilizzo delle direttive #pragma che non sono conformi allo standard ANSI C ma di sicuro facilitano di molto le cose:

// prototipi di funzione
void interrupt_priorita_bassa(void);
void interrupt_priorita_alta(void);
 
// specifico che la successiva porzione di codice deve essere messa
// nella locazione 0x08, ovvero quella destinata all'interrupt
// ad alta priorità. I nomi che assegno sia al vettore che
// alla funzione li posso dare a piacere e comunque di questi
// nomi non ne avrò più bisogno nel resto del codice
#pragma code vettore_alta_priorita = 0x08
void ISRH(void)
{
// imposto, in assembler, un salto alla mia routine di
// gestione dell'interrupt a priorità alta
_asm GOTO interrupt_priorita_alta _endasm
}
 
// specifico che la successiva porzione di codice deve essere messa
// nella locazione 0x18, ovvero quella destinata all'interrupt
// a bassa priorità.
#pragma code vettore_bassa_priorita = 0x18
void ISRL(void)
{
// imposto, in assembler, un salto alla mia routine di
// gestione dell'interrupt a priorità bassa
_asm GOTO interrupt_priorita_bassa _endasm
}
 
// specifico che il codice seguente andrà in altre locazioni
// di memoria scelte dal compilatore
#pragma code
 
// specifico che la funzione seguente è associata ad un interrupt
#pragma interrupt interrupt_priorita_alta
void interrupt_priorita_alta(void)
{
// codice per gestire interrupt a priorità alta
}
 
// specifico che la funzione seguente è associata ad un interrupt
// a bassa priorità
#pragma interruptlow interrupt_priorita_bassa
void interrupt_priorita_bassa(void)
{
// codice per gestire interrupt a priorità bassa
}
 
void main(void)
{
// main
}

Nel caso in cui si lavora in modalità compatibile basta eliminare le porzioni di codice che gesticono gli interrupt a bassa priorità. Nelle routine di interrupt, infine, come sui pic12/16 dovremo mettere una serie di IF per verificare quale particolare interrupt ha causato il salto alla ISR. Si controlleranno quindi i vari flag di interrupt -IF. Appena finito di servire l’interrupt, dovremo ricordarci come sempre di azzerarne il flag.

Gestione degli interrupt sui pic a 16bit (PIC24 e dsPIC)

I pic a 16 bit (PIC24 e dsPIC) hanno una moltitudine di interrupt vectors a partire dalla locazione 0x000004. A differenza dei pic ad 8 bit, qui ogni sorgente di interrupt, sia esterna che interna, ha il proprio esclusivo interrupt vector. Chi è abituato con i pic ad 8 bit si trova davanti ad una cosa nuova. Per questi pic abbiamo una Interrupt Vector Table (IVT) nella quale sono contenuti tutti i vettori di interrupt. La tabella è possibile trovarla nel datasheet del dispositivo o nel family reference manual:

estratto dal dsPic33 Family Reference Manual – Parte 1, sezione 6

Chiariremo dopo il significato dei valori delle colonne di questa tabella.

Apparentemente riuscire a gestire oltre un centinaio di sorgenti di interrupt su questi pic (ben 126!) potrebbe apparire una cosa molto complicata. In realtà la gestione degli interrupt sui pic a 16bit è ancora più semplice rispetto a quella dei cugini ad 8 bit; il C30 ci viene in aiuto con una funzione di interrupt per ogni richiesta di interruzione: non avremo più, quindi, un’unica ISR (o due come nel caso dei pic18) all’interno della quale dovremo andare a discernere l’interrupt che si è verificato bensì tante funzioni di interrupt separate. Vedremo tra poco come tutto questo si traduce in codice.

Lo stack, inoltre, sui pic a 16 bit è gestito in maniera completamente diversa e non ha un limite fisso. Abbiamo visto che sui pic18 lo stack è gestito via hardware mediante una pila di 32 registri e uno stack pointer. Sui dsPic e pic24 lo stack è gestito via software nella memoria RAM a partire dalla locazione 0x0800 (appena al di sotto dell’area dedicata agli SFR) e il suo limite viene imposto tramite il registro SPLIM. La funzione di stack pointer è assolta dal working register W15.

Le funzioni di interrupt e la parola chiave __attribute__

La scrittura delle funzioni di interrupt sul C30 utilizza una parola chiave facente parte del set avanzato di istruzioni del C Ansi: __attribute__ seguita da uno o più attributi separati da virgole inclusi in parentesi tonde doppie. Questa parola chiave serve per specificare degli attributi particolari associati alle funzioni (o alle variabili) che ne fanno utilizzo. Per farla breve, la funzione di interrupt associata al Timer1 su un pic a 16 bit, utilizzando MPLAB C30, andrà scritta come:

void __attribute__((interrupt)) _T1Interrupt(void)
{
// funzioni da eseguire sull'interrupt del Timer1
}

Con le parole chiave __attribute__((interrupt)) stiamo specificando che questa non è una normale funzione, bensì è una funzione da richiamare in automatico al verificarsi del particolare interrupt che abbiamo specificato dopo: _T1Interrupt.

L‘identificatore _T1Interrupt è associato all’interrupt vector dell’overflow sul timer1 (come specificato nella IVT), non è una parola che ho scelto io in maniera arbitraria ma è definita nel file linker (che per i pic a 16 bit ha estensione .GLD anzichè .LKR) del pic in questione.

Ad esempio, per il dsPIC33FJ128GP802 il file di linker si trova in:

C:\programmi\Microchip\MPLAB C30\support\dsPIC33F\gld\

ed ha più o meno lo stesso nome del dsPIC in questione: p33FJ128GP802.gld. Andando ad aprire tale file con un normale editor di testo (io consiglio sempre Notepad++), vediamo che da riga 236 partono le definizioni della interrupt vector table.

A parte i primi 8 interrupt che sono particolari e di cui parlerò dopo, possiamo individuare dei nomi abbastanza semplici da capire, l’unica differenza è che in questo file i vettori di interrupt sono indicati con due underscore prima del nome, mentre noi nel codice ne andremo a mettere uno solo. Dal momento che consultare il file di linker è abbastanza noioso, l’elenco dei nomi mnemonici associati ai vettori di interrupt è anche contenuto nella guida utente dell’ MPLAB C30 (capitolo 8): vedete che sono riportati i nomi mnemonici per le varie famiglie di pic a 16bit.

La guida utente dell’ MPLAB C30, come tutte le guide dei compilatori, si trova nella cartella docs del percorso di installazione.

Ogni interrupt è identificato anche da un numero, indicato in tabella come IRQ# (Interrupt Request number): difatti gli interrupt li possiamo chiamare, oltre che con il loro nome mnemonico, anche come _Interrupt72 ad esempio. Il numero di IRQ, inoltre, stabilisce anche un certo livello di priorità naturale come vedremo dopo.

Nota per gli “anziani”: ricordate quando sui vecchi 386/486 ecc per far funzionare la scheda audio nei giochi dovevamo per forza settare manualmente gli IRQ? Se due dispositivi condividevano lo stesso numero di IRQ ecco che qualcosa non funzionava…

La tabella interrupt alternativa

Al disotto dello spazio di memoria occupato dalla IVT, fisicamente, c’è la AIVT (Alternate Interrupt Vector Table). La tabella alternativa per gli interrupt viene utilizzata per applicazioni particolari o in fase di test/debug. Settando il bit ALTIVT nel registro INTCON2 tale tabella viene abilitata con la conseguenza che gli interrupt non saranno più serviti dalle funzioni che utilizzano i vettori della IVT ma da quelle che utilizzano i vettori della AIVT (ogni interrupt ha difatti un vettore sia nella IVT che nella AIVT).

Il vettore della AIVT viene identificato con il prefisso Alt- dopo l’underscore. Volendo possiamo quindi definire, nello stesso programma, due ISR per lo stesso interrupt:

void __attribute__((interrupt)) _T1Interrupt(void)

e

void __attribute__((interrupt)) _AltT1Interrupt(void)

ovviamente di default al verificarsi dell’interrupt sul timer1 verrà eseguita unicamente la prima funzione, se invece mettiamo a 1 il bit ALTIVT verrà eseguita unicamente la seconda.

Macro per le ISR

Ricordarsi ogni volta di scrivere __attribute__((interrupt)) è abbastanza noioso, lungo e difficile da ricordare, per questo motivo la Microchip ha inserito alcune macro nei file header dei pic a 16bit che ci permettono di scrivere le funzioni di interrupt come:

void _ISR _T1Interrupt(void)

vediamo difatti che nel file header di un qualsiasi pic a 16 bit è appunto definita la macro:

#define _ISR __attribute__((interrupt))

che ci permette di scrivere la stessa cosa ma in maniera più corta!

Le ISR “rapide”

Quando si verifica un interrupt, sappiamo che la CPU deve salvarsi lo stato di tutti i registri vitali (in questo caso i 4 registri di lavoro e il registro di stato SR) in maniera tale da poter ripristinare la situazione operativa alle condizioni precedenti una volta che l’interrupt è terminato.

I pic a 16bit, come i pic18, hanno un set di registri shadow da utilizzare per il salvataggio rapido dello stato. Utilizzando tali registri l’interrupt viene servito nella maniera più rapida possibile. Per sfruttare questa caratteristica si aggiunge un altro attributo all’isr che serve appunto a specificare la volontà di utilizzare questi registri shadow:

void __attribute__((interrupt,shadow)) _T1Interrupt(void)

oppure utilizzando la macro _ISRFAST:

void _ISRFAST _T1Interrupt(void)

Il set di registri shadow è uno solo per cui una, ed una sola, routine di interrupt può utilizzare questa caratteristica. In altre parole, se nel nostro programma intendiamo gestire più di un interrupt, una sola ISR potrà fare uso della _ISRFAST e tutte le altre dovranno usare la normale _ISR.

I livelli di priorità e l’annidamento

Abbiamo visto che i pic18 hanno due livelli di priorità. Sui pic a 16 bit ne abbiamo ben 7 impostabili per i “nostri” interrupt. Le cose quindi si stanno complicando! Anche qui se si verificano contemporaneamente due interrupt a diversi livelli di priorità, la CPU servirà prima quello a priorità più alta. Nel caso in cui si verifichino contemporaneamente due interrupt allo stesso livello di priorità, verrà servito per primo quello con l’IRQ più basso, ecco perchè prima parlavo di “priorità naturale”.

Sui pic a 16 bit anche la CPU ha un suo livello di priorità che ha la funzione di mascherare gli interrupt indesiderati: gli interrupt che hanno un livello di priorità minore o uguale al livello di priorità impostato per il processore (IPL), vengono ignorati.

Ad esempio se IPL si trova a 4, verranno serviti soltanto gli interrupt con un livello da 5 a salire. Al powerup IPL si trova a zero e il livello di priorità di tutti gli interrupt è posto a 4, per cui di default tutti gli interrupt vengono serviti.

Il livello di priorità del processore è impostabile manualmente via software agendo sui bit IPL0,IPL1,IPL2 del registro SR (CPU Status Register):

Avendo 3 bit a disposizione, possiamo impostare livelli di priorità da 0 a 7 (8 livelli). IPL può essere impostato utilizzando alcune macro (definite nel file H di ogni pic a 16bit):

SET_CPU_IPL(ipl) // imposta ad ipl il livello di priorità del processore. ipl da 0 a 7
 
SET_AND_SAVE_CPU_IPL(save_to, ipl) // salva il livello corrente nella locazione di memoria indicata da save_to e imposta il livello attuale ad ipl
 
RESTORE_CPU_IPL(saved_to) // reimposta il livello di priorità a quello salvato nella locazione saved_to, esegue la stessa operazione della macro SET_CPU_IPL

Il livello di priorità degli interrupt, invece, viene settato nei registri IPCx. Ad esempio, per l’interrupt su Timer1, il livello di priorità viene impostato nel registro IPC0 agendo sui bit <14:12>. Il livello di priorità degli interrupt può variare da 1 a 7 (7 livelli), non può quindi assumere valore 0: impostare a zero il livello di priorità di un interrupt equivale a disabilitarlo.

La descrizione dei registri IPCx purtroppo non si trova nel datasheet ma nel Family Reference Manual della famiglia del pic a 16 bit in uso (per i dsPic33 fare riferimento al dsPIC33 Family Reference Manual – Part 5 – Section 47, per i pic24F fare riferimento al PIC24F Family Reference ManualSection 8).

Ricordo di nuovo che ponendo livello di priorità del processore (IPL) a 0, tutti gli interrupt vengono serviti in quanto i livello di priorità degli interrupt vale minimo 1.

I pic a 16 bit supportano l’annidamento degli interrupt: se il processore sta servendo un interrupt ad un certo livello di priorità e durante queste operazioni si verifica un interrupt ad un livello di priorità più alto, l’esecuzione dell’interrupt a livello più basso viene messa in pausa per poter servire l’interrupt a livello alto. Questo comportamento è quello di default e può essere alterato settando il bit NSTDIS del registro INTCON1. Mettendo ad 1 il bit NSTDIS (Nested Interrupt Disable), gli interrupt vengono serviti in sequenza.

In altre parole, ponendo NSTDIS a 1, un interrupt, anche se ha un livello di priorità più alto di quello correntemente servito, dovrà attendere che l’interrupt precedente sia stato completato. Tuttavia nel caso in cui si verifichino contemporaneamente due interrupt, verrà servito per prima quello a priorità più alta e quindi il livello di priorità, in questo particolare caso, viene sfruttato unicamente per risolvere eventuali conflitti.

Il processore realizza questa funzione ponendo in automatico a 7 il livello di priorità del processore non appena un interrupt si verifica ed inizia ad essere servito: in questo modo se si verificano altri interrupt durante questo tempo, non saranno serviti. IPL viene resettato non appena l’interrupt corrente cessa di essere servito consentendo alla CPU di catturare gli altri interrupt. Con NSTDIS a 1, inoltre, IPL può essere soltanto letto ma non scritto.

Impostare manualmente IPL a 7 causa la disattivazione degli interrupt in quanto il livello di priorità dei normali interrupt non può essere superiore a 7 e un interrupt per essere servito deve avere un livello di priorità superiore a quello del processore.

Possiamo assumere che con NSTDIS=1 il comportamento è simile a quello dei pic16.

Abilitare e rilevare gli interrupt sui pic a 16bit

Sui pic a 16bit, come sugli altri pic, ogni interrupt può essere attivato/disattivato tramite il proprio bit di abilitazione (che ha suffisso -IE). Però qui la situazione è più ordinata: i bit di abilitazione si trovano nei registri IECx, quindi non c’è più distinzione tra interrupt “normali” e interrupt di periferica.

La rilevazione, come abbiamo visto, viene invece eseguita tramite le ISR dedicate. In ogni caso anche qui, ovviamente, ogni interrupt ha il proprio flag che indica se l’interrupt si è verificato o meno. I flag di avvenuto interrupt hanno il suffisso -IF, si trovano nei registri IFSx, e vengono settati comunque anche se l’interrupt non è stato abilitato.

La descrizione dei registri IECx e IFSx purtroppo non si trova nel datasheet ma nel Family Reference Manual della famiglia del pic a 16 bit in uso (per i dsPic33 fare riferimento al dsPIC33 Family Reference Manual – Part 5 – Section 47, per i pic24F fare riferimento al PIC24F Family Reference ManualSection 8).

I flag di interrupt anche qui vanno azzerati non appena l’interrupt è stato servito, altrimenti non si esce mai dalle ISR. Nel caso del timer1, quindi, la routine di interrupt sarà:

void _ISR _T1Interrupt(void)
{
// istruzioni
_T1IF=0; // azzero il flag di interrupt su Timer1
}

A differenza dei pic di fascia inferiore, qui non abbiamo il bit GIE che permette il mascheramento degli interrupt (nè tantomeno un bit PIE per abilitare gli interrupt di periferica): ogni interrupt ha il suo bit di abilitazione, punto e basta. Agendo sui livelli di priorità, però, dal momento che nessun interrupt può avere priorità 7, basta mettere a 7 il livello di priorità del processore per mascherare gli interrupt.

Esiste inoltre un’istruzione assembler (DISI) che permette di disabilitare tutti gli interrupt per un determinato numero di cicli:

asm volatile ("disi #0x3FFF");

in alternativa è possibile utilizzare una builtin:

__builtin__disi(0x3FFF);

Questa istruzione in pratica disabilita gli interrupt per i prossimi 0x3FFF (16383) cicli macchina (valore massimo). L’istruzione DISI ferma temporaneamente solo gli interrupt aventi livelli di priorità da 1 a 6, gli interrupt con livelli superiori non vengono fermati.

Se i cicli macchina impostati non sono ancora passati ma vogliamo comunque ripristinare gli interrupt, si può farlo azzerando il registro di conteggio dei cicli macchina dopo i quali riattivare gli interrupt (DISICNT : Disable Interrupt Control Register):

DISICNT=0;

Il registro DISICNT si trova normalmente a zero, se lo impostiamo su un valore diverso da zero (operazione che in pratica viene eseguita dall’istruzione DISI), gli interrupt vengono disabilitati temporaneamente e il contatore esegue un conto alla rovescia man mano che i cicli macchina procedono; arrivato a zero gli interrupt vengono riabilitati. L’eventuale entrata in funzione dell’istruzione DISI è anche indicata dal bit DISI nel registro INTCON2.

Questa istruzione diventa necessaria nei punti in cui modifichiamo al volo i livelli di priorità nel nostro programma, in quanto durante queste operazioni gli interrupt devono essere temporaneamente disattivati oppure può tornare utile in quei punti del programma in cui sono richieste operazioni critiche che richiedono di essere eseguite in un certo tempo e quindi non devono essere interrotte.

Le trappole

Quando abbiamo visto la IVT vi ho anticipato che i primi 8 interrupt sono particolari. Questi interrupt vengono chiamati trappole (traps) e sono utilizzati per catturare le condizioni di errore del processore o malfunzionamenti (guasto dell’oscillatore, operazioni matematiche errate ecc).

Dal momento che i problemi rilevati da questi interrupt sono abbastanza gravi e compromettono il funzionamento della nostra applicazione, le trappole hanno un livello di priorità al di sopra di tutti gli altri interrupt (hanno un livello da 8 a 15), non vengono mai disattivati dall’istruzione DISI e non vengono rallentati dal flag NSTDIS (passano sempre avanti). Le trappole sfruttano comunque l’annidamento perchè è ovvio che una condizione di allarme a priorità più alta ne scavalchi una a priorità più bassa. Le trappole vengono anche dette interrupt non mascherabili.

Dal momento che le trappole hanno un livello di priorità da 8 a 15, oltre ai 3 bit IPL visti prima (che possono quindi contenere un numero fino a 7), c’è anche un bit IPL3 nel registro CORCON, che serve appunto a rilevare questa condizione di interrupt particolare (direi più condizione di allarme). Il bit IPL3, difatti, a differenza degli IPL0:2, può solo essere letto o resettato ma non posto a 1.

La trappola a priorità più alta è il guasto dell’oscillatore (sebbene sopra di lei ci sia una trappola attualmente inutilizzata, probabilmente prevista per usi futuri). La trappola del controller DMA (livello 10), la trappola di errore matematico (livello 11) e la trappola di errore dello stack (livello 12) sono anche dette soft traps, le altre sono dette hard traps. La differenza sta nel fatto che le trappole “hard” forzano la CPU ad interrompere l’esecuzione del codice appena dopo che l’istruzione che ha causato la trappola viene completata, mentre le trappole soft si prendono un ciclo in più per confermare la condizione di errore.

MPLAB C30 associa, di default, le trappole ad una routine di reset del processore ma nulla vieta di dirottare la gestione delle trappole scrivendo delle nostre routine come per i normali interrupt. Anche le trappole, quindi, hanno il loro vettore alternativo nella AIVT.
Quando una trappola causa il reset del processore viene settato il bit TRAPR (Trap Reset Flag) del registro RCON (Reset Control register).

Come vedete sui pic a 16bit la gestione degli interrupt è molto raffinata ed orientata alla massima sicurezza.

Bibliografia

Se questo articolo ti è piaciuto, condividilo su un social:
Se l'articolo ti è piaciuto o ti è stato utile, potresti dedicare un minuto a leggere questa pagina, dove ho elencato alcune cose che potrebbero farmi contento? Grazie :)