Indice |
Introduzione
Se è vero che un programma in C lo si può scrivere all' interno di un unico file è anche vero l' utilizzo di moduli di programma da collegare a livello di linker permette di ottenere diversi vantaggi. In questo breve articolo ne descriverò uno di questi (nello specifico un modulo che contiene le funzioni a basso livello per il pilotaggio di un display alfanumerico che utilizza il famoso controller HD44780) con l' intento di fornire un' idea su come si possono organizzare i programmi che fanno uso dei moduli. Questo non è un articolo che descrive un modulo che può far parte di una libreria perché incompleto, ma uno vorrebbe essere stimolo per lo sperimentatore.
Non mi soffermerò nella spiegazione del display e del suo controller poiché è già stato fatto in modo egregio da Paolino in questi due articoli:
- LO HAI MAI REALIZZATO CON UN PIC? - I PICMicro e i display LCD alfanumerici (parte 1)
- LO HAI MAI REALIZZATO CON UN PIC? - I PICMicro e i display LCD alfanumerici (parte 2)
La conoscenza del controller è indispensabile e quindi la lettura del suo datasheet o degli articoli sopra indicati è una condizione necessaria per proseguire con l' articolo.
Per la prova pratica del modulo e del programma utilizzerò il PIERIN PIC18, il sistema di sviluppo MPLAB con compilatore C18 partendo dal progetto di base che ho descritto in questo articolo.
Il file pierin_display_alfa.rar contiene i sorgenti del progetto e l' immagine hex dell' eseguibile.
I moduli in C
Un modulo può essere considerato una specie di black-box da includere nel proprio programma ed utilizzare facilmente. Anche il modulo oggetto di questo articolo può essere inglobato in un progetto ed utilizzato facilmente.
Una delle caratteristiche dei moduli è quella di poter nascondere informazioni, dichiarazioni e quant' altro in modo che non sia possibile vederle dall' esterno del modulo stesso. Questa caratteristica viene chiamata "information hiding" ed è molto utile per dare ordine al programma. Le informazioni non visibili all' esterno del modulo vengono chiamate locali mentre quelle che potranno essere viste da altri moduli o dal programma principale vengono chiamate pubbliche.
Un' altra caratteristica dei moduli è che sono compilati un ad uno ed il codice oggetto viene collegato fra di loro a livello di linker. In questo modo si possono scrivere moduli non solo in C ma anche in linguaggio assembly se necessario.
Vediamo come sono organizzate le "dipendenze" del main nel progetto che fa uso del modulo per il display.
Ed ora li prendiamo uno per uno per capire a cosa servono e perché sono stati scritti.
main.h
Contiene le definizioni (#define) dei simboli, le dichiarazioni di variabili ed i prototipi delle funzioni del programma principale che possono essere visibili ad altri moduli che ne fanno uso. In questo caso il file è vuoto ma si potrebbe palesare la necessità che alcuni moduli abbiano bisogno di utilizzare funzioni o variabili che sono dichiarate all' interno del main. Questo file serve appunto come collegamento fra il main e gli altri moduli del progetto.
configurazione.h
E un file che potrebbe benissimo essere racchiuso dentro main.c ma il suo contenuto è stato scritto all' interno di un file a se per motivi di ordine e di leggibilità.
mappa_int.h
Anche questo file potrebbe benissimo rimanere all' interno del main.c ma anche in questo caso lo si è scritto all' interno di un file a se per motivi di ordine e di leggibilità.
hardware_def.h
Questo contiene le definizioni dei piedini di I/O e le opzioni per il moduli che fanno da driver per eventuali dispositivi esterni (display, memorie, periferiche in generale) del progetto.
Tecnicamente queste dichiarazioni si potrebbero mettere all' interno del file main.h in quanto sarebbero comuni a tutto il progetto. Ho preferito fare un file a se perché ... sono abituato a fare così eh eh eh. Preferisco avere un file che serva solo all' hardware ma è solo una questione di scelte personali.
HD44780.h
E' il file che contiene le definizioni, dichiarazioni ed i prototipi pubblici del file HD44780.c E' parte integrante del modulo e può essere incluso da tutti quei moduli che hanno bisogno di comandare il display.
Essendo questo un modulo che potrà essere utilizzato in altri progetti ha alcune particolarità che vedremo più avanti quando lo analizzeremo a fondo.
main.c
E' il programma principale che contiene le sue definizioni, dichiarazioni, i prototipi e le funzioni locali ed infine, ovviamente, il programma principale.
Il modulo HD44780
Prima di procedere con l' analisi di quello che vi è contenuto è bene cedere come è strutturato il modulo al completo. Il modulo è costituito da due files: HD44780.h e HD44780.c . Il primo contiene tutto quello che serve per poter utilizzare il modulo mentre il secondo contiene il sorgente delle varie funzioni che lo compongono. Alcune sono visibili all' esterno del modulo stesso (pubbliche) perché sono quelle che potranno essere utilizzate da altri moduli (ricordo che anche il main è un modulo!), ed altre sono visibili sono all' interno del modulo stesso per il semplice motivo che non avrebbe senso renderle visibili. Questa è la struttura del modulo, o meglio le sue dipendenze.
Quindi il file HD44780.c potrà essere compilato autonomamente ma ha bisogno degli altri due files. Tecnicamente deve includere solo il suo header ma quest' ultimo include anche il file di configurazione dell' hardware.
HD44780.h
Vediamo ora cosa c' è dentro questo file, lo analizzerò parte per parte.
#ifndef HD44780_H #define HD44780_H // Inclusione del file contenente le definizioni dei pin #include "hardware_def.h" //------------------------------------------------------------------------------ // Defines che si possono utilizzare (comandi) #define HD44780_CLR_DISP 0x01 #define HD44780_HOME 0x02 #define HD44780_SET_CGADR 0x40 #define HD44780_SET_ADR 0x80 //------------------------------------------------------------------------------ // Prototipi visibili extern void HD44780_init(void); extern void HD44780_writeData(unsigned char n); extern void HD44780_writeCmd(unsigned char n);
Le prime due righe sono le solite direttive che si usano per evitare inclusioni multiple del file. L' inclusione del file hardware_def.h viene fatta in questo file perché è più facile modificare questo file per adattarlo a progetti strutturati in maniera diversa piuttosto che mettere le mani nel file HD44780.c che contiene tutto il codice.
Dopo troviamo le defines che possono essere usate dall' esterno e che in pratica sono alcuni comandi del display (non li ho messi tutti solo per questioni di tempo), quelli più utilizzati. In questo modo non c'è bisogno di andarseli a cercare sul datasheet. Lascio allo sperimentatore il gusto di completare la lista dei comandi.
Infine troviamo i prototipi delle funzioni pubbliche, quelle che possono essere utilizzate. In questo modulo c'è il minimo sindacale per potere visualizzare qualcosa infatti lo scopo dell' articolo non è quello di fornire un modulo completo ma di introdurre all' uso dei moduli in generale.
Nella seconda parte troviamo un qualcosa che può essere interessante
//------------------------------------------------------------------------------ // Impostazioni di default. // Nel caso i segnali non siano difiniti venegono impostati di default // D4 = RD0 // D5 = RD1 // D6 = RD2 // D7 = RD3 // RS = RE0 // E = RE1 #ifndef HD44780_D4 #define HD44780_D4 LATDbits.LATD0 #define HD44780_D4_DIR TRISDbits.TRISD0 #endif #ifndef HD44780_D5 #define HD44780_D5 LATDbits.LATD1 #define HD44780_D5_DIR TRISDbits.TRISD1 #endif #ifndef HD44780_D6 #define HD44780_D6 LATDbits.LATD2 #define HD44780_D6_DIR TRISDbits.TRISD2 #endif #ifndef HD44780_D7 #define HD44780_D7 LATDbits.LATD3 #define HD44780_D7_DIR TRISDbits.TRISD3 #endif #ifndef HD44780_RS #define HD44780_RS LATEbits.LATE0 #define HD44780_RS_DIR TRISEbits.TRISE0 #endif #ifndef HD44780_E #define HD44780_E LATEbits.LATE1 #define HD44780_E_DIR TRISEbits.TRISE1 #endif // Di default il numero di linee è impostato ad 1 #ifndef HD44780_LINES #define HD44780_LINES 1 #endif #endif
Abbiamo una serie di compilazioni condizionate per ottenere due scopi:
- Essere sicuri che i simboli utilizzati dalle funzioni siano COMUNQUE dichiarati (per evitare errori di compilazione)
- Dare dei valori di default a questi simboli (ai pin di I/O ed il numero di linee hardware)
Quindi se i due simboli che definiscono il pin (HD44780_D4 ad esempio) ed il suo bit di direzione (HD44780_D4_DIR) non sono già stati dichiarati prima vengono dichiarati in questo momento assegnando loro i rispettivi valori di default.
HD44780.c
Anche di questo file ne analizziamo una parte per volta
#include "HD44780.h" #include <delays.h> // Esempio di adattamento ritardi per frequenze inferiori a quella massima #if (12000000 == CLOCK_FREQ) // Ritardi per frequenza di clock 12 MHz #define delay15ms() Delay10KTCYx(45) #define delay4100us() Delay100TCYx(125) #define delay1600us() Delay100TCYx(48) #define delay120us() Delay10TCYx(36) #define delay43us() Delay10TCYx(13) #else // Ritardi per frequenza di clock 48 MHz (caso peggiore, più lento) #define delay15ms() Delay10KTCYx(180) #define delay4100us() Delay1KTCYx(50) #define delay1600us() Delay100TCYx(192) #define delay120us() Delay10TCYx(144) #define delay43us() Delay10TCYx(52) #endif
La prima riga è l' inclusione del file header dove sono contenuti tutti i simboli (e su questo non c'è niente da dire) seguito dall' inclusione dell' header per utilizzare le funzioni di ritardo. La cosa che vorrei far notare è la compilazione condizionata delle definizioni delle macro per generare i ritardi. Sappiamo che il display in questione si arrabbia se non si aspetta il tempo necessario affinché lui concluda le operazioni. E' anche vero che si potrebbe testare il bit di busy ma in questa implementazione il segnale di R/W non è utilizzato quindi ci tocca aspettare un tempo sufficientemente lungo per permettergli di portare a compimento le operazioni.
Visto che questi sono ritardi in cicli dipendono dalla frequenza di clock del sistema, il caso peggiore è quando il micro funziona alla sua massima velocità (48MHz) quindi vengono utilizzati quei ritardi. Supponiamo però di voler evitare di fare ritardi inutilmente lunghi il che vuol dire cambiare i valori alle funzioni di ritardo. Io ho fatto una prova con il micro che funziona a 12MHz (semplicemente non ho attivato il PLL) e quindi ho messo in compilazione condizionata anche la possibilità che il simbolo CLOCK_FREQ potesse valere 12000000, in tal caso le macro utilizzate sarebbero il primo gruppo.
Proseguendo troviamo le dichiarazioni delle funzioni pubbliche
//----------------------------------------------------------------------------- // Prototipi pubblici void HD44780_init(void); void HD44780_writeData(unsigned char n); void HD44780_writeCmd(unsigned char n); //----------------------------------------------------------------------------- // Prototipi locali static void writeDigit(unsigned char n); static void writeByte(unsigned char n);
e dopo le dichiarazioni delle funzioni locali.
Nota di programmazione: Per fare in modo che una funzione sia visibile solo a livello di modulo è necessario utilizzare la classe di memorizzazione static che ha significato diverso da quello utilizzato nelle dichiarazioni di variabili. Una funzione dichiarata come static può essere vista ed utilizzata esclusivamente all' interno del modulo in cui viene scritta.
Entriamo quindi nel vivo delle funzioni analizzando quelle visibili a livello di modulo
//----------------------------------------------------------------------------- // Scrive un digit nel bus dei dati del display static void writeDigit(unsigned char n) { // Setup che dovrebbe essere di 10ns HD44780_E = 1; // Mette il digit nelle linee da D4 a D7 if (n & 0x01) HD44780_D4 = 1; else HD44780_D4 = 0; if (n & 0x02) HD44780_D5 = 1; else HD44780_D5 = 0; if (n & 0x04) HD44780_D6 = 1; else HD44780_D6 = 0; if (n & 0x08) HD44780_D7 = 1; else HD44780_D7 = 0; //Hold time sicuramente superiore a 80ns. HD44780_E = 0; } //----------------------------------------------------------------------------- // Scrive un byte nel display utilizzando due scritture consecutive di digit static void writeByte(unsigned char n) { // Prima invia la parte alta writeDigit(n >> 4); // Dopo invia la parte bassa writeDigit(n & 0x0F); }
Salta subito all' occhio il fatto che le linee sono scritte una per una. Questo per fare in modo da poter sceglie di utilizzare qualsiasi pin per qualsiasi segnale. Il micro ci mette un po' più di tempo ma la flessibilità diventa grandissima.
Queste funzioni sono state dichiarate come static perché non interessa a nessun poterne fruire. Quello che interessa è riuscire ad inviare un dato o un comando. Il come viene fatto interessa solo il modulo che funge da driver.
Infine troviamo le funzioni pubbliche
//----------------------------------------------------------------------------- void HD44780_writeCmd(unsigned char n) { // Per inviare un comando la linea RS deve essere posta a 0 HD44780_RS = 0; // Invia il dato writeByte(n); // Ritarda per attendere la completa esecuzione del comando // In questo caso dipende dal comando // questi ritardi sono di 1600 e 43 us massimi if (!(n & 0xFC)) delay1600us(); else delay43us(); } //----------------------------------------------------------------------------- void HD44780_writeData(unsigned char n) { // Per inviare un comando la linea RS deve essere posta a 0 HD44780_RS = 1; // Invia il dato writeByte(n); // Ritarda per attendere la completa esecuzione del comando // anche questo ritardo deve essere 43us delay43us(); } //----------------------------------------------------------------------------- void HD44780_init(void) { // inizializza le linee di uscita HD44780_D4_DIR = 0; HD44780_D5_DIR = 0; HD44780_D6_DIR = 0; HD44780_D7_DIR = 0; HD44780_RS_DIR = 0; HD44780_RS = 0; HD44780_E_DIR = 0; HD44780_E = 0; // Ritardo di 15ms per stabilizzazione VCC delay15ms(); // esegue la sequenza di reset, quella che si usa in caso di alimentazione // che non garantisce un buon reset interno. HD44780_RS = 0; // mette RS a 0 writeDigit(0x03); delay4100us(); // sul datasheet e' indicato 4,1 ms writeDigit(0x03); delay120us(); // 120us. Il datasheet indica > 100 us. writeDigit(0x03); delay120us(); writeDigit(0x02); delay120us(); // Fine sequenza di reset // Programma il controller con il numero di righe da pilotare (1 o 2) // Data lenght 4 bit e font 5x8 #if (1 == HD44780_LINES) HD44780_writeCmd(0x20); #elif (2 == HD44780_LINES) HD44780_writeCmd(0x28); #else errore [HD44780] numero di righe non supportato. #endif // Cancella il display HD44780_writeCmd(0x01); // Imposta direzione d' incremento in avanti senza shift del display HD44780_writeCmd(0x06); // lcd acceso e cursore lampeggiante a linea non visibile HD44780_writeCmd(0x0C); }
Il programma principale
Prima di analizzare il file che contiene il programma vero e proprio diamo uno sguardo al file hardware_def.h
#ifndef HARDWARE_DEF_H #define HARDWARE_DEF_H //------------------------------------------------------------------------------ // Informazioni per i moduli // Definizione della frequenza di clock del sistema //#define CLOCK_FREQ 12000000 //------------------------------------------------------------------------------ // Modulo LCD con HD44780 // Come connessioni si adottano quelle di default // Definizione numero di righe del controller #define HD44780_LINES 2 #endif // End of file
La prima parte contiene le informazioni (simboli) che possono essere utili a gli eventuali moduli del progetto. Nello specifico troviamo (sotto commento) la definizione della frequenza di clock di sistema. E' sotto commento perché l' ho utilizzata per provare le macro dei ritardi del modulo del display. Invece di toglierla l' ho lascita come esempio.
Un simbolo che invece sono obbligato a definire è il numero di linee hardware (HD44780_LINES). Il valore di default è 1 ma io utilizzo un display a 4 righe che corrispondono a 2 linee hardware. Quindi ho dovuto specificarlo.
Nella parte che interessa il module HD44780 non ho scritto niente perché ho tenuto la definizione di default delle linee di I/O. Nel caso avessi voluto, chessò, assegnare la linea RS al pin RB2 avrei dovuto scrivere
#define HD44780_RS LATBbits.LATB2 #define HD44780_RS_DIR TRISBbits.TRISB2
In tal caso tale simbolo sarebbe stato definito da me e quindi, invece di assumere il valore di default avrebbe mantenuto il valore assegnatogli da me in questo file.
L' astrazione dell' hardware
Una pratica che mi piace molto è quella di fare in modo di svincolarsi dall' hardware. In pratica il principio è questo: oggi nel mio progetto uso un display alfanumerico ma potrei anche, un domani, usarne uno grafico. Voglio comunque mantenere le stesse funzioni per la scrittura di caratteri o per posizionare il cursore. Quindi, sebbeno io sappia che per scrivere un carattere sul display mi basta inviarci il codice con la funzione HD44780_writeByte preferisco scrivermi la funzione apposta. Se un giorno cambierò display dovrò soltanto modificare la funzione LCD_writeChar. Queste sono le funzioni del "layer" superiore a quello hardware che ho scritto. Da notare anche che le funzioni del modulo (specifico per il HD44780) iniziano tutte con HD4470_ mentre quelle che fanno parte del layer di astrazione iniziano con un generico LCD_.
//------------------------------------------------------------------------------ // Scrive un carattere sul display alla posizione del cursore void LCD_writeChar(char ch) { HD44780_writeData(ch); } //------------------------------------------------------------------------------ // Cancella lo schermo LCD void LCD_clear(void) { HD44780_writeCmd(HD44780_CLR_DISP); } //------------------------------------------------------------------------------ // Imposta la posizione del cursore nel display LCD void LCD_setPos(unsigned char x, unsigned char y) { unsigned char p; switch(y) { case 0: p = x; break; case 1: p = 64 + x; break; case 2: p = 20 + x; break; case 3: p = 84 + x; break; } p |= HD44780_SET_ADR; HD44780_writeCmd(p); } //------------------------------------------------------------------------------ // Visualizza una stringa in ROM nel display LCD void LCD_writeStrC(rom const char* s) { while(*s) HD44780_writeData(*s++); } //------------------------------------------------------------------------------ // Visualizza una stringa in RAM nel display LCD void LCD_writeStr(char* s) { while(*s) HD44780_writeData(*s++); }
Questa pratica mi è tornata estremamente utile al punto che per me è diventato un standard. Di solito racchiudo le funzioni di astrazione in un modulo a se ma in questo caso ho preferito evitare di incasinare il progetto.
Il programma principale
Iniziamo con uno sguardo all' inizio del file dove troviamo le inclusioni
// File di definizione dei registri del micro. #include "p18f47j53.h" #include <delays.h> // Header del main #include "main.h" // File di configurazione dei fuses #include "configurazione.h" // Mappatura delle interrupt #include "mappa_int.h" // Definizioni hardware #include "hardware_def.h" // Header del modulo display #include "HD44780.h"
Saltando poi tutte le varie parti del main che conosciamo già (sono partito dal progetto di base) andiamo direttamente alla funzione main()
//------------------------------------------------------------------------------ // MAIN FUNCTION //------------------------------------------------------------------------------ void main(void) { // Fa partire il PLL. // Anche se viene selezionato tramite i bit di configurazione // il suo funzionamento non è automatico. Ha bisogno di un comando. OSCTUNEbits.PLLEN = 1; // Attende abbastanza tempo per far stabilizzare il PLL Delay1KTCYx(100); // -------- Inizializzazione delle periferiche -------- // -------- Selezione ed abilitazione delle interrupt -------- // -------- Attivazione delle periferiche -------- // Inizializzazione modulo display LCD HD44780_init(); // Scritte dimostrative LCD_setPos(4,0); LCD_writeStrC("PIERIN PIC18"); LCD_setPos(5,1); LCD_writeStrC("ElectroYou"); LCD_setPos(1,2); LCD_writeStrC("modulo display LCD"); LCD_setPos(5,3); LCD_writeStrC("con HD44780"); // -------- Ciclo infinito di funzionamento -------- for(;;) { // Inserire il programma qui. } }
Avendo inserito l' header del modulo (HD44780.h) l' utilizzo è semplicissimo. Innanzi tutto si chiama la funzione di inizializzazione e poi le funzioni per fare quello che vogliamo fare. Essendo un programma di prova si ferma nel ciclo infinito di programma.
Tutto qui.
Il risultato finale è questo
Conclusioni
Come ho detto all' inizio questo articolo, almeno nelle intenzioni dello scrivente, vorrebbe essere una sorta di punto d' ingresso per la progettazione di un firmware che vada oltre il livello hobbistico. Se per sperimentare qualsiasi cosa va bene, per realizzare progetti che dovranno poi far funzionare un prodotto le cose cambiano. Purtroppo non sono capace ad insegnare, semmai posso cercare di trasmettere un po' di esperienza. Spero di esserci riuscito e mi scuso per eventuali imprecisioni o errori che spero mi vengano evidenziati in modo da poter correggere questo articolo (scritto frettolosamente).
L' organizzazione del progetto è sicuramente migliorabile e non è di certo il non plus ultra, ma l' intenzione era quella di stimolare lo sperimentatore ad approfondire questo argomento che, se non ricordo male, viene definito come "ingegneria del software".
E quindi siamo arrivati al punto in cui auguro di cuore una BUONA SPERIMENTAZIONE!