Cos'è ElectroYou | Login Iscriviti

ElectroYou - la comunità dei professionisti del mondo elettrico

12
voti

Le interrupt. Utilizzo pratico con il PIERIN PIC18

Indice

Introduzione

Nel precedente articolo ho cercato di spiegare cosa sono le interrupt, in questo vorrei spiegare come si possono implementare in pratica. Per l' implementazione utilizzerò il PIERIN PIC18 spiegando nel dettaglio come viene implementata nel programma Demo. Il funzionamento del programma di Demo è semplice ed implementa un' interrupt ciclica che gestisce un solo timer (software) che conta all' indietro fino ad arrivare a zero per poi fermarsi (non diventa negativo). Questo timer software viene decrementato ogni millisecondo.
Per fare questo è necessario, indispensabile utilizzare una interrupt? La risposta è NO! Si sarebbe potuto benissimo scrivere il programma semplicemente usando il timer senza interrupt. Se però servissero diversi timer software allora si che avrebbe senso implementare una interrupt ciclica.
E' stata comunque implementata a scopo didattico per fornire un esempio di come si gestisce una interrupt, ed ora torna buona per questo articolo.

I timer software

L' idea che sta alla base dei timer software è questa: utilizzo un solo timer la cui interrupt interrompe il normale funzionamento ogni tot di tempo (in questo caso ogni ms) per decrementare più variabili numeriche che, osservate a livello di programma principale, si comportano come dei timer.
Dicevo prima che utilizzare un' interrupt ciclica per un solo timer software ha poco senso, in effetti è vero. Ma se di timer ne avessi bisogno di 2, 3 o1 0, 20 allora si che sarebbe la scelta ottimale! Con poche righe di codice e poco lavoro per il micro potrei benissimo implementare una cinquantina di timer. Se poi il programma del micro ha a che fare con lampeggi, ritardi "umani" la scelta dei timer software è quasi obbligatoria perché semplifica enormemente la vita. Pensiamo ad esempio ad un antifurto. E' pieno di timer:

  • Due timer per la zona ritardata. Quando esco di casa e devo inserire l' antifurto ho bisogno di una zona che, anche se rileva la mia presenza mi lasci del tempo per uscire (che può essere lungo visto che appena uscito di caso sono ancora nei paraggi) o per entrare e disinserire l' antifurto (tempo più breve).
  • Un timer per l' indicatore di inserzione. Quando inserisco l' antifurto devo avere una qualche segnalazione che mi dica che è inserito e che mi sta dando il tempo per uscire, oppure quando rientro che mi indichi che devo disinserire l' antifurto. Quale indicazione migliore di un LED lampeggiante? Per implementare il lampeggio serve un timer.
  • Un timer per l' allarme. Non posso far suonare l' allarme 24 ore su 24! Dopo un certo tempo deve smettere altrimenti il vicinato mi darebbe fuoco alla casa (e farebbe bene)
  • Se ho comprato un combinatore telefonico questi dovrà essere attivato da un impulso, un contatto che rimane chiuso per un certo tempo (per esempio 500ms). Anche per questo c'è bisogno di un timer.

Notare bene che tutti questi ritardi sono ritardi "umani", si va da 200 ms. giù di lì per il lampeggio del LED ad alcuni minuti per l' uscita di casa o per l' attivazione della sirena, quindi i timer possono benissimo avere una risoluzione di 10 ms, ad esempio e tutto fila liscio.

Il timer 2

L' interrupt ciclica, nel programma di demo, viene generata dal timer 2. Perché si è utilizzato proprio il timer 2 e non l' 1 o il 3? Perché il timer 2 è un timer con comparatore ed anche perché il timer 2 è il più semplice. Vediamolo nel dettaglio.

Timer2.jpg

Timer2.jpg

