Corso programmazione PICMicro in C – Lezione 8 – Pilotare i display a led a 7 segmenti in modalita´ multiplex su interrupt del Timer0. Realizziamo un contatore up/down
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.
In questo articolo parleremo del pilotaggio dei display a led a 7 segmenti, molto diffusi, di un certo fascino e abbastanza semplici da pilotare. Realizzeremo un contatore up/down con due pulsanti in modalità multiplex e utilizzante l’interrupt per l’aggiornamento delle cifre! Quindi il funzionamento del programma sarà non certo semplice da intuire al primo colpo ma vi assicuro che, realizzato in questo modo, ci permette davvero di sfruttare appieno il concetto di sistema operativo in real-time.
I più bravi possono sfruttare circuito e schema presentati in questa lezione anche per realizzare un timer eliminando i pulsanti oppure un elimina-code, o ancora un contapezzi in cui al posto dei pulsanti ci saranno dei sensori ecc. Il limite è dato solo dalla vostra fantasia!
Incominciamo prima con una breve introduzione ai display a 7 segmenti (dei quali qualcosa abbiamo già letto in un nostro precedente articolo dal titolo: La codifica BCD e i display a 7 segmenti).
I display a led più semplici e diffusi sono costituiti da 8 led: 7 che compongono la cifra e uno per il punto (generalmente usato come separatore decimale e posizionato in basso a destra), tutti i led hanno gli anodi (display ad anodo comune, es: TDSR5150, HDSP5501) oppure i catodi (display a catodo comune, es: TDSR5160, FND500, HDSP5503 ) collegati insieme.
Esistono display doppi (es.: il MAN6440, HDSP5523 -catodo comune- o MAN6410, HDSP5521 -anodo comune-), tripli (es.: BDD3301xxx) e quadrupli (es.:BDD2481xxx) che presentano più di una cifra in un unico corpo, display con doppio punto decimale (in basso a destra e in basso a sinistra), e display a 14 segmenti (che possono formare anche lettere oltre che i numeri), oppure ancora display esadecimali (come il TIL302 o il TIL311) che possono mostrare le lettere A-B-C-D-E-F oltre alle 10 cifre e ancora display che possono mostrare solo il numero uno con il simbolo + o – davanti (HDSPH5507 -anodo comune-, HDSP5508 -catodo comune-).
Nei display esadecimali o a 14 segmenti non fuoriescono i pin relativi ai tutti i singoli segmenti perchè sarebbero davvero troppi e molti hanno una logica integrata (ad esempio il TIL311) per ridurre il numero di pin necessari al controllo.
Pilotare un singolo display è piuttosto semplice, basta difatti far accendere i segmenti giusti per ottenere il numero che vogliamo. L’immagine seguente mostra come vengono comunemente indicati i segmenti di un display a led:
DP sta per Decimal Point. Supponendo di disporre di un display a catodo comune, collegheremo il catodo a massa e per comporre, ad esempio il numero 1, faremo arrivare la tensione positiva (tramite resistenze) agli anodi dei segmenti B e C: si tratta soltanto di accendere dei led, fin qui nulla di complicato.
La cosa in realtà si complica quando dobbiamo pilotare più di un display per comporre cifre di una certa grandezza. Quando dobbiamo pilotare un singolo display con un microcontrollore, supponendo di voler tralasciare il punto, abbiamo bisogno di 7 linee di comunicazione: una per ogni segmento, le linee di comunicazione si riducono a 4 se utilizziamo la codifica BCD ed un apposito circuito integrato.
Ma… Se dobbiamo pilotare 2 display? Avremmo bisogno in teoria di almeno 14 linee di comunicazione: 7 per ogni display (consideriamo sempre il caso di non voler usare il punto), oppure 8 se usiamo la codifica BCD. La cosa diventa sempre più complicata quando aumenta il numero di display.
Nella pratica, in realtà, non si agisce in questo modo ma si utilizza una tecnica chiamata Multiplexing. Tale tecnica è concettualmente molto semplice e si basa sul fenomeno della persistenza della visione: si accende un unico display per volta, ad intervalli di tempo talmente brevi da non poter essere percepiti dall’occhio umano: accendiamo prima un display, lo lasciamo acceso per pochi millisecondi, lo speghiamo e accendiamo il successivo e così via. Ma come si realizza tutto questo?
E’ molto semplice: oltre alle normali 7 linee di comunicazione (8 se vogliamo usare anche il puntino, tralasciamo in questa sede il pilotaggio con un BCD converter), avremo bisogno di una linea aggiuntiva per ogni display per pilotare il comune (l’anodo o il catodo a seconda del tipo di display che abbiamo scelto).
Ve lo spiego con un esempio: supponiamo di avere 3 display a catodo comune, di non voler usare il punto e di non voler usare un ulteriore integrato per effettuare la codifica BCD. Ci sarà bisogno quindi di 7 linee per i segmenti e ulteriori 3 linee per il pilotaggio dei catodi dei 3 display: 10 linee di output in totale. Mantenendo i catodi separati, collegheremo insieme tutti gli anodi degli stessi segmenti: collegheremo insieme i 3 pin dei 3 display che pilotano il segmento A, e così per tutti gli altri pin. Supponiamo quindi di voler mandare sui display il numero 123: nella prima fase, volendo iniziare da sinistra (possiamo pure iniziare da destra, è indifferente), dovremo prima accendere il numero 1 sul primo display: manderemo tensione ai segmenti B e C: la tensione arriverà su tutti e 3 i display perchè i segmenti uguali sono tutti collegati tra loro, ma attiveremo soltanto il catodo del primo display! In questo modo il numero 1 verrà visualizzato solo sul primo display, lo lasceremo acceso per un po’, dopodichè toglieremo la massa dal catodo del primo display (per fare questo si utilizza un transistor), predisporremo le linee per comporre il numero 2 e quindi manderemo la massa al catodo del secondo display e così via. I display si accenderanno e spegneranno così velocemente che l’occhio non sarà in grado di accorgersene e i numeri ci appariranno fissi.
Nota:
I display aventi più cifre in un unico corpo, come i BDD3301 e i BDD2481 (giusto per fare un esempio) hanno spesso già i segmenti uguali collegati insieme sia per facilitare il cablaggio, sia per rendere il packaging del componente meno “affollato”. I display BDD2481, poi, sono ottimi per realizzare, ad esempio, un orologio avendo 4 cifre separate dai due punti centrali.
In questa lezione utilizzeremo due display a catodo comune per realizzare un semplice contatore up/down: avremo un tasto che permette di incrementare il numero visualizzato e un tasto per decrementarlo.
Nella lezione successiva sfrutteremo lo stesso circuito con ulteriori due pulsanti aggiuntivi per memorizzare il numero nell’eeprom interna e quindi richiamarlo.
Utilizzando dei display a catodo comune, i segmenti si accenderanno con una tensione positiva mentre al comune viene mandata la massa utilizzando un transistor di tipo NPN. Di display a catodo comune ne esistono un’infinità, io, come potete vedere nella foto introduttiva, ho utilizzato un display doppio del tipo MAN6440, del quale riporto qui la piedinatura:
Con K1 e K2 sono indicati i catodi rispettivamente della prima e della seconda cifra, ogni segmento è seguito dal numero 1 o 2 per indicare che appartiene alla prima o alla seconda cifra. Il MAN6440, così come moltri altri tipi di display viene venduto in varie colorazioni, quello in mio possesso ha i segmenti di colore verde. Altri display che potete usare per questa lezione sono, ad esempio, i “vecchi” FND500 oppure i TDSR5160, entrambi hanno la seguente piedinatura (che è standard per numerosi modelli di display, verificate sempre e comunque il datasheet):
Di questi 3 display (e di altri) potete scaricarne il datasheet in fondo all’articolo. Comunque un display vale l’altro, ce ne sono tanti, in questa lezione, comunque, lo ripeto, l’esempio verrà fatto con display a catodo comune.
Nella foto seguente, da sinistra verso destra, sono illustrati un MAN6440, un TDSR5160 e un FND500:
Diamo quindi uno sguardo al circuito che andremo a realizzare:
Come vedete nulla di complicato, abbiamo i due pulsanti btn_up e btn_down, collegati nella solita configurazione e che avranno la funzione di incrementare e decrementare il valore del contatore. I segmenti uguali dei display sono stati collegati insieme (il segmento A del primo display è collegato con il segmento A del secondo display e così via). I catodi vengono pilotati con due transistor BJT di tipo NPN: quando alla base giunge il segnale logico alto dal picmicro, il transistor va in saturazione e porta il catodo a massa, facendo accendere i segmenti a cui abbiamo dato tensione: si utilizza un transistor perchè nel catodo comune fluisce la corrente di tutti i segmenti accesi, quindi già da 2 segmenti in poi non è possibile utilizzare il pin del picmicro in maniera diretta altrimenti si friggerebbe. Al posto del BC547 indicato nello schema potete utilizzare un NPN di bassa potenza qualsiasi.
Nella sezione Risorse di questo sito, potete scaricare gratuitamente il documento “Pinout ed equivalenze transistor bjt di bassa potenza” in maniera tale da poter sostituire i due transistor con altri che magari avete a portata di mano.
Da notare che ho omesso la parte relativa al connettore di programmazione e alla circuiteria di reset.
Bene, adesso passiamo al codice, come sempre il codice sarà formato dal modulo principale (main.c), da un file header contenente le configurazioni (settings.h) più altri file ausiliari (delay.c e delay.h utilizzati per la generazione dei ritardi). Scaricate i file della lezione in fondo all’articolo e diamo dapprima uno sguardo al file settings.h. Tralasciando le cose che ormai ci dovrebbero essere ben chiare, andiamo ad analizzare le parti essenziali per capire come funziona il programma. E’ innanzitutto da notare la riga:
18 | #define dispReg PORTB // Banco al quale sono collegati i display |
Questo l’ho fatto in maniera tale che potete modificare semplicemente “PORTB” con il banco che voi preferite se avete già realizzato uno schema simile, senza andare a cambiare tutti i “PORTB” all’interno del codice: questo è un buon modo di rendere “portabile” un codice: in futuro lo potremo modificare per adattarlo ad un altro pic o ad un’altra situazione, senza fare troppa fatica.
Proseguendo, incontriamo la definizione del valore dei segmenti:
48 49 50 51 52 53 54 | #define SA 64 #define SB 32 #define SC 16 #define SD 8 #define SE 4 #define SF 2 #define SG 1 |
In pratica cosa ho fatto: avendo collegato il segmento A dei display alla porta RB6, il segmento B alla porta RB5 ecc, ho definito dei “simboli” che mi identificano il valore numerico del bit della porta al quale il segmento è collegato (RB6 è il bit 6 della porta B, il bit 6 in decimale ha valore 64 ecc)
Poi ancora:
56 57 58 59 60 61 62 63 64 65 66 | // Segmenti da accendere in base al numero (Nn) da comporre #define N0 SA+SB+SC+SD+SE+SF #define N1 SB+SC #define N2 SA+SB+SG+SE+SD #define N3 SA+SB+SG+SC+SD #define N4 SF+SG+SB+SC #define N5 SA+SF+SG+SC+SD #define N6 SA+SF+SG+SE+SC+SD #define N7 SA+SB+SC #define N8 SA+SB+SF+SG+SC+SD+SE #define N9 SA+SB+SF+SG+SC+SD |
Non ho fatto altro che dire: per formare il numero 0 (identificato con N0), devo accendere i segmenti A, B, C, D, E, F. Così facendo è più semplice anche per voi capire cosa stiamo facendo e definirvi, perchè no, anche delle lettere (questi display possono anche visualizzare una A maiuscola, una E, una F o una b minuscola ecc ;) )
In seguito mi sono definito un’array in cui sono contenuti proprio questi numeri:
79 | const unsigned char dispnum[]={N0,N1,N2,N3,N4,N5,N6,N7,N8,N9}; |
Qui c’è da fare una precisazione sull’attributo “const”: mettendo questa parola chiave davanti alla definizione di una variabile, faccio in modo innanzitutto che questa variabile non sia allocata nella memoria RAM del mio microcontrollore ma nella memoria programma, ovvero quella destinata ad accogliere materialmente il nostro programma, appunto. E’ buono usare questa definizione quando si dichiarano valori costanti, ovvero il cui valore numerico non deve mai cambiare durante l’esecuzione del programma, questo, oltretutto, ci fa risparmiare memoria RAM, in questo caso non ne avevamo la reale necessità (di risparmiare ram), ma trattandosi di valori costanti ho preferito dichiararli come “const”. Lo scopo di questo array così dichiarato è presto detto: quando vorrò visualizzare sul mio display, ad esempio, il numero 7, mi basterà fare una cosa del tipo:
PORTB=dispnum[7]; |
Ovvero assocerò alla porta alla quale i display sono collegati, il valore numerico contenuto dall’elemento dell’array avente lo stesso indice numerico della cifra che voglio visualizzare.
Per il resto nulla di complicato: ci sono i prototipi delle funzioni, la dichiarazione di alcune variabili utilizzate nel programma e l’impostazione dell’interrupt ogni millisecondo come abbiamo già visto nelle precedenti lezioni.
Passiamo ora al programma principale. Il main non fa niente di eccezionale: si limita a leggere lo stato dei due pulsanti e ad incrementare o decrementare il valore del contatore, memorizzato nella variabile “counter” che è poi quella che viene visualizzata sui display. Da notare che quando ci troviamo al valore 99 e premiamo ancora il tasto di incremento, il valore viene portato a zero, al contrario se ci troviamo al valore 0 premiamo il tasto di decremento, il valore viene portato a 99, ci sono inoltre le routine antirimbalzo già viste nelle lezioni precedenti:
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | // pulsante "su" premuto if (btnUp==Pressed) { DelayMs(100); if (btnUp==Pressed) // antibounce { counter++; // incremento il contatore // se il contatore supera 99, lo azzero if (counter>99) { counter=0; } } } // pulsante "giu" premuto if (btnDn==Pressed) { DelayMs(100); if (btnDn==Pressed) // antibounce { if (counter==0) { counter=99; } else { counter--; // decremento il contatore } } } |
La parte interessante è invece svolta dalla interrupt service routine. Ogni millisecondo, la funzione ISR viene richiamata dall’interrupt causato dall’overflow del Timer0. In questa funzione abbiamo il contatore “muxcount” il quale viene incrementato ad ogni interrupt. Ogni volta che tale contatore raggiunge il valore di 5, viene invertito il flag “actdisplay” che stabilisce se deve rimanere acceso il display delle unità o quello delle decine, ogni display rimane quindi acceso per 5 millisecondi, dopodichè si spegne e viene acceso l’altro: stiamo realizzando il multiplexing.
Ci calcoliamo quindi il numero da visualizzare con delle semplici divisioni:
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | if (actdisplay==0) // devo mostrare solo le unità { displayU=ON; // accendo il display delle unità // mi calcolo quindi la sola unità da mostrare result=counter/10; // ottengo la decina (esempio: 28/10 = 2) // nota: in C la divisione tra interi restituisce un intero, quindi privo di decimali result=counter-(result*10); // ottengo la sola unità // se, ad esempio, il contatore era 28, result ora //qui vale 2, quindi dal contatore sottraggo (2*10) e ottengo 8 } else // devo mostrare solo le decine { result=counter/10; // ottengo la sola decina // accendo la decina solo se il risultato è maggiore di zero if (result) { displayD=ON; } } |
Nel caso che deve rimare acceso il display delle decine, dobbiamo visualizzare soltanto la decina: se abbiamo quindi che il valore del contatore è 28, dovremo visualizzare il solo 2, questo si ottiene semplicemente dividendo il valore del counter per 10: il risultato è un numero intero, privo dei decimali. La decina, inoltre, sarà visualizzata soltanto se il suo valore è diverso da zero. Per l’unità è la stessa cosa: faremo soltanto un’operazione in più: sottraiamo il valore della decina dal contatore per ottenere la sola unità, il concetto è molto semplice; basta tener conto che in C la divisione tra interi restituisce un intero, quindi privo dei decimali.
In seguito associo al registro, sul quale sono collegati i display, il valore numerico da visualizzare, che è stato memorizzato nella variabile “result”:
130 | dispReg=dispnum[result]; |
Quindi eseguo delle altre operazioni:
134 135 136 137 138 139 140 141 142 | if (muxcount==5) { muxcount=0; // azzero il contatore actdisplay=actdisplay^1; // cambio il display per il prossimo ciclo dispReg=0; // azzero il display attualmente visualizzato //per prevenire l'effetto "ghosting" displayU=OFF; // spengo display unità displayD=OFF; // spengo display decine } |
In pratica: azzero il contatore del multiplexer se ha raggiunto il valore di 5, in maniera da cambiare il display da accendere. E’ interessante la riga:
138 | dispReg=0; |
Questo in pratica viene fatto per eliminare l’effeto di “ghosting“, ovvero la scia rimasta dalla cifra precedente: provate a vedere cosa succede eliminando questa riga, in realtà l’effetto potrebbe non notarsi a determinate frequenze e con determinati display.
Infine resetto il flag di interrupt sul timer0, altrimenti non lo posso più intercettare:
144 | T0IF=0;// Reset flag interrupt su timer 0 |
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.
Nel file seguente è contenuto il codice sorgente completo:
Se volete realizzare questo esempio con display ad anodo comune anzichè a catodo comune dovete tenere conto che:
- Al posto di transistor NPN dovrete utilizzare PNP e i display non verranno più attivati con livello logico alto ma con livello logico basso.
- I singoli segmenti dei display non si attiveranno più mettendo ad 1 il relativo bit ma mettendolo a zero. Quindi per definire i segmenti da accendere per ogni singolo numero anzichè fare, ad esempio, SA+SB, farete: 255-(SA+SB)
Se volete realizzare questo programma utilizzando più di 2 display vi fornisco qui solo due consigli principali, il resto ormai dovreste essere bravi per intuirlo da soli:
- La variabile destinata a contenere il conteggio non potrà essere più di tipo CHAR perchè il tipo CHAR, essendo ad 8 bit, contiene al massimo un numero fino a 255.
- Il flag “actdisplay”, utilizzato in questo esempio per effettuare lo switch del display, non potrà essere più di tipo BIT ma dovrà essere di tipo CHAR e gestito come un contatore, si deciderà quindi quale display accendere tramite una struttura SWITCH…CASE