La prima caratteristica necessaria che un programma deve avere è quella di funzionare, questo è vero. Ma non è affatto vero che un programma funzionante sia un buon programma. In questo breve articolo vorrei introdurre un paio di concetti: l' astrazione dell'hardware (hardware abstraction) e la tecnica per nascondere le informazioni (information hiding). Sappiamo tutti che le cattive abitudini sono quelle più difficili da perdere, quindi abituarsi a seguire quelle buone aiuterà, sopratutto chi si è avvicinato da poco al mondo dei microcontrollori, a scrivere da subito buoni programmi. L'articolo non vuole essere affatto una lezione ma semplicemente un' introduzione ad un modo di programmare che ritengo molto valido. Non me ne vogliano quindi gli informatici se mi prenderò alcune licenze e ricorrerò ad approssimazioni. Per illustrare questi due semplici ma utilissime tecniche utilizzerò un programma molto semplice: il classico LED lampeggiante, la versione da microcontrollore del programma "Hello World".
Indice |
Un programma funzionante
Come esempio prendo il programma di prova che ho utilizzato in questo articolo
Una cosa che salta subito all' occhio guardano le prime #include è che utilizza un AVR, quindi è un programma specifico per questa famiglia di micro. Scendendo più in basso troviamo la routine di servizo dell'interrupt ciclica utilizzata per implementare un timer software e, all'interno del main riusciamo a capire che il LED è collegato al pin 0 della porta B. Quello che faremo è organizzare il programma in modo che sia un programma di tipo generale, adattabile a qualsiasi micro e con la possibilità di essere svincolati, oltre che dal micr,o anche dall'hardware di questo.
//-------------------------------------------------------------------------- // Lampeggio.c // // Created: Tanto tempo fa in una galassia molto lontana // Author: TardoFreak //-------------------------------------------------------------------------- #include <avr/io.h> #include <avr/interrupt.h> #include <avr/wdt.h> #include <avr/pgmspace.h> // Timer software. // Nota: vanno dichiarate come "volatile" per fare in modo che il // valore sia sempre e comunque letto evitando che l' ottimizzazione // non lo faccia. volatile unsigned short SoftTimer1; // Routine di servizio chiamata quando il contenuto del comparatore A // corrisponde al valore del timer. Quando raggiunge tale valore il // timer viene resettato e viene invocata questa routine. ISR(TIMER1_COMPA_vect) { if(SoftTimer1) SoftTimer1--; } //-------------------------------------------------------------------------- int main(void) { // disabilita watchdog MCUSR &= ~(1 << WDRF); wdt_disable(); // Inizializza timer 1 per timeout 10ms // Questa istruzione assicura che l' I/O clock per il timer1 sia abilitato PRR0 &= ~(1<<PRTIM1); // Carica il registro di comparazione per ottenere 20ms OCR1A = 3124; // Enable output compare A match TIMSK1 = (1<<OCIE1A)|(0<<TOIE1); // Avvia il timer1, prescaler 1/64 modo operativo Clear Top Count TCCR1B = (0<<WGM13)|(1<<WGM12)|(0<<CS12)|(1<<CS11)|(1<<CS10); // Predispone la porta B come uscita DDRB = 0xff; // Abilita le interrupt sei(); while(1) { if(!SoftTimer1) { PORTB ^= 0x01; // Commuta l' uscita PB0 SoftTimer1 = 25; // Carica il timer software per intervallo 500ms. } } }
L' astrazione dell' hardware
Svincolarsi dall' hardware significa evitare di avere a che fare con le operazioni a basso livello. Per fare questo modifichiamo il programma dapprima definendo dei simboli per identificare la porta del LED ed il bit a cui è collegato:
#define LED_PORT PORTB #define LED_DDR DDRB #define LED_MASK 0x01
Questo è già un primo passo per svincolarsi dall' hardware perché, se un giorno noi decidessimo che il LED dovrà essere collegato ad un altro pin, ci basterà modificare queste defines per assegnare l'uscita dove meglio crediamo.
L'uscita del LED deve essere inizializzata. Nel programma originale l'inizializzazione viene fatta con una semplice istruzione, trasformiamola in funzione e, per non farci mancare niente scriviamo anche due funzioni: una per accendere il LED e l' altra per spegnerlo. In questo modo abbiamo tutte le funzioni per controllare il LED come meglio ci aggrada.
//-------------------------------------------------------------------------- // void HAL_LEDon(void) // accende il LED void HAL_LEDon(void) { LED_PORT |= LED_MASK; } //-------------------------------------------------------------------------- // void HAL_LEDoff(void) // spegne il LED void HAL_LEDoff(void) { LED_PORT &= (LED_MASK ^ 0xff); } //-------------------------------------------------------------------------- // void HAL_initlED(void) // inizializza l' uscita che pilota il LED void HAL_initLED(void) { // Predispone il pin 0 della la porta B come uscita LED_DDR |= LED_MASK; HAL_LEDoff(); }
Ora scriviamo anche la funzione che fa commutare il LED.
//-------------------------------------------------------------------------- // void HAL_toggleLED(void) // cambia lo stato del LED void HAL_toggleLED(void) { LED_PORT ^= LED_MASK; }
Domanda: ma è necessario tutto questo casino, questa complicazione?
Non è indispensabile per scrivere un programma che funzioni ma lo è per scrivere un buon programma, più avanti sarà facile caipre il perché.
Una piccola nota: il suffisso HAL messo davanti al nome delle funzioni è un acronimo che significa "Hardware Abstraction Layer". Anche questo ha ragione di esistere ed anche la sua presenza sarà più chiara più avanti.
Un modulo solo per l' hardware
Siamo già ad un buon punto. Per fare le cose ben fatte la cosa migliore è prendere queste funzioni e metterle all' interno di un modulo a parte. Così facendo non impesteremo il programma principale con le funzioni relative all' hardware rendendolo più semplice da leggere. Inoltre l' avere tutte le funzioni per la gestione dell' hardware in un solo modulo ci premetterà una facile manutenzione del programma.
In buona sostanza un modulo è costituito da due files: un file sorgente dove risiedono le funzioni ed un file header da includere nel programma che usa questo modulo. Dobbiamo quindi creare questi due files che chiamiamo HAL_pierin.c (dove ci sono le funzioni) e HAL_pierin.h dove ci sono i prototipi di queste funzioni.
Incominciamo con il file header
#ifndef HAL_PIERIN_H #define HAL_PIERIN_H #define LED_PORT PORTB #define LED_DDR DDRB #define LED_MASK 0x01 extern void HAL_LEDon(void); extern void HAL_LEDoff(void); extern void HAL_initLED(void); extern void HAL_toggleLED(void); #endif // Fine file header
Nota: la prima e la seconda linea del file header servono per evitare inclusioni multiple. Una volta incluso il file il simbolo HAL_PIERIN_H sarà definito quindi, in caso di inclusioni annidate i simboli seguenti non verranno ridefiniti e non si genererà nessun errore.
E questo è il file del modulo
#include <avr/io.h> #include <avr/interrupt.h> #include <avr/wdt.h> #include <avr/pgmspace.h> #include "HAL_pierin.h" //-------------------------------------------------------------------------- // void HAL_LEDon(void) // accende il LED void HAL_LEDon(void) { LED_PORT |= LED_MASK; } //-------------------------------------------------------------------------- // void HAL_LEDoff(void) // spegne il LED void HAL_LEDoff(void) { LED_PORT &= (LED_MASK ^ 0xff); } //-------------------------------------------------------------------------- // void HAL_initlED(void) // inizializza l' uscita che pilota il LED void HAL_initLED(void) { // Predispone il pin 0 della la porta B come uscita LED_DDR |= LED_MASK; HAL_LEDoff(); } //-------------------------------------------------------------------------- // void HAL_toggleLED(void) // cambia lo stato del LED void HAL_toggleLED(void) { LED_PORT ^= LED_MASK; } // Fine modulo
Ora basta includere il file header nel programma principale e le funzioni per la gestione dell' hardware saranno disponibili. Il programma principale sarà ora questo:
//-------------------------------------------------------------------------- // Lampeggio.c // // Created: Tanto tempo fa in una galassia molto lontana // Author: TardoFreak //-------------------------------------------------------------------------- #include <avr/io.h> #include <avr/interrupt.h> #include <avr/wdt.h> #include <avr/pgmspace.h> #include "HAL_pierin.h" // Timer software. // Nota: vanno dichiarate come "volatile" per fare in modo che il // valore sia sempre e comunque letto evitando che l' ottimizzazione // non lo faccia. volatile unsigned short SoftTimer1; // Routine di servizio chiamata quando il contenuto del comparatore A // corrisponde al valore del timer. Quando raggiunge tale valore il // timer viene resettato e viene invocata questa routine. ISR(TIMER1_COMPA_vect) { if(SoftTimer1) SoftTimer1--; } //-------------------------------------------------------------------------- int main(void) { // disabilita watchdog MCUSR &= ~(1 << WDRF); wdt_disable(); // Inizializza timer 1 per timeout 10ms // Questa istruzione assicura che l' I/O clock per il timer1 sia abilitato PRR0 &= ~(1<<PRTIM1); // Carica il registro di comparazione per ottenere 20ms OCR1A = 3124; // Enable output compare A match TIMSK1 = (1<<OCIE1A)|(0<<TOIE1); // Avvia il timer1, prescaler 1/64 modo operativo Clear Top Count TCCR1B = (0<<WGM13)|(1<<WGM12)|(0<<CS12)|(1<<CS11)|(1<<CS10); HAL_initLED(); // Abilita le interrupt sei(); while(1) { if(!SoftTimer1) { //PORTB ^= 0x01; // Commuta l' uscita PB0 HAL_toggleLED(); SoftTimer1 = 25; // Carica il timer software per intervallo 500ms. } } }
Avanti così!
Come si dice "abbiamo fatto trenta, facciamo trentuno". E' ora di includere nel modulo di astrazione dell' hardware anche tutte le altre funzioni ed inizializzazioni della macchina compresa la funzione di servizio dell' interrupt ciclica. In questo modo il programma principale sarà ridotto all' essenziale e non ci sarà traccia di qualsiasi cosa dipendente dall' hardware. Aggiungiamo quindi al nostro modulo le funzione di inizializzazione del microcontrollore e dell' interrupt ciclica.
//-------------------------------------------------------------------------- // void initMicro(void) // inizializza il microcontrollore void HAL_initMicro(void) { // disabilita watchdog MCUSR &= ~(1 << WDRF); wdt_disable(); } //-------------------------------------------------------------------------- // void initCyclicInt(void) // Inizializza l' interrupt ciclica void HAL_initCyclicInt(void) { // Inizializza timer 1 per timeout 10ms // Questa istruzione assicura che l' I/O clock per il timer1 sia abilitato PRR0 &= ~(1<<PRTIM1); // Carica il registro di comparazione per ottenere 20ms OCR1A = 3124; // Enable output compare A match TIMSK1 = (1<<OCIE1A)|(0<<TOIE1); // Avvia il timer1, prescaler 1/64 modo operativo Clear Top Count TCCR1B = (0<<WGM13)|(1<<WGM12)|(0<<CS12)|(1<<CS11)|(1<<CS10); }
E quindi facciamo che spostare anche la funzione di servizio dell' interrupt. Questa funzione utilizza però una variabile chiamata SoftTimer1 che è dichiarata come variabile globale. E' buona cosa utilizzare per leggere il suo valore e per impostarlo delle funzioni a posta in modo da essere sicuri che nessuno possa accedervi erroneamente. Quindi dichiariamo la variabile all' interno del modulo HAL e scriviamo le due funzioni per leggerne il valore e per impostarlo.
//-------------------------------------------------------------------------- // void HAL_setTimer1(unsigned int valore) // imposta il valore del timer software 1 void HAL_setTimer1(unsigned int valore) { SoftTimer1 = valore; } //-------------------------------------------------------------------------- // unsigned int HAL_getTimer1(void) // legge il valore del timer software 1 unsigned int HAL_getTimer1(void) { return(SoftTimer1); }
Ora il nostro programma principale sarà ridotto a questo.
//-------------------------------------------------------------------------- // Lampeggio.c // // Created: Tanto tempo fa in una galassia molto lontana // Author: TardoFreak //-------------------------------------------------------------------------- #include <avr/io.h> #include <avr/interrupt.h> #include <avr/wdt.h> #include <avr/pgmspace.h> #include "HAL_pierin.h" //-------------------------------------------------------------------------- int main(void) { HAL_initMicro(); HAL_initCyclicInt(); HAL_initLED(); // Abilita le interrupt sei(); while(1) { if(!HAL_getTimer1()) { HAL_toggleLED(); HAL_setTimer1(25); // Carica il timer software per intervallo 500ms. } } }
Come si può notare non vi è più traccia di operazioni a basso livello ma non abbiamo ancora finito!
Il tocco finale
E' arrivato finalmente il momento di dare il tocco finale all' operazione. Come potete notare ci sono ancora alcune funzioni che sono specifiche del microcontrollore utilizzato come l' istruzione di abilitazione delle interrupt e le inizializzazioni. Vogliamo che queste non solo siano incluse nel modulo relativo all' hardware ma che siano raggruppate in un unica funzione di inizializzazione del sistema. In questo modo potremo anche togliere dal main l' inclusione dei files del micro e lasciare solo l' inclusione del modulo che fa riferimento al microcontrollore ed al suo hardware.
Dobbiamo nascondere al programma principale alcune informazioni come le defines del pin del LED e della variabile volatile utilizzata per il timer software. Scrivendo un unica funzione di inizializzazione potremo anche nascondere le inizializzazioni specifiche della interrupt ciclica, dei LED e del micro. In questo modo siamo sicuri che nessuna parte del programma principale possa fare casini aumentandone l' affidabilità. Inoltre possiamo rendere queste funzioni visibili solo a livello di modulo togliendo i prototipi dal file header ed assicurarci che non possano essere in qualsiasi modo richiamate. Per fare questo useremo la parolina magica static che metteremo davanti alla dichiarazione delle funzioni che vogliamo nascondere. I tre files che compongono il progetto, alla fine di tutto questo lavoro saranno questi.
// File HAL_pierin.h #ifndef HAL_PIERIN_H #define HAL_PIERIN_H extern void HAL_LEDon(void); extern void HAL_LEDoff(void); extern void HAL_toggleLED(void); extern void HAL_setTimer1(unsigned int valore); extern unsigned int HAL_getTimer1(void); extern void HAL_initSystem(void); #endif // Fine file header
// File HAL_pierin.c #include <avr/io.h> #include <avr/interrupt.h> #include <avr/wdt.h> #include <avr/pgmspace.h> #include "HAL_pierin.h" // Defines per il pin di uscita per il pilotaggio del LED #define LED_PORT PORTB #define LED_DDR DDRB #define LED_MASK 0x01 static void HAL_initMicro(void); static void HAL_initCyclicInt(void); // Timer software. // Nota: vanno dichiarate come "volatile" per fare in modo che il // valore sia sempre e comunque letto evitando che l' ottimizzazione // non lo faccia. volatile unsigned short SoftTimer1; //-------------------------------------------------------------------------- // void HAL_LEDon(void) // accende il LED void HAL_LEDon(void) { LED_PORT |= LED_MASK; } //-------------------------------------------------------------------------- // void HAL_LEDoff(void) // spegne il LED void HAL_LEDoff(void) { LED_PORT &= (LED_MASK ^ 0xff); } //-------------------------------------------------------------------------- // void HAL_initLED(void) // inizializza l' uscita che pilota il LED void HAL_initLED(void) { // Predispone il pin 0 della la porta B come uscita LED_DDR |= LED_MASK; HAL_LEDoff(); } //-------------------------------------------------------------------------- // void HAL_toggleLED(void) // cambia lo stato del LED void HAL_toggleLED(void) { LED_PORT ^= LED_MASK; } //-------------------------------------------------------------------------- // void HAL_initMicro(void) // inizializza il microcontrollore static void HAL_initMicro(void) { // disabilita watchdog MCUSR &= ~(1 << WDRF); wdt_disable(); } //-------------------------------------------------------------------------- // void HAL_initCyclicInt(void) // Inizializza l' interrupt ciclica static void HAL_initCyclicInt(void) { // Inizializza timer 1 per timeout 10ms // Questa istruzione assicura che l' I/O clock per il timer1 sia abilitato PRR0 &= ~(1<<PRTIM1); // Carica il registro di comparazione per ottenere 20ms OCR1A = 3124; // Enable output compare A match TIMSK1 = (1<<OCIE1A)|(0<<TOIE1); // Avvia il timer1, prescaler 1/64 modo operativo Clear Top Count TCCR1B = (0<<WGM13)|(1<<WGM12)|(0<<CS12)|(1<<CS11)|(1<<CS10); } //-------------------------------------------------------------------------- // Routine di servizio chiamata quando il contenuto del comparatore A // corrisponde al valore del timer. Quando raggiunge tale valore il // timer viene resettato e viene invocata questa routine. ISR(TIMER1_COMPA_vect) { if(SoftTimer1) SoftTimer1--; } //-------------------------------------------------------------------------- // void HAL_setTimer1(unsigned int valore) // imposta il valore del timer software 1 void HAL_setTimer1(unsigned int valore) { SoftTimer1 = valore; } //-------------------------------------------------------------------------- // unsigned int HAL_getTimer1(void) // legge il valore del timer software 1 unsigned int HAL_getTimer1(void) { return(SoftTimer1); } //-------------------------------------------------------------------------- // void HAL_initSystem(void) // inizializza tutto il sistema void HAL_initSystem(void) { HAL_initMicro(); HAL_initCyclicInt(); HAL_initLED(); // Abilita le interrupt sei(); } // Fine modulo
//-------------------------------------------------------------------------- // Lampeggio.c // // Created: Tanto tempo fa in una galassia molto lontana // Author: TardoFreak //-------------------------------------------------------------------------- #include "HAL_pierin.h" //-------------------------------------------------------------------------- int main(void) { HAL_initSystem(); while(1) { if(!HAL_getTimer1()) { HAL_toggleLED(); HAL_setTimer1(25); // Carica il timer software per intervallo 500ms. } } }
Cambiare pin e microcontrollore
E' facile intuire come si può cambiare il pin che pilota il LED infatti basta modificare le defines poste all' inizio del modulo di astrazione dell' hardware ma non solo, è anche possibile, modificando opportunamente le funzioni di accensione/spegnimento/commutazione del LED utilizzare anche una circuiteria diversa. In queso esempio il pin è collegato al LED con una semplice resistenza ed il LED è collegato a 0V. Nel caso dovessimo per forza, comodità o altri motivi utilizzare un LED collegato verso l' alimentazione sarà sufficiente modificare le funzioni. Ora è evidente che il programma principale è una boiata ma supponiamo di avere un programma grande e complesso dove in più punti è necessario comandare il LED. Se si usassero operazioni a basso livello bisognerebbe modificarle tutte mentre così basta modificare solo le tre funzioni.
E se invece il LED deve essere sostituito, ad esempio, con un pallino su uno schermo LCD? Anche qui il problema non esiste perché sempre solo tre sono le funzioni da modificare ma non si toccherebbe affatto il programma principale.
E se dobbiamo cambiare il microcontrollore sarà sufficiente sviluppare il modulo specifico per il nuovo microcontrollore lasciando così inalterato il programma principale. Se, ad esempio, vorrò utilizzare un PIC o un ARM mi basterà riscrivere le funzioni di gestione dell' hardware per levarmi la paura.
Conclusioni
Ho preso come esempio un programma semplice ma mi pare che oramai sia chiaro che questo modo di scrivere i programmi raggiunge gli obiettivi principali: ordine, facile manutenibiltà, affidabilità e flessibilità. Si potrebbe obiettare che il codice ottenuto è più lento e corposo. Questo potrebbe essere vero se i compilatori fossero stupidi ma i compilatori di oggi sono mooolto furbi. Se si accorgono che una funzione è corta scrivono il codice direttamente senza chiamarla. I compilatori della Mikroelektronika (MikroC), seppur economici, già lo fanno. Per non parlare poi di compilatori come quello della KEIL. Sono talmente furbi che queste cose le fanno senza dire niente a nessuno.
In questo breve articolo ho illustrato solo il layer di astrazione dell' hardware ma non è l' unico layer che di solito si implementa. Una altro layer è il cosiddetto API (Application Program Interface) che solitamente racchiude anche quello di astrazione dell' hardware.
I prefissi HAL che ho messo davanti alle funzioni servono appunto per indicare il layer a cui si fa riferimento proprio per evitare intrusioni accidentali a livelli più bassi.
Un' altra piccola considerazione: la pratica di nascondere informazioni e di incapsulare i moduli dentro altri moduli è quello che poi ha determinato lo sviluppo dei linguaggio orientati a gli oggetti (come il C++) e, di conseguenza, anche quelli fortemente orientati agli oggetti come il Java per sempio. In C++, ad esempio, quando si crea un oggetto viene eseguito subito il metodo chiamato "costruttore" che poi altro non è che la onnipresente funzione di inizializzazione di un modulo scritto in C e l' ereditarità è resa più semplice mentre in C passa attraverso i files header.
Che altro dire? Divertitevi con i micro e, se potete, evitate di prendere cattive abitudini di programmazione in modo da poter scrivere buoni programmi.
La fine del mondo non è arrivata, il Natale è alle porte quindi non mi resta che auguravi BUONE FESTE e buona sperimentazione.