Corso programmazione microcontrollori PIC® in C (aggiornamento MPLAB X) – Realizziamo un cronometro al centesimo di secondo con i display led a 7 segmenti

Questa lezione è un aggiornamento per MPLAB X IDE e compilatore XC della vecchia lezione sul pilotaggio dei display a 7 segmenti in multiplex. Per tutta la parte preliminare di teoria potete fare riferimento alle lezioni del vecchio corso di programmazione microcontrollori PIC in C in cui parlo del pilotaggio dei display a 7 segmenti.

La vecchia lezione a cui faccio riferimento è la seguente:

Non tratterò quindi tutta la parte già scritta in quell’articolo, che vi invito a leggere, se non l’avete già fatto, prima di continuare dato che spiega i display a 7 segmenti e la tecnica del Multiplexing.

Andremo a realizzare un Cronometro digitale al centesimo di secondo sfruttando un PIC® e un display led 7 segmenti a 4 digit. Il codice è riutilizzabile per fare un orologio, un contapezzi, un elimina-code, un countdown ecc.

Il Display utilizzato

Questo esempio non utilizza nessun modulo specifico del PIC per cui le cose, rispetto alla vecchia lezione, non sono cambiate tantissimo. Ho cambiato il tipo di display utilizzato: nella vecchia lezione usavo due display singoli a catodo comune, in questo esempio ho preferito utilizzare un display a 4 cifre ad anodo comune, perchè più facile sia da pilotare che da reperire a ottimo prezzo.

Il modello di display utilizzato ha la sigla YSD-439AB4B-35 ed è in vendita su Robot-Italy e su SparkFun in varie colorazioni (giallo, rosso, verde e blu) ad un prezzo inferiore a quello a cui può arrivare un display singolo. Questo display ha le cifre alte 0.56″, è lungo 5cm e alto 1.9cm.

Questo tipo di display ha 16 pin (8+8)

Attenzione: questo modello di display esiste anche in versione “seriale”, io non sto usando quello seriale ma quello ad anodo comune, che ha 16 pin disposti su 2 file. Non sbagliatevi.

Ho scelto questo tipo di display perchè ha già tutti i segmenti uguali delle diverse cifre collegati tra loro (è già predisposto per il multiplexing), per cui il numero dei collegamenti si riduce moltissimo ed è un sollievo specie quando si utilizzano millefori o breadboard. In aggiunta abbiamo i due punti di separazione centrali delle cifre (colon), che possono essere sfruttati per realizzare orologi (sul datasheet, scaricabile in fondo all’articolo, sono indicati come L1 ed L2). Come su tutti gli altri tipi di display a 7 segmenti abbiamo i punti decimali associati ad ogni cifra (DPx) e infine un altro puntino in alto a destra affianco al Digit 3 (L3 – apostrofo) che può essere utilizzato, ad esempio, nel caso costruissimo un termometro digitale, per indicare il simbolo dei gradi centigradi, visualizzando, magari, anche una C sul digit 4.

Altri Display a 4 digits

Attenzione ai cloni cinesi su ebay: guardando con attenzione le immagini di tali display (che riportano la sigla HSX410561K) ci devono saltare subito all’occhio due cose: non hanno l’apostrofo (e vabbè, direte voi, chissenefrega,  tanto non mi serve – ed è giusto) ma… hanno soltanto 12 pin! Se vi fate due conti, 12 pin servono soltanto per i 4 comuni+7 segmenti+punto decimale… Il che vuol dire che o non è possibile pilotare i due puntini centrali anche se sul display ci sono, o che sono scesi a quache strano compromesso…

Questo display, che si trova anche al modico prezzo di €1, ha solo 6 pin… ma ci sono anche i due puntini al centro… i conti non tornano!

Con tantissima fatica sono riuscito a recuperare uno schema di questo tipo di display (potete scaricarlo in fondo all’articolo) e si nota una cosa: i due puntini centrali (indicati come D1 e D2) hanno l’anodo collegato a quello del digit 2 e i catodi collegati a quelli dei puntini decimali, il che vuol dire che i due puntini vanno gestiti insieme al digit 2, e se li accendiamo, si accende anche il punto decimale! Una cosa un po’ brutta da vedersi…

