Un ottimo sistema per iniziare un nuovo progetto, indipendente dal microcontrollore o dal sistema di sviluppo utilizzati, è partire da un modello di base. In questo breve articolo illustrò i passi necessari per iniziare un nuovo progetto per il PIERIN PIC18 partendo da un modello di base che ho scritto e che utilizza il sistema di sviluppo MPLAB-IDE unitamente al compilatore MPLB-C18.
Indice |
Creare il proprio progetto
La procedura per creare un proprio progetto partendo da quello di base è semplicissima. Ovviamente bisogna avere installato MPLAB-IDE ed il compilatore MPLAB-C18:
- Allegato a questo articolo c'è un file .rar chiamato base_pierin.rar . Bisogna aprirlo ed estrarre la cartella chiamata base_pierin e metterla da qualche parte, anche sul desktopo del PC.
- Creare la propria cartella del proprio progetto nel punto che più aggrada. Io di solito metto le cartelle dei progetti nella grande cartella Documenti in così che, quando faccio il backup di tale cartella, faccio il backup di tutti i miei lavori. Mettetela comunque dove vi pare, non è importante dove.
- Aprire la cartella base_pierin ed aprire il file base.mcw. MPLAB partirà e vi presenterà il progetto di base.
- Selezionare sul menù Project -> Save Project As ...
- Selezionate la cartella del vostro progetto ed apritela. Dentro non ci troverete niente (ovvio, è vuota!)
- Scrivere nella riga Nome File il nome del vostro progetto, ad esempio mio_progetto. MPLAB salverà nella cartella tutti i files del progetto di base.
A questo punto si può già provare a compilare il progetto. E' vuoto ma ha già tutto quello che serve per partire immediatamente a scrivere il proprio programma, i moduli e tutto il resto.
Sarebbe buona cosa, per verificare che tutto il sistema di sviluppo sia a posto, provare a compilare il progetto appena salvato. Deve poterlo compilare e senza errori.
Uno sguardo alla struttura del progetto
Diamo ora uno sguardo sul progetto e sui files che lo compongono. In MPLAB dovremmo vedere, nella finestra relativa al progetto, questi files:
- main.c E' il file dove è contenuto il programma principale.
- configurazione.h E' il file dove sono contenuti tutti i parametri e le opzioni di configurazione.
- main.h E' (quasi) vuoto e conterrà le defines relative al main che dovranno essere eventualmente visibili a gli altri moduli del progetto.
- rm18f47j53_g.lkr E' lo script del linker.
Analizziamoli uno per uno per capire a cosa servono e perché il progetto è organizzato in questo modo.
main.c
All' inizio del file troviamo questa parte: le inclusioni.
// File di definizione dei registri del micro. #include "p18f47j53.h" // File di configurazione dei fuses #include "configurazione.h" // Mappatura delle interrupt #include "mappa_int.h" // Header del main #include "main.h"
Beh, c'è poco da dire. Il primo file contiene le definizioni del micro ed è quindi necessario. configurazione.h contiene le opzioni del micro. Ho preferito metterlo in un file separato per motivi di ordine e per aumentare la leggibilità del programma. In questo file ci sono le configurazioni che vanno bene praticamente per tutte i programmi che si scrivono per il PIERIN e quindi potremmo anche non prestarci attenzione (anmche se ci andremo a ficcare il naso dentro).
mappa_int.h è un file che contiene una serie di dichiarazioni e trucchetti per fare in modo che il nostro programma funzioni sia che lo carichiamo con il bootloader interno, sia che vogliamo usare il PIERIN con il Pickit3. Non è da modificare e si deve includere per forza.
main.h è l' header del main e per forza di cose va incluso.
Andiamo avanti e guardiamo il resto.
//------------------------------------------------------------------------------ // Variabili globali //------------------------------------------------------------------------------ #pragma udata //------------------------------------------------------------------------------ // Funzione di servizio delle interrupt ad ALTA priorità //------------------------------------------------------------------------------ #pragma code #pragma interrupt highPriorityInterrupt void highPriorityInterrupt() { // Verifica quale flag ha causato l' interrupt // Esegui la parte di codice di servizio dell' interrupt // Azzera il flag che ha causato l' interrupt // ... } //------------------------------------------------------------------------------ // Funzione di servizio delle interrupt a BASSA priorità //------------------------------------------------------------------------------ #pragma interruptlow lowPriorityInterrupt void lowPriorityInterrupt() { // Verifica quale flag ha causato l' interrupt // Esegui la parte di codice di servizio dell' interrupt // Azzera il flag che ha causato l' interrupt // ... }
Troviamo ora lo spazio per dichiarare le variabili globali. E' stato messo prima delle funzioni di servizio delle interrupt proprio perché questa potrebbe utilizzarne. Di seguito troviamo la funzione di servizio delle interrupt ad alta priorita e quelle a bassa priorità. Come è scritto nei commenti all' interno di queste bisogna scrivere il codice per la gestione delle interrupt.
Dopo troviamo l' ultima parte, cioè lo spazio per la dichiarazione dei prototipi delle funzioni e quello per le funzioni stesse.
//------------------------------------------------------------------------------ // Prototipi delle funzioni //------------------------------------------------------------------------------ #pragma code //------------------------------------------------------------------------------ // Funzioni //------------------------------------------------------------------------------ #pragma code //------------------------------------------------------------------------------ // MAIN FUNCTION //------------------------------------------------------------------------------ void main(void) { // -------- Inizializzazione delle periferiche -------- // -------- Selezione ed abilitazione delle interrupt -------- // -------- Attivazione delle periferiche -------- // -------- Ciclo infinito di funzionamento -------- for(;;) { // Inserire il programma qui. } }
Prestiamo ora attenzione al main. E' scritto secondo la struttura di base di un programma per microcontrollore, quindi per prima cosa si dovranno inserire le istruzioni (o le chiamate alle funzioni) per l' inizializzazione delle periferiche (porte, timers, SPI e quant' altro).
Dopo bisognerà scriverci le istruzioni per selezionare che tipo di interrupt intendiamo usare (se solo quelle ad alta priorità o anche quelle a bassa priorità) ed abilitarle. Ovviamente, nella sezione di inizializzazione delle periferiche avremo avuto l' accortezza di abilitare le varie interrupt delle periferiche stesse, interrupt che non partiranno fino a quando non verrano abilitate, cioè fino al punto in cui siamo arrivati adesso.
L' ultima parte dell' inizializzazione è quella dove si attivano le periferiche. E' bene attivarle dopo l' abilitazione delle interrupt perché così siamo sicuri di intercettarle tutte. Ad esempio, se utilizzo un timer che mi deve generare una interrupt prima lo inizializzerò, abiliterò l' interrupt specifica del timer, poi abilito le interrupt in generale e quindi farò partire il timer.
Come ultima cosa troviamo il ciclo infinito di funzionamento. Infinito perché un micro non smette mai di funzionare, al limite lo si può mandare in sleep ma non lo si può fermare o fare terminare il programma come un programma che gira su PC.
Per fare un ciclo infinito si potrebbe anche scrivere
while(1)ma per una ragione, diciamo storica, preferisco usare il for vuoto. Un for vuoto non fa niente la prima volta che viene chiamato, non fa niente quando finisce il ciclo e non controlla niente. La while(1) invece fa un confronto con una costante (probabilmente viene anche tolto dal compilatore) ma anni addietro risparmiare anche solo un paio di byte di programma aveva senso. Così ho imparato e così fò.
configurazione.h
Come si può vedere qui ci sono le varie configurazioni dei registri di opzione e sono adattate per l' ardware del PIERIN e per il fatto che questi può utilizzare la USB. Non è niente di speciale ma uno sguardo al fondo di questo file è bene darlo.
// Opzioni di configurazione. // Qui di seguto ci sono le opzioni di configurazione del microcontrollore. // Per conoscerne il significato consulatre il datasheet del PIC18F47J53 // alla sezione "SPECIAL FEATURES OF THE CPU" che fa riferimento ai // registri di configurazione. // Configurazione del tipo di oscillatore // Oscillatore HS, PLL abilitato e HSPLL utilizzato dalla USB #pragma config OSC = HSPLL // Divisiore del PLL. // Divide per 3 (Quarzo da 12 MHz) #pragma config PLLDIV = 3 // Postscaler per il clock della CPU. // Alla CPU viene mandato il clock senza divisioni #pragma config CPUDIV = OSC1 // RESET in caso di stack overflow/underflow abilitato. #pragma config STVREN = ON // Watchdog disabilitato al RESET- Sarà poi possibile abilitarlo // all' interno del programma tramite il bit SWDTEN #pragma config WDTEN = OFF // Divisore per il clock fornito al watchdog // Clock di sistema diviso per 32768 #pragma config WDTPS = 32768 // Set di istruzioni esteso disabilitato #pragma config XINST = OFF // Protezione della memoria di programma. // La memoria di programma non è protetta. #pragma config CP0 = OFF // Fail-Safe Clock Monitor disabilitato #pragma config FCMEN = OFF // Partenza a doppia velocità disabilitata #pragma config IESO = OFF // Clock per il Deep Sleep Watchdog Timer. // Viene fornito il clock INTOSC/INTRC #pragma config DSWDTOSC = INTOSCREF // Clock per il Real Time Clock Calendar // Il clock proviene dall' oscillatore del Timer 1 #pragma config RTCOSC = T1OSCREF // BOR nello stato di Deep Sleep disabilitato #pragma config DSBOREN = OFF // Deep Sleep watchdog disabilitato #pragma config DSWDTEN = OFF // Deep Sleep watchdog postscaler impostato a 8192 (8,5 secondi) #pragma config DSWDTPS = 8192 // Il bit di sicurezza IOL1WAY può essere attivato e disattivato #pragma config IOL1WAY = OFF // MSSP address masking a 7 bit (indirizzi I2C in funzionamento slave) #pragma config MSSP7B_EN = MSK7 // Protizione delle pagine della FLASH abilitata #pragma config WPDIS = OFF //WPFP[5:0], WPEND, and WPCFG bits ignored // La protezione va dalla pagina 0 alla 1 per proteggere il bootloader #pragma config WPFP = PAGE_1 //Write Protect Program Flash Page 0 #pragma config WPEND = PAGE_0 //Start protection at page 0 #pragma config WPCFG = OFF //Write/Erase last page protect Disabled // Define che indica che verrà utilizzato il bootloader. Il PIERIN utilizza // il bootloader quindi non è da modificiare #define USE_HID_BOOTLOADER // Definizione della frequenza di clock del sistema #define CLOCK_FREQ 48000000 #define GetSystemClock() CLOCK_FREQ
La terzultima define istruisce il progetto sul fatto che verrà utilizzato il bootloader, quindi è meglio lasciarli così com'è a meno che non si utilizzi il PIERIN con un pickit3 ad esempio. In tal caso non serve. Ma se si usa con il bootloader questa define serve ad istruire il file che vedremo dopo per rimappare convenientemente le interrupt.
Le ultime due defines potrebbero anche non esserci ma è meglio che ci siano. Diciamo che è quasi uno standard dei programmi per microcontrollori avere una define e/o una funzione che restituisce un' informazione sulla frequenza di clock del sistema. Serve per i ritardi, per le funzioni che vogliono adattarsi a diverse frequenze. Insomma non mangiano, non bevono, non sporcano neanche ma se ce n' è bisogno sono già belle e pronte.
main.h
Come accennato prima in questo file ci sono le dichiarazioni del main. Visto che però nel mai ci mettiamo già tutto questo file è inutile, giusto? No, sbagliato!
Questo file non serve al main ma a tutti i moduli che in un modo o nell' altro hanno bisogno di rifersi alle defines o dichiarazioni del main. Facciamo un esempio:
Poniamo il caso che io abbia sviluppato un modulo per il pilotaggio di un display LCD. Sarebbe comodo definire, secondo lo standard del modulo, quali sono i pin del micro che utilizzerò per i collegamenti. La cosa più semplice sarebbe scrivere delle defines che mi definiscano la porta ed il pin per ogni segnale. Ma come faccio a farglielo sapere al modulo che contiene le funzioni del display? Dentro il modulo del display inserisco questa semplice linea:
#include "main.h"
In questo modo il mio modulo rimane sempre lo stesso e le defines sono diponibili anche per glialtri moduli. Diciamo che è un modo per definire il profilo hardware del mio progetto (sempre che i moduli siano scritti in modo furbo). Ma guardiamo questo file vuoto:
#ifndef MAIN_H #define MAIN_H // Inserire qui dentro le dichiarazioni. #endif
Lo prendo come spunto per far notare la tecnica comunemente utilizzata per evitare inclusioni multiple. E' noto che se per disgrazia includo più volte lo stesso file il compilatore si arrabbia perché non accetta dichiarazioni duplicate. L' inclusione multipla si evita utilizzando un simbolo che di solito è il nome dell' header. Nel nostro caso la prima riga dice al preprocessore "il simbolo MAIN_H non è ancora stato dichiarato?" se non è stato dichiarato lui lo dichiara per dire al resto del modulo "Guarda che questo file è già stato incluso" e prosegue con l' inclusione. Se all' interno del modulo viene ancora incontrato un #include main.h il simbolo è già stato dichiarato ed il preprocessore salta tutte le dichiarazioni.
Facile, no?
mappa_int.h
Ed eccoci al file che serve per rimappare conveniente le interrupt per il bootloader. Da notare la dichiarazione dei prototipi delle due funzioni di gestione dell' interrupt che abbiamo visto nel main.
//****************************************************************************** // Sezione per la mappatura delle interrupt. // Questo file di configurazione serve per edirezionare opportunamente // Le interrupt sia nei casi in cui si usa il bootloader o meno. // QUESTA PARTE DEL FILE NON DEVE ESSERE MODIFICATA! //****************************************************************************** // Interrupt function prototypes void highPriorityInterrupt(); void lowPriorityInterrupt(); #if defined(USE_HID_BOOTLOADER) #define REMAPPED_RESET_VECTOR_ADDRESS 0x1000 #define REMAPPED_HIGH_INT_VECTOR_ADDRESS 0x1008 #define REMAPPED_LOW_INT_VECTOR_ADDRESS 0x1018 #else #define REMAPPED_RESET_VECTOR_ADDRESS 0x00 #define REMAPPED_HIGH_INT_VECTOR_ADDRESS 0x08 #define REMAPPED_LOW_INT_VECTOR_ADDRESS 0x18 #endif #if defined(USE_HID_BOOTLOADER) extern void _startup (void); // See c018i.c in your C18 compiler dir #pragma code REMAPPED_RESET_VECTOR = REMAPPED_RESET_VECTOR_ADDRESS void _reset (void) { _asm goto _startup _endasm } #endif #pragma code REMAPPED_HIGH_INT_VECTOR = REMAPPED_HIGH_INT_VECTOR_ADDRESS void Remapped_High_ISR (void) { _asm goto highPriorityInterrupt _endasm } #pragma code REMAPPED_LOW_INT_VECTOR = REMAPPED_LOW_INT_VECTOR_ADDRESS void Remapped_Low_ISR (void) { _asm goto lowPriorityInterrupt _endasm } #if defined(USE_HID_BOOTLOADER) //Note: If this project is built while one of the bootloaders has //been defined, but then the output hex file is not programmed with //the bootloader, addresses 0x08 and 0x18 would end up programmed with 0xFFFF. //As a result, if an actual interrupt was enabled and occured, the PC would jump //to 0x08 (or 0x18) and would begin executing "0xFFFF" (unprogrammed space). This //executes as nop instructions, but the PC would eventually reach the REMAPPED_RESET_VECTOR_ADDRESS //(0x1000 or 0x800, depending upon bootloader), and would execute the "goto _startup". This //would effective reset the application. //To fix this situation, we should always deliberately place a //"goto REMAPPED_HIGH_INT_VECTOR_ADDRESS" at address 0x08, and a //"goto REMAPPED_LOW_INT_VECTOR_ADDRESS" at address 0x18. When the output //hex file of this project is programmed with the bootloader, these sections do not //get bootloaded (as they overlap the bootloader space). If the output hex file is not //programmed using the bootloader, then the below goto instructions do get programmed, //and the hex file still works like normal. The below section is only required to fix this //scenario. #pragma code HIGH_INTERRUPT_VECTOR = 0x08 void High_ISR (void) { _asm goto REMAPPED_HIGH_INT_VECTOR_ADDRESS _endasm } #pragma code LOW_INTERRUPT_VECTOR = 0x18 void Low_ISR (void) { _asm goto REMAPPED_LOW_INT_VECTOR_ADDRESS _endasm } #endif //end of "#if defined(PROGRAMMABLE_WITH_USB_HID_BOOTLOADER)"
Poi segue tutto il resto. Senza il bootloader i vettori delle interrupt risiedono a gli indirizzi 0x08 e 0x018 ma se abbiamo il bootloader bisogna dirottarle al di fuori di questo, quindi vengono rimappatte dopo lo spazio occupato dal bootloader e precisamente a gli indirizzi 0x1000 e 0x1008. Anche il RESET deve essere rimappato altrimenti non riusciremmo mai a fare partire il nostro programma, partirebbe sempre e solo il bootloader e questo noi non lo volgiamo.
Lascio alla buona volontà del lettore l' analisi di come è stata fatta la rimappatura perché questa esula dallo scopo di questo articolo.
rm18f47j53_g.lkr
E' il classico file del linker, potremmo anche non guardarlo perché è emozionante come guardare il cemento che si asciuga, ma laparte finale è interessante e, se ben conosciuta, eviterà di imbattersi in quello che è l' incubo dei microcontrollisti: lo stack.
// File: 18f47j53_g.lkr // Generic linker script for the PIC18F47J53 processor #DEFINE _CODEEND _DEBUGCODESTART - 1 #DEFINE _CEND _CODEEND + _DEBUGCODELEN #DEFINE _DATAEND _DEBUGDATASTART - 1 #DEFINE _DEND _DATAEND + _DEBUGDATALEN LIBPATH . #IFDEF _CRUNTIME #IFDEF _EXTENDEDMODE FILES c018i_e.o FILES clib_e.lib FILES p18f47j53_e.lib #ELSE FILES c018i.o FILES clib.lib FILES p18f47j53.lib #FI #FI #IFDEF _DEBUGCODESTART CODEPAGE NAME=bootloader START=0x0 END=0xFFF PROTECTED CODEPAGE NAME=vectors START=0x1000 END=0x1029 PROTECTED CODEPAGE NAME=page START=0x102A END=_CODEEND CODEPAGE NAME=debug START=_DEBUGCODESTART END=_CEND PROTECTED #ELSE CODEPAGE NAME=bootloader START=0x0 END=0xFFF PROTECTED CODEPAGE NAME=vectors START=0x1000 END=0x1029 PROTECTED CODEPAGE NAME=page START=0x102A END=0x1FFF7 #FI CODEPAGE NAME=config START=0x1FFF8 END=0x1FFFF PROTECTED CODEPAGE NAME=devid START=0x3FFFFE END=0x3FFFFF PROTECTED #IFDEF _EXTENDEDMODE DATABANK NAME=gpre START=0x0 END=0x5F #ELSE ACCESSBANK NAME=accessram START=0x0 END=0x5F #FI DATABANK NAME=gpr0 START=0x60 END=0xFF DATABANK NAME=gpr1 START=0x100 END=0x1FF DATABANK NAME=gpr2 START=0x200 END=0x2FF DATABANK NAME=gpr3 START=0x300 END=0x3FF DATABANK NAME=gpr4 START=0x400 END=0x4FF DATABANK NAME=gpr5 START=0x500 END=0x5FF DATABANK NAME=gpr6 START=0x600 END=0x6FF DATABANK NAME=gpr7 START=0x700 END=0x7FF DATABANK NAME=gpr8 START=0x800 END=0x8FF DATABANK NAME=gpr9 START=0x900 END=0x9FF DATABANK NAME=gpr10 START=0xA00 END=0xAFF DATABANK NAME=gpr11 START=0xB00 END=0xBFF #IFDEF _DEBUGDATASTART DATABANK NAME=gpr12 START=0xC00 END=_DATAEND DATABANK NAME=dbgspr START=_DEBUGDATASTART END=_DEND PROTECTED #ELSE //no debug DATABANK NAME=gpr12 START=0xC00 END=0xCFF #FI DATABANK NAME=gpr13 START=0xD00 END=0xDFF DATABANK NAME=gpr14 START=0xE00 END=0xEAF DATABANK NAME=sfr14 START=0xEB0 END=0xEFF PROTECTED DATABANK NAME=sfr15 START=0xF00 END=0xF5F PROTECTED ACCESSBANK NAME=accesssfr START=0xF60 END=0xFFF PROTECTED SECTION NAME=USB_VARS RAM=gpr10 #IFDEF _CRUNTIME SECTION NAME=CONFIG ROM=config #IFDEF _DEBUGDATASTART STACK SIZE=0x100 RAM=gpr11 #ELSE STACK SIZE=0x100 RAM=gpr12 #FI #FI
Guardiamo la parte che inizia da #IFDEF _CRUNTIME. Questa riguarda lo stack. Nel caso si usi il debugger lo stack è allocato in una zona diversa da quella dove sarebbe allocato se non si usasse il debugger, e fin qui tutto fila liscio. E' ovvio che il debugger ha la sua parte di codice che, unitamente al Pickit3, espleta la sua funzione.
Quello che invece vorrei far notare è che è anche indicata la dimensione, che in questo caso è di 256 bytes (0x100). E' bene saperlo perché se non è sufficiente e proprio qui che dobbiamo intervenire per aumentarne la grandezza. Quindi è bene ricordarselo.
Nota importante: non sempre è sufficiente cambiare tale valore per cambiare la grandezza dello stack, sopratutto se lo si vuole aumentare oltre il 256 bytes. La procedura completa la si può ritrovare nel C18 User Guide al capitolo Calling conventions selezionando a fondo pagina il link Managing the Software Stack dove spiega cosa cambiare e la direttiva da aggiungere nelle opzioni del compilatore.
Conclusioni
Bene, siamo arrivati al termine dello spiegone. Ho voluto illustrare per motivi didattici come è stato strutturato questo modello di base. Dal punto di vista pratico non è strettamente necessario sapere per filo e per segno tutto quello che si trova dentro questi file. Infatti la parte più interessante e divertente è il programma che verrà scritto. Questo è solo uno scheletro, una base appunto dalla quale partire.
Tuttavia invito chi ne ha voglia di analizzarsi bene tutto quanto. E' uno studio utile per conoscere bene questo microcontrollore in particolare ma anche i microcontrollori in generale.
Quindi non mi resta che augurare a tutti una BUONA SPERIMENTAZIONE!
Il file con il modello di base si può scaricare a questo link