Esaminiamo nel dettaglio come funziona questo aggeggio

  • Il clock (in basso a sinistra) è preso direttamente dal clock del sistema (frequenza dell' oscillatore diviso 4) ed entra in un prescalere il cui rapporto di divisione può essere di 1:1, 1:4 o 1:16 e viene selezionato tramite due bit T2CKPS<1.0> del registro di controllo T2CON. Quindi nel nostro caso possiamo scegliere di fargli arrivare direttamente i 12MHz oppure 3MHz o 750KHz. Altre possibilità non ce ne sono. E' molto semplice ma anche limitato proprio dalla sua semplicità ma a noi basta ed avanza.
  • L' uscita del prescaler va ad incrementare ad ogni impulso il registro ad 8 bit del timer TMR2 che conta in avanti.
  • Il comparatore confronta continuamente il contenuto del registro del timer con il registro PR2 (in basso a destra)
  • Quando il contenuto del timer e del PR2 sono identici il comparatore genera un impulso che nello schema a blocchi è indicato come TMR2/PR2 Match (proprio sopra il comparatore) che fa due cose: azzera il timer e manda un impulso al postscaler. A dire il vero invia anche l' impulso alla periferica per il PWM ma questo, per ora, non ci interessa.
  • Il postscaler (in alto) è anche lui un contatore che genera un impulso dopo aver ricevuto un tot di impulsi dal comparatore. Il numero di impulsi da ricevere per mandarne via uno è programmabile tramite i 4 bit T2OUTPS<3.0> del registro di controllo T2CON. In pratica possiamo fare in modo che l' uscita del postscaler sia attiva dopo, chessò, 3 impulsi o 12 impulsi, insomma da 1 a 16. Impostare questi bit a 0 vuol dire che l' uscita Set TMR2IF (in alto a destra) viene attivata dopo il primo TMR2/PR2 Match.
  • L' uscita Set TMR2IF non fa altro che mettere ad 1 e lasciarlo così quello che è il flag dell' interrupt.

Come funziona l' interrupt

L' interrupt del timer 2 innanzi tutto funziona solo se è abilitata. Dopo il reset tutte le interrupt sono disabilitate e si abilitano settando opportunamente i bit di abilitazione. Nel caso del timer 2 il bit di abilitazione si trova nel registro PIE1 (Peripheral Interrupt Enable) ed è il bit TMR2IE. Qundi se vogliamo utilizzare questa interrupt dovremo quindi mettere ad 1 questo bit. Il PIC18F47J53 ha la possibilità (ma non si è obbligati a farlo) di impostare la priorità dell' interrupt. Di priorità ce ne sono solo due: alta e bassa. Nel caso del programma Demo non c' era bisogno di impostare il micro per gestire anche le priorità proprio perché di interrupt ce n'è una sola ma si è voluto farlo a scopo didattico. Quindi, oltre ad abilitare l' interrupt, dobbiamo anche impostare la priorità e questo viene fatto tramite il bit TMR2IP del registro IPR1 (Interrupt Priority Register). Settando questo bit la priorità sarà alta mentre resettandolo la priorità sarà bassa.
Analizziamo come funziona tutto l' ambaradan.

  • Il micro è li, bello paciarotto, che sta eseguendo il suo programma principale come io in questo momento sto scrivendo questo articolo.
  • Il blocco del timer mette ad 1 il bit TMR2IF. Visto che l' interrupt è abilitata questa viene generata. Nel mio caso sarebbe come se squillasse il telefono.
  • Il micro salva automaticamente il contesto e si fionda nella funzione di servizio delle interrupt (bassa o alta). Nel caso del programma Demo nella funzione di servizio delle priorità basse
  • E qui arriva il bello (o il brutto, dipende dai punti di vista) il micro deve capire chi diamine ha generato l' interrupt. Questa è una particolarità dei PIC, negli ATmega non funziona così, ogni interrupt ha la sua funzione di servizio. Il PIC no e quindi, per capire chi è stato ad interrompere il suo lieto fare deve andarsi a guardare i flag di interrupt. Ovvio che si guardano solo i flag che interessano. Nel Demo l' unico flag che interessa è il TMR2IF. Si potrebbe anche evitare di andarlo a testare ma si fa lo stesso perché se è vero che in questo programma di interrupt ce n'è una sola, in altri programmi ce ne possono essere molte di più.
  • Una volta capito che il rompiscatole è stato il timer 2 fa quello che deve fare per servire l' interrupt. Nel nostro caso se la variabile che funge da timer software ha un valore maggiore di 0 la decrementa.
  • Resetta il bit TMR2IF. Questo deve sempre essere fatto alla fine.
  • A questo punto è finita l' esecuzione della funzione di servizio. Il micro recupera automaticamente il contesto e torna a fare quello che stava facendo prima felice e contento.

L' implementazione

E siamo giunti al punto in cui andiamo a vedere nello specifico come si scrive un programma che usa l' interrupt del timer 2 analizzando il sorgente del programma Demo.

Programmazione del timer

Al reset tutte le periferiche sono inattive (Deo Gratias) e quindi bisogna in qualche modo programmarle, settare bit a destra ed a sinistra per far fare loro quello che vogliamo. La prima affermazione (le periferiche sono disattive al reset) fa però a pugni con quello che si legge nel sorgente del programma infatti la prima istruzione relativa all' inizializzazione del timer è

  // De-inizializza il timer2. Non sarebbe necessario perché il micro esce
  // allo stato di RESET ma è comunque buona pratica de-inizializzare sempre
  // le periferiche per non tralasciare nessun bit.
  timer2_deInit();

In effetti la chiamata alla funzione timer2_deInit è assolutamente inutile in questo caso. Però questo è un modo di fare che si usa con microcontrollori più potenti e complessi come i Cortex-M o bestioni del genere. E visto che al mondo non esistono solo i PIC18, oggi si lavora con un micro semplice ma un domani si lavorerà magari con un micro più complesso e potente penso che sia meglio prendere da subito la buona abitudine di fare in questo modo: scrivere e chiamare una funzione di de-inizializzazione prima di inizializzare una periferica. Per carità, non si è obbligati a farlo ma io lo suggerisco, anche solo per prendere una buona abitudine.
Andiamo avanti

  // Inizializza il timer 2 per interrupt ogni millisecondo. 
  // prescaler divide per 16
  T2CONbits.T2CKPS = 2;
  // Postscaler divide per 5
  T2CONbits.T2OUTPS = 4;
  // Imposta il valore comparatore a 150
  PR2 = 150;

vogliamo che il timer generi una interrupt ogni milllisecondo. Il nostro clock di sistema è di 12MHz (frequenza dell' oscillatore divisa per 4). Impostando il prescaler a 2 (cioè divide per 16) avremo un clock che entra dentro al registro del timer con frequenza 750KHz. Nel registro di comparazione PR2 impostiamo il valore di 150. Questo vuol dire che dopo 150 conteggi a 750KHz il comparatore genererà un impulso, cioè ogni 200 us. Per avere una interrupt ogni millisecondo non ci resta che impostare il postscaler a 5 in modo che generi una interrupt ogni 5 match ed il gioco è fatto.

Abilitazione delle interrupt

Dopo avere opportunamente inizializzato il timer, prima di farlo partire, dobbiamo istruire il micro a gestire interrupt con priorità sia alta che bassa ed abilitarle tutte.

// -------- Selezione ed abilitazione delle interrupt --------  
  // Ora che si sono inizializzate tutte le periferiche si possono abilitare
  // Oppurtunamente le interrupt
  // abilita le interrupt a bassa priorita'
  RCONbits.IPEN = 1;
  // abilta tutte le interrupt a priorità bassa
  INTCONbits.GIEL = 1;
  // Abilita tutte le interrupt in generale
  INTCONbits.GIEH = 1;

Non ci resta altro da fare che far partire il timer mettendo ad 1 il bit TMR2ON del registro T2CON.

  // Con le interrupt abilitate possiamo ora far partire il timer 2
  // Accende il timer
  T2CONbits.TMR2ON = 1;

Da questo momento in poi il timer sta funzionando e genererà continuamente una interrupt ogni millisecondo.

La funzione di servizio

"Signore e signori è il comandante che vi parla. In questo momento stiamo sorvolando la funzione di servizio delle interrupt, zona di turbolenze. Vi preghiamo di ritornare ai vostri posti ed allacciare le cinture di sicurezza. Il personale di bordo sarà a vostra disposizione. Grazie per avere scelto PIERIN-AIR per i vostri spostamenti"
In fondo tanto turbolenta non lo è, diamoci uno sguardo

//------------------------------------------------------------------------------
// Funzione di servizio delle interrupt a BASSA priorità
//------------------------------------------------------------------------------ 
#pragma interruptlow lowPriorityInterrupt
void lowPriorityInterrupt()
{
  // Verifica quale flag ha causato l' interrupt
  // Esegui la parte di codice di servizio dell' interrupt
  // Azzera il flag che ha causato l' interrupt
  // ...
  
  // Gestione dell' interrupt del timer 2
  if(PIR1bits.TMR2IF)
  {
    // gestione del timer software. Il timer software deve decrementarsi
    // fino ad arrivare a 0. Una volta arrivato a 0 resta fermo a 0.
    if (timer_delay) timer_delay--;
		
    // Resetta il flag che ha generato l' interrupt
    PIR1bits.TMR2IF = 0;
  } 
} 

La prima istruzione è una direttiva del compilatore per dire che l' interrupt di priorità bassa interruptlow sarà gestita dalla funzione lowPriorityInterrupt il cui prototipo (da scrivere prima di questa #pragma) è contenuto nel file mappa_int.h. Questo perché il PIERIN può funzionare con il suo bootloader oppure abbinato al Pickit e quindi sono necessarie alcune compilazioni condizionate contenute, appunto, nel file mappa_int.h
Subito dopo troviamo la dichiarazione della funzione di servizio della interrupt. All' interno ci sono alcuni commenti che spiegano già come fare per scrivere correttamente la funzione, cosa che poi è fatta più avanti dopo la riga di commento // Gestione dell' interrupt del timer 2

if(PIR1bits.TMR2IF)

e' l' istruzione che testa il bit TMR2IF per sapere se è stato il timer 2 a generare l' interrupt. Nel caso che sia lui allora si procede con

if (timer_delay) timer_delay--;

timer_delay è una variabile di tipo unsigned int e dichiarata come volatile.
Nota di programmazione: la classe di memorizzazione volatile obbliga il compilatore a leggere sempre il valore della variabile. Questo perché l' eventuale ottimizzatore potrebbe dare per scontato che la variabile, una volta letta, non cambi di valore.
La si è dichiarata così perché questa variabile potrebbe essere modificata da un momento all' altro e non si può sapere a priori in quale punto del programma principale arriverà la interrupt. in ogni caso l' operazione che si fa è semplice: se questa variabile è diversa da 0 la si decrementa, altrimenti la si lascia stare a 0. Otteniamo così una variabile a cui possiamo dare un valore in qualsiasi punto del programma principale sapendo che verrà decrementata ogni millisecondo fino ad arrivare a 0. Quindi se gli daremo un valore di 100 sapremo che quando il suo valore sarà 0 vuol dire che sono passati 100 ms.
La cosa interessante è che noi potremmo utilizzare quante variabili vogliamo per far fare loro il timer e quindi, mediante l' utilizzo di un solo modulo timer, noi potremmo avere un numero a piacere di timer software.
Come ultima istruzione

PIR1bits.TMR2IF = 0;

azzera il flag che ha generato l' interrupt. La funzione è terminata e quindi il micro ritornerà a fare il suo lavoro di sempre.

Conclusioni

L' interrupt sul timer è una di quelle classiche che si trovano un po' ovunque, ed è anche ottima per capire come usare le interrupt e perché. In particolare la interrupt ciclica torna utile per diverse applicazioni, un esempio è quello riportato nell' articolo che parla del PWM facile. Si può anche facilmente implementare un orologio, oppure usarla come base tempi per il pilotaggio di display in multiplexing o per la scansione di tastiere a matrice. Insomma le applicazioni sono tante.
Un ultima considerazione su quando usare le interrpt. Vedo a volte alcuni principianti che vorrebbero usare le interrupt per leggere un valore, ad esempio dall' ADC. In quel caso non serve implementare una interrupt, anzi è controproducente perché ci si va ad incasinare la vita per niente. Nel caso dell' ADC, lo si fa partire, si aspetta che abbia finito la conversione e poi si legge il risultato. Le interrupt ci stanno come la bagna caoda sul tiramisù. Certamente ci sono casi particolari in cui si usano anche con l' ADC ma non è la norma, almeno fino a quando si parla di programmi per uso amatoriale o didattico. Le interrupt si devono usare solo quando servono altrimenti se ne fa a meno.
Come ho detto all' inizio del articolo il programma di Demo si può fare benissimo senza implementare l' interrupt, basterebbe sapere, andando a testare il bit TMR2IF, che questo ha raggiunto il valore del comparatore e che quindi è trascorso il tempo di un millisecondo direttamente dal programma principale. Come ho detto all' inizio del articolo il compito del programma Demo non è solo quello di fare quello che deve fare ma anche di dare qualche un esempio di come si usano le interrupt.
Sperando di aver dato un contributo per l' apprendimento delle interrupt (perdonatemi errori ed inesattezze e magari fatemeli notare) non mi resta che augurare a tutti BUONA SPERIMENTAZIONE!

6

Commenti e note

Inserisci un commento

di ,

Complimenti, è stato molto utile. Inizialmente il codice non funzionava perchè stranamente manca l'istruzione PIE1bits.TMR2IE=1; che è chiaramente indicata nelle spiegazioni. Grazie.

Rispondi

di ,

Ottimo lavoro, TardoFreak!

Rispondi

di ,

GRANDE. Ormai è l'ennesima volta che lo scrivo, ma il concetto è questo: tu scrivi in maniera semplice e chiara, è un piacere vedere la descrizione accompagnata dal codice. Alla sera, tornato dal lavoro, uno non ha voglia di mettersi a spulciare datasheet e cercare tutorial: i tuoi articoli sono un utile e intelligente compendio serale, che lasciano la piacevole sensazione di aver acquisito qualcosa di nuovo e interessante. A presto, non vedo l'ora di scrivereti: "Ehi, grazie al tuo articolo ho finalmente implementato una funzione che fa uso dell'interrupt!". A presto! GRANDE!!!

Rispondi

di ,

interruzione -> interruption -> interrupt ;)

Rispondi

di ,

"Le interrupt" o "Gli interrutp"? Me lo sono sempre chiesto.... BTW: Bravo. Ciao.

Rispondi

di ,

Ottimo come sempre TardoFreak!
Grazie :)

Rispondi

Inserisci un commento

Per inserire commenti è necessario iscriversi ad ElectroYou. Se sei già iscritto, effettua il login.