Corso programmazione PICMicro in C – Lezione 4 – Cosa sono gli interrupt, concetti di base per sistemi operativi MultiTasking su PICMicro
Aggiornamento Ottobre 2017
La Microchip ha rilasciato nuovi tool per lo sviluppo: MPLAB X IDE e i compilatori XC. Per far fronte alle novità e per non riscrivere tutti gli articoli daccapo, ho scritto delle lezioni integrative per consentire il passaggio dai vecchi strumenti a quelli nuovi. Le nozioni teoriche riportate negli articoli “vecchi” sono ancora valide. Per quanto concerne la scrittura del codice, l’utilizzo dell’IDE, i codici di esempio ecc, fate riferimento alle nuove lezioni che si trovano nella categoria PICmicro nuovo corso.
Alla fine di questa lezione e della successiva, con il nostro programma che andremo a caricare nel picmicro, saremo in grado di eseguire ben 2 compiti contemporaneamente: faremo lampeggiare un led mentre un cicalino suona una nota fissa. Tale cosa non è realizzabile normalmente senza ricorrere a particolari ed elaborati artifizi: un programma viene difatti eseguito in maniera sequenziale: un’istruzione dopo l’altra, e realizzare due compiti in contemporanea, con tale sistema, non è assolutamente possibile. Chi sa programmare sul pc con i linguaggi ad alto livello, sa benissimo che per eseguire due o più compiti in contemporanea è necessario ricorrere alla creazione di thread separati: nella nostra fantasia possiamo immaginare che il nostro programma principale crei uno spazio in memoria, al quale affida un compito: trovandosi in uno spazio separato, la funzione può lavorare contemporaneamente al programma principale senza influire sulla sua normale esecuzione.
In un picmicro non è possibile la creazione di thread nel senso stretto, e si ricorre ad un vecchio metodo sempre molto valido (che può comunque essere utilizzato anche nei linguaggi di programmazione ad alto livello): si sfruttano i timer per scandire a tempo le operazioni. Facendo un esempio pratico: voglio che ogni 2 secondi venga eseguita la funzione A e ogni mezzo secondo la funzione B: un timer principale effettuerà il conteggio del tempo (mentre il resto del programma continua ad essere eseguito), incrementando a sua volta due diversi contatori: uno che terrà il tempo per la funzione A e uno che terrà il tempo per la funzione B, ad ogni evento particolare associato al timer principale (vedremo poi), entrambi i contatori saranno incrementati: quando il B arriverà a mezzo secondo, ecco che sarà quindi azzerato/ricaricato e verrà eseguita la funzione B associata ad esso, stessa cosa per il contatore A arrivato a 2 secondi.
Ovviamente qui ci sarebbe da obiettare non poco, dal momento che tali funzioni effettivamente non vengono eseguite in contemporanea, ma a scadenze predeterminate, certo: avete perfettamente ragione. Immaginiamo però che i tempi non siano così “palpabili”: definiamo scadenze bassissime, di cui non possiamo renderci conto: parliamo di millesimi di secondo, e mettiamo caso che le nostre funzioni vengano eseguite così velocemente da non prendere più di un millisecondo di tempo… Vedete? Le cose stanno già cambiando: con tale velocità di esecuzione, anche se i vari task (i vari compiti) sono schedulati, al nostro occhio sembrerà che tutto avvenga in contemporanea.
Indice dei contenuti
Gli interrupt
La chiave per ottenere tutto questo tramite i timer è l’utilizzo degli interrupt. Il Timer0 dei picmicro può generare un interrupt. Ma procediamo per gradi, cosa è un interrupt? Su Wikipedia viene definito come:
un segnale asincrono che indica il bisogno di attenzione oppure un evento sincrono che consente l’interruzione di un processo qualora si verifichino determinate condizioni
Questa spiegazione è molto valida e rende bene il concetto (asincrono: non segue il normale ritmo del flusso principale), ma ovviamente un esempio può chiarire meglio come agisce un interrupt, per tale motivo ricorderò qui il famoso esempio del telefono di Sergio Tanzilli, al quale devo molto dal momento che è grazie a lui che sono entrato nel mondo dei picmicro.
Siamo a casa e abbiamo un telefono: per vedere se qualcuno ci sta chiamando è necessario che ogni tanto alziamo la cornetta? No, che fesseria direte voi! Il telefono squilla avvisandoci che qualcuno ci sta chiamando, quindi alzeremo la cornetta solo quando sta squillando (vi sembra una banalità eh? Non lo è affatto).
Immaginate: siamo a casa ad effettuare le nostre faccende, non abbiamo bisogno di alzare ogni tanto la cornetta, perchè il telefono ci avviserà con uno squillo, consentendoci di interrompere ciò che stiamo facendo e dedicarci quindi alla telefonata.
Ecco il concetto chiave: lo squillo del telefono è il nostro interrupt: ci consente di interrompere momentaneamente ciò che stavamo facendo per dedicarci alla situazione che ha generato l’interrupt fino a quando non decidiamo di riprendere le nostre faccende nel punto in cui le avevamo rimaste.
Un interrupt periodico (come è quindi quello scatenato da un timer), legato a vari contatori, ci aiuta a creare sistemi multitasking che operano in tempo reale: ovvero più funzioni eseguite in contemporanea, indipendentemente dal programma principale!
Ma continuiamo a leggere per addentrarci di più in questi concetti e capire come possiamo mettere in pratica tutto questo.
Le sorgenti di interrupt dei PICMicro
L’esempio del telefono era qualcosa di molto banale, certo, ma rende chiaramente il concetto che sta alla base di un interrupt. In un picmicro c’è più di un “campanello” che ci permette di fermare momentaneamente il nostro programma in un punto, dedicarci a una determinata funzione, e quindi riprendere nel punto esatto in cui ci eravamo fermati (senza la necessità di andare a controllare di continuo questo o quel particolare evento).
Le varie sorgenti di interrupt, in particolare nel PIC16F877, sono:
Interrupt su overflow del Timer0 (indicato come T0IF o TMR0IF): il Timer0 è un contatore ad 8 bit che si autoincrementa (indipendentemente dall’esecuzione del programma principale) ogni tot di tempo. Possiamo stabilire noi ogni quanto tempo deve essere incrementato tramite appositi settaggi (che vedremo nella prossima lezione). Essendo un contatore ad 8 bit, potrà avere valori che vanno da 0 a 255. Quando è arrivato a 255 e arriva il momento dell’incremento, passa a zero sollevando un interrupt, che possiamo intercettare e sfruttare. E’ proprio questa funzione che utilizzeremo (è la funzione più sfruttata in generale) per incrementare altri contatori con i quali attivare o disattivare determinate funzioni nei tempi che vogliamo, vedremo in seguito come.
Interrupt esterno sul pin RB0 (INTF). Come potete vedere dalla designazione dei pin del 16F877, il pin N°33 è indicato come RB0/INT. Sappiamo che quando c’è una sbarra che separa più nomi, vuol dire che quel pin può avere più di una funzione. Difatti quel pin funziona sicuramente come I/O digitale, ma se lo settiamo come Ingresso e se abilitiamo la gestione dell’interrupt su RB0, ecco che funziona anche come INT: si avrà la generazione di un interrupt ogniqualvolta tale pin viene settato a livello logico alto dall’esterno. Difatti questo viene anche definito Interrupt Esterno dal momento che non è generato dalla circuiteria interna al picmicro ma da un evento esterno che pilota tale pin.
Interrupt su cambio di stato dei pin RB4-RB7 (RBIF). Si può verificare un interrupt anche al cambio di stato di uno di tali 4 pin: ovviamente se abilitiamo tale sorgente di interrupt, basta che cambi lo stato di almeno uno dei 4 per sollevare l’interrupt.
Interrupt di periferica: in questa categoria rientrano gli interrupt generati da periferiche come l’eeprom interna ( la fine delle operazioni di scrittura sulla memoria eeprom interna: se abilitata questa sorgente di interrupt, quando andremo a scrivere sull’eeprom e la scrittura sarà terminata, si verificherà un interrupt), l’overflow sul timer1, la ricezione di un byte nel buffer dicomunicazione seriale ecc. Ovviamente alcuni di questi interrupt sono disponibili unicamente sui pic che hanno a disposizione la periferica che lo genera (ad esempio l’USART, che permette la comunicazione seriale, o il modulo MSSP, che permette la comunicazione seriale I2C o SPI, non sono disponibili su tutti i pic).
Quando si verifica un interrupt, si scatenano i seguenti eventi (spiegati in maniera molto banale):
- Il programma principale viene interrotto.
- Viene salvato il valore numerico del PC (Program Counter, che è un contatore interno del PICMicro che tiene conto di quale riga di programma sta eseguendo) nello Stack (che è un registro apposito per memorizzare tale valore).
- Il PICMicro salta ad una determinata locazione di memoria (denominata Interrupt Vector) nella quale vengono appunto memorizzate le istruzioni da eseguire in caso di interrupt.
- Il PICMicro esegue le istruzioni contenute nell’interrupt vector.
- Finito di eseguire tali istruzioni, il picmicro preleva dallo Stack la riga del programma principale in cui si era fermato.
- Il programma principale riprende dal punto in cui era stato interrotto dall’interrupt.
Da ciò si capisce che le istruzioni da eseguire durante un interrupt, vengono memorizzate in una locazione di memoria apposita: in assembler dovremo difatti specificare nel nostro programma la locazione di memoria adatta (che varia in base al picmicro) in cui scrivere le istruzioni da eseguire in caso di interrupt.
In C questo non è necessario: si utilizzerà difatti una notazione particolare di funzione: basterà anteporre la parola “interrupt” davanti al nome della funzione che vogliamo eseguire (questo è valido per l’Hitec-C, altri compilatori utilizzano una notazione differente per definire le istruzioni dell’interrupt vector), sarà quindi il compilatore a posizionare tali istruzioni nella locazione di memoria adatta in base al picmicro che abbiamo scelto per lo sviluppo.
In pratica si scriverà una cosa del genere:
void interrupt NomeDellaFunzione(void) { } |
Quindi: ogni qualvolta si verificherà un interrupt, il picmicro eseguirà in automatico quella funzione, fermandosi momentaneamente nell’esecuzione del main. In tale funzione, inoltre, dovremo specificare le istruzioni da eseguire in base all’interrupt verificatosi, generalmente questo si fa eseguendo una serie di IF sul flag settato dall’interrupt. Difatti se si verifica un interrupt causato dal Timer0, in automatico il suo flag (T0IF) viene settato su VERO, e quindi possiamo capire CHI ha causato l’interrupt e agire di conseguenza. Finito di eseguire le istruzioni dovrà essere nostro compito reimpostare manualmente a FALSO tale flag, altrimenti non saremo più in grado di reintercettare l’interrupt:
void interrupt ISR(void) { if (T0IF) { // qui vanno le istruzioni da eseguire se l'interrupt è stato causato dal Timer0 T0IF=0; // devo resettare il flag altrimenti non mi sarà più possibile reintercettare tale interrupt } } |
NOTE:
Personalmente chiamo sempre le funzioni di interrupt come ISR (Interrupt Service Routine), perchè questo è il nome che comunemente viene dato alla serie di istruzioni eseguite durante un interrupt.Tutte le funzioni da eseguire dovranno necessariamente essere scritte nell’ISR: l’ISR non può difatti richiamare altre funzioni come è invece possibile fare nel main, questo perchè come detto prima, l’ISR si trova in una zona di memoria differente. In caso ci sia la necessità di eseguire funzioni esterne il trucco consiste nel definire dei flag, abilitarli nell’ISR e creare delle routine nel main che controllano tali flag: se verificano che un determinato flag (chiamato generalmente semaforo) è stato abilitato, allora lo resettano e richiamano la funzione desiderata, questa è una maniera indiretta di richiamare funzioni dall’ISR: l’utilizzo dei “semafori di stato”.
Ovviamente il fatto che abbiamo scritto una funzione apposita nell’interrupt vector non è sufficiente a gestire l’interrupt: bisogna abilitarli. Questo si fa tramite appositi flag nel registro di configurazione degli interrupt, denominato come INTCON (pag.24 del datasheet):
come vedete si sono dei bit contrassegnati come Flag e altri contrassegnati come Enable. Quelli contrassegnati come Enable sono in pratica gli “interruttori” che permettono di attivare o disattivare un determinato evento di interrupt, i Flag invece sono le “spie” che ci avvisano se si è verificato questo o quel determinato tipo di interrupt. e vengono settati (cioè impostati a 1) dal picmicro e devono essere resettati (cioè impostati a zero) da noi via software.
L’impostazione degli interrupt può essere fatta o settando in un solo colpo il registro INTCON, dandogli un valore numerico, oppure settando uno ad uno i vari bit, (vediamo più giù come), dal momento che anche i singoli bit godono di un proprio nome mnemonico. Spieghiamo il significato dei vari bit:
Bit di abilitazione (Enable Bits)
GIE (Global Interrupt Enable) = è l’ “interruttore” generale, impostato su zero disattiva tutti i tipi di interrupt
PEIE (Peripheral Interrupt Enable) = abilita/disabilita gli interrupt di periferica (eeprom, timer1, usart ecc ecc)
TMR0IE (Timer0 Interrupt Enable) = abilita/disabilita l’interrupt sull’overflow del Timer0.
INTE (INT Enable) = abilita/disabilita l’interrupt sull’ingresso contrassegnato come INT (RB0)
RBIE (RB Interrupt Enable) = abilita/disabilita l’interrupt sul cambio di stato dei pin RB4-RB7
“Spie“ che indicano l’avvenuto interrupt (Flag bits)
TMR0IF (Timer0 Interrupt Flag) = viene settato in automatico quando il Timer0 ha finito il suo conteggio ed è ripartito daccapo (overflow). Viene anche indicato anche come T0IF
INTF (INT Flag) = viene settato in automatico se il pin RB0/INT viene portato a livello logico alto dall’esterno
RBIF (RB Interrupt Flag) = viene settato in automatico se una delle porte RB4:RB7 cambia stato
Tutti i Flag devono essere resettati via software (ovvero riportati a zero) se vogliamo continuare a monitorare l’interrupt.
Come vedete tra questi flag non ci sono quelli relativi alle periferiche che si trovano invece in altri registri che poi vedremo mano a mano.
Volendo quindi abilitare l’interrupt sull’overflow del Timer0 possiamo procedere quindi in due modi: impostando in un unico colpo il registro INTCON:
// Impostazione Interrupt INTCON=0b10100000; // bit 0 -> RBIF - Flag interrupt su porte B // bit 1 -> INTF - Flag interrupt su RB0/INT // bit 2 -> T0IF - Flag interrupt su Timer0 // bit 3 -> RBIE, Interrupt su porte B disattivato // bit 4 -> INTE, Interrupt su porta RB0/INT disattivato // bit 5 -> TMR0IE, Interrupt su Timer0 attivato // bit 6 -> PEIE, Interrupt di periferica disattivato // bit 7 -> GIE, Gestione Interrupt attiva |
Stessa cosa sarebbe stato scrivere:
INTCON=0xA0; // l'ho scritto in esadecimale |
Oppure:
INTCON=160; // l'ho scritto in decimale |
Oppure ancora possiamo settare uno ad uno i singoli bit se ciò ci riesce più facile da ricordare:
TMR0IE=1; // abilito la gestione dell'interrupt sull'overflow del Timer0 GIE=1; // abilito la gestione generale degli interrupt |
Nel software andremo poi a verificare quale interrupt ha causato il salto all’interrupt vector, ovviamente quando abilitiamo unicamente una sola sorgente di interrupt, fare questa operazione non è necessario, ma per me è buona norma farlo lo stesso per abituarsi ed entrare in una certa mentalità di programmazione che aiuta ad essere ordinati.
Nella prossima lezione vedremo come si imposta il Timer0 per fare in modo che l’interrupt si verifichi nei tempi che desideriamo e come sfruttarlo per fare in modo che il PIC esegua due cose in contemporanea: faremo lampeggiare un led mentre un altro pin emette un’onda quadra che pilota un cicalino.
Un’approfondimento del funzionamento degli interrupt e una comparazione della gestione degli interrupt tra le varie fasce di pic è presente in questo articolo