Altri display cinesi a poco prezzo, siglati CPS02841AR (attenzione: questo modello è più piccolo! E’ alto la metà: 0.28″! se la sigla finisce con AR è a catodo comune, se BR è anodo comune) pure hanno questa conformazione (manca l’apostrofo e hanno 12 pin) ma guardando il datasheet (potete scaricarlo in fondo all’articolo) emerge una cosa ancora più triste: fermo restando che i due puntini sono collegati anch’essi insieme al punto decimale del digit2, mancano del punto decimale sul digit1 anche se all’esterno sembra sia presente. Qui non capisco questa scelta: dato che il punto decimale sul primo digit manca, potevano collegare i due puntini a quello, in maniera da poterli accendere separatamente… boh!

Calcolo della resistenza

Chiusa parentesi display cinesi, torniamo al display che ho utilizzato io: i comuni di ogni digit (pins 1,2,6 e 8 del display) vanno pilotati ognuno col proprio transistor per assicurare il quantitativo di corrente necessaria (teniamo conto di un massimo di 20mA per ogni segmento/puntino), per cui ipotizzando 8 led accesi contemporaneamente (7 segmenti + puntino decimale), il transistor, tra collettore ed emettitore, deve far scorrere un massimo di 160mA. Ho utilizzato un 2N3906 che ha una Ic di 200mA.

Per ogni segmento, va calcolata la resistenza in base alla tensione di lavoro, Vf e alla corrente di lavoro, If (la F sta per Forward) del led. I calcoli li ho fatti tenendo conto di un led rosso, che è quello che generalmente ha una tensione di lavoro più bassa e quindi necessita di una resistenza di valore maggiore rispetto ai led di altri colori. Per un led rosso, cha ha una If di 15mA e una Vf di 1.8V, dato che vengono alimentati a 5V, la resistenza è R=(Vdd-Vf)/If = (5-1.8)/0.015 = 213Ω, dato che non è un valore commerciale, ho utilizzato una resistenza da 220Ω.

Io ho usato il display con led Blu, che hanno una Vf di 3.4V e una If di 20mA, per cui avrei dovuto usare resistenze da 80Ω. A dire il vero il display è molto luminoso e per me si vede benissimo anche così: un valore di 220Ω vi assicura di non bruciare nulla in nessun caso. Ad ogni modo potete utilizzare questa simpatica pagina per calcolare la resistenza da mettere in serie ai segmenti tenendo conto dei valori standard per i 4 colori in cui è possibile trovare questo display.

Il circuito

Il circuito è molto semplice: 7 IO andranno utilizzati per pilotare i singoli segmenti delle cifre, con la propria resistenza in serie, e altri 4 IO andranno utilizzati per pilotare gli anodi comuni tramite il transistor PNP.

Nel caso utilizzassimo un display a catodo comune, il transistor da utilizzare deve essere NPN e la base va collegata a GND. A tutto il resto ci pensa il codice, che ho predisposto con una macro, dato che la logica di funzionamento si inverte.  A riga 56  del main.c abbiamo difatti:

56
57
58
// se il display utilizzato è del tipo a catodo comune,
// allora commentare la seguente riga. Se è ad anodo comune, lasciare così
#define COMMON_ANODE

Se usate il mio stesso tipo di display, lasciate così, se invece usate un display a catodo comune, oltre alla modifica del transistor che già vi ho detto, dovete commentare la riga 58. La macro COMMON_ANODE viene difatti utilizzata in varie parti del codice per adattare il tipo di display. Ad esempio a riga 60 c’è una nota che spiega la cosa:

60
61
62
63
64
65
66
67
68
69
70
71
72
73
/* se il display è ad anodo comune, i segmenti si accenderanno
 * mandando un livello logico 0 ai singoli catodi e sulla base
 * del transistor PNP che pilota l'anodo comune.
 * Viceversa se il display è a catodo comune, i segmenti si
 * accenderanno mandando un livello logico 1 ai singoli anodi
 * e sulla base del transistor NPN che pilota il catodo comune
 */
#ifdef COMMON_ANODE
#define TURN_ON 0
#define TURN_OFF 1
#else
#define TURN_ON 1
#define TURN_OFF 0
#endif

Ad ogni modo non ho testato il codice per il catodo comune, quindi non vi do garanzia di funzionamento anche se il codice è già tutto adattato e se ci sono errori è sicuramente qualcosa di banale.

Ho utilizzato un altro IO per pilotare i due punti centrali dato che andremo a realizzare un cronometro digitale, ma nella vostra applicazione potreste scegliere di collegare a questo IO il segmento del punto decimale.

In realtà, molto spesso si preferisce la semplicità e non risulta necessario gestire anche il punto decimale. E’ difatti consuetudine, quando si realizza uno strumento che mostra i decimali, di utilizzare la virgola fissa sul display. Per cui, nel caso di un termometro, ad esempio, oltre a visualizzare l’apostrofo (L3) e la lettera C sul digit 4, possiamo tenere fisso il punto decimale DP2 e gestire quindi unicamente i primi 3 digit e andremo a risparmiare 2 I/O.

Lo schema è il seguente:

Infine altri due IO saranno utilizzati per i pulsanti che avranno le funzioni di start/stop e reset, rispetto al vecchio schema non ho usato le resistenze di pull-up esterne perchè andrò a sfruttare quelle interne.

Per quanto riguarda il PICmicro, ho utilizzato sia un PIC16F887 che un PIC16F18877 dato che, come vi dicevo nell’articolo precedente (che dovete leggere dal momento che illustra nozioni relative ai nuovi PICmicro enhanched) il 16F887 è già dichiarato obsoleto e verrà soppiantato definitivamente dal 16F18877.

Il PIC16F18877 in particolare ha la possibilità di abilitare le resistenze di pull-up su tutte le porte mediante i registri WPUx (dove x viene sostituito dalla lettera del banco), il PIC16F887 soltanto sul banco B tramite il registro OPTION_REG

Non mi dilungo ulteriormente perchè tutta la parte di teoria relativa al multiplexing l’ho spiegata nel vecchio articolo. Ricordo soltanto che i digit vengono accesi uno alla volta molto velocemente, per cui all’occhio danno l’impressione di essere accesi tutti insieme, è quindi necessario farsi bene i conti per evitare fastidiosi sfarfallii del display (che sono sempre visibili quando si visualizzano i display attraverso una fotocamera).

Le configurazioni

Come dicevo più alto il progetto l’ho realizzato per due diversi PICmicro. Tale progetto, che potete trovare al paragrafo downloads, contiene due configurazioni: ovvero è realizzato per due differenti PICmicro contemporaneamente sfruttando la gestione configurazioni offerta da MPLABX.

Dopo aver scaricato e aperto il progetto in MPLABX, cliccate col tasto destro sul nome del progetto e andate su Set Configuration: si apre un sottomenù da cui è possibile scegliere due diverse configurazioni che ho chiamato con il nome dei due PICmicro utilizzati:

Le configurazioni non vengono utilizzate soltanto per adattare un progetto a più PIC, ma anche per adattarlo a diversi compilatori, programmatori, debuggers ecc. Una configurazione include difatti la scelta del microcontrollore, del programmatore e delle sue opzioni e del compilatore da utilizzare.

In particolare, per evitare errori durante la compilazione, modificate il progetto per impostare il vostro programmatore e la vostra versione del compilatore XC.

In particolare ho fatto in modo che il progetto venga adattato in automatico al PIC cambiando soltanto la configurazione del progetto. Per ogni PIC scelto nel progetto viene definito un nome mnemonico che inizia per underscore ed è uguale alla sigla del PIC ma senza il suffisso PIC, per cui definendo un progetto per PIC16F887 mi sarà automaticamente definito il nome mnemonico _16F887 e, nel caso di più configurazioni dello stesso progetto, posso tener conto di porzioni di codice piuttosto che di altre semplicemente utilizzando le direttive #ifdef.

Nel Main all’inizio ho scritto:

#ifdef _16F887
// PIC16F887 con quarzo esterno da 20MHz
#include "conf_16F887.h"
#define _XTAL_FREQ 20000000
#endif
 
#ifdef _16F18877
// PIC16F18877 con oscillatore interno a 32MHz
#include "conf_16F18877.h"
#define _XTAL_FREQ 32000000
#endif

facendo in questo modo, se seleziono la configurazione per il 16F887, dato che viene definito _16F887, tramite le direttive #ifdef carico un file header piuttosto che un altro e posso anche definire gli stessi nomi mnemonici ma con valori diversi (_XTAL_FREQ). Nella gestione delle configurazioni è anche possibile includere/escludere files in base alla configurazione scelta, ma questo è utile soltanto nel caso in cui ci siano più files C da compilare, nel caso dei files header generici non ha utilità pratica e, nel caso siano diversi a seconda del PIC, bisogna usare il sistema che ho usato io.

Vedrete che a seconda della configurazione utilizzata, grazie alle direttive #ifdef, intere parti di codice diventano grigie proprio per indicare all’utente che quelle porzioni non verranno prese in considerazione. Giusto per fare un esempio, dato che il flag di interrupt su overflow Timer0 sui due PIC si chiama diversamente e si trova anche in due registri diversi, nella ISR troverete:

260
261
262
263
264
265
266
267
268
269
270
271
#ifdef _16F887
    if (INTCONbits.T0IF) // L'interrupt è stato causato da un overflow del timer0 ?
        {
        // Ricarico subito il Timer0
        TMR0=102; // 100+2
#endif
#ifdef _16F18877
    if (PIR0bits.TMR0IF) // L'interrupt è stato causato da un overflow del timer0 ?
        {
        // Ricarico subito il Timer0
        TMR0L=8; //6+2
#endif

Come notate ho inserito nelle zone #ifdef/#endif anche la parentesi graffa aperta dell’if, che poi verrà chiusa un’unica volta verso la fine della ISR, proprio perchè il compilatore prenderà in pasto o l’uno o l’altro pezzo di codice.

Ricordo che tutto quello che inizia con # nel codice scritto in C appartiene al gruppo delle direttive preprocessore: il preprocessore è una parte della toolchain di sviluppo che ha il compito di analizzare il sorgente e sostituire parti di codice prima di passare il tutto al compilatore. Giusto per capirci meglio: anche quando scriviamo i nomi mnemonici, questi vengono sostituiti dal preprocessore con le relative aree di memoria del PIC utilizzate proprio perchè il compilatore non conosce nomi ma solo numeri. Le direttive sono quindi dei comandi espliciti che forniamo a questo utile intermediario che ci facilita di molto il lavoro di programmazione!

Funzionamento del programma

Come nella vecchia lezione viene sfruttato il Timer0 per generare un interrupt ogni mS. Sul PIC16F18877 il Timer0 può operare anche in modalità 16bit e ha numerose opzioni in più: non utilizzo le nuove features, anche perchè non mi servono in questa applicazione e utilizzo il Timer in modalità 8bit.

Tutto il cronometro ruota intorno ad un counter. Il counter è un numero intero a 16 bit che nella fattispecie viene utilizzato per contenere un numero da 0 a 9999. La cifra più a destra (il digit 4, che nel sorgente indico come “unità”) viene incrementata ogni 10mS, la cifra successiva (il digit 3, che indico come “decine”) scatta quindi ogni (10×10=100mS) e così via, per cui i due numeri più a destra indicano i centesimi di secondo, quelli a sinistra i secondi.

Ho preferito chiamare, nel codice, i digit come unità, decine, centinaia e migliaia perchè lo stesso codice può essere facilmente adattato per realizzare un contapezzi, un elimina-code ecc. Poi, voglio dire, si tratta unicamente di dare dei nomi, voi potete chiamarli come vi pare!

Ad ogni interrupt (ogni millisecondo) viene pertanto:

  • Gestito il contatore del Multiplex (muxcount) che seleziona il digit (1,2,3,4) da accendere (e che nel sorgente ho indicato rispettivamente come migliaia, centinaia, decine, unità) cambiando il valore della variabile actdisplay ogni 4 interrupt. Ogni digit rimane acceso per 4mS e di conseguenza spento per altri 12mS (62.5Hz). Questa è la parte più onerosa per il PIC dato che deve eseguire delle divisioni per estrarre la singola cifra dal counter per poterla mostrare sul relativo digit del display.
  • Incrementato il counter principale counter (quello che serve a visualizzare il tempo sui display), che aumenta di una unità ogni 10mS, conteggiati dalla variabile counterinc, incrementata ogni interrupt.
  • Incrementato un counter (dotsCounter) che gestisce il lampeggio dei due punti ogni 500mS, solo in modalità run

Questo è quello che possiamo visualizzare sull’analizzatore logico collegato sui 4 anodi del display:

I tempi come vedete sono più o meno rispettati. Ricordo che le cifre si attivano fornendo GND sugli anodi comuni (e quindi Vdd sulla base del relativo transistor PNP che li pilota), vedete che i livelli bassi (digit acceso) durano circa 4mS a cui segue una pausa di 12mS data dalla somma dei tempi di accensione dei rimanenti 3 digits.

Ho fatto delle prove ad 8MHz riconfigurando opportunamente le opzioni del timer per avere sempre l’interrupt ogni ms ma i tempi si dilatavano di molto: segno che con tale frequenza, nel tempo di 1mS, la CPU non riesce ad eseguire tutti i calcoli, per cui, per questo circuito, se avete il PIC16F887 non è possibile utilizzare l’oscillatore interno dato che è a 8MHz, ed è quindi necessario il quarzo esterno da 20MHz (che è la velocità massima supportata da quel PIC), nessun problema per il PIC16F18877 usando l’oscillatore interno settato a 32MHz.

Riguardo all’utilizzo dell’analizzatore logico segnalo una cosa curiosa che mi è capitata con RE2 sul PIC16F18877: collegando un canale qualsiasi dell’analizzatore logico a tale porta, la cifra relativa ha dei comportamenti anomali, come se risentisse dell’impedenza dello strumento che rende l’uscita instabile, cosa che in realtà non dovrebbe accadere. Ho provato con altri PIC sulla stessa scheda e non ho avuto nessun problema. Il problema lo da unicamente quel PIC e solo su quella porta, qualsiasi sia l’oscillatore selezionato (sia interno che esterno). Se qualcuno ha suggerimenti/idee sul perchè di questo comportamento, può scriverlo nei commenti.

In realtà, per un orologio/un cronometro, si potevano gestire le cose diversamente, in maniera più efficiente e meno onerosa in termini di calcoli della CPU (e che poteva quindi funzionare senza problemi anche con l’oscillatore a 8MHz), impostando due counter separati per i centesimi e per i secondi: le operazioni che difatti richiedono più cicli macchina sono le divisioni e i moduli, necessari in questa applicazione per estrarre le singole cifre dal counter. Io ho lasciato un unico counter da 0 a 9999, accontentandomi di usare una maggiore frequenza di clock, perchè il progetto originale era un contatore up/down.

Nel caso vorreste realizzare un orologio è quella appena detta la soluzione da utilizzare: due variabili separate per le ore e per i minuti, per fare in modo che quando la variabile che conteggia i minuti arriva a 60, incrementi la variabile che contiene l’ora. Ad ogni modo non è consigliabile utilizzare l’oscillatore principale per realizzare un orologio: meglio utilizzare un quarzo da 32.768KHz usato sull’oscillatore secondario in abbinamento al Timer1. Per questo ho già realizzato due articoli in passato: utilizzare il Timer1 come base dei tempi per realizzare orologi e Simple Clock: un semplice orologio con PICmicro a 18pin e display a 7 segmenti.

I due puntini di separazione vengono pilotati a parte dato che viene gestito il lampeggio: rimangono accesi 500mS e spenti altrettanto in maniera tale che ogni volta che si accendono, scatta un secondo. Quando si mette in pausa il cronometro (premendo di nuovo il tasto start), i due punti rimangono fissi. Viene memorizzato lo stato precedente dei due punti per non perdere la sincronizzazione quando si riparte dato che sono indipendenti dal counter.

Questo è il risultato:

Downloads

Se questo articolo ti è piaciuto, condividilo su un social:
Se l'articolo ti è piaciuto o ti è stato utile, potresti dedicare un minuto a leggere questa pagina, dove ho elencato alcune cose che potrebbero farmi contento? Grazie :)