Corso programmazione PICMicro in C – Lezione 5 – Timer0 e Prescaler: come si impostano per generare l’interrupt nei tempi che vogliamo, applicazione pratica
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.
Una periferica sempre presente in tutti i PICMicro è il Timer0. Il Timer0 è un semplice contatore ad 8 bit, il suo conteggio viene incrementato automaticamente di 1 (essendo ad 8 bit va da 0 a 255) ad ogni ciclo di istruzioni.
Il ciclo di istruzioni in un PICMicro è pari alla frequenza di clock divisa per 4: ciò equivale a dire che una singola istruzione (parliamo però di istruzioni assembler=linguaggio macchina e non di istruzioni C!) viene eseguita in 4 cicli di clock: indicando con Fosc la frequenza di clock (la frequenza del quarzo o dell’oscillatore in genere), la frequenza di un ciclo di istruzioni è quindi pari a Fosc/4.
In realtà, anche se la maggior parte delle istruzioni rispetta questa regola, ve ne sono alcune che richiedono più di 4 cicli di clock per essere eseguite, ma per ora soprassediamo su questo argomento. Quello che ci interessa sapere in questa lezione è che il Timer0 si incrementa di 1 unità ogni 4 colpi di clock, ovvero con una frequenza di Fosc/4: se stiamo utilizzando un quarzo da 20Mhz, la frequenza di esecuzione delle istruzioni è pari a 20/4 = 5MHz, sappiamo che la frequenza è l’inverso del tempo (periodo), per cui ogni istruzione sarà eseguita nel tempo di 1/5 = 0,2 μS.
Fate molta attenzione alle unità di misura quando eseguite questi calcoli: se stiamo utilizzando una frequenza espressa in MHz, andandone a fare il reciproco per conoscere il tempo, il risultato sarà espresso in microsecondi (μS = secondi • 10-6), se facciamo il reciproco di una frequenza espressa in Hz, il risultato è in secondi.
Quindi, ricapitolando: utilizzando un quarzo da 20Mhz, ogni istruzione verrà eseguita nel tempo di 0,2 μS, per cui anche il Timer0 incrementerà di una unità ogni 0,2 μS. Bene.
Abbiamo accennato nella lezione precedente della possibilità di intercettare un interrupt sull’overflow del Timer0: in pratica quando il Timer0 va in overflow (cioè passa dal valore 255 al valore zero), è possibile fargli generare, e quindi possiamo intercettare, un interrupt: un “campanello” che ci permette di fermare momentaneamente l’esecuzione del programma principale ed eseguire altre funzioni apposite.
Utilizzando un quarzo da 20Mhz, abbiamo che il Timer0 va in overflow dopo 256*0,2μS = 51,2μS (ho utilizzato il valore 256 e non 255 perchè devo conteggiare anche lo zero). Tale tempo, per la stragrande maggioranza delle applicazioni, è troppo basso, generalmente abbiamo bisogno di tempi più “palpabili” per poter lavorare in tranquillità, o per ottenere temporizzazioni ben definite in base a ciò che vogliamo realizzare. Per tale motivo i PICMicro hanno a bordo un’utile dispositivo chiamato Prescaler.
Indice dei contenuti
Il Prescaler
Il Prescaler altro non è che un divisore di frequenza, ci permette cioè di dividere la frequenza (la velocità) del nostro ciclo di istruzioni (Fosc/4) in valori più piccoli, facendoci ottenere tempi di esecuzione più alti e quindi più facilmente gestibili o in ogni caso impostabili sui valori che desideriamo. Ovviamente il prescaler non influisce sulla velocità di esecuzione di tutte le istruzioni, (sarebbe assurdo!): le istruzioni continueranno ad essere eseguite ad una velocità di Fosc/4: esso difatti influisce unicamente sull’incremento del Timer0 (cioè non è completamente vero, difatti il prescaler può essere assegnato o al Timer0 o al Watchdog Timer, -o l’uno o l’altro-, di questo però non ci occuperemo: in questa sede ci occuperemo soltanto del prescaler assegnato al Timer0).
I valori su cui possiamo impostare il prescaler (ovvero il divisore) sono : 2, 4, 8, 16, 32, 64, 128, 256. Come vedete il valore di 1 non è presente: in questo caso basta semplicemente non assegnare il prescaler al Timer0.
Otteniamo tempi precisi con il Timer0 e il Prescaler: formule
Facciamo un esempio pratico: stiamo utilizzando un quarzo da 20Mhz, abbiamo assegnato il Prescaler al Timer0 (vediamo dopo come si fa) e abbiamo impostato il prescaler a 32. In tali condizioni, la frequenza con cui il Timer0 si incrementa è pari a:
(ricordo ancora che con Fosc indico la frequenza del Quarzo, con PS indico invece il valore del PreScaler)
ovvero: (20/4)/32 = 0,156Mhz
Ma noi vogliamo ragionare in termini di tempo, perchè così ci risulta più facile lavorare, quindi prendiamo la formula precedente e facciamone il reciproco per ottenere il valore di tempo:
ovvero: (4/20)*32 = 6,4 μS
Quindi in tali condizioni, il nostro Timer0 incrementerà di una unità ogni 6,4 μS, andrà quindi in overflow dopo (6,4*256) = 1638,4 μS ovvero 1,63 mS … Come vedete il tempo è già aumentato.
Supponiamo adesso di voler ottenere un tempo ben preciso (che ci può servire per generare una ben determinata frequenza oppure semplicemente perchè più facile per fare i conti ecc ecc…).
Diciamo che vogliamo ottenere un interrupt ogni 1 millisecondi, ovvero ogni 1000μS. In questi casi si ricorre ad un trucchetto estremamente semplice: basta semplicemente non far partire il Timer0 dal valore 0: impostiamo noi il valore da cui partire… Come? Semplice:
Abbiamo appena detto che con queste impostazioni, il Timer0 incrementa di una unità ogni 6,4 μS (ovvero il suo ciclo di istruzioni, grazie al prescaler, adesso è di 6,4 μS/ciclo), ciò significa che un tempo di 1000 μS lo raggiunge dopo:
1000/6,4 = 156,25 cicli
teniamo però conto del valore intero: 156 (i cicli sono finiti, non possiamo certo usare il decimale). Bene, se con tali impostazioni, dopo 156 cicli otteniamo un millisecondo, è facile far generare l’interrupt giusto ad un millisecondo: basterà far partire il Timer0 da: 256-156 = 100
Partendo da 100, il Timer0 andrà in overflow dopo 156 cicli, che ci darà appunto il tempo di 1 millisecondo! Una volta generato l’interrupt reimposteremo di nuovo il Timer0 a 100, e così ogni volta, ad ogni interrupt. Come avete visto è molto semplice fare i calcoli. In realtà, avendo tralasciato (per forza!) dei decimali, il tempo preciso non sarà di 1000 μS, calcoliamo il tempo reale che si ottiene :
sappiamo che:
quindi:
quindi, nel nostro esempio: Tempo = 156 * 6,4 = 998,4 μS
come vedete c’è un piccolo scarto rispetto ai 1000μS (-1,6μS) , ma generalmente tale errore ci sta bene, possiamo comunque correggerlo immettendo altre piccole temporizzazioni per riportarlo al valore corretto (ma in genere questo viene fatto soltanto in applicazioni di precisione).
Ricapitolando, per conoscere il valore su cui impostare il Timer0 possiamo quindi utilizzare la formula:
“Semplificando” un po’:
Dove:
PreloadTMR0 = valore da dare al Timer0 (valore intero: privo dei decimali!)
Td = Tempo desiderato
Fosc = Frequenza del quarzo
PS=Valore del prescaler (uno dei valori: 2, 4, 8, 16, 32, 64, 128, 256)
Fate attenzione alle unità di misura: se esprimiamo la frequenza del quarzo in Mhz, dovremo esprimere il tempo in microsecondi, se esprimiamo la frequenza in Hz, il tempo andrà espresso in secondi. Il tempo reale invece sarà dato dalla formula che abbiamo già visto sopra:
Ricordo che c’è questa differenza tra tempo realmente ottenuto e tempo impostato per il motivo che sul valore di preload del Timer0 dobbiamo eliminare i decimali, andando quindi a fare il calcolo inverso tralasciando i decimali, ecco che si ha questo scostamento inevitabile.
C’è inoltre da fare un’ulteriore precisazione quando si effettuano queste operazioni: quando si va a scrivere un valore nel Timer0 (quando cioè lo impostiamo noi manualmente su un determinato valore), vengono persi 2 cicli di istruzioni, per cui in effetti, al posto di scrivere 100, dovremmo in realtà scrivere 102 (dal momento che 2 cicli vengono già persi nel momento in cui scriviamo, per cui dobbiamo far partire il Timer0 “più avanti”), ciò è chiaramente specificato nel PICMicro Mid-Range MCU Family al paragrafo 11.7 “Design tips” (pagina 177 con Adobe Reader). Ricordo che tale documento è scaricabile nella lezione 2.
Software per il calcolo del valore da assegnare al Prescaler e al Timer0
Per aiutarvi ad effettuare questi calcoli, ho realizzato un piccolo software: PICTimer, scaricabile gratuitamente dalla sezione software. In questo modo vi basterà inserire unicamente la frequenza del quarzo che intendete utilizzare e il tempo che desiderate per generare l’interrupt, il software effettuerà le varie combinazioni con i valori di Prescaler per ottenere il valore che più si avvicina mostrando anche il tempo realmente ottenuto e la percentuale di scarto. Il Software genera inoltre il codice (in Hitec-C) necessario per impostare Timer0 e Prescaler. Nel manuale incluso col software vengono date tutte le spiegazioni di funzionamento.
Impostare sul PICMicro il Prescaler e il Timer0
Vediamo adesso come vanno impostati Timer0 e Prescaler, andiamo a pag. 171 del Midrange MCU Family Reference Manual (ricordo: scaricabile dalla secona lezione), oppure, che è lo stesso, a pag.56 del Datasheet del PIC16F877 (ogni picmicro ha comunque nel datasheet tale pagina, dal momento che il Timer0 con il prescaler sono implementati in tutti i pic) e diamo uno sguardo alla tabella riportata in questa pagina:
Il funzionamento di Timer0 e l’impostazione del Prescaler vengono effettuati dal registro denominato OPTION_REG (nome mnemonico quando programmate in C : OPTION). Cominciamo la spiegazione dai bit meno significativi:
I Bits 0,1 e 2 (chiamati anche individualmente PS0 PS1 e PS2) servono per impostare il valore di divisione della frequenza (il valore del prescaler), come vediamo dalla tabella, impostando ad esempio questi 3 bit sul valore 101, otterremo un valore di divisione della frequenza del ciclo istruzioni pari a 64 (ovvero moltiplicheremo per 64 il relativo tempo). In questa tabella vi sono due colonne: quella sinistra è relativa al valore di divisione che si ottiene se assegniamo il prescaler al Timer0, quella destra rappresenta il valore di divisione che si ottiene se invece il prescaler è assegnato al Watchdog timer (non ci interessa).
Bit3 (PSA) : bit di assegnazione del Prescaler, se impostiamo tale bit a 0, il prescaler viene assegnato al Timer0.
Bits 4 e 5 : qui c’è da fare una piccola considerazione. Abbiamo difatti detto che l’incremento del Timer0 è legato al quarzo che fornisce il clock al pic, in effetti possiamo anche fare in modo che l’incremento del Timer0 sia legato ad una sorgente di clock esterna (questo può essere utile quando dobbiamo ad esempio sviluppare un’applicazione di precisione e ottenere quindi un tempo estremamente preciso che non abbiamo modo di realizzare con i metodi fin qui visti). Se vogliamo che il Timer0 si incrementi tramite una sorgente esterna di clock, dobbiamo impostare il bit 5 ad 1 (in questo caso il clock andrà applicato al pin contrassegnato sul PICMicro con la sigla T0CKI), se invece vogliamo che il Timer0 si incrementi tramite la circuiteria interna del PIC (e quindi tramite il quarzo), allora imposteremo tale bit a zero. Il bit 4 stabilisce invece come deve avvenire l’incremento del Timer0 unicamente quando si sfrutta una sorgente esterna: se impostiamo ad uno, l’incremento si avrà nel momento in cui il segnale effettua il passaggio da stato logico alto a stato logico basso.
Bit 6 : Stabilisce il modo in cui deve avvenire l’interrupt sul pin RB0/INT quando è stata selezionata tale sorgente di interrupt. Se abilitiamo l’interrupt sul pin RB0/INT e impostiamo questo bit (il Bit 6 del registro OPTION – INTEDG) sul valore 1, avremo che l’interrupt sarà generato sul fronte di salita del segnale applicato ad RB0/INT, se impostiamo tale bit a zero, l’interrupt scatterà sul fronte di discesa del segnale. Ovviamente tale impostazione (come anche la successiva) non c’entra assolutamente nulla col Timer0, ma fa comunque parte del registro delle opzioni.
Bit 7 : abilita (0) oppure disabilita (1) le resistenze di pull-up sulle porte B. Non abbiamo ancora parlato di cosa sia una resistenza di pull-up, diciamo per ora che le resistenze di pull-up servono a fare in modo che un pin configurato come ingresso non rimanga “appeso” ovvero non rimanga senza segnali applicati ad esso (condizione che può portare a malfunzionamenti). Generalmente sui pin configurati come ingresso andremo sempre ad inserire una resistenza di pull-up che mantiene il pin a valore logico alto (up). Le porte B dei PIC hanno questa caratteristica: possiamo infatti evitare di mettere tali resistenze all’esterno, si può sfruttare la circuiteria interna del PIC ponendo questo bit a zero.
Ricapitoliamo facendo un esempio: vogliamo assegnare il prescaler al Timer0 e impostare una divisione di 32, in C andremo a scrivere (tenete sempre conto che il bit 0 è quello più a destra):
1 2 3 4 5 6 7 8 9 10 | OPTION=0b10000100; // Impostazione Registro Opzioni // bit 0 -> PS0 Prescaler Rate Select bit 0 // bit 1 -> PS1 Prescaler Rate Select bit 1 // bit 2 -> PS2 Prescaler Rate Select bit 2 // bit 3 -> PSA Prescaler assegnato al Timer0 (1=al Watchdog Timer) // bit 4 -> T0SE Incremento Timer0 su transizione: 0=low->high 1=high->low // bit 5 -> T0CS Clock Timer0 da clock interno (1=su transizione pin T0CKI) // bit 6 -> INTEDG Interrupt (INT) edge (1=salita 0=discesa) // bit 7 -> RBPU Resistenze pull-up su porta B (1=off 0=on) |
Il valore di partenza del Timer0 si imposta semplicemente così:
1 | TMR0=100; |
(ovviamente una volta andato in overflow, ripartirà da zero, a meno che non intercettiamo l’interrupt e reimpostiamo il Timer0 sul valore voluto).
Esempio pratico di un semplice sistema multitasking
Bene, passiamo ora a fare un esempio pratico di come sfruttare tutto questo: realizzeremo un circuito in cui un led lampeggia mentre un cicalino emette un suono ad una frequenza fissa. Il circuito che andremo a realizzare è praticamente uguale a quello della lezione 3 con la sola differenza che andremo a mettere un cicalino (con relativa resistenza in serie) sulla porta RE1. Se utilizzate la scheda Freedom di Mauro Laurenti, c’è già la predisposizione sullo stampato, altrimenti realizzando il circuito in altro modo, potete anche connettere il cicalino alla porta che più vi aggrada: modificare il codice sorgente non dovrebbe essere un problema.
Lo schema è pertanto il seguente:
Come vedete, in più rispetto al circuito della lezione 3, abbiamo messo una resistenza da 220Ω in serie ad un cicalino (sul terminale positivo), l’altro terminale del cicalino va a massa.
Nota sul cicalino: il cicalino utilizzato in questo esperimento non è un cicalino autooscillante, ma un semplice cicalino miniaturizzato di quelli che si trovano normalmente anche nei vecchi modem ISA o PCI per segnalare il tono sulla linea telefonica: tale tipo di cicalino non suona se gli si applica una tensione continua, per farlo suonare è necessario applicarvi un segnale periodico (si comporta in pratica come un altoparlante miniaturizzato). Se avessimo utilizzato un cicalino autooscillante (che tra l’altro costa anche molto di più, ed è più difficile da trovare), sarebbe stato troppo semplice. Dovremo difatti generare un’onda quadra per fargli emettere un tono. Questi sono, ad esempio, alcuni cicalini del tipo corretto che ho smontato da vecchi modem guasti per pc:
Utilizzando tale tipo di cicalino, la resistenza in serie può anche essere omessa. Ogni cicalino ha una propria frequenza di risonanza (che si aggira normalmente intorno ai 2÷4KHz) alla quale il suono è più intenso: i cicalini difatti sono costituiti da materiale piezoelettrico, simile a quello utilizzato per la costruzione dei quarzi.
(un grazie a Mauro Laurenti)
Passiamo quindi al listato. Questa volta andremo a scrivere il programma mettendo le impostazioni in un file separato (file header) e le istruzioni nel file principale (.c), in questo modo ci abituiamo a scrivere codice riutilizzabile. Passiamo quindi a vedere il file delle impostazioni, che per logica chiameremo settings.h :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | #define TEMPOLED 250 #define LED RD0 #define CICALINO RE1 unsigned char TimerLed=0; //Char è un tipo di dato a 8 bit, quindi può arrivare a contenere valori fino a 255, a noi serve massimo 250, quindi va bene void settings(void) { // Tutte le porte come output TRISA=0; TRISB=0; TRISC=0; TRISD=0; TRISE=0; // Impostazione del registro OPTION (pag.55 del datasheet) OPTION=0b11000100; // bit 0 -> Prescaler Rate Select bit 0 // bit 1 -> Prescaler Rate Select bit 0 // bit 2 -> Prescaler Rate Select bit 0 (1:32) // bit 3 -> Prescaler assegnato al Timer0 // bit 4 -> Non importa // bit 5 -> Clock per Timer0 derivato da ciclo di clock interno // bit 6 -> Non importa // bit 7 -> Resistenze di pull-up su porta B disattivate // 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 TMR0=100; // Imposto Timer0 a 100 } |
Nelle righe 1,2 e 3 stiamo soltanto utilizzando dei simboli: quando il compilatore nel programma troverà la parola “TEMPOLED” la sostituirà con il numero 250 e così via. In particolare: TEMPOLED verrà utilizzato per invertire lo stato del led ogni 250 millisecondi, le altre due definizioni ci servono ovviamente per identificare il led (collegato sulla porta RD0) e il cicalino (collegato sulla porta RD1).
Alla riga 5 troviamo qualcosa di molto interessante:
5 | unsigned char TimerLed=0; |
Stiamo in pratica definendo una variabile, alla quale abbiamo dato il nome TimerLed (ricordatevi di rispettare maiuscole e minuscole), abbiamo inoltre detto che questa variabile non deve avere segno (unsigned) ed è una variabile di tipo Char (ovvero ad 8 bit, quindi tale variabile può assumere tutti i valori interi compresi tra 0 e 255), abbiamo inoltre inizializzato tale variabile al valore 0.
Quando vengono dichiarate le variabili vanno sempre messi in sequenza:
unsigned oppure signed / tipo di dato / nome variabile / eventuale inizializzazione / ;
I tipi di dato sono stati elencati nella lezione 3 (tabella 3.1) e sono comunque disponibili nel manuale del compilatore.
Abbiamo quindi una funzione:
7 8 | void settings(void) { |
In questa funzione (che verrà richiamata in seguito nel programma principale), abbiamo inserito tutte le opzioni di configurazione. Come vedete abbiamo impostato il registro OPTION e il registro INTCON in maniera tale da attivare l’interrupt sul Timer0 ed impostare il prescaler sul valore 1:32, in questo modo, facendo partire il Timer0 dal valore 100, otterremo (con il quarzo da 20MHz) un tempo di interrupt pari a 1 millisecondo per quanto detto in precedenza.
Passiamo quindi al programma principale, contenuto nel file main.c :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | #define XTAL_FREQ 20MHZ // questo è utilizzato dalle routine di ritardo contenute in Delay.C #include <pic.h> // contiene i nomi mnemonici di registri e porte // Fuses di configurazione __CONFIG (HS & WDTDIS & PWRTEN & BORDIS & LVPDIS & DUNPROT & WRTEN & DEBUGDIS & UNPROTECT); #include "delay.c" // routine per ritardi #include "settings.h" // settaggi del picmicro // funzione principale, eseguita all'avvio del picmicro void main(void) { settings(); // eseguo la funzione settings contenuta nel file header settings.h, così imposto le porte e i registri while(1) // eseguo un ciclo finito { /* L'unica cosa che eseguo durante questo ciclo infinito, è l'inversione dello stato del cicalino ogni 200microsecondi, in maniera tale da generare un'onda quadra di 2,5KHz che, applicata al cicalino, appunto, mi permette di farlo suonare facendogli emettere una nota a tale frequenza */ DelayUs(200); CICALINO=1; DelayUs(200); CICALINO=0; }// Fine ciclo continuo } // Fine main /* Questa routine, avendo l'attributo "interrupt" prima del nome della routine stessa, viene chiamata in automatico ogni qualvolta si verifica un interrupt. Essendo le sorgenti di interrupt di vari tipi, in questa routine dobbiamo capire quale elemento ha generato l'interrupt. Con le impostazioni utilizzate, Timer0 genererà un interrupt ogni millisecondo. */ void interrupt ISR (void) { if (T0IF) // L'interrupt è stato causato da un overflow del timer0 ? { TMR0 = 100; // Reimposto Timer0 TimerLed++; // Incremento il Timer per il lampeggio del led if (TimerLed >= TEMPOLED) // Se il tempo è passato { LED=LED^1; // Inverto lo stato del led per farlo lampeggiare TimerLed=0; // Ricarico il timer del led per ricominciare daccapo } T0IF=0; // Resetto il flag interrupt su timer 0, } // fine che interrupt verificatosi su timer0 } // fine interrupt service routine |
Tralasciando le righe da 1 a 7 che dovrebbero essere chiare, vediamo che nella riga 8 richiamiamo il nostro file di configurazione, parte quindi il programma principale e subito, alla riga 13, viene richiamata la funzione definita in settings.h :
13 | settings(); |
In pratica verranno eseguite tutte le istruzioni contenute nella funzione settings: saranno impostati i registri.
Notiamo in questo listato un altro modo di includere i commenti: fino ad ora includevamo i commenti mettendo un doppio slash // : questo tipo di notazione permette di mettere il commento su una sola riga, possiamo invece includere commenti multiriga mettendo /* prima del commento e */ alla fine.
Alla riga 15 viene quindi avviato il ciclo infinito, come vedete, tale ciclo non fa altro che eseguire di continuo le istruzioni:
23 24 25 26 | DelayUs(200); CICALINO=1; DelayUs(200); CICALINO=0; |
In pratica il cicalino rimane acceso (CICALINO=1) per 200 microsecondi, e spento (CICALINO=0) per altri 200 microsecondi, per poi ricominciare daccapo (dal momento che si trova nel ciclo while(1) ). In pratica, se avessimo a disposizione un oscilloscopio, potremmo vedere che sul pin al quale è collegato il cicalino (ricordo: RE1 in questo esempio) fuoriesce un’onda quadra alla frequenza di 2,5KHz (200μS ON e 200μS OFF = periodo di 400μS = 2500 Hertz, che come nota musicale corrisponde all’incirca ad un RE#7 – corrispondenza note/frequenze presa da: febat.com). Quindi come vedete, generare un’onda quadra ed ottenere una nota da applicare ad un altoparlante è abbastanza semplice. Ma se nel contempo che viene suonata la nota, volessimo fare altro?
Qui entra in gioco l’interrupt del Timer0: con le impostazioni che abbiamo dato in settings, abbiamo fatto in modo che ogni millisecondo, il programma main si interrompa momentaneamente ed esegua le istruzioni contenute nell’apposita funzione:
38 39 | void interrupt ISR (void) { |
In questa routine, richiamata al verificarsi di un interrupt, dovremo capire da chi è stata causata l’interruzione (come spiegato nella precedente lezione) , per tale motivo mettiamo la seguente condizione:
40 41 | if (T0IF) // L'interrupt è stato causato da un overflow del timer0 ? { |
(In realtà non ce ne sarebbe neanche bisogno dal momento che l’unica sorgente di interrupt che abbiamo abilitato è il Timer0, ma abituiamoci ad essere ordinati). Verificato quindi che l’interruzione è stata causata da TMR0, la prima cosa che facciamo è quella di reimpostare il Timer0 al valore di partenza:
42 | TMR0 = 100; // Reimposto Timer0 |
Incrementiamo quindi la nostra variabile di conteggio di una unità (l’operatore ++ viene chiamato incremento unario):
43 | TimerLed++; |
Passiamo quindi a controllare che il valore di questo nostro contatore TimerLed abbia superato il valore di 250 (ricordate? in settings abbiamo definito TEMPOLED come uguale a 250):
44 45 | if (TimerLed >= TEMPOLED) // Se il tempo è passato { |
Se tale condizione viene verificata, invertiamo lo stato del led ed azzeriamo tale variabile:
46 47 48 | LED=LED^1; // Inverto lo stato del led per farlo lampeggiare TimerLed=0; // Ricarico il timer del led per ricominciare daccapo } |
L’effetto che si otterrà è in pratica un led che lampeggia con un periodo di mezzo secondo (250ms ON e 250ms OFF). Passiamo quindi ad azzerare il flag di interrupt sul Timer0, altrimenti non potremo più intercettarlo:
49 50 51 | T0IF=0; // Resetto il flag interrupt su timer 0, } // fine che interrupt verificatosi su timer0 } // fine interrupt service routine |
L’interrupt service routine è quindi terminata e il controllo viene restituito al programma principale. Dal momento che tale sequenza di istruzioni (verifica delle condizioni, incremento del timer, inversione dello stato del led ecc), prende pochi microsecondi, l’effetto sarà quello di vedere il led lampeggiare mentre il cicalino suona: due task eseguiti contemporaneamente (in realtà eseguiti in tempi separati, ma molto, molto rapidamente).
In sostanza: abbiamo definito una variabile (TimerLed) che tiene tempo di un conteggio: essendo l’interrupt service routine richiamata ogni millisecondo (come da nostre impostazioni), tale variabile sarà incrementata di una unità ogni millisecondo: ci siamo in pratica creati un contatore che obbedisce alle nostre regole! Sempre nell’ISR controlliamo il valore di questo nostro contatore per fargli eseguire una funzione non appena giunge ad un certo valore. Ovviamente sarà nostra cura reimpostare questo nostro contatore sul valore zero e reimpostare il Timer0 dal punto che abbiamo scelto per la partenza. Sfruttando quindi il Timer0 in abbinamento al prescaler, possiamo in pratica crearci tutti i contatori che vogliamo definendo delle variabile apposite da utilizzare come contatori.
Il modo per compilare il programma e caricarlo sul picmicro l’avete appreso nella lezione 3, per cui non dovreste avere difficoltà, anche in questo programma utilizzeremo le routine delay (incluse nel download). Per compilare il programma ricordo che nel progetto dovrete includere unicamente il file main.c
Downloads
Nota: i programmi di esempio sono stati sviluppati con una versione precedente dell’Hitec-C Compiler, per cui compilati con la nuova versione, restituiscono errori. Fate riferimento a questo articolo per maggiori informazioni su come adattare i vecchi programmi. Consiglio spassionato se volete davvero imparare a programmare: non utilizzate l’include legacy headers, ma imparate a cambiare i nomi mnemonici.
File di supporto alla quinta lezione del corso di programmazione picmicro in C (1462 download)
Il codice aggiornato per il nuovo Corso di Programmazione PICmicro in C con MPLAB X e compilatore XC8 si trova in quest’altro articolo.