Indice |
Display a 7 segmenti
I display a LED a 7 segmenti fecero apparizione più di 30 anni fa nelle prime calcolatrici tascabili, prima dell' avvento dei display a cristalli liquidi, i cosiddetti LCD. Prima dei display a LED si utilizzavano i display elettroluminescenti (VFD), sicuramente più belli dal punto di vista estetico (sono utilizzati ancora oggi in alcune autoradio) ma più difficili da pilotare. I display VFD non erano modulari, come anche gli odierni LCD se escludiamo qualche eccezione. I display a LED sono invece modulari, si possono affiancare per ottenere un visualizzatore con il numero di cifre che si desiderano. Per l' hobbista sono un semplice e comodo sistema per visualizzare numeri, per realizzare sveglie digitali, strumenti e quant' altro.
I microcontrollori odierni hanno i pin delle porte di uscita in grado di pilotare direttamente i LED senza bisogno di interporre circuiti di potenza. I problemi iniziano a sorgere quando si ha bisogno di pilotare un visualizzatore composto da più cifre. Collegando direttamente i display alle uscite si fa in fretta ad esaurirle visto che ogni display a 7 segmenti necessita di almeno 7 uscite per pilotare i rispettivi segmenti (8 se si considera anche il punto decimale). Per ridurre il numero di pin di uscita si utilizza una tecnica chiamata multiplexing. Si tratta, in buona sostanza, di accendere un display per volta, per un tempo breve, trascorso il quale il display verrà spento e verrà acceso il successivo. Se la velocità di scansione è sufficientemente elevata si avrà l' impressione di vedere tutti i display accesi contemporaneamente.
Questo breve articolo descrive un software che ho realizzato per implementare il multiplexing sui microcontrollori PIC della Microchip. Il software è scritto in modo da adattarsi ai display a catodo o ad anodo comune, permette la scelta dei piedini del micro deputati alle varie funzioni ed è facilmente adattabile alla maggior parte delle esigenze di visualizzazione.
Il Multiplexing
Lo schema elettrico qui sotto è un classico esempio di come vengono collegati i displays per realizzare un visualizzatore multiplexato a 8 digit (se ne vedono di meno perchè l' immagine è tagliata). Si tratta una basetta che ho costruito per sviluppare un' applicazione con una logica programmabile. Ho dovuto inserire dei buffer perché la CPLD che usavo non è in grado di pilotare direttamente i display ma analizzandola si capisce bene il principio di funzionamento. Gli otto display sono ad anodo comune con tutti i segmenti dello stesso nome collegati insieme. Per far comparire un "1" nel display DL8 si imposta il dato sulle linee che vanno da SEGa a SEGdp (rispettivamene a 10011111, lo zero attiva il segmento) che, per così dire, collegheranno i segmenti (catodi) a massa attraverso le resistenze da 100R, poi si comanda il transistor Q8 mandando a livello 0 la linea AN7. Attraverso la resistenza R8 il transistor Q8 andra' in saturazione collegando di fatto tutti gli anodi dei 7 segmenti all' alimentazione. Il display si accende ed indica il numero "1".
Questo procedimento (impostare i segmenti ed accendere il display corrispondente) viene eseguito parecchie volte al secondo. Per evitare lo sfarfallìo la scansione completa di tutti i display deve avvenire in almeno 40ms. Una frequenza maggiore è preferibile perché si ottiene una visione più stabile e confortevole. In questo caso specifico abbiamo un multiplexing con rapporto 1/8 (8 sono i display) ed è un limite che è meglio non superare per non pilotare i displays con correnti troppo elevate che rischierebbero di di distruggerli. Infatti la corrente di pilotaggio è più alta di quella utilizzata per un pilotaggio diretto. Che lo si voglia o no il multiplexing diminuisce la luminosità ma permette un notevole risparmio di pin di uscita del micro.
Il Firmware
Il firmware è scritto in C per MPLAB per il microcontrollore PIC18F4685 (ma si adatta a tutti i microcontrollori) quindi semplice da capire. Oltre a fare quello che deve fare, cioè gestire un display multiplexato, può essere un valido esempio di come scrivere del software riutilizzabile in altre applicazioni.
Analizziamolo quindi nel dettaglio.
La prima cosa che si nota è una riga di commento che indica un' inclusione di un file. Per motivi di analisi e per per capirlo meglio avendolo tutto sott' occhio l' ho scritto in un unico file ma la prima parte suggerisco di separarla e conservarla in un file a se per i eventuali futuri utilizzi.
La seconda parte è la definizione dell' hardware. Questo è il classico esempio di come scrivere un modulo software molto flessibile. In passato si tendeva (e lo si fa ancora oggi ma di meno) a raggruppare le linee di pilotaggio dei displays in una unica porta per scrivere un firmware più compatto e veloce. Oggi, fortunatamente, i micro hanno parecchia ROM e sono molto più veloci. Il signore su cui gira questo software funziona con un clock a 32MHz. ed ha ben 96 kB di flash e non ha problemi di velocità o di spazio in memoria. Quindi è preferibile fare moduli facilmente adattabili. Nel nostro caso l' adattabilità è massima. Si scelgono le linee una per una. Possono anche essere mischiate fra di loro ed il sotware funzionerà sempre egregiamente. Il prezzo che si paga è un codice più lungo e più lento ma non rappresenta affatto un problema. Nel sorgente vediamo che ho assegnato, ad esempio, il comando dell' anodo del display 0 al pin RA0, quindi DISPLAY0 sara assegnato con una #define al bit 0 del LAT relativo alla porta A. Per ogni segnale è poi necessario definire il suo bit che indica la direzione (input o output) e quindi troviamo DISPLAY0_DIR (il DIR sta per "direzione") assegnato al bit 0 del TRIS della porta A. Lo stesso si fa per le linee di pilotaggio dei segmenti. Questa parte di defines è quella che dovrà poi essere modificata per adattarla all' hardware. Alla fine di questa sezione troviamo l' assegnazione dei livelli di pilotaggio dei displays e dei segmenti.
Dopo incontriamo la funzione di inizializzazione che va chiamata all' inizio del programma almeno una volta. Questa prepara opportunamente le linee di pilotaggio e disattiva tutto il display in modo da non lasciare niente di acceso. Infine troviamo la funzione che serve per accendere il display indicato dalla variabile disp7_pt e che mette sulle linee di pilotaggio dei segmenti il suo valore. Questa funzione deve essere chiamata continuamente aggiornando di volta in volta la variabile disp7_pt affinchè punti al display successivo o, arrivata all' ultimo display, che si riporti a 0.
Subito dopo troviamo la routine di servizio delle interrupt a priorità alta. Il software vero e proprio per la gestione del miultiplexing fa solo tre cose: aggiorna la variabile disp7_pt, chiama la funzione di scan e ricarica il timer che genererà l' interrupt. Ho scritto le due routines (quella ad alta e quella a bassa priorità) in modo che siano già presenti nello scheletro del programma pronte per essere ampliate ed utilizzate. Il compilatore richiede anche l' impostazione dei vettori per la chiamata alle routines di gestione dell' interrupt. Questi vettori sono definiti subito dopo le routines di servizio.
Arriviamo infine alla main function. Che dire? Molto semplice. Inzializza il display, interrupts e timer. Quindi carica dei numeri di prova per il display ed esegue il loop di funzionamento che in questo esempio è vuoto, o meglio, deve essere riempito con tutto il software dell' applicazione, quello cha da il vero senso al programma!
Il programma in c
Download
http://www.electroportal.net/users/files/sevendisp.txt
#include <p18f4685.h> //------------------------#include "disp7_mux.c" ------------- // Array contenente i codici dei segmenti dei displays. // Per accendere il segmento // il bit corrispondente deve essere messo a 1. // Corrispondenza dei segmenti // Bit Bit7 Bit6 Bit5 Bit4 Bit3 Bit2 Bit1 Bit0 // Segmento (punto) G F E D C B A unsigned char disp7_dig[8]; // Variabile utilizzata per la scansione. // Punta al display attualmente acceso // NON VIENE INCREMENTATA IN AUTOMATICO // ma va impostata prima di chiamare la // funzione scan_disp7() char disp7_pt; // Array per la decodifica dei numeri da 0 a 10 in 7 segmenti. //All' occorrenza si possono aggiungere anche i simboli da A ad F //per avere la decodifica esadecimale rom unsigned char disp7_decode[10]=\ {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x27,0x7f,0x6f}; // -------------------------- Definizione dell' hardware ----------------------- // defines delle linee di pilotaggio dei displays. // Queste dipendono dal circuito. // Si possono usare qualsiasi pin di qualsiasi porta a patto che possa // essere configurato come pin di uscita #define DISPLAY0 LATAbits.LATA0 #define DISPLAY0_DIR TRISAbits.TRISA0 #define DISPLAY1 LATAbits.LATA1 #define DISPLAY1_DIR TRISAbits.TRISA1 #define DISPLAY2 LATAbits.LATA2 #define DISPLAY2_DIR TRISAbits.TRISA2 #define DISPLAY3 LATAbits.LATA3 #define DISPLAY3_DIR TRISAbits.TRISA3 #define DISPLAY4 LATAbits.LATA4 #define DISPLAY4_DIR TRISAbits.TRISA4 #define DISPLAY5 LATAbits.LATA5 #define DISPLAY5_DIR TRISAbits.TRISA5 #define DISPLAY6 LATEbits.LATE0 #define DISPLAY6_DIR TRISEbits.TRISE0 #define DISPLAY7 LATEbits.LATE1 #define DISPLAY7_DIR TRISEbits.TRISE1 // Defines delle linee di pilotaggio dei segmenti. // Queste dipendono dal circuito. // Si possono usare qualsiasi pin di qualsiasi porta a patto che possa // essere configurato come pin di uscita #define SEG_A LATDbits.LATD0 #define SEG_A_DIR TRISDbits.TRISD0 #define SEG_B LATDbits.LATD1 #define SEG_B_DIR TRISDbits.TRISD1 #define SEG_C LATDbits.LATD2 #define SEG_C_DIR TRISDbits.TRISD2 #define SEG_D LATDbits.LATD3 #define SEG_D_DIR TRISDbits.TRISD3 #define SEG_E LATDbits.LATD4 #define SEG_E_DIR TRISDbits.TRISD4 #define SEG_F LATDbits.LATD5 #define SEG_F_DIR TRISDbits.TRISD5 #define SEG_G LATDbits.LATD6 #define SEG_G_DIR TRISDbits.TRISD6 #define SEG_dp LATDbits.LATD7 #define SEG_dp_DIR TRISDbits.TRISD7 // Definizione del tipo di pilotaggio del dispay. In questo caso // usiamo display ad ANODO comune, quindi questi saranno // accesi con uno 0 logico. #define SEG_ON 0 #define SEG_OFF 1 // Nel caso si usino displays a catodo comune usare queste due // defines e mettere come commento le due precedenti //#define SEG_ON 1 //#define SEG_OFF 0 // Definizione del tipo di pilotaggio dei comuni dei displays. // Nel nostro caso il display avra' il comune collegato quando // in uscita si avra' uno 0 perche' usiamo un transistor PNP // e display ad ANODO comune. #define DISP_ON 0 #define DISP_OFF 1 // ------------Fine della definizione dell' hardware ---------- // Inizializzazione display a 7 segmenti void disp7_init(void) { // Inizializzazione dei pin che pilotano i segmenti SEG_A_DIR = 0; SEG_A = SEG_OFF; SEG_B_DIR = 0; SEG_B = SEG_OFF; SEG_C_DIR = 0; SEG_C = SEG_OFF; SEG_D_DIR = 0; SEG_D = SEG_OFF; SEG_E_DIR = 0; SEG_E = SEG_OFF; SEG_F_DIR = 0; SEG_F = SEG_OFF; SEG_G_DIR = 0; SEG_G = SEG_OFF; SEG_dp_DIR = 0; SEG_dp = SEG_OFF; // Inizializzazione dei pin che comandano il comune dei displays DISPLAY0_DIR = 0; DISPLAY0 = DISP_OFF; DISPLAY1_DIR = 0; DISPLAY1 = DISP_OFF; DISPLAY2_DIR = 0; DISPLAY2 = DISP_OFF; DISPLAY3_DIR = 0; DISPLAY3 = DISP_OFF; DISPLAY4_DIR = 0; DISPLAY4 = DISP_OFF; DISPLAY5_DIR = 0; DISPLAY5 = DISP_OFF; DISPLAY6_DIR = 0; DISPLAY6 = DISP_OFF; DISPLAY7_DIR = 0; DISPLAY7 = DISP_OFF; disp7_pt = 0; } // Step di scansione dei displays // Questa funzione accende il display indicato // dalla variabile disp7_pt ed accende opportunamente // i segmenti corrispondenti alla cifra puntata void scan_disp7(void) { unsigned char dato; // Spegne tutti i displays. Questa operazione va fatta subito // per evitare di visualizzare dei segmenti "fantasma" dovuti // alla commutazione DISPLAY0 = DISPLAY1 = DISPLAY2 = DISPLAY3 = DISPLAY4 \ = DISPLAY5 = DISPLAY6 = DISPLAY7 = DISP_OFF; // Imposta i segmenti dato = disp7_dig[disp7_pt]; // Analizza ogni bit per vedere se accendere il segmento // corrispondente e' necessario farlo bit per bit per poter // avere la possibilita' di scegliere arbitrariamente le uscite // del micro ed i livello di accensione // quindi prima di tutto spegne tutti i segmenti SEG_A = SEG_B = SEG_C = SEG_D = SEG_E \ = SEG_F = SEG_G = SEG_dp = SEG_OFF; // e poi accende quelli da accendere if (dato & 0x01) SEG_A = SEG_ON; if (dato & 0x02) SEG_B = SEG_ON; if (dato & 0x04) SEG_C = SEG_ON; if (dato & 0x08) SEG_D = SEG_ON; if (dato & 0x10) SEG_E = SEG_ON; if (dato & 0x20) SEG_F = SEG_ON; if (dato & 0x40) SEG_G = SEG_ON; if (dato & 0x80) SEG_dp = SEG_ON; // Attiva il nuovo display // In questa parte si sceglie quale linea attivare switch (disp7_pt) { case 0: DISPLAY0 = DISP_ON; break; case 1: DISPLAY1 = DISP_ON; break; case 2: DISPLAY2 = DISP_ON; break; case 3: DISPLAY3 = DISP_ON; break; case 4: DISPLAY4 = DISP_ON; break; case 5: DISPLAY5 = DISP_ON; break; case 6: DISPLAY6 = DISP_ON; break; case 7: DISPLAY7 = DISP_ON; break; } } // ---------------------- fine file disp7_mux.c ---------------------- //--------------- Routines di servizio delle interrupts ----------- // Routine di servizio livello alto #pragma interrupt interrupt_high void interrupt_high(void) { // Per riconoscere quale dispositivo ha generato l' interrupt // e' necessario leggere lo stato del bit associato. Questo // perche' c'e' una sola routine di servizo dell' interrupt e // qui dentro tutto deve essere fatto // Timer0 interrupt sull' overflow. if (INTCONbits.TMR0IF) { // Una volta verificato che e' il Timer0 a richiedere e' necessario // resettare il bit che ha generato l' interrupt per prevenire // ulteriori chiamate INTCONbits.TMR0IF = 0; // Da qui in poi bisogna scrivere il software vero e proprio che // deve essere eseguito. // Per prima cosa incrementa il puntatore alla cifra che dovra' // essere accesa if (disp7_pt <7) disp7_pt++; else disp7_pt = 0; // Chiama ora la funzione che effettua il refresh del display scan_disp7(); // Infine ricarica il Timer0. Questo andra' ad incrementarsi. // Quando andra' in overflow questa routine verra' // nuovamente chiamata. TMR0H = 0xF0; TMR0L = 0x00; } } #pragma code // Routine di servizio livello basso #pragma interruptlow interrupt_low void interrupt_low(void) { } #pragma code //--------------- Definizione dei vettori di interrupt ------------------------- // Queste due funzioni servono esclusivamente a dirottare il // programma su quelle che sono le effettive funzioni di gestione // delle interrupts // Vettore per livello alto #pragma code InterruptVectorHigh = 0x08 void InterruptVectorHigh (void) { _asm goto interrupt_high _endasm } //jump to interrupt routine // Vettore per livello basso #pragma code InterruptVectorLow = 0x18 void InterruptVectorLow (void) { _asm goto interrupt_low _endasm } //jump to interrupt routine #pragma code // ----------------------- Main Function --------------------------------------- void main(void) { ADCON1 = 0x0F; // Tutti ingressi digitali // Inizializza il display disp7_init(); // Inizializza le interrupts ed il timer 0 che servira' per la scansione INTCON = 0x20; //disable global and enable TMR0 interrupt INTCON2 = 0x84; //TMR0 high priority RCONbits.IPEN = 1; //enable priority levels TMR0H = 0; //clear timer TMR0L = 0; //clear timer T0CON = 0x80; //set up timer0 - prescaler 1:2 INTCONbits.GIEH = 1; //enable interrupts // Per provare il software il display indichera' "01234567" disp7_dig[0] = disp7_decode[7]; disp7_dig[1] = disp7_decode[6]; disp7_dig[2] = disp7_decode[5]; disp7_dig[3] = disp7_decode[4]; disp7_dig[4] = disp7_decode[3]; disp7_dig[5] = disp7_decode[2]; disp7_dig[6] = disp7_decode[1]; disp7_dig[7] = disp7_decode[0]; // loop infinito principale while(1) { } }