Esercitazioni ASM - PIC18

 


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:

  1. scrivere verso il controller
  2. 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:

  1. impostare il bit di controllo RW = 0 per indicare che si tratta di una scrittura
  2. impostare il bit di controllo RS = 1  se vogliamo inviare un dato e RS =  0 se si tratta di un comando
  3. porre sul bus dati il byte da trasmettere
  4. 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.


 

Copyright © afg. Tutti i diritti riservati.
Aggiornato il 16/11/11.