Corso programmazione PICMicro in C – Lezione 7 (parte 2/3) – Interfaccia con LCD – Hello World + semplice menu sul display per attivare delle funzioni
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.
Dopo aver visto tutta (o quasi) la teoria che c’è alla base del pilotaggio dei display LCD basati sul controller Hitachi HD44780 (o compatibile) eccoci qui pronti a scrivere qualche programmino di esempio da caricare sul nostro picmicro.
In questa lezione sfrutteremo una libreria per il pilotaggio degli LCD scritta dal sig. Andrea Bonzini e disponibile anche sul sito di Sergio Fiocco.
Tale libreria prevede il controllo dell’LCD in modalità 4 bit e in sola lettura (il pin RW, difatti, andrà collegato a massa e non sarà pilotabile via software). Ho apportato delle piccole modifiche a tale libreria in maniera da avere disponibili anche altre funzioni. L’elenco delle modifiche da me apportate è descritto nel file changelog.txt allegato alla libreria, ho inoltre incluso la libreria originale.
Se spulciate nella cartella “samples” dell’installazione Hitec-C, troverete una routine simile in “LCDemo”
Passiamo a vedere come montare il circuito.
Indice dei contenuti
Schema e montaggio del circuito
Anche se in apparenza può sembrare complicato, lo schema in realtà è abbastanza semplice, non vi impressionate:
A parte le porzioni di circuito che abbiamo già visto nelle precedenti lezioni, ci sono le seguenti novità:
– I 4 led: ogni led avrà la sua resistenza da 330Ohm in serie, il terminale lungo del led andrà verso il pic (con la resistenza in serie), il terminale corto verso massa. Il led contrassegnato come LED1 andrà collegato ad RB7 fino ad arrivare a LED4 collegato a RB4.
– I 3 pulsanti: sono contrassegnati sullo schema come BTNOK (collegato a RB2), BTNDOWN (collegato a RB1) e BTNUP (collegato a RB0). I 3 pin del picmicro ai quali sono collegati i pulsanti sono costantantemente alimentati ognuno da una propria resistenza di pullup da 1KOhm. Premendo il pulsante, il pin relativo viene a trovarsi a massa, quindi intercetteremo via software la pressione del pulsante verificando che il pin relativo sia a livello logico basso.
Se si abilitano le resistenze di pullup sulle porte B, non è necessario includere le resistenze da 1KOhm.
– Cicalino: collegato su RE1. Per questa prova è assolutamente necessario utilizzare un cicalino non autooscillante dal momento che genereremo un diverso tono per ogni funzione. La resistenza in serie al cicalino può anche essere omessa.
– Il display LCD. Controllate bene la piedinatura del display che avete in vostro possesso. Il terminale GND va a massa, il terminale Vcc va ai 5 volt positivi, il terminale V0 (contrasto) va collegato al cursore (pin centrale) di un trimmer da 10KOhm, le altre due estremità del trimmer vanno una ai 5 volt e l’altra a massa.
Il terminale dell’LCD contrassegnato come RS (Register Select) lo collegheremo sul pin 21 del 16F877 (RD2).
Il terminare dell’LCD E o EN (Enable) va al pin 22 (RD3)
I terminali da D4 a D7 vanno rispettivamente collegati ai pin da 27 a 30 (da RD4 a RD7)
Se vogliamo, possiamo collegare all’alimentazione i due pin del display destinati alla retroilluminazione.
I rimanenti pin del display vanno posti a massa.
Se utilizzate la scheda Freedom di Mauro Laurenti, è disponibile il connettore LCD, i quali pin portano già alle porte del picmicro qui definite.
La libreria per il pilotaggio dell’LCD
La libreria (lcd.c) è scaricabile in fondo all’articolo insieme ai programmi presentati in queste pagine, è caldamente raccomandata la lettura del codice sorgente da effettuarsi confrontandola con la prima parte di questa lezione: solo così potete capire perfettamente tutta la tecnica che c’è alla base.
Tale libreria comprende numerose utili funzioni, basterà semplicemente includerla nel nostro programma nel modo consueto:
#include "lcd.c" |
Tale libreria ha bisogno delle routine Delay per poter funzionare, tali routine sono comunque incluse nel download e sono le stesse presenti nelle lezioni precedenti.
Riassumo qui le funzioni disponibili:
LCD_INIT();
Serve ad effettuare l’inizializzazione del display come detto nella parte precedente, va richiamata una solta volta e prima di qualsiasi altra funzione, altrimenti il display non sarà in grado di operare.
LCD_CLEAR();
Effettua la pulizia del display, ovvero cancella il contenuto della DDRAM e porta il cursore nella prima posizione (riga 1, colonna 1).
LCD_CMD(char);
Invia un comando al display, tale funzione viene utilizzata, ad esempio da LCD_CLEAR e da altre, è anche possibile inviare comandi personalizzati, come ad esempio i codici numerici associati ai vari entry mode, sarà comunque una funzione molto poco sfruttata direttamente.
LCD_GOTO(riga,colonna);
Posiziona il cursore per la scrittura alla riga e alla colonna specificata. La numerazione è in base 1, per cui scrivendo:
LCD_GOTO(1,1); |
Il cursore si posizionerà alla prima riga, prima colonna.
LCD_PUTCH(char);
Scrive un unico carattere sul display, char ovviamente può essere immesso in qualsiasi formato numerico (decimale, binario -anteponendo come sempre 0b-, esadecimale -anteponendo 0x- o formato carattere -scrivendo il carattere tra singoli apici ”-), è utile per scrivere quei caratteri che non hanno corrispondenza del codice ascii sulla tastiera, come specificato nella precedente parte della lezione: basta avere sottomano la tabella con l’elenco dei codici carattere del controller che abbiamo a disposizione sull’LCD. Esempio:
LCD_PUTCH(0b11110100); //scrive il simbolo omega |
LCD_PUTS(“stringa”);
Scrive una stringa intera sul display. Se ci posizioniamo alla prima colonna della prima riga, e abbiamo a disposizione un LCD 16×2 e in tale stringa scriviamo una parola superiore ai 16 caratteri, ovviamente i caratteri dal diciassettesimo in poi non saranno visibili, per cui stiamo attenti quando utilizziamo questa funzione se non vogliamo avere effetti indesiderati sul display. Esempio:
LCD_PUTS("Settorezero.com"); |
LCD_PUTUN(numero);
Scrive un numero privo di segno sul display, come numero sono accettati il formato CHAR (8bit) o INT (16 bit), per cui sono ammessi numeri interi da 0 fino a 65535.
LCD_PUTSN(numero);
Scrive un numero compreso di segno. Per numeri positivi il segno non viene rappresentato, per numeri negativi, viene anteposto il segno meno davanti al numero. Supporta sia CHAR che INT in ingresso, ovviamente accettando un numero “signed” i valori ammissibili vanno da -32768 a 32767.
LCD_CUSTOMCHAR(pos,byte1…byte8);
Permette la personalizzazione del carattere in posizione “pos”. Vedremo questa funzione nei dettagli nella terza parte di questa lezione, che sarà dedicata soltanto alla personalizzazione dei caratteri.
Il file lcd.c va impostato prima di poter essere utilizzato. Innanzitutto apriamolo e andiamo alla riga 94:
94 95 96 97 98 99 | #define LCD_RS RD2 // Register select #define LCD_EN RD3 // Enable #define LCD_D4 RD4 // LCD data 4 #define LCD_D5 RD5 // LCD data 5 #define LCD_D6 RD6 // LCD data 6 #define LCD_D7 RD7 // LCD data 7 |
Qui impostiamo le linee dati. Se seguite lo schema elettrico proposto in cima alla lezione, vedete che i pin del display (indicati come LCD_XX) sono già associati ai rispettivi pin del picmicro.
Successivamente dalla riga 110 fino alla riga 115 incontriamo altri parametri da impostare:
110 | #define LCD_ROWS 2 // valid numbers are: 1,2 (set to 2 for 2 or more rows) |
Qui va impostato il numero di righe che ha il nostro display, per le nostre prove terremo conto di avere un display da 2 righe, se avete un display da 4 righe dovete comunque lasciare il valore 2, se avete un display da una riga dovrete cambiare il 2 con l’1, ma poi dovrete anche modificarvi i programmi mostrati in questa lezione.
111 | #define LCD_COLS 16 // valid numbers are: 8,16,20 |
numero di colonne dell’LCD, qui utilizzo un display da 16 colonne, se avete un display da 20 colonne, modificate il numero, se avete un display da 8 colonne, oltre al numero dovrete modificarvi anche i programmi.
112 | #define LCD_CURSOR_VISIBLE 0 // 1:cursor on 0:cursor off |
se mettiamo 0, il cursore non sarà visibile, se mettiamo 1, potremo vedere il cursore visualizzato sul display, il quale si presenterà come una lineetta in basso ai caratteri.
113 | #define LCD_CURSOR_BLINK 0 // 1:cursor blinks 0:cursor doesn't blinks |
se mettiamo 0, il cursore rimarrà fisso, impostando ad 1 il cursore lampeggerà
114 | #define LCD_TYPEWRITE 0 // 1:delay between characters |
se mettiamo 0, la scrittura sul display sarà immediata, impostando ad 1, ci sarà un breve ritardo tra la scrittura di un carattere e l’altro, dando un effetto “macchina da scrivere”.
115 | #define LCD_TW_DELAY 50 // milliseconds between characters |
questo rapprenta il ritardo espresso in millisecondi tra la scrittura di un carattere e il successivo, se abilitiamo il settaggio precedente.
Gli ultimi 4 settaggi, relativi per lo più ad effetti grafici, sono già impostati ai valori più adatti per gli esempi, difatti avere la scrittura rallentata, col cursore, in un menù può essere molto fastidioso.
Impostati questi parametri, non abbiamo bisogno più di niente per far funzionare il display (a parte richiamare la funzione LCD_INIT prima di usarlo).
In questa lezione farò 2 esempi di programma: il primo, molto semplice, è il classico “HELLO WORLD!” che non poteva mancare, serve soltanto a farvi capire in maniera molto elementare come scrivere sul display, ed è un tutorial che si trova un po dappertutto, pertanto in questo esempio non faremo uso ne dei led, ne dei pulsanti ne del cicalino. Il secondo esempio, un po’ più elaborato è un semplice menù mostrato sul display, nel quale potremo navigare con i pulsanti e attivare/disattivare le 4 uscite alle quali sono collegati i led, visualizzandone lo stato sul display.
Hello World!
Il file settings è molto semplice, abbiamo soltanto impostato tutte le porte del pic come uscita.
Nel file main.c, dopo la config word, abbiamo le 3 inclusioni:
#include "settings.h" #include "delay.c" #include "lcd.c" |
Come già detto in precedenza, è necessario includere prima delay.c e poi lcd.c, in quanto lcd.c fa uso delle funzioni delay.
Abbiamo quindi il main:
void main(void) { settings(); // imposto le porte LCD_INIT(); // inizializzo l'LCD DelayMs(100); // piccolo ritardo iniziale LCD_CLEAR(); // ripulisco il display LCD_GOTO(1,1); // posiziono il cursore sull'LCD: riga1, colonna1 LCD_PUTS("HELLO"); LCD_GOTO(2,1); // riga2, colonna1 LCD_PUTS("WORLD !"); while(1) { } } |
Come vedete, dopo aver eseguito le impostazioni, inizializzamo il display come detto prima con l’apposita funzione, personalmente dopo l’inizializzazione do sempre un piccolo ritardo per stabilizzare le linee di comunicazione, in realtà non ce ne sarebbe bisogno.
Effettuo quindi la pulizia del display con LCD_CLEAR(), mi posiziono alla prima riga, prima colonna e scrivo “HELLO”, mi posiziono quindi alla seconda riga, prima colonna e scrivo “WORLD !”, effettuo quindi il ciclo continuo, vuoto, per non far bloccare il pic. Il risultato è il seguente:
Avendo impostato il cursore lampeggiante e l’effetto macchina da scrivere, vedrete il cursore muoversi mentre compone la scritta.
Come vedete, utilizzando una apposita routine, scrivere su un LCD è un gioco da ragazzi: bastano poche righe di codice per avere una gran bella soddisfazione!
Spero non vi siate accontentati di questo semplice esempio, preparatevi perchè vi attende qualcosa di molto più complicato (ma non troppo).
In questo programma, controlleremo lo stato di 4 uscite del picmicro dal display: due tasti (BTNUP e BTNDOWN) ci permetteranno di muoverci su e giù nel menù, un terzo tasto (BTNOK) ci permetterà di cambiare lo stato dell’ uscita selezionata. Lo stato dell’uscita sarà segnalato sul display (e comunque evidenziato anche dall’accensione/spegnimento del led ad essa collegato: tenete conto che al posto dei led potete anche montare dei relè come spiegato nelle precedenti lezioni). Premendo i tasti di navigazione, sarà udibile un suono prodotto dal cicalino, la pressione del tasto OK, invece, produrrà un suono di tipo diverso.
Il menù avrà questo aspetto:
In pratica con i tasti su e giù spostiamo la freccia, posizionandola affiando alla voce desiderata (L1 sta per LED1), premendo OK, cambiamo lo stato dell’uscita selezionata, la scritta affianco quindi si aggiornerà riflettendo lo stato dell’uscita (ON se livello logico alto e quindi relativo led acceso).
Analizziamo prima il file di configurazione, settings.h:
#define BTNUP RB0 #define BTNDOWN RB1 #define BTNOK RB2 #define LED1 RB7 #define LED2 RB6 #define LED3 RB5 #define LED4 RB4 #define PRESSED 0 // per verifica pulsante premuto #define ARROW 0x7E // codice del carattere freccia #define ON 1 #define OFF 0 #define CICALINO RE1 |
Qui ci sono i vari define, abbiamo soltanto dato dei nomi più semplici da ricordare alle varie uscite/ingressi.
Ho definito un simbolo PRESSED che utilizzerò per fare la verifica del pulsante premuto, avrei difatti potuto fare come sempre:
if (!BTNOK) // oppure if(BTNOK==0) { // istruzioni } |
Ma qui espongo invece un metodo alternativo, che può aiutare a capire meglio:
if (BTNOK==PRESSED) { // istruzioni } |
avendo definito PRESSED come zero, la sostanza non cambia, ma forse, scritto così, il programma è più semplice da leggere.
C’è quindi un simbolo “ARROW” che corrisponde al codice carattere della freccia rivolta verso destra sul controller HD44780 (vedete la tabella caratteri nella prima parte di questa lezione), ho espresso il codice in esadecimale (come vedete infatti il numero è preceduto da 0x).
Ho quindi definito due simboli ON e OFF, difatti per alcuni il codice è più leggibile se scriviamo:
LED1=ON; // anzichè LED1=1; |
Segue quindi il define del cicalino. Proseguendo troviamo:
signed char menupos=0; // variabile che tiene conto della posizione della freccia nel menù |
Questa variabile la utilizzo per tenere in memoria la posizione attuale della freccia che utilizzo per navigare nel menù: in alto a sinistra (posizione L1) questa variabile varrà zero, fino ad arrivare al valore 3 nell’ultima posizione (4 posizioni in totale). Le do l’attributo signed perchè può accadere che assuma anche valore negativo.
Segue quindi la funzione settings che non ha nulla di particolare: tutte le porte impostate come uscita, tranne quelle destinate ad alloggiare i pulsanti. I led e il cicalino vengono posti a livello logico basso all’accensione.
La routine lcd.c allegata al progetto (scaricabile insieme al resto in fondo all’articolo per gli utenti iscritti) è configurata in maniera tale da non visualizzare il cursore e da fornire una scrittura diretta (senza ritardi).
Analizziamo quindi il main.c, notiamo qualcosa che fino ad ora non avevamo utilizzato, ovvero i prototipi di funzione:
// prototipi di funzione void main(void); // programma principale void beep(char beeptype); // esegue il beep, usato quando premo i tasti void posizionafreccia(char posizione); // posiziona la freccia nel menù void cambiastato(char posizione); // inverte lo stato del led void scrivistato(char posizione,char stato); // scrive lo stato del led sul display |
Come vedete, un prototipo di funzione è la funzione scritta normalmente, ma seguita dal punto e virgola anzichè dalle parentesi graffe contenenti le routine da eseguire (una funzione definita, ma vuota). Questo viene fatto per avvisare il compilatore delle funzioni (che verranno poi definite realmente), in maniera tale da non generare errori se incontra un simbolo che non è stato precedentemente dichiarato, è la stessa cosa che si fa con le variabili: così come dichiaramo una variabile scrivendo:
signed char menupos=0; |
Così dichiaramo una funzione, dicendo anche se restituisce o meno un valore e se accetta o meno parametri in ingresso
Ricordo ancora una volta che il void indica che non ci sono valori: il void davanti indica che la funzione non ritorna valori, il void tra parentesi indica che la funzione non prevede argomenti da elaborare.
Nel main prepariamo quindi il display, in maniera da farlo apparire come l’immagine presentata più in alto.
Nel ciclo infinito controlliamo unicamente la pressione dei 3 pulsanti. Ogni pulsante è dotato di antirimbalzo software come spiegato nelle precedenti lezioni. La pressione del tasto BTNUP (SU) decrementa la variabile menupos (che come abbiamo detto serve per tener traccia di dove si trova la freccia: un indice, in pratica, che ci permette di ricordarci in tutto il programma, in quale punto del menù ci troviamo), se il valore di questa variabile diviene minore di zero, allora le assegno il valore 3, in maniera tale che se ci troviamo in posizione 0 e premiamo ancora SU, la freccia verrà a trovarsi in posizione 3 (ultima posizione):
// pressione tasto SU // decremento il contatore che mi posiziona la freccia if (BTNUP==PRESSED) { DelayMs(100); if (BTNUP==PRESSED) { beep(0); // suono menupos--; // decremento la posizione // se in posizione 0 ho premuto su // vado in posizione 3 if (menupos<0) { menupos=3; } // posiziono quindi la freccia posizionafreccia(menupos); } } // fine pressione tasto su |
Per chi non lo ricordasse: la seconda verifica della pressione del pulsante, dopo un ritardo, costituisce l’antirimbalzo software come spiegato in una lezione precedente.
All’atto della pressione, richiamo la funzione beep per emettere il suono. La funzione beep accetta in ingresso un valore numerico (da 0 a 2) in maniera tale da emettere un suono diverso (una diversa frequenza) in base al valore che passiamo:
void beep(char beeptype) { char F; switch (beeptype) { case 0: F=70; break; case 1: F=90; break; case 2: F=110; break; } for (char a=40; a--; a!=0) { DelayUs(F); CICALINO=ON; DelayUs(F); CICALINO=OFF; } } |
Questa funzione in pratica è uguale a quella vista nelle precedenti lezioni con due sostanziali differenze: il ritardo necessario a generare l’onda quadra è variabile e dipendente dall’argomento passato alla funzione, e inoltre viene eseguito un ciclo di 40 ripetizioni, ma anzichè andare da 0 a 40 come normalmente è spontaneo scrivere, il ciclo viene eseguito al contrario: da 40 verso zero. La sostanza non cambia, ma scritto in questo modo ci permette di rendere il ciclo più efficiente come spiegato in questo articolo su microchipc.
Le funzioni svolte dal tasto BTNDOWN sono pressochè identiche ma eseguite al contrario: il contatore verrà incrementato, se supera il valore di 3, gli verrà dato il valore zero in maniera tale da far ricomparire la freccia in prima posizione.
La pressione di questi due pulsanti (BTNUP e BTNDOWN), richiama la funzione “posizionafreccia” la quale accetta in ingresso il valore di posizione attuale. Tale routine esegue 3 funzioni principali. Prima di tutto cancella (scrive uno spazio) nelle 4 posizioni in cui deve apparire la freccia, in maniera tale da cancellare il carattere freccia precedente:
LCD_GOTO(1,1); LCD_PUTCH(32); // 32=codice dello spazio LCD_GOTO(2,1); LCD_PUTCH(32); LCD_GOTO(1,10); LCD_PUTCH(32); LCD_GOTO(2,10); LCD_PUTCH(32); |
Facile capire che quei LCD_GOTO puntano alle posizioni dove le frecce devono comparire:
Successivamente si fa utilizzo di un costrutto switch (vedi manuale Tricky C per sapere come si usa) per analizzare la posizione attuale (passata alla funzione): in base al numero di posizione, viene piazzato il cursore nella “casella” giusta del display:
switch (posizione) { case 0: LCD_GOTO(1,1); break; case 1: LCD_GOTO(2,1); break; case 2: LCD_GOTO(1,10); break; case 3: LCD_GOTO(2,10); break; } |
Infine, viene stampato il carattere della freccia:
LCD_PUTCH(ARROW); |
Ricordo che abbiamo definito il valore di “ARROW” in settings.h. Come vedete utilizzo la funzione LCD_PUTCH che mi permette di scrivere un solo carattere.
Spero che fin qui sia tutto chiaro: premiamo i pulsanti su e giù e quindi posizioniamo la freccia sul display. Andiamo avanti.
La pressione del tasto BTNOK, produce un beep differente e richiama un’altra routine: cambiastato, alla quale viene sempre passato il valore di posizione attuale. Tale routine definisce innanzitutto una variabile:
char STATO_LED; |
la quale serve per tener conto dello stato attuale dell’uscita dopo il cambio. Viene quindi effettuato uno switch in base alla posizione nel menù, in maniera tale da invertire lo stato dell’uscita relativa:
switch (posizione) { case 0: LED1=LED1^1; STATO_LED=LED1; break; case 1: LED2=LED2^1; STATO_LED=LED2; break; case 2: LED3=LED3^1; STATO_LED=LED3; break; case 3: LED4=LED4^1; STATO_LED=LED4; break; } |
La variabile STATO_LED, come vedete, memorizza lo stato attuale dell’uscita (del led) che abbiamo selezionato. Infine viene richiamata l’ultima funzione:
scrivistato(posizione,STATO_LED); |
la quale ha la funzione di scrivere ON oppure OFF nella posizione relativa sul display: per fare questo dovremo sempre passare la posizione in cui ci troviamo e quindi anche lo stato dell’uscita. Quest’ultima funzione si articola nella stessa maniera delle altre, tranne che per la posizione in cui andare a scrivere. Da notare che ON sarà scritto con uno spazio finale, in maniera tale da cancellare l’ultima lettera (F) di OFF, se tralasciamo lo spazio, apparirà sul display una cosa del genere: L1:ONF
Downloads
Il file zip contiene lo schema elettrico, la routine lcd.c e i codici sorgente e compilati dei due programmi di esempio qui esposti, per ognuno dei due programmi c’è la copia della routine lcd.c già impostata, ho inoltre incluso il foglio excel che ho utilizzato io per disegnare come appare il display, che può essere utile per fare i vostri progetti di menù e ricavare quindi la posizione del cursore.
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.