ESERCITAZIONE # 12
Costruiamo un driver per gli LCD a
caratteri
Disporre di un LCD a una o più linee di caratteri è molto comodo in tanti
progetti: termometri, orologi, strumenti di misura, terminali e simili non ne possono fare a meno.
|
Sono semplici da reperire, sono economici, offrono un ampio spettro di formati,
da 8 a 80 caratteri, da una a quattro righe, in vari colori e dimensioni.
Però il loro impiego richiede che il programma del microcontroller disponga
delle istruzioni necessarie alla gestione di questi display. |
Per utilizzarli ci sono due vie:
- scrivere in ogni programma le istruzioni necessarie
- crearsi una libreria da utilizzare in ogni occasione
La prima soluzione è, purtroppo, quella impiegata da molti, ma è la via
meno sensata. La seconda è meno comune, ma è l'unico modo per non dover
replicare ogni volta le stesse cose già scritte, con il rischio di fare errori.
Una libreria è un blocco di istruzioni che, scritte una volta per tutte,
permettono, richiamandole, di sfruttare immediatamente le funzioni per cui sono
state scritte.
Qualsiasi linguaggio ad alto livello è basato su librerie, senza le quali non
avrebbe alcuna possibilità di essere usato in modo pratico.
Ma anche per l' Assembly è semplice crearsi librerie per qualsiasi scopo.
In questo caso vogliamo realizzare una semplice libreria che ci permetta di
inserire una gestione degli LCD a caratteri in qualsiasi nostro progetto.
Qui vogliamo trattare i display a caratteri del tipo più diffuso, cioè
quelli basati sul controller Hitachi HD44780 e compatibili. Una scorsa al
foglio dati e a qualche tutorial
ci consente di verificare che, anche in questo caso, si tratta sempre di una semplice gestione di segnali digitali di I/O.
Dunque, una libreria per il display sarà costituita dalle routines necessarie a
governare il controller HD44780.
|
Iniziamo la nostra esperienza con uno dei modelli più comuni, il
formato 2 linee da 16 caratteri.
La cosa ovviamente funziona anche per qualsiasi altro formato (escluso i
4 x 40 che richiede qualche nota aggiuntiva che faremo in seguito).
Quindi se avete per le mani un display diverso da 2x16, o anche un 2x20,
va bene
ugualmente, basta che il controller, per ora, sia HD44780 compatibile.
|
|
Dal punto di vista dei collegamenti con il PIC, iniziamo ad utilizzare
l' interfaccia parallela completa, ovvero:
- un bus da 8 bit, utilizzando un intero port
- tre linee di controllo, utilizzando tre pin di un' altro port
Ovviamente sarà necessario aggiungere la tensione di alimentazione,
quella di contrasto e la retro illuminazione, ma queste voci non fanno
parte del driver vero e proprio, non essendo dipendenti dal controller
HD44780.
Si tratta ora di esaminare quali sono le funzioni base del controller
e come gestirle.
|
Scrivere un driver : scrittura verso il modulo LCD
Vediamo con questa esercitazione una via per scrivere un driver funzionale.
In quest' ottica, la descrizione procederà nel modo più dettagliato possibile,
per far comprendere come, a partire dall' analisi delle caratteristiche della
periferica, si possa arrivare alla scrittura di un firmware di discreta qualità
senza grosse difficoltà. L' importante è procedere con metodo.
Se osserviamo il foglio dati del componente, la documentazione originale di
Hitachi e le nostre
pagine sull' argomento, possiamo rilevare facilmente che le funzioni
essenziali sono essenzialmente due:
- scrivere verso il controller
- leggere dal controller
La prima operazione, scrittura, permette di inviare al display comandi e
dati. Sempre le documentazioni sopra ricordate ci permettono con facilità
di scrivere una sintetica descrizione di come funziona questa fase:
- impostare il bit di controllo RW = 0 per indicare che si tratta di una
scrittura
- impostare il bit di controllo RS = 1 se vogliamo inviare un dato
e RS = 0 se si tratta di un comando
- porre sul bus dati il byte da trasmettere
- dare un impulso positivo sul pin di Enable
Da questa lista possiamo immediatamente tracciare un flow chart e, se
seguendo questo, trasformarlo in istruzioni.
Utilizziamo il solito approccio di richiamare la routine con il dato da
trasmettere in WREG.
|
La gestione della linea RS diventa fondamentale in quanto è il suo
valore che permette al controller del display di interpretare i dati sul
bus come comandi oppure come caratteri da visualizzare.
; scrivi comando sull'
LCD
bcf
LCD_RS ;RS=0
per i comandi
bra
lcdwr ;salta
avanti
; scrivi dato sull' LCD
bsf
LCD_RS ;RS=1
per i dati
lcdwr bcf LCD_RW
;RW=0 per scrivere
movwf
LCD_port ;byte
sul bus
bsf
LCD_E ;E=1
bcf
LCD_E ;E=0
Possiamo notare che le azioni sono semplicemente il settaggio
di alcuni bit sui port, risolti con le istruzioni bsf
e bcf.
Però, l' impulso sul pin di enable E può essere oggetto di qualche
osservazione.
Innanzitutto, richiede almeno due istruzioni e, in secondo luogo, dai
fogli dati si evince che questo impulso deve avere un minimo tempo di
esecuzione. In ultimo, questo impulso deve essere ripetuto in modo
identico per ogni operazione di scrittura.
Per queste ragioni, nel flowchart a lato lo abbiamo rappresentato
con un blocco che indica la sua natura di processo per definito, ovvero
di una
subroutine o di una macro. |
Il definire la procedura dell' impulso su E come un blocco separato ha la
funzione primaria di creare un elemento indipendente che potrà essere variato a
piacere senza modificare il resto del driver.
E dove sono necessarie queste modifiche? Sono necessarie per rispettare la
durata dell' impulso secondo quanto specificato dal foglio dati del controller
HD44780U e della gran parte dei compatibili, ovvero 220-250 ns.
Abbiamo scritto il tratto di programma che effettua l' impulso
sul pin E nella forma più semplice, alzando ed abbassando il livello logico del
bit. Ma quanto dura questo impulso? In questa forma la sua durata è quella di
un ciclo di istruzione, essendo bsf
/ bcf a singolo
ciclo; e il ciclo ha una durata dipendente dalla frequenza del clock del
microcontroller.
Quindi, con il variare del clock, varierà anche la durata dell' impulso su E
;
impulso positivo su E |
durata ciclo istruzione
a seconda del clock |
4 MHz |
8 MHz |
20 MHz |
40 MHz |
bsf
E_pin ; E = 1 |
1 us |
.5 us |
200 ns |
100 ns |
bcf
E_pin ; E = 0 |
1 us |
.5 us |
200 ns |
100 ns |
... |
|
|
|
|
durata impulso |
1 us |
0.5 us |
200 ns |
100 ns |
Possiamo subito notare che, se a 4 MHz di clock il tempo minimo
di 1 us per l' impulso di E è rispettato, non lo è quando il clock va oltre.
Questo è un punto solitamente del tutto non considerato da molti autori sul
WEB, in quanto si sovrappongono due fatti.
Per primo, purtroppo, ben pochi leggono compiutamente i fogli dati prima di
buttarsi a scrivere qualcosa.
In secondo luogo, in varie prove fatte, il tempo di 1 us può essere facilmente
ridotto in quanto i controller hanno tempi di assestamento migliori e,
importante, il controller accetta il dato sul fronte di discesa dell' impulso.
Ma è evidente che la sua durata, a partire dal momento in cui si forma il
fronte di salita, deve servire al controller per "accorgersi" che
dovrà caricare il byte sul bus. E questo dipende da controller a controller o
anche solamente dal loro clock interno, che può avere un certo ambito di
variazione.
E, in effetti, alcuni display si sono verificati essere piuttosto
"duri" e richiedere più tempo di altri. E questo non è rilevabile se
non con prove dirette.
Quindi, anche se i controller
possono avere caratteristiche solitamente migliori di quelle indicate
nel foglio dati, non si vede la necessità di rischiare che il
comando o il dato possano essere presi malamente a causa di una
temporizzazione scorretta. |
Inoltre va considerato che spesso tra il display e l' I/O del
microcontroller è interposto un cavo, la cui lunghezza è inversamente
proporzionale alla qualità del segnale, a causa delle capacità e delle
induttanze parassite, deformando i fronti di salita e di discesa.
Quindi sarà necessario aggiungere nop
in quantità tale da rendere sicura la scrittura alla frequenza utilizzata. Ad
esempio:
< 8 MHz |
20 MHz |
40 MHz |
bsf
LCD_E
bcf LCD_E |
bsf
LCD_E
nop
bcf LCD_E |
bsf
LCD_E
nop
nop
bcf LCD_E |
In questo senso si potrà adattare il blocco di formazione dell' impulso su E
ad ogni possibile clock del microcontroller. Più avanti vedremo come è
possibile automatizzare anche questa funzione sfruttando le funzioni di MPASM.
Per ora è importante afferrare che, nella realizzazione di un driver di comando
di una periferica, non basta individuare le necessità software, ma occorre
anche verificare se quanto scritto è adeguato per le necessità hardware della
periferica. Altrimenti è probabile che quella che pare una impeccabile routine che svolge
esattamente le operazioni richieste, non funzioni o, peggio,
funzioni malamente, solamente perchè non ci si è preoccupati di rispettare le
temporizzazioni che il componente richiede.
Dunque l' impulso su E prende forma di un processo pre definito e questo può
essere espresso come una macro o una subroutine. Dato che l' impulso su E è
costituito da poche istruzioni e fa parte di un blocco che verrà richiamato
nella libreria come una subroutine, è probabilmente più sensato definirlo come
macro.
Quindi posiamo aggiungere al blocco il sotto blocco di impulso. Ad esempio, per
clock fino 8 MHz circa
;**********************************************************
; LCD_clk
; Impulso di clock positivo sul pin EN per accettare dati
; o comandi
;**********************************************************
LCD_clk macro ; impulso positivo su E
bsf
LCD_E ; E=1
nop
; insert nop
nop
bcf
LCD_E ; E=0
nop
endm
; fine macro
|
Aggiungiamo un ulteriore nop dopo il ritorno a 0 di
E per far stabilizzare i livelli dei segnali, cosa che di solito si rende
necessaria se il modulo è collegato al port con un tratto ci cavo.
Se, invece, preferiamo usare la forma della subroutine:
;**********************************************************
; LCD_clk
; Impulso di clock positivo sul pin E per accettare dati
; o comandi
;**********************************************************
; clock LCD
LCD_clk bsf LCD_E ; set E = 1
; inserire NOP per rispettare i tempi previsti dal
; costruttore
nop
; insert nop
nop
bcf LCD_E ;
E = 0
nop
return |
Da osservare che, nel caso in cui il foglio dati del modulo indichi tempi
maggiori, ad esempio è tipico 450-500 ns, occorrerà aggiungere nop
per ottenere la giusta temporizzazione.
Possiamo allora dare una forma
più compiuta a questa prima sezione, in forma di subroutines. Il dato da
scrivere sul controller è in WREG.
;**********************************************************
; LCDwrcmd - LCDwrdat
; Trasmette un dato o un comando al display:
; Dato o comando sono in WREG.
;**********************************************************
; Trasmette un comando. RS=0 RW=0. Comando in WREG
LCDwrcmd:
bcf
LCD_RS ; set flag pr invio comando
bra
lcdwr
; Trasmette un dato. RS=1 RW=0. Dato in WREG
LCDwrdat:
bsf
LCD_RS ; set flag per invio dati
lcdwr movwf LCDportw ;
emesso sul PORT
bcf
LCD_RW ; RW = 0 per scrittura
nop
; stabilizzazione livelli logici
rcall LCD_clk
; clock E
; reset LCD bus e controlli
clrf LCDportw
; clear PORT
bcf
LCD_RW ; clear RW
bcf
LCD_RS ; clear RS
return |
In questo modo, ponendo il dato
da scrivere in WREG, basterà semplicemente scrivere:
; Trasmetto un dato al display
movlw
dato
call
LCDwrdat |
e altrettanto per i comandi:
; Trasmetti un comando al display
movlw
comando
call
LCDwrcmd |
Inizializzazione port
Prima di proseguire con l' analisi della fase di lettura, possiamo prevedere
nel driver anche le istruzioni di inizializzazione dei port che comanderanno il
display.
Certamente è possibile scrivere ogni volta questa inizializzazione, ma, nell'
ottica di una programmazione strutturata, l' uso di blocchi logici pre costruiti
consente di risparmiare tempo (ed errori) nella stesura del sorgente, che, tra
l' altro, risulterà più facilmente leggibile.
Dunque, con poche istruzioni, creiamo una sub a questo scopo.
Tutte le linee di controllo sono uscite e inizialmente anche il bus viene
configurato come uscite per effettuare poi la fase di inizializzazione del
display.
Siccome viene usato un bus a 8 bit, un intero port viene dedicato a questa
connessione.
Per le linee di controllo (E, RS, RW) si potranno utilizzare, come in questo
caso, pin di uno stesso port. In tal senso si potrebbe essere tentati di
utilizzare una forma del genere:
;**********************************************************
; LCDIoIni
; Questa routine inizializza i pin di controllo dell' LCD
; bus su portx e controlli sui primi tre bit di porty
;**********************************************************
LCDIoIni:
; set I/O per LCD
movlw b'11111000' ;
primi tre bit a 0
andwf porty
; RS pin clear
clrf LCDport
; data port clear
clrf LCDtris
; set bus come uscita
movlw b'11111000' ;
primi tre bit come uscita
andwf trisy
return |
Certamente sono impiegate poche istruzioni, ma questa soluzione NON è
adeguata allo scopo che vogliamo ottenere, ovvero una generalizzazione più
ampia possibile del driver. Infatti: se i primi tre bit di porty non fossero
disponibili, ma fossero disponibili altri bit? Sarà, allora, molto meglio una
forma che permetta ai controlli di essere assegnati a pin di qualunque port; per
questa ragione si interviene con istruzioni bcf e
bsf , bit per bit, per settare singoli bit che possono appartenere a
qualsiasi port e qualsiasi tris.
;**********************************************************
; LCDIoIni
; Questa routine inizializza i pin di controllo dell' LCD
;**********************************************************
LCDIoIni:
; set I/O per LCD
bcf LCD_E
; E pin clear
bcf LCD_RS
; RS pin clear
bcf LCD_RW
; RW pin clear
bcf LCD_Bckl
; Backlight pin clear (spento)
clrf LCDport
; data port clear
clrf LCDtris
; set bus come uscita
bcf LCD_Etris
; set linee di comando come uscite
bcf LCD_RStris
bcf LCD_RWtris
return |
Anche se ha più linee di istruzione della precedente, il suo uso è molto
più pratico e "universale".
Questa funzione è scritta sotto forma di subroutine, ma potrebbe benissimo
essere una macro: anche se composta di più righe, sarà richiamata poche volte,
al limite una sola, quindi nessun
problema di occupazione di memoria programma:
;**********************************************************
; LCDIoIni
; Questa macro inizializza i pin di controllo dell' LCD
;**********************************************************
LCDIoIni macro
; set I/O per LCD
bcf LCD_E
; E pin clear
bcf LCD_RS
; RS pin clear
bcf LCD_RW
; RW pin clear
bcf LCD_Bckl
; Backlight pin clear (spento)
clrf LCDport
; data port clear
clrf LCDtris
; set bus come uscita
bcf LCD_Etris
; set linee di comando come uscite
bcf LCD_RStris
bcf LCD_RWtris
endm |
Va ricordato che, nel caso di uso come macro, la sua definizione, nel
sorgente, va messa PRIMA
dell' uso.
Lettura
dal modulo LCD
Perchè leggere qualcosa dal
modulo LCD?
Questa funzione serve a tre scopi:
- leggere il valore di AC all'
indirizzo corrente
- leggere il contenuto della
memoria (DDRAM o CGRAM) all' indirizzo corrente
- leggere lo stato del flag BF
Le prime due operazioni servono
se si sta implementando una gestione complessa del display, ad esempio per
realizzare "effetti speciali" o formattazioni particolari del testo
presentato, e, altrimenti, questa funzione non è di per se
necessaria.
Lo diventa però quando si deve verificare lo stato del flag BF, il flag di busy
che il controller
HD44780U alza
per indicare che sta elaborando il comando
precedente e non può riceverne altri.
E di questo abbiamo bisogno per gestire la nostra interfaccia
"completa", come l' abbiamo schematizzata all' inizio.
Certamente esiste una via meno "laboriosa" in cui al posto del test
sul BF si introducono dei ritardi fissi, ma questa, se più semplice, dato che
non occorre più alcuna lettura del BF, e quindi non serve implementare una
routine di lettura, ha prestazioni minori: per quanto provato molti controller
impiegano meno del tempo indicato sui fogli dati per eseguire i comandi e l'
attesa a tempo fisso, che, forzatamente, dovrà considerare la situazione
peggiore, ovvero la necessità del tempo più lungo, è penalizzante rispetto al
test su BF.
Una interfaccia a 8 bit richiede un solo passaggio di lettura o scrittura,
contro i due richiesti dall' interfaccia a 4 bit; quindi, la prima è molto più
veloce. E, avendo a disposizione pin di I/O per questa interfaccia
"veloce", non ha senso risparmiare un I/O per RW a massa e penalizzare
l' esecuzione con i tempi fissi.
Da
notare che questa considerazione è molto "purista" in
quanto i tempi di esecuzione delle operazioni sul display hanno
dimensioni ben superiori al tempo di istruzione del microcontroller: il
display LCD è una periferica "lenta" e quindi, salvare una
manciata di microsecondi è poco significativo per gran parte delle
applicazioni. Però può capitare il caso in cui si voglia spremere il
massimo di prestazione.
Ma, principalmente, è ragionevole conoscere
TUTTE le possibilità della periferica, per poter scegliere con criterio
quale via utilizzare.
|
Detto questo, vediamo come procedere alla lettura.
Anche in questo caso RS indica al controller se vogliamo leggere BF+AC oppure la
RAM.
Anche qui RS seleziona cosa si vuole leggere.
|
Quindi anche in questo caso possiamo unificare i due casi della funzione di
lettura in un unico flowchart.
Va osservato che ora dobbiamo leggere dal bus, quindi occorre che
il port relativo venga configurato come ingresso (mentre i controlli
E/RS/RW restano sempre fissati come uscite).
"In corso d'opera" una analisi sullo stato del pin RS
seleziona se si sta leggendo BF+AC o RAM.
Nel primo caso la routine è bloccante, ovvero fino a che BF non è
rilasciato, il test prosegue. Quando HD44780U rilascia BF, il byte letto
viene salvato per futuri possibili usi.
Nel caso in cui si legga la RAM, non occorrono attese.
Il bit E viene portato a 1 durante la fase di lettura, quindi ri
azzerato.
; Leggi RAM dall' LCD
LCDrdram
bsf
LCD_RS ;RS=1 per la
RAM
bra
lcdr0 ;salta
avanti
; Leggi BF + AC dall' LCD
LCDrdBF bcf LCD_RS
;RS=0 per BF+AC
lcdr0 movlw 0xFF
;data port = input
movwf LCDtris
bsf
LCD_RW ;RW=1 per
leggere
nop
lcdr1 bsf LCD_E
;E = 1 abilita display
btfsc
LCD_RS ;Ram o BF?
bra
notbusy ;RS=1, Ram
; RS=0, test busy dall' LCD
btfss
LCDportr, 7 ;se set = busy
bra
notbusy ;se = 0 busy end
nop
;aggiungere nop per
nop
;ottenere cicli da
bra lcdr1
;500 ns
notbusy movf LCDportr, w ;salva il dato letto)
bcf
LCD_E ;E = 0 - disabilita
nop
nop
return
|
Se siamo entrati nella routine da LCDrdBF
la routine resta attiva fino a che BF sia zero, quindi ritorna con il dato letto
(BF+AC) in WREG.
Se siamo entrati nella routine da LCDrdRam la
routine ritorna con il dato letto (RAM) in WREG.
Potrebbe non essere necessario
disabilitare il display e riabilitarlo ad ogni loop di test di BF, ma si è
verificato che questa pare una soluzione migliore che non lasciare E sempre a 1,
ed è quella consigliata dai fogli dati.
Il fare eseguire una serie di clic successivi all' impulso su E richiede che sia
verificato un ulteriore limite hardware imposto dal foglio dati, ovvero che ci
un tempo minimo (tcyc) tra un impulso e il successivo. Quindi la routine di lettura
aggiunge alcuni nop. Anche qui, se il tcyc è indicato a 1 us occorrerà
un aumento opportuno dei nop dove necessario.
Anche in questo caso ci si
potrebbe porre il problema se utilizzare una sub o una macro. Però qui la
considerazione da fare è che la lettura di BF dovrà essere richiamata ad ogni
operazione sul display e quindi ne è preferibile l'implementazione come
subroutine.
Alcuni
ritocchi
Se osserviamo il port del bus,
vediamo che le routines di scrittura lo devono avere configurato come uscita,
mentre quelle di lettura devo averlo come input. Inoltre RS e RW cambiano stato
a seconda ci cosa vogliamo fare.
Se lasciamo le cose come abbiamo delineato finora, ci troviamo che bus e linee
di controllo dovranno essere settati sicuramente all' inizio di ogni routine,
che li lascia nella situazione in cui li ha usati.
Questa pratica non è la migliore strategia, in quanto è molto meglio che ogni
routine riporti in uscita i bit di I/O usati alla condizione in cui li ha
trovati all' ingresso.
Sicuramente in alcune circostanze questo impone alcune istruzioni duplicate, ma
ha il grande vantaggio che, in ogni momento, il programmatore sa esattamente in
che stato si trovano gli I/O interessati.
Dunque, aggiungiamo alcune righe per:
- riportare a livello 0 i bit
di controllo
- configurare il bus come
uscita e azzerarlo
;**********************************************************
;**********************************************************
; Driver per LCD a caratteri
;**********************************************************
;**********************************************************
; Sono definite le seguenti funzioni:
; LCDioini : inizializza pin di I/O
; LCDwrdat : scrivi un dato sull' LCD
; LCDwrcmd : scrivi un comando sull' LCD
; LCDrdram : leggi RAM dall' LCD
; LCDrdbfa : leggi BF e AC dall' LCD
;**********************************************************
; LCDioini
; Questa routine inizializza i pin di controllo dell' LCD
;**********************************************************
LCDioini:
; set I/O per LCD
bcf LCD_E
; EN pin clear
bcf LCD_RS
; RS pin clear
bcf LCD_RW
; RW pin clear
clrf LCDport
; data port clear
clrf LCDtris
; set bus come uscita
bcf
LCD_Etris ; set linee di
comando come uscite
bcf LCD_RStris
bcf LCD_RWtris
return
;**********************************************************
; LCDwrcmd - LCDwrdat
; Trasmette un dato o un comando al display:
; Dato o comando sono in WREG.
;**********************************************************
; Trasmette un comando. Comando in WREG
LCDwrcmd:
bcf
LCD_RS ; set flag pr
invio comando
bra
lcwr
; Trasmette un dato. Dato in WREG
LCDwrdat:
bsf LCD_RS
; set flag per invio dati
lcwr movwf LCD_port
; mettilo sul PORT
bcf
LCD_RW ; RW = 0 per
scrittura
rcall LCDclk
; clock E
; reset LCD bus e controlli
clrf LCDportw
; clear PORT
bcf
LCD_RW ; clear RW
bcf
LCD_RS ; clear RS
return
;**********************************************************
; LCD_clk
; Impulso di clock positivo sul pin EN per accettare dati
; o comandi
;**********************************************************
; clock LCD
LCD_clk bsf LCD_E ; set EN = 1
; inserire NOP per rispettare i tempi previsti dal
; costruttore
nop
; insert nop
nop
bcf LCD_E ; EN = 0
nop
return
;**********************************************************
; LCDrdram - LCDrdbfa
; Legge contenuto RAM o BF+AC, dato reso in WREG
; Per BF+AC la routine rientra solo quando BF=0
;**********************************************************
; Leggi RAM dall' LCD
LCDrdram
bsf
LCD_RS ;RS=1 per la
RAM
bra
lcdr0 ;salta
avanti
; Leggi BF + AC dall' LCD
LCDrdBF bcf LCD_RS
;RS=0 per BF+AC
lcdr0 movlw 0xFF
; data port = input
movwf LCDtris
bsf
LCD_RW ;RW=1 per
leggere
nop
lcdr1 bsf LCD_E
;E = 1 abilita display
btfsc
LCD_RS ;Ram o BF?
bra
notbusy ;RS=1, Ram
; RS=0, test busy dall' LCD
btfss
LCDportr, 7 ; se BF=1 è busy
bra
notbusy ; se = 0 busy end
nop
;aggiungere nop per
nop
; ottenere cicli da
bra lcdr1
; 1 us
notbusy movf LCDportr, w ; salva il dato letto)
bcf
LCD_E ;E = 0 - disabilita
nop
nop
; reset LCD bus e controlli
clrf LCDportw
;clear PORT
clrf LCDtris ;set port = output
bcf
LCD_RWw ;clear RW
bcf
LCD_RSw ;clear RS
return
|
E così abbiamo definito le principali funzioni
L' inizializzazione
Disponendo delle funzioni di lettura e scrittura, possiamo
affrontare la fase di inizializzazione, il cui flowchart è fornito
direttamente dal foglio dati del controller.
Le motivazioni per attenersi a questa procedura le
trovate qui.
Trasformiamo il flowchart in istruzioni.
|
;**********************************************************
; LCDSwini
; Inizializzazione per istruzioni.
;**********************************************************
; Delay iniziale >= 15 ms specificato per Hitachi 44780
; Se l' avviamento hardware utilizza un tempo sensibile,
; ridurre questa attesa
LCDswini
Delay 15ms
; Per le prime scritture non è possibile utilizzare il flag
; di busy, ma si impiega un ritardo fisso
;
movlw 0x30
; comando 0x30 - 8 bit mode
movwf LCDportw
;
rcall
LCD_clk ; primo
clock
Delay 4100ms
; primo ritardo
rcall
LCD_clk ; secondo clock
Delay 100us
; secondo ritardo
rcall
LCD_clk ; terzo
clock
; Da ora in poi è possibile usare il falg busy.
; Invia i comandi di definizione del modo di funzionamento
; standard con:
; - display su due o più righe
; - display on
; - carattere 5x7
; - cursore on, non lampeggiante
; - cursore con movimento da sinistra a destra
; Per altre modalità modificare gli ultimi due comandi
rcall
LCDrdbfa ;check per Busy
movlw LCD_2L
;8 bit/2 linee/ font 5x7
rcall
LCDwrcmd
rcall
LCDrdbfa ;check per Busy
movlw LCD_OFF
;display off
rcall LCDwrcmd
rcall
LCDrdbfa ;check per Busy
movlw LCD_CLR
;Clear & Home
rcall
LCDwrcmd
rcall
LCDrdbfa ;check per Busy
movlw LCD_CR
;cursor move l->r
rcall LCDwrcmd
rcall
LCDrdbfa ;check per Busy
movlw LCD_ONC
;display on/cursor off/blink off
rcall
LCDwrcmd
return
|
Osserviamo che:
-
prima di ogni scrittura di un comando, testiamo BF per avere
il via libera.
-
occorrono delle routines di ritardo. Possiamo usare quelle descritte
qui, o qualsiasi altra soluzione adeguata.
dato che ogni operazione di scrittura deve avere la
verifica del BF prima di procedere. Per evitare la ripetizione della coppia
rcall
LCDwrcmd
rcall
LCDrdbfa ;check per Busy |
possiamo scrivere una ulteriore routine con cui sostituire le due
righe , che rende il listato sorgente molto più leggibile:
;**********************************************************
; LCDWrCmd - scrive comando verso il display
; LCDWrdat - scrive dato verso il display
; e verifica il rilascio di BF per il comando successivo
;**********************************************************
LCDWrCmd rcall LCDwrcmd
bra
LCDrdbfa ;check per Busy
LCDWrDat rcall LCDwrdat
bra
LCDrdbfa |
Notare che il ritorno dalla chiamata:
rcall
LCDWrCmd
oppure:
rcall
LCDWrDat |
avviene in "modo sporco" utilizzando il return
residuo di LCDrdbfa ;
prassi non canonica, ma utile a risparmiare una istruzione.
Da notare anche che le label LCDWrCmd
e LCDwrcmd sono due cose diverse per
MPASM,
dato che impostato come case sensitive.
Abbiamo osservato dalle specifiche
che ogni comando è costituito da un byte (8 bit).
Una via semplicistica è
quella di scrivere un valore esadecimale o binario (o decimale) equivalente al
comando ogni volta che se ne richiama uno. Questa pratica di usare valori
assoluti è particolarmente perversa e condannabile in quanto obbliga a consultare
continuamente i fogli dati per rilevare i valori necessari, che non è possibile
avere a memoria. Inoltre da adito a facili errori e rende il listato illeggibile.
La via corretta è quella di cerare una serie di definizioni
iniziali in cui i valori assoluti sono sostituiti da label, che evitano errori,
non richiedono memorizzazioni speciali, rendo il listato leggibile.
Le assegnazioni consistono nel attribuire il valore di una
costante a label mnemoniche per ognuno dei possibili comandi, con la direttiva
MPASM CONSTANT. La sua sintassi è:
constant <label> =
<valore>
Possiamo tabellarne alcune:
;************************************************************************
;* Comandi del controller Hitachi 44780
;
; Comandi come constanti - Esempio di uso:
LCDwrcmd LCDCLR
; Clear and home - richiede 1.6 ms per l' esecuzione
CONSTANT LCD_CLR = b'00000001' ;
clear display & cursor home
CONSTANT LCD_CH = b'00000010' ; cursor home
CONSTANT LCD_CR = b'00000110' ; cursor l -> r, shift off
CONSTANT LCD_CL = b'00000100' ; cursor r -> l, shift off
CONSTANT LCD_OFF = b'00001000' ; display off
CONSTANT LCD_ON = b'00001100' ; display on, cursor off
CONSTANT LCD_ONC = b'00001110' ; display on, cursor on, blink off
CONSTANT LCD_ONCB = b'00001111' ; display on, cursor on, blink on
;Costanti di selezione inizio linea
CONSTANT LCDL1 = 0x00
; 0x00, inizio linea 1
CONSTANT LCDL2 = 0x40
; 0x40, inizio linea 2
|
Se quanto sopra basta per quello che vogliamo fare in questo
esercizio, sempre nell' ottica di scrivere un driver adeguato ad ogni
circostanza, senza bisogno di creare nulla da zero ogni volta, proponiamo una
tabella con il set completo dei comandi HD44780, che è opportuno
leggere e cercare di capire.
;************************************************************************
;********** Set standard comandi del controller Hitachi 44780 ***********
;
;
RS R/W DB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0
;
== === === === === === === === === ===
;Clear Display
0 0 0 0 0 0
0 0 0 1
;Return Home
0 0 0 0 0 0
0 0 1 *
;Entry Mode Set
0 0 0 0 0 0
0 1 I/D S
;Display ON/OFF
0 0 0 0 0 0
1 D C B
;Cursor and Display Shift 0 0 0
0 0 1 S/C R/L * *
;Function Set
0 0 0 0 1 DL
N F * *
;Set CG RAM address
0 0 0 1 A A
A A A A
;Set DD RAM address
0 0 1 A A A
A A A A
;Read busy flag and address 0 1 BF A
A A A A A A
;Write data to CG or DD RAM 1 0 D
D D D D D D
D
;Read data from CG or DD RAM 1 1 D D
D D D D D D
; Comandi come constanti - Esempio di uso:
LCDwrcmd LCDCLR
; Clear and home - richiede 1.6 ms per l' esecuzione
CONSTANT LCD_CLR = b'00000001' ;
clear display & cursor home
CONSTANT LCD_CH = b'00000010' ; cursor home
; altri comandi con tempo di esecuzione di 40
us
; Mode set 000001IS
;
||- S=0 no shft S=1 display shft
;
|-- I=0 decrement I=1 increment
CONSTANT LCD_CR = b'00000110' ; cursor l -> r, shift off
CONSTANT LCD_CL = b'00000100' ; cursor r -> l, shift off
CONSTANT LCD_SHd = b'00000101' ; display shift on decrement
CONSTANT LCD_SHi = b'00000111' ; display shift on increment
CONSTANT LCD_MODE = 0x04 ; mask for below
CONSTANT LCD_ID = 0x02
; I/D decrement/increment
CONSTANT LCD_S = 0x01
; S shift off/on
; Display on/off 00001DCB
;
|||- B=0 blink off B=1 blink on
;
||-- C=0 cursor off C=1 cursor on
;
|--- D=0 display off D=1 display on
CONSTANT LCD_OFF = b'00001000' ; display off
CONSTANT LCD_ON = b'00001100' ; display on, cursor off
CONSTANT LCD_ONC = b'00001110' ; display on, cursor on, blink off
CONSTANT LCD_ONCB = b'00001111' ; display on, cursor on, blink on
CONSTANT LCD_ONOFF = 0x08 ; mask for below
CONSTANT LCD_D = 0x04
; D display off/on
CONSTANT LCD_C = 0x02
; C cursor off/on
CONSTANT LCD_B = 0x01
; B cur. blink off/on
; cursor/disp shift : 0001SRxx
;
||--- R/L=0 to left =1 to right
;
|---- S/C=0 shft cursor =1 shft display
CONSTANT LCD_MCL = b'00010000' ; move cursor left
CONSTANT LCD_MCR = b'00010100' ; move cursor right
CONSTANT LC_DSL = b'00011000' ; shift display content left
CONSTANT LC_DSR = b'00011100' ; shift display content right
CONSTANT LCD_SHFT = 0x10 ; mask for below
CONSTANT LCD_SC = 0x08
; S/C shift cursor/display
CONSTANT LCD_RL = 0x04
; R/L shift left/right
; function set: 001DNFxx
;
|||--- F=0 5x7 font F=1 5x10 font
;
||---- N=0 1 line N=1 2 lines
;
|----- D=0 4 bit D=1 8 bits
CONSTANT LCD_2L = b'00111000' ; 8 bit mode, 2 lines, 5x7 dots
CONSTANT LCD_1L = b'00101000' ; 8 bit mode, 1 line, 5x7 dots
CONSTANT LCD_FSET = 0x20 ; mask for below
CONSTANT LCD_F = 0x04
; F font
CONSTANT LCD_N = 0x08
; N line num.
CONSTANT LCD_DL = 0x10
; DL 4/8 bit interface
;Costanti di selezione inizio linea
CONSTANT LCDL1 = 0x00
; 0x00, inizio linea 1
CONSTANT LCDL2 = 0x40
; 0x40, inizio linea 2
|
A chi sembra eccessiva questa serie di assegnazioni, va
ricordato che:
-
Anche se una applicazione utilizza solo una piccola parte di
questi comandi, la libreria deve essere più completa possibile in modo da fare
fronte a tutte le possibili necessità.
-
le assegnazioni sono elementi usate da MPASM per creare il
compilato, ma esse entrano a far parte del file
.hex solamente se e quando sono richiamate. Una libreria, richiamata con un
include del sorgente,
viene compilata solamente per le parti utilizzate dal programma. Quindi
qualsiasi costante, line o routine che non viene usata, non occupa alcuno spazio
nella memoria programma.
Quindi, la loro lista può essere lunga a piacere e questo non incide
minimamente sullo spazio occupato nella memoria programma. Analogamente ai
commenti.
-
Sviluppando su un PC anche anzianotto, la lunghezza del
testo del sorgente non influenza in modo sensibile il tempo di compilazione:
si tratta di una lista di testo e, anche se la sua stampa occupa molte
pagine, questo non appesantisce i tempi di lavoro.
Una libreria ben fatta serve a lungo e in tutte le
occasioni. Quindi non esiste ragione per non farla al meglio possibile, anche se
pare inutilmente più complicata rispetto alle strette necessità di una data
applicazione.
E, sempre nell' ottica di sostituire assoluti con label, sarà
indispensabile una azione iniziale, ovvero quella di definire con label anche le assegnazioni di I/O.
Cosa comporta questa fase? Semplicemente la potente possibilità di cambiare
assegnazioni ai pin di I/O a seconda delle necessità dello schema SENZA
MODIFICARE IL DRIVER, ma solo la parte delle assegnazioni.
Cambiando hardware, cioè usando altri pin del microcontroller, basterà variare
solamente queste assegnazioni, lasciando intatto tutto il resto.
;**************************************************************************
; Assegnazioni I/O
; ================
;--> cambiare se l' hardware è differente
;LCD - linee dati
LCDtris equ TRISB
; direzione LCD data port
LCDportr equ PORTB
; LCD data port - lettura
LCDportw equ LATB
; LCD data port - scrittura
;LCD - linee di controllo
#define LCD_Etris TRISC,0x00
; E on portC,0
#define LCD_RStris TRISC,0x01
; RS on portC,1
#define LCD_Ew LATC, 0x00
#define LCD_Er PORTC,0x00
#define LCD_RSw LATC, 0x01
#define LCD_RSr PORTC,0x01
#define LCD_RWtris TRISC,0x02
; RW on portC,2
#define LCD_RWw LATC, 0x02
#define LCD_RWr PORTC,0x02 |
Qui, avendo come base i PIC Enhanced possiamo ricorrere alla finezza
di utilizzare i due registri del port:
-
LATx in scrittura
-
PORTx in lettura
L'
aggiunta di una definizione in più per I/O non ha alcun peso: MPASM penserà da
sè ad effettuare le sostituzioni label-assoluti in modo corretto senza alcun
altro intervento da parte nostra.
Comunque questa soluzione può non essere necessaria in quanto
il carico delle linee del bus del display non è oneroso per i port del
microcontroller e
il problema dell' R-M-W non dovrebbe essere presente. Però, potendo
operare sia su LAT che su PORT, una piccola complicazione in più elimina
completamente qualsiasi possibilità di mal funzionamento.
Per concludere aggiungiamo un paio di subroutines per cambiare
linea del display. Si tratta semplicemente di inviare al controller l' indirizzo
di inizio della linea che si vuole utilizzare: questo sposta i cursore e l' AC
in quella determinata posizione.
Ricordiamo che, come si evince dal
foglio dati, il comando di indirizzo DDRAM richiede il valore dell'
indirizzo a 6 bit, con il bit7=1, che portiamo a questo valore con un OR.
;**********************************************************
; LCDlineX - porta il cursore all' inizio della linea
; specificata
; Avvertenza: la linea chiamata deve esistere nel display
;**********************************************************
LCDline1 movlw LCDL1
bra
LCDDDAdr ; linea 1
LCDline2 movlw LCDL2
; linea 2
;**********************************************************
; LCDDDADR - Richiama una specifica posizione in DDRAM
; Entrare con l' indirizzo da richiamare in WREG
; Avvertenza: la RAM chiamata deve esistere nel display
;**********************************************************
LCDDDAdr iorlw b'10000000' ;
bit 7=1 per il comando
rcall LCDwrcmd
return |
Ovviamente, se
il display ha una sola linea, questo non serve e se ne ha 4 occorre ampliare
il comando.
Siamo così
arrivati ad una prima versione ragionevole del nostro driver, che potete
scaricare qui.
Questa semplice
libreria rende disponibili alcune funzioni. Esse vanno viste come blocchi
logici in cui un ingresso darà origine ad un determinato risultato.
Funzione |
Input |
Output |
LCDioini |
- |
Inizializza I/O necessari alla gestione dell' LCD |
LCDswini |
- |
Inizializzazione software del display |
LCDWrCmd |
codice comando in W |
Invia un comando in W al display.; esce dopo il test di BF |
LCDWrDat |
codice dato in W |
Invia un dato in W al display; esce dopo il test di BF |
LCDrdram |
- |
Legge contenuto RAM e ritorna con questo in WREG |
LCDrdbfa |
- |
Legge BF + AC e ritorna con questo in WREG. Esce dopo il test di BF |
LCDDDAdr |
indirizzo in W |
porta il cursore all' indirizzo in WREG |
LCDline1 |
- |
porta il cursore all' inizio della linea 1 |
LCDline2 |
- |
porta il cursore all' inizio della linea 2 |
Le funzioni saranno
richiamate come subroutines. Come esse operano, per l' utente potrebbe essere
del tutto indifferente: questa è la funzione della libreria, ovvero quella di
mettere a disposizione macro funzioni semplicemente da utilizzare. La
libreria andrà linkata con un #include in
un punto qualunque del sorgente, preferibilmente verso la fine. Ora proviamolo
sul campo.
Se si incontrano errori nella compilazione è opportuno verificarli con la
lista di descrizione degli errori e correggere dove si è sbagliato.
|