Sei sulla pagina 1di 507

Terza Edizione

XC8 Step by Step


Imparare a programmare i PIC 18

BK002-IT

Mauro Laurenti

Informativa
Come prescritto dall'art. 1, comma 1, della legge 21 maggio 2004 n.128, l'autore avvisa
di aver assolto, per la seguente opera dell'ingegno, a tutti gli obblighi della legge 22 Aprile
del 1941 n. 633, sulla tutela del diritto d'autore.
Tutti i diritti di questa opera sono riservati. Ogni riproduzione ed ogni altra forma di
diffusione al pubblico dell'opera, o parte di essa, senza un'autorizzazione scritta
dell'autore, rappresenta una violazione della legge che tutela il diritto d'autore, in
particolare non ne consentito un utilizzo per trarne profitto.
La mancata osservanza della legge 22 Aprile del 1941 n. 633 perseguibile con la
reclusione o sanzione pecuniaria, come descritto al Titolo III, Capo III, Sezione II.
A norma dell'art. 70 comunque consentito, per scopi di critica o discussione, il riassunto
e la citazione, accompagnati dalla menzione del titolo dell'opera e dal nome dell'autore.

Avvertenze
Chiunque decida di far uso delle nozioni riportate nella seguente opera o decida di
realizzare i circuiti proposti, tenuto a prestare la massima attenzione in osservanza alle
normative in vigore sulla sicurezza.
L'autore declina ogni responsabilit per eventuali danni causati a persone, animali o
cose derivante dall'utilizzo diretto o indiretto del materiale, dei dispositivi o del software
presentati nella seguente opera.
Si fa inoltre presente che quanto riportato viene fornito cosi com', a solo scopo didattico
e formativo, senza garanzia alcuna della sua correttezza.
Tutti i marchi citati in quest'opera sono dei rispettivi proprietari.

Copyright 2014 Mauro Laurenti

Licenza Software
Tutto esempi Software presentati in questo libro sono rilasciati sotto licenza GNU, di
seguito riportata. Le librerie utilizzate potrebbero essere soggette a differente licenza
dunque per un utilizzo corretto del software e sua ridistribuzione far riferimento alle
particolari librerie incluse.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
Additional Terms
As stated in section 7 of the license, these additional terms are included:
1) The copyright statement must be preserved.
2) Any Warranty Disclaimers must be preserved.

Copyright 2014 Mauro Laurenti

Presentazione alla Terza Edizione


Il testo C18 Step by Step ha raggiunto oltre 30.000 Download diventando la guida di
riferimento per iniziare a programmare i microcontrollori PIC18. Sebbene la seconda
edizione abbia apportato molte modifiche e miglioramenti, lo spazio per migliorare e
giustificare una terza edizione non mancato. Aggiornamenti da parte di Microchip con
l'ambiente di sviluppo MPLAB X e relativo compilatore XC8, hanno forzato in un certo
qual modo un aggiornamento, ma al tempo stesso numerosi feedback hanno avuto modo
di prendere forma in questa terza edizione.
La nuova edizione apporta un piccolo cambiamento nel titolo del testo, divenuto
XC8 Step by Step. Dal momento che il nuovo ambiente di sviluppo diverso da quello
presentato nel testo C18 Step by Step, entrambe le edizioni rimangono valide e disponibili
ma i numerosi miglioramenti ed estensioni apportate in ogni Capitolo fanno del testo
XC8 Step by Step la guida migliore per iniziare.
Visto il numero crescente di professori che fanno uso del testo come guida per corsi
pomeridiani per studenti volenterosi, ogni Capitolo stato riorganizzato fornendo un
riassunto di fine Capitolo e approfondimenti con proposte di esercitazioni e domande.
Spesso le risposte alle domande non si trovano nel testo, costringendo ad una ricerca su
fonti esterne, fornendo dunque un'ulteriore ragione di studio e approfondimento. Molti
Capitoli introducono inoltre degli esempi Progetto, ovvero esempi pi organici e di
utilit pratica. Gli esempi di questa categoria non sono volutamente documentati con lo
stesso livello di dettagli che caratterizzano gli esempi pi facili. Questa scelta stata fatta
al fine di favorire un ulteriore occasione di discussione.
Ogni Capitolo introduce nuovi esempi al fine di completare le applicazioni base con
cui utilizzare i moduli interni. In aggiunta a nuovi esempi, sono presenti i nuovi Capitoli
relativi al modulo SPI e al display GLCD ed altri nati da approfondimenti ed estensioni
dei vecchi.
Sebbene il corso faccia riferimento alla scheda di sviluppo Freedom II e Freedom
Light, ogni Capitolo possiede ora il dettaglio dello schema elettrico minimo necessario per
eseguire gli esempi proposti, permettendo dunque di realizzare anche delle schede di
sviluppo dedicate.
Il crescente numero di schede di sviluppo offerte nel sito LaurTec permette inoltre di
utilizzare varie schede a seconda delle proprie esigenze ed obiettivi. Tra le principali
schede di sviluppo si ricordano Freedom II, Freedom Light, EasyUSB e miniCOM USB.
Una nuova edizione offre nuove opportunit di apprendimento, ma si presenta pur
sempre con limiti e possibilit di miglioramenti.
Ringrazio anticipatamente per la segnalazione di ogni errore.

Formattazione del testo


Il testo segue una formattazione standard, al fine di rendere la sua lettura quanto pi
scorrevole possibile. In particolare si sono seguite le seguenti regole:
Testo Standard
Il seguente testo rappresenta il formato standard utilizzato nei vari capitoli al fine di
descrivere i vari argomenti.
Testo inglese
Il testo in inglese, almeno dei termini poco usati in italiano, sono evidenziati in corsivo.
Codice Sorgente
Il codice di un programma evidenziato nel seguente modo:
int main (void) {
// Il mio programma
}

Voci di menu e percorsi


Voci del menu e percorsi sono evidenziati nel seguente modo:
File Save

Nota

Parti del testo a cui si vuole dare particolare risalto sono scritti come testo
standard al fianco dell'icona Nota.

Com' organizzato il testo


I vari Capitoli del testo XC8 Step by Step sono organizzati in modo da guidare passo
passo il lettore, nello sviluppo dei propri sistemi embedded. Il testo assume ad ogni passo
che il lettore sappia quanto spiegato precedentemente, dunque consigliabile che la
lettura avvenga seguendo l'ordine dei Capitoli proposto. Tutto il Software scritto in
maniera tale che possa essere facilmente copiato ed incollato nel proprio progetto. Tutti i
progetti sono inoltre raccolti e scaricabili direttamente dal sito www.LaurTec.it. Alcuni
progetti pi complessi e lunghi, non sono riportati nel testo ma solo nel file degli esempi
scaricabile dal sito. I vari Capitoli possono essere raggruppati nel seguente modo:
Capitolo I, II, III,
In questi Capitoli vengono introdotti i concetti base dei PIC18 e gli strumenti necessari
per lo sviluppo di applicazioni embedded. In particolare viene introdotta l'architettura dei
PIC18 in maniera da poter affrontare le applicazioni descritte nei Capitoli successivi.
Capitolo IV, V,
Il Capitolo IV mostra un primo esempio di programmazione senza entrare nel vivo
delle potenzialit dei PIC18, mostrando piuttosto la semplicit d'uso. Il Capitolo V mostra
come simulare i programmi senza necessitare di una scheda di sviluppo per i test. In
questo Capitolo sono introdotti anche alcuni concetti per il Debug.
Capitolo VI, VII, VIII
Nel Capitolo VI sono introdotti tutti gli aspetti fondamentali della programmazione in
C focalizzando il suo utilizzo alla programmazione dei PIC18. Numerosi esempi sono
utilizzati per ogni istruzione. La spiegazione descrive anche le possibilit di ottimizzazione
del codice, mostrando in particolare l'utilit di scrivere un codice chiaro piuttosto che
ottimizzato e difficile da leggere. In particolare viene spesso messo in evidenza che un
codice apparentemente ottimizzato non necessariamente un codice pi veloce!
Il Capitolo VII introduce il concetto di funzione e libreria, al fine di permettere la scrittura
di programmi pi complessi e mantenere un certo ordine strutturale.
Dopo aver appreso gli aspetti base della programmazione, il Capitolo VIII introduce le
interruzioni, aprendo una nuova forma per organizzare un programma, sfruttando i
segnali interni forniti da molti moduli interni.
Capitolo IX, X, XI, XII, XIII, XIV, XV, XVI
Ogni Capitolo affronta una particolare periferica interna ed esterna al PIC, il cui
utilizzo permette di realizzare applicazioni complesse e professionali. Ogni Periferica
trattata accompagnata dalla spiegazione dell'hardware e sue impostazioni. A seconda dei
casi vengono inoltre introdotte le librerie Microchip e LaurTec appositamente scritte per
controllare l'Hardware d'interesse.

il tempo di pensare a chi dedicare le pagine che seguiranno queste parole. Credo che non bisogna

forse pensare troppo che se questo libro nato legato al fatto che io stesso sia nato e cresciuto con i giusti
stimoli ed interessi, dunque non posso non dedicare questo libro ai miei familiari, pap, mamma e i miei
tre fratelli e singola sorellina. Vorrei fare anche un passo in alto e ricordare mio nonno Adolfo che tanto
mi ha stimato per i traguardi raggiunti ma che sfortunatamente non ha avuto modo di vedere il tempo
della mia laurea e quello che sarebbe seguito.
Non vorrei dimenticare gli altri miei nonni i cui ricordi sono per offuscati dalla giovane et in cui li
ho conosciuti e visti per l'ultima volta...ma ero il loro Bobo.
Last but not least un pensiero al mio amore che mi sopporta da un po' di tempo...ma la vita
ancora lunga...!
Mauro Laurenti

Indice
Presentazione alla Terza Edizione.........................................................................................................4
Formattazione del testo...........................................................................................................................5
Com' organizzato il testo.......................................................................................................................6
Capitolo I................................................................................................................................................14
Un mondo programmabile...................................................................................................................14
Cos' un microcontrollore ..................................................................................................................14
I sistemi embedded ............................................................................................................................16
I microcontrollori della Microchip.....................................................................................................17
Cosa c' oltre al mondo Microchip.....................................................................................................19
Altri microcontrollori.........................................................................................................................21
Riassunto............................................................................................................................................23
Punti chiave ed approfondimenti........................................................................................................23
Domande ed Esercizi..........................................................................................................................24
Capitolo II..............................................................................................................................................25
Strumenti per iniziare...........................................................................................................................25
Perch si scelto il mondo Microchip...............................................................................................25
L'ambiente di sviluppo MPLAB X.....................................................................................................26
Il compilatore XC8.............................................................................................................................28
Utilizzare il C o l'Assembly?..............................................................................................................31
Il programmatore................................................................................................................................33
Scheda di sviluppo e test ...................................................................................................................35
Hardware di sviluppo base.................................................................................................................38
Riassunto............................................................................................................................................42
Punti chiave ed approfondimenti........................................................................................................42
Domande ed Esercizi..........................................................................................................................43
Capitolo III.............................................................................................................................................44
Architettura ed Hardware dei PIC18 .................................................................................................44
Architettura dei PIC18 .......................................................................................................................44
Organizzazione della Memoria...........................................................................................................49
L'oscillatore .......................................................................................................................................59
Power Management............................................................................................................................64
Circuiteria di Reset.............................................................................................................................67
Le porte d'ingresso uscita...................................................................................................................72
Riassunto............................................................................................................................................77
Punti chiave ed approfondimenti........................................................................................................78
Domande ed Esercizi..........................................................................................................................78
Capitolo IV.............................................................................................................................................80
Salutiamo il mondo per iniziare ..........................................................................................................80
Schema elettrico.................................................................................................................................80
Impostare le schede di sviluppo.........................................................................................................82
Impostazioni software........................................................................................................................82
Il nostro primo progetto......................................................................................................................83
Impostare l'ambiente di lavoro...........................................................................................................91
Scrivere e Compilare un progetto ......................................................................................................95
Programmare il microcontrollore.....................................................................................................101
Capire come salutare il mondo.........................................................................................................103
Riassunto..........................................................................................................................................111
Punti chiave ed approfondimenti......................................................................................................111
Copyright 2014 Mauro Laurenti

Domande ed Esercizi........................................................................................................................112
Capitolo V............................................................................................................................................113
Simuliamo i nostri programmi...........................................................................................................113
Schema elettrico...............................................................................................................................113
Impostare le schede di sviluppo.......................................................................................................114
Impostazioni software......................................................................................................................116
Fase di Simulazione e Debug...........................................................................................................116
Avvio di una sessione di Simulazione..............................................................................................117
Avvio di una sessione di Debug.......................................................................................................120
Il codice Assembly...........................................................................................................................121
Utilizzo dei Breakpoint.....................................................................................................................124
Controllo delle variabili e registri.....................................................................................................128
Controllo della memoria...................................................................................................................131
Analisi temporale..............................................................................................................................132
Semplici regole di programmazione.................................................................................................136
Districarsi tra il codice......................................................................................................................138
Riassunto..........................................................................................................................................141
Punti chiave ed approfondimenti......................................................................................................141
Domande ed Esercizi........................................................................................................................142
Capitolo VI...........................................................................................................................................143
Impariamo a programmare in C.......................................................................................................143
Schema elettrico...............................................................................................................................143
Impostare le schede di sviluppo.......................................................................................................144
Impostazioni software......................................................................................................................145
La sintassi in breve...........................................................................................................................146
Tipi di variabili intere.......................................................................................................................148
Tipi di variabili floating point..........................................................................................................155
Dichiarazione ed inizializzazione di variabili..................................................................................158
Gli Array...........................................................................................................................................160
Le strutture........................................................................................................................................164
Le costanti........................................................................................................................................167
I puntatori.........................................................................................................................................169
Operatori matematici........................................................................................................................175
Casting delle variabili.......................................................................................................................182
Operatori logici.................................................................................................................................184
Operatori bitwise .............................................................................................................................185
Istruzione condizionale while ( )......................................................................................................190
Il ciclo for ( ).....................................................................................................................................195
Istruzione condizionale if ( ).............................................................................................................203
Istruzione condizionale switch ( ).....................................................................................................210
Riassunto..........................................................................................................................................216
Punti chiave ed approfondimenti......................................................................................................216
Domande ed Esercizi........................................................................................................................217
Capitolo VII.........................................................................................................................................219
Le Funzioni..........................................................................................................................................219
Schema elettrico...............................................................................................................................219
Impostare le schede di sviluppo.......................................................................................................220
Impostazioni software......................................................................................................................221
Cosa sono le funzioni.......................................................................................................................221
Visibilit delle variabili....................................................................................................................226
Passaggio di variabili per valore e riferimento.................................................................................232
9

Scriviamo una libreria personale......................................................................................................234


Perch scrivere una libreria..............................................................................................................235
Le direttive........................................................................................................................................236
Progetto 1 Creare una Libreria......................................................................................................239
Utilizziamo la nostra Libreria..........................................................................................................244
Documentazione del codice..............................................................................................................246
Riassunto..........................................................................................................................................249
Punti chiave ed approfondimenti......................................................................................................249
Domande ed Esercizi........................................................................................................................251
Capitolo VIII........................................................................................................................................252
Le Interruzioni.....................................................................................................................................252
Schema elettrico...............................................................................................................................252
Impostare le schede di sviluppo.......................................................................................................254
Impostazioni software......................................................................................................................255
Che cosa sono le interruzioni?..........................................................................................................255
Le interruzioni..................................................................................................................................255
Considerazioni sull'uso delle ISR.....................................................................................................264
Riassunto..........................................................................................................................................268
Punti chiave ed approfondimenti......................................................................................................268
Domande ed Esercizi........................................................................................................................269
Capitolo IX...........................................................................................................................................270
Utilizziamo un Display alfanumerico LCD.......................................................................................270
Schema elettrico...............................................................................................................................270
Impostare le schede di sviluppo.......................................................................................................272
Impostazioni software......................................................................................................................273
Descrizione dell'hardware e sue applicazioni...................................................................................274
Utilizzo della libreria LCD...............................................................................................................276
Leggiamo finalmente Hello World...................................................................................................280
Progetto 2 Creare un menu multilingua.........................................................................................287
Riassunto..........................................................................................................................................292
Punti chiave ed approfondimenti......................................................................................................292
Domande ed Esercizi........................................................................................................................293
Capitolo X............................................................................................................................................294
Utilizziamo un Display Grafico LCD................................................................................................294
Schema elettrico...............................................................................................................................294
Impostare le schede di sviluppo.......................................................................................................296
Impostazioni software......................................................................................................................297
Descrizione dell'hardware e sue applicazioni...................................................................................298
Utilizzo della libreria GLCD............................................................................................................303
Disegniamo Hello World..................................................................................................................307
Progetto 3 Rappresentare dati in un istogramma...........................................................................309
Riassunto..........................................................................................................................................312
Punti chiave ed approfondimenti......................................................................................................312
Domande ed Esercizi........................................................................................................................312
Capitolo XI...........................................................................................................................................313
Utilizziamo l'EUSART interna al PIC..............................................................................................313
Schema elettrico...............................................................................................................................313
Impostare le schede di sviluppo.......................................................................................................314
Impostazioni software......................................................................................................................315
Il protocollo RS232..........................................................................................................................315
Descrizione del modulo EUSART...................................................................................................319
10

Registri di configurazione del modulo EUSART.............................................................................322


Libreria per l'utilizzo del modulo EUSART.....................................................................................326
Esempio di utilizzo dell'EUSART in polling...................................................................................332
Esempio di utilizzo dell'EUSART con interruzione.........................................................................335
Progetto 4 Controllo remoto di LED.............................................................................................339
Riassunto..........................................................................................................................................342
Punti chiave ed approfondimenti......................................................................................................342
Domande ed Esercizi........................................................................................................................343
Capitolo XII.........................................................................................................................................344
Utilizziamo il modulo I2C interno al PIC.........................................................................................344
Schema elettrico...............................................................................................................................344
Impostare le schede di sviluppo.......................................................................................................346
Impostazioni software......................................................................................................................347
Il protocollo I2C...............................................................................................................................347
Descrizione del modulo I2C.............................................................................................................355
Registri di configurazione del modulo I2C......................................................................................357
Libreria per l'utilizzo del modulo I2C..............................................................................................363
Esempio Master - Slave, comunicazione tra due PIC......................................................................367
Esempio di lettura di una memoria EEPROM I2C..........................................................................371
Esempio di utilizzo di un Clock Calendar I2C.................................................................................376
Il programma funziona, quindi tutto bene........................................................................................381
Riassunto..........................................................................................................................................382
Punti chiave ed approfondimenti......................................................................................................382
Domande ed Esercizi........................................................................................................................383
Capitolo XIII........................................................................................................................................384
Utilizziamo il modulo SPI interno al PIC..........................................................................................384
Schema elettrico...............................................................................................................................384
Impostare le schede di sviluppo.......................................................................................................386
Impostazioni software......................................................................................................................387
Il protocollo SPI...............................................................................................................................387
Descrizione del modulo SPI.............................................................................................................397
Registri di configurazione del modulo SPI......................................................................................398
Libreria per l'utilizzo del modulo SPI..............................................................................................402
Esempio Master - Slave, comunicazione tra due PIC......................................................................404
Progetto 5 Generatore di tensione di riferimento .........................................................................408
Riassunto..........................................................................................................................................411
Punti chiave ed approfondimenti......................................................................................................411
Domande ed Esercizi........................................................................................................................412
Capitolo XIV........................................................................................................................................413
Utilizziamo i Timer interni al PIC.....................................................................................................413
Schema elettrico...............................................................................................................................413
Impostare le schede di sviluppo.......................................................................................................414
Impostazioni software......................................................................................................................415
Descrizione ed applicazione dei Timer............................................................................................415
Descrizione del modulo Timer0.......................................................................................................416
Registri di configurazione del modulo Timer0................................................................................418
Descrizione del modulo Timer1.......................................................................................................420
Registri di configurazione del modulo Timer1................................................................................421
Descrizione del modulo Timer2.......................................................................................................423
Registri di configurazione del modulo Timer2................................................................................424
Descrizione del modulo Timer3.......................................................................................................426
11

Registri di configurazione del modulo Timer3................................................................................427


Esempi di utilizzo del Timer0..........................................................................................................429
Impostare il tempo di un Timer........................................................................................................435
Progetto 6 Frequenzimetro onda quadra da 2MHz........................................................................437
Riassunto..........................................................................................................................................438
Punti chiave ed approfondimenti......................................................................................................438
Domande ed Esercizi........................................................................................................................439
Capitolo XV.........................................................................................................................................440
Utilizziamo il modulo CCP interno al PIC........................................................................................440
Schema elettrico...............................................................................................................................440
Impostare le schede di sviluppo.......................................................................................................441
Impostazioni software......................................................................................................................442
Descrizione del modulo CCP...........................................................................................................443
Descrizione del modulo CCP modalit Capture............................................................................443
Descrizione del modulo CCP modalit Compare..........................................................................444
Descrizione del modulo CCP modalit PWM...............................................................................445
Registri di configurazione del modulo CCP.....................................................................................450
Libreria per l'utilizzo del modulo CCP - PWM................................................................................454
Esempio di utilizzo del modulo CCP - Capture...............................................................................455
Esempio di utilizzo del modulo CCP - Compare.............................................................................459
Esempio di utilizzo del modulo CCP - PWM..................................................................................462
Progetto 7 Generatore segnale ECG..............................................................................................465
Riassunto..........................................................................................................................................470
Punti chiave ed approfondimenti......................................................................................................471
Domande ed Esercizi........................................................................................................................471
Capitolo XVI........................................................................................................................................472
Utilizziamo il modulo ADC interno al PIC.......................................................................................472
Schema elettrico...............................................................................................................................472
Impostare le schede di sviluppo.......................................................................................................473
Impostazioni software......................................................................................................................474
Conversione analogico digitale........................................................................................................475
Descrizione del modulo ADC..........................................................................................................478
Registri di configurazione del modulo ADC....................................................................................483
Libreria per l'utilizzo del modulo ADC............................................................................................488
Esempio di lettura di una tensione....................................................................................................493
Esempio di lettura della temperatura................................................................................................497
Esempio di lettura dell'intensit luminosa........................................................................................500
Progetto 8 Misura multipla ADC con lettura remota....................................................................502
Riassunto..........................................................................................................................................503
Punti chiave ed approfondimenti......................................................................................................503
Domande ed Esercizi........................................................................................................................504
Bibliografia..........................................................................................................................................505
Internet Link.....................................................................................................................................505

12

Indice dei Progetti


Capitolo VII.........................................................................................................................................219
Progetto 1 Creare una Libreria......................................................................................................239
Capitolo IX...........................................................................................................................................270
Progetto 2 Creare un menu multilingua.........................................................................................287
Capitolo X............................................................................................................................................294
Progetto 3 Rappresentare dati in un istogramma...........................................................................309
Capitolo XI...........................................................................................................................................313
Progetto 4 Controllo remoto di LED.............................................................................................339
Capitolo XIII........................................................................................................................................384
Progetto 5 Generatore di tensione di riferimento .........................................................................408
Capitolo XIV........................................................................................................................................413
Progetto 6 Frequenzimetro onda quadra da 2MHz........................................................................437
Capitolo XV.........................................................................................................................................440
Progetto 7 Generatore segnale ECG..............................................................................................465
Capitolo XVI........................................................................................................................................472
Progetto 8 Misura multipla ADC con lettura remota....................................................................502

13

Capitolo I

Un mondo programmabile
Oggigiorno si sente parlare di lavatrici intelligenti, di frigoriferi che si riempiono da soli
e giocattoli che parlano. Tutto questo stato in parte reso possibile grazie ai bassi costi
delle logiche programmabili, tra queste vi sono i microcontrollori. In questo Capitolo
dopo un'introduzione sui microcontrollori e in particolare quelli della Microchip, sono
discusse anche le altre alternative, le quali per non sono ulteriormente trattate nel seguito
del testo.
Il mercato pieno di prodotti validi, e sebbene un testo debba certamente focalizzarsi su
una famiglia, un progettista deve opportunamente valutare quale utilizzare nel proprio
progetto al fine di soddisfare ogni specifica.

Cos' un microcontrollore
Un microcontrollore o microcalcolatore (MCU), un circuito integrato, ovvero un
semiconduttore. Al suo interno sono presenti da poche migliaia a centinaia di migliaia di
componenti elettronici elementari; generalmente le tecnologie attuali utilizzano transistor
CMOS. Le dimensioni del singolo transistor utilizzato nei processi produttivi standard
variano da 65nm a 180nm, anche se tecnologie pi piccole sono gi presenti sul mercato 1.
Fin qui nulla di nuovo se confrontati con gli altri circuiti integrati, ed in particolare con i
microprocessori. In realt ci sono molte differenze che li rendono appetibili rispetto ai
microprocessori. Se consideriamo i microprocessori attualmente sul mercato, per esempio
quelli della Intel o AMD, ben sappiamo che hanno potenze di calcolo enormi, non
sappiamo quanto, ma sappiamo che possiamo farci molto! In realt solo con il
microprocessore non si pu fare nulla! Per poter utilizzare il microprocessore necessario
circondarlo di molto altro Hardware, quale per esempio, la RAM, Hard Disk e di tutte le
altre periferiche necessarie quali per esempio una scheda video, porte di comunicazione
(USB, seriale, Ethernet, ecc...).
Il microprocessore con tutto l'armamentario attorno richiede notevole potenza, non a
caso un PC ha spesso le ventole di raffreddamento!
Bene, al microcontrollore tutto questo non serve, RAM e memoria non volatile
(tipicamente memoria Flash) sono al suo interno, come anche la maggior parte delle
periferiche di base o specifiche per determinate applicazioni 2. In ultimo, ma non meno
importante, il microcontrollore assorbe cosi poco, che alcune applicazioni, tipo orologi
digitali, possono essere alimentati per mezzo di un limone usato come batteria. Se il
microcontrollore ha tutti questi vantaggi vi chiederete per quale ragione si continuano ad
1

Normalmente tecnologie pi piccole si traducono in pi bassi costi per un microcontrollore, visto che a parit di funzioni
possibile usare una minor area di silicio. Ciononostante altre variabili associate al marketing determinano il costo finale di un
microcontrollore a prescindere dal reale costo del componente stesso.
Poich il microcontrollore possiede al suo interno un gran numero di sistemi, viene anche detto SoC, ovvero System on
Chip.

14

utilizzare i microprocessori; la ragione semplice, per poter raggiungere assorbimenti cosi


ridotti e le dimensioni di una lenticchia, la potenza di calcolo deve essere sacrificata.
La parola sacrificata deve essere letta con il corretto peso, infatti non significa che il
microcontrollore destinato ad essere utilizzato in applicazioni tipo il pallottoliere,
mentre il microprocessore far calcoli astrofisici. Infatti la tecnologia e la potenza di
calcolo dei computer a bordo dell'Apollo 11, per mandare l'uomo sulla luna, era inferiore
a quella attualmente possibile da alcuni microcontrollori. In ultimo giusto citare il fatto
che alcuni microcontrollori altro non sono che vecchi progetti di microprocessori estesi a
diventare microcontrollori. Per esempio facile trovare microcontrollori ad 8 bit basati
sulla CPU Z80 o sull'8051. In particolare il core 8051 viene frequentemente utilizzato
come CPU all'interno di circuiti integrati pi complessi. Infatti, quando bisogna
aggiungere delle capacit di calcolo a degli integrati, anche se non sono microcontrollori,
pi facile inserire un core (modulo) bello e pronto. Infatti, far uso di un core standard
significa includere un file di libreria VHDL3 o Verilog4 e far uso dei nomi relativi agli
input e gli output del core. Il VHDL e Verilog sono dei linguaggi descrittivi5 (Hardware
Description Language) per mezzo dei quali vengono progettati i sistemi digitali quali un
microcontrollore.
Da quanto detto, si capisce che non da escludere che qualche microprocessore attuale
diventer un banale core per microcontrollori 6 del futuro. Questa affermazione forse
supportata dalla stessa legge di Moore7 che afferma:
Le prestazioni dei processori e il numero di transistor in esso contenuti, raddoppiano ogni 12
mesi.

Per cui si capisce che quello che oggi rappresenta un'elevata integrazione, domani
rappresenter un semplice circuito integrato ottenibile con processi produttivi standard.

I sistemi embedded
Capiti i pregi e difetti del microprocessore e del microcontrollore vediamo di descrivere
brevemente le applicazioni dove i microcontrollori vengono utilizzati. Esempi tipici sono
elettrodomestici, giocattoli, sistemi di misura dedicati e applicazioni consumer in generale.
Tutte queste applicazioni rientrano in quella pi ampia dei sistemi embedded. Le
applicazioni o sistemi embedded sono un gruppo di applicazioni accomunate dal fatto di
essere un sistema di calcolo progettato per un determinato scopo (ad hoc) il cui hardware
(risorse) disponibili sono ridotte al minimo . Per tale ragione, applicazioni con i
microcontrollori rientrano nella categoria delle applicazioni embedded. Nonostante
un'applicazione embedded sia ritenuta un'applicazione con ridotte risorse hardware, tale
descrizione un po' vaga. Infatti molte schede di sviluppo utilizzate in applicazioni
3

4
5

6
7

VHDL l'acronimo di VHSIC Hardware Description Language, dove VHSIC l'acronimo di Very High Speed Integrated
Circuits.
Verilog un linguaggio descrittivo come il VHDL, ed anche noto come Verilog HDL.
Un linguaggio descrittivo per l'hardware come il VHDL e il Verilog, si presenta molto simile ad un linguaggio di
programmazione ma vi sono molti aspetti che lo rendono differenti, in particolare la concorrenzialit ovvero la
contemporanea esecuzione di parti diverse del codice da parte dell'hardware che traduce il codice stesso.
Come si vedr a breve il core ARM spesso utilizzato all'interno di microcontrollori a 32 bit.
Gordon Moore cofondatore di Intel con Robert Noyce. Nel 1965 ipotizz che le prestazioni dei microprocessori e loro
integrazione sarebbe raddoppiata ogni 12 mesi. Attualmente la legge stata ridimensionata con tempi di 2 anni. Si capisce
che sebbene sia chiamata legge, in realt non c' nulla di matematico o fisico, ma solo una previsione ben posta.

16

embedded altro non sono che mini schede di PC il cui microprocessore era il top di
qualche anno prima. Per esempio schede per applicazioni embedded fanno
frequentemente uso di microprocessori Intel della serie Pentium II e AtomTM. Il Pentium
II quando venne introdotto nel mercato era un gioiello della tecnologia, il cui costo
portava il prezzo di un PC pari al valore di uno stipendio medio italiano del tempo. Oggi
possibile acquistare schede embedded con Pentium II a meno di 100 euro.
Da quando detto si capisce dunque che la tipologia dell'hardware utilizzato in
applicazioni embedded varia con il tempo, e quello che che oggi il top della tecnologia,
domani potr essere utilizzato in applicazioni embedded. In tutto questo, per possibile
dire che per definizione se si fa uso di un microcontrollore invece di un microprocessore,
a buon diritto il sistema da considerarsi embedded. In caso contrario sar il tempo a
decidere.
In ultimo vediamo brevemente cosa significa programmare per un sistema embedded.
Qualora si faccia uso di un microcontrollore di fascia bassa, ovvero con architettura ad 8
o 16 bit, frequentemente il software, escluso le librerie utilizzabili ed offerte con il
compilatore, viene scritto dal nulla e in maniera specifica per l'applicazione. Negli ultimi
anni sono comparsi dei piccoli sistemi operativi RTOS (Real Time Operative System) dedicati
ad applicazioni generiche e utilizzabili su vari microcontrollori in cui sia richiesta la
funzione Real Time. Nel caso in cui si stia sviluppando un sistema utilizzando una
piattaforma con microcontrollore o microprocessore a 32 bit, la questione dello sviluppo
software in generale differente. Per queste strutture, qualora la CPU o MCU possieda la
MMU (Memory Managment Unit), possibile utilizzare sistemi operativi standard quali
Linux8 e Windows. L'installazione e la gestione di applicazioni naturalmente non
semplice come per un PC, visto che i sistemi operativi si trovano ad essere installati in
ambienti un po' stretti! Alcuni esempi di applicazioni a 32 bit dove vengono spesso
utilizzati sistemi operativi come Linux e Windows, sono i palmari, cellulari, router,
stampanti e molti altri. Il sistema operativo Linux stato addirittura installato in un
orologio da polso, al fine di dimostrare le poche risorse necessarie per la sua esecuzione.
L'applicazione non era molto pratica ma stato certamente un esperimento interessante.

I microcontrollori della Microchip


Tra i vari produttori di microcontrollori vie la Microchip, che produce una vasta
gamma di microcontrollori per applicazioni generiche (MCU) e microcontrollori per
applicazione mirate all'elaborazione digitale di segnali analogici (DSC, Digital Signal
Controller o DSP, Digital Signal Controller). La scelta spazia da microcontrollori a 8, 16 e 32
bit. La famiglia ad 8 bit quella che fa di Microchip la societ leader dei microcontrollori,
mentre le strutture a 16 e 32 bit, come vedremo, hanno molti pi competitori.
Cominciamo per gradi e descriviamo le caratteristiche offerte dalle varie famiglie 8, 16, 32
bit.

Architettura ad 8 bit
Come detto la famiglia ad 8 bit rappresenta la pi vasta, questa si divide in:

Vi sono alcuni progetti in cui vengono supportati anche CPU e MCU a 16 bit e senza MMU, ma il loro supporto e
potenzialit ridotto. Per tale ragione, qualora si voglia far uso di Linux, consigliabile utilizzare strutture a 32 bit con
MMU.

17

Architettura base (Base Architecture), PIC10, PIC12, PIC16


Architettura media (Midrange Architecture), PIC12, PIC16
Architettura media potenziata (Enhanced Midrange Architecture), PIC12,
PIC16
Architettura avanzata (Advanced Architecture), PIC18

In Tabella 1 sono riportate le caratteristiche principali di ogni famiglia con i relativi


microcontrollori che ne fanno parte.
Architettura base

Architettura media

Architettura media pot.

Architettura Avanzata

Numero pin

6-40

8-64

8-64

18-100

Performance

5MIPS

5MIPS

8MIPS

10MIPS-16MIPS

Memoria
programma

Fino a 3KB

Fino a 14KB

Fino a 56KB

Fino a 128KB

Memoria Dati

Fino a 138B

Fino a 368B

Fino a 4K

Fino a 4K

no

Si (singola)

Si (singola)

Si (multipla)

Include PIC10, PIC12 e


PIC16

Include PIC12 e PIC16

Include PIC12F1 e PIC16F1

PIC18

Interruzioni
PIC

Tabella 1: Caratteristiche principali delle varie famiglie di microcontrollori a 8 bit.

Come mostrato in Tabella 1 possibile vedere che la Microchip realizza


microcontrollori con soli 6 pin. Questo significa che possibile inserire della piccola
intelligenza in ogni piccolo sistema elettronico. La famiglia pi performante tra
quelle ad 8 bit la famiglia PIC18, di cui andremo a descrivere ogni dettaglio nelle
pagine che seguiranno, ovvero in tutto il testo.
Negli ultimi anni, dopo la crisi del 2009, Microchip ha colto l'occasione per
espandere ed arricchire diverse linee di prodotto, specialmente in ambito RF,
acquisendo societ la cui mission era appunto in ambito RF. Queste acquisizioni
hanno portato indirettamente ad avere microcontrollori basati sull'architettura 8051.
Ciononostante questi microcontrollori non sono pubblicizzati e non appartengono
ai prodotti supportati dall'ambiente di sviluppo comune MPLAB X.

Architettura a 16 bit
La famiglia a 16 bit si suddivide in microcontrollori a 16 bit e DSP (Digital Signal
Processor) noti come dsPIC. Di questa famiglia fanno parte i PIC24 e i dsPIC30,
dsPIC33. I dsPIC si differenziano dalla serie PIC24 solamente per la presenza, nella
serie dsPIC, del core DSP. L'aggiunta del core DSP fa dei dsPIC la scelta ideale in
applicazioni audio, controllo motore e grafiche (con risoluzione QVGA). Nella
famiglia 16 bit possibile trovare periferiche tipo ADC 10/12 bit, DAC 10/16 bit,
USB, DMA (Direct Memory Access9). Il numero di periferiche e risoluzione dei moduli
analogici si va sempre estendendo, visto che tra le applicazioni embedded dei dsPIC
e microcontrollori a 16 bit ci sono gli strumenti di misura residenziali, quali il
contatore della corrente, gas, acqua, per i quali sono richiesti ADC con risoluzioni

La presenza del DMA permette di gestire un grosso flusso dati da diverse periferiche interne e la memoria dati, senza
l'intervento della CPU. Per esempio se si volesse fare una FFT con risoluzione a 64 campioni, sarebbe possibile mandare i
campioni dall'ADC direttamente alla memoria dati, e far s che la CPU inizi ad accedere ai campioni, solo quando sono
presenti tutti e 64.

18

maggiori o uguali a 12 bit. La potenza di calcolo dei PIC a 16 bit di 40 MIPS.


Architettura a 32 bit
La famiglia a 32 bit stata introdotta dalla Microchip nel Novembre del 2007,
con un certo ritardo rispetto ai grandi produttori di microcontrollori. L'architettura
dei PIC32, diversamente da molti microcontrollori a 32 bit non basata su
architettura ARM bens MIPS32 M4K. I microcontrollori PIC32 bit hanno
frequenze di clock fino a 80MHz, il che significa che non possono essere
confrontati con altri microcontrollori ARM a 120MHz e oltre, le cui applicazioni
sono naturalmente differenti. Ciononostante tra i microcontrollori della stessa fascia
di frequenza di clock, la struttura utilizzata da Microchip non ha nulla da invidiare ai
microcontrollori basati su architetture ARM.
Unico neo dei PIC32 che sono arrivati tardi sul mercato. Questo ritardo non ha
nulla a che fare con le performance, ma ha ridotto Microchip in una pi ristretta
fascia di mercato di partenza.

Cosa c' oltre al mondo Microchip


Dopo tutto questo parlare di Microchip rischio d'indottrinarvi a tal punto che
ignorerete il mondo esterno. Il mondo bello perch vario, e il mondo dei
microcontrollori e delle logiche programmabili non da meno. Utilizzare le soluzioni di
un solo costruttore pu essere limitante, anche se certamente pratico. Frequentemente la
scelta di una particolare famiglia di microcontrollori discende da aspetti pratici, quali per
esempio, conoscenze disponibili, costi per iniziare e materiale informativo disponibile
(Application Notes, Libri, esempi di programmazione). Molti ingegneri dopo aver letto
questo testo tenderanno per esempio ad utilizzare i PIC18 in molte applicazioni dove
anche altri microcontrollori o famiglie di microcontrollori risultino pi economici. Iniziare
a studiare un microcontrollore di una nuova famiglia richiede sempre del tempo da
investire.
In progetti in cui si dovessero produrre un milione di pezzi all'anno, si tende a scegliere
il microcontrollore pi economico tra quelli che possono soddisfare le specifiche di
progetto, visto che i costi di avvio sono generalmente trascurabili rispetto a quelli che si
perderebbero scegliendo un microcontrollore pi costoso. Apriamo la finestra e vediamo
cosa possiamo trovare nel mercato:
FPGA e CPLD

Come prima cosa, doveroso almeno citare le logiche programmabili per eccellenza, le
CPLD e FPGA10. CPLD l'acronimo inglese Complex Programmable Logic Device mentre
FPGA l'acronimo inglese di Field Programmable Gate Array. Tale dispositivi sono
composti rispettivamente da migliaia e centinaia di migliaia di porte elementari o gate. La
differenza principale tra le due famiglie di dispositivi proprio il numero di porte e le
tipologie di moduli integrati, ovvero la complessit interna, quindi il costo!
In particolare le logiche CPLD coprono applicazioni di fascia bassa 11 mentre le FPGA
10

11

I loro predecessori, ancora utilizzati ma pi per esigenze di vecchi progetti che per nuovi, sono i PLD e PAL. Il CPLD
praticamente uguale ma supporta un numero maggiore di gate e moduli interni. Tra i pi grandi produttori di FPGA e
CPLD si ricorda Xilinx e Altera.
Visti i costi ridotti delle logiche CPLD, queste risultano, da un punto di vista economico, anche una valida alternativa ai
microcontrollori, ma come sempre il tutto dipende dal progetto, quindi dalle sue specifiche.

19

quelle di fascia medio alta. La programmazione consiste nel bruciare o meno alcuni
fusibili12 per permettere o meno la connessione di determinate porte. In questo modo
possibile creare anche piccole CPU13 o comunque una qualunque funzione logica.
Frequentemente le FPGA e CPLD gi incorporano moduli elementari quali CPU o
ALU14. Le FPGA di ultima generazione hanno incorporati anche dei core ARM Cortex
M, che come vedremo a breve, rappresenta il core standard per molti microcontrollori a
32 bit. Questi dispositivi programmabili vengono utilizzati al posto dei microcontrollori
quando richiesto un elevato data throughput, ovvero una elevata quantit di dati deve
passare al loro interno. Per esempio per comunicazioni ad alta velocit (PCI express),
gestione bus o macchine a stati. In particolare le FPGA pi potenti possono essere
utilizzate in applicazioni dove le CPU pi veloci risultano troppo lente! Non un caso che
applicazioni militari tipo i missili abbondano di FPGA!
Altre applicazioni tipiche dove le FPGA trovano un largo utilizzo, sono le strumentazioni
elettroniche, come per esempio gli oscilloscopi, gli analizzatori di spettro e analizzatori di
rete. Infatti poter gestire ADC (Analog to Digital Converter15) con frequenze di
campionamento superiori al GS/s si ha necessariamente bisogno di FPGA. Le
elaborazioni delle immagini relative ad una misura, sono frequentemente delegate ad un
microcontrollore o DSP a 32bit che spesso affiancano le FPGA.
DSP

I DSP, acronimo inglese di Digital Signal Processing, o Processor, sono praticamente dei
microcontrollori ottimizzati per applicazioni di elaborazione dei segnali, ovvero in cui un
segnale analogico convertito in digitale, richiede di essere elaborato al fine di estrarre
informazioni utili. Normalmente quello che li differenzia dai microcontrollori standard
non tanto la frequenza di clock, che comunque pi o meno elevata 16, bens la presenza
di pi accumulatori (registri speciali per fare operazioni), operatori di somma e
spostamento, ottimizzati per implementare filtri digitali FIR17 e IIR18 e bus dedicati per
accedere a pi aree di memoria in contemporanea 19. I DSP sono normalmente utilizzati
per applicazioni audio e video. Molti cellulari e ripetitori per cellulari, oltre ad avere
FPGA hanno frequentemente anche dei DSP. Per tale ragione le FPGA pi potenti
stanno avendo la tendenza d'implementare core (moduli) DSP direttamente all'interno
dell'FPGA come anche microcontrollori della famiglia Cortex M.
ARM

ARM, acronimo di Advanced RISC Machine, rappresenta un core venduto su licenza


dalla ARM Holding Ltd. ed integrato all'interno di microprocessori e microcontrollori.
ARM Holding Ltd non produce circuiti integrati ma offre solo licenze per il core
progettato. Come vedremo nei Capitoli successivi, i PIC della Microchip utilizzano una
struttura RISC non di tipo ARM. Il vantaggio di utilizzare microprocessori o
microcontrollori basati sul core ARM sta nel fatto che sono tra loro pi o meno
12

13
14
15
16
17
18
19

I fusibili dei primi modelli di FPGA e CPLD permettevano una sola scrittura. Le tecnologie attuali permettono invece di
riscrivere i dispositivi come se fossero delle memorie Flash.
CPU l'acronimo inglese di Central Processing Unit.
ALU l'acronimo inglese per Arithmetic Logic Unit.
ADC l'acronimo di Analog to Digital Converter, ovvero convertitore Analogico Digitale.
I DSP pi potenti attualmente sul mercato hanno un dual e quad core e frequenza di clock di circa 2GHz.
FIR l'acronimo inglese per Finite Impulse Response.
IIR l'acronimo inglese per Infinite Impulse Response.
Poter accedere a pi aree di memoria in contemporanea permette in generale di poter eseguire istruzioni che utilizzano pi
operandi, in un tempo ridotto, spesso pari ad un solo ciclo di clock.

20

compatibili e vi sono strumenti di sviluppo e Debug prodotti da molte societ. Questa


variet e ricchezza di strumenti sta permettendo una continua espansione del core ARM
di cui vengono rilasciate continuamente nuove versioni. Un'altra ragione per cui il core
ARM sta avendo successo legato al fatto che in applicazioni embedded viene sempre pi
utilizzato Linux che pu essere compilato per mezzo di cross compiler anche per il core
ARM. La struttura ARM non per la sola a condividere le grazie di Linux,
microprocessori a 32 bit basati su architettura 80x86 e MIPS sono altrettanto usati.
L'architettura ARM disponibile in varie versioni, quella utilizzata nei microcontrollori
la cosiddetta famiglia Cortex M che appartiene alla versione ARMv7-M, dove in
particolare si ricordano le variati Cortex M0, M0+, M3 e M420. Il modello Cortex M0+
stato recentemente introdotto sul mercato per supportare le applicazioni a bassa energia
mentre. Il modello Cortex M4 stato introdotto per supportare applicazioni in cui si fa
tipicamente uso dei DSP, anche se il modello Cortex M4 non ha un core DSP. Il core
Cortex M4 possibile pensarlo come un superset del Cortex M3, dove le nuove istruzioni
sono state pensate per supportare applicazioni analogiche quali filtri digitali o sistemi di
controllo. Oltre alla famiglia Cortex M presente la serie Cortex R per applicazioni Real
Time e la famiglia Cortex A per applicazioni high hand quali per esempio tablet, cellulari,
TV set, dove normalmente richiesto un sistema operativo.

Altri microcontrollori
Non gentile chiamare un sotto paragrafo altri microcontrollori ma sono talmente
tanti e ognuno meriterebbe un testo a parte, per cui la lista che segue deve intendersi pi
come un breve riassunto delle offerte del mercato piuttosto che una successione di
alternative ordinate per importanza. Per tale ragione questo raggruppamento non li deve
affatto mettere in ombra visto che i microcontrollori dei produttori descritti sono molto
utilizzati e Microchip stessa in continua competizione con gli stessi.

20

ATMEL
Senza dubbio il competitore pi grande con il quale Microchip si trova a dividere
il mercato dei microcontrollori ad 8 bit. Per tale ragione nella crisi economica
globale del 2009, Microchip ha cercato di acquisire Atmel. La famiglia dei
microcontrollori ad 8 bit prodotta da Atmel nota come AVR. Atmel possiede tra
le sue linee di prodotti, microcontrollori a 32 bit, AVR32 basasti su una struttura
RISC proprietaria, ma allo stesso tempo ha prodotto anche microcontrollori a 32 bit
basati su core ARM (AT91SAM, brevemente noti anche come SAM).
Uno dei punti di forza dei microcontrollori a 32 bit basati su core ARM legato al
fatto che sono supportati dalla comunit di Linux. In particolare i microcontrollori
ARM sono forse secondi solo all'architettura 80x86. Atmel produce
microcontrollori anche con core basato sulla CPU 8051.

ST
La ST Microelectronics un altro grosso produttore di microcontrollori ad 8 bit,
famose sono la famiglia ST6 e ST7 . Oltre ai microcontrollori ad 8 bit la ST
produce microcontrollori a 16 e 32 bit. Quelli a 16 bit sono la famiglia ST10 mentre
quelli a 32 bit sono basati su core ARM. Probabilmente ST tra i produttori pi noti

E' presente anche il core M1, pensato per poter essere integrato in FPGA ma non noto come gli altri core.

21

di microcontrollori basati su architettura ARM e possiede forse la fetta di mercato


pi ampia.

TI
La Texas Instruments uno dei colossi del mondo dell'elettronica, praticamente
produce tutti i tipi di componenti elettronici, eccetto quelli discreti. Dal punto di
vista dei microcontrollori produce microcontrollori a 16 e 32 bit e processori
multicore a 32 bit. La famiglia a 16 bit nominata MSP430 ed nota per
applicazioni Ultra Low Power21, mentre per la famiglia a 32 bit c' il DSP C2000
basato su architettura proprietaria. Con architettura a 32 bit la TI offre anche
microcontrollori basati sul core ARM Cortex M3 e Cortex M4F22. La famiglia
Cortex M nota come TIVA (precedentemente era chiamata Stellaris23). Si noti
come TI allo stato attuale non abbia Cortex M0 o Cortex M0+. Il core Cortex M3
possibile trovarlo anche in SoC (Sytem On Chip) RF come per esempio il CC2538 per
applicazioni Zigbee.
Oltre ai microcontrollori, TI produce diverse famiglie di DSP tra cui C6000, C5000
e high hand MPU (MicroProcessor Unit) come per esempio Sitara.

Analog Devices
L'Analog Devices non un vero competitore della Microchip, visto che si
differenziata producendo un ristretto numero di microcontrollori ottimizzati per
applicazioni analogiche (DSP). In particolare possiede microcontrollori di fascia alta
basati come per TI sul core ARM. L'Analog Devices ha microcontrollori sia a 16
che 32 bit. Oltre ai microcontrollori di fascia alta, possiede qualche microcontrollore
basato sul core 8052. Particolarmente famosa la famiglia di DSP che va sotto il
nome di Shark (squalo). Simpatico il fatto che TI ha un DSP nella famiglia C2000
che va sotto il nome di Delfino!
Maxim Integrated
Maxim Integrate, (Maxim IC) utilizza un approccio simile a quello dell'Analog
Devices, ovvero produce microcontrollori a 16 bit per applicazioni specifiche. La
scelta dei microcontrollori a disposizione non paragonabile con quella della
Microchip. Il core principale dei microcontrollori della Maxim il core 8051.

Zilog
La Zilog particolarmente famosa per aver introdotto il primo microprocessore
di successo, lo Z80. Attualmente lo studio dello Z80 previsto nei piani ministeriali
degli istituti tecnici industriali. Nonostante il successo dello Z80, lo stato attuale non
come in passato, ma senza dubbio Zilog produce microcontrollori di qualit. In
particolare produce microcontrollori ad 8,16 e 32 bit. Recentemente la Zilog
divenuta parte della societ IXYS.

21

22
23

NXP
NXP nata dalla divisione componenti elettronici della Philips. Attualmente

Un corso sugli MSP430 disponibile sul sito www.LaurTec.it. Il corso introduce gli strumenti e l'architettura di tali
microcontrollori, mettendoli a confronto con le altre soluzioni presenti sul mercato.
La F dopo M4 sta ad indicare che il core possiede anche il modulo Floating Point Unit.
Il nome Stellaris discende dalla famiglia di microcontrollori della societ Luminary acquisita dalla TI nel 2009. Luminary
stata la prima societ a prendere in licenza il progetto Cortex M3 da ARM

22

produce una vasta gamma di microcontrollori, in particolare possiede


microcontrollori ad 8 bit basati sul core 8051 e un ridotto numero di
microcontrollori a 16 bit. Dall'altro lato possiede una vasta scelta di microcontrollori
a 32 bit basati sul core ARM Cortex M0, M0+, M3 ed M4 (famiglia LPC).
Renesas
Renesas una delle societ leader nella produzione di microcontrollori e
microprocessori. Possiede un'ampia scelta di microcontrollori che va dagli 8 bit fino
ai 32 bit. Particolarmente famosi sono i SuperH a 32 bit, supportati dai cross
compiler Linux, dunque i SuperH supportano Linux embedded.

Riassunto
Molte applicazioni commerciali fanno ormai uso di un microcontrollore pi o meno
potente. Oggigiorno anche un semplice giocattolo potrebbe essere considerato un sistema
embedded qualora sia capace di parlare o riconoscere il proprio padrone. In questa
moltitudine di applicazioni sono presenti molti produttori di circuiti integrati che cercano
di fornire la soluzione ideale per il progettista. Il mercato offre microcontrollori da 8, 16 e
32 bit. In particolare tra le famiglie ad 8 e 16 bit facile trovare architetture proprietarie,
mentre tra quelle a 32 bit non insolito avere a che fare con CPU ARM Cortex M. Nel
mondo ad 8 bit, Microchip tra i produttori pi famosi, ma per le famiglie a 32 bit il
mercato favorisce altri produttori come ST e TI.
Le numerose alternative offerte nel mercato permettono al progettista di valutare e
cercare la soluzione ideale per il proprio progetto, ma lo pone anche spesso nel dilemma
di quale microcontrollore utilizzare.

Punti chiave ed approfondimenti


Sistema embedded: Si definisce sistema embedded un qualunque sistema di calcolo ad
hoc con limitate risorse che permette di soddisfare le specifiche di progetto. La
definizione piuttosto ampia e non pone dei limiti reali sulle performance del sistema
stesso. Basti pensare che un sistema embedded odierno pu essere pi potente di un
super computer di pochi decenni fa.
Architettura: Per architettura di un microcontrollore o microprocessore si soliti
indicare la tipologia della CPU con il proprio set di istruzioni o il parallelismo della stessa.
Per cui si pu parlare di architettura ARM o MIPS come anche di architettura ad 8 bit, 16
o 32 bit.
Architettura ARM Cortex M: L'architettura ARM Cortex M definisce l'architettura della
CPU e dell'instruction set, oltre a particolari moduli specifici, quali per esempio il modulo
delle interruzioni e di Debug.
Lo spazio di memoria suddiviso in maniera da mantenere pi o meno standard le
funzioni associate al memory mapping delle periferiche.
Sebbene diversi microcontrollori possano avere la stessa CPU le periferiche e i registri
di configurazione delle stesse sono diversi, per cui non possibile compilare un
23

programma per microcontrollori di produttori diversi, a meno di non apportare le


modifiche necessarie per la gestione delle periferiche interne. ARM sta promuovendo le
specifiche AMBA per poter standardizzare anche i moduli e periferiche ausiliare alla CPU,
ma i produttori di microcontrollori sono restii a seguirle visto che tenderebbero a perdere
la differenziazione dei loro prodotti sul mercato mentre ARM ci guadagnerebbe potendo
vendere pi licenze.

Domande ed Esercizi
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.

Enunciare e descrivere la legge di Moore.


Enunciare degli esempi di sistemi embedded e sistemi che non sono embedded.
Qual' la differenza tra un PC e un sistema embedded?
Che cosa sono e a cosa servono il VHDL e Verilog?
Qual' la differenza principale tra le CPLD e le FPGA?
Per quale ragione Linux richiede la presenza di una MMU?
Quali sono le principali architetture di microcontrollori supportate da Linux?
Quali sono i vantaggi dell'utilizzare un core ARM?
Per quali applicazioni stato pensato il core Cortex M0+?
Per quali applicazioni stato pensato il core Cortex R?
Qual' la particolarit delle nuove istruzioni introdotte nel core Cortex M4?
Quali sono le applicazioni tipiche di un MSP430?
Per quali applicazioni pensato un DSP?
Che cos' un filtro FIR?
In quali applicazioni si predilige una FPGA piuttosto che un DSP?

24

Capitolo II

II

Strumenti per iniziare


In questo Capitolo sono elencati gli strumenti necessari per poter sviluppare le
applicazioni spiegate in questo testo, ciononostante non si scriver nessuna riga di codice.
Lo scopo infatti quello di permettere i preparativi sul tavolo del laboratorio e di
raggiungere la motivazione giusta per andare avanti. In questo Capitolo sono motivate le
scelte che hanno condotto alla selezione della Microchip e dei vari strumenti di sviluppo.
Sono inoltre brevemente illustrati i vari passi da seguire per installare la nostra tool chain
(catena di strumenti). In ultimo si introduce l'Hardware base necessario per permettere di
testare i vari esempi che verranno proposti, illustrando le possibili alternative basate sulle
schede di sviluppo Freedom II, Freedom Light e casalinghe.

Perch si scelto il mondo Microchip


Quando ci si trova a dover scegliere una famiglia di microcontrollori, qualora non si
abbia il supporto diretto delle societ produttrici (e questo il caso di ogni piccola societ
o singolo individuo), bene selezionare un microcontrollore di cui sia facile reperire
documentazione ufficiale, esempi, schede di sviluppo e una tool chain economica (ancor
meglio se gratuita).
Osservando i microcontrollori della Microchip si pu vedere subito che internet pieno
di materiale che spiega come realizzare sistemi con microcontrollori PIC, in particolare si
trovano tutorial, progetti, programmatori e software per programmare. Per chi si dovesse
imbattere nella programmazione dei microcontrollori Renesas si render subito conto che
trovare esempi di programmazione e documentazione esaustiva molto difficile. Queste
difficolt portano molti studenti ad ignorare i microcontrollori Renesas, il che si traduce
in community praticamente inesistenti. Per tale ragione il testo non parla dei
microcontrollori Renesas24. Dall'altro lato, se internet pieno di materiale, vi chiederete
per quale ragione sia necessario scrivere un altro testo sui microcontrollori PIC18. La
ragione molto semplice ed l'ordine dell'esposizione. Infatti quando ci si trova in un
mare di informazione dare un filo logico al tutto sempre difficile, soprattutto qualora si
stia iniziando.
Molti degli strumenti di supporto per i microcontrollori PIC che possibile trovare
online sono gratuiti, ma il tempo e l'esperienza di partenza necessari per il loro utilizzo,
non sono trascurabili. Tutto il mondo gratuito per quanto bello possa essere, ha una grave
lacuna, la manutenibilit del Tool. In particolare ogni volta che viene aggiunto un PIC o
trovato un problema, la Microchip aggiorna i propri strumenti, mentre quelli gratuiti
possono impiegare anni prima di aggiornarsi...se mai lo saranno!
Per tale ragione, sebbene internet offra molti progetti sviluppati gratuitamente, il testo
24

Sebbene Renesas non sia stata scelta per il testo, questo non vuol dire che non sia un prodotto di qualit. In effetti Renesas
ha molto successo in progetti dove sicurezza e qualit sono importanti, come per esempio in ambito Automobilistico.

25

XC8 Step by Step basato su strumenti Microchip ufficiali. Avere il servizio Microchip a
portata di mano non un beneficio da sottovalutare, specialmente se consideriamo il fatto
che una delle politiche Microchip quella di vendere i propri strumenti di sviluppo a
basso costo, sviluppando strumenti per diverse esigenze, dall'hobbysta al professionista.
La Microchip sviluppa inoltre anche il compilatore per i propri microcontrollori. Per ogni
architettura a 8, 16 e 32 bit presente un compilatore nominato rispettivamente XC8,
XC16 e XC32. Questi compilatori sono disponibili gratuitamente, anche per applicazioni
commerciali25. In particolare il seguente testo, descrivendo i PIC18 fa riferimento al
compilatore XC8. Dal momento che il compilatore XC8 supporta anche le altre sub
famiglie ad 8 bit, sebbene si faccia riferimento ai PIC18, molti esempi possono essere
utilizzati anche su altri microcontrollori con architettura ad 8 bit, quali per esempio i
PIC16.
Ricapitolando, scegliendo gli strumenti Microchip o compatibili Microchip, come la
scheda di sviluppo Freedom II e Freedom Light, si ha la possibilit di avere sempre gli
strumenti supportati direttamente dal produttore. Scegliendo invece di realizzare i vostri
strumenti, sarete lasciati allo sbaraglio delle incertezze e delle difficolt che incontrerete.
Passerete giorni a cercare di capire il problema, per poi scoprire che l'hardware da voi
usato non era supportato dal software. La spesa che sosterrete comprando un
programmatore Microchip vi far risparmiare tempo e anche denaro!

L'ambiente di sviluppo MPLAB X


L'ambiente di sviluppo scelto per le applicazioni ed esempi proposti sul testo
MPLAB X, ovvero l'IDE (Integrated Development Environment) o ambiente di sviluppo
integrato della Microchip. MPLAB X diversamente dal precedente ambiente di sviluppo
MPLAB IDE supporta sia Windows, Linux che MAC ed basato sull'IDE offerto da
Oracle, ovvero Netbeens.
Tale software pu essere scaricato dal sito della Microchip al seguente link
www.microchip.com/mplabx. La versione trattata nel seguente testo la MPLAB X 2.05
installata su PC a 64 bit e Windows 7. L'installazione dell'ambiente di sviluppo piuttosto
semplice e consiste semplicemente nell'accettare la licenza e procedere avanti con i passi
raccomandati.
Per mezzo dell'IDE MPLAB X possibile creare progetti per ogni tipo di PIC, dai
PIC10F ai PIC32 e scrivere programmi sia in C che in Assembly. L'ambiente integra
inoltre gli strumenti di sviluppo quali il programmatore, l'emulatore e il Debug sia
hardware che software. Il Debug software pu essere fatto tramite il simulatore
incorporato. Tutto quanto quello descritto 100% gratuito sia per applicazioni
accademiche che commerciali. Avere un ambiente di sviluppo integrato permette di
accelerare notevolmente i tempi di sviluppo, infatti dalla scrittura alla compilazione e alla
successiva programmazione del dispositivo, bastano pochi click di mouse. Se si volesse si
potrebbe anche evitare di utilizzare tale ambiente di sviluppo, visto che gran parte dei
software Microchip sono tra loro integrati sfruttando semplice linea di comando.
Personalmente non ho mai usato tale approccio e ve lo sconsiglio visto che inutile
25

Il compilatore C18 che ha preceduto la nuova famiglia di compilatori XC era gratuito solo per applicazioni accademiche. I
compilatori della famiglia XC sono invece gratuiti indipendentemente dall'utilizzo. Ciononostante sono presenti versioni a
pagamento che forniscono un livello di ottimizzazione del codice pi avanzata.

26

andare in salita quando si pu raggiungere lo stesso scopo in discesa 26.


Se avete intrapreso la strada del costruirvi il programmatore da soli e vi volete fare
ancora del male potete a vostro scapito utilizzare la linea di comando!
Come visto l'IDE scelto della Microchip, ma sul mercato sono presenti anche altri
IDE noti per la loro qualit. Uno dei migliori ambienti di sviluppo era quello della HITECH recentemente acquisita dalla Microchip e sul quale sono basati i nuovi compilatori
della famiglia XC. Un altro famoso ambiente di sviluppo completo di tutti gli strumenti di
sviluppo, quello sviluppato da mikroElektronica www.mikroe.com .
Tale societ nota per la semplicit di utilizzo del loro IDE e della ricca libreria a
supporto di molte periferiche e applicazioni. La societ fornisce una versione scaricabile
gratuitamente dell'IDE e Compilatore ma i suoi limiti possono creare qualche fastidio. In
particolare sono presenti vari compilatori, per il C, il Basic e Pascal. Vi rimando al
paragrafo precedente per le motivazioni sullo scegliere l'ambiente di sviluppo Microchip.

Il compilatore XC8
Una volta installato l'ambiente MPLAB X, possibile installare il compilatore per
PIC18 ovvero XC8. Tale compilatore racchiude i vantaggi dei due precedenti compilatori
Microchip, ovvero il C18 e C HITECH. In particolare per la sintassi ha prevalso il C18
mentre il compilatore vero e proprio rimasto C HITECH, visto che permetteva di
raggiungere ottimizzazioni del codice migliori del C18 27. Come per l'IDE anche i
compilatori della famiglia XC possono essere installati sia in ambiente Windows, Linux e
MAC.
Il nuovo ambiente si sviluppo MPLAB X permette di installare diverse versioni per il
compilatore. Ogni progetto ha in particolare associata la versione di un compilatore ma
pu essere cambiata in un qualunque momento per mezzo delle opzioni del progetto
stesso. La versione utilizzata nel testo fa riferimento alla 1.21.
Versioni diverse del compilatore o livelli di ottimizzazione differenti potrebbero mostrare
alcuni risultati differenti per quanto riguarda gli esempi in Assembly mostrati durante il
corso.
Installare il compilatore piuttosto semplice visto che ci si trova in ambiente Windows.
Il percorso standard d'installazione del compilatore C all'interno della directory:
[...]/Microchip/xc8

Il percorso radice pu essere variato a piacimento. In particolare altri compilatori della


famiglia XC o il vecchio compilatore C18 sono installati sempre all'interno della directory
Microchip. All'interno della directory xc8 vengono istallate tutte le versioni del
compilatore, per cui possibile trovare altre cartelle il cui nome pari alla versione del
26

27

Utilizzare la riga di comando ed integrare il compilatore con un altro IDE pu essere certamente educativo, ma dal
momento che state per imparare molte altre cose inutile mettere troppa carne sul fuoco e rischiare di creare difficolt
inutili non inerenti allo scopo principale, ovvero imparare a programmare i PIC18.
Maggiori dettagli sulle differenze tra il C18 e il nuovo compilatore XC8 possono essere trovate nell'articolo Il Compilatore
XC8, come migrare dal C18 scaricabile gratuitamente dal sito www.LaurTec.it .

28

compilatore. Nel nostro caso avremo almeno la versione v1.21 ovvero la cartella v1.21.
All'interno della cartella relativa alla versione del compilatore sono presenti altre
sottocartelle in cui sono raggruppati tutti i file necessari per poter compilare propriamente
un progetto. bene mettere subito in evidenza alcune cartelle che verranno di seguito
utilizzate.

bin
All'interno di questa cartella sono presenti i sorgenti del compilatore e del linker,
utilizzati per creare il vostro codice da caricare all'interno del PIC.

dat
La Directory dat contiene l'importante directory lkr, ovvero la directory all'interno
della quale sono presenti i linker file. Per ogni PIC presente un file che deve essere
incluso nel progetto. Se per esempio stiamo utilizzando il PIC18F4550 bisogna
includere il file 18f4550_g.lkr. Al suo interno sono presenti informazioni utilizzate
dal linker, relative all'organizzazione della
memoria interna al PIC. Tale
organizzazione varia a seconda delle opzioni utilizzate per la compilazione stessa. Il
fatto di attivare o meno le funzioni di Debug, per esempio, cambia l'organizzazione
della memoria. Consiglio di aprire il file d'interesse almeno una volta, il file scritto
in testo normale ed facilmente comprensibile. Come si vedr a breve, qualora si
faccia uso del file linker standard, l'ambiente di sviluppo MPLAB X include
direttamente il file di linker necessario, durante la fase di creazione del progetto
stesso. Infatti durante la creazione di un progetto si specifica il PIC utilizzato, per
cui viene automaticamente incluso il file di linker richiesto. File di linker diversi
possono essere inclusi in progetti particolari, come per esempio qualora si voglia
utilizzare un bootloader USB. Per ora bene non dire altro per evitare di
confondere le idee, ma l'argomento verr ripreso a tempo debito.
docs
All'interno di tale directory presente tutta la documentazione base utile per
l'utilizzo del compilatore. In particolare presente la guida d'utilizzo al compilatore
(manual.pdf) e la guida relativa alle librerie disponibili per i vari moduli interni al
microcontrollore.
All'interno della directory docs presente la sottocartella chips che contiene i file
di configurazione html per ogni chip, ovvero varianti di microcontrollori della
famiglia PIC18. Ogni PIC possiede infatti particolari configurazioni, per le quali
necessario sapere l'opportuno settaggio. Tale file risulta particolarmente comodo
all'inizio della programmazione e verr descritto in dettaglio nei prossimi Capitoli.
include
All'interno di questa directory sono presenti gli header file associati ad ogni
PIC18. Per esempio per il PIC18F4550 presente il file p18f4550.h. Anche in
questo caso, come per il file di linker il file viene incluso nel progetto
automaticamente.28 All'interno di questo file sono presenti o meglio definiti i nomi
dei pin del PIC selezionato e i registri interni. Come visibile dal Datasheet ogni PIC
ha molti registri con nomi differenti, tali nomi vengono definiti all'interno
dell'header file del PIC in modo da poter usare gli stessi nomi utilizzati nel

28

Facendo uso del compilatore C18 era necessario include tale file manualmente.

29

Datasheet. Se per esempio stiamo utilizzando il PIC18F4550 se apriamo il file


p18f4550.h troveremo al suo interno la definizione di molte strutture dati. Un
esempio la PORTD definita come:
extern volatile near unsigned char PORTD;
extern volatile near union {
struct {
unsigned RD0:1;
unsigned RD1:1;
unsigned RD2:1;
unsigned RD3:1;
unsigned RD4:1;
unsigned RD5:1;
unsigned RD6:1;
unsigned RD7:1;
};
struct {
unsigned SPP0:1;
unsigned SPP1:1;
unsigned SPP2:1;
unsigned SPP3:1;
unsigned SPP4:1;
unsigned SPP5:1;
unsigned SPP6:1;
unsigned SPP7:1;
};
} PORTDbits;

In particolare sono definiti i nomi PORTD e PORTDbits con relativi bit. La


struttura PORTDbits pu essere utilizzata per accedere i singoli bit del registro.
Ogni registro, come si vedr, ha una struttura con il nome del registro stesso, tipo
REGISTRO e la struttura REGISTRObits per poter accedere ai singoli bit. Per
accedere il singolo bit bisogna interporre un punto tra il nome del registro e quello
del bit. Per esempio per accedere al bit RD1 bisogna scrivere PORTDbits.RD1.
All'interno della directory include sono presenti due sotto cartelle nominate
legacy e plib. Nella directory legacy sono presenti alcuni header file dei PIC10, PIC12 e
PIC16. Si ricorda che il compilatore XC8 supporta, oltre che ai PIC18 anche i
PIC10, PIC12, PIC16. Nella sotto directory plib si trovano invece gli header file
relativi alle librerie fornite dalla Microchip per i relativi moduli interni ai vari
microcontrollori. Tali file devono essere inclusi in ogni progetto, a seconda delle
esigenze, per permettere l'utilizzo dei moduli stessi, a meno di non voler accedere al
modulo per mezzo dei registri.

lib
All'interno di tale directory sono presenti i codici binari, ovvero gli eseguibili
associati ad ogni libreria, in particolare per ogni PIC viene fornito un file di libreria
che contiene compilate tutte le librerie fornite dalla microchip. Se si apre per
esempio il file
pic18-plib-c18-18c242.lpp

sebbene non sia comprensibile o leggibile chiaramente, possibile leggere il nome di


30

varie funzioni relative alle varie librerie. Nell'esempio del testo riportato sotto
possibile vedere funzioni di libreria per il modulo SPI, I2C e UART.
Pplib/SPI/spi2read.o/Pplib/SPI/spi2writ.o
CPplib/SPI/spi_clos.o_CloseSPI
Pplib/SPI/spi_dtrd.o_DataRdySPI
BPplib/SPI/spi_gets.o_getsSPI
Pplib/SPI/spi_open.o_OpenSPI
TPplib/SPI/spi_puts.o_putsSPI
pPplib/SPI/spi_read.o_ReadSPI
Pplib/SPI/spi_writ.o_WriteSPI
Pplib/SW_I2C/swacki2c.o_SWAckI2C
Pplib/SW_I2C/swckti2c.o_Clock_test'dPplib/SW_I2C/swgtci2c.o_I2
C_BUFFER_BIT_COUNTER_SWReadI2C
Pplib/SW_I2C/swgtsi2c.o_SWGetsI2C
Pplib/SW_I2C/swptci2c.o_SWWriteI2CnPplib/SW_I2C/swptsi2c.o_SWPu
tsI2CPplib/SW_I2C/swrsti2c.o_SWRestartI2C
Pplib/SW_I2C/swstpi2c.o_SWStopI2C
Pplib/SW_I2C/swstri2c.o_SWStartI2C
Pplib/SW_RTCC/close_rtcc.o_Close_RTCC
Pplib/SW_RTCC/open_rtcc.o_Open_RTCC:Pplib/SW_RTCC/update_rtcc.o

_update_RTCCPplib/SW_SPI/clrcsspi.o_ClearCSSWSPIKPplib/SW_SPI/
opensspi.o_OpenSWSPI
Pplib/SW_SPI/setcsspi.o_SetCSSWSPI
7
Pplib/SW_SPI/wrtsspi.o_WriteSWSPI
Pplib/SW_UART/openuart.o_OpenUART-

Da questa piccola nota si capisce che qualora si voglia scrivere una propria
libreria, se la si volesse utilizzare per ogni PIC18 sarebbe necessario compilarla per
ogni modello. Come verr messo in evidenza nei capitoli successivi, al fine di evitare
di dover creare file di libreria per ogni PIC baster scrivere un file .h e un file .c ed
includere entrambi nel proprio progetto. Ogni volta che si compiler il progetto i
file di libreria verranno automaticamente compilati per il modello di
microcontrollore utilizzato. Anche questo solo un anticipo di quello che verr
spiegato nel Capitolo dedicato ai file di libreria.

Utilizzare il C o l'Assembly?
Giunti alla fine dell'istallazione del compilatore C giusto spendere due parole
sull'alternativa al compilatore C ovvero la programmazione in Assembly o Assembler29.
Il linguaggio Assembly diversamente dal C in relazione 1:1 con il linguaggio macchina,
ovvero il linguaggio del microcontrollore. Le istruzioni scritte in C non sono tradotte 1:1
con il linguaggio macchina, ci permette un livello di astrazione tale da non dover
ricordare il formato di una istruzione con relativi operandi.
Per mezzo dell'Assembly possibile scrivere il codice ottimizzato al meglio, buon cervello
permettendo. Allora perch non utilizzare direttamente l'Assembly?
Una delle ragioni principali per cui bene non scrivere direttamente in Assembly legato
al fatto che la leggibilit del codice notevolmente inferiore, rendendo dunque la
29

In questo paragone tra linguaggi, non si tiene conto del fatto che sono presenti anche altri linguaggi ad alto livello come il
Basic e il Pascal. Il confronto tra C e Assembly pu essere esteso anche tra gli altri linguaggi e l'Assembly.

31

programmazione propensa a sbagli e i problemi sono difficilmente rintracciabili. La


leggibilit del codice una delle pratiche della programmazione pi importanti. Un indice
di qualit del software la sua facile manutenibilit, ovvero la possibilit di aggiornare e
rimettere mani sul software in un secondo momento. Si capisce che se il software non
facile da leggere rimetterci le mani diventa complicato. A peggiorare il tutto, immaginate
se il software debba essere letto da un'altra persona...e magari l'altra persona siete proprio
voi! Ringraziereste il programmatore se avesse potuto scrivere in maniera pi chiara 30.
Un altro neo della programmazione in Assembly legata al fatto che il livello di
conoscenza del microcontrollore che viene richiesta pi alta se paragonata alle esigenze
del C, o altro linguaggio ad alto livello, per cui non sempre si riesce a scrivere un
programma Assembly migliore di quello che un compilatore creerebbe partendo dal
codice C. Quando si programma in C il livello di astrazione pi alto, dunque possibile
leggere un codice C in maniera pi simile ad un libro.
Nonostante abbia denigrato il linguaggio Assembly bene ora dargli il giusto
riconoscimento. Diversamente da quanto ho pi volte sentito in ambiente universitario
che l'Assembly non pi usato, questo viene ancora utilizzato molte volte e non solo per i
PIC. In questi ultimi anni in cui ho supportato i microcontrollori della famiglia MSP430
ho visto diverse societ che programmano questo microcontrollore esclusivamente in
Assembly. Questa scelta discende spesso da abitudine o reali esigenze di avere un
controllo totale del microcontrollore. Altre situazioni in cui si pu scrivere in Assembly,
sono i casi in cui si faccia uso di microcontrollori con poca memoria flash. In ogni modo
qualora si volesse ottimizzare del codice possibile integrare pezzi di codice Assembly
direttamente nel codice C piuttosto che scrivere tutto in Assembly.
Ottimizzare il codice ha senso solo dopo essersi resi conto che c' realmente esigenza
di ottimizzarlo. Infatti l'ottimizzazione del codice porta in generale a una minor leggibilit
del codice, dunque al fine di preservare la sua leggibilit bene non ottimizzare fino a
quando non necessario.
Alcuni dati statistici hanno messo in evidenza che normalmente il 20% del codice a
prendere 80% delle risorse; questa analisi statistica anche nota come la regola 20 80.
Quanto appena detto mette in evidenza che quando ci si rende conto che bisogna
ottimizzare, non bisogna ottimizzare a caso ma bisogna andare alla ricerca di quel 20% di
codice responsabile dei nostri problemi. Un'altra cosa importante da non dimenticare,
quella di verificare sempre i miglioramenti ottenuti quando si ottimizzato un del codice.
Se i benefici non sono quelli che ci si aspetta, meglio togliere l'ottimizzazione e tenere il
codice leggibile.
Riassumendo, sebbene in Assembly sia possibile raggiungere il maggior livello di
ottimizzazione possibile, questo non significa che scrivere in Assembly automaticamente
migliori le cose. Ottimizzare richiede una una profonda conoscenza dell'hardware per cui
se non si sufficientemente esperti scrivere in Assembly potrebbe peggiorare le cose.
Inoltre bene ricordare che inserire del codice Assembly nel proprio codice in C
complica le cose, infatti attualmente il compilatore XC8 non garantisce gli stessi livelli di
controllo e verifica sul codice misto C/Assembly, che invece possibile avere scrivendo
solo in C. In ambito professionale, acquistando le licenze del compilatore XC8 Standard o
PRO possibile abilitare livelli di ottimizzazioni del codice che potrebbero essere migliori
di quelli ottenibili con la nostra esperienza. Il vantaggio di queste opzioni sarebbe quella di
mantenere il codice scritto in C e facilmente leggibile piuttosto che rendere il codice di pi
difficile comprensione.
30

Un altro modo per migliorare la leggibilit del software quello di usare buoni commenti e nomi di variabili appropriate.

32

meglio privilegiare una soluzione semplice e facilmente comprensibile


piuttosto che una soluzione smart ma difficilmente comprensibile!

Il programmatore
Ogni volta che si scrive un programma, sia esso in C o in Assembly, per dar vita al
programma necessario poterlo installare o caricare all'interno del microcontrollore. Il
programma, come si vedr nei prossimi Capitoli, per poter essere installato deve essere
prima compilato. La fase di compilazione genera un file in linguaggio macchina, ovvero la
traduzione del nostro programma C nel linguaggio del microcontrollore. Sar proprio
questo file in linguaggio macchina che verr caricato all'interno del microcontrollore. Per
poter fare questo necessario un programmatore al quale poter attaccare il nostro
microcontrollore o scheda di sviluppo. In alcuni casi, se nel PIC stato precedentemente
caricato un bootloader, possibile caricare il programma per mezzo di una normale
connessione RS232 o USB, ovvero senza programmatore, ciononostante per caricare un
bootloader necessario di un programmatore.
In commercio possibile trovare un'ampia gamma di programmatori per PIC ma molti
hanno lo svantaggio di non essere direttamente integrabili con l'ambiente IDE della
Microchip e non supportano, se non con lunghi ritardi, gli ultimi modelli dei PIC. Per tale
ragione anche in questo caso consiglio un programmatore della Microchip.
Internet fornisce anche molte soluzioni fai da te, ma per esperienza nel supportare
utenti del Forum LaurTec, possono dire che sebbene gli schemi in rete non siano
complicati, non sempre funzionano. Quando si inizia a studiare qualcosa di nuovo avere
troppe variabili in cui cercare il problema pu essere piuttosto frustrante e potrebbe
indurre a lasciare lo studio. Per tale ragione vi consiglio di procedere senza indugi
nell'acquisto di un programmatore Microchip, il cui prezzo per i modelli base si aggirano
intorno a 30-50Euro.
Nel seguito del testo far uso del programmatore PIC KIT3 della Microchip. Per
completezza bene comunque riportare qualche dettaglio sugli altri programmatori.

PICKIT 2
il cavallo di battaglia tra i programmatori ma possiede tutte le funzioni richieste
da un hobbysta o studente alle prime esperienze. integrabile nell'ambiente
MPLAB X e programma i PIC10F, PIC12F5xx, PIC16F5xx, midrange PIC12F6xx,
PIC16F, PIC18F, PIC24, dsPIC30, dsPIC33, e PIC32. Il programmatore pu
inoltre essere utilizzato come data logger per mezzo del programma Programmer
Logic Analyzer scaricabile direttamente dalla home page del PICKIT 2. Il suo costo
veramente vantaggioso, viene venduto singolarmente o con piccola Evaluation
Board con un PIC della famiglia PIC16, sul mercato sono anche presenti dei cloni
visto che lo schema elettrico del PICKIT 2 fornito dalla stessa Microchip.
33

Personalmente non li ho mai provati, ma visto che sono basati sullo schema
originale del PICKIT 2, non hanno molti problemi. Visto per che il costo dei cloni
paragonabile a quello originale consigliabile comprare direttamente quello
originale. Unico neo del PIC KIT 2 che supportato in versione beta dal nuovo
ambiente MPLAB X e probabilmente rimarr tale. In particolare la Microchip non
sta rilasciando pi aggiornamenti per i nuovi microcontrollori. Ciononostante i
microcontrollori supportati sono praticamente tutti quelli pi usati nei progetti
comuni. In applicazioni hobbystiche pu ancora convenire acquistare un PICKIT 2
e risparmiare 10-15 euro piuttosto che comprare il PICKIT 3. Se si studenti di
ingegneria e si pensa di voler progredire e cavalcare l'onda delle novit bene
spendere qualche euro in pi anche se per 1-2 anni non avrete probabilmente
benefici.

PICKIT 3
Il PICKIT 3 praticamente una evoluzione del PICKIT 2 anche se in alcuni
aspetti le cose non sono realmente migliorate, ma piuttosto peggiorate o rese pi
noiose. Il PICKIT 3 infatti pi lento a programmare che non il PICKIT 2 e
richiede un aggiornamento del firmware interno per ogni sub famiglia di PIC
programmato. Il suo costo circa 10-15 euro superiore al PICKIT 2, per cui
potrebbe convenire il suo acquisto se paragonato al PICKIT 2. Personalmente sto
usando il PICKIT 3 perch 3 pi grande di 2 e non perch abbia realmente avuto
esigenza. Il testo XC8 Step by Step si pu seguire interamente con il PICKIT 2 come
anche tutti gli esempi attualmente disponibili sul sito www.LaurTec.it.

ICD 2
il primo programmatore e real-time Debugger per una fascia di utilizzatori
professionisti. Le capacit di Debug sono migliorate rispetto ai programmatori della
serie PICKIT. Come quest'ultimi supporta tutta la serie di microcontrollori
sviluppati dalla Microchip. Tale programmatore risulta obsoleto e non pi
supportato da MPLAB X. Per poterlo utilizzare necessario utilizzare MPLAB
IDE, ovvero il vecchio IDE Microchip.

ICD 3
Nuovamente un fratello maggiore! ICD 3 stato introdotto per sostituire l'ICD 2.
Tanto vero che la Microchip alla sua introduzione ha iniziato un programma di
riciclo per ritirare gli ICD2 fornendo un coupon con uno sconto del 25%
sull'acquisto dell'ICD 3 e il rottamaggio dell'ICD 2. Le caratteristiche pi
importanti dell'ICD 3 sono un ulteriore miglioramento delle capacit di Debug e un
aumento della velocit di programmazione. Diversamente dagli altri programmatori,
l'ICD 3 possiede al suo interno una FPGA al fine di supportare le alte velocit di
programmazione. Per scoraggiare ulteriormente l'acquisto dell'ICD2, l'ICD 3 costa
meno dell'ICD 2! Unico neo dei programmatori/debugger della serie ICD che il
loro costo circa 5 volte superiore al PICKIT 2 e PICKIT 3.

Caratteristica di tutti i programmatori ora descritti, che sono tra loro compatibili da
un punto di vista meccanico, ovvero il connettore dei vari programmatori la stessa.
Questo permette di sviluppare una scheda e poterla programmare con uno qualunque dei
programmatori ora descritti.
34

L'installazione del programmatore deve avvenire dopo l'installazione dell'ambiente di


sviluppo MPLAB X, al fine di avere i driver propriamente installati sul PC. indifferente
se il compilatore stato gi installato o meno. Per i passi da seguire per la sua installazione
si rimanda al manuale del programmatore selezionato, ma in generale se tutto va bene si
tratta solamente di attaccare il dispositivo e Windows trover il driver pi opportuno. Si fa
presente che alcuni programmatori hanno la possibilit di alimentare la scheda di sviluppo
(denominata anche target). Una regola generale da seguire che il programmatore deve
essere collegato al PC e propriamente alimentato prima di essere collegato alla scheda
target, che a seconda del programmatore dovr o meno essere alimentata. Dal momento
che i programmatori alimentati direttamente da USB possono fornire alla scheda target
poche decine di mA, bene prevedere un alimentatore esterno per il proprio sistema di
sviluppo. Per esempio Freedom II, avendo anche l'LCD con retroilluminazione pu
superare consumi di 100mA, che sono oltre le possibilit sia del PICKIT 2 che del
PICKIT 3. Nonostante si sia scoraggiato l'utilizzo di altri programmatori, naturalmente
possibile utilizzarne di altri, soprattutto se si pensa di utilizzare PIC standard per
applicazioni hobbystiche. Come per gi detto, costruirsi un programmatore pu portare
la spesa a prezzi pi alti che non un PICKIT 2, il quale ha tutti i vantaggi di un buon
programmatore. Nel caso in cui, per una qualunque ragione si avesse un programmatore
di altra marca comunque possibile costruire un semplice connettore per adattare il
connettore del proprio programmatore con quello della Microchip. I pin utilizzati per la
programmazione, oltre a quelli di alimentazione Vcc e GND, sono i seguenti:
RB6: Funzione PGD
RB7: Funzione PGC
MCLR: Tensione di programmazione

Alcuni PIC possono avere dei pin di programmazione diversi, come per esempio il
PIC18F14K50, ma gran parte fanno uso dei pin RB6 RB7. Durante la fase di
programmazione, sul pin MCLR viene fornita la tensione di programmazione di circa
14V31, mentre le linee RB6 e RB7 rappresentano il canale di comunicazione seriale per la
scrittura e lettura dei dati all'interno del microcontrollore. Ulteriori dettagli sulla
connessione di questi pin fornita a fine Capitolo.

Scheda di sviluppo e test


Questa volta la storia un po' diversa e merita qualche nota. La scheda di sviluppo in
generale una scheda che permette di testare vari programmi. Frequentemente la scheda
pu essere direttamente un proprio progetto, dunque pu essere di ogni genere. La
Microchip come anche molti altri produttori forniscono molte schede pi o meno
ottimizzate per poter testare un determinato tipo di hardware, in generale se la scheda
flessibile per mezzo di espansioni, il suo costo lievita intorno a 150euro, pi il costo delle
varie schede di espansione.
Per la scheda di sviluppo, quello che realmente serve, che abbia l'hardware che si
vuole testare e che sia compatibile con gli strumenti Microchip, ovvero collegabile ai
programmatori Microchip. Il connettore di programmazione come detto standard,
dunque una volta che si pu utilizzare il PICKIT si pu usare anche l'ICD 32. Per tale
31
32

Nel caso si dovesse abilitare la programmazione a bassa tensione, non viene fornita la tensione di 14V, ma viene usata Vcc.
La Microchip per i programmatori utilizza due tipi di connettori, per i quali fornisce per l'adattatore stesso.

35

ragione, se si volesse, si potrebbe anche realizzare una propria scheda di sviluppo su


Breadboard o su mille fori. Personalmente, sconsiglio questa pratica poich i fili volanti
potrebbero causare qualche problema, ma non certamente una via complicata
soprattutto per gli esempi pi semplici. Nel testo far uso della scheda che ho
personalmente progettato ad hoc per scopi didattici e sulla quale si basa il corso stesso; la
scheda in questione nominata Freedom II ed riportata in Figura 1.

Figura 1: Scheda di sviluppo Freedom II.

Oltre alla scheda Freedom II possibile utilizzare la scheda Freedom Light, in cui
alcuni componenti sono stati rimossi per ottimizzare i costi, come riportato in Figura 2.
Ogni Capitolo in cui siano esposti degli esempi inizia con una breve spiegazione delle
impostazioni necessarie sia per la scheda Freedom II che per la scheda Freedom Light,
mostrando anche il relativo schema base utilizzato negli esempi. Qualora Freedom Light
non possieda l'hardware necessario per lo svolgimento dell'esercitazione sar necessario
collegare i componenti necessari sulla scheda di espansione PJ7011 33 riportata in Figura 3
o una Breadboard.

Figura 2: Scheda di sviluppo Freedom Light.


33

La scheda di espansione PJ7011 compatibile con il connettore di espansione EX1 di Freedom II e Freedom Light per cui
pu essere utilizzata indifferentemente su entrambe le schede.

36

Figura 3: Scheda di Espansione PJ7011.

Al fine di utilizzare propriamente le schede di sviluppo, oltre a tener conto delle


impostazioni necessarie per ogni esercitazione, bene aver letto anche la relativa
documentazione associata ad ogni scheda. La documentazione della scheda scaricabile
gratuitamente dal sito www.LaurTec.it alla sezione Progetti. Il PCB di diverse schede di
sviluppo sono inoltre richiedibili, previa donazione di supporto, alla sezione Servizi. La
donazione di supporto utilizzata per realizzare opere e progetti gratuiti come il libro che
state leggendo. Freedom II e Freedom Light sono particolarmente compatte e possiedono
un gran numero di periferiche. Il connettore di espansione permette inoltre di collegare
ulteriore hardware, permettendo praticamente di effettuare ogni tipo di esperimento.
Naturalmente la scheda Freedom II e Freedom Light sono compatibili con i
programmatori Microchip, permettendo un'integrazione completa con l'ambiente di
sviluppo fin ora descritto, in particolare possiedono il connettore di programmazione per
PICKIT 2 e PICKIT 3. Se si volesse utilizzare un programmatore ICD necessario
l'adattatore fornito dalla Microchip stessa.
Altre schede di sviluppo possono essere utilizzate, ma i programmi che verranno
descritti negli esempi dovranno essere modificati nel settaggio dei pin delle varie
porte, in modo da poter utilizzare l'hardware d'interesse.

Come detto la scheda Freedom II e Freedom Light sono compatibili con la serie
PIC16 e PIC18 a 40 pin. Nel seguente corso, si utilizzer il PIC18F4550. Altri PIC
potranno essere utilizzati, ma bisogner accertarsi che i vari codici presentati siano
opportunamente modificati.
A solo scopo riassuntivo si riportano le specifiche delle due schede usate nel corso:

37

Freedom II
Supporto USB 2.0 Low Speed e Full Speed
Supporto RS232
Supporto CAN
EEPROM I2C 24LC32
Clock Calendar I2C PCF8563
Cicalino
8 LED
4 pulsanti
Sensore temperatura LM35
Sensore Luminosit
Trimmer per ingresso analogico
LCD 16x2 con retroilluminazione
Controllo software della retroilluminazione
Trimmer per contrasto LCD
Connettore espansione 40 pin EX1
Programmazione per mezzo dell'USB Bootloader
Programmazione on board e Debug compatibile con gli strumenti Microchip
Freedom Light
Supporto USB 2.0 Low Speed e Full Speed
2 pulsanti + Reset
2 Led di utilizzo generico
LCD alfanumerico 16x2
Trimmer per contrasto LCD o ingresso analogico
Buzzer
Connettore espansione 40 pin EX1
Programmazione per mezzo dell'USB Bootloader
Programmazione on-board e Debug compatibile con gli strumenti Microchip

Hardware di sviluppo base


Imparare ad utilizzare un microcontrollore richiede una tipologia di conoscenze miste
che in particolare non vanno cercate solamente nella programmazione del
microcontrollore stesso. Non insolito che il progettista di applicazioni embedded sia
responsabile sia della programmazione del microcontrollore che della progettazione del
sistema. Per tale ragione bene spendere qualche parola sull'hardware minimo necessario
per poter provare gli esempi presentati nel seguito del testo, ovvero poter permettere al
microcontrollore di funzionare. Sebbene siano presentati gli aspetti base e si consiglia di
provare un esempio semplice costruendo il tutto su Breadboard, bene che il primo
esempio in assoluto faccia uso di una scheda di sviluppo in cui si sia certi del
funzionamento hardware. Infatti il primo esempio sempre accompagnato da molte
difficolt di vario genere ed introdurre troppe variabili pu aumentare le ragioni di errori e
problemi che comunque non mancheranno.
Il microcontrollore un sistema programmabile di calcolo che permette di eseguire un
38

programma ad una frequenza scandita da un clock. Un clock non fa altro che scandire il
tempo con il quale il microcontrollore deve eseguire le operazioni. Molti microcontrollori,
tra cui il PIC18F4550 possiedono al loro interno un oscillatore che permette di generare il
clock necessario per il nostro microcontrollore. Ciononostante per ragioni pratiche che si
capiranno di seguito e compatibilit con le schede di sviluppo presentate, bene avere un
cristallo esterno collegato tra OSC1 e OSC2, il quale garantisce di ottenere una frequenza
pi accurata utile in applicazioni quali USB. Il cristallo utilizzato, al fine di non dover
cambiare le impostazioni presentate negli esempi del testo deve essere di 20MHz. Cristalli
di questa frequenza richiedono normalmente delle capacit di carico pari a 22pF, ovvero
C1 e C2. Le capacit di carico devono essere di tipo ceramico, ed in particolare devono
avere dei collegamenti corti, come anche il quarzo, in maniera da evitare problemi con
l'oscillazione del quarzo stesso. Se il quarzo non dovesse oscillare, il microcontrollore non
potrebbe eseguire correttamente il programma, ed in particolare rimarrebbe fermo 34.
Come ogni circuito integrato bene avere delle capacit di disaccoppiamento al fine di
garantire che il sistema non subisca dei Reset (riavvi di sistema) inaspettati. La capacit di
disaccoppiamento permettono di fornire rapidamente corrente al sistema ogni qual volta
sia necessario e ci sia una variazione brusca. Questo, considerando che un
microcontrollore possiede un clock per scandire le operazioni, pu avvenire alla stessa
frequenza del cristallo, per cui piuttosto frequentemente ma picchi molto rapidi.
Per permettere il corretto avvio del microcontrollore, necessario che venga eseguito
un primo Reset di sistema, questo avviene automaticamente grazie ad una capacit interna
al microcontrollore, ma al fine di garantire il corretto funzionamento della circuiteria di
Reset necessario avere anche il resistore R1 collegato alla linea MCLR. Per permettere
dei Reset manuali bene anche prevedere un pulsante di Reset BT1.
Naturalmente, come ogni sistema elettronico necessario che venga fornita energia al
sistema stesso. Per i PIC e gli esempi presentati si alimenta il tutto con 5V. bene evitare
di utilizzare alimentatori a 5V di tipo switching35 visto che possono avere una regolazione
piuttosto limitata dell'uscita e causare dei Reset del microcontrollore. meglio prevedere
un semplice alimentatore con un regolatore LM7805 il cui ingresso pu essere collegato
sia ad una batteria da 9V che ad un alimentatore da 9V-12V, questa volta anche switching,
visto che che il regolatore lineare LM7805 garantisce un'ottima regolazione della tensione
di uscita. Oltre a questo Hardware base bene prevedere almeno un LED di uscita, che
nel nostro caso collegheremo a RD0 e un pulsante in ingresso che collegheremo a RB4.
In ultimo, al fine di poter programmare il PIC necessario prevedere i collegamenti per il
programmatore, ovvero prevedere i seguenti collegamenti:
RB6: Funzione PGD
RB7: Funzione PGC
MCLR: Tensione di programmazione

Le linee RB6 e RB7, dal momento che sono utilizzate per la programmazione non
devono avere altri carichi come LED o collegamenti verso GND, Vcc con resistori di
34

35

Ci sono in realt delle scappatoie nel caso in cui l'oscillatore primario non dovesse oscillare. Le situazioni di clock fault sono
gestite in automatico a livello hardware. Questa funzione disponibile in molti microcontrollori e le impostazioni possono
dipendere dal modello usato.
Un tipico alimentatore switching che fornisce 5V in uscita e si tentati di usare, rappresenta il convertitore DC-DC dei
Personal Computer. Normalmente se si usano i 5V in uscita da questi sistemi si possono avere comportamenti anomali del
microcontrollore a causa di continui Reset di sistema. I 5V devono essere propriamente filtrati prima di poter essere forniti
ad un microcontrollore.

39

pull-down o pull-up, al fine di evitare alterazioni dei segnali di programmazione. Sui pin si
devono evitare anche carichi capacitivi. Normalmente, al fine di evitare errori di
programmazione bene che i cavi di collegamento tra RB6-RB7 con il programmatore
non eccedano 10cm. Lo schema elettrico riassuntivo di quanto appena detto a parole
riportato in Figura 4.

Figura 4: Schema elettrico base per semplici esempi ingresso-uscita.

Si noti che sebbene lo schema sia quello base sono gi presenti un numero non esiguo
di collegamenti. Volendo pi LED e pulsanti il numero di fili necessari raggiungerebbe
facilmente un numero proibitivo in cui sarebbe richiesto un notevole ordine per garantire
il corretto funzionamento. Al fine di facilitare il montaggio su Breadboard bene utilizzare
contenitori Dual in Line (DIL). Oltre ai componenti base descritti si aggiunto il diodo di
protezione D1 che permette di proteggere il sistema in caso di inversione di polarit
dell'alimentazione. Allo stesso modo R1 stato aggiunto al fine di proteggere il
programmatore qualora si dovesse inavvertitamente premere il tasto di Reset durante la
fase di programmazione.

Utilizzo della Breadboard


La Breadboard rappresenta un semplice strumento di supporto che non dovrebbe
mancare in nessun laboratorio sia esso di un professionista o di un hobbysta. La
Breadboard permette infatti di montare in maniera molto veloce dei semplici circuiti di
prova senza dover far uso di un saldatore.
Sebbene il montaggio di un sistema risulti pratico e veloce questa praticit diviene lo
40

svantaggio stesso qualora il sistema dovesse diventare complesso. Una Tipica Breadboard
riportata in Figura 5. La Figura mostra un modello pi completo rispetto a quello che
normalmente si trova a pochi euro. In particolare contiene due Breadboard affiancate una
sopra l'altra (le linee rosse al centro appartengono alle due Breadboard) e possiede i
connettori banana per facilitare il collegamento ad un alimentazione esterna.

Figura 5: Esempio di Breadboard.

Ogni Breadboard possiede normalmente due linee di alimentazioni superiori e inferiori,


di colore blu e rosso, per il collegamento di GND (massa) e Vcc (+5V). Le linee mostrate
in Figura 5 sono elettricamente collegate tra loro, in gergo fisico si direbbe che sono
equipotenziali, per cui collegare un filo in un qualunque punto della linea equivalente. I
fili, opportunamente ripuliti dalla calza isolante, sono collegati all'interno dei fori della
Breadboard. Sotto i fori sono presenti delle lingue metalliche elastiche che permettono il
fissaggio del cavo, garantendo un buon collegamento elettrico.
La Breadboard ha al suo centro un solco, e ai lati del solco sono presenti molti fori. I vari
fori sono allineati tra loro ed equipotenziali in verticale, ovvero come mostrato dalle linee
versi. Ogni linea verde elettricamente isolata dall'altra mentre i punti raggruppati dalla
linea verde sono equipotenziali. Il passo dei fori sulle Breadboard di 2.54mm ed lo
stesso utilizzato dai circuiti integrati. Il numero strano 2.54mm discende dalla conversione
della misura anglosassone 0.1 inches (pollici), che rappresenta appunto lo standard
internazionale maggiormente utilizzato per le dimensioni meccaniche dei componenti
elettronici.
importante osservare che il collegamento con i fili deve essere fatto in maniera pi
ordinata possibile. Si consiglia di utilizzare fili rigidi, some quelli spesso utilizzati per i cavi
citofonici e spellare i lati del filo per almeno mezzo centimetro, facendo uso di apposite
pinze o spella fili. Sono da preferirsi cavi colorati in maniera da utilizzare un qualunque
schema cromatico che possa permettere di rivedere velocemente lo schema montato. Per
montaggi complessi bene ritagliare i cavi della lunghezza necessaria evitando di creare
intrecci sopra gli integrati, i quali bene che rimangano liberi. Garantendo di non avere
fili sopra gli integrati si ha la possibilit di cambiare l'integrato in caso di problemi e
soprattutto accedere ai pin per effettuare le misure senza urtare con i fili di collegamento.
Problemi tipici che si possono avere con questi tipi di prototipi la rottura di un cavo
rigido, il quale essendo rigido rimarr fermo creando un apparente collegamento. Nel caso
di cavi volanti si ha il problema che se urtati possono sfilarsi e creare dei falsi contatti.
41

Altro problema tipico che si viene a verificare quello di avere le alette di fissaggio sotto i
fori che si allentano, creando falsi contatti. Questo si viene a creare soprattutto per quei
fori nei quali si sono collegati componenti i cui pin sono molto pi grandi del diametro
dei cavi usati. Tale problema si presenta con il tempo e usura della scheda. Per limitare il
verificarsi di questi problemi anche per i circuiti integrati, bene montare i componenti
pi grossi sui lati superiori o inferiori della scheda.

Riassunto
Poter programmare un microcontrollore, sia esso Microchip o di altro fornitore
richiede un numero di strumenti sia Hardware che Software. Il Capitolo ha messo in
evidenza come si debbano privilegiare strumenti ufficiali rispetto a quelli pi economici
ma non ufficiali. Dopo una breve trattazione sull'installazione dell'ambiente di sviluppo
MPLAB X e il compilatore XC8, usati nel corso, si mostrato anche l'hardware base
necessario per seguire il corso, in particolare introducendo le schede di sviluppo Freedom
II e Freedom Light, oltre a mostrare l'hardware base necessario per permettere al sistema
di funzionare anche se montato su una semplice Breadboard.

Punti chiave ed approfondimenti


Assembly: Il linguaggio Assembly, anche noto come Assembler, rappresenta il primo
livello di astrazione tra il linguaggio umano e il linguaggio macchina. Questo linguaggio
permette di scrivere codici facendo uso di un codice mnemonico per ogni istruzione. La
sua traduzione 1:1 con il linguaggio macchina.
Compilatore: Il Compilatore l'applicazione che permette di tradurre il programma
scritto in C, o altro linguaggio, in codice macchina, passando per livelli intermedi. Il grado
di ottimizzazione pu essere in generale impostato al fine di favorire la velocit di
esecuzione, la dimensione del codice o consumi. Normalmente queste ottimizzazioni
sono in contrasto tra loro per cui difficile raggiungere allo stesso tempo, il massimo
livello di ottimizzazione per ogni esigenza.
IDE: L'Integrated Development Environment rappresenta l'ambiente di sviluppo che integra
tutti gli strumenti necessari per sviluppare un'applicazione embedded per quanto riguarda
gli aspetti legati alla programmazione. Gli strumenti integrati sono l'editor, il compilatore,
il Linker, la gestione del Debugger, la gestione programmatore e strumenti vari.
Normalmente, ad eccezione dell'editor, ogni altro strumento integrato spesso
un'applicazione indipendente, che viene richiamata dall'IDE.
Linker: Il Linker quell'applicazione che si occupa di allocare i vari file di progetto
compilati e di libreria (precedentemente compilati) in un unico oggetto, tenendo conto dei
vincoli imposti dal file di Linker.

42

Domande ed Esercizi
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.

Su quale IDE basato MPLAB X?


Quali sono i vantaggi dell'utilizzare un IDE standard piuttosto che uno dedicato?
Quando consigliabile l'Assembly?
Perch bene non ottimizzare il codice in favore di una facile leggibilit dello stesso?
Quali sono i contenuti del file di Linker?
Quali sono i contenuti dell'header file p18f4550.h?
Per quali ragioni bene usare un cristallo esterno piuttosto che l'oscillatore interno?
Qual' il comportamento del microcontrollore nel caso in cui dovesse mancare il clock di
riferimento?
Perch bene usare una capacit di disaccoppiamento sull'alimentazione di un micro?
Perch in un sistema embedded bene prevedere un pulsante di Reset?
Per quale ragione bene mettere un resistore in serie con il pulsante di Reset?
Quali sono gli svantaggi dell'usare una Breadboard?
Cosa significa collegamento equipotenziale?
Per quale ragione bene non usare un alimentatore switching per alimentare il
microcontrollore?
Quali sono le problematiche nell'integrare un codice Assembly nel codice C, qualora si
faccia uso del compilatore XC8?
Quali sono le linee di programmazione usate dal microcontrollore e quali precauzioni
bisogna prendere?
Cosa la programmazione LVP (Low Voltage Programming)?
Che tipo di interfaccia viene usata per la programmazione dei PIC18?
Per quale ragione necessario applicare 14V per permettere la programmazione?
Se necessario fornire 14V per la programmazione, come potrebbe funzionare la modalit
LVP, in cui non viene fornita la tensione di 14V?

43

Capitolo III

III

Architettura ed Hardware dei PIC18


In questo Capitolo descritta l'architettura base dei PIC18, in particolare fa riferimento
al PIC18F4550 visto che utilizzato negli esempi dei Capitoli successivi. Il Capitolo non
vuole sostituire il Datasheet, al quale si rimanda per tutti i dettagli tecnici. Nel Capitolo
non sono affrontate tutte le periferiche che possibile trovare all'interno dei PIC
essenzialmente per tre ragioni: la prima legata al fatto che molte periferiche hardware
non sono presenti in tutti i PIC, dunque nei casi specifici si rimanda al Datasheet del PIC
utilizzato. Una seconda ragione legata al fatto che alcune delle periferiche interne sono
descritte in dettaglio nei Capitoli successivi, dove vengono riportati i programmi di
esempio per il loro utilizzo. Questo permette d'imparare la periferica direttamente con
degli esempi tra le mani. In ultimo ma non meno importante, le periferiche non
appartengono all'architettura del microcontrollore ma rappresentano delle opzioni
aggiuntive.

Architettura dei PIC18


I PIC18 come come anche le altre famiglie di PIC ad 8 bit, hanno un'architettura di
tipo Harvard ed una CPU (Central Processing Unit) di tipo RISC (Reduced Instruction Set
Computer). La struttura Harvard, riportata in Figura 6, caratterizzata dal fatto che la
memoria utilizzata per il programma (Program Memory) e quella utilizzata per i dati (Data
Memory) possiedono un bus dedicato.

Figura 6: Architettura base dei PIC18 (Harvard).

Questo significa che non sono presenti conflitti legati al contemporaneo utilizzo della
memoria, per la lettura di dati o del programma.
A questa struttura si contrappone l'architettura di von Neumann, la quale possiede un
44

unico bus d'indirizzamento, o spazio di memoria, condiviso tra la memoria programma e


memoria dati. In Figura 7 riportato lo schema a blocchi relativo all'architettura di von
Neumann.

Figura 7: Architettura di von Neumann.

Tale struttura, quella utilizzata nei computer classici, infatti i microprocessori hanno
in genere un unico bus con cui possono accedere la memoria RAM, nella quale sono
presenti sia i programmi che i dati. I microprocessori di ultima generazione non hanno
nemmeno accesso diretto alla RAM, la quale viene gestita per mezzo dell'unit DMA
(Direct Memory Access). Per mezzo di cache di vario livello e tecniche di predizione dei salti,
si cerca di avere sempre in cache quello di cui si ha bisogno. La cache una delle tecniche
per mezzo delle quali si ha un accesso veloce ai dati e al programma.
Sebbene l'architettura di von Neumann non permetta accessi contemporanei alla
memoria dati e programma, possiede il vantaggio della flessibilit della memoria stessa.
Infatti i registri delle configurazioni dei moduli interni non sono altro che celle di
memoria RAM per cui facile accedere a qualunque registro per mezzo del DMA
indipendentemente dal fatto che sia un accesso alla memoria dati o programma. Questa
architettura permette inoltre di eseguire il programma indipendentemente dalla memoria,
sia essa memoria programma flash o RAM. Questo favorisce quelle applicazioni, che per
ridurre i consumi dovuti a algoritmi di calcolo complessi, sfruttano i minori consumi
raggiungibili dalla memoria RAM, dell'ordine di 60A/MHz.
L'architettura di von Neumann risulta inoltre pi semplice da implementare, favorendo
ulteriormente sistemi che devono operare con bassi consumi. Quante sono tra le ragioni
principali per cui microcontrollori come gli MSP430 della Texas Instruments fanno uso
dell'architettura di von Neumann piuttosto che Harvard.
Oltre all'architettura Harvard, si detto che la CPU dei PIC18 di tipo RISC. Il nome
RISC discende dal fatto che il numero di istruzioni eseguibili dal PIC sono limitate se
paragonate a quelle messe a disposizione da un microprocessore classico con CPU di tipo
CISC (Complex Instruction Set Computer). Infatti i PIC18 possiedono 75 istruzioni base 36
mentre microprocessori tipo il Pentium ne possiedono diverse centinaia. Come per il
discorso effettuato per i sistemi embedded, parlare di RISC e CISC semplicemente su una
base numerica non ben definita non permette di definire in maniera assoluta quando si
36

I PIC18 possiedono la modalit estesa che aggiunge 8 ulteriori istruzioni a quelle base. Tale istruzioni sono state aggiunte
per poter ottimizzare la compilazione del codice C.

46

debba parlare di RISC o CISC. In futuro ci che oggi considerato CISC verr forse
considerato RISC. I PIC della famiglia Midrange, per esempio i PIC16, possiedono per
esempio solo 35 istruzioni per cui i PIC18 potrebbero essere considerati quasi
un'architettura CISC se confrontata con i PIC16.
Dopo questa breve nota introduttiva sull'architettura dei PIC18, vediamo qualche
dettaglio in pi, in particolare considerando la struttura interna del PIC18F4550, riportata
in Figura 8.
Dallo schema a blocchi, presente in ogni Datasheet, immediato vedere le periferiche
che possiede il PIC d'interesse, in particolare l'organizzazione dei registri della memoria e
delle periferiche. Sono inoltre messi in evidenza i vari bus che interconnettono le varie
periferiche e il loro parallelismo, ovvero il numero di linee disponibili. In particolare
possibile vedere che la memoria programma e dati sono separate. Una caratteristica
importante introdotta nei PIC18 la presenza del moltiplicatore 8x8, per mezzo del quale
possibile ridurre notevolmente il numero di istruzioni (quindi il tempo di esecuzione)
necessario per l'esecuzione di moltiplicazioni. Un'altra importante caratteristica presente
nei PIC18, non visibile a livello di schema a blocchi, l'introduzione dei livelli di priorit
delle interruzioni. Diversamente dalla famiglia Midrange, i PIC 18 hanno infatti un livello
di priorit delle interruzioni alto ed uno basso. Il livello d'interruzione alto compatibile
con quello presente anche nei PIC16.
I i quadrati con la croce interna rappresentano i pin fisici del PIC, ovvero
disponibili sul contenitore del componente.

Sulla destra di Figura 8 sono presenti diversi blocchi nominati PORTx, questi
rappresentano forse i moduli principali del PIC, per mezzo dei quali possibile creare dei
percorsi di input o di output dal PIC stesso, ovvero potersi interfacciare con il mondo
esterno. Le porte PORTx permettono infatti di leggere dati dall'esterno come per esempio
dei pulsanti, sensori, ma al tempo stesso permettono di attivare e fornire delle
informazioni per mezzo di luci, LED, display o altro.

47

Figura 8: Schema a blocchi del PIC18F4550.

48

Organizzazione della Memoria


Ogni microcontrollore possiede al suo interno almeno due tipi di memoria, quella per
memorizzare il programma in maniera permanente e la memoria dati 37. La memoria
permanente ha il vantaggio di poter mantenere il contenuto anche a sistema spento. La
memoria RAM invece di tipo volatile, ovvero il suo contenuto viene perso allo
spegnimento del sistema. I primi microcontrollori Microchip facevano uso della memoria
OTP (One Time Programmable) ovvero programmabili una sola volta. Successivamente sono
comparse nel mercato le memorie EPROM cancellabili con raggi ultravioletti per mezzo
di una finestra posta sul microcontrollore. Solo verso la fine degli anni 90 la memoria
flash divenuta la memoria principale di tipo non volatile, grazie alla possibilit di poter
essere riscritta, ovvero cancellata per mezzo di opportune tensioni superiori a quelle
operative, applicate ai pin di programmazione. La tipologia della memoria facilmente
individuabile dal nome del PIC stesso. Infatti i PIC con memoria UV EPROM hanno una
C e sono caratterizzati da una finestra di vetro sul contenitore stesso.
La presenza della sola C, come PIC16Cxxx individua i dispositivi con memoria OTP,
mentre i PIC18, tutti con memoria Flash, hanno la lettera F, ovvero PIC18Fxxxx.
Come visto i PIC18 hanno una struttura Harvard, ovvero la memoria programma e dati
hanno bus dedicati, permettendo dunque un accesso simultaneo. I PIC18, come anche i
PIC delle altre famiglie possiedono in particolare i seguenti tipi di memoria:

Memoria Programma (Program Memory)


Memoria Dati (Data Memory)
Memoria EEPROM (EEPROM Memory)

Vediamo ora in maggior dettaglio i vari tipi di memoria e le loro caratteristiche.

Memoria Programma
La memoria programma quella memoria dove risiede in maniera permanente il
programma che il microcontrollore deve eseguire. In particolare i PIC18 possiedono una
memoria Flash38. Tale tipo di memoria ha la caratteristica di poter essere cancellata e
riscritta pi volte (circa 100.000). Un'altra caratteristica importante della memoria Flash
quella di poter mantenere il programma memorizzato anche quando il microcontrollore
non alimentato39.
Normalmente il programma viene caricato all'interno della memoria per mezzo del
programmatore, sfruttando i pin del microcontrollore dedicati alla programmazione. Oltre
a questa modalit di programmazione, i PIC18, come anche molti altri PIC di altre
famiglie, hanno la caratteristica di auto-programmazione per mezzo del software (Selfprogramming under software control). Questo significa che possibile scrivere un programma
sul PIC e permettere al programma stesso di utilizzare la memoria flash sia in lettura che
scrittura40. Tale caratteristica permette ad un programma di aggiornarsi senza aver bisogno
37

38

39

40

Ci sono in realt casi particolari, non nei PIC18, in cui la memoria esterna al microcontrollore stesso, e viene letta per
mezzo dell'interfaccia SPI.
importante ricordare che vi sono anche PIC di altre famiglie, che al posto della memoria flash, fanno uso di una memoria
OTP, ovvero scrivibile una sola volta.
La memoria flash, come anche quella EEPROM, sono specificate per mantenere memoria dei dati per oltre 40 anni;
naturalmente tale valore solo un valore statistico.
I primi microcontrollori non avevano la possibilit di riscrivere la memoria flash interna, la quale era disponibile in sola
lettura. Programmare la memoria richiede una tensione di circa 14V mentre l'alimentazione tipicamente di 5V o 3.3V. I

49

di un programmatore. L'applicazione che permette di aggiornare un programma noto


come bootloader. La Microchip fornisce vari tipi di bootloader che possono essere integrati
con un qualunque programma e permettono il loro aggiornamento per mezzo della porta
seriale RS232, USB, CAN e Ethernet. E' comunque possibile utilizzare un qualunque altro
canale di comunicazione per mezzo del quale pu essere trasferita la nuova versione di
programma che si vuole installare.
La caratteristica di auto-programmazione pu essere utilizzata per memorizzare in
maniera permanente dei dati evitando di utilizzare la EEPROM, di dimensioni pi ridotte.
Ciononostante l'utilizzo della memoria Flash per memorizzare dati pu essere pi
complicato della memoria EEPROM, che pu essere letta e scritta per singole celle di
memoria. Infatti la memoria Flash dei PIC18, come anche negli altri microcontrollori,
pu essere scritta e cancellata solo per blocchi 41. In particolare la memoria pu essere letta
un byte alla volta, scritta 32 byte alla volta e cancellata 64 byte alla volta 42. Questa
divisione in blocchi pu portare ad un notevole spreco di memoria o lavoro di salvataggio
e riscrittura dati qualora si voglia scrivere anche un solo byte.
Un indirizzo di memoria Flash, vuoto o cancellato possiede tutti i bit ad 1. Durante la
scrittura della cella di memoria si pu tranquillamente invertire un bit da 1 a 0.
L'inversione da 0 a 1 possibile solo per mezzo della cancellazione della zona di memoria,
ovvero pagina dove presentente il byte da cancellare.
Dopo questa panoramica sulle caratteristiche fisiche della memoria programma,
vediamo in maggior dettaglio le sue caratteristiche ed utilizzo da parte della CPU.
Abbiamo per ora compreso che il programma scritto in C, una volta convertito in
linguaggio macchina, viene caricato in memoria, importante ora capire dove viene
caricato. La memoria programma una memoria Flash la cui larghezza della parola
memorizzata per cella di un byte, ovvero 8 bit. Le istruzioni del PIC, ovvero i comandi
che riesce a compiere, sono per la maggior parte di ampiezza pari a due byte, ma ce ne
sono alcune (CALL, MOVFF, GOTO, e LSFR) che necessitano di 4 byte. Questo
significa che per ogni istruzione sono necessarie rispettivamente 2 o 4 celle di memoria.
In Figura 8 possibile vedere che la memoria programma viene letta con l'ausilio del
registro speciale PC (Program Counter). Tale registro di 21 bit, ovvero potenzialmente
permette di puntare fino a 2MByte 43 di memoria. Il valore del registro PC rappresenta
l'indirizzo di memoria programma associata all'istruzione che deve essere caricata (fetch).
Quando l'istruzione viene caricata viene incrementato di 2 in modo da puntare l'istruzione
successiva44. Il PIC18F4550 possiede 32KByte di memoria, ovvero possibile scrivere
circa 16.000 istruzioni elementari. Se il PC viene impostato ad un valore di memoria non
valido, il valore letto pari a 0 come visibile in Figura 9.

41

42
43
44

microcontrollori con possibilit di riprogrammare la flash hanno una charge pump (pompa di carica) che permette di elevare
la tensione di alimentazione a 14V permettendo la cancellazione e riscrittura della memoria flash.
La necessit di lettura, scrittura e cancellazione in blocchi una caratteristica delle memorie Flash, non legata alla struttura
del PIC18.
Prima di poter scrivere nuovi dati su di un blocco di memoria, necessario effettuare la cancellazione dei vecchi dati.
Attualmente non vi sono PIC18 che possiedono 2MByte di memoria.
Nel caso di istruzioni a 4 byte, il registro PC viene incrementato di 4.

50

Figura 9: Architettura della memoria programma del PIC18F4550.

La Figura 9 mette in evidenza il fatto che la memoria programma sequenziale, dal


valore 0000h (valore espresso in esadecimale) al suo valore massimo. Dalla Figura
possibile per leggere che l'indirizzo 0h nominato Reset Vector, mentre l'indirizzo 08h e
18h sono rispettivamente nominati High Priority Interrupt Vector e Low Priority Interrupt
Vector. Questi indirizzi di memoria hanno un significato speciale e deve essere ben
compreso. In particolare scrivendo in C ci si scorder quasi del Reset Vector e di quanto si
dir a breve, mentre quanto verr detto per gli Interrupt Vector richiesto anche durante la
programmazione in C e verr ripreso a tempo debito.
L'indirizzo 0000h, ovvero il Reset Vector, rappresenta l'indirizzo a cui viene posto il PC
quando viene resettato il microcontrollore. Questo rappresenta dunque il punto di
partenza del nostro programma, ed infatti in questo punto che il programmatore
inserisce le nostre prime istruzioni45. A questo punto vi sarete resi conto che il secondo
indirizzo speciale posto a soli 8 indirizzi dopo, dunque il nostro programma non pu
essere memorizzato in maniera sequenziale. Infatti quello che il compilatore fa, o quello
che il programmatore dovrebbe fare scrivendo il programma in Assembly, quello di
utilizzare gli indirizzi iniziali solo per memorizzare un salto, che normalmente va a
puntare la funzione main del nostro programma, ovvero la funzione principale della
45

Il Reset Vector presente in tutti i microcontrollori e Microprocessori di qualunque marca. L'indirizzo a cui questo viene
posto pu differire a seconda dei modelli.

51

nostra applicazione46.
Gli indirizzi 0008h e 0018h rappresentano gli indirizzi particolari ai quali viene posto il
registro PC quando viene generata un'interruzione, ovvero quando la CPU viene
interrotta dalla normale esecuzione del programma al fine di compiere altre azioni. In
particolare un'interruzione ad alta priorit pone il PC al valore 0008h mentre
un'interruzione a bassa priorit pone il PC al valore 0018h. Come per il caso del Reset
Vector, il numero di locazioni disponibili tra i due Interrupt Vector non sufficiente per
gestire eventuali periferiche che hanno generato un'interruzione, per tale ragione le
locazioni di memoria associate agli Interrupt Vector sono in generale utilizzate per fare un
salto alla funzione che gestisce le interruzioni, ovvero l'Interrupt Service Routine (ISR).
Maggiori dettagli sull'argomento sono forniti nel Capitolo dedicato alle interruzioni.
Oltre al registro PC, in Figura 9 possibile vedere che in alto sono presenti delle celle
di memoria particolari nominate Stack. In particolare sono presenti 31 celle di memoria
della larghezza di 21 bit (giusti, giusti per memorizzare il registro PC). Tali locazioni di
memoria rappresentano un buffer di indirizzi nel quale viene memorizzato il registro PC
ogni volta che bisogna fare un salto nel nostro programma, sia per eseguire una funzione
generica sia per eseguire una routine associata ad un'interruzione. Questo buffer
necessario poich quando si interrompe il normale flusso del programma necessario
memorizzare l'indirizzo al quale bisogna tornare (ovvero quale istruzione eseguire) al
termine dell'esecuzione delle istruzioni associate alla funzione che ha interrotto il
programma. Il valore del PC che viene salvato proprio quello dell'istruzione successiva
che deve essere eseguita. L'indirizzo contenuto nel PC salvato nel buffer in pila, ovvero
l'ultimo indirizzo salvato il primo al quale ritornare, e cosi via. Dal momento che sono
presenti 31 livelli di Stack, possibile utilizzare fino a 31 livelli annidati di funzioni 47. Allo
Stack associato un registro particolare nominato Stack Pointer (STKPTR), che ha il
compito di puntare il livello di ritorno, ovvero indirizzo, che deve essere caricato nel
registro PC.
In ultimo bene ricordare che sia il Program Counter che lo Stack Pointer possono essere
gestiti manualmente via software facendo uso di registri interni dedicati. Le modalit della
loro gestione e applicazioni esulano dagli scopi di questo testo. La loro gestione di
fondamentale importanza nel caso in cui si faccia uso di sistemi operativi RTOS (Real
Time Operative System) dedicati.
Quanto detto fin ora legato strettamente alla struttura della memoria, per far
funzionare il tutto necessario un clock per sincronizzare i vari blocchi e permettere il
giusto incremento o decremento dei registri. Il clock interno al PIC, come verr spiegato
nel paragrafo dedicato, pu essere generato per mezzo dell'oscillatore interno o esterno,
in particolare vi sono diverse opzioni che possibile selezionare. Usando per esempio un
quarzo esterno da 20MHz, la CPU non lavora direttamente a tale frequenza bens ad un
quarto della frequenza principale. Dal clock principale si ottengono infatti altri quattro
segnali di clock, tra loro sfasati, ricavati dividendo per 4 il clock principale; la Figura 10
riporta il clock principale e le sue fasi derivate dalla divisione per 4. In particolare 4 cicli
del clock principale rappresentano un ciclo istruzione, ovvero la CPU impiega 4 cicli del
clock principale per poter eseguire un'istruzione.
46

47

In realt prima della funzione main vengono richiamate altre funzioni di inizializzazione, che il programmatore normalmente
non utilizza ma potrebbe sovrascrivere o arricchire.
Nel caso si faccia uso della modalit DEBUG, 5 livelli saranno utilizzati dalla funzionalit di DEBUG, dunque devono
essere preventivamente considerati per evitare problemi.

52

Figura 10: Clock principale e clock interno.

Quanto detto visibile nella parte bassa della Figura 10, dove possibile vedere che
durante i 4 cicli di clock che rappresentano un ciclo istruzione, viene eseguita una fase di
caricamento istruzione e una decodifica. Bisogna per porre attenzione al fatto che lo
schema a blocchi indica che la fase di caricamento dell'istruzione (detta Fetch) relativa al
valore del registro PC; ovvero c' la preparazione per caricare l'istruzione presente nella
locazione PC e PC+1 la quale caricata nell'Istruction Register (IR) durante la fase di
Execute. La fase di esecuzione (nominata Execute) invece associata al valore del registro
PC-2, ovvero all'istruzione precedente, in particolare la fase di esecuzione preceduta
dalla fase di decodifica, ovvero di comprensione dell'istruzione stessa 48. Le due fasi di
Fetch ed Execute sono eseguite per mezzo di una pipeline ovvero in parallelo49, dunque
anche se si carica un'istruzione e si esegue l'istruzione precedente, a tutti gli effetti si ha
l'esecuzione di un'istruzione completa ogni 4 cicli del clock principale ovvero ad ogni
ciclo di clock istruzione. A questa regola fanno eccezione le istruzioni di salto, le quali
richiedono due cicli istruzioni.

Memoria Dati
Ogni programma durante la sua esecuzione ha in genere bisogno di variabili per
mantenere determinati dati o risultati di operazioni. Tali informazioni sono spesso
memorizzate nella memoria dati. I PIC18 come anche gli altri microcontrollori,
implementano la memoria dati per mezzo di memoria RAM (Random Access Memory) di
tipo statica, tale tipo di memoria allo spegnimento dell'alimentazione, perde tutti i dati. La
larghezza dell'indirizzo utilizzato per puntare la memoria RAM di 12 bit. Questo
permette di indirizzare fino a 4096 locazioni di memoria. Ciononostante la memoria
48

49

Frequentemente si trova che un'istruzione viene suddivisa in tre fasi: caricamento, decodifica ed esecuzione. La fase di
decodifica ed esecuzione sono spesso tra loro combinate un'unica fase.
Una pipeline di questo tipo detta a due livelli. Oltre ad un'aggiunta di ulteriore hardware per eseguire e caricare due
istruzioni, la sua realizzazione non comporta molti problemi. CPU pi complesse, quali per esempio i microprocessori Intel,
AMD hanno molti livelli di pipeline per permettere l'esecuzione contemporanea di pi istruzioni. Con molti livelli di pipeline
l'esecuzione e la gestione del programma diviene molto pi complessa. Per esempio in caso di salti condizionali necessario
cercare di predire se il salto sar da un lato o dall'altro...se la predizione sar errata sar necessario gettare tutti i valori nella
pipeline e ricominciare i calcoli. A questo livello di complessit anche i compilatori possono ottimizzare il codice macchina
per aiutare la pipeline nell'esecuzione del codice. Si capisce dunque che a parit di frequenza, due CPU possono essere una
pi veloce dell'altra poich hanno due pipeline differenti.

53

implementata all'interno del PIC pu variare da modello a modello, in particolare il


PIC18F4550 possiede al suo interno 2048 byte di memoria RAM. La memoria RAM
interna al PIC suddivisa in blocchi (nominati bank) di 256 byte, dunque il PIC18F4550
possiede 8 banchi di memoria come riportato in Figura 11.

Figura 11: Divisione in blocchi della memoria.

Per tale ragione ogni volta che si deve accedere ad una variabile necessario sapere in
quale blocco situata. Molte istruzioni che accedono alla memoria possiedono solo 8 bit
per l'indirizzo, che appunto il numero di bit necessario per puntare le 256 locazioni di
memoria all'interno di un blocco. I restanti 4 bit, per determinare il blocco, vengono
prelevati dai bit BSR3:BSR0 del registro BSR.
La Figura 11 mette in evidenza che gli 8 banchi di memoria sono nominati GPR
ovvero General Purpose Register (registri per uso generico). Oltre ai registri GPR sono
presenti anche i registri SFRs ovvero Special Function Registers (registri per uso speciale).
54

I registri per usi speciali sono tutti quei registri utilizzati per impostare le periferiche
interne al PIC stesso. Infatti ogni periferica ha sempre dei registri speciali ad essa dedicati,
per mezzo dei quali possibile impostare il comportamento della periferica stessa.
Maggiori dettagli si vedranno quando si studieranno le varie periferiche interne al PIC.
Un registro speciale un po' particolare lo Status Register (Registro di Stato), tale registro
praticamente presente in ogni architettura sia essa un microcontrollore che un
microprocessore. Lo Status Register contiene lo stato aritmetico dell'ALU (Arithmetic Logic
Unit) ovvero di quella parte della CPU che viene utilizzata per svolgere tutte le operazioni
binarie. In particolare l'ALU svolge le proprie operazioni per mezzo del registro speciale
W, nominato accumulatore50. Ogni volta che necessario svolgere delle operazioni queste
devono essere svolte all'interno del registro W o tra W ed un altro registro. Ad ogni
operazione il registro di stato fornisce l'informazione con opportuni bit, se l'operazione
ha generato un risultato nullo, un overflow51 (trabocco), un numero negativo o si
verificato un riporto.
Si fa notare che la descrizione della memoria dati stata spiegata per sommi capi
poich gran parte dei dettagli associati ai blocchi possono essere trascurati quando si
programma in C. Ciononostante, qualora si dovesse avere l'esigenza di ottimizzare
l'accesso in lettura e scrittura dati all'interno di strutture dati dedicate a particolari
applicazioni, potrebbe essere necessaria una maggior comprensione dell'architettura della
memoria dati e delle sue modalit di accesso. In particolare si ricorda che la modalit
estesa dei PIC, con le sue 8 nuove istruzioni, introduce due nuove modalit di accesso alla
memoria dati, oltre alle 4 gi esistenti, permettendo di ottimizzare ulteriormente il
programma. Alcuni problemi con la memoria dati e la sua divisione in blocchi si pu
avere anche nel caso in cui si debbano creare delle strutture dati che richiedono pi di 256
byte o che comunque vengono dislocate in due o pi blocchi di memoria. Ultima nota per
quanto riguarda il PIC18F4550, che i blocchi di memoria da 4 a 7 sono dedicati al buffer
del modulo USB, qualora questo risulti attivo. Per maggiori dettagli si rimanda al
Datasheet del PIC utilizzato.

Memoria EEPROM
Qualora si abbia la necessit di memorizzare un codice segreto durante l'esecuzione del
programma, o semplicemente la temperatura massima e minima della giornata, pu
ritornare utile l'utilizzo di una memoria permanente che possa mantenere il valore
memorizzato anche nel caso in cui il microcontrollore non dovesse essere pi alimentato.
Come detto possibile fare questo per mezzo della memoria Programma ovvero Flash. In
particolare se il codice segreto deve essere fisso ed noto sin dall'inizio, sicuramente
pratico far uso della memoria programma, come si vedr nei Capitoli successivi basta
dichiarare una costante52. Qualora per i dati debbano essere cambiati durante
l'esecuzione del programma, l'utilizzo della memoria Flash risulta poco pratico, infatti,
come detto, possibile cancellare la memoria Flash solo in pagine, ovvero blocchi. Per
50

51

52

Microprocessori pi complessi possiedono normalmente pi accumulatori. Questo permette di ottimizzare i tempi di calcolo
poich non bisogna sempre caricare i dati in un solo accumulatore. La presenza di pi accumulatori inoltre fondamentale
in architetture con pipeline.
Se per esempio si sommano due numeri contenuti in un byte ed il risultato salvato in un byte, il valore deve essere al
massimo pari a 255. Se la somma fornisce un risultato maggiore di 255 si ha un overflow del registro.
Un valore costante, ovvero definito come const, viene posto dal compilatore XC8 direttamente in memoria Flash.

55

queste applicazioni i PIC18 mettono a disposizione la memoria EEPROM (Electrically


Erasable Programmable Read Only Memory).
Dal momento che ogni cella di memoria pu essere scritta singolarmente, la parola Read
all'interno dell'acronimo EEPROM deve essere considerata come Read e Write.
Diversamente dalla memoria Flash, precedentemente descritta, non necessario
cancellare manualmente la cella di memoria, la cancellazione dei suoi contenuti, pur
sempre necessaria, viene fatta in automatico all'avvio di un ciclo di scrittura.
La quantit di memoria EEPROM ridotta a poche locazioni di memoria se
paragonate alla memoria programma, sono infatti presenti solo 256 celle, ciononostante la
sua presenza certamente un bene prezioso 53. Nel caso in cui dovesse essere necessaria
pi memoria EEPROM, sempre possibile aggiungere molti KB per mezzo delle
memorie EEPROM a soli 8 pin, controllabili per mezzo dei bus seriali I2C o SPI54. La
Microchip fornisce oltre ai microcontrollori PIC una delle pi ampie scelte di memorie
esterne I2C e SPI.
La memoria EEPROM, interna ai PIC, non direttamente mappata n tra i Register
File n nella Memoria Programma. La sua gestione avviene per mezzo di alcuni registri
speciali dedicati (Special Function Registers, SFR):

EECON1
EECON2
EEADR
EEDATA

EECON1 il registro utilizzato per il controllo della fase di scrittura e lettura. Tale
registro lo stesso che necessario utilizzare per programmare la memoria Flash, i bit
CFGS e EEPGD determinano se il suo accesso associato alla memoria Flash o
EEPROM. Di seguito si riporta il contenuto del registro EECON1. La rappresentazione
della tabella la stessa utilizzata nei Datasheet della Microchip. Il registro EECON2 viene
utilizzato per rendere sicura la fase di scrittura in memoria, ovvero evitare scritture
accidentali della memoria. Infatti prima di avviare una sequenza di scrittura, necessario
scrivere in successione nel registro EECON2 prima il valore 55h e poi AAh, in modo da
dichiarare la volont di avviare un processo di scrittura.
I registri EEADR e EEDATA sono rispettivamente utilizzati per scrivere l'indirizzo
della locazione di memoria da andare a leggere o scrivere e il valore da assegnare o
restituito dall'indirizzo selezionato55.
Vediamo ora in maggior dettaglio la sequenza di istruzioni necessarie per scrivere e
leggere la memoria EEPROM. Per ragioni di praticit e per introdurre qualche altra
informazione utile alla programmazione, si mostrano le sequenze in Assembly. In questo
caso sono molto semplici e di facile comprensione anche se non si ha una conoscenza
dell'Assembly56.
53

54
55

56

I PIC18, oltre alla protezione in lettura della memoria Programma, per permettere la protezione dell'IP (Intellectual Propriety)
associato al programma, permette di proteggere anche la EEPROM da letture esterne effettuate per mezzo del
programmatore.
Un esempio della gestione di una EEPROM per mezzo del bus I2C verr descritto nei successivi Capitoli.
Il registro EEADR un registro di un byte, da ci discende anche il limite di 256 locazioni di memoria EEPROM.
Naturalmente se si fosse utilizzato un registro pi ampio o due registri si sarebbe potuta avere pi memoria, ma i costi della
memoria sono in generale piuttosto grandi, dunque se si vuole pi memoria bisogna pagarla.
Si rimanda al Datasheet del PIC utilizzato per maggiori informazioni riguardo all'utilizzo delle istruzioni Assembly.

56

Registro EECON1: Data EEPROM Control Register 1


R/W-x

R/W-x

U-0

R/W-0

R/W-x

R/W-1

R/S-0

R/S-0

EEPGD

CFGS

FREE

WRERR

WREN

WR

RD

Bit 7

Bit 6

Bit 5

Bit 4

Bit 3

Bit 2

Bit 1

Bit 0

Leggenda
R = Readable bit
-n = Value at POR

W = Writable bit
1 = Bit is set

U = Unimplemented bit read as 0


0 = Bit is cleared

Bit 7

EEPGD : Selezione memoria Programma (Flash) o EEPROM.


1 : Seleziona accesso memoria Programma.
0 : Seleziona accesso memoria EEPROM.

Bit 6

CFGS : Memoria Programma/EEPROM o configurazione.


1 : Accedi ai registri di configurazione.
0 : Accedi alla memoria Programma o EEPROM.

Bit 5

Non implementato, letto come 0.

Bit 4

FREE : Flash Row Erase Enable bit.


1 : Cancella la sezione di memoria Programma al prossimo ciclo di scrittura.
0 : Effettua solo la scrittura.

Bit 3

WRERR : Bit di errore scrittura memoria Programma o EEPROM.


1: Il ciclo di scrittura stato prematuramente terminato.
0: Il ciclo di scrittura stato propriamente terminato.

Bit 2

WREN : Abilita la possibilit di scrittura.


1: Abilita la scrittura nella memoria Programma o EEPROM.
0: Disabilita la scrittura nella memoria Programma o EEPROM.

Bit 1

WR : Bit di controllo della scrittura.


1: Avvia la scrittura. Il bit viene posto a 0 a fine scrittura.
0: Il ciclo di scrittura terminato.

Bit 0

RD : Bit di controllo della lettura.


1: Avvia il ciclo di lettura.
0: Non avvia il ciclo di lettura.

S : Settable bit
x = Bit is unknown

Sequenza di lettura
Dal codice relativo alla sequenza di lettura possibile vedere che possibile scrivere i
commenti al fianco dell'istruzione, precedendo quest'ultimi con un punto e virgola. Il
primo passo consiste nel caricare il valore dell'indirizzo di cui si vuole leggere il
contenuto. In C si sarebbe scritto solamente EEADR = indirizzo mentre in Assembly
tutti i movimenti tra registri vengono generalmente fatti per mezzo del registro
intermediario W, ovvero l'accumulatore57. Tale passaggio sarebbe anche il modo con cui il
compilatore traduce la nostra istruzione in C.
MOLW
MOVWF
BCF
BCF
BSF
MOVF

DATA_EE_ADDR
EEADR
EECON1, EEPGD
EECON1, CFGS
EECON1, RD
EEDATA, W

;
;
;
;
;
;

Carico l'indirizzo da leggere in W


Carico l'indirizzo da leggere in EEADR
Pongo a 0 il bit EEPGD di EECON1
Pongo a 0 il bit CFGS di EECON1
Pongo a 1 RD, avviando la lettura
Leggo il contenuto di EEDATA

Una volta caricato il valore del nostro indirizzo in EEADR, necessario impostare i bit
EEPGD e CFGS. Quando il loro valore 0 vuol dire che la fase di Read associata alla
EEPROM e non alla memoria Flash. Per porre a 0 tali bit ci sono vari modi, ma
sicuramente il pi pratico quello di usare l'istruzione BCF (Bit Clear F) che pone appunto
57

L'accumulatore non solo necessario per il movimento di dati tra registri ma anche necessario per compiere operazioni tra
registri, quali per esempio somma e sottrazione. Infatti il registro W collegato direttamente con il modulo ALU (Arithmetic
Logic Unit) che permette di svolgere le operazioni tra registri.

57

a 0 (Clear) il bit di un determinato registro. Impostati i bit di selezione per la EEPROM


possibile avviare la lettura per mezzo del bit RD che deve essere posto ad 1. Per fare
questo si utilizza l'istruzione complementare a BCF ovvero BSF. Avviata la lettura, il dato
reso disponibile all'interno del registro EEDATA a partire dal ciclo istruzione
successiva, ovvero possibile leggere EEDATA subito dopo. Il registro EEDATA
mantiene il dato fino ad una nuova lettura o eventuale scrittura su si esso. Come visibile,
anche questa volta il valore viene letto per mezzo del registro W. Una volta posto il dato
in W possibile porlo in un qualunque altro registro presente nella memoria dati, ovvero
in una qualunque nostra variabile.
Sequenza di scrittura
La sequenza di scrittura diversamente da quella di lettura un po' pi laboriosa, per tale
ragione ho deciso di scrivere una libreria ad hoc per semplificare la fase lettura e di
scrittura in EEPROM.
MOLW
MOVWF
MOLW
MOVWF
BCF
BCF
BSF

DATA_EE_ADDR
EEADR
DATA_EE_DATA
EEDATA
EECON1, EEPGD
EECON1, CFGS
EECON1, WREN

;
;
;
;
;
;
;

Carico l'indirizzo da scrivere in W


Carico l'indirizzo da scrivere in EEADR
Carico il dato da scrivere in W
Carico l'indirizzo da leggere in EEADR
Pongo a 0 il bit EEPGD di EECON1
Pongo a 0 il bit CFGS di EECON1
Abilito la fase di scrittura (non avvio)

BCF

INTCON, GIE

; Disabilito gli Interrupt

MOLW
MOVWF
MOLW
MOVWF

55h
EECON2
AAh
EECON2

;
;
;
;

BSF

EECON1, WR

; Avvio la fase di scrittura

BSF

INTCON, GIE

; Abilito nuovamente gli Interrupt

BCF

EECON1, WREN

; Disabilito la scrittura

Carico
Carico
Carico
Carico

55h in W
il dato in EECON2
AAh in W
il dato in EECON2

La parte iniziale della sequenza praticamente identica alla precedente, per oltre ad
impostare il registro dell'indirizzo EEADR necessario impostare anche il registro
EEDATA con il dato che si vuole scrivere. Fatto questo si seleziona la EEPROM
piuttosto che la memoria Flash, per mezzo dei bit EEPGD e CFGS. In ultimo si abilita la
possibilit di scrivere la memoria EEPROM settando il valore del bit WREN58.
Una volta abilitata la possibilit di scrivere in EEPROM consigliato disabilitare le
interruzioni (se usate), se infatti la sequenza di scrittura dovesse essere interrotta si
potrebbe avere la perdita di dati o dati corrotti. Disabilitate le interruzioni, necessario
scrivere la sequenza magica all'interno del registro EECON2, ovvero prima il byte 55h e
successivamente AAh. Come visibile tale scrittura avviene sempre per mezzo del registro
W. Dopo tale sequenza possibile avviare la scrittura ponendo ad 1 il bit WR nel registro
EECON1. Tale bit rimane ad 1 per tutto il processo di scrittura, che diversamente dalla
lettura pu impiegare pi di un ciclo di clock. In particolare se si ha interesse a sapere la
58

Oltre alla sequenza 55h e AAh da scrivere nel registro EECON2, un'altra tecnica per proteggere la EEPROM da scritture
accidentali e data dal bit WREN. In particolare all'avvio del PIC (Power Cycle) tale bit viene posto a 0. Inoltre la scrittura in
EEPROM durante la fase di Power-Up disabilitata. In questo modo si evita di compromettere i dati in EEPROM a causa di
scritture accidentali.

58

fine del processo di scrittura si pu controllare in polling (ovvero leggendo ripetutamente)


il bit WR. Se ci sono errori in scrittura, ovvero la scrittura non stata portata a termine,
viene settato il bit WRERR. Tale errore non dice se il contenuto quello da noi voluto,
ma solo che la fase di scrittura terminata prematuramente. Per controllare che la fase di
scrittura avvenuta propriamente bene controllare il dato per mezzo di una sequenza di
lettura dell'indirizzo stesso. Tale approccio consigliato soprattutto in quelle applicazioni
in cui ogni cella di memoria possa essere scritta un numero di volte confrontabile con il
numero massimo di cicli di scrittura, tipicamente 100.000-1.000.000. In ogni modo un
controllo del dato scritto, rimane una buona pratica di programmazione a prescindere da
tale problema.

L'oscillatore
Ogni microcontrollore, per il suo funzionamento, necessita di un clock, ovvero di quel
segnale digitale per mezzo del quale vengono scandite tutte le operazioni. Come illustrato
gi in precedenza c' un legame diretto tra le istruzioni eseguite ed il clock. Il segnale di
clock viene generato per mezzo di un oscillatore. I PIC18 possiedono al loro interno tutta
la circuiteria per generare un clock interno ma anche la circuiteria per ottenere un clock
facendo uso di un quarzo esterno. La circuiteria associata all'oscillatore pi o meno
complessa a seconda del PIC utilizzato. In Figura 12 mostrato lo schema a blocchi
relativo al PIC18F4550.
Nel nostro caso, avendo preso come riferimento il PIC18F4550 bene mettere subito
in evidenza che si ha che fare con una circuiteria un po' complessa. Questo discende dal
fatto che il PIC18F4550 possiede al suo interno il modulo USB. E' possibile subito vedere
che sono presenti diverse selezioni e percorsi di cui si parler a breve.
Prima di entrare nel dettaglio dello schema a blocchi bene mettere in evidenza il fatto
che il PIC18F4550 pu avere il proprio oscillatore interno (o meglio oscillatori)
configurabile in 12 diverse modalit. Dallo schema a blocchi di Figura 12 possibile
vedere che sono presenti due oscillatori nominati primario e secondario. Il primario
normalmente utilizzato per permettere l'oscillazione di un quarzo esterno per generare il
clock utilizzato dal microcontrollore. Questo valido in generale per i PIC18, nel caso del
PIC18F4550 l'oscillatore primario viene anche utilizzato per generare il clock per il
modulo USB, quando questo viene abilitato. Oltre a questi due oscillatori, presente
l'oscillatore interno ad 8MHz. Quando abilitato pu essere utilizzato per generare il clock
del microcontrollore. E' possibile notare che la sua uscita collegata ad un postscaler
(divisore di frequenza), per mezzo del quale possibile ottenere una serie di frequenze
interne che possibile selezionare per mezzo del MUX interno. Sulla destra dello schema
a blocchi possibile osservare altri due MUX, per mezzo dei quali possibile selezionare
il clock da utilizzare per le periferiche e se utilizzare l'oscillatore principale per il modulo
USB. Un'altra caratteristica importante presente in molti PIC18 la presenza del PLL
(Phase Lock Loop) interno59. Tale dispositivo permette di ottenere clock ad elevata
frequenza, partendo da quarzi a frequenza ridotta. In particolare per l'utilizzo del modulo
USB spesso necessario l'uso del PLL al fine di ottenere i 48MHz necessari per le
specifiche USB High Speed.
59

Si noti che per il corretto funzionamento del PLL necessario dividere la frequenza del clock in maniera da avere una
frequenza in ingresso al PLL pari a 4 MHz. Per esempio facendo uso di un quarzo da 20MHz necessario impostare il
PLLDIV al valore di 5.

59

Figura 12: Circuiteria di clock del PIC18F4550.

Il diagramma a blocchi, come detto, mostra la presenza di diversi oscillatori. Questi


possono essere impostati per operare in diverse modalit, alcune di queste vengono
selezionate per mezzo dei registri CONFIG1L e CONFIG1H. Tali impostazioni sono
impostate nel PIC durante la fase di programmazione e rimangono inalterate fino a
quando il PIC non viene riprogrammato. Altre impostazioni sono invece selezionabili on
the fly, ovvero durante l'esecuzione del programma stesso, il registro da modificare
OSCCON. Questo registro permette di selezionare la sorgente del clock da utilizzare
come base tempo60. I modi operativi dell'oscillatore del PIC18F4550 sono, come detto 12.
Questi possono essere selezionati per mezzo dei registri CONFIG.
60

Vi sono anche altri registri associati alle impostazioni dell'oscillatore. Per maggiori dettagli si rimanda al Datasheet del PIC
utilizzato.

60

XT
La modalit XT utilizza l'oscillatore primario, al quale deve essere collegato un
quarzo.

XTPLL
E' come la modalit XT ma con il PLL abilitato.

HS
E' come la modalit XT ma viene utilizzata nel caso di quarzi ad alta frequenza.
Tale modalit pu per essere utilizzata anche senza quarzo, facendo uso di un
oscillatore esterno, come riportato in Figura 13. Quando si fa utilizzo di un
oscillatore esterno, non vi il tipico problema di ritardo legato allo start up (avvio)
dell'oscillatore. Tale ritardo pu anche essere associato ad un risveglio del
microcontrollore, oltre che ad un Power Up del PIC o del sistema. Il pin di uscita
risulta molto utile nel caso si vogliano sincronizzare altri dispositivi o effettuare delle
calibrazioni, senza influenzare l'oscillatore principale.

Figura 13: Modalit HS con oscillatore esterno.

HSPLL
E' come la modalit HS ma viene abilitato il PLL interno.

EC
In questa modalit un clock esterno viene fornito in ingresso al PIC, ovvero non
richiesto il quarzo sull'oscillatore principale. Il secondo piedino normalmente
utilizzato per il quarzo viene utilizzato per riportare la Fosc/4 in uscita. Lo schema a
blocchi riportato in Figura 14.

Figura 14: Modalit EC con oscillatore esterno.

ECIO
Come la modalit EC, ma il pin RA6 pu essere utilizzato come I/O standard
invece che Fosc/4. Lo schema a blocchi riportato in Figura 15.

61

Figura 15: Modalit EC con oscillatore esterno e pin RA6.

ECPLL
Come la modalit EC ma con il PLL interno abilitato.

ECPIO
Come la modalit ECIO ma con il PLL interno abilitato.

INTHS
L'oscillatore interno utilizzato come clock per il microcontrollore, mentre
l'oscillatore HS utilizzato come sorgente per il clock utilizzato per il modulo USB.

INTXT
L'oscillatore interno utilizzato come clock per il microcontrollore, mentre
l'oscillatore XT utilizzato come sorgente per il clock utilizzato per il modulo USB.

INTIO
L'oscillatore interno utilizzato come clock per il microcontrollore, mentre
l'oscillatore EC utilizzato come sorgente per il clock utilizzato per il modulo USB.
Il pin RA6 utilizzato come I/O standard.

INCKO
L'oscillatore interno utilizzato come clock per il microcontrollore, mentre
l'oscillatore EC utilizzato come sorgente per il clock utilizzato per il modulo USB.

Vediamo in maggior dettaglio le modalit HS, XT, HSPLL e XTPLL. In tutte queste
modalit necessario un quarzo, la configurazione utilizzata riportata in Figura 16. E'
possibile notare che oltre al quarzo sono presenti anche due condensatori.

Figura 16: Utilizzo del quarzo esterno.

La circuiteria potrebbe essere anche pi complessa a seconda delle esigenze e del


quarzo utilizzato. I condensatori C1 e C2 sono dello stesso valore e vanno scelti a
62

seconda del quarzo utilizzato. In progetti che devono essere rilasciati per applicazioni
professionali, sempre bene fare test sulla qualit dell'oscillazione, al variare della
temperatura e tensione di alimentazione, nel range dei valori per cui deve essere
specificato il sistema. La Microchip fornisce delle linee guida che permettono di
selezionare i condensatori per alcuni valori standard di cristalli, in Tabella 2 sono riportati
i valori presenti sul Datasheet. Ciononostante al fine di scegliere il valore corretto
sempre bene far riferimento al Datasheet del quarzo utilizzato e verificare la Capacit di
carico CL richiesta per garantire una corretta oscillazione.
Modalit

Frequenza

C1 e C2

XT

4 MHz

27 pF

HS

4 MHz

27 pF

HS

8 MHz

22 pF

HS

20 MHz

15 pF

Tabella 2: Valori tipici di C1 e C2.

bene mettere in evidenza che indipendentemente dalla modalit selezionata


presente un postscaler (divisore) che pu essere utilizzato per impostare valori di frequenza
di clock inferiori a quello del quarzo stesso. Il valore pu essere impostato per mezzo del
registro interno CPUDIV.
Da quanto ora descritto, si certamente capito che sono presenti diverse possibilit per
generare il clock utilizzato dal microcontrollore. Non avendo ancora degli esempi tra le
mani molte cose sono ancora incomprensibili ma presto prenderanno forma.
Nel caso specifico del PIC18F4550 ci sono altre opzioni per il clock specificatamente
pensate per il modulo USB. In particolare bene mettere in evidenza che il modulo USB
a seconda delle modalit supportate (Low Speed o High Speed) richiede rispettivamente una
frequenza operativa di 6MHz e 48MHz. Nel caso della modalit Low Speed obbligatorio
far uso di un quarzo esterno da 24MHz mentre nel caso High Speed si ha maggior
flessibilit. Poich richiesta una frequenza di 48MHz, per il modulo USB necessario
far uso del modulo PLL interno precedentemente descritto.
Qualora il clock per il microcontrollore sia generato dalla stessa sorgente, il modulo
USB pu operare a 48MHz, mentre per mezzo dei postscaler possibile impostare la
frequenza del clock del microcontrollore a valori inferiori. Una caratteristica introdotta
dalla Microchip nei PIC18 quella di poter cambiare anche la frequenza di clock e
sorgente (oscillatore primario, secondario o interno) durante l'esecuzione del programma,
questo pu essere fatto per mezzo dei bit SCS1:SCS0 del registro OSCCON. Tale
caratteristica stata fornita prevalentemente per ragioni associate all'efficienza del
dispositivo, infatti abbassando la frequenza di clock possibile ridurre la potenza dissipata
dal microcontrollore. Questo significa che in condizioni in cui non richiesta alta velocit,
possibile ridurre i consumi riducendo la frequenza o la sorgente di clock. Altri
microcontrollori come gli MSP430 della Texas Instruments, possiedono varie modalit
Ultra Low Power, che si traducono appunto nell'abilitare o disabilitare determinate sorgenti
del clock disponibili internamente61.
Un'altra caratteristica importante dei PIC18 la presenza di un clock secondario in
61

Per maggiori dettagli sugli MSP430 far riferimento al corso MSP430 disponibile gratuitamente al sito www.LaurTec.it.

63

caso di malfunzionamento del clock delle periferiche (Fail Safe Clock Monitor). Qualora il
clock associato alle periferiche si dovesse interrompere sia esso il primario, secondario o
interno, il clock viene cambiato in automatico sul clock di guardia ottenuto dall'oscillatore
INTRC. Normalmente questo clock risulta a frequenza pi bassa di quella normalmente
utilizzata, dunque particolari moduli potrebbero non funzionare pi (il modulo USB tra
questi), ciononostante tale opzione risulta particolarmente importante poich permette di
abilitare funzioni di emergenza o effettuare uno spegnimento del sistema controllato. Lo
schema a blocchi di tale funzione riportato in Figura 17.

Figura 17: Schema a blocchi del monitoraggio del clock.

Per abilitare tale funzione necessario settare il bit FCMEN del registro CONFIG1H.
Il clock secondario ha gli ingressi T1OSO e T1OSI ai quali pu essere collegato anche un
quarzo esterno. Questo secondo oscillatore pensato per applicazioni a bassa frequenza
ed in particolare pu essere utilizzato per far oscillare quarzi a 32KHz o meglio 32768Hz.
Il valore ti tale cristallo pensato ad hoc per realizzare in maniera pratica riferimenti di 1
secondo necessari per esempio per realizzare degli orologi, ovvero calendari. Questa
funzione, come si vedr nei capitoli successivi pu essere implementata nel proprio
sistema anche facendo uso di circuiti integrati esterni come il PCF8563, ovvero per mezzo
di RTCC Real Time Clock Calendar.

Power Management
In un mondo che richiede sempre pi energia, avere la possibilit di ridurre i consumi
sicuramente un'opzione ben gradita. Le nuove tecnologie grazie alla miniaturizzazione dei
processi di realizzazione dei circuiti integrati, nonch della diminuzione delle tensioni
operative, ha permesso di ridurre notevolmente i consumi. Oltre alle innovazioni
tecnologiche, accorgimenti pi semplici possono essere altrettanto validi quale opzione
per ridurre i consumi. In particolare una tecnica utilizzata quella di utilizzare una
frequenza di clock non pi alta del necessario, infatti all'aumentare della frequenza di
clock aumenta anche la potenza richiesta dal sistema. I PIC18 mettono a disposizione la
circuiteria necessaria per cambiare la frequenza di clock anche durante l'esecuzione del
programma. Oltre alla possibilit di variare la frequenza di clock vi sono altre opzioni e
tecniche utilizzate per la riduzione dei consumi. Il PIC18F4550 possiede sette modalit
operative differenti, per mezzo delle quali possibile gestire in maniera ottimale i

64

consumi, in accordo con le specifiche del progetto 62. Le varie modalit si raggruppano in
tre diversi stati in cui il microcontrollore pu trovarsi:

Modalit Run
Modalit Idle
Modalit Sleep

Nella modalit Run, sia la CPU che le periferiche sono attive, il variare della frequenza
di clock permette di controllare i consumi. Nella modalit Idle, la CPU disattivata mentre
le periferiche possono rimanere abilitate a seconda delle esigenze. In questa modalit
possibile ottenere consumi ridotti fino a 5.8A (far riferimento al Datasheet per i valori
esatti associati al modello utilizzato). Nella modalit Sleep, sia la CPU che le periferiche
vengono disabilitate riducendo ulteriormente i consumi a 0.1A. Dal momento che sia
nella modalit Idle che Sleep la CPU viene disattivata, si capisce che non viene eseguita
nessuna istruzione. Da ci discende il fatto che l'unico modo per riattivare la CPU per
mezzo di un Reset o di un Interrupt dalle periferiche (preventivamente abilitato), in
particolare il Reset pu anche essere generato dal Watchdog Timer (descritto a breve).
Alcuni modelli di microcontrollori Microchip hanno anche la modalit Deep Sleep Mode,
ovvero sonno profondo, in cui i consumi sono ridotti ulteriormente a poche decine di
nA. Il risveglio da questo stato possibile per mezzo di Interrupt e Reset, oltre
naturalmente a un Power Cycle (Accendi e Spegni). In Deep Sleep Mode viene tutto congelato
e i dati in memoria vengono persi. Anche il clock a bassa frequenza e l'RTC viene
disattivato per il Watchdog Timer viene disattivato. La circuiteria BOR (Brown Out Reset)
viene anche disattivata a meno di non volerla attiva a scapito di maggiori consumi. In
applicazioni industriali e professionali la circuiteria BOR viene generalmente sempre
lasciata attiva. Dal momento che i dati in memoria vengono persi, vuol dire che al riavvio
del sistema necessaria una nuova inizializzazione delle periferiche. I microcontrollori
con opzione Deep Sleep Mode hanno spesso l'opzione di un Back up System per mezzo di
un'alimentazione ausiliaria, per esempio una batteria a bottone, che permette di mantenere
alimentate alcune celle RAM speciali in cui salvare dati importanti prima di entrare in Deep
Sleep Mode.
Le varie modalit di risparmio energetico sono mostrate in Tabella 3, dove possibile
vedere i dettagli dei bit di configurazione necessari per avviarle. In particolare si fa uso del
bit IDLEN presente nel registro OSCCON e dei bit SCS1:SCS0 presenti ancora nel
registro OSCCON.
Per poter entrare nella modalit Idle o Sleep, necessario eseguire l'istruzione SLEEP,
poi, a seconda del valore del bit IDLEN, il microcontrollore entra nello stato di Sleep o
Idle; i bit SCS1:SCS0 determinano poi la sorgente di clock. Si fa subito notare che in caso
si voglia utilizzare la modalit Idle o Sleep, dal momento che dopo l'esecuzione
dell'istruzione SLEEP non vengono pi eseguite ulteriori istruzioni, i bit di competenza
della modalit d'interesse devono essere preventivamente impostati come richiesto.

62

Nella famiglia PIC18 I dispositivi ottimizzati per avere consumi ridotti appartengono alla famiglia XLP (Extra Low Power). Il
nome discende probabilmente da ragioni di marketing per contrapporsi agli MSP430 i cui bassi consumi sono invece
pubblicizzati con la dicitura ULP (Ultra Low Power).

65

Mode

IDLEN

SCS1:SCS0

Stato CPU

Stato Periferiche

Clock disponibile e sorgente

PRI_RUN

N/A

00

ON

ON

Modalit primaria, tutte le sorgenti clock sono


disponibili.

SEC_RUN

N/A

01

ON

ON

Modalit secondaria, il clock dal Timer1


disponibile.

RC_RUN

N/A

1x

ON

ON

Oscillatore RC interno genera il clock.

PRI_IDL

00

OFF

ON

Modalit primaria, tutte le sorgenti clock sono


disponibili.

SEC_IDL

01

OFF

ON

Modalit secondaria, il clock dal Timer1


disponibile.

RC_IDL

1x

OFF

ON

Oscillatore RC interno genera il clock.

Sleep

N/A

OFF

OFF

Nessuno, tutte le sorgenti sono disabilitate

Tabella 3: Modalit risparmio energetici disponibili.

Vediamo qualche dettaglio in pi sulle diverse modalit:

Modalit PRI_RUN
Questa modalit rappresenta quella principale che viene impostata dopo il Reset.
Il clock disponibile quello selezionato per mezzo dei bit FOSC3:FOSC0 del
registro di configurazione CONFIG1H.

Modalit SEC_RUN
Questa modalit rappresenta quella compatibile con gli altri microcontrollori
PIC18 che possiedono la modalit di risparmio energetico. Questa viene impostata
cambiando il valore dei bit SCS1:SCS0 al valore 00. Il clock disponibile il Timer1.

Modalit RC_RUN
Questa modalit rappresenta quella a maggior risparmio energetico, dal momento
che fa uso della frequenza di clock pi bassa, generata per mezzo dell'oscillatore RC
interno. Sia la CPU che le periferiche prelevano il clock dal INTOSC multiplexer,
inoltre il clock primario disattivato, riducendo ulteriormente i consumi 63. Tale
modalit viene selezionata impostando i bit SCS1:SCS0 a 10 64.

Modalit PRI_IDL
Questa modalit la prima dello stato Idle ed quella che permette un minor
risparmio energetico tra le modalit Idle. Questo discende dal fatto che, pur
disattivando la CPU, le periferiche continuano ad essere alimentate ed hanno a
disposizione il clock principale. Questo se da un lato mantiene i consumi associati al
clock primario, permette di passare dallo stato Idle allo stato Run in maniera veloce,
visto che l'oscillatore primario non deve essere riattivato. Per entrare in questa
modalit bisogna settare il bit IDLEN, impostare i bit SCS1:SCS0 al valore 00 ed
eseguire l'istruzione di SLEEP. Al risveglio del microcontrollore la CPU utilizza
come clock l'oscillatore primario.

63

64

Modalit SEC_IDL

Dal momento che il clock primario viene disattivato, quando si ha la necessit di passare nuovamente al clock primario, si ha
il ritardo dovuto all'attivazione e la stabilizzazione dello stesso. Tale ritardo lo stesso che si ha con un power cycle.
In realt per impostare tale modalit sia il valore 10 che 11 verrebbero accettati, per, al fine di mantenere eventuali
compatibilit con versioni successive di PIC si raccomanda di utilizzare il valore 10 piuttosto che 11.

66

Questa modalit, diversamente dalla precedente, disattiva il clock primario, oltre


alla CPU. Il clock fornito alle periferiche ricavato dal Timer1. Per entrare in questa
modalit bisogna settare il bit IDLEN, impostare i bit SCS1:SCS0 al valore 01 ed
eseguire l'istruzione di SLEEP. Al risveglio del microcontrollore la CPU utilizza
come clock il Timer1.

Modalit RC_IDL
Questa modalit, quella a maggior risparmio energetico tra le modalit Idle,
poich possibile impostare il clock a pi bassa frequenza. La CPU, come per ogni
modalit Idle, viene disattivata. Il clock fornito alle periferiche quello generato
dall'INTOSC. Per entrare in questa modalit bisogna settare il bit IDLEN,
impostare i bit SCS1:SCS0 al valore 01 ed eseguire l'istruzione di SLEEP. Al
risveglio del microcontrollore la CPU utilizza come clock l'oscillatore INTOSC.

Modalit Sleep
Questa modalit rappresenta quella a maggior risparmio energetico poich tutte le
periferiche e la CPU vengono disattivate.
Se il Watchdog Timer attivo, il suo clock viene mantenuto attivo garantendo in
questo modo l'evento di time out del Watchdog. Tale evento, assieme all'evento di
Reset e Interrupt da periferiche, permette il risveglio del microcontrollore. Il clock
che vine utilizzato al risveglio dipende dal valore dei bit SCS1:SCS0 prima
dell'attivazione della modalit Sleep.

Circuiteria di Reset
Come si visto nella parte relativa all'organizzazione della memoria, il programma
eseguito a partire dall'indirizzo del Reset Vector. L'evento di Reset pu essere considerato
un tipo d'interruzione ad alta priorit per la CPU, in particolare la CPU non pu ignorare
una richiesta di Reset. Ogni microcontrollore, come anche microprocessore, possiede una
circuiteria interna di Reset. Nonostante l'operazione di Reset sia apparentemente
semplice, ovvero portare il Program Counter nella posizione del Reset Vector, la circuiteria
associata al Reset relativamente pi complicata di quanto si possa pensare. La circuiteria
di Reset del PIC18F4550 riportata in Figura 18.
La ragione della sua complessit legata al fatto che i PIC18 possiedono differenti
sorgenti che possono generare lo stato di Reset della CPU. Lo stato di Reset oltre che a
cambiare il valore del Program Counter determina anche il cambio del valore di alcuni
registri interni. In particolare il Datasheet riporta sempre le tabelle con i vari registri e il
valore assunto dai vari bit in caso di un Reset della CPU. Alcune volte i bit vengono posti
a 0 o 1, mentre altre volte assumono un valore casuale. Ritornando alla Figura 18
possibile osservare che il segnale di Reset rappresenta il risultato di operazioni logiche pi
o meno complesse, ma fondamentalmente presente una OR con ingresso i segnali
derivanti dalle varie possibilit di Reset e una AND che disabilita le funzioni di Reset nel
caso in cui non sia trascorso un tempo minimo dall'attivazione del microcontrollore.

67

Figura 18: Circuiteria di Reset del PIC18F4550.

I vari eventi che possono causare la generazione di un Reset di un PIC18 sono:

Power On Reset (POR)


Master Clear Reset (MCLR)
Programmable Brown-out Reset (BOR)
Watchdog Timer (WDT)
Stack pieno (Stack Overflow)
Stack vuoto
Istruzione RESET

Per sapere quale tipo di evento ha generato il Reset, possibile leggere il valore del
registro RCON. Vediamo ora in che modo la CPU pu essere resettata.

Power On Reset
Il Power On Reset (POR) rappresenta uno dei primi eventi che si viene a verificare una
volta che si alimenta il sistema. Una volta passato un tempo minimo dal momento in cui
viene rilevata la tensione minima operativa, viene attivato un Reset automatico, in maniera
da portare i registri interni al PIC in uno stato noto. Per sapere il valore iniziale dei registri
bene sempre far affidamento al Datasheet del PIC utilizzato.
68

La ragione per cui necessario attendere un tempo minimo legata al fatto che prima
di iniziare una qualunque operazione, necessario che l'oscillatore si sia stabilizzato. In
questo modo possibile garantire che le varie istruzioni siano eseguite correttamente e
soprattutto. Per permettere il corretto funzionamento della circuiteria interna POR
necessario collegare un resistore compreso tra 1Kohm e 10Kohm tra il pin MCLR e Vcc
(per un esempio pratico si faccia riferimento allo schema base di Figura 4).

Master Clear Reset


Il Master Clear Reset (MCLR) la tecnica principale per mezzo della quale possibile
resettare manualmente il PIC. Il segnale MCLR riportato in uscita al pin nominato
appunto MCLR. Tale pin, se collegato a massa resetta il PIC; per tale ragione viene
normalmente collegato ad un pulsante verso massa. Dal momento che tale pin viene
anche utilizzato per la programmazione on board, al fine di non danneggiare il
programmatore, in caso di pressione accidentale del tasto di Reset durante la fase di
programmazione, il tasto di Reset frequentemente collegato a massa per mezzo di un
resistore.
La circuiteria associata al pin MCLR possiede al suo interno un filtro anti-rimbalzo,
evitando che spike accidentali possano resettare la CPU. In schemi elettrici frequente
vedere la linea MCLR collegata a VCC per mezzo di un resistore di 10Kohm, tale
resistore viene in realt utilizzato dalla circuiteria POR (Power On Reset). Il resistore pu
avere un valore compreso tra 1Kohm e 10Kohm (per un esempio pratico si faccia
riferimento allo schema base di Figura 4).
Nel caso in cui si dovesse essere a corto di ingressi, il pin MCLR potrebbe anche essere
impostato come input, perdendo per la funzione di resettare la CPU dall'esterno.
Personalmente sconsiglio di utilizzare il pin MCLR per altre funzioni, a meno di forti
ragioni pratiche in cui un tasto di Reset non servirebbe a molto di pi di un power cycle.

Programmable Brown-out Reset (BOR)


Tale funzione particolarmente utile poich permette di resettare il PIC qualora la
tensione vada al disotto di una soglia stabilit. La sua utilit la si apprezza specialmente in
sistemi in cui l'affidabilit delle misure viene ad essere ridotta in caso in cui la tensione
non sufficiente. Naturalmente tali situazioni possono essere gestite anche in altro modo.
La circuiteria BOR pu funzionare in varie modalit selezionabili per mezzo dei bit
BOREN1:BOREN0 del registro CONFIG2L. Normalmente la modalit Hardware
selezionata di Default. Oltre a tali bit di controllo sono presenti i bit BORV1:BORV0
sempre nel registro CONFIG2L, per mezzo dei quali possibile selezionare la soglia alla
quale deve avvenire il Reset. La tensione di riferimento utilizzata per il confronto quella
generata dal generatore band gap da 1.2V. In applicazioni a batteria o sistemi professionali
il modulo BOR, spesso non utilizzato in ambito sperimentale, rappresenta una funzione
di sicurezza di cui raramente si fa a meno. I microcontrollori MSP430, dal momento che
sono pensati per applicazioni a batteria, non hanno l'opzione per disabilitare il modulo, il
cui monitoraggio sempre garantito anche il LPM4.5, paragonabile al Deep Sleep Mode
della Microchip.

69

Watchdog Timer (WDT)


Il Watchdog Timer, letteralmente cane da guardia, rappresenta un timer utilizzato per
resettare la CPU qualora qualcosa dovesse andare male. Per il suo principio di
funzionamento per potrebbe non rilevare tutti i casi di anomalia. Lo schema a blocchi
della sua circuiteria riportato in Figura 19.

Figura 19: Schema a blocchi del modulo Watchdog. .

Il suo funzionamento molto semplice, fondamentalmente il Watchdog un timer che


viene sempre incrementato, qualora dovesse arrivare a fine conteggio resetta la CPU. Si
capisce che, se il Watchdog viene abilitato, al fine di evitare il Reset della CPU necessario
sempre azzerare il conteggio. L'idea dietro il suo funzionamento legata al fatto che devo
azzerare periodicamente il conteggio, dunque se il programma dovesse entrare in un
loop vizioso o entrare in stallo, il Watchdog continuerebbe il suo conteggio fino a
resettare la CPU. La base tempi del Watchdog fissa ed rappresentata dall'oscillatore
INTRC. Tale oscillatore indipendente dall'oscillatore principale, per cui il suo
funzionamento garantito anche se il microcontrollore in stato di Sleep o Idle. Come
visibile in Figura 19 il clock rappresentato da INTRC dopo essere andato al Watchdog,
passa per il Postscaler (un divisore), ovvero un secondo contatore per mezzo del quale
ritardare ulteriormente il conteggio principale del Watchdog. Sempre da Figura 19 inoltre
possibile vedere che il Watchdog e il Postscaler sono azzerati nel caso in cui venga eseguita
l'istruzione CLRWDT, un Reset generale, o un cambio del dei bit IRCF (associati al
cambio della frequenza operativa del microcontrollore). Il Watchdog viene attivato o
disattivato per mezzo delle Configuration Words, per cui viene impostato nella fase della
programmazione e non pu essere cambiato successivamente dall'applicazione. Questa
funzione rappresenta un metodo per garantire che il Watchdog non possa essere disabilitato
per errore dall'applicazione. Ciononostante alcuni modelli di PIC18 hanno anche
l'opzione di poter abilitare e disabilitare il Watchdog via software per mezzo
dell'applicazione. Gli MSP340 hanno anche il Watchdog che di Default, all'avvio
dell'applicazione, risulta attivo. Negli MSP430 per poter disattivare il Watchdog, al fine di
garantire un livello di sicurezza contro cambi erronei del suo stato, necessario prima
scrivere una password in un registro speciale. Questo approccio simile a quanto gi
mostrato per il processo di scrittura in memoria EEPROM.

70

Stack Reset
Il Reset associato allo Stack pu risultare particolarmente utile per ripristinare il
corretto funzionamento del sistema. Si ricorda infatti che lo Stack rappresentato da
quell'insieme di registri il cui compito di memorizzare l'indirizzo di ritorno da una
chiamata ad una funzione, sia essa associata ad un Interrupt o semplicemente una
funzione standard. Si capisce che se lo Stack dovesse avere un evento di overflow, ovvero di
traboccamento, vorrebbe dire che delle informazioni di ritorno sono perse, portando
prima o poi il sistema in uno stato non voluto. Lo stesso potrebbe accadere in caso di
underflow ovvero si prelevino informazioni dallo Stack anche se non presenti. Il Reset
associato allo Stack viene abilitato per mezzo del bit STVREN del registro CONFIG4L.
Se abilitato (posto ad 1), un eventuale overflow o underflow dello Stack, genera un Reset del
sistema.
Nel caso in cui il Reset dello Stack non dovesse essere abilitato, i bit STKFUL e
STKUNF vengono settati di conseguenza. Tali bit sono presenti nel registro STKPTR e
vengono resettati o via software o successivamente ad un POR Reset. In sistemi in cui sia
richiesta una certa robustezza, il Reset da parte dello Stack permette di ripristinare il
normale funzionamento del sistema, ciononostante, eventuali Reset dovuti a problemi
dello Stack, possono essere indice di problemi software. In applicazioni in cui il software
si trova in esecuzione 24 ore su 24, 365 giorni all'anno, pur non essendo presenti
problemi software si possono comunque verificare problemi nell'esecuzione del
programma a causa di disturbi esterni. In particolare radiazioni elettromagnetiche
potrebbero cambiare i contenuti della memoria RAM alternando delle impostazioni o la
normale esecuzione del programma. Con il tempo anche la memoria Flash pu essere
soggetta ad alterazioni dei contenuti, specialmente in condizioni di alta temperatura.
Infatti non insolito che bit di valore 1 possano invertirsi in 065.
Errori software che possono portare a un Reset sono pi frequenti se si scrive il
programma in Assembly. Dal momento per che tale tipologia di Reset deve essere
abilitata, qualora si tenga lo Stack Reset disabilitato e non si ha nessun Reset che inizializza
nuovamente il sistema, quello che si osserva generalmente un comportamento strano
dello stesso.
Poich lo Stack ha un numero limitato di celle, bisogna fare attenzione a non riempirlo
eseguendo un numero elevato di chiamate a funzioni annidate. Bisogna inoltre sempre
tenere a mente che oltre alle funzioni annidate, qualora si stiano usando gli Interrupt,
bisogna preventivare sempre altri due livelli per le interruzioni ad alta e bassa priorit.
Alcuni microcontrollori come gli MSP430 non hanno un numero prefissato di celle RAM
per lo Stack. Infatti lo Stack Pointer indirizza memoria RAM standard, la quale viene
assegnata nel file di linker.

Istruzione RESET
Tutti i vari eventi finora descritti sono pi o meno legati ad eventi hardware.
L'ultimo caso qui descritto in realt di tipo software. Tra le varie istruzioni che il PIC18
pu eseguire vi quella di RESET ovvero l'istruzione che permette di resettare la CPU per
mezzo del codice stesso. Questa istruzione, una volta eseguita ha gli stessi effetti di un
65

Il caso inverso, ovvero l'inversione da 0 a 1 non in generale possibile o meglio improbabile. Infatti ogni bit della memoria
Flash un condensatore, ed pi probabile che sia soggetto a scarica che non a carica. In particolare ad alte temperature la
corrente di dispersione (Leakage Current) tende ad aumentare ed agevolare la scarica delle celle di memoria Flash.

71

Reset Hardware. Tale istruzione risulta molto utile per permettere il Reset del sistema per
mezzo di un altro sistema master, si pensi ad esempio ad una rete composta da vari nodi.
Un altro utilizzo particolarmente utile potrebbe essere associato ad un sistema
diagnostico, si pensi ad esempio al caso in cui il software si sia reso conto che qualcosa
accaduto e si siano persi dei dati, l'istruzione Reset potrebbe tornare utile al fine di forzare
una inizializzazione di sistema.
La possibilit che un sistema si possa resettare, sebbene possa sembrare un mezzo
preventivo per garantire una buona affidabilit del sistema, non deve causare Reset di
sistema ogni 5 minuti. I casi estremi si potrebbero verificare una volta l'anno in sistemi
che sono sempre attivi. Naturalmente tutto dipende dal software e la qualit con cui
scritto, quindi una buona responsabilit ricade sul programmatore. Sebbene il software
possa essere soggetto a bug, non bisogna scordarsi che anche un microcontrollore nella
sua architettura soggetto a bug per cui pu essere fonte di anomalie anche qualora il
software sia perfetto. Per tale ragione, al fine di scrivere un software robusto sempre
bene verificare se i bug del microcontrollore utilizzato non possano influenzare il corretto
funzionamento del sistema.
Queste informazioni possibile trovarle nel documento Errata, scaricabile dalla stessa
pagina in cui possibile prelevare il Datasheet. Normalmente, quando possibile,
nell'Errata viene anche fornito il cosiddetto Workaround, ovvero una soluzione e/o
comportamento per evitare il problema derivante dal bug.
Non inusuale che uno stesso microcontrollore sia presente sul mercato con diverse
versioni, le quali sono soggette a bug differenti. Alcune volte pu capitare che lo scrivere
il software al fine di implementare il Workaround per un bug possa causare problemi
qualora lo stesso software sia utilizzato su una versione del microcontrollore senza il bug.

Le porte d'ingresso uscita


Tutti i microcontrollori, per essere di qualche utilit, oltre ad eseguire delle istruzioni,
devono poter essere in grado di interagire con il mondo esterno. In particolare devono
poter leggere delle informazioni e/o scrivere dei risultati. Un microcontrollore che non ha
nessuna possibilit di leggere delle informazioni esterne e non visualizza o pone nessun
risultato come uscita, non ha nessuna ragione di essere programmato. Proprio per tale
ragione, al fine di avere un'utilit, i PIC18 possiedono, oltre ai pin di alimentazione, molti
altri pin che possono essere impostati per poter leggere o scrivere delle informazioni. In
questo modo i PIC hanno una ragione per eseguire le istruzioni interne. In particolare
ogni pin pu essere in generale un pin digitale di I/O (Input/Output). Dal momento che
il PIC possiede molte periferiche interne i pin oltre a poter essere utilizzati come I/O
standard, sono spesso multiplexati con funzioni di altre periferiche, ovvero hanno pi
funzioni. I vari pin del PIC sono raggruppati in piccoli insiemi al massimo di 8 pin; ogni
gruppo viene nominato PORTx, dove x prende il valore di una lettera A, B, C... la lettera
massima dipende dal numero di gruppi presenti, ovvero dal numero di pin disponibili sul
PIC. Non sempre il gruppo formato da 8 pin, alcune volte la porta incompleta, ovvero
formata da meno di 8 pin. Il pin di ogni gruppo prende il nome della porta, seguito da un
numero crescente compreso tra 0 e 7. In particolare i pin della PORTB saranno
RB0..RB7. I raggruppamenti del PIC18F4550 sono visibili alla destra di Figura 8. In
Figura 20 riportato lo schema a blocchi di un pin generico.
72

Figura 20: Schema a blocchi delle porte di uscita.

Ad ogni gruppo di pin sono associati 3 registri, rispettivamente chiamati:

TRIS
Il registro TRIS ha il compito di impostare la direzione (ingresso od uscita) del
pin. Tale registro composto da un byte ovvero da 8 bit; ad ogni bit corrisponde
il pin della porta. Per esempio il bit 0 di TRISA imposta il pin RA0. Se il bit vale
0 il pin impostato come output, mentre se vale 1, il pin viene impostato come
ingresso. Una semplice regola per ricordare tale impostazione quella di pensare
1 come I (Input) e 0 come O (Output).

PORT
Il registro PORT viene utilizzato per scrivere un valore in uscita e/o leggere il
valore del pin.

LAT
Il registro LAT viene utilizzato per scrivere un valore in uscita e/o leggere il
valore del Latch66 (non il pin).

Lo schema a blocchi di ogni pin, riportato in Figura 20. In tale schema a blocchi non
visualizzato l'hardware specifico per ogni pin, necessario per gestire le opzioni di
ulteriori periferiche. In particolare visibile la presenza di due Latch, uno associato al
registro TRIS, mentre il secondo associato al registro PORT. Quando l'uscita Q del
66

Il Latch rappresenta la cella elementare di memoria che permette di memorizzare un bit. Un registro composto da un
insieme di Latch. Dunque una porta ha fino a 8 Latch.

73

Latch TRIS vale 1, il buffer che porta l'uscita Q del Latch PORT all'uscita del pin, viene
disabilitato, ovvero posto ad alta impedenza. Questo permette di poter leggere il pin senza
essere influenzati dal valore del Latch PORT. Il valore del registro TRIS normalmente
viene impostato all'inizio del programma e non viene pi cambiato, ma nulla vieta di
sviluppare applicazioni in cui sia necessario cambiare il valore del registro TRIS durante
l'esecuzione del programma. Qualora l'uscita Q del Latch TRIS valga 0, viene abilitato il
buffer di uscita, che permette di portare il valore dell'uscita Q del registro PORT in uscita
al pin.
Vediamo meglio cosa sono i registri PORT e LAT e quali sono le loro differenze. In
particolare, per chi ha gi studiato i PIC16, quale per esempio il famoso PIC16F84 e
PIC16F877, il registro LAT una cosa nuova, infatti i PIC citati possiedono solo il
registro TRIS e PORT. Effettivamente possibile scrivere 1 o 0 sul pin di uscita facendo
solo uso del registro PORT; inoltre anche possibile leggere il valore del pin di uscita e
sapere se vale 0 o 1. Da quanto detto, non si capisce bene la ragione per cui nei PIC18 sia
stato introdotto il registro LAT, per mezzo del quale possibile a sua volta scrivere il pin
di uscita o leggere il valore del Latch di uscita (non il valore del pin). Il registro LAT
funzionalmente identico al registro PORT in scrittura, infatti in Figura 20 possibile
vedere che il data Latch scrive il valore presente sul data bus in uscita, indistintamente se
si effettua una scrittura sul registro PORT o LAT. La differenza dei due registri nella
fase di lettura, infatti quando si legge il registro PORT, il valore che viene posto sul data
bus proprio quello del pin, mentre quando si effettua la lettura del registro LAT, il dato
posto sul data bus il valore d'uscita del Latch.
Bene, allora qual' la differenza?! Quando pongo il pin ad 1, l'uscita del Latch vale 1 e il pin in
uscita vale 1, dunque leggere il registro PORT o LAT uguale!
La differenza proprio qui, il valore del registro PORT e LAT non sono sempre
uguali! Il fatto va ricercato esternamente al PIC. Infatti la periferica o carico al quale il pin
collegato, potrebbe trovarsi ad una certa distanza dal PIC stesso, tale distanza potrebbe
essere di soli 10cm, ma le piste potrebbero avere elementi parassiti (R, C ed L) tali per cui
sia necessario del tempo prima che il valore cambi di stato da 0 a 1 o viceversa. Dunque il
valore del pin potrebbe valere 0, quando invece il valore in uscita al Latch 1. Dunque
leggere il pin tramite il registro PORT o leggere il Latch per mezzo del registro LAT pu
avere le sue differenze67. Questo causa dei problemi quando vengono eseguite istruzioni
per cui, prima di cambiare il valore del bit richiesto sapere il valore del pin stesso.
Dal momento che in scrittura i due registri LAT e PORT sono uguali,
mentre in lettura LAT pu evitare dei problemi, quando si utilizza un pin come uscita
consigliabile utilizzare sempre LAT sia in lettura che in scrittura. Qualora il pin sia
impostato come ingresso il pin deve essere letto per mezzo del registro PORT.
Vediamo ora in breve le peculiarit di ogni porta, ed in particolare le sue funzioni.
Quanto di seguito descritto fa rifermento al PIC18F4550, il numero di porte e funzioni
pu variare in base al PIC utilizzato. Per maggior dettagli fare sempre riferimento al
Datasheet del PIC utilizzato.
67

Il problema ora spiegato viene anche a dipendere dalla velocit (frequenza) a cui vengono letti e scritti i registri.

74

PORTA
La PORTA possiede al massimo 7 pin, ovvero fino ad RA6, in particolare RA6
disponibile solo se non si fa uso del quarzo esterno. Per poter utilizzare il pin RA6
necessario scrivere la direttiva:
#pragma config OSC = IRCIO67

Dal momento che si fa uso di una direttiva, tale impostazione non pu essere pi
cambiata, se non riprogrammando il PIC 68. I pin della PORTA sono inoltre utilizzati
come ingressi analogici. All'avvio del PIC tale funzione prevale sul valore impostato
sul registro TRISA. Per poter utilizzare i pin della PORTA tutti come I/O
necessario scrivere 0x0F (ovvero 15) nel registro ADCON169. Maggiori dettagli su
tale registro sono riportati nel Capitolo dedicato agli ADC. Ogni volta che si avvia il
PIC bene che tra le prime istruzioni sia presente l'inizializzazione delle porte, in
modo tale da evitare il danneggiamento dei sistemi collegati al PIC, nonch il PIC
stesso. L'inizializzazione della PORTA, facendo uso di LATA invece di PORTA,
la seguente:
LATA = 0;
ADCON1= 16;

// Pongo a zero le uscite


// Imposto come I/O i pin della PORTA

TRISA =

// RA0..RA3 sono input, il resto output

0b00001111;

Il valore di ADCON1 e TRISA potrebbero essere differenti a seconda delle


esigenze. In particolare il formato numerico che pu essere utilizzato pu anche
variare (binario, esadecimale, decimale). Come detto il nome dei pin RA0..RA6, se
per si fa uso del registro LAT, i nomi utilizzati sono LATA0...LATA6.

PORTB
La PORTB, diversamente dalla PORTA possiede 8 pin, dunque possiede i pin
RB0..RB7 e LATB0...LATB7 rispettivamente. Come le altre porte, ogni pin
bidirezionale ma pu essere anche utilizzato per altre funzioni. In particolare,
all'avvio del PIC, i pin RB0-RB4 sono impostati come ingressi analogici. Per poterli
utilizzare come ingressi digitali necessario utilizzare la direttiva:
#pragma config PBADEN = OFF

Dal momento che si fa uso di una direttiva, tale impostazione non pu essere pi
cambiata, se non riprogrammando il PIC. Qualora si debbano utilizzare i pin come
ingressi analogici necessario porre ad ON l'opzione PBADEN. Una caratteristica
importante della PORTB che spesso viene trascurata, la presenza di resistori di
pull-up che possono essere abilitati per mezzo del bit RBPU del registro
INTCON2. Tale funzione particolarmente utile nel caso si utilizzino dei pulsanti
esterni, infatti permette di evitare l'utilizzo dei resistori di pull-up altrimenti necessari
68

69

In realt essendo la memoria Flash di tipo Self Programming, essendo presente una Charge Pump integrata, vi la possibilit di
riprogrammare le Configuration Words. Il bootloader sfrutta per esempio questa opzione per riprogrammare i bit di
configurazione.
Si faccia riferimento al Capitolo sul modulo ADC, per maggior dettagli su come impostare tale registro.

75

per fissare il livello logico del pulsante aperto 70. Un'altra caratteristica della PORTB
quella delle interruzioni al variare dello stato logico, cio viene generata
un'interruzione se uno qualunque dei pin RB4-RB7 cambia di valore, ovvero da 0 ad
1 o viceversa. Si vedranno esempi su quanto descritto, nei prossimi Capitoli. La
PORTB pu essere inizializzata per mezzo delle seguenti istruzioni:
LATB = 0;
ADCON1= xx;

// Pongo a zero le uscite


// xx da impostare se PBADEN = ON

TRISB =

// RB0..RB3 sono input, il resto output

0b00001111;

Il valore di ADCON1 e TRISB potrebbe essere differente a seconda delle


esigenze. In particolare il formato numerico che pu essere utilizzato pu anche
variare (binario, esadecimale, decimale).

PORTC
La PORTC possiede 7 pin ovvero RC0-RC6 e LATC0...LATC6 rispettivamente.
Tra le periferiche principali che utilizzano tale porta si ricorda la EUSART e il
modulo USB, il quale fa per utilizzo anche della PORTA. Di default, diversamente
dalle PORTA e PORTB i pin sono digitali, dunque la sua inizializzazione pi
semplice:
LATC = 0;

// Pongo a zero le uscite

TRISC =

// RC0..RC3 sono input, il resto output

0b00001111;

Il valore di TRISC potrebbe essere differente a seconda delle esigenze. In


particolare il formato numerico che pu essere utilizzato pu anche variare (binario,
esadecimale, decimale). Una periferica di particolare importanza che fa uso dei pin
della PORTC il modulo PWM, frequentemente utilizzato in applicazioni robotiche
per il controllo della velocit dei motori. Altra applicazione potrebbe essere il
controllo dell'intensit luminosa di una lampada. Maggiori dettagli sono forniti nel
Capitolo dedicato al modulo CCP (Capture Compare PWM).

PORTD
La PORTD possiede 8 pin ovvero RD0-RD7 e LATD0...LATD7
rispettivamente. Tra le periferiche principali presenti su tale porta si ricorda la
presenza dell'Enhanced PWM, ottimizzato per il controllo dei motori, grazie alla
presenza di uscite complementari. La sua inizializzazione tipo quella della PORTC:
LATD = 0;

// Pongo a zero le uscite

TRISD =

//RD0..RD3 sono input, il resto output

0b00001111;

Il valore di TRISD potrebbe essere differente a seconda delle esigenze.

70

Se si leggesse il valore di un pulsante aperto, equivarrebbe a leggere un valore fluttuante che potrebbe essere sia 0 che 1 e
variare col tempo. Il resistore di pull-up fissa ad 1 il valore del pulsante aperto. Come alternativa anche possibile utilizzare
resistori di pull-down che fissano il valore del pulsante aperto a 0, ma visto che presente il resistore di pull-up interno al PIC,
bene far uso di quello a meno di non avere altre otivazioni.

76

PORTE
La PORTE
possiede 4 pin ovvero RE0-RE3 e
LATE0...LATE3
rispettivamente. Una loro peculiarit sta nel fatto che gli ingressi hanno un buffer a
Trigger di Schmitt, permettendo agli ingressi di avere una maggiore immunit a
disturbi esterni. Come per la PORTA la PORTE possiede i pin multiplexati con il
convertitore analogico digitale. In particolare, dal momento che all'avvio del PIC
PORTE impostata con ingressi analogici, necessario impostare propriamente il
registro ADCON1:
LATE = 0;
ADCON1= xx;

// Pongo a zero le uscite


// xx da impostare se PBADEN = ON

TRISE =

// RE0..RE3 sono input

0b00001111;

Il valore di ADCON1 e TRISB potrebbe essere differente a seconda delle


esigenze.
Dopo quanto esposto bene mettere in evidenza che alcune periferiche, quando
abilitate sovrascrivono il registro TRIS dunque indifferente se il pin stato impostato
come ingresso o uscita. Altre volte necessario che il pin sia propriamente impostato nel
registro TRIS per permettere il corretto funzionamento della periferica. Quanto detto si
applica come regola generale a tutte le porte. Per maggior dettagli sempre bene far
riferimento al Datasheet del PIC utilizzato, in particolare si raccomanda la lettura del
paragrafo associato alle porte e alla periferica d'interesse.
Sebbene si sia parlato delle porte I/O nel capitolo dell'architettura bene ricordare che
non appartengono in realt all'architettura del microcontrollore, ma rappresentano un
modulo hardware interno come potrebbe essere il modulo Timer. Ciononostante per la
loro importanza ho preferito descriverle in questo Capitolo. Alcune impostazioni trattate,
dal momento che non sono state appoggiate da esempi, sono probabilmente state
premature, ma la loro comprensione sar pi facile quando saranno introdotti gli esempi
pratici.

Riassunto
In questo Capitolo stata trattata con qualche dettaglio l'architettura dei PIC18, in
particolare focalizzando l'attenzione sul PIC18F4550. Partendo dall'architettura base
Harvard e di von Neumann, si sono descritte le varie parti principali del microcontrollore
descrivendo vantaggi e svantaggi derivanti dall'utilizzo di un'architettura rispetto ad
un'altra. Al fine di agevolare la comprensione e confronto, si sono introdotte anche le
caratteristiche base degli MSP430. Ogni parte trattata ha mostrato in particolare come
determinate caratteristiche architetturali discendano dalla volont di poter soddisfare
esigenze pratiche di prodotti reali. Per tale ragione aspetti apparentemente semplici come
il clock sono spesso piuttosto complicati visto che la flessibilit dei moduli fornisce molte
opzioni al fine di adeguarsi a molteplici applicazioni.

77

Punti chiave ed approfondimenti


SFR: Special Function Register, ovvero registro con funzioni speciali. Sono registri speciali il
cui compito contenere informazioni speciali come per esempio il Program Counter, Stack
Pointer o registri di configurazione. Nel caso dei registri di configurazione ogni bit del
registro ha una funzione particolare che viene a dipendere dal registro stesso. Ogni
modulo, ovvero periferica internal ai PIC18 possiedono un certo numero di SFR dedicati
alla configurazione della periferica stessa.
PC: Il Program Counter il registro speciale che tiene il segno dell'istruzione che deve
essere eseguita al prossimo ciclo istruzione. In particolare permette di indirizzare la
memoria programma ovvero la memoria Flash dei PIC18.
Statck Pointer: Lo Stack Pointer un registro speciale simile al PC, ma il cui compito
quello di tenere memoria dell'indirizzo (puntatore) di memoria all'interno dello Stack, dal
quale prelevare l'indirizzo di ritorno dopo una chiamata di una funzione, ovvero dopo
l'esecuzione dell'istruzione Assembly RETURN e RETFIE.
Accumulatore: L'accumulatore, nominato W, rappresenta un registro speciale ad 8 bit
utilizzato nell'architettura dei PIC18 per il movimento dei dati tra registri e anche per
permettere le operazioni aritmetiche e binarie tra i registri, facendo uso dell'ALU.
L'accumulatore presente in tutte le architetture di microcontrollori e microprocessori. In
architetture pi complesse non insolito trovarne anche pi di uno.
ALU: L'ALU o Arithmetic Logic Unit, rappresenta l'Unita Logica Aritmetica, che permette
di svolgere le operazioni aritmetiche e binarie tra registri. Nei PIC18 un registro dell'ALU
sempre l'accumulatore ovvero il registro W. In base al risultato dell'operazione il registro
Status Register ha i relativi bit cambiati di conseguenza.
Status Register: Lo Status Register o Registro di Stato, rappresenta un registro speciale i
cui bit riflettono il risultato dell'operazione eseguita dall'ALU. In particolare permette di
rilevare il segno dell'operazione (bit N), un Overflow (bit OV), risultato nullo (bit Z), Carry
(bit DC e bit C a seconda del Carry avvenuto nel quarto od ottavo bit).

Domande ed Esercizi
1. Qual' la differenza tra l'architettura di Von Neumann e Harvard?
2. Qual' la differenza tra il registro LAT e PORT e quando bene usare l'uno o l'altro?
3. Descrivere le funzioni del registro TRIS associato ad ogni porta.
4. Per quale ragione diminuire la frequenza di clock permette di ridurre i consumi della CPU?
5. Qual' la funzione del PLL?
6. Qual' la frequenza di clock richiesta per supportare una comunicazione USB Full Speed?
7. Qual' l'utilizzo tipico della memoria Flash?
8. Qual' l'utilizzo tipico della memoria RAM?
9. Che cos' la FRAM?
10. Qual' il valore di default della memoria Flash una volta cancellata?
11. Per quale ragione la memoria Flash potrebbe perdere memoria del valore memorizzato al
78

suo interno qualora sia sottoposta ad alte temperature?


12. Che cos' il registro Stack Pointer?
13. Che cos' il registro Program Counter?
14. Qual' il limite principale derivante dall'avere uno Stack Memory a dimensione fissa?
15. Come si dimensiona il condensatore richiesto dal Quarzo dell'oscillatore primario?
16. Quali sono i pro e contro nell'usare il generatore del clock interno piuttosto che un quarzo
esterno?
17. Descrivere la funzione del contatore Watchdog.
18. Descrivere le varie fonti che possono creare un Reset.
19. Che cos' la modalit Deep Sleep Mode?
20. Quali sono i tempi tipici di risveglio della CPU da una modalit Idle e Sleep?
21. Cosa determina il differente tempo di risveglio del microcontrollore al variare della
modalit di risparmio energetico?
22. Per quale ragione necessario scrivere 55h e AAh nel registro EECON2 prima di scrivere
in memoria EEPROM?
23. Quali funzioni ha il registro W (Accumulatore)?
24. Che cos' una Pipeline?
25. Descrivere le ragioni per cui avere pi accumulatori pu essere utile in sistemi complessi
con Pipeline con pi di due livelli.
26. Descrivere le ragioni per cui avere pi accumulatori pu essere utile in sistemi complessi
anche se non sia presente alcuna Pipeline.
27. Descrivere le fasi di Fetch, Decode ed Execute.
28. Per quale ragione dal clock principale si derivano altri 4 clock secondari?
29. Che cos' il ciclo istruzione?
30. Qual' la capacit di calcolo massima del PIC18F4550 espressa in MIPS?

79

Capitolo IV

IV

Salutiamo il mondo per iniziare


Non credo che ci sia nessun corso di un linguaggio di programmazione che non inizi
salutando il mondo. Sar una forma di educazione o per semplicit che anche noi
inizieremo con un bel saluto. In questo Capitolo anche se non si capir completamente
l'esempio, si illustrano i vari passi che portano alla progettazione e sviluppo di
un'applicazione completa. Inizieremo illustrando come creare un nuovo progetto per poi
compilarlo e caricarlo nella scheda di sviluppo, ovvero all'interno del microcontrollore.

Schema elettrico
Per poter seguire l'esempio presentato in questo capitolo necessario realizzare su
Breadboard lo schema elettrico di Figura 21 o far uso delle schede di sviluppo impostate come
descritto nel paragrafo successivo.

Figura 21: Schema elettrico di esempio del Capitolo IV.

80

Impostare le schede di sviluppo


Al fine di poter seguire gli esempi mostrati in questo Capitolo necessario impostare la
scheda di sviluppo utilizzata come descritto nei paragrafi seguenti.

Freedom II
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom II deve avere i Jumper posizionati nel seguente modo:

Figura 22: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

Freedom Light
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom Light deve avere i Jumper posizionati nel seguente modo:

Figura 23: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

Impostazioni software
Gli esempi mostrati non richiedono alcuna impostazione Software particolare oltre alla
semplice creazione di un progetto.
Gli esempi mostrati sono spiegati nel dettaglio, mostrando il codice Assembly,
per cui, a seconda delle impostazioni delle ottimizzazioni e della versione del
compilatore utilizzato, possono essere presenti delle differenze tra il codice
mostrato e quello che otterrete in fase di compilazione.
82

Il nostro primo progetto


Per poter iniziare a sviluppare una nuova applicazione, ovvero Software da poter
caricare all'interno del nostro microcontrollore, necessario creare un progetto. Un
progetto non altro che una raccolta di file. In particolare sempre presente il
programma sorgente del nostro programma ed eventuali file di libreria. L'ambiente di
lavoro aggiunge poi altri file per mantenere anche altre informazioni.
...allora si inizia.
Come prima cosa si deve avviare MPLAB X, ovvero l'ambiente di sviluppo dal quale
compieremo tutte le operazioni di sviluppo, programmazione, compilazione e
simulazione. La schermata di avvio di MPLAB X , al suo primo avvio, simile a Figura 24.
E' possibile subito notare che l'ambiente di sviluppo suddiviso in varie parti, quadranti,
la cui funzione verr mostrata a breve.
Per creare un nuovo progetto bisogna andare sul men File e selezionare
New Project...

Si aprir una finestra che guider la creazione di un nuovo progetto, come riportato in
Figura 25. A questo punto, nel riquadro Categories, bisogna selezionare la voce Microchip
Embedded mentre nel riquadro Projects, bisogna selezionare Standalone Project. Cliccando sul
pulsante Next si avr la nuova finestra di Figura 26, dove bisogner selezionare il PIC
della famiglia PIC18 che si vuole utilizzare. Negli esempi che seguiranno si far uso del
PIC18F4550 che andr dunque montato sul sistema embedded Freedom II 71 o scheda di
sviluppo utilizzata. In generale anche altri PIC potranno essere utilizzati senza alcun
problema, ma si dovr avere l'accortezza di cambiare l'header file, di cui si parler a breve,
associato al PIC utilizzato. Dopo aver selezionato il PIC si pu premere nuovamente il
pulsante Next. Nella schermata successiva riportata in Figura 27 fornita la possibilit di
selezionare lo strumento di programmazione e Debug, che nel seguente corso il
PICKIT 3. Si noti che al fianco di ogni strumento sono presenti dei cerchi colorati. Verde
significa che lo strumento supportato, giallo che lo strumento parzialmente supportato
e rosso ad indicare che lo strumento non supportato.
In particolare si noti che il programmatore PICKIT 2 parzialmente supportato e
probabilmente rimarr tale, mentre il programmatore Debugger ICD 2 non supportato.
Tra gli strumenti selezionabili vi anche il simulatore, da selezionare qualora non si abbia
nessun Hardware a disposizione e si voglia testare il software per mezzo del PC. La
selezione fatta in questo passo, come anche le precedenti, possono essere cambiate in un
secondo tempo. Cosa interessante da notare che lo strumento selezionato e relativo
numero seriale, vengono memorizzati nel progetto. Qualora cambiassimo il
programmatore in un secondo momento, per esempio si dovesse usare un PICKIT 2
invece del PICKIT 3, questo verrebbe segnalato e prima di poter programmare
nuovamente il microcontrollore richiesta la conferma per poter utilizzare il nuovo
programmatore.

71

Un qualunque altro sistema di sviluppo o PIC della famiglia PIC18 in generale utilizzabile.

83

Figura 24: Primo avvio di MPLAB X.

Figura 25: Primo passo per la creazione del progetto.

84

Figura 26: Finestra di dialogo per la selezione del PIC.

Figura 27: Selezione dello strumento di programmazione.

Premendo il tasto Next viene data la possibilit di selezionare il compilatore ovvero


Compiler Toolchains come riportato in Figura 28. La finestra mostra in particolare i vari
compilatori installati e relativa versione, per cui possibile selezionare non solo il
compilatore ma anche la versione dello stesso. Questa caratteristica permette di creare un
progetto per mezzo di una versione del compilatore e mantenere la stessa anche se sul PC
sono state installate nuove versioni 72. Compilatore e versione possono essere cambiati in
un secondo momento dalle propriet del progetto. Nel caso specifico selezionare il
compilatore XC8 nella sua ultima versione installata.
72

Il precedente ambiente di sviluppo MPLAB IDE non permetteva di selezionare la versione del compilatore, e ad ogni
installazione di un nuovo compilatore si era costretti a ricompilare il progetto con la nuova versione. Sebbene molti siano
abituati al concetto dell'avere sempre l'ultima versione del software, questo , in applicazioni professionali sensibili, come per
esempio in ambito automobilistico e industriale, non accettato. Infatti aggiornare un compilatore richiede il dover testare
l'intera applicazione dall'inizio.

85

Figura 28: Selezione del compilatore e relativa versione.

Premendo il tasto Next possibile procedere con l'ultima impostazione del progetto,
ovvero il nome e percorso in cui salvarlo, come mostrato in Figura 29.
In particolare selezionare il percorso da impostare nella Project Location e fornire un nome
nel Project Name73. Il campo Project Folder verr automaticamente popolato con la cartella
del progetto, che prender il nome fornito e un .X alla fine.

Figura 29: Selezione del percorso e nome del progetto.

Il check box Set as main project permette di selezionare il progetto appena creato come
principale. Infatti l'ambiente di sviluppo permette di avere pi progetti aperti ma solo uno
attivo.

Premendo il tasto Finish, il nostro progetto completo di tutte le informazioni e


l'ambiente di sviluppo mostrer il progetto come in Figura 30.
73

Dal momento che il nuovo ambiente di sviluppo supporta anche l'ambiente di sviluppo Linux e MAC bene creare nomi
non separati da spazi ma da underscore. Sebbene sia possibile usare gli spazi non raccomandato farlo.

86

Figura 30: Ambiente di sviluppo al termine della creazione del progetto.

Si osservi come la sinistra dell'ambiente di lavoro si sia popolata con molte


informazioni. In particolare in alto a sinistra presente la struttura, sebbene ancora vuota
del nostro progetto, come in Figura 31.

Figura 31: Dettagli sulla struttura del progetto.

La struttura del progetto rappresenta un'astrazione del progetto stesso, ovvero le


cartelle e file non coincidono realmente con quelle che troveremo nel percorso
selezionato durante la creazione del progetto. I dettagli reali sono invece mostrati
selezionando il Tab Files, come mostrato in Figura 32.

87

Figura 32: Dettagli sui file del progetto.

Mostrando i Files, si pu subito vedere che pur non avendo scritto una riga di codice, il
nostro progetto ha gi oltre 10 Files. Questi Files sono creati in automatico dall'ambiente
di sviluppo basato su Netbeans ed in particolare mantengono le informazioni del nostro
progetto e dell'editor. Dal momento che non ci troveremo a dover modificare questi Files,
bene mantenere le informazioni del Tab Projects, come mostrato in Figura 31.
Altre informazioni utili sul progetto sono mostrate in basso a sinistra, nella Dashboard,
come mostrato in Figura 33.

Figura 33: Dettagli mostrati nella Dashboard del progetto.

In particolare la Dashboard rappresenta un riassunto delle varie impostazioni


precedentemente selezionate, ovvero mostra il nome del progetto, percorso, Tool Chain,
88

programmatore e memoria utilizzata dal nostro programma.


Per cambiare le propriet del progetto appena creato, qualora per esempio si voglia
ricompilare il progetto per un altro microcontrollore o si voglia per esempio ricompilare il
tutto con la nuova versione del compilatore, possibile premere il trasto della chiave con
bulloni, presente sul lato della Dashboard.
Le impostazioni del progetto sono mostrate in Figura 34.

Figura 34: Propriet del progetto.

Si noti come la finestra riassuma tutte le impostazioni che abbiamo precedentemente


impostato durante la creazione del progetto. Un altro modo per aprire le propriet del
progetto semplicemente selezionando il progetto nella finestra in alto a sinistra e con il
tasto destro del mouse far aprire il menu a finestra dal quale selezionare la voce:
Properties

Il menu mostra molte altre voci che piano piano bene conoscere al fine di prendere
maggiore dimestichezza con l'ambiente di sviluppo. Questo permetter, durante lo
sviluppo di progetti pi complessi di risparmiare molto tempo.
Dopo molte parole abbiamo in realt ancora un progetto vuoto, per cui a questo punto
bene iniziare a scrivere il nostro programma. Per poter scrivere il programma
necessario creare un file di testo con estensione .c (il nostro programma infatti scritto in
C). Il file creato deve poi essere incluso tra i file di progetto nella directory Project Files.
Per fare questo si pu creare per esempio nella directory del nostro progetto un file
nominato main.c , selezionare la voce Source Files dalla finestra Files di Figura 31 e con il
89

tasto destro del mouse far comparire il menu laterale dal quale selezionare la voce
Add Existing Items...

Selezionata la voce viene mostrata una finestra di dialogo Open File standard, dalla quale
cercare il file appena creato all'interno della nostra directory. Questa tecnica pu essere
pratica qualora il file sorgente esista gi e debba essere incluso nel progetto. Qualora il file
non esista ancora il modo pi pratico riportato in Figura 35, ovvero selezionando la
voce Source Files e premendo il tasto destro del mouse selezionare la voce:
New C Source File...

come mostrato in Figura 35.

Figura 35: Creazione del file main.c da inserire nel progetto.

Nella finestra di dialogo che si apre, scrivere il nome


Name, come mostrato in Figura 36.

main

(principale) nella casella File

Figura 36: Creazione del file main.c da inserire nel progetto.

90

Il nome main sta solo ad indicare che il file che stiamo creando quello principale ma
potrebbe avere anche un altro nome. Quando creeremo progetti pi complessi e
divideremo il nostro programma in pi file sorgenti, dovremo creare altri file con
estensione .c, dando ad ognuno un nome significativo che riflette i contenuti del file
stesso. Premendo il tasto Finish, l'ambiente di sviluppo mostrer i cambiamenti come
mostrato in Figura 37. Si noti in particolare che la cartella Source Files, contiene ora il file
main.c mentre la finestra principale sul lato destro mostra in file vuoto main.c .

Figura 37: Ambiente di sviluppo al termine dell'aggiunta dei file.

A questo punto abbiamo completato il nostro progetto, ma naturalmente sebbene sia


stato richiesto un certo sforzo, non fa assolutamente nulla.

Impostare l'ambiente di lavoro


Completato lo scheletro del nostro primo progetto, prima di poter scrivere il nostro
programma necessario impostare ancora dei parametri al fine di poter compilare con
successo il programma che andremo a scrivere a breve.
Le impostazioni da cambiare sono riportate nella finestra delle propriet del progetto di
Figura 34, precedentemente mostrata. Una volta aperta tale finestra necessario
selezionare tra le impostazione globali del compilatore XC8 la voce XC8 Compiler, come
riportato in Figura 38. Fatto questo, compariranno sulla destra una serie di impostazioni.
Tra le varie impostazioni bisogna abilitare il CCI Common C Interface. Questa opzione
permette di abilitare una sintassi standard che permetter di scrivere codici che potranno
essere pi facilmente riscritti anche per altri microcontrollori Microchip non ad 8 bit. In
particolare si deciso di farne uso visto che la Microchip ne raccomanda l'utilizzo.
91

Figura 38: Impostazioni del compilatore (Preprocessing and Messages).

Selezionando tra le Option Categories (in alto al centro), la voce Optimizations


compariranno le opzioni di Figura 39. In particolare in questa finestra possibile
selezionare il livello con cui il compilatore deve ottimizzare il codice che scriveremo. Nei
nostri esempi base si possono lasciare le ottimizzazioni di Default ma bene tenere a
mente che quando si effettua il Debug necessario disabilitare le ottimizzazioni al fine di
garantire una equivalenza tra il codice scritto in C e il codice Assembly. In particolare
necessario abilitare la voce Debug. Quando saremo pi esperti vedremo maggiori dettagli
e capiremo anche meglio la ragione per cui necessario disabilitare le ottimizzazioni. Da
questa stessa finestra possibile vedere la licenza del compilatore, che nel nostro caso
Free. Abilitando tale licenza non si hanno limiti temporali sull'utilizzo del compilatore,
anche se questo va a scapito del livello di ottimizzazione raggiungibile.

92

Figura 39: Impostazioni del compilatore (Optimizations).

A questo punto bene impostare anche qualche opzione del Linker, come riportato in
Figura 40. In particolare necessario impostare la voce XC Linker e l'Option Categories
Memory Model. Tra le opzioni che vengono visualizzate necessario cambiare l'opzione
Size of Double e Size of Float, da 24 bit a 32 bit. Questo cambiamento serve solo ad evitare
che compaiano le seguenti Warning:
:: warning: 24-bit floating point types are not supported; float have been
changed to 32-bits
:: warning: 24-bit floating point types are not supported; double have
been changed to 32-bits

Quindi se non cambiamo il formato da 24 bit a 32 bit ci penserebbe il compilatore in ogni


modo. Come si vedr nel corso del testo, al fine di scrivere un codice pulito e robusto, si
cercher sempre di eliminare ogni Warning (Avviso).

93

Figura 40: Impostazioni del compilatore (Optimizations).

Fatto questo facciamo un passo indietro, anche se in realt quanto segue potr essere
fatto in contemporanea agli altri cambiamenti. Per poter compilare i codice di esempio del
testo si fa uso della libreria LaurTec per PIC18, scaricabile gratuitamente dal sito
LaurTec74. Una volta scaricata e salvata la libreria, necessario impostare l'ambiente di
lavoro al fine di utilizzarle. In Figura 41 mostrato un dettaglio dei percorsi da aggiungere
nel campo Include Directories. In particolare necessario includere i seguenti tre percorsi
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.2.0\conf
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.2.0\inc
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.2.0\src

Le tre directory, conf, inc e src rappresentano rispettivamente le directory dove sono
presenti i file di configurazione, gli header file di libreria e i codici sorgenti delle librerie 75.
Nei primi esempi del testo basta includere il solo percorso conf, visto che non faremo uso
di librerie per alcuni Capitoli.
Impostare il percorso delle librerie un passo sempre necessario che bisogna
compiere in qualunque ambiente di sviluppo e microcontrollore utilizzato.
Errori nell'impostare i percorsi di libreria rappresentano normalmente la
maggioranza dei problemi che si incontrano quando si inizia programmare.

74
75

La libreria viene fornita in file zip, per cui necessario scompattare il file prima di poterlo utilizzare.
Per ora non vi preoccupate per come sono strutturate le directory e sul significato di header file, maggiori dettagli saranno
dati a tempo debito.

94

Figura 41: Impostazioni dei percorsi di libreria.

A questo punto abbiamo realmente completato ogni impostazione per poter compilare
con successo il nostro programma. Tali operazioni andranno ripetute alla creazione di
ogni progetto. Per maggiori dettagli sulle altre impostazioni del compilatore e del Linker,
rimando alla user guide del compilatore XC8 che possibile trovare all'interno della
directory di installazione del compilatore stesso. Un esempio di percorso dove trovarla il
seguente:
C:\Program Files (x86)\Microchip\xc8\v1.21\docs

Scrivere e Compilare un progetto


Una volta creato il progetto possibile cominciare a scrivere il programma all'interno
della finestra di testo nominata main.c. Normalmente questa fase, anche se in questo caso
consiste semplicemente in un copia e incolla, deve essere preceduta dalla fase di analisi per
la risoluzione del progetto.
Non bisogna mai iniziare a programmare senza aver analizzato in maniera
opportuna il problema. La fase di programmazione deve essere solo una
traduzione della soluzione dal linguaggio umano, pi o meno astratto, in
linguaggio C.

95

In ogni modo questa non la sede in cui vi insegner l'approccio alla programmazione,
ma non posso non consigliarvi la lettura di testi per l'ingegneria del software.
giunto il momento di programmare...
Copiate ed incollate brutalmente il seguente codice, facendo attenzione che non
compaiano simboli strani dovuti per esempio al passaggio da una pagina all'altra. Quando
sarete pi esperti scriverete il vostro programma, ma in questa fase ho solo interesse nel
mostrare i passi da compiere per scrivere e compilare il file sorgente, ovvero il file C. Il
programma non molto complesso, ma dal momento che ogni sua linea una cosa
nuova, sar analizzato in dettaglio, ma tempo al tempo. Una volta scritto o copiato il
programma nel file main.c, salvate il file e avviate la compilazione per mezzo della voce
del menu
Run Build and Clean Main Project

o cliccare sull'icona della Tool bar:

#include <xc.h>
#include "PIC18F4550_config.h"
int main (void){
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutti ingressi e RD0 come uscita
LATD = 0x00;
TRISD = 0b11111110;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
LATDbits.LATD0 = 1;
// Ciclo infinito
while (1) {
}

96

Quando si avvia la compilazione, la finestra di testo in basso a destra visualizza come


output tutte le informazioni relative alle attivit svolte e al loro esisto, come riportato in
Figura 42.

Figura 42: Finestra di Output.

Osservando il testo con maggior dettaglio si ha:


CLEAN SUCCESSFUL (total time: 101ms)
make -f nbproject/Makefile-default.mk SUBPROJECTS= .build-conf
make[1]: Entering directory `D:/01_Design/02_Tutorial/01XC8_step_by_step/Software/CP_04/01_Hello_World.X'
make -f nbproject/Makefile-default.mk
dist/default/production/01_Hello_World.X.production.hex
make[2]: Entering directory `D:/01_Design/02_Tutorial/01XC8_step_by_step/Software/CP_04/01_Hello_World.X'
"C:\Program Files (x86)\Microchip\xc8\v1.21\bin\xc8.exe" --pass1
--chip=18F4550 -Q -G --double=32 --float=32 --emi=wordwrite
--opt=default,+asm,+asmfile,-speed,+space,-debug --addrqual=ignore
--mode=free -P -N255
-I"../../../../../01_Progetti/Library/LaurTec_c18_libraries/LaurTec_PIC_li
braries_v_3.1.3/conf"
-I"../../../../../01_Progetti/Library/LaurTec_c18_libraries/LaurTec_PIC_li
braries_v_3.1.3/inc"
-I"../../../../../01_Progetti/Library/LaurTec_c18_libraries/LaurTec_PIC_li
braries_v_3.1.3/src" --warn=0 --cci --asmlist --summary=default,-psect,class,+mem,-hex,-file --output=default,-inhx032 --runtime=default,+clear,
+init,-keep,-no_startup,-download,+config,+clib,+plib --output=-mcof,+elf
"--errformat=%%f:%%l: error: (%%n) %%s" "--warnformat=%%f:%%l: warning: (%
%n) %%s" "--msgformat=%%f:%%l: advisory: (%%n) %%s"
-obuild/default/production/main.p1 main.c
"C:\Program Files (x86)\Microchip\xc8\v1.21\bin\xc8.exe" --chip=18F4550
-G -mdist/default/production/01_Hello_World.X.production.map --double=32
--float=32 --emi=wordwrite --opt=default,+asm,+asmfile,-speed,+space,debug --addrqual=ignore --mode=free -P -N255
-I"../../../../../01_Progetti/Library/LaurTec_c18_libraries/LaurTec_PIC_li
braries_v_3.1.3/conf"
-I"../../../../../01_Progetti/Library/LaurTec_c18_libraries/LaurTec_PIC_li
braries_v_3.1.3/inc"
-I"../../../../../01_Progetti/Library/LaurTec_c18_libraries/LaurTec_PIC_li
braries_v_3.1.3/src" --warn=0 --cci --asmlist --summary=default,-psect,class,+mem,-hex,-file --output=default,-inhx032 --runtime=default,+clear,
+init,-keep,-no_startup,-download,+config,+clib,+plib --output=-mcof,+elf
"--errformat=%%f:%%l: error: %%s" "--warnformat=%%f:%%l: warning: (%%n) %
%s" "--msgformat=%%f:%%l: advisory: (%%n) %%s"
-odist/default/production/01_Hello_World.X.production.elf
build/default/production/main.p1
Microchip MPLAB XC8 C Compiler V1.21
Copyright (C) 2013 Microchip Technology Inc.

97

License type: Node Configuration


:: advisory: (1233) Employing 18F4550 errata work-arounds:
:: advisory: (1234) * Corrupted fast interrupt shadow registers
:: warning: (1273) Omniscient Code Generation not available in Free mode
Memory Summary:
Program space
Data space
Configuration bits
EEPROM space
ID Location space

used
used
used
used
used

2Eh
0h
7h
0h
8h

(
(
(
(
(

46)
0)
7)
0)
8)

of
of
of
of
of

8000h
800h
7h
100h
8h

bytes
bytes
words
bytes
bytes

( 0.1%)
( 0.0%)
(100.0%)
( 0.0%)
(100.0%)

Running this compiler in PRO mode, with Omniscient Code Generation


enabled,
often produces code which is 60% smaller and at least 400% faster than in
Free mode. The MPLAB XC8 PRO compiler output for this code could be
21 bytes smaller and run 4 times faster.
See http://www.microchip.com for more information.
make[2]: Leaving directory `D:/01_Design/02_Tutorial/01XC8_step_by_step/Software/CP_04/01_Hello_World.X'
make[1]: Leaving directory `D:/01_Design/02_Tutorial/01XC8_step_by_step/Software/CP_04/01_Hello_World.X'
BUILD SUCCESSFUL (total time: 10s)
Loading code from D:/01_Design/02_Tutorial/01XC8_step_by_step/Software/CP_04/01_Hello_World.X/dist/default/production/0
1_Hello_World.X.production.hex...
Loading symbols from D:/01_Design/02_Tutorial/01XC8_step_by_step/Software/CP_04/01_Hello_World.X/dist/default/production/0
1_Hello_World.X.production.elf...
Loading completed

In particolare la prima operazione il Cleaning (pulizia) dei file intermedi generati da


compilazioni precedenti. Nel nostro caso non c' molto da ripulire visto che la nostra
prima compilazione. La fase di pulizia discende dal fatto che abbiamo premuto il tasto
con il martello e pennello invece del tasto con il solo martello:

Premendo il tasto con il solo martello si effettuerebbe la sola compilazione del


programma senza la pulizia dei file intermedi. In progetti grandi, formati da pi file in cui
si possono apportare varie modifiche, sempre bene premere il tasto con martello e
pennello, in maniera tale da cancellare i file intermedi ed essere certi che il nuovo
programma compilato sia effettivamente quello derivante dal progetto con le modifiche e
non uno derivante dal mix del nuovo e vecchio progetto. In questo secondo caso potreste
vedere comportamenti strani. La pulizia con il Cleaning dura qualche secondo di pi ma
pu evitare frustrazioni.
Alla fine del testo, se tutto andato a buon fine, viene visualizzato il messaggio BUILD
SUCCEEDED, ovvero la compilazione ha avuto successo. In caso contrario vengono
visualizzati gli errori e le Warning che il programma ha generato. In particolare se sono
presenti errori non viene generato il codice macchina ovvero .hex per cui stiamo facendo
tanti sforzi. Ogni messaggio di errore e Warning accompagnato anche dalla riga alla quale
98

stato generato il messaggio76.


Frequentemente quando ci si scorda il ; alla fine di ogni riga, l'errore viene
segnalato alla riga successiva. Dunque per trovare un errore sempre bene
guardare anche le righe che precedono la riga dove stato segnalato l'errore.

Per eliminare gli errori bene procedere risolvendo il primo errore, infatti pu capitare
che altri errori siano solo dovuti ad effetti collaterali del primo. Per tale ragione, quando si
pensa di aver risolto il primo errore, bene ricompilare il codice. Questo dove essere
ripetuto fino alla eliminazione di ogni errore.
Qualora dovessero essere generate delle Warning il nostro file .hex
generato senza problemi.

viene

A questo punto bene mettere in evidenza che ogni programma, per buona pratica di
programmazione e robustezza delle applicazioni, deve essere compilato senza nessun
messaggio di Warning. Qualora dovessero essere visualizzati dei messaggi di Warning
bene capire fino in fondo il significato del messaggio e fare in modo che non venga
visualizzato77. Le Warning possono nascondere infatti dei bug subdoli...la cui caccia pu
essere poco divertente. Dopo aver compilato il nostro primo programma, diamo
un'occhiata all'interno della directory 01_Hello_World per vedere i vari file che sono stati
generati per il nostro progetto.

Figura 43: File all'interno della cartella del progetto.


76
77

Un modo per far scomparire tutti i messaggi di Warning quello di disabilitarle per mezzo delle opzioni di progetto associate
al compilatore XC8, sezione Preprocessing and Messages. Normalmente vengono visualizzati sia gli errori che le Warning ma pu
essere scelto di visualizzare solo gli errori. Questa pratica per altamente sconsigliata, le Warning devono essere eliminate
capendo la causa che le ha generate.

99

Dalla Figura 43, selezionando il Tab Files, possibile vedere l'architettura delle cartelle
all'interno della cartella principale. In particolare tutti i file generati dalla compilazione
sono salvati all'interno della cartella
dist default production

Il file pi importante il file .hex ovvero : 01_Hello_World.X.production.hex. Questo file


rappresenta il file in linguaggio macchina, scritto in esadecimale. Viene ottenuto per
mezzo del lavoro congiunto del compilatore e successivamente il Linker. Il compilatore si
occupa di tradurre il nostro programma scritto in C in linguaggio macchina comprensibile
dal microcontrollore, mentre il Linker si occupa di organizzare i file oggetto generati dal
compilatore (potrebbero essere pi di uno, visto che un progetto potrebbe contenere altri
file e librerie) e allocarli all'interno della memoria del microcontrollore, rispettando le
informazioni del file .lkr78. Il suo formato generalmente basato sullo standard Intel HEX
ovvero semplice file ASCII leggibile con normale editor. Tale formato accettato
praticamente da tutti i programmatori e viene utilizzato anche come formato per scrivere i
dati all'interno di memorie EEPROM, OTP e Flash. Nel nostro primo esempio il file di
uscita il seguente:
:04000000EBEF3FF0F3
:107FD6000001EEEF3FF0000E896E9268000E8A6E89
:107FE6009368000E8B6E9468000E8C6EFE0E956E76
:0A7FF600000E8D6E96688C80FFD798
:020000040020DA
:08000000FFFFFFFFFFFFFFFF00
:020000040030CA
:0E000000200C0000FF8081FF0FC00FE00F40BA
:00000001FF

Le informazioni sono divise nel seguente modo:


:04000000EBEF3FF0F3
Numero di Byte
Indirizzo di partenza
Tipo di dati
Byte dati (programma)
Checksum di controllo

Tra i vari file generati vi il file .map ovvero il mapping file. Questo file possiede al suo
interno le informazioni relative alle varie variabili e registri utilizzati nel programma. Viene
generato come file di output dal Linker. Al suo interno possibile anche trovare le
informazioni associate alla quantit di memoria utilizzata (visualizzate anche nella finestra
di Output).
Un modo pi pratico per vedere la memoria utilizzata per mezzo della finestra
Dashboard visualizzata in Figura 44. Nel caso in cui la Dashboard non sia visibile possibile
mostrarla dal menu:
78

Il Linker file contiene le informazioni relative all'organizzazione della memoria e il suo possibile utilizzo. In particolare
impone dei vincoli in maniera da permettere al Linker di comprendere dove poter posizionare il programma. Maggiori
dettagli li si comprenderanno nel corso del testo.

100

Window Dashboard

In particolare mostrato l'utilizzo della memoria RAM e Flash. Nel caso specifico viene
mostrato un suo percentuale pari a 0%. Nel caso della memoria RAM, non avendo
dichiarato alcuna variabile non ci sorprende ma per la memoria Flash, osservando con
maggior dettaglio si sono usati 68 byte ovvero 0x44 byte. 68 Byte su 32700 disponibili
sono visualizzati come 0%. Il numero di byte pari a 68 si potrebbe anche ricavare dal hex
file sommando tutti i campi numero di byte il cui Tipo dati pari a 00.
In ultimo bene evidenziare che il numero e tipo di file generati viene a dipendere
dalle impostazioni del compilatore.

Figura 44: Utilizzo della memoria.

Programmare il microcontrollore
Una volta scritto il nostro programma e compilato con successo, possibile caricare il
nostro file .hex all'interno del nostro PIC. Questa fase pu essere differente a seconda del
programmatore utilizzato. Nel caso di programmatori ufficiali Microchip, supportati
dall'ambiente MPLAB X stato gi selezionato durante la fase di creazione del progetto.
Qualora il programmatore non fosse stato selezionato, andr impostato tra le propriet
del progetto, come mostrato in precedenza 79. I passi che seguono fanno riferimento al
programmatore PICKIT 3 ma sono in realt simili anche per gli altri programmatori
integrati con MPLAB X .
In Figura 45 riportato il dettaglio della Tool Bar, qualora il programmatore strato
selezionato (selezionando il simulatore, come si vedr, la Tool Bar differente).

79

Qualora si facesse uso di un programmatore con integrato con l'ambiente di sviluppo MPLAB X, necessario aprire la GUI
disponibile per il programmatore e selezionare il relativo PIC da programmare. Successivamente sar necessario importare il
file .hex da caricare.

101

Figura 45: Tool Bar che mostra il tasto di programmazione.

A questo punto possibile, se non precedentemente fatto, collegare il programmatore.


Si ricorda che il programmatore PICKIT 3, come anche altri programmatori deve essere
collegato all'USB (dunque essere alimentato) prima di essere collegato alla scheda di
sviluppo (Target Board). Una volta collegato il programmatore al PC, possibile collegarlo
alla scheda di sviluppo, la quale deve essere gi alimentata. Fatto questo possibile
premerne il tasto di programmazione:

Nel caso in cui il programmatore non sia collegato o diverso da quello impostato
durante la creazione del progetto, viene visualizzata la seguente finestra di messaggio:

Figura 46: Finestra di messaggio nel caso in cui il programmatore non sia collegato o sia stato cambiato.

Le altre icone presenti nella Tool Bar permettono rispettivamente di leggere il contenuto
del microcontrollore:
Di mantenere in stato di Reset la scheda dopo la programmazione:

e di avviare una sessione di Debug:


Durante la fase di programmazione del microcontrollore si vengono a creare dei Reset
dello stesso per cui prima che il nuovo programma venga caricato si possono avere delle
102

esecuzioni momentanee del programma precedente. La fase di programmazione sul


PICKIT 3 segnalata dal lampeggio del LED rosso. A fine programmazione, nel caso in
cui il tasto Hold in Reset non sia attivo, il programma viene automaticamente eseguito.
Qualora Hold in Reset sia attivo, per avviare il programma necessario disattivarlo o
staccare il programmatore.
Questa funzione potrebbe essere utile qualora il nostro programma faccia per esempio
girare un motore e non volessimo farlo girare subito dopo la programmazione.
Durante la programmazione potrebbe essere visualizzato il seguente messaggio:

Figura 47: Messaggio di avviso alla prima programmazione.

Nel caso specifico delle schede di sviluppo usate nel testo, essendo l'alimentazione pari
a 5V, non ci sono problemi, per cui possibile selezionare il check box Do not show this
message again.

Capire come salutare il mondo


Dopo aver capito i vari passi da compiere per creare, compilare e caricare il nostro
progetto all'interno del microcontrollore, bene fare un passo indietro per capire meglio il
programma che abbiamo scritto. Tale spiegazione costruir le basi sulle quali inizieremo
poi la spiegazione pi dettagliata del linguaggio C.
Normalmente il codice sorgente principale inizia con la direttiva #include, la quale
permette, come dice il nome stesso, di includere qualcosa. Nel nostro caso abbiamo
#include <xc.h>

Le direttive in C iniziano sempre con # e rappresentano delle indicazioni che il


preprocessore (e non il compilatore) utilizza prima di avviare la compilazione del
programma. Il preprocessore ha il compito, in un certo qual modo, di ordinare la scrivania
con tutti i documenti, costanti, file, necessari alla corretta compilazione.
Le direttive non appartengono al programma che il PIC fisicamente eseguir; questo
significa che una direttiva non un'istruzione eseguibile dal PIC e non viene dunque
tradotta in codice eseguibile80.
Includere il file xc.h, fornisce molte informazioni, in particolare permette di includere
80

Si noti che al termine della riga non presente il punto e virgola, che invece verr utilizzato al termine di ogni istruzione C.

103

indirettamente il file pic18f4550.h, che riflette quello selezionato nel progetto, per cui
cambiando microcontrollore viene incluso il relativo file. Questo file, come detto,
contiene le informazioni del PIC che si sta utilizzando81.
Il file pic18f4550.h incluso e il file di Linker sono tra loro sempre coerenti, visto che
l'informazione prelevata dal progetto stesso, ovvero dal microcontrollore che abbiamo
selezionato. Sebbene sia possibile includere manualmente il file pic18f4550.h, facendo
uso della modalit CCI (Common C Interface) necessario includere il file xc.h. In
particolare questo file deve essere incluso in tutti i file di progetto che vengono inclusi
nell'applicazione. Come si vedr negli esempi successivi la direttiva #include viene anche
utilizzata per includere file di libreria.
A seguire viene incluso un secondo file:
#include <PIC18F4550_config.h>

Questo file viene prelevato dalla libreria LaurTec e contiene tutte le configurazioni
necessarie per il PIC18F4550, per tale ragione durante la creazione del progetto stato
necessario impostare il percorso d'inclusione della libreria LaurTec, proprio per
permettere che questo file sia trovato. Dal nome si capisce che in base al microcontrollore
utilizzato bisogna cambiare file82.
Posizionando il mouse sopra il file e premendo CTRL+B possibile aprire il file stesso
ed esplorare il suo contenuto. In particolare possibile trovare la nuova direttiva #pragma
utilizzata per impostare le varie configurazioni 83. Tra le principali che bisogna impostare vi
sono le seguenti.
#pragma
#pragma
#pragma
#pragma

config
config
config
config

FOSC = HS
WDT = OFF
LVP = OFF
PBADEN = OFF

Il compilatore XC8 diversamente dal precedente compilatore C18, richiede che tutte le
configurazioni siano impostate, altrimenti vengono generate delle Warning. Proprio per
tale ragione, visto il numero elevato di configurazioni necessarie ho creato un file che le
racchiude tutte.
Come visto durante la spiegazione dell'architettura PIC18, ogni PIC ha diversi registri
di configurazione (registri CONFIG) il cui scopo settare l'hardware di base del PIC
stesso in modo che possa funzionare correttamente ogni volta che viene alimentato il
PIC. Per poter cambiare il valore dei registri di configurazione necessario utilizzare
l'opzione config, seguita dal nome dell'opzione da impostare. Per ogni particolare
configurazione possibile assegnare vari valori a seconda dell'hardware disponibile. Il
nome delle opzioni e le loro possibili impostazioni sono mostrate per ogni PIC all'interno
del file 18f4550.html che possibile trovare nella directory chips presente nella directory
d'installazione del compilatore:
81

82
83

Tra le informazioni presenti nel file vi il nome dei registri che in generale hanno lo stesso nome utilizzato anche sul
Datasheet del PIC che si sta utilizzando. L'utilizzo del nome dei registri permette di non considerare la locazione di memoria
in cui sono fisicamente presenti permettendo dunque di raggiungere un livello di astrazione che semplifica il programma
stesso.
I file di configurazione disponibili nella libreria LaurTec possibile trovarli all'interno della directory conf.
La direttiva #pragma pu essere utilizzata anche in altri contesti oltre alla configurazione dei parametri interni del
microcontrollore.

104

C:\Program Files (x86)\Microchip\xc8\v1.21\docs\chips

Il nome delle opzioni non sempre uguale per tutti i PIC, dunque bene sempre fare
riferimento al file indicato per maggior informazioni. L'utilizzo di parametri errati genera
un errore del tipo opzione non valida84.
Qualora il file di configurazione non fosse disponibile nelle librerie LaurTec, possibile
crearne uno facendo uso dello strumento integrato all'interno di MPLAB X come
riportato in Figura 48. Questo strumento possibile trovarlo nel menu:
Window PIC Memory Views Configuration Bits

Si pu notare che le varie configurazioni cambiate vengono segnalate in rosso e la relativa


descrizione disponibile sulla destra.
Premendo il tasto Generate Source Code to Output possibile creare un file di testo con le
nuove impostazioni. Il testo visualizzato possibile copiarlo all'interno di un file .h
usando la nomenclatura usata nella libreria LaurTec, ma un qualunque altro nome va
bene, purch il nuovo file venga poi incluso nel progetto.

Figura 48: Tool d'impostazione dei registri di Configurazione.

Le configurazioni impostate si riflettono all'interno del file .hex che viene generato alla
fine della compilazione del progetto, per cui vengono programmate all'interno della
microcontrollore durante la stessa fase di programmazione.
Molti programmi venduti con i programmatori permettono anche di impostare i bit di
configurazione prima di programmare il PIC, sovrascrivendo eventuali valori letti dal file
.hex.
Tra le impostazioni necessarie per le schede di sviluppo proposte, FOSC impostato
ad HS, ovvero ad alta velocit questo poich le schede di sviluppo fanno uso di un
quarzo da 20MHz. Qualora si voglia far uso del quarzo interno si deve selezionare
l'opzione appropriata. Volendo usare il modulo USB a sua volta necessario l'utilizzo di
un'altra opzione. Qualora si abbia l'esigenza di cambiare delle configurazioni si consiglia di
lasciare il file originale PIC18F4550_config.h invariato e creare un nuovo file cambiando
84

Le varie configurazioni possibile impostarle, solo per mezzo del codice e non pi per mezzo della GUI, come era possibile
per mezzo di MLAB IDE.

105

il nome ed apportare le modifiche solo su quest'ultimo.


Tra le altre impostazioni importanti si fa notare che il WDT ovvero il Watchdog
disattivato avendo scritto OFF. Anche la modalit LVP viene disattivata visto che non
utilizzata85. In ultimo il bit PBADEN posto ad OFF, in modo da utilizzare gli ingressi
analogici presenti sulla PORTB come normali I/O digitali. Si fa presente che questo bit
non sempre presente su tutti PIC poich non tutti i PIC18 hanno ingressi analogici
anche sulla PORTB. Quanto appena detto descritto come commento all'interno del file
PIC18F4550_config.h

Questo permette di rendere pi facile un eventuale cambio di configurazione e


comprendere anche quella selezionata. In particolare il commento per la configurazione
CPUDIV :
// System Clock Postscaler Selection bits
//OSC2_PLL3
[Primary Oscillator Src: /2][96
//OSC4_PLL6
[Primary Oscillator Src: /4][96
//OSC3_PLL4
[Primary Oscillator Src: /3][96
//OSC1_PLL2
[Primary Oscillator Src: /1][96
#pragma config CPUDIV =
OSC1_PLL2

MHz
MHz
MHz
MHz

PLL
PLL
PLL
PLL

Src:
Src:
Src:
Src:

/3]
/6]
/4]
/2]

I commenti in C devono essere preceduti dal doppio slash //. I commenti scritti in
questo modo devono per stare su di una sola riga. Quando si ha l'esigenza di scrivere
pi righe di commento bisogna scrivere // all'inizio di ogni riga o scrivere /* alla prima
riga e scrivere */ alla fine dell'ultima riga. Un possibile esempio di commento a riga
multipla il seguente:
// questo un commento
// che non entrerebbe su una sola riga

o anche
/* questo un commento
che non entrerebbe su una sola riga */

o ancora
/* questo un commento
che non entrerebbe su una sola riga
*/

o ancora
/*
* questo un commento
* che non entrerebbe su una sola riga
*/

85

La modalit LVP pu risultare pratica qualora non sia disponibile la tensione normalmente utilizzata per programmare,
normalmente pi alta di quella di alimentazione. Nel nostro caso non un problema poich la tensione di programmazione
ottenuta direttamente dal programmatore.

106

Normalmente il commento, oltre a descrivere una riga di codice, viene anche utilizzato
per disattivare eventuali righe di codice che stanno potenzialmente creando problemi. Per
agevolare la fase di Debug in cui si voglia commentare una parte di codice da disattivare
per poi riattivarla in un secondo momento, l'editor di MPLAB X fornisce i seguenti
pulsanti:

Che permettono appunto di commentare una parte di codice selezionata o rimuovere il


commento, senza doversi preoccupare di aggiungere gli slash necessari.
Vediamo ora il programma vero e proprio. Ogni programma C una collezione di
funzioni86 che possono o meno essere scritte nello stesso file. Il numero di funzioni
presente in ogni programma viene a dipendere dal programmatore e dal modo con cui ha
organizzato la soluzione del suo problema. In ogni modo all'interno di ogni programma C
deve essere sempre presente una funzione nominata main, una ed una sola funzione sar
chiamata main87. Personalmente scrivo tale funzione all'interno del file principale del
progetto che chiamo appunto main.c.
La funzione main quella che il compilatore cerca per prima e dalla quale organizza la
compilazione88 delle altre funzioni che sono ragionevolmente collegate direttamente o
indirettamente alla funzione main. Ogni funzione deve essere dichiarata nel modo
seguente:
tipo_var_restituita NomeFunzione (tipo_var_in_ingresso1)

La funzione main, secondo le specifiche del compilatore XC8, restituisce un valore


intero dunque viene scritto int. Inoltre la funzione main almeno per i PIC18 non accetta
variabili in ingresso e dunque viene scritto void, che significa appunto nessun valore.
Come si vedr in maggior dettaglio nel Capitolo dedicato alle funzioni, ogni funzione pu
restituire al massimo un valore o puntatore, ma pu accettare in ingresso pi variabili.
Ogni funzione, per sua natura, svolge qualche operazione che viene poi tradotta dal
compilatore in istruzioni eseguibili dal PIC. Queste istruzioni devono essere contenute
all'interno di due parentesi graffe, una aperta e una chiusa, che stanno ad indicare l'inizio
della funzione e la sua fine.
Per scrivere le parentesi graffe con tastiere italiane pu essere un problema, questo
problema non sentito dagli americani (che guarda caso hanno inventato il C) poich le
parentesi graffe, come anche le quadre, sono apparecchiate sulla tastiera. Un modo per
scrivere le parentisi graffe per mezzo dei codici ASCII 123 ({) e 125 (}). Per scrivere tali
caratteri sulla tastiera bisogna tenere premuto il tasto ALT e digitare il codice 123 o 125, e
poi rilasciare il tasto ALT. Un secondo modo, utilizzabile solo se si hanno le parentesi
quadre sulla tastiera, quello di premere freccia maiuscole + Alt Gr + parentesi quadra.
Un programma che non fa nulla potrebbe essere scritto nel seguente modo:
86

87
88

Ogni funzione contiene un certo numero di istruzioni per mezzo delle quali la funzione svolge il compito per cui stata
creata.
Tutte le funzioni all'interno del nostro progetto devono essere individuate da un nome univoco.
Quanto appena detto non deve essere preso per oro colato, ma un modo valido per semplificare la trattazione
dell'argomento. Infatti dietro le quinte, prima della funzione main sono presenti altre funzioni nascoste, dalle quali viene
richiamata la funzione main.

107

int main (void) {


// Non faccio assolutamente nulla
}

Personalmente metto le parentesi graffe come appena scritto, ma le si potrebbe anche


scrivere in quest'altro modo.
void main (void)
{
// Non faccio assolutamente nulla
}

Per molto tempo ho usato questo secondo modo, ma dal momento che tutti i
programmatori pi famosi fanno pi uso della prima modalit, ho deciso di utilizzare la
prima forma89. L'aderire ad una versione o l'altra, non di vitale importanza ma
importante che una volta scelta la preferita si mantenga lo stile scelto. Questo rende il
codice pi leggibile, grazie alla sua uniformit. un qualcosa che apprezzerete quando
dovrete scrivere del codice che va oltre qualche linea per salutare il mondo, e lo
apprezzerete ancor pi quando sarete costretti a rimettere mani sul codice a distanza di
tempo.
Vediamo ora il programma che abbiamo scritto all'interno delle nostre parentesi
graffe. Queste sono le prime righe di codice:
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;

Questo codice, da quanto spiegato nei Capitoli precedenti permette di inizializzare il


Latch di uscita della PORTA, ponendo i bit del registro LATA 90 a zero, ed imposta tutti i
pin della PORTA come ingressi, infatti FF in esadecimale 255 che in binario
11111111. Si ricorda che ponendo a 1 il bit del registro TRIS si ha che il pin
corrispondente un input. possibile vedere che ogni istruzione termina con un punto e
virgola.
Per chi ha esperienza di programmazione in Basic...e non, un errore tipico di
sintassi scordarsi il punto e virgola. Il compilatore in questo caso individua
l'errore alla riga successiva a quella in cui manca effettivamente il punto e virgola.
Da notare inoltre che il punto e virgola nella sintassi in Assembly, dichiara invece
un commento.
Il nome dei registri LATA e TRISA sono stati scritti in maiuscolo, questo non stato
fatto per metterli in evidenza ma poich sono dichiarate maiuscole. Il compilatore C
case sensitive ovvero distingue tra maiuscole e minuscole. Dunque TRISA per il
89

90

Questa scelta non mi fa rientrare tra i programmatori pi famosi ma dovrebbe aiutare la gente che ha letto i libri di
programmatori famosi, visto che in questo testo user lo stesso standard.
Si fa notare che il nome LATA non era stato definito da nessuna parte. Questo discende dal fatto che il nome in realt
definito all'interno del file p18f4580.h incluso indirettamente includendo il file xc.h.

108

compilatore diverso da TrisA.


Vediamo ora il valore che si scritto nei registri LATA e TRISA. Il valore 0xFF
rappresenta un valore alfanumerico espresso in esadecimale il suo formato piuttosto
standard. Ogni volta che si scrive un numero importante scriverlo nella sintassi giusta,
in modo da permettere al compilatore di riconoscere la base utilizzata (normalmente 2, 10
e 16) e di conseguenza il valore rappresentato. Se si volesse per esempio impostare i bit
RD0, RD1, RD2, RD3 come ingressi, mentre i bit RD4, RD5, RD6, RD7 come uscite, il
valore da scrivere nel registro TRISD nei vari formati numerici sarebbe il seguente:
// Numero binario
TRISD = 0b00001111;
// Numero esadecimale
TRISD = 0x0F;
// Numero decimale
TRISD = 15;

Vi sarete forse chiesti per quale ragione la PORTA stata impostata con tutti i pin
come ingresso, anche se in realt la PORTA non affatto utilizzata nel nostro
programma. La ragione solo a scopo precauzionale, infatti, compatibilmente con
l'hardware della scheda, bene porre un pin non utilizzato come ingresso in modo da
limitare problemi di cortocircuito accidentale91.
Tutte le porte seguono le impostazioni appena descritte per la PORTA, tranne la
PORTD, dal momento che presente il LED con il quale si vuole salutare il mondo. I pin
dei PIC possono pilotare senza problemi un LED standard fino a correnti massime di
20mA92. Per ragioni pratiche, quando devo impostare un bit noto a 0 o 1 preferisco la
rappresentazione binaria, visto che non necessario sapere il valore rappresentato dal
codice binario. In particolare per impostare RD0 come uscita si ha il seguente codice:
// Imposto PORTD tutti ingressi e RD0 come uscita
LATD = 0x00;
TRISD = 0b11111110;

In questo caso per bisogna fare attenzione che il numero totale di 0 e 1 sia 8, infatti
facile metterne uno 0 o 1 di troppo e in meno. Qualora si dovesse verificare questa
situazione, si hanno normalmente comportamenti strani, visto che il valore scritto nel
registro TRISx non quello che in realt si vuole.
Una volta inizializzati tutti i registri d'interesse viene acceso il LED ponendo ad 1 il bit del
91

92

Sebbene porre un pin come ingresso possa aiutare durante la fase di sviluppo per evitare dei cortocircuiti accidentali, qualora
il sistema sia progettato per applicazioni a basso consumo necessario che gli ingressi non siano floating ovvero lasciati
liberi. Questo pu infatti causare delle transazioni sporadiche del livello logico in ingresso, dovute al rumore, causando dei
consumi maggiori. Per tale ragione in questi casi si preferisce impostare un pin come uscita o come ingresso ma con
resistore di pull-up o pull-down, in maniera da tenere vincolato il livello logico in ingresso.
Si ricorda che, anche se possibile pilotare un LED direttamente con il pin di uscita del PIC, ogni porta di uscita ha un
limite massimo di corrente. Per maggiori informazioni sui limiti massimi di ogni porta si rimanda alle caratteristiche
elettriche descritte nel Datasheet del PIC utilizzato. In generale tale valore non supporta il caso limite in cui ogni pin si trova
a fornire il massimo della corrente. Oltre a questo limite bisogna anche osservare il limite massimo della corrente che
possono fornire rispettivamente le linee Vpp e Vdd (Vcc GND).

109

registro LATD, ovvero:


LATDbits.LATD0 = 1;

Un altro modo per accendere il LED, visto che il resto dei pin della PORTD impostato
come input, potrebbe essere:
LATD = 0x01;

Ogni porta oltre ad avere il registro LATx possiede una variabile particolare (una
struttura) nominata LATxbits che permette di accedere ogni singolo pin della porta stessa
con il suo nome. La struttura associata al registro LATx presente anche per gli altri
registri interni al PIC e torna utile quando si ha la necessit di leggere o scrivere singoli
bit93. Una volta acceso il LED, presente il seguente codice:
while (1){
}

Attualmente la spiegazione del suo significato un po' prematura, ma vi basti sapere


che l'istruzione rappresenta un ciclo infinito. Qualunque istruzione venisse messa
all'interno del blocco delle parentesi graffe, verrebbe ripetuta all'infinito. Dal momento
che non presente nessuna istruzione vuol dire che il PIC bloccato in un dolce far nulla.
Qualora non sia presente il ciclo infinito, il programma termina e si riavvia in
automatico. Istruzioni bloccanti di questo tipo sono anche necessarie quando si scrive un
programma in C in ambiente Windows. Qualora non si blocchi il programma per mezzo
per esempio di una lettura da tastiera (tipo, premi un tasto per continuare) il terminale
DOS verrebbe chiuso.
Per chi sa gi programmare in C potrebbe obiettare che le parentesi graffe non
servono, ed infatti vero, si pu anche scrivere:
while (1);

In generale preferisco comunque scrivere le parentesi in modo da mettere in evidenza


che il ciclo un blocco vuoto. Questo risulta molto utile per evitare brutte sorprese
quando si aggiunge del nuovo codice. Di questo si parler comunque in seguito, ma
rappresenta pi che altro una regola di programmazione generale e non associata ai PIC.
Un'ultima nota sullo stile di programmazione va data in merito ai commenti.
Frequentemente il commento viene visto sulla stessa riga del codice, tipo:
LATD = 0x00;

// Imposto PORTD tutti ingressi e RD0 come uscita

Nel seguito del testo i commenti sono invece scritti prima del codice, ovvero
// Imposto PORTD tutti ingressi e RD0 come uscita
LATD = 0x00;
93

Come detto il LED si sarebbe potuto accendere anche con il registro PORTD.

110

TRISD = 0b11111110;

Questo risulta particolarmente pratico poich permette al commento di seguire


l'indentazione del codice, rendendo il tutto pi leggibile. Questo secondo metodo
consigliato poich il pi utilizzato a livello internazionale. All'interno dei programmi scritti
per il Kernel Linux i programmatori preferiscono scrivere i commenti nel seguente modo:
/* Imposto PORTD tutti ingressi e RD0 come uscita */
LATD = 0x00;
TRISD = 0b11111110;

Riassunto
In questo capitolo si mostrato come creare il primo progetto utilizzando l'ambiente di
sviluppo MPLAB X. In particolare sono stati messi in evidenza le varie impostazioni
richieste per creare un progetto e come configurare il compilatore al fine di poter
utilizzare la configurazione CCI. Il primo programma di esempio stato mostrato con
molti dettagli anche se alcuni aspetti della programmazione sono stati rimandati nei
capitoli che seguiranno. Sebbene posto in secondo piano stato mostrato anche lo stile
di programmazione adottato nel testo, che si consiglia di utilizzare al fine di avere uno
stile di programmazione definito.

Punti chiave ed approfondimenti


CCI: Common C Interface, ovvero interfaccia C comune. Questa opzione permette di
abilitare una sintassi comune di programmazione tra le varie famiglie di microcontrollori
offerti dalla Microchip. La User Guide del compilatore XC8 ne consiglia l'uso.
Intel HEX: Rappresenta il formato esadecimale in formato Intel. Con tale formato viene
salvato di default il risultato della compilazione del programma ovvero il file .hex. Tale
formato, essendo uno standard di fatto, viene aperto da molte applicazioni.
File .hex: Tale file rappresenta il risultato della compilazione. Viene generato solo se il
programma non ha errori. In caso di Warning il file viene generato ma bisogna fare
attenzione che le Warning non siano un avviso di possibili malfunzionamenti. Il file .hex
rappresenta il programma in formato codice macchina e pu essere caricato all'interno del
microcontrollore per mezzo di una programmatore e relativa GUI.
Include Path: I percorsi da includere permettono al compilatore di trovare i vari file
inclusi nel progetto. In particolare devono essere impostati per permettere di trovare le
varie librerie utilizzate nel progetto.
direttiva: La direttiva un'istruzione per il precompilatore e non un'istruzione eseguita
dal microcontrollore. Vine introdotta con il carattere speciale #. Il compito delle direttive
quello di spianare la strada al compilatore, fornendo le informazioni relative ai percorsi
dove trovare i file, costanti e configurazioni del microcontrollore, oltre ad altre
111

informazioni e vincoli di compilazione.

Domande ed Esercizi
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.

Perch importante che un programma venga compilato senza Warning?


Aprire il file xc.h fino e cercare il punto di inclusione del file pic18f4550.h.
Che cosa sono le direttive?
Per quale ragione consigliabile impostare i pin non usati come ingressi?
Quali problemi si possono verificare in applicazioni a bassi consumi, qualora un pin sia
impostato come ingresso?
Come possibile evitare i problemi della domanda precedente?
Qual' la corrente massima che pu fornire il buffer di uscita del PIC18F4550?
Qual' il limite massimo di corrente che pu fornire una PORTA del PIC18F4550?
A cosa serve l'applicazione MPLAB IPE?
Compilare il programma riportato sotto. Commentare le dimensioni della memoria Flash
richiesta al fine di realizzare un programma che non fa nulla.
#include <xc.h>
#include PIC18F4550_config.h
int main (void){
}

112

Capitolo V

Simuliamo i nostri programmi


In questo Capitolo si introducono le basi per poter utilizzare il simulatore presente
all'interno dell'ambiente di sviluppo Microchip MPLAB X. Simulare pu risultare
particolarmente utile qualora si voglia vedere in dettaglio il comportamento del
microcontrollore durante l'esecuzione del programma. Il suo valore didattico inoltre
particolarmente interessante, visto che permette di programmare ed eseguire programmi
senza nemmeno avere una scheda di sviluppo. Il Capitolo estende poi la simulazione alla
fase di Debug, ovvero all'esecuzione del programma all'interno del microcontrollore e
mostra come esplorare il contenuto dei registri .

Schema elettrico
Per poter seguire gli esempi presentati in questo Capitolo, esclusa la parte della della
simulazione, in cui non necessario alcun hardware, richiesto realizzare su Breadboard
lo schema elettrico di Figura 49 o far uso delle schede di sviluppo impostate come
descritto nel paragrafo successivo.

Figura 49: Schema elettrico di esempio del Capitolo V.

113

Impostare le schede di sviluppo


Al fine di poter seguire gli esempi mostrati in questo Capitolo necessario impostare la
scheda di sviluppo utilizzata come descritto nei paragrafi seguenti.

Freedom II
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo, la
scheda di sviluppo Freedom II deve avere i Jumper posizionati nel seguente modo:

Figura 50: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

Freedom Light
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom Light deve avere i Jumper posizionati nel seguente modo:

Figura 51: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

La scheda Freedom Light possiede i LED solo sui pin RD0 e RD1. Per poter
visualizzare gli altri LED necessario montarne altri sul connettore di
espansione, facendo per esempio uso della scheda di espansione Breadboard
PJ7011 o limitare il Debug su due soli LED.

114

Impostazioni software
Gli esempi mostrati non richiedono alcuna impostazione software particolare oltre alla
semplice creazione di un progetto.
Gli esempi mostrati sono spiegati nel dettaglio, mostrando il codice Assembly,
per cui, a seconda delle impostazioni delle ottimizzazioni e della versione del
compilatore utilizzato, possono essere presenti delle differenze tra il codice
mostrato e quello che otterrete in fase di compilazione.

Fase di Simulazione e Debug


Ogni qual volta si voglia realizzare un programma, ci si trova ad eseguire una
successione di passi standard:

Analisi del problema.


Scrittura di un algoritmo.
Scrittura del codice che traduce l'algoritmo.
Compilazione del programma.
Test e Debug.

I vari punti, sebbene sequenziali possono essere ripetuti in maniera ciclica dopo una fase
di Test e Debug, ovvero di controllo. In particolare se qualcosa non dovesse funzionare ci
si ritrova a dover riscrivere il codice o rianalizzare anche il problema, per poi ripetere i
passi che seguono.
Indipendentemente da quanto tempo si sia passato ad analizzare il problema, si
possono verificare difficolt pi o meno banali che necessario risolvere. Avere una
scheda di sviluppo sulla quale caricare il programma certamente un aiuto (sempre che sia
possibile osservare fisicamente il problema). Nel caso in cui il problema sia difficilmente
osservabile, come per esempio una comunicazione seriale che non funziona, risolvere la
situazione pu risultare tutt'altro che banale. Gli approcci che vengono seguiti al fine di
risolvere questa tipologia di problemi possono essere diversi e l'esperienza del
programmatore gioca sicuramente un ruolo importante. Questa fase dello sviluppo, come
detto, prende il nome di Debug o Debugging, e mette a dura prova il programmatore. La
soluzione del problema richiede spesso aspetti multidisciplinari che vanno ben oltre la
semplice programmazione. Spesso l'esperienza pratica e l'utilizzo di strumenti di
laboratorio come il multimetro digitale e l'oscilloscopio, ritornano molto utili.
Tra gli strumenti per il Debug che la Microchip mette a disposizione, vi il Simulatore
MPLAB SIM che permette di emulare il microcontrollore a livello software, ovvero senza
aver bisogno di schede di sviluppo dove caricare il programma. Una seconda modalit per
investigare o comprendere un problema per mezzo della funzione di Debug offerta dai
Debugger, che permettono di accedere ai registri interni del PIC durante l'esecuzione del
programma. I programmatori Microchip della serie PICKIT 2, PICKIT 3, ICD 3 possono
tutti funzionare come Debugger oltre che come programmatori. Le performance dei vari
Debugger sono in generale proporzionali al costo dello stesso. I limiti sono associati al
116

numero di Breakpoint hardware che possono gestire e la velocit degli stessi.


Gli strumenti forniti dal Simulatore e Debug sono molteplici, ma nel seguente Capitolo
sono descritti solo quelli di uso pi frequente. Infatti il Simulatore se da un lato pu
aiutare a comprendere alcuni aspetti, pu creare dall'altro un'astrazione tale per cui realt e
simulatore non sono pi in accordo. Questo accade quando non si sono a conoscenza dei
limiti del simulatore ovvero non si conoscono tutte le specifiche e dettagli di ci che viene
effettivamente simulato.

Avvio di una sessione di Simulazione


Per poter avviare una simulazione software non c' bisogno della scheda di sviluppo
dove poter caricare il programma, bens di una semplice impostazione del progetto stesso.
In particolare come mostrato in Figura 52, il Simulatore pu essere selezionato al posto di
un programmatore, durante la fase di creazione del progetto stesso. Qualora il progetto
sia stato gi creato selezionando un programmatore, possibile passare dal
programmatore al Simulatore semplicemente per mezzo delle propriet del Progetto,
come stato mostrato nel Capitolo precedente.

Figura 52: Selezione del Simulatore durante la creazione del Progetto.

A questo punto potete creare un nuovo progetto e creare un file main all'interno del
quale copiare il codice sorgente che segue. L'esempio leggermente pi complesso del
precedente ed in particolare si sono introdotti alcuni concetti nuovi che per il momento
non necessario spiegare nel dettaglio e spero che il semplice commento possa chiarire le
prime domande. Il programma introduce all'inizio della funzione main delle variabili
117

mentre prima del ciclo infinito while introduce un conteggio for che permette di contare,
al fine di perdere tempo, ovvero abbiamo creato una pausa.
#include <xc.h>
#include "PIC18F4550_config.h"
int main (void){
// Variabili per memorizzare dei dati/informazioni
unsigned char mia_variabile = 0x55;
unsigned char mia_stringa [] = "LaurTec";
unsigned int contatore = 0;
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD come porta di uscita
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
LATD =

mia_variabile;

// Piccola pausa in cui il microcontrollore conta


// perdendo solo tempo
for (contatore=0; contatore<20000; contatore++) {
}
// Ciclo infinito
while (1) {
}
}

Creato il programma possibile avviare la fase di Simulazione premendo il tasto


Debug:

Alla pressione del tasto viene compilato il programma, il quale, qualora non siano presenti
errori di compilazione, viene avviato in automatico. Se tutto andato a buon fine
compare la barra degli strumenti aggiuntiva mostrata in Figura 53 (i tasti abilitati possono
118

differire).

Figura 53: Strumenti di Simulazione.

A partire dalla sinistra le funzioni dei vari tasti sono:

Finish Debugging Session: Il tasto permette di terminare la sessione di Debug,


chiudendo in particolare la relativa barra degli strumenti.

Pause: Il tasto di permette di mettere in Pausa l'esecuzione del simulatore. Il


valore del Program Counter e dei registri rappresenta lo stato corrente della CPU.

Reset: Il tasto permette di resettare il simulatore ripristinando il Program Counter al


Reset Vector, ovvero l'esecuzione delle istruzioni riprende dalla prima. I registri
interni sono inoltre impostati al valore di Default come indicato nel Datasheet.

Continue: Il tasto permette di eseguire in maniera continua il programma.


L'esecuzione piuttosto rapida per cui torna utile solo qualora si siano impostati
dei Breakpoint (punti di blocco/interruzione) e non si voglia premere il tasto di
avanzamento di un'istruzione alla volta.

Step Over: Il tasto permette di eseguire delle funzioni 94 in maniera trasparente,


come se fossero delle singole istruzioni.

Step Into: Il tasto permette di eseguire le funzioni entrando nelle stesse e


proseguendo la simulazione istruzione per istruzione.

Run to Cursor: Il tasto permette di eseguire il programma fino alla posizione del
cursore del mouse, preventivamente posizionato in un'istruzione d'interesse nel
programma.

Set PC at Cursor: Il tasto permette di posizionare il Program Counter al valore


d'indirizzo rappresentato dalla posizione del cursore nel programma stesso, ma
solo se precedenti all'istruzione stessa (permette solo salti a gambero).

Focus Cursor at PC: Posiziona il cursore del mouse alla posizione attuale del PC.

Dal momento che l'esecuzione del programma si avvia in automatico con la fase di
Debug, ed piuttosto veloce, non ci si rende conto di nulla. Per tali ragioni, la
simulazione di un programma viene normalmente eseguita passo passo, ovvero
un'istruzione alla volta. Per fare questo necessario prima arrestare la simulazione in
corso premendo il tasto Pause:
94

Una funzione rappresenta un insieme di istruzioni le quali possono essere richiamate per mezzo di un nome. Maggiori
dettagli sono dati in seguito chiarendo e definendo meglio il tutto, visto che una funzione certamente di pi che un
semplice insieme di istruzioni.

119

Successivamente bene riavviare il simulatore resettando la CPU, premendo il tasto


Reset:

Il tasto Reset permette, come detto, di posizionare nuovamente il Program Counter al


valore del Reset Vector, ovvero di riavviare la simulazione a partire dalla prima istruzione
del nostro programma. I registri interni sono inoltre impostai al valore di Default come
indicato nel Datasheet.
Durante la fase di Simulazione e Debug vengono offerti diversi strumenti utili per
mezzo dei quali possibile vedere il valore delle variabili e registri all'interno del
microcontrollore, permettendo dunque di individuare eventuali anomalie. La descrizione
su come utilizzare ed impostare tali funzioni descritta a breve dal momento che
praticamente comune anche alla modalit di Debug.

Avvio di una sessione di Debug


Per poter avviare una sessione di Debug, diversamente dalla simulazione Software,
necessario possedere una scheda di sviluppo e di un Debugger compatibile con l'ambiente
di sviluppo Microchip. A questo punto, se siete stati ostinati a non prendere strumenti
compatibili Microchip non avrete modo di testare la modalit di Debug. Nella discussione
che segue si preso come esempio il Debugger PICKIT 3 ma altri Debugger lavoreranno in
maniera analoga.
Una volta alimentata la scheda di sviluppo e collegato il Debugger, al fine di avviare una
sessione di Debug, basta selezionare lo strumento di sviluppo PICKIT 3 nella finestra
delle propriet del progetto e premere ok. Successivamente premere il tasto Debug Main
Project dalla barra degli strumenti:

A questo punto il programma viene compilato e caricato all'interno del microcontrollore.


Diversamente da una programmazione standard, in fase di Debug si ha la possibilit di
comunicare con il microcontrollore, prelevando informazioni importanti durante
l'esecuzione del programma stesso.
La barra degli strumenti, in fase di Debug, fornisce gli stessi strumenti offerti da una fase
di simulazione, in particolare permette di eseguire il programma passo passo.
In Figura 54 mostrata la barra degli strumenti in fase di Debug.

Figura 54: Strumenti di Debug.

120

In particolare si noti che nella finestra Dashboard sono presenti le seguenti informazioni
relative alla fase di Debug:

Figura 55: Risorse assegnate al Debugger.

Queste mostrano il numero di Breakpoint (BP) utilizzati (il numero di quelli disponibili
viene a dipendere dal Debugger utilizzato). In particolare interessante notare come alcune
risorse del sistema siano dedicate al Debug, ovvero 32 Byte di RAM e 577 Byte di
memoria Flash. Questo significa che in applicazioni particolarmente grandi, qualora
stessimo utilizzando tutta o quasi la memoria RAM o Flash, potremmo trovarci nella
situazione di non poter effettuare il Debug a causa dell'impossibilit di poter assegnare
risorse al Debugger. Negli esempi che seguiranno questo non un problema visto le ridotte
dimensioni degli stessi. Si ricorda nuovamente che quanto verr detto a breve per la fase
di Debug vale anche per la fase di simulazione, tranne alcune eccezioni che sono messe in
evidenza.

Il codice Assembly
Durante la fase di Debug, soprattutto quando la nostra esperienza aumenter, torna
particolarmente utile poter visualizzare il codice Assembly, ovvero il codice generato dal
compilatore quale traduzione del nostro programma scritto in C. Ogni passo eseguito nel
codice sorgente si traduce in uno o pi istruzioni Assembly. Poter esplorare il codice
Assembly particolarmente utile anche in fase di ottimizzazione, al fine di valutare se
cambiando il nostro codice C si riscontrano differenze nel numero di istruzioni Assembly
utilizzate. Negli esempi mostrati nei prossimi Capitoli il codice Assembly generato dal
compilatore spesso utilizzato nella spiegazione delle varie istruzioni al fine di mostrare il
comportamento del compilatore. Per visualizzare il codice Assembly basta andare al menu:
Window Output Disassembly Listing File

Nel caso specifico del nostro esempio, il codice Assembly che si ottiene :
1:
2:
3:
4:
5:
6:
7:
8:
9:

#include <xc.h>
#include "PIC18F4550_config.h"
int main (void){
// Variabili per memorizzare dei dati/informazioni
unsigned char mia_variabile = 0x55;

121

7CBC
7CBE
7CC0
7CC2
10:
7CC4
7CC6
7CC8
7CCA
7CCC
7CCE
7CD0
7CD2
7CD4
11:
12:
13:
7CD6
7CD8
14:
7CDA
15:
16:
17:
7CDC
7CDE
18:
7CE0
19:
20:
21:
7CE2
7CE4
22:
7CE6
23:
24:
25:
7CE8
7CEA
26:
7CEC
7CEE
27:
28:
29:
7CF0
7CF2
30:
7CF4
31:
32:
7CF6
7CF8
33:
34:
35:
36:
7CFA
37:
7CFC
38:

6E01
0E55
6E0A
5001
EE20
F00B
EE10
F002
0E08
CFDE
FFE6
2EE8
D7FC

0E00
6E89
6892

0E00
6E8A
6893

0E00
6E8B
6894

0E00
6E8C
0E00
6E95

MOVWF 0x1, ACCESS


MOVLW 0x55
MOVWF mia_variabile, ACCESS
MOVF 0x1, W, ACCESS
unsigned char mia_stringa [] = "LaurTec";
LFSR 2, 0xB
NOP
LFSR 1, 0x2
NOP
MOVLW 0x8
MOVFF POSTINC2, POSTINC1
NOP
DECFSZ WREG, F, ACCESS
BRA 0x7CCE
// Imposto PORTA tutti ingressi
LATA = 0x00;
MOVLW 0x0
MOVWF LATA, ACCESS
TRISA = 0xFF;
SETF TRISA, ACCESS
// Imposto PORTB tutti ingressi
LATB = 0x00;
MOVLW 0x0
MOVWF LATB, ACCESS
TRISB = 0xFF;
SETF TRISB, ACCESS
// Imposto PORTC tutti ingressi
LATC = 0x00;
MOVLW 0x0
MOVWF LATC, ACCESS
TRISC = 0xFF;
SETF TRISC, ACCESS
// Imposto PORTD come porta di uscita
LATD = 0x00;
MOVLW 0x0
MOVWF LATD, ACCESS
TRISD = 0x00;
MOVLW 0x0
MOVWF TRISD, ACCESS

6896

// Imposto PORTE tutti ingressi


LATE = 0x00;
MOVLW 0x0
MOVWF LATE, ACCESS
TRISE = 0xFF;
SETF TRISE, ACCESS

C00A
FF8C

LATD = mia_variabile;
MOVFF mia_variabile, LATD
NOP

0E00
6E8D

908C
808C

// Ciclo infinito
while (1) {
LATDbits.LATD0 = 0;
BCF LATD, 0, ACCESS
LATDbits.LATD0 = 1;
BSF LATD, 0, ACCESS
}

122

39:
40:

possibile notare che il codice C riportato tra il codice Assembly, seguito dalla
traduzione dello stesso. Sulla sinistra presente il valore dell'indirizzo di memoria in cui
salvata una determinata istruzione, ovvero rappresenta i vari valori che assume il Program
Counter durante l'esecuzione del programma.
La finestra appena visualizzata sebbene visualizzi il codice, non rappresenta altro che la
visualizzazione del file di output generato durante la fase di compilazione, che possibile
trovare tra i vari file nella cartella del progetto.
Durante la fase di Debug, volendo eseguire il codice e visualizzare la particolare istruzione
in esecuzione, bisogna aprire il codice Assembly dal menu:
Window Debugging Disassembly

La finestra con il codice Assembly pu essere spostata (trascinata) come in Figura 56, al
fine di visualizzarla in contemporanea al codice sorgente C.
Nel caso particolare mostrata una fase di Debug gi avviata in cui alcune istruzione
sono state gi eseguite. In particolare la riga selezionata rappresenta l'istruzione che verr
eseguita al prossimo passo e viene visualizzata sia nel codice C che nel codice Assembly. Si
noti che nel codice Assembly l'istruzione TRISD=0x00 tradotta in due passi, ovvero due
istruzioni base. Prima si carica il valore 0x00 nell'accumulatore (Registro W) e poi si carica
l'accumulatore nel registro TRISD. Queste due istruzioni sono rispettivamente
all'indirizzo 0x7CEC e 0x7CEE. In alto a destra dell'immagine possibile notare che il
Program Counter posizionato proprio sull'indirizzo 0x7CEC. Premendo nuovamente il
tasto Step Over per eseguire una nuova istruzione, si ha che il PC si posiziona su 0x7CF0,
ovvero all'indirizzo associato alla nuova istruzione LATE=0x00. Questo mostra che
l'eseguire una istruzione alla volta in C spesso tradotta in pi istruzioni Assembly, ma il
tutto trasparente.

123

Figura 56: Finestra con il codice Assembly.

Utilizzo dei Breakpoint


Eseguire il Debug di un programma significa spesso doverlo eseguire passo passo, per
cui la funzione Step Over torna molto utile. Ciononostante, in programmi molto grandi si
potrebbe non aver interesse ad eseguire ogni istruzione ma ma piuttosto partire da un
determinato punto d'interesse. Per fare questo si hanno due modi, o si posiziona il cursore
sul punto d'interesse e si preme il tasto Run To Cursor:

o si posiziona un Breakpoint (punto di arresto) nel punto del programma in cui si vuole che
l'esecuzione si arresti in attesa di un nuovo comando.
Per posizionare un Breakpoint basta cliccare con il tasto sinistro del mouse sul numero
della riga d'interesse, come mostrato in Figura 57. La riga con il Breakpoint viene
124

evidenziata in rosso e un quadrato rosso al posto del numero della riga stessa.

Figura 57: Posizionamento di un Breakpoint.

A questo punto se si continua a premere il tasto Step Over, il programma continua un


passo alla volta come se il Breakpoint non sia presente, ma se si preme il tasto Continue:
che normalmente esegue il codice in maniera continua fino alla pressione del tasto Pause, il
programma si arresta una volta giunto al Breakpoint, ovvero senza dover premere il tasto
Pause. In particolare il Program Counter si posiziona all'istruzione successiva da eseguire,
come riportato in Figura 58, ovvero quella puntata dal Breakpoint.

Figura 58: Esecuzione del programma fino al Breakpoint.

Nel caso specifico il Breakpoint usato solo per eseguire poche istruzioni e non
rappresenta il caso tipico di utilizzo. Come detto la funzione Run to Cursor avrebbe
funzionato altrettanto bene. Un utilizzo importante del Breakpoint nell'arrestare il
programma quando si verifica una determinata condizione o un determinato codice viene
eseguito. Supponiamo per esempio che il programma svolge delle determinate funzioni,
quali per esempio il controllo della luce ambientale. Qualora dovesse essere premuto un
pulsante si potrebbe per esempio voler accendere una luce. Supponiamo che durante i
nostri test ci accorgiamo che alla pressione della luce, invece di accendere la luce ne
accendiamo un'altra. In questo caso potrebbe essere interessante mettere un Breakpoint
all'inizio del codice del programma che viene eseguito alla pressione del tasto e da quel
punto in poi eseguire passo passo per vedere il contenuto dei registri e variabili di sistema.
A seconda dell'ambiente di sviluppo utilizzato, ad un Breakpoint possono essere
associate anche altre funzioni, che non necessariamente implicano l'arresto dell'esecuzione
del programma. Alcune funzioni interessanti potrebbero essere il dump (salvataggio) dei
125

registri interni o intervallo di memoria, ogni qual volta viene incontrato il Breakpoint,
salvando il tutto in un file di testo (nel caso di MPLAB X questa funzione non
presente). Per vedere le propriet del Breakpoint basta posizionarsi con il mouse sul
quadrato rosso del Breakpoint e premere il tasto destro del mouse. Selezionare poi la voce:
Breakpoint Properties

La finestra di Figura 59 mostra che la propriet di Default quella di arrestare il


programma (Always Break) ma anche presente la propriet Event must Occur Count Times,
ovvero l'evento deve avvenire un numero di volte pari a Count, che pu essere selezionato
sotto nella casella di Testo, come mostrato in Figura 60.

Figura 59: Propriet del Breakpoint.

Figura 60: Propriet del Breakpoint.

Ogni volta che si inseriscono i Breakpoint possibile vedere che il numero mostrato
nella Dashboard aumenta. Inserendo l'ultimo dei 3 disponibili, viene mostrato un avviso
come in Figura 61, in cui si avvisa che l'ultimo disponibile sta per essere usato. Nel caso
di una simulazione non sono presenti questi limiti.

126

Figura 61: Avviso di utilizzo dell'ultimo Breakpoint.

Qualora sia necessario un altro Breakpoint, necessario eliminarne uno o disabilitarlo.


Per eliminarlo basta cliccare sul quadrato rosso, mentre per disabilitarlo basta cliccare con
il tasto destro sul quadrato rosso del Breakpoint e selezionare la voce
Breakpoint Enabled

Un Breakpoint disabilitato viene visualizzato in grigio. In Figura 62 mostrato un esempio


in cui sono presenti alcuni Breakpoint attivi (Rossi) e alcuni Breakpoint disattivi (Grigi) 95.
Qualora si stia lavorando su di progetto particolarmente esteso e i Breakpoint sono
posizionati su vari file, pu tornare utile una visione d'insieme. Questa pu essere
visualizzata per mezzo del menu:
Window Debugging Breakpoints

Il dettaglio dei Breakpoint disponibili mostrato in Figura 63.

Figura 62: Esempio di Breakpoint multipli (abilitati e disabilitati).


95

Si fa notare che l'esempio un po' limite visto l'uso ravvicinato degli stessi Breakpoint.

127

La Figura 63 oltre a mostrare la lista dei Breakpoint disponibili permette di attivarli e


disattivarli con un click. In particolare si noti come il quarto Breakpoint (Disattivo) ha
scritto
Not resolvable to a valid memory address

Questo significa che il Breakpoint non pu essere tradotto in un indirizzo di memoria


valido. Infatti dalla Figura 62 si pu vedere che il Breakpoint non stato posizionato su
un'istruzione ma su un commento. Questo si traduce nel fatto che il compilatore, per
quella riga di codice non associa nessuna istruzione Assembly, per cui non presente alcun
indirizzo di memoria Flash valido.

Figura 63: Vista dei Breakpoint utilizzati nella fase di Debug.

L'importanza di avere un indirizzo valido risiede nel funzionamento del Breakpoint


stesso che altro non che un controllo sul Program Counter. Quando infatti posizioniamo
un Breakpoint, quello che facciamo dire: quando il Program Counter raggiunge questo
indirizzo esegui l'azione associata al Breakpoint.
In alcuni ambienti di lavoro possibile anche posizionare dei Breakpoint all'accesso di un
determinato indirizzo di memoria o variabile, ovvero controllando l'Address bus associato
alla memoria RAM e non solo quello della memoria Flash del microcontrollore 96.

Controllo delle variabili e registri


Poter eseguire il programma passo passo e bloccarlo a nostro piacimento sicuramente
utile, ma senza vedere i contenuti delle variabili e registri interni al PIC, quanto fin ora
spiegato non avrebbe alcuna utilit. Una delle funzioni principali della sessione di
simulazione software e di Debug, proprio quella di poter scrutare i contenuti dei registri
e delle variabili definite nel nostro programma. In questo modo possibile capire per
quale ragione un LED non si accende o per quale ragione una periferica non sta
funzionando correttamente. Uno dei modi pi pratici per la visualizzazione delle variabili
per mezzo della finestra di Watches, visualizzabile dal men:
Window Debugging Watches

Un esempio di finestra Watches in cui viene visualizzato il registro LATD riportato in


Figura 64.

96

Nel caso dei microcontrollori MSP430, vista l'architettura di von Neumann e la presenza di un solo bus degli indirizzi, i
Breakpoint sono piuttosto flessibili ed offrono indifferentemente un controllo sull'accesso di un indirizzo in memoria Flash,
RAM (variabili) o di un registro delle impostazioni.

128

Figura 64: Finestra di visualizzazione delle variabili Watches.

Per mezzo del tasto destro del mouse e il menu Add Watch, possibile inserire una
qualunque variabile dichiarata nel programma o altro registro interno, come riportato in
Figura 65, basta infatti scrivere il nome del registro o variabile all'interno della casella di
testo.

Figura 65: Finestra per aggiungere una variabile nella finestra Watch.

Il contenuto della variabile o registro viene visualizzato all'interno della colonna Value.
In particolare il suo valore espresso in esadecimale. Per visualizzare altri formati
possibile cliccare il tasto destro con il mouse sopra la il nome Value, ottenendo la finestra
di Figura 66. Per mezzo di questa finestra possibile selezionare le colonne da aggiungere.

Figura 66: Finestra d'impostazione del formato delle variabili visualizzate.

129

Un esempio di visualizzazione con pi formati riportato in Figura 67.

Figura 67: Esempio in cui sono mostrati vari formati.

La colonna Value, diversamente dalle colonne che hanno il nome del formato nella
colonna stessa (Char, Hexadecimal, Decimal, Binary) pu mostrare un qualunque formato.
Per selezionare quello di maggior interesse basta cliccare con il tasto destro sulla tabella e
selezionare Display Value Columns As, e selezionare il valore d'interesse.
Oltre alla finestra Watch possibile aprire anche la finestra Variables, sempre dal menu
Debugging, ma il risultato praticamente lo stesso della finestra Watches. Un modo molto
semplice per aggiungere delle variabili, piuttosto che scrivere il nome nella tabella Watches,
quello di porre il cursore del mouse sulla variabile scritta nel codice sorgente e con il
tasto destro del mouse selezionare la voce
New Watch...

Un esempio in cui si sono aggiunte le variabili mia_variabile e mia_stringa riportato in


Figura 68. possibile notare che un array, insieme di pi variabili dello stesso tipo, pu
essere visualizzato nel suo insieme o come variabile singola. In particolare la singola
variabile coincide in questo esempio con un singolo carattere. La stringa ha alla sua fine
anche un carattere speciale che non abbiamo aggiunto, ovvero \0, che rappresenta un
indicazione di fine stringa. Maggiori dettagli a riguardo saranno dati quando si parler
delle stringhe.

Figura 68: Finestra Watches nel caso di un Array.

E' importante notare che all'interno della tabella Watches possibile aggiungere molte
variabili e monitorarle in contemporanea. Il valore delle variabili viene aggiornato ad ogni
130

esecuzione di un passo della Simulazione o Debug. Le variabili che subiscono un


cambiamento di valore sono colorate di rosso, in maniera da facilitarne l'individuazione.

Controllo della memoria


Abbiamo gi visto come durante la fase di Debug torni utile controllare il contenuto
dei registri. Oltre a questi spesso richiesto il controllo della memoria, sia essa RAM,
Flash o EEPROM. Gli strumenti di controllo della memoria, permettono indirettamente
il controllo dei registri, visto che quest'ultimi si trovano comunque in memoria RAM. Le
varie visualizzazioni della memoria possono essere selezionate per mezzo del menu:
Window PIC Memory Views

In particolare possibile selezionare:

Program Memory
File Registers
SFRs
Configuration Bits
EE Data Memory
User ID Memory

In Figura 69 riportata una visualizzazione della memoria Flash, in concomitanza ad una


esecuzione del programma passo passo.

Figura 69: Esempio di visualizzazione della memoria Flash.

131

Si noti che l'indirizzo della memoria Flash viene selezionato sia nella finestra Assembly che
nella finestra della memoria Flash.

Analisi temporale
In alcune applicazioni torna utile poter eseguire un determinato insieme di istruzioni in
un tempo noto o inferiore a quanto richiesto da una determinata specifica di progetto.
Queste applicazioni ed esigenze normalmente non rientrano nelle applicazioni degli
esempi base mostrati in questo testo, ma comunque bene tenerlo a mente per sviluppi
futuri.
Dal momento che il tempo di esecuzione viene a dipendere dal clock utilizzato, si
capisce che necessario impostare tale parametro all'interno dell'ambiente di sviluppo.
Questo possibile farlo selezionando il Simulatore tra gli strumenti di sviluppo del
progetto, ovvero nelle propriet del progetto. Selezionato il Simulatore possibile
selezionare la voce Simulator come riportato in Figura 70.

Figura 70: Finestra di dialogo per le impostazioni della frequenza di clock.

Da questa finestra possibile impostare la frequenza del Clock del ciclo istruzioni,
ovvero pari a quella del clock diviso 4, che nel caso di Freedom II pari a 5MHz, ovvero
con un clock pari a 20MHz. importante notare come in questo caso si sia considerato
20MHz come il cristallo esterno, ma bene notare che la frequenza di clock potrebbe non
coincidere con quella del cristallo. In particolare il clock utilizzato per l'esecuzione delle
istruzione potrebbe provenire dall'uscita del PLL che in generale ha una frequenza pi alta
del cristallo. Nel valutare la frequenza di Clock bisogna anche tenere a mente eventuali
132

divisioni effettuate da un pre-scaler o post-scaler. Per tale ragione bisogna fare attenzione
alle impostazioni del modulo associato al Clock, il quale differisce in base al modello del
PIC utilizzato. Detto questo, impostati i nostri 5MHz, possibile premere ok.
A questo punto si pu avviare il nostro cronometro dal menu:
Debugging Stopwatch

Si possono poi posizionare due Breakpoint nei punti tra cui si vuole misurare il tempo di
esecuzione del programma ed avviare la simulazione. Il Programma si arresta al
raggiungimento del primo Breakpoint come mostrato in Figura 71.

Figura 71: Esempio di utilizzo dello strumento Stopwatch.

Lo strumento Stopwatch mostra il tempo di esecuzione fino a quel punto, come


mostrato in Figura 72.

Figura 72: Esempio di avvio del programma e prima misura.

Dalla finestra possibile vedere che sono tate eseguite 126 istruzioni e il tempo di
esecuzione con un clock di 5MHz (riportato in basso a destra), pari a 25.2us. Premendo
il tasto:
133

possibile azzerare il cronometro, prima di riavviare il programma e misurare il tempo di


esecuzione tra i due Breakpoint. Premendo nuovamente il tasto di Debug Continue:

possibile far arrestare il Program Counter sul secondo Breakpoint. A questo punto il
nostro strumento Stopwatch visualizza il nuovo tempo di esecuzione, come riportato in
Figura 73.

Figura 73: Esempio di misura dello strumento StopWatch.

In particolare il tempo di esecuzione mostra che sono stati impiegati 2.2us per eseguire
11 istruzioni. Controllando il codice Assembly si pu vedere che effettivamente tra il
primo Breakpoint ed il secondo sono appunto eseguite 11 istruzioni.
!// Imposto PORTA tutti ingressi
!LATA = 0x00;
0x7FD6: MOVLW 0x0
0x7FD8: MOVWF LATA, ACCESS
!
TRISA = 0xFF;
0x7FDA: SETF TRISA, ACCESS
!// Imposto PORTB tutti ingressi
!
LATB = 0x00;
0x7FDC: MOVLW 0x0
0x7FDE: MOVWF LATB, ACCESS
!
TRISB = 0xFF;
0x7FE0: SETF TRISB, ACCESS
!// Imposto PORTC tutti ingressi
!
LATC = 0x00;
0x7FE2: MOVLW 0x0
0x7FE4: MOVWF LATC, ACCESS
!
TRISC = 0xFF;
0x7FE6: SETF TRISC, ACCESS
!// Imposto PORTD come porta di uscita
!
LATD = 0x00;
0x7FE8: MOVLW 0x0
0x7FEA: MOVWF LATD, ACCESS
!
TRISD = 0x00;
0x7FEC: MOVLW 0x0
0x7FEE: MOVWF TRISD, ACCESS

134

In particolare il conteggio delle istruzioni Assembly deve essere effettuato tra:


0x7FD6: MOVLW 0x0

e
0x7FEA: MOVWF LATD, ACCESS

L'esempio appena mostrato non mette in evidenza l'utilit dello strumento, infatti per
sole 11 istruzioni si potrebbe fare un conteggio manuale. Nel caso di codice di
programma pi lunghi questo potrebbe non essere pi pratico per cui lo strumento
StopWatch di particolare importanza. Un utile esercizio potrebbe essere svolto
posizionando un altro Breakpoint dopo il ciclo for e vedere come il numero delle
istruzioni eseguite e il tempo di esecuzione aumentano anche se apparentemente il
numero di righe di codice eseguite non sono molte.
A solo scopo informativo si fa presente che il tempo di esecuzione del programma
d'interesse viene a dipendere anche dalla versione del compilatore e dal livello di
ottimizzazione utilizzato. Infatti in ambedue i casi si potrebbero avere dei codici
Assembly differenti da quello mostrato, per cui il numero di istruzioni da eseguire sarebbe
diverso. Questa una ragione per cui in applicazioni sensibili al tempo di esecuzione del
programma, come per esempio un sistema operativo, si garantisce il funzionamento dello
stesso per determinate versioni del compilatore e livelli di ottimizzazione.
Frequentemente per rendere il codice sorgente indipendente dal compilatore e livello di
ottimizzazione, si scrivono delle parti di codice direttamente in Assembly, in maniera da
impedire ottimizzazioni da parte del compilatore. A seconda dell'ambiente di sviluppo e
compilatore, potrebbero essere presenti anche altre possibilit per impedire al compilatore
di effettuare ottimizzazioni in particolari parti del codice.
Un piccolo esempio di ottimizzazione si pu notare anche nel codice di esempio appena
mostrato. Infatti per impostare a 0xFF il registro TRISB (ovvero tutti ingressi), viene
eseguita una sola istruzione, sfruttando l'istruzione SETF, che permette appunto di settare
un determinato registro a 0xFF, in questo caso TRISB.
!
TRISB = 0xFF;
0x7FE0: SETF TRISB, ACCESS

Per impostare il registro LATB a 0x00 viene invece eseguito il seguente codice:
!
LATB = 0x00;
0x7FDC: MOVLW 0x0
0x7FDE: MOVWF LATB, ACCESS

Ovvero viene caricato il valore 0x00 nell'accumulatore e successivamente caricato il valore


dell'accumulatore in LATB. Mentre il codice si sarebbe potuto ottimizzare con una sola
istruzione facendo uso di CLRF, ovvero:
!
LATB = 0x00;
0x7FDC: CLRF LATB, ACCESS

135

per cui il tempo di esecuzione a parit di codice C sarebbe cambiato.

Semplici regole di programmazione


Gli strumenti che sono messi a disposizione per il Debug sono certamente potenti,
affiancati ad una buona esperienza e degli strumenti di misura non ci sar problema che
non potrete risolvere. Sebbene questa frase sia incoraggiante, non significa che bisogna
sbattere necessariamente la testa contro il muro o scottarsi. Alcune volte con le giuste
accortezze possibile evitare gli ostacoli. Risolvere un problema porta soddisfazione ma
porta altrettanta frustrazione prima che la soddisfazione possa arrivare. Allontanandosi da
aspetti filosofici del problema, vediamo qualche buon consiglio per limitare il tempo che
bisogna passare per il Debug di un sistema.
Abbiamo gi visto che la fase di sviluppo di un programma suddiviso in varie passi.

Analisi del problema


Scrittura di un algoritmo
Scrittura del codice che traduce l'algoritmo
Compilazione del programma
Test e Debug

I vari passi presuppongono che l'hardware sia stato gi sviluppato altrimenti come
primo passo sar presente lo sviluppo dell'hardware. Qualora l'hardware non sia ancora
testato sempre bene procedere per passi e realizzare dei piccoli programmi di test che
permettano di testare i singoli componenti e funzioni che l'hardware supporta. E' inutile
iniziare con il programma completo che include tutte le funzioni, altrimenti le variabili in
gioco che possono nascondere un problema sono troppe. Quando si ha un hardware che
funziona ci si pu concentrare sul solo software, anche se in un sistema embedded, sia
l'hardware che il software possono nascondere problemi allo stesso tempo. Per tale
ragione, sebbene si abbia un hardware testato non sottovalutare mai che il problema possa
essere anche hardware.
Quando si inizia lo sviluppo del software non bisogna mai partire in quinta o si rischia
di perdere qualche pezzo che poi difficile ritrovare. Bisogna come prima cosa iniziare a
programmare con carta e penna e non con monitor e tastiera. Una volta scritto il nostro
programma in maniera astratta e scorporate le varie funzioni, possibile entrare nel
dettaglio di ogni funzione e scrivere i dettagli di quello che deve fare ognuna di esse.
Una volta individuati i vari blocchi e scritto l'algoritmo nero su bianco, possibile iniziare
a programmare.
Quando si scrive il programma bene testarlo per pezzi, in maniera progressiva. Una
semplice compilazione aiuta a ripulire il programma da semplici errori di battitura.
Il programma deve essere compilato sempre senza errori e senza Warning. Ogni
Warning, se presente, deve essere compresa al fine di essere certi che non possa causare
problemi al programma. Un modo per rinforzare la confidenza che il programma
136

robusto pu essere fatto ricompilando il programma abbassando la soglia di Warning da 0


a -1. La soglia di Warning pu essere cambiata dalla finestra delle Propriet del programma
come mostrato in Figura 74. Di Default vale 0 ma pu essere cambiata da -9 a 9.
Normalmente scendere sotto il livello -2 mostra molte Warning anche da parte delle
librerie Microchip. Le librerie LaurTec sono per esempio compilate con livello Warning
-1.

Figura 74: Cambio del livello di Warning.

Oltre a queste regole, basate sull'affidarsi al compilatore e la sua capacit di trovare


errori, bene scrivere il programma sempre in maniera ordinata, permettendo anche ai
nostri occhi di poter scrutare il codice in maniera appropriata.
In progetti software sviluppati da pi programmatori sono frequentemente presenti
delle regole da rispettare per quanto riguarda lo stile di programmazione, e vanno sotto il
nome di Coding Stile. Questo permette ad ogni sviluppatore di poter leggere il codice di
altri e avere la sensazione di averlo scritto lui stesso. Oltre a semplici regole di scrittura il
codice pu essere sottoposto ad ulteriori regole derivanti da standard di programmazione.
Per esempio per lo sviluppo di applicazioni in cui la sicurezza importante, spesso si fa
controllare il codice da un parser che controlla lo stesso garantendo che siano rispettate
determinate regole. In particolare tra gli standard pi noti presente l'insieme di regole
MISRA-C: 2004 redatte da Motor Industry Software Reliability Association (MISRA).
Altri test che possono essere eseguiti sono per esempio di tipo statico ed in particolare tra
quelli pi noti sono presenti le applicazioni lint e splint che prendono in pasto i nostri file
C e controllano gli stessi per potenziali vulnerabilit ed errori di programmazione.
Normalmente lint e splint sono utilizzati durante la programmazione di sistemi pi
complessi, per cui non entrer nel dettaglio degli stessi.
Una volta compilato il programma con successo possibile programmare il
microcontrollore e testare le varie funzioni implementate nell'applicazione. Al fine di
rendere robusto il test bene avere le idee chiare di cosa testare prima ancora di iniziare a
137

testare! Sebbene possa essere esagerato, in realt avere un piano sistematico su cosa
testare pu aiutare molto. In molti esempi semplici non serve una pianificazione ma
quando il sistema diviene complesso bene creare una tabella in cui scrivere la serie di
test da eseguire e mettere lo spazio con le croci ad indicare che il test stato passato con
successo. Quanto detto, sebbene accennato in fretta lo si comprender meglio durante la
lettura del testo, e quando farete vostre queste regole, vedrete che i programmi saranno
pi robusti e il tempo che passerete ad effettuare il Debug di sistema, alla ricerca di
problemi, si ridurr.

Districarsi tra il codice


Un codice ben scritto aiuta la navigazione e comprensione dello stesso ogni qual volta
ci dovessimo trovare a dover risolvere dei problemi. Sebbene quanto si dir non
appartiene realmente al Debug, essere a conoscenza di cosa mette a disposizione
l'ambiente di sviluppo al fine di navigare facilmente nel codice e apportare le modifiche
allo stesso, pu tornare utile. In particolare vedremo brevemente i seguenti strumenti:

Graph
Refactoring
Local History

Un progetto, quando di grande dimensioni, pu richiedere il salto da una pagina del


codice ad un'altra. Ricordarsi dove sia definita o implementata una funzione pu essere
difficile, ma grazie agli strumenti integrati nell'IDE si pu avere una vita facile.
Spesso quando si ha interesse nel vedere dove sia definita una funzione o una variabile,
basta posizionarsi con il mouse sul nome della stessa, premendo il tasto destro,
selezionare dal menu:
Navigate Go to Decleration
Navigate Go to Definition

Per mezzo di queste funzioni possibile andare direttamente nel punto esatto in cui
stata dichiarata o implementata la variabile che stiamo usando (si capir in seguito la
differenza tra dichiarazione e definizione).
Un'altra funzione molto utile sempre dal menu che compare premendo il tasto destro :
Find Usage

Questa funzione permette di trovare tutti i riferimenti nel progetto in cui viene utilizzata
la variabile o funzione selezionata.
Nel caso di variabili globali questa funzione torna molto utile. Nel caso di funzioni, Find
usage permette di visualizzare tutte le chiamate alla funzione. Nel caso delle funzioni torna
utile anche la voce del menu:
Show Call Graph

che permette di visualizzare le chiamate ad altre funzioni da parte della funzione


138

selezionata, ed il tutto in maniera grafica, come riportato in Figura 75.

Figura 75: Visualizzazione grafica delle chiamate di funzioni.

Cliccando su ogni singola funzione viene visualizzato nel codice il punto in cui viene
implementata.
Spesso, al fine di rendere il codice pi leggibile, anche qualora tutto stia funzionando
bene, ci si trova nella situazione di voler cambiare il nome di una variabile. Questa
situazione viene detta in gergo software, Refactoring. Sebbene il cambio del nome sia un
aspetto relativamente facile, ci sono libri interi che spiegano molte tecniche al fine di
evitare errori. Con poche righe di codice, il tutto pu sembrare banale, quando per ci si
trova a dover gestire progetti di migliaia di righe di codice o centinaia di migliaia di righe,
la questione si fa seria ed bene affidarsi a strumenti collaudati per lo scopo.
Un modo per cambiare il nome ad una variabile potrebbe essere quella di cambiare il
nome della stessa nel punto della definizione e ricompilare il programma. Il compilatore
trover molti errori, precisamente nei punti in cui la variabile con il vecchio nome viene
utilizzata. In questo modo, errore per errore possibile cambiare il nome della variabile in
ogni punto. Allo stesso modo si pu fare con una funzione.
Per semplificare il tutto si pu fare uso di uno strumento molto pi comodo, ovvero degli
strumenti di Refactoring disponibili in MPLAB X. Personalmente tendo ad usare spesso
solo quello per rinominare funzioni e variabili, ma ce ne sono disponibili di vari a seconda
delle esigenze. In particolare mettendo il mouse sulla variabile o funzione da rinominare,
premere il tasto destro e selezionare la voce del menu:
Refactor Rename...

Fatto questo compare la finestra come in Figura 76, in cui possibile cambiare il nome
della variabile, che nel nostro caso ho cambiato da mia_variabile a mia_variabile_2.

Figura 76: Strumento di Refactoring per rinominare una variabile.

139

Premendo il tasto Refactor, tutti i punti in cui si fa uso della variabile vengono cambiati
con il nuovo nome. Interessante la vista Preview che permette di visualizzare il codice
prima e dopo il cambio, ovvero individuare tutti i punti in cui verr effettuato il cambio
del nome della variabile o della funzione. In Figura 77 mostrato l'esempio della variabile
mia_variabile.

Figura 77: Strumento per il confronto del vecchio codice.

Quando ci si trova a dover apportare molte modifiche bene salvare delle copie del
progetto in maniera da poter fare dei salti a gambero e ripristinare delle copie intermedie.
In ambito professionale ci sono server dedicati allo scopo, ma alcuni strumenti dell'IDE
possono tornare utili quando non avessimo fatto una copia di recente. Dal menu:
Team Local History Show Local History

possibile visualizzare, ed eventualmente ripristinare, vecchie copie del progetto associate


a compilazioni precedenti, risalenti anche di diversi giorni addietro, come mostrato in
Figura 78. Lo strumento Local History torna molto utile in fase di Debug per poter
ripristinare copie intermedie o annullare alcune modifiche.

140

Figura 78: Strumento Local History per il confronto del vecchio codice e ripristino.

Riassunto
Nel Capitolo si sono introdotti vari strumenti di Debug offerti dall'ambiente di
sviluppo MPLAB X, mostrando in particolare l'utilit del visualizzare il codice Assembly
al fine di comprendere meglio il comportamento del programma. Oltre agli strumenti di
Debug si sono introdotte semplici regole di buon comportamento al fine di limitare il
tempo che dovremo passare ad effettuare il Debug del nostro programma. Infatti, pur
dovendo sempre testare il programma, alcuni problemi possono essere ridotti facendo
attenzione al modo con cui si scrive il programma stesso. Una certa importanza stata
data anche allo stile di programmazione, mostrando come un codice ordinato possa essere
riletto e corretto con maggior facilit. Su questo aspetto si torner spesso anche nei
capitoli successivi. In ultimo si sono introdotti alcuni strumenti che permettono di
agevolare la navigazione e il cambio del codice, ovvero gli strumenti di Refactoring.

Punti chiave ed approfondimenti


Refactoring: Con il termine Refactoring si intende quella fase di programmazione in cui il
codice viene cambiato al fine di migliorarne alcuni suoi aspetti mantenendo invariato
l'aspetto funzionale. In questo ambito rientrano per esempio il cambio del nome di una
variabile o una funzione, come anche accorpare righe di codice frequentemente ripetute,
in una funzione.
Parser: Il parser, diversamente dal compilatore fornisce la possibilit di controllare il
codice in tempo reale, offrendo dei feedback importanti su errori comuni o informazioni
importanti. Errori spesso segnalati dal parser sono l'utilizzo di nomi di variabili inesistenti,
dichiarazione di variabili mai usate, file inclusi che non possibile trovare e altro ancora.
141

Errori di Parsing non si traducono sempre in errori di compilazione, ma bene eliminarli.


In MPLAB X, gli errori individuati dal parser sono sottolineati in rosso o grigio a seconda
dei casi.
Breakpoint: Il Breakpoint uno degli strumenti base per il Debug ma piuttosto potente
per poter trovare degli errori. Il suo compito quello di avviare un'azione al verificarsi
dell'evento di trigger per il Breakpoint stesso. Il Breakpoint tipico ha spesso il compito di
interrompere l'esecuzione del programma al raggiungimento di una determinata riga di
codice, ovvero al verificarsi dell'eguaglianza tra il PC e l'indirizzo associato ad una
istruzione d'interesse in cui si vuole interrompere il programma.

Domande ed Esercizi
1.
2.
3.
4.
5.
6.

7.
8.
9.
10.
11.
12.

Spiegare la funzione di Refactor offerta dall'IDE MPLAB X e la sua utilit.


Spiegare l'utilit dello strumento Local History.
Che cos' il CVS?
Quali sono i principi cardine dell'Extreme Programming?
Descrivere un modo per impedire al compilatore di ottimizzare il codice C.
Per quale ragione importante sapere versione del compilatore e livello di
ottimizzazione al fine di misurare e riprodurre i tempi di esecuzioni di un codice C (o
altro linguaggio ad alto livello)?
Per quale ragione pu tornare utile visualizzare il codice Assembly?
Che cos' un Breakpoint?
Che cosa sono le regole MISRA e qual' la loro utilit?
Descrivere un errore di parsing che non si traduce in errore di compilazione.
Ripetere l'esempio del Capitolo ponendo il Breakpoint prima e dopo il ciclo for.
Discutere il risultato.
A cosa serve la combinazione di tasti CTRL + Space?

142

Capitolo VI

VI

Impariamo a programmare in C
Forti dell'esperienza e delle conoscenze dei Capitoli precedenti, viene ora introdotta
con maggior dettaglio la sintassi e le parole chiave del linguaggio C. Il tutto viene spiegato
partendo da zero, non facendo affidamento a nessun background che non sia basato sui
Capitoli precedenti. In ogni modo qualora abbiate familiarit con il C o altri linguaggi di
programmazione, il tutto sar di pi facile comprensione.

Schema elettrico
Per poter seguire gli esempi presentati in questo Capitolo, richiesta la realizzazione su
Breadboard dello schema elettrico di Figura 79 o far uso delle schede di sviluppo
impostate come descritto nel paragrafo successivo.

Figura 79: Schema elettrico di esempio del Capitolo VI.

143

Impostare le schede di sviluppo


Al fine di poter seguire gli esempi mostrati in questo Capitolo necessario impostare la
scheda di sviluppo utilizzata come descritto nei paragrafi seguenti.

Freedom II
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom II deve avere i Jumper posizionati nel seguente modo:

Figura 80: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

Freedom Light
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom Light deve avere i Jumper posizionati nel seguente modo:

Figura 81: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

La scheda Freedom Light possiede i LED solo sui pin RD0 e RD1. Per poter
visualizzare gli altri LED necessario montarne altri sul connettore di
espansione, facendo per esempio uso della scheda di espansione Breadboard
PJ7011 o limitare il Debug su due soli LED.

144

Impostazioni software
Gli esempi mostrati non richiedono alcuna impostazione software particolare oltre alla
semplice creazione di un progetto.
Dal momento che molti esempi mostrati per le varie istruzioni sono solo parti di codice
incomplete pu tornare utile scrivere lo scheletro di un codice con delle inizializzazioni
base, dopo le quali possibile scrivere il frammento di codice di esempio:
#include <xc.h>
#include "PIC18F4550_config.h"
int main (void){
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// *******************************************************
// Il codice di esempio deve seguire il seguente commento
// *******************************************************

// Ciclo infinito
while (1) {
}
}

Gli esempi mostrati sono spiegati nel dettaglio, mostrando il codice Assembly,
per cui, a seconda delle impostazioni delle ottimizzazioni e della versione del
compilatore utilizzato, possono essere presenti delle differenze tra il codice
mostrato e quello che otterrete in fase di compilazione.

145

La sintassi in breve
Nel nostro primo progetto abbiamo gi visto alcuni aspetti legati alla sintassi del C,
ovvero a quell'insieme di regole che permettono di strutturare il programma in maniera
tale che il compilatore possa propriamente interpretarle. Il C possiede in particolare delle
parole chiave ovvero Keyword che in grado d'interpretare. Ogni programma rappresenta
una combinazione pi o meno complessa di tali parole chiave pi delle istruzioni. Il
compilatore XC8 conforme allo standard ANSI C dunque possiede le seguenti parole
chiave:
Parole Chiave ANSI C
auto

int

break

long

case

register

char

return

const

short

continue

signed

default

sizeof

do

static

double

struct

else

switch

enum

typedef

extern

union

float

unsigned

for

void

goto

volatile

if

while

Tabella 4: Parole chiave dell'ANSI C.

Ogni parola chiave, qualora si stia facendo uso dell'IDE MPLAB X viene
automaticamente riconosciuta e messa in grassetto 97. Nel seguente Capitolo sono
illustrate le parole chiave pi utilizzate, mostrando per ognuna degli esempi. Essendo il
compilatore XC8 conforme allo standard ANSI C un qualunque testo di C sufficiente
per comprendere le parole chiave non trattate o approfondire quanto descritto di
seguito.
Come detto il programma oltre che essere composto da parole chiave composto da
istruzioni, ovvero operazioni da compiere. Ogni istruzione deve essere terminata per
mezzo di un punto e virgola.
a = b + c;

Su ogni linea possibile scrivere anche pi istruzioni, purch siano tutte sperate da un
punto e virgola:
97

Per mezzo delle propriet dell'Editor possibile cambiare il colore ed altre propriet associate alle varie tipologie di testo e
caratteri che l'IDE riconosce automaticamente.

146

a = b + c; d = e a; f = d;

Si capisce che in questo modo...non si capisce nulla, dunque bene scrivere il codice in
maniera da avere un'istruzione per linea:
a = b + c;
d = e a;
f = d;

In questo modo si aumenta molto la leggibilit del codice stesso. Negli esempi che
seguiranno verr messo pi volte in evidenza che la leggibilit del codice molto
importante.
Alla regola del punto e virgola fanno eccezione le direttive, dal momento che non sono
delle istruzioni che il compilatore traduce in codice macchina. Un esempio potrebbe
essere la riga di codice che abbiamo gi incontrato:
#include <xc.h>

Alcune parole chiave fanno utilizzo di blocchi d'istruzione ovvero insieme di istruzioni.
Un blocco identificato da parentesi graffe:
if (a < b) {
d = e a;
f = d;
}

Come si vedr in seguito l'insieme di istruzioni nelle parentesi graffe viene eseguito
solo se a minore di b.
Le varie linee di codice ammettono commenti che possono essere introdotti per mezzo
di varie sintassi, introdotte nel primo esempio di programmazione. Come per le parole
chiave, l'IDE MPLAB X riconosce in automatico i commenti e li colora normalmente di
verde.
Una caratteristica particolare del C, che lo differenzia da altri linguaggi di
programmazione, il fatto che un linguaggio case sensitive, ovvero sensibile alle maiuscole
e minuscole. Questo significa che le parole chiave appena introdotte, se scritte in maniera
differente non saranno riconosciute come tali. Quanto detto va inoltre generalizzato
anche al nome di variabili e funzioni. Un esempio potrebbe essere:
int temperatura = 0;

risulta differente dalla variabile:


int Temperatura = 0;

147

In generale anche se le due variabili risultano differenti si sconsiglia di utilizzare questa


tecnica per differenziare delle variabili all'interno dello stesso spazio di utilizzo (ovvero
scope, come verr spiegato nei paragrafi successivi).
In alcuni ambienti di sviluppo potrebbe essere possibile disabilitare la caratteristica case
sensitive, ma bene non farlo al fine da rimanere coerenti con le specifiche generali del
linguaggio C.

Tipi di variabili intere


Ogni volta che si scrive un programma e si compiono delle operazioni, necessario
memorizzare i risultati dell'operazione stessa. Il luogo dove viene memorizzato il risultato
prende il nome di registro o variabile. I registri, ovvero variabili, non sono altro che
locazioni di memoria RAM interna al microcontrollore, che come abbiamo visto viene
ottenuta per mezzo di celle di RAM statica ad 8 bit, ovvero un byte. Il numero di
locazioni di memoria necessarie per rappresentare un tipo di variabile varia a seconda del
tipo di variabile. Si capisce intuitivamente che per memorizzare un numero piccolo
bastano pochi registri, mentre per contenere un numero grande sono necessari un
numero maggiori di locazioni di memoria RAM.
In base al numero di celle di memoria RAM necessarie per memorizzare una variabile,
si distinguono vari tipi. In particolare il compilatore XC8 fornisce i tipi di variabili intere
riportati in Tabella 5 (i tipi di variabili floating sono discussi a breve).
Tipo

Dimensione

Tipo aritmetica

bit

1 bit

Intero senza segno

signed char

8 bit

Intero con segno

unsigned char

8 bit

Intero senza segno

signed short

16 bit

Intero con segno

unsigned short

16 bit

Intero senza segno

signed int

16 bit

Intero con segno

unsigned int

16 bit

Intero senza segno

signed short long

24 bit

Intero con segno

unsigned short long

24 bit

Intero senza segno

signed long

32 bit

Intero con segno

unsigned long

32 bit

Intero senza segno

signed long long

32 bit

Intero con segno

unsigned long long

32 bit

Intero senza segno

Minimo

Massimo
0

-128

+127

255

-32.768

+32.767

65535

-32.768

+32.767

65535

-8388608

8388607

16777215

-2147483648

2147483647

4294967295

-2147483648

2147483647

4294967295

Tabella 5: Tipi di variabili disponibili.

Come possibile osservare, a seconda del tipo di variabile, la dimensione, ovvero il


numero di byte necessari per contenere la variabile, diverso. Inoltre al variare del tipo di
variabile, anche il valore minimo e massimo che possibile contenere nella variabile,
ovvero rappresentare, cambia.
bene subito mettere in evidenza che il tipo di variabili bit e short long non
appartengono allo standar C99 per cui bene limitarne l'uso.
Il formato con cui sono salvate le variabili all'interno della memoria RAM del tipo little
148

indian ovvero con il bit meno significativo posto alla parte bassa del byte a all'indirizzo
meno significativo.
Sebbene la dimensione minima di una variabile sia una locazione di memoria
RAM, ovvero 8 bit (1 byte), la variabile bit viene dichiarata essere di un solo bit.
La ragione discende dal fatto che il compilatore, pu accorpare pi variabili bit
all'interno di una sola locazione di memoria, al fine di ottimizzare spazio e tempo
di esecuzione del codice.
Le variabili di tipo bit permettono di ottimizzare il codice sia per quanto riguarda lo
spazio che il tempo di esecuzione poich sono lette e scritte facendo uso delle istruzioni
Assembly BSF e BCF. Il loro uso ritorna utile per memorizzare dei flag, ovvero promemoria
che segnalano o meno un determinato evento, contenuto o qual si voglia significato si
voglia associare a 0 e 1. La variabile bit paragonabile alla variabile Boolean (ovvero
booleana) supportata da alcuni compilatori e linguaggi di programmazione (non il
compilatore XC8). In particolare una variabile Boolean pu assumere il solo valore 0 e 1 o
TRUE e FALSE. Per un corretto utilizzo delle variabile bit, bisogna dichiararle globali o
statiche (maggiori dettagli sullo scope delle variabili sar dato in seguito).
Si fa notare che i tipi di variabile:
signed short int
unsigned short int
short long int
long int
long long int

possono essere dichiarate, secondo lo standard C, anche senza scrivere la parola int, per
cui a signed short equivale a scrivere signed short int.
Negli esempi che seguiranno dichiaro sempre in maniera esplicita il tipo in maniera da
evitare equivoci ed essere espliciti in ci che potrebbe essere anche sottinteso.
Si noti che ogni tipo riportato in Tabella 5 esplicitamente dichiarato di tipo
signed o unsigned, ovvero con o senza segno. Nel caso in cui si dovesse
omettere di scrivere tali parole chiave, il compilatore XC8 considera la variabile
come signed ad eccezione del char, che di default sono considerate unsigned.
Da questa inconsistenza si capisce che sempre bene scrivere il tipo della
variabile.
L'interfaccia CCI (Common C Interface) richiede, che ogni variabile sia esplicitamente
dichiarata signed o unsigned visto che i vari compilatori della famiglia XC8 si
comportano, di default, in maniera diversa, per cui esplicitare il segno risolve ogni
problema.
Da quanto appena visto si capisce che a seconda del tipo di dato che bisogna
memorizzare e dell'operazione da compiere si viene ad essere vincolati dal valore
numerico minimo o massimo del risultato, per cui bisogna scegliere un tipo di variabile
idonea. Una scelta attenta permette di risparmiare locazioni di memoria RAM e anche il
149

tempo necessario per lo svolgimento delle operazioni. Infatti fare una somma tra due
interi contenuti in una variabile unsigned char pi veloce che fare la somma tra due
interi di tipo unsigned int. Quanto detto pu essere visto con un semplice esempio in
cui si dichiarano delle variabili unsigned char e si fa la somma tra loro. Lo stesso
esempio poi ripetuto per mezzo di variabili di tipo unsigned int.
#include <xc.h>
#include "PIC18F4550_config.h"
// Variabili globali
bit flagA;
bit flagB;
bit flagC;
int main (void){
// Dichiarazione delle variabili
unsigned char charA;
unsigned char charB;
unsigned char charC;
unsigned int intA;
unsigned int intB;
unsigned int intC;
// Esempio inizializzazione e somma tra char
charA = 5;
charB = 8;
charC = charA + charB;
// Esempio inizializzazione e somma tra int
intA = 5;
intB = 8;
intC = intA + intB;
// Esempio inizializzazione di variabili bit
flagA = 0;
flagB = 1;
flagC = 1;
flagC = 0;
// Ciclo infinito
while (1) {
}
}

Nell'esempio possibile osservare che per dichiarare una variabile di un certo tipo
necessario precedere il nome della variabile con il tipo voluto.
// Dichiarazione delle variabili
unsigned char charA;

150

unsigned char charB;


unsigned char charC;
unsigned int intA;
unsigned int intB;
unsigned int intC;
// Esempio inizializzazione e somma tra char
charA = 5;
charB = 8;
charC = charA + charB;
// Esempio inizializzazione e somma tra int
intA = 5;
intB = 8;
intC = intA + intB;

Il nome di una variabile pu contenere caratteri alfanumerici ma non pu


iniziare con un numero. Il carattere under score _ , sebbene possa essere utilizzato
per iniziare il nome di una variabile, in ambiente XC8 ne viene sconsigliato l'uso
dal momento che se ne fa uso nelle librerie interne.
Per verificare se quanto abbiamo detto vero bisogna vedere il codice Assembly
ottenuto dopo la compilazione del programma. Come detto, tale tecnica in generale utile
durante la fase di ottimizzazione del codice, per verificare se effettivamente valga o meno
la pena modificare delle sue parti. Si fa notare inoltre che questo approccio utile
qualunque sia il linguaggio di programmazione utilizzato. In ogni modo bene far notare
che non bisogna ottimizzare il codice sin dall'inizio98, poich il codice ottimizzato, frutto
di idee brillanti, pu in generale essere di pi difficile lettura...soprattutto a distanza di
mesi99. Per verificare il codice Assembly associato ad una compilazione possibile aprire la
finestra Assembly:
Window Output Disassembly Listing File

In tale finestra mostrato il codice generato dalla compilazione. Dal codice sotto
riportato possibile subito vedere che la parte associata all'assegnazione dei valori nelle
variabili unsigned char (ovvero la loro inizializzazione) risulta molto semplice, e come ci
si poteva aspettare coinvolge semplici movimenti con il registro W (accumulatore). Uno
per caricare la costante e l'altro per caricare tale valore nel registro voluto. La somma tra i
due registri risulta un po' pi laboriosa dell'inizializzazione, ma pur sempre piuttosto
semplice.
17:
18:
7FAC
98

99

6E01

// Esempio inizializzazione e somma tra char


charA = 5;
MOVWF 0x1, ACCESS

Anche se in generale non si consiglia di ottimizzare il codice sin dall'inizio, in applicazioni embedded sempre bene tenere
a mente che le risorse sono limitate e la scelta opportuna delle variabili bene farla sin dall'inizio.
Nei casi in cui si faccia uso di soluzioni brillanti sempre bene documentare il tutto con buoni commenti. L'essere brillanti
ristretto a frangenti temporali che difficile ripetere...gran parte delle volte abbiamo bisogno di una bella spiegazione per
raggiungere quello che un momento di lucidit ha richiesto pochi secondi!

151

7FAE
7FB0
7FB2
19:
7FB4
7FB6
7FB8
7FBA
20:
21:
7FBC
7FBE
7FC0

0E05
6E09
5001
6E01
0E08
6E0A
5001

MOVLW 0x5
MOVWF charA, ACCESS
MOVF 0x1, W, ACCESS
charB = 8;
MOVWF 0x1, ACCESS
MOVLW 0x8
MOVWF charB, ACCESS
MOVF 0x1, W, ACCESS

5009
240A
6E04

charC = charA + charB;


MOVF charA, W, ACCESS
ADDWF charB, W, ACCESS
MOVWF charC, ACCESS

In realt i movimenti di inizializzazione sono un poco superiori del necessario.


Confrontando il codice Assembly sopra con una compilazione effettuata in passato con il
compilatore C18 si pu osservare che il livello di ottimizzazione XC8 base differisce da
quello C18 (pur restando il fatto che il compilatore XC8 migliore del C18.
43:
44:
00C6
00C8
45:
00CA
00CC
00CE
46:
47:
00D0
00D2
00D4
00D6
00D8
00DA
00DC

52DE
0E08
6EDD

// Esempio inizializzazione e somma tra char


charA = 5;
MOVLW 0x5
MOVWF 0xfdf, ACCESS
charB = 8;
MOVF 0xfde, F, ACCESS
MOVLW 0x8
MOVWF 0xfdd, ACCESS

0E01
50DB
24DF
6EE7
0E02
CFE7
FFDB

charC = charA + charB;


MOVLW 0x1
MOVF 0xfdb, W, ACCESS
ADDWF 0xfdf, W, ACCESS
MOVWF 0xfe7, ACCESS
MOVLW 0x2
MOVFF 0xfe7, 0xfdb
NOP

0E05
6EDF

Le due compilazioni diverse mostrano come a differenza del compilatore ed


ottimizzazioni, il codice Assembly che si ottiene possa risultare diverso.
Nel caso di somma tra due interi si vede subito che l'aver utilizzato un tipo di variabile
unsigned int
comporta una maggiore complessit del codice ad esso associato.
Naturalmente a noi interessa poco di capire ogni riga, poich quello che facciamo
semplicemente dichiarare delle variabili e fare la somma tra loro. In ogni modo bene
tenere a mente che un utilizzo corretto delle variabili pu portare ad un codice migliore,
permettendo non solo di risparmiare memoria di programma (poich si utilizzano meno
istruzioni), ma anche ridurre i tempi di esecuzioni e la RAM necessaria.
23:
24:
7FC2
7FC4
7FC6
7FC8
25:
7FCA
7FCC

0E00
6E06
0E05
6E05
0E00
6E08

// Esempio inizializzazione e somma tra int


intA = 5;
MOVLW 0x0
MOVWF 0x6, ACCESS
MOVLW 0x5
MOVWF intA, ACCESS
intB = 8;
MOVLW 0x0
MOVWF 0x8, ACCESS

152

7FCE
7FD0
26:
27:
7FD2
7FD4
7FD6
7FD8
7FDA
7FDC

0E08
6E07

MOVLW 0x8
MOVWF intB, ACCESS

5007
2405
6E02
5008
2006
6E03

intC = intA + intB;


MOVF intB, W, ACCESS
ADDWF intA, W, ACCESS
MOVWF intC, ACCESS
MOVF 0x8, W, ACCESS
ADDWFC 0x6, W, ACCESS
MOVWF 0x3, ACCESS

Lo stesso esempio, compilato con il compilatore C18 mostra come l'inizializzazione


delle variabili intere richieda un numero maggiori istruzioni che non in XC8. A questo
livello di ottimizzazioni in XC8 non ci sono molti vantaggi tra inizializzare una variabile
unsigned int o una variabile unsigned char, cosa visibile in C18. In entrambi i
compilatori per possibile vedere come le operazioni tra le variabili favoriscano quelle
pi piccole, ovvero unsigned char.
49:
50:
00DE
00E0
00E2
00E4
00E6
00E8
00EA
51:
00EC
00EE
00F0
00F2
00F4
00F6
00F8
52:
53:
00FA
00FC
00FE
0100
0102
0104
0106
0108
010A
010C
010E
0110
0112
0114
0116
0118
011A
011C
011E
0120

0E08
6EF3
0E05
CFF3
FFDB
0E06
6ADB

// Esempio inizializzazione e somma tra int


intA = 5;
MOVLW 0x5
MOVWF 0xff3, ACCESS
MOVLW 0x3
MOVFF 0xff3, 0xfdb
NOP
MOVLW 0x4
CLRF 0xfdb, ACCESS
intB = 8;
MOVLW 0x8
MOVWF 0xff3, ACCESS
MOVLW 0x5
MOVFF 0xff3, 0xfdb
NOP
MOVLW 0x6
CLRF 0xfdb, ACCESS

0E05
CFDB
F002
0E06
CFDB
F003
0E03
50DB
2402
6E00
0E04
50DB
2003
6E01
0E07
C000
FFDB
0E08
C001
FFDB

intC = intA + intB;


MOVLW 0x5
MOVFF 0xfdb, 0x2
NOP
MOVLW 0x6
MOVFF 0xfdb, 0x3
NOP
MOVLW 0x3
MOVF 0xfdb, W, ACCESS
ADDWF 0x2, W, ACCESS
MOVWF 0, ACCESS
MOVLW 0x4
MOVF 0xfdb, W, ACCESS
ADDWFC 0x3, W, ACCESS
MOVWF 0x1, ACCESS
MOVLW 0x7
MOVFF 0, 0xfdb
NOP
MOVLW 0x8
MOVFF 0x1, 0xfdb
NOP

0E05
6EF3
0E03
CFF3
FFDB
0E04
6ADB

153

La variabile di tipo unsigned char anche se formalmente pensata per contenere il


valore numerico di un carattere ASCII (almeno da ragioni storiche) pu risultare utile in
molti altri casi. Basti infatti pensare che le porte di uscita del PIC sono a 8 bit, dunque
ogni volta che bisogna memorizzare qualche tipo di dato che deve essere poi posto in
uscita, la variabile unsigned char sicuramente una buona scelta.
Per quanto riguarda le variabili bit, che certamente avranno attratto la vostra attenzione
vediamo come viene tradotto il seguente codice:
// Esempio inizializzazione di variabili bit
flagA = 0;
flagB = 1;
flagC = 1;
flagC = 0;

si noti che la loro dichiarazione:


// Variabili globali
bit flagA;
bit flagB;
bit flagC;

diversamente dalla altre variabili, la si posta prima della funzione main. Come si vedr in
seguito questo il modo per poter dichiarare delle variabili in maniera globale, cosa
richiesta per le variabili bit. Il codice Assembly derivante dalla compilazione del codice :
35:
36:
7FEE
7FF0
37:
7FF2
7FF4
38:
7FF6
7FF8
39:
7FFA
7FFC

0100
900C
0100
800B
0100
820B
0100
920B

// Esempio inizializzazione di variabili bit


flagA = 0;
MOVLB 0x0
BCF flagA, 0, ACCESS
flagB = 1;
MOVLB 0x0
BSF flagB, 0, ACCESS
flagC = 1;
MOVLB 0x0
BSF flagB, 1, ACCESS
flagC = 0;
MOVLB 0x0
BCF flagB, 1, ACCESS

Si pu notare come lo scrivere una variabile


le istruzioni BCF e BSF.

bit

ad 1 o 0 non richieda l'Accumulatore ma

Si noti come per impostare ad 1 il flagB si scriva:


BSF flagB, 0, ACCESS

mentre per scrivere a 1 la variabile flagC si scriva:


BCF flagB, 1, ACCESS

In particolare in entrambi i casi l'istruzione BCF punta il registro flagB, ovvero lo stesso
indirizzo di memoria ma in un caso scrive nel bit 0 mentre nell'altro nel bit 1. Questo
154

mostra come pi variabili bit possano essere accorpate dal compilatore in un unico byte.
Nel caso specifico, effettuando un Debug del codice e visualizzando le variabili bit, come
riportato in Figura 82, possibile vedere come flagC e flagB abbiano lo stesso indirizzo
mentre flagA ne abbia uno diverso.
Sebbene possiate essere tentati di farne uso, dal momento che la variabile bit non
standard pensateci due volte, personalmente nel testo non ne far uso.

Figura 82: Esempio di variabili bit nella finestra di Debug Watches.

Tipi di variabili floating point


Per mezzo del linguaggio C anche possibile effettuare divisioni oltre che somme e
sottrazioni; per poter memorizzare il risultato in generale richiesto un numero decimale,
ovvero floating point. Per questa esigenza sono presenti altri due tipi di variabili ovvero
float e double, come riportato in Tabella 6.
Le variabili floating point a differenza delle variabili intere sono caratterizzate da un
segno, un esponente e una mantissa, e dal fatto che non tutti i valori compresi
nell'intervallo minimo e massimo sono in realt possibili (si ha una granularit). Questo
significa che un risultato floating point in generale il valore pi prossimo al risultato
reale100.
Tipo

Dimensione

Exp. min

Exp. max.

Min. normalizzato

Max. normalizzato

float

32 bit

-126

+128

2126 1.17549435e - 38

2128 * (2-215) 6.80564693e + 38

double

32 bit

-126

+128

2126 1.17549435e - 38

2128 * (2-215) 6.80564693e + 38

long double

32 bit

-126

+128

2126 1.17549435e - 38

2128 * (2-215) 6.80564693e + 38

Tabella 6: Tipi di variabili floating point disponibili.

In Tabella 6 mostrato il fatto che le varie variabili sono in formato a 32 bit. In realt
tra le configurazioni di progetto il formato pu essere cambiato tra 24 bit o 32 bit. Nel
caso in cui si faccia uso dell'interfaccia CCI necessario cambiare il valore di Default da
24 bit a 32 bit, come riportato in Figura 83.

100

Il fatto che un numero floating point non sia sempre un valore esatto del risultato spesso sottovalutato. In algoritmi in cui
viene calcolato in maniera iterativa un risultato si pu avere la divergenza del risultato ovvero dell'algoritmo, proprio a causa
di questi errori. Questo problema non riguarder comunque nessuna delle applicazioni discusse in questo testo.

155

Figura 83: Impostazione del formato delle variabili float e double .

Sebbene la Tabella 6 riporti il fatto che entrambi i formati float e double sono a 32
bit, in realt il formato 24 bit e 32 bit pu essere cambiato singolarmente, per, come
detto la sintassi CCI richiede che entrambi siano in formato 32 bit. Non impostando il
formato a 32 bit si ottiene le seguente Warning:
:: warning: (1426) 24-bit floating point types are not supported; float
have been changed to 32-bits
:: warning: (1426) 24-bit floating point types are not supported; double
have been changed to 32-bits

interessante notare che i formati floating point, dal momento che, diversamente dai
numeri interi tendono ad approssimare il risultato con il valore pi prossimo
rappresentabile nel formato floating point, potrebbero generare dei risultati apparentemente
errati, derivanti da approssimazioni.
La user Guide del compilatore XC8 riporta un semplice esempio molto rappresentativo in
cui viene mostrato che nel caso di un confronto tra due numeri floating si possono avere
confronti positivi anche qualora i numeri siano apparentemente diversi.
float myFloat;
unsigned char test;
myFloat = 95002.0;
test = 0;

156

// il valore viene approssimato


if(myFloat == 95001.0){

// il codice viene eseguito!


test = 0x01;

Le variabili tipo floating point devono essere utilizzate in maniera meticolosa visto che la
loro complessit porta a codici Assembly pi complessi. Per esempio una semplice somma
tra due variabili float risulta:
12:
7CE6
7CE8
7CEA
7CEC
7CEE
7CF0
7CF2
7CF4
13:
7CF6
7CF8
7CFA
7CFC
7CFE
7D00
7D02
7D04
14:
15:
7D06
7D08
7D0A
7D0C
7D0E
7D10
7D12
7D14
7D16
7D18
7D1A
7D1C
7D1E
7D20
7D22
7D24
7D26
7D28
7D2A
7D2C
7D2E
7D30
7D32
7D34
7D36
7D38

0E00
6E1F
0E00
6E20
0E48
6E21
0E42
6E22

float_A = 100.0;
MOVLW 0x0
MOVWF float_A, ACCESS
MOVLW 0x0
MOVWF 0x1C, ACCESS
MOVLW 0xC8
MOVWF 0x1D, ACCESS
MOVLW 0x42
MOVWF 0x1E, ACCESS
float_B = 50.0;
MOVLW 0x0
MOVWF float_B, ACCESS
MOVLW 0x0
MOVWF 0x20, ACCESS
MOVLW 0x48
MOVWF 0x21, ACCESS
MOVLW 0x42
MOVWF 0x22, ACCESS

C01B
F00B
C01C
F00C
C01D
F00D
C01E
F00E
C01F
F00F
C020
F010
C021
F011
C022
F012
EC1C
F03F
C00E
F01A
C00D
F019
C00C
F018
C00B
F017

float_C = float_A + float_B;


MOVFF float_A, f1
NOP
MOVFF 0x1C, 0xC
NOP
MOVFF 0x1D, 0xD
NOP
MOVFF 0x1E, 0xE
NOP
MOVFF float_B, f2
NOP
MOVFF 0x20, 0x10
NOP
MOVFF 0x21, 0x11
NOP
MOVFF 0x22, 0x12
NOP
CALL 0x7E38, 0
NOP
MOVFF 0xE, 0x1A
NOP
MOVFF 0xD, 0x19
NOP
MOVFF 0xC, 0x18
NOP
MOVFF f1, float_C
NOP

0E00
6E1B
0E00
6E1C
0EC8
6E1D
0E42
6E1E

Il codice visibilmente pi lungo della semplice somma tra interi, ma con occhi attenti si
157

pu vere che le cose sono in realt peggiori di quello che sembrano.


Nel codice Assembly vi infatti una chiamata ad un'altra funzione:
CALL 0x7E38, 0

Cercando nel codice Assembly l'indirizzo 0x7E38 dove inizia la funzione, si pu vedere
come la somma tra le due variabili float richieda molte pi istruzioni. Il semplice esempio
richiede per esempio circa 800 byte di memoria flash mentre l'intero esempio delle
variabili unsigned char e unsigned int, richiede solamente 100 byte.

Dichiarazione ed inizializzazione di variabili


Quando si dichiara una variabile non si fa altro che dare un nome ad un indirizzo di
memoria RAM, o insieme di indirizzi, a seconda del tipo di variabile. Il compilatore
infatti, quando trova un nome preceduto dalla parola chiave associata ad un tipo di
variabile, riserva un numero di indirizzi di memoria idonei a contenere il tipo stesso.
All'interno degli indirizzi di memoria riservati potrebbe essere presente un qualunque
valore, infatti riservare della memoria non comporta l'inizializzazione della stessa ad un
valore predefinito. Per tale ragione, ogni volta che si dichiara una variabile molto
importante inizializzarla ovvero dargli un valore iniziale, che generalmente il valore
nullo. Questo pu essere fatto sia in fase di dichiarazione della variabile che
successivamente. L'esempio che segue mostra come inizializzare nei due modi le variabili
i e x.
int main (void) {
// Dichiarazione della variabile intera i senza inizializzazione
int i;
// Dichiarazione ed inizializzazione della variabile intera x
int x = 0;
// Inizializzazione della variabile intera i
i = 0;
}

Le due modalit d'inizializzazione delle variabili sono del tutto equivalenti ai fini pratici.
Inizializzare una variabile una buona abitudine al fine di evitare comportamenti anomali
del software. Durante la fase di compilazione, qualora dovessimo usare una variabile
senza averla prima inizializzata riceveremo una Warning che ci avvisa appunto che la
variabile x usata senza essere stata inizializzata.
warning: (1257) local variable "x" is used but never given a value

Probabilmente le cose potrebbero funzionare in ogni modo visto che il compilatore


XC8 inizializza in automatico tutte le variabili dichiarate anche se non esplicitamente
inizializzate. In particolare l'inizializzazione avviene nello startup code che viene eseguito
prima della funzione main e dal quale viene richiamata la funzione main. Da questo si
158

capisce che in realt la funzione main non proprio la prima funzione ad essere
richiamata. Sebbene si possa intervenire nello startup code inserendo del proprio codice, i
casi che effettivamente lo potrebbero richiedere sono limitati e hanno in generale a che
fare con un'esigenza di una rapida esecuzione del codice prima che venga richiamata la
funzione main.
L'inizializzazione delle variabili permette come detto di resettare in automatico ogni
variabile per cui ad ogni Reset tutte le nostre variabili perdono il loro contenuto e sono
inizializzate a 0x00. Questo comportamento di Default potrebbe non essere voluto,
ovvero si potrebbe avere l'esigenza di mantenere il contenuto della variabile dopo un
Reset di sistema.
Per ottenere questo comportamento bisogna dichiarare la variabile persistent, in
particolare usando lo standard CCI bisogna dichiarare la variabile __persistent come nel
seguente esempio:
#include <xc.h>
#include "PIC18F4550_config.h"
int main (void){
__persistent unsigned char y;
y = 0xAA;
// Inizializzo la PORTD come uscita
TRISD = 0x00;
LATD = y;
// Ciclo infinito
while (1) {
}

bene ricordare che la dichiarazione delle variabili deve essere fatta all'inizio,

ogni funzione sia essa la funzione main o un'altra, deve dichiarare le sue variabili
prima d'iniziare a svolgere una qualunque operazione. Chi abituato a
programmare in C++, a dichiarare le variabili in prossimit del proprio utilizzo,
deve momentaneamente perdere tale buona abitudine, da non perdere per
quando si scrive in C++.

159

Gli Array
Le variabili fin ora introdotte sono sufficienti per organizzare ogni tipo di programma,
ma spesso ci sono grandezze per le quali utile avere pi variabili dello stesso tipo. Si
pensi ad esempio ad un programma che debba memorizzare la temperatura ogni ora.
Piuttosto che dichiarare 24 variabili dello stesso tipo risulta utile dichiarare una sola
variabile che permetta di contenerle tutte. Questi tipi di variabili si chiamano Array o
vettori, e possono essere anche multidimensionali 101. Nel caso preso in esame si ha a che
fare con un Array monodimensionale poich un indice sufficiente ad individuare tutti i
suoi elementi. In XC8 per dichiarare un Array si procede come per il C standard, ovvero:
int main (void){
// Array di caratteri con 10 elementi
unsigned char mioArray[10];
}

Nell'esempio appena scritto si dichiarato un Array di caratteri di 10 elementi. Per


richiamare un elemento di un Array sufficiente scrivere l'indice dell'elemento che si
vuole richiamare all'interno delle parentesi quadre. Ci rimane valido anche se si vuole
scrivere all'interno di un elemento dell'Array stesso. Quanto detto fino ad ora nasconde
per qualcosa di pericoloso...se non noto. Si affermato che mioArray un Array di 10
interi, il primo elemento per mioArray[0] mentre l'ultimo mioArray[9] e non
mioArray[10] come si potrebbe pensare. Altro rischio quando si lavora con gli Array
che bisogna essere sempre certi che il programma non vada a leggere indici maggiori di 9
(almeno in questo esempio). Infatti possibile leggere anche mioArray[28] ma questo
elemento non in realt parte del nostro Array102. Come esempio vediamo questo
segmento di programma:
int main (void){
// Dichiarazione Array di caratteri con 10 elementi
unsigned char mioArray[10];
// Scrivo 23 nel primo elemento dell'Array
mioArray[0] = 23;

// Copio l'elemento 0 nell'elemento 2


mioArray[2] = mioArray[0];

Si fa presente che l'Array, una volta dichiarato, possiede al suo interno valori casuali;
compito del programmatore aver cura d'inizializzarlo. L'inizializzazione di un Array pu
avvenire in vari modi. Un modo quello di inizializzare un valore alla volta richiamando
un indice alla volta. Un secondo metodo pi pratico quello di inizializzare l'Array
101
102

Questi tipi di variabili vengono spesso utilizzati per la realizzazione di buffer di lettura e scrittura.
Errori tipo questi sono difficili da individuare e possono creare comportamenti inaspettati solo quando l'indice va oltre il
range consentito. Questo potrebbe avvenire durante la fase di sviluppo, mostrando dunque la presenza del problema, ma
potrebbe, nel caso peggiore, mostrarsi solo quando abbiamo messo il nostro sistema nelle mani del cliente. Nella
programmazione su PC tale tipo di problema noto come buffer overflow e viene sfruttato dai virus per accedere locazioni di
memoria altrimenti inaccessibili. Questo problema potrebbe anche permettere ad un semplice file di testo di inglobare un
virus...anche se apparentemente un file di testo non un eseguibile!

160

durante la sua dichiarazione, la sintassi la seguente:


unsigned char miaStringa [] = "ciao";
unsigned int mioArray [] = {1,2,3,4,5};

In questo esempio si creata una stringa, ovvero un insieme di caratteri e una semplice
tabella numerica. In particolare bene notare che la lunghezza dell'Array non stata
definita, infatti si sono lasciate le parentesi quadre senza dimensioni. Il compilatore, a
seconda del valore d'inizializzazione assegna le dimensioni opportune, in questo caso 5 in
entrambi gli esempi.
Si noti inoltre che per inizializzare la stringa si fatto uso del doppio pedice mentre
per la tabella numerica si fatto uso delle parentesi graffe { }.
Qualcuno se avr contato il numero di lettere contenute nella parola ciao, avr
commentato, ciao contiene solo 4 caratteri e non 5. Questo vero ma le stringhe sono
una cosa a parte. Il C sebbene non supporti nativamente il formato dati di tipo stringa,
come altri linguaggi, supporta le stesse come Array di caratteri con la caratteristica che
siano terminati con il carattere speciale NULL ovvero \0.
Sebbene il carattere \0 sia composto di due simboli, viene in realt tradotto in un
solo valore numerico. In particolare tale simbolo viene nominato Escape
Character. Il C contiene altri Escape Character, tra i quali si ricorda \n (nuova
Riga), \t (tab), \r (Ritorno a capo). Questi Escape Character sono molto usati per
formattare i dati in scrittura sul monitor di un PC. A meno di non usare un
display grafico, sono meno usati in applicazioni embedded.
Il fatto di avere usato il doppio pedice nell'inizializzazione della stringa, serve proprio per
avvisare il compilatore che l'Array deve essere considerato come una stringa. Per tale
ragione la dimensione dell'Array 5 e non 4, proprio per la presenza del carattere \0.
Da quanto detto si pu intuire che il carattere \0 rappresenta il carattere di fine stringa.
Qualora si voglia dichiarare un Array di caratteri senza avere una stringa, si potrebbe
scrivere:
unsigned char mioArray [] = {'c','i','a','o'};

ovvero scrivere un Array come nel caso dell'Array numerico. I caratteri dell'alfabeto, al
fine di essere scritti nell'Array devono essere contenuti tra due pedici. In questo caso
l'Array risultante ha una dimensione pari a 4.
Il compilatore quando deve scrivere una lettera all'interno di un Array, scrive il valore
numerico ASCII del carattere. Il codice ASCII (American Standard Code for Information
Interchange) rappresenta uno standard Americano per mezzo del quale, usando un valore
numerico a 7 bit, viene associato ad ogni numero un carattere. Tale standard ideato nel
1961 da Bob Bemer (IBM) stato successivamente standardizzato dall'ente ISO
(International Organization for Standardization) sotto lo standard ISO646, facendo del codice
ASCII uno standard internazionale ufficiale. In Tabella 5 riportato il dettaglio delle
corrispondenze tra i caratteri dell'alfabeto e il relativo valore numerico. Per esempio alla
lettera 'a' corrisponde il valore numerico 97 ovvero 0x61 in esadecimale.

161

Dec

Hex

Char

Dec

Hex

Char

Dec

Hex

Char

Dec

Hex

Char

0x00

Null

32

0x20

Space

64

0x40

96

0x60

0x01

Start of Heading

33

0x21

65

0x41

97

0x61

0x02

Start of Text

34

0x22

66

0x42

98

0x62

0x03

End of Text

35

0x23

67

0x43

99

0x63

0x04

End of transmit

36

0x24

68

0x44

100

0x64

0x05

Inquiry

37

0x25

69

0x45

101

0x65

0x06

Acknowledge

38

0x26

&

70

0x46

102

0x66

0x07

Audible Bell

39

0x27

'

71

0x47

103

0x67

0x08

Backspace

40

0x28

72

0x48

104

0x68

0x09

Horizontal Tab

41

0x29

73

0x49

105

0x69

10

0x0A

Line feed

42

0x2A

74

0x4A

106

0x6A

11

0x0B

Vertical Tab

43

0x2B

75

0x4B

107

0x6B

12

0x0C

Form Feed

44

0x2C

76

0x4C

108

0x6C

13

0x0D

Carriage Return

45

0x2D

77

0x4D

109

0x6D

14

0x0E

Shift Out

46

0x2E

78

0x4E

110

0x6E

15

0x0F

Shift In

47

0x2F

79

0x4F

111

0x6F

16

0x10

Data link Escape

48

0x30

80

0x50

112

0x70

17

0x11

Device control 1

49

0x31

81

0x51

113

0x71

18

0x12

Device control 2

50

0x32

82

0x52

114

0x72

19

0x13

Device control 3

51

0x33

83

0x53

115

0x73

20

0x14

Device control 4

52

0x34

84

0x54

116

0x74

21

0x15

Neg. Acknowledge

53

0x35

85

0x55

117

0x75

22

0x16

Synchronous idle

54

0x36

86

0x56

118

0x76

23

0x17

End trans. block

55

0x37

87

0x57

119

0x77

24

0x18

Cancel

56

0x38

88

0x58

120

0x78

25

0x19

End of medium

57

0x39

89

0x59

121

0x79

26

0x1A

Substitution

58

0x3A

90

0x5A

122

0x7A

27

0x1B

Escape

59

0x3B

91

0x5B

123

0x7B

28

0x1C

File Separator

60

0x3C

<

92

0x5C

124

0x7C

29

0x1D

Group Separator

61

0x3D

93

0x5D

125

0x7D

30

0x1E

Record Separator

62

0x3E

>

94

0x5E

126

0x7E

31

0x1F

Unit Separator

63

0x3F

95

0x5F

127

0x7F

Tabella 7: Codice ASCII.

Dal momento che il compilare converte i vari caratteri in valori numerici, si capisce che
i due Array:
unsigned char mioArray [] = {'c','i','a','o'};

e
unsigned char mioArray [] = {99,105,97,111};

sono equivalenti.

162

Nel caso di Array bidimensionali l'inizializzazione fatta nel seguente modo:


unsigned int mioArray[3][4] = {{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10,
11}};

per ragioni prettamente grafiche si potrebbe anche scrivere:


unsigned int mioArray[3][4] = {{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}};

creando un aspetto a matrice pi familiare.


Scrivendo il seguente programma che non fa altro che inizializzare i vari Array, ed
effettuando il Debug per mezzo del Simulatore o dispositivo reale, si possono vedere gli
effetti delle varie inizializzazioni e dimensione degli Array stessi.
#include <xc.h>
#include "PIC18F4550_config.h"
int main (void){
unsigned char miaStringa [] = "ciao";
unsigned char ArrayCaratteri [] = {'c','i','a','o'};
unsigned char ArrayCaratteriBis [] = {99,105,97,111};
unsigned int ArrayNumerico [] = {1,2,3,4,5};
unsigned char mioArray[3][4] = {{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}};
// Ciclo infinito
while (1) {
}
}

Visualizzando il contenuto delle variabili come riportato in Figura 84 si pu osservare:

L'Array che contiene la stringa effettivamente di 5 elementi.


L'Array che contiene la stringa contiene il carattere \0 (NULL) alla fine
I due Array di caratteri pur avendo inizializzazioni apparentemente diverse,
contengono in realt dli stessi valori.
L'Array multidimensionale contiene altri tre sotto Array che possibile esplorare.
L'Array ArrayNumerico ha cinque elementi. Diversamente dagli altri, essendo un
Array di interi, ha la colonna Binary con due ottetti, essendo ogni elemento di due
Byte.
Controllando gli indirizzi di memoria di ArrayNumerico si pu notare che vanno
163

due a due poich un intero richiede due byte. Gli altri Array hanno indirizzi
consecutivi.
possibile notare che l'indirizzo di ogni Array coincide con l'indirizzo del primo
elemento dell'Array stesso.

Figura 84: Contenuto degli Array.

Gli elementi degli Array di char sono visualizzati con indice 0, 1, 2 non
consecutivo, anche se l'indirizzo di memoria lo . Questo solo un problema di
visualizzazione e potrebbe sparire in versioni successivi di MPLAB X. Il
contenuto di ogni indirizzo infatti corretto.

Le strutture
Il tipo di variabili introdotte permettono di soddisfare le esigenze di molti progetti, ma
alcune volte potrebbe essere utile avere quel qualcosa in pi che permetta di organizzare i
dati della nostra applicazione. Quando si avverte questa esigenza si ha anche la possibilit
di definirne nuovi tipi, per mezzo dei quali possibile gestire in maniera pi snella
strutture dati pi complesse. Il tipo di variabile definibile dall'utente va sotto il nome di
struttura ovvero struct.
Per esempio, se dobbiamo risolvere un problema che gestisce rettangoli, sappiamo che
un rettangolo ha sempre un'altezza e una larghezza. Dal momento che questi parametri
caratterizzano ogni rettangolo, possibile dichiarare un nostro tipo di variabile chiamata
164

rettangolo e che sia caratterizzata dai parametri precedentemente scritti, ovvero larghezza
e altezza. Per dichiarare una variabile rettangolo si deve procedere come segue:
typedef struct {
unsigned char larghezza;
unsigned char altezza;
} rettangolo;

Dunque bisogna scrivere typedef struct per poi scrivere all'interno delle parentesi
graffe tutti i campi che caratterizzano la nostra variabile. Le variabili interne devono essere
tipi primitivi, come int, char, o tipi che abbiamo precedentemente dichiarato con un'altra
struttura. Alla fine delle parentisi graffe va scritto il nome della nostra struttura, ovvero il
tipo della nostra variabile. Una volta creato il nostro tipo, possiamo utilizzarlo per
dichiarare delle variabili come si farebbe per una variabile di un tipo primitivo. Vediamo il
seguente esempio:
#include <xc.h>
#include "PIC18F4550_config.h"
// Dichiarazione della struttura rettangolo
typedef struct {
unsigned char larghezza;
unsigned char altezza;
} rettangolo;
int main (void){
// Dichiarazione della variabile di tipo rettangolo
rettangolo figura;
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// Inizializzazione del record
figura.altezza = 10;

165

figura.larghezza = 3;
// Scrivo su PORTD l'altezza
LATD = figura.altezza;
// Ciclo infinito
while (1) {
}
}

possibile vedere che la dichiarazione del nostro tipo stata fatta fuori dalla funzione

main,

questo non obbligatorio ma potrebbe essere utile poich in questo modo posso
utilizzare questa dichiarazione anche per altre funzioni e non solo nella funzione main103.
Per quanto riguarda la spiegazione, essendo il programma del tutto simile al progetto
Hello_World, non mi soffermo sui dettagli. bene notare che la sintassi del C non
permette di inizializzare le variabili interne alla struttura nella fase della loro dichiarazione.
L'inizializzazione delle variabili definite all'interno della struttura deve essere fatta un
campo alla volta. Dalle seguenti righe di codice possibile notare che la dichiarazione di
una variabile facendo uso di una struttura, del tutto analoga alla dichiarazione di una
variabile di tipo standard. Si possono anche creare degli Array di tipo rettangolo.
// Dichiarazione della variabile di tipo rettangolo
rettangolo figura;

Dal seguente codice possibile vedere che per accedere ai campi della nostra variabile
figura bisogna utilizzare il punto. Da quanto si detto si capisce quanto precedentemente
spiegato sulla porta LATxbits, ovvero che dietro tale variabile presente una struttura.
// Inizializzazione del record
figura.altezza = 10;
figura.larghezza = 3;

In ultimo il valore della variabile viene scritta sulla PORTD.


// Scrivo su PORTD l'altezza
LATD = figura.altezza;

Dal codice scritto possibile notare che facendo uso di commenti ben posti e di un
nome di variabili chiare, sia possibile leggere il codice come se stessimo leggendo un libro.
Un buon codice deve infatti essere auto-spiegante senza necessitare di un commento
per ogni linea di codice104.
Effettuando il Debug del programma interessante vedere come viene mostrato il
contenuto della variabile figura. possibile notare che l'indirizzo della struttura figura
0x02 e coincide con l'indirizzo della variabile larghezza, ovvero il primo elemento della
struttura, proprio come per un Array.

103
104

Per ulteriori informazioni a riguardo si rimanda al paragrafo sulla visibilit delle variabili (scope).
In questo testo si fa uso di pi commenti di quanto non siano in realt necessari.

166

Figura 85: Contenuto della variabile figura.

Le costanti
Alcune volte si ha l'esigenza di avere una specie di variabile che conterr lo stesso
valore durante tutto il programma. Questo tipo di variabile pi propriamente detta
costante poich a differenza di una variabile non possibile variare il suo valore durante
l'esecuzione del programma, ovvero possibile cambiare, o meglio impostare, un valore
solo prima della compilazione del programma. Normalmente il nome delle costanti viene
scritto in maiuscolo ma questa solo una convenzione. In C per poter definire una
costante si usa la direttiva #define105 per mezzo della quale si definisce una
corrispondenza tra un nome e un valore numerico. In questo modo ogni volta che
bisogna scrivere lo stesso valore, basta scrivere il nome che gli si assegnato. Questo un
tipico esempio:
#define MAX_VALUE 56

Si capisce che l'utilizzo delle costanti risulta particolarmente utile qualora si vogliano
definire dei limiti. L'utilit nel definire la costante MAX_VALUE che se si dovesse
variare questo valore non necessario cambiarlo in ogni punto del programma ma basta
cambiare la riga precedente con il nuovo valore.
L'utilizzo di numeri nel codice noto come utilizzo di numeri magici (magic number) il
suo utilizzo altamente sconsigliato, salvo in codici brevi di esempio. Infatti quando ci si
trova a cambiare dei magic number, facile cambiare un numero per un altro, creando
problemi al codice. Un esempio di utilizzo della direttiva #define pu essere il seguente:
#define BUFFER_SIZE 10
int main (void) {
// Dichiarazione Array di caratteri con BUFFER_SIZE elementi
unsigned char mioArray[BUFFER_SIZE];
// ...resto del codice
}
105

La direttiva #define pu essere utilizzata anche per la definizione di macro ovvero un blocco di codice che possibile
riscrivere facendo riferimento al nome con cui stato definito. Una macro non una funzione poich al posto del nome da
dove viene richiamata viene sostituito l'intero codice e non un salto. Il suo utilizzo viene sconsigliato in C++ poich pu
portare a problemi difficili da trovare. Personalmente consiglio di realizzare una funzione ogni qual volta si stia pensando di
realizzare una macro. Eccezioni sono sempre possibili, ma state in guardia.

167

Si vede subito la sua utilit qualora il buffer dovesse avere dimensioni diverse.
La direttiva #define oltre che a definire costanti risulta molto utile per assegnare un
nome ad un determinato pin del PIC. In questo modo se il pin viene spesso utilizzato nel
programma si pu far riferimento al nome piuttosto che al nome del pin stesso. Questo
metodo di rinominare il pin risulta particolarmente utile qualora, in fase di sviluppo, si
voglia cambiare il pin dove per esempio attaccato un LED rosso di allarme. Infatti
baster in un solo punto del programma cambiare l'assegnazione di un nome ed il resto
del programma far uso del nuovo pin. Un esempio di assegnazione di un nome ad un pin
il seguente:
#define LED_ROSSO LATDbits.LATD1

o facendo uso del registro PORTx:


#define LED_ROSSO PORTDbits.RD1

Nel caso in cui si voglia dichiarare una famiglia di costanti, la direttiva #define pu
essere utilizzata per dichiarare tutte le costanti. Per esempio se si volesse scrivere un
valore di inizializzazione particolare per i valori contenuti in una struttura del tipo
rettangolo, potremmo scrivere:
#define BASE_STANDARD 10
#define ALTEZZA_STANDARD 20

ed inizializzare il nostro record o Array con tali valori. Un altro modo utilizzato in C per
raggruppare delle costanti per mezzo della struttura enum, ovvero enumerando le
costanti da utilizzare. La sintassi per definire una struttura enum molto simile a quella
utilizzata per definire un nuovo tipo di variabile.
enum RETTANGOLO_STANDARD {
BASE_STANDARD
ALTEZZA_STANDARD

= 10,
= 20

};

Si pu vedere che dopo enum bisogna mettere il nome della raccolta di costanti. Le
costanti diversamente dalla direttiva #define vengono assegnate ad un valore per mezzo
dell'operatore =, inoltre al temine della riga viene posta una virgola piuttosto che un
punto e virgola, mentre l'ultimo elemento, diversamente dagli altri non ha la virgola. La
costante definita all'interno di una struttura enum pu essere utilizzata allo stesso modo di
una costante definita per mezzo della direttiva #define. Negli esempi che seguiranno nel
testo si fa spesso uso della definizione di costanti prediligendo la direttiva #define.
Le costanti, diversamente dalle variabili, non risiedono in RAM ma in memoria Flash,
questo discende dal fatto che non si ha l'esigenza del doverle cambiare.
Detto questo vediamo un altro modo per dichiarare delle costanti, ed in particolare avere
anche la possibilit di definire il tipo della stessa.
168

const unsigned char stringa[] =

"LaurTec";

La stringa definita, diversamente dalla stringa definita nel paragrafo degli Array, non
pu essere pi cambiata. Anche in questo caso stringa, apparentemente una variabile,
viene in realt salvata in memoria Flash. Il vantaggio nel definire una costante in questo
modo risiede nel fatto che il compilatore pu effettuare un numero di controlli maggiori
conoscendo il tipo della costante.

I puntatori
Frequentemente si sente dire che il linguaggio C un linguaggio pi potente o
migliore di altri. Sebbene non si capisca spesso il significato di quello che si sta
affermando. Il linguaggio C fornisce molte possibilit al programmatore di intervenire
sulle strutture dati, fornendo spesso la stessa flessibilit che rivendica il programmatore
che fa uso del solo Assembly. Tra gli strumenti che permettono un ottimo controllo sulle
strutture dati vi il puntatore.
Il puntatore un tipo di variabile particolare, visto che il suo contenuto un
indirizzo. Come dice il nome stesso, un puntatore serve per puntare, ovvero
indirizzare il contenuto di una locazione di memoria.

In un certo qual modo la possibilit di indirizzare in maniera trasparente un indirizzo di


memoria lo abbiamo gi usato, infatti quando definiamo delle variabili non facciamo altro
che dare un nome ad un indirizzo di memoria RAM e accedere al suo contenuto per
mezzo del nome che gli abbiamo assegnato. In base al tipo di variabile, indirettamente
diamo un nome ad uno o pi byte di memoria. In ogni caso accedere ad una variabile
non richiede la conoscenza dell'indirizzo in cui risiede, infatti per leggere e scrivere dati al
particolare indirizzo assegnato alla variabile, basta usare il nome assegnato alla stessa.
Il puntatore permette di accedere ad una variabile per mezzo del suo indirizzo,
permettendo di sviluppare programmi particolarmente flessibili. Alcuni esempi saranno
mostrati nei capitoli che seguiranno quando si avr una maggiore padronanza del C.
In ogni modo vediamo meglio come definire un puntatore e alcune sue caratteristiche. Per
definire un puntatore ad una variabile unsigned char bisogna scrivere:
unsigned char *puntatore;

Si vede subito che definire un puntatore praticamente identico alla dichiarazione di


una variabile con l'eccezione che il nome della variabile deve essere preceduta da un
asterisco *. La necessit di definire un puntatore con un tipo discende dal fatto che per
accedere correttamente ad una variabile non basta solo un indirizzo di memoria ma anche
la struttura stessa. Nel caso di un unsigned char un puntatore potrebbe essere un
semplice indirizzo di memoria, ma nel caso di un unsigned int, essendo la variabile
composta da due byte necessario avere maggiori informazioni. Per tale motivo
definendo il tipo, ci si assicura che oltre all'indirizzo di memoria si mantiene
l'informazione sull'organizzazione della memoria associata alla variabile.
169

A questo punto vi starete chiedendo, ok, il puntatore pu memorizzare l'indirizzo della


mia variabile, ma come faccio a sapere l'indirizzo di una variabile che ho dichiarato?
In C si procede in questo modo:
unsigned char mia_variabile;
unsigned char *puntatore;
puntatore = &mia_variabile;

dall'ultima istruzione si vede che per accedere all'informazione dell'indirizzo di memoria


della variabile mia_variabile basta precedere il nome della variabile con il simbolo &.
In questo modo abbiamo ora il nostro indirizzo nel puntatore che possiamo utilizzare per
accedere alla variabile mia_variabile per mezzo dell'indirizzo di memoria. Per fare
questo bisogna scrivere:
*puntatore = 0x05;

ovvero precedere il nome del puntatore con l'asterisco permette di scrivere all'interno
dell'indirizzo contenuto nel puntatore. Vediamo un esempio completo ed effettuiamo il
Debug dello stesso.
#include <xc.h>
#include "PIC18F4550_config.h"
int main (void){
unsigned char mia_variabile;
unsigned char *puntatore;
puntatore = &mia_variabile;
*puntatore = 0x05;
// Imposto la PORTD come output
TRISD = 0x00;
LATD = mia_variabile;
// Ciclo infinito
while (1) {
}
}

Effettuando il Debug del programma e visualizzando il contenuto della variabile


mia_variabile e del puntatore si ha:

170

Figura 86: Contenuto della variabile mia_variabile e puntatore.

Dalla Figura 86 possibile vedere come il contenuto finale della variabile mia_variabile
sia 0x05. Tale valore lo si scritto all'interno della variabile accedendo direttamente
all'indirizzo della stessa e non per mezzo del suo nome. L'indirizzo della variabile
mia_variabile 0x03 mentre l'indirizzo del puntatore, che rappresenta pur sempre una
variabile nella memoria RAM pari a 0x01. Il contenuto del puntatore pari a 0x03, che
rappresenta proprio l'indirizzo della variabile mia_variabile. Il contenuto di *puntatore
invece 0x05 ovvero il contenuto dell'indirizzo 0x03 cio di mia_variabile.
Qualora si provasse a dichiarare un puntatore di un tipo ma assegnare l'indirizzo di una
variabile di altro tipo, come per esempio in questo codice:
unsigned int mia_variabile;
unsigned char *puntatore;
puntatore = &mia_variabile;

si otterrebbe il seguente messaggio di Warning.


warning: (359) illegal conversion between pointer types

Il messaggio di Warning permette comunque di avere il programma compilato con


successo, ma si potrebbero avere effetti collaterali che potrebbero compromettere il
corretto funzionamento del programma. Queste Warning bene comprenderle ed
eliminarle per mezzo di un casting adeguato, se necessario e corretto farlo (maggiori
dettagli sul casting saranno dati a breve).
A questo punto avrete capito come usare il puntatore per accedere ad una variabile ma
non avrete probabilmente compreso la sua utilit, ma normale visto che non sono stati
mostrati esempi particolarmente utili.
Prima di procedere oltre bene vedere qualche altro esempio di utilizzo dei puntatori,
in particolare il caso di puntatore ad Array e puntatore ad una struttura.
Nel caso di un Array si visto che l'indirizzo del primo elemento coincide con l'inizio
dell'Array stesso, per cui definendo un Array si potrebbe scrivere:
unsigned char mio_Array[5];
unsigned char *puntatore;

Per assegnare al puntatore il valore di inizio dell'Array bisogna scrivere:


171

puntatore = &mio_Array[0];

ovvero assegnare l'indirizzo del primo elemento dell'Array. Un altro modo potrebbe
essere:
puntatore = mio_Array;

Ovvero semplicemente scrivere il nome dell'Array. Scrivere il solo nome dell'Array


equivale ad assegnare al puntatore l'indirizzo dell'Array. Questo secondo metodo quello
pi comunemente usato. Per accedere ai vari elementi dell'Array possibile usare il
puntatore scrivendo semplicemente:
puntatore[3] = 0x05;

Nel caso di una struttura, come per esempio quella precedentemente usata:
typedef struct {
unsigned char larghezza;
unsigned char altezza;
} rettangolo;

si potrebbe creare un puntatore nel seguente modo:


rettangolo figura;
rettangolo *puntatore;

ed assegnare al puntatore l'indirizzo della struttura nel seguente modo:


puntatore = &figura;

Per accedere ad un elemento della struttura facendo uso del puntatore si potrebbe pensare
di usare il seguente metodo:
puntatore.larghezza = 0x05;

In realt sebbene sia logico usare questo metodo, in C, qualora si debba selezionare un
elemento di una struttura per mezzo di un puntatore, si fa uso della seguente sintassi:
puntatore->larghezza = 0x05;

ovvero del simbolo composto ->.


Vediamo un esempio che riassume il tutto:
#include <xc.h>
#include "PIC18F4550_config.h"
typedef struct {
unsigned char larghezza;

172

unsigned char altezza;


} rettangolo;
int main (void){
// Definizione Array di caratteri e relativi puntatori
unsigned char mio_array_char[5];
unsigned char *puntatore_array_char;
unsigned int *puntatore_array_int;
// Definizione di una variabile rettangolo e relativo puntatore
rettangolo figura;
rettangolo *puntatore_figura;
// gestione di un Array di caratteri
puntatore_array_char = mio_array_char;
puntatore_array_int = mio_array_char;
puntatore_array_char[0]
puntatore_array_char[1]
puntatore_array_char[2]
puntatore_array_char[3]
puntatore_array_char[4]

=
=
=
=
=

0x01;
0x02;
0x03;
0x04;
0x05;

// gestione di una struttura


puntatore_figura = &figura;
puntatore_figura->altezza = 0x8;
puntatore_figura->larghezza = 0x3;
// Imposto la PORTD come output
TRISD = 0x00;
LATD = 0x00;
LATD = puntatore_array_char[0];
LATD = puntatore_array_int[1];
LATD = puntatore_figura->altezza;
// Ciclo infinito
while (1) {
}

Compilando il programma avremo la seguente Warning:


warning: (359) illegal conversion between pointer types

Non ce ne curiamo ed andiamo avanti.


Eseguendo il Debug del programma, effettuando un passo alla volta, possiamo vedere gli
effetti delle singole istruzioni. In particolare arrivati alla fine possibile vedere il valore
delle varie variabili, inizializzate per mezzo dei puntatori.
LATD = puntatore_array_char[0];
LATD = puntatore_array_int[1];
LATD = puntatore_figura->altezza;

173

In particolare vedremo i LED sulla PORT D accendersi nel seguente modo:


00000001
00000011
00001000

Tutto ok? Suggerendo la risposta direi di no. Nel primo caso


LATD = puntatore_array_char[0];

L'elemento 0 vale effettivamente 0x01 per cui giusto che si accenda un solo LED, ma
nel secondo caso :
LATD = puntatore_array_int[1];

Puntando il secondo elemento ci si aspetterebbe il valore 0x02 ovvero 000000010.


Quello che sta succedendo che la Warning ci ha detto che abbiamo fatto qualcosa di
sospetto e anomalo, visto che abbiamo usato un puntatore ad interi per un Array di char.
Il problema sta nel fatto che un char composto di un solo byte mentre un intero
composto di due byte. Quando con l'indice dentro le parentesi quadre ci spostiamo di 1, il
puntatore ad interi si sposta in realt di due prelevando un dato sbagliato.
Quanto detto possibile vederlo nel dettaglio visualizzando il contenuto dei puntatori
nella finestra Watches, come mostrato in Figura 87. E' possibile notare il contenuto dei due
puntatori:
unsigned char *puntatore_array_char;
unsigned int *puntatore_array_int;

lo stesso, ed pari all'indirizzo dell'Array mio_array_char, ovvero 0x01, ciononostante


il valore puntato dai due puntatori, ovvero l'elemento [0] diverso e rispettivamente:
00000001 e 00000010 00000001

ovvero ad 8 bit (1 byte) nel primo caso e a 16 bit (2 byte) nel secondo.

Figura 87: Contenuto dei vari puntatori ed Array.

174

Quando il puntatore ad interi viene impostato per puntare l'elemento [1] viene sommato 2
all'indirizzo, per cui il valore puntato (non mostrato) sarebbe:
00000100 00000011

Quando si scrive il valore a 16 bit nella porta PORTD, di soli 8 bit, viene visualizzato il
solo valore
00000011

In questo esempio si messo in evidenza il fatto che Warning associate ai puntatori


possono nascondere problemi subdoli non sempre visibili, per cui sempre bene
comprendere una Warning qualora si decida di ignorarla. In particolare bene fare dei test
che possano validare le nostre ragioni nell'avere ignorato la Warning.
Frequentemente il nome dei puntatori inizia con una lettere p minuscola, proprio
per indicare che la variabile un puntatore. Questo potrebbe essere utile qualora
si abbia un codice programma di una certa dimensione. Nomi tipici potrebbero
essere pArray, pLingua.

Operatori matematici
Ogni volta che si crea una variabile si ha pi o meno l'esigenza di manipolare il suo
contenuto per mezzo di operazioni algebriche. Il compilatore XC8 supporta in maniera
nativa i seguenti operatori matematici:
+
/
*
%

: operatore somma
: operatore sottrazione
: operatore divisione
: operatore moltiplicazione
: resto divisione tra interi o modulo

Per operatori nativi si intende che possibile utilizzarli senza dover includere delle
librerie esterne. A questi operatori si aggiungono altre funzioni matematiche particolari,
quali i sen(x), cos(x), log(x), ecc., e relative funzioni inverse. Per poter utilizzare questi
ulteriori operatori bisogna includere, per mezzo della direttiva #include, la libreria
math.h106.
Nonostante per gli operatori base non sia necessario includere alcuna libreria, non vuol
dire che la relativa operazione sia direttamente supportata dal microcontrollore per mezzo
di una sola istruzione Assembly. Si gi visto infatti, che nel fare una divisione tra numeri
reali viene richiamata una funzione per la gestione del segno, mantissa ed esponente.
Ciononostante questo avviene in maniera trasparente senza dover includere delle librerie
esterne.
106

Per ulteriori informazioni su tale libreria si rimanda alla documentazione ufficiale della Microchip che possibile trovare
nella directory doc della cartella principale dove stato installato il C18.

175

Gli operatori matematici si comportano in maniera analoga a quanto imparato in


algebra, ad eccezione del simbolo % che in un certo qual modo nuovo ma non fa altro
che restituire il resto di una divisione tra interi. Per esempio scrivendo:
a = 7 % 2;

il valore della variabile a 1.


Oltre agli operatori matematici classici, sono presente gli operatori di assegnamento
composti, che in un certo qual modo si comportano come gli operatori matematici, ma
hanno la caratteristica di usare una variabile come valore dell'operazione e assegnare il
risultato alla variabile stessa. Per esempio si potrebbe scrivere:
a = 5;
a = a + 2;

Dopo aver inizializzato a al valore 5 si somma il valore 2.


Lo stesso codice si potrebbe riscrivere con gli operatori di assegnamento composti nel
seguente modo:
a = 5;
a += 2;

Ovvero non si fa altro che far precedere il segno di uguale con il relativo operatore
d'interesse, lasciando sottintesa la variabile a.
Il vantaggio della seconda scrittura quello di permettere al compilatore di effettuare
alcune ottimizzazioni sul codice. Dal codice Assembly si pu vedere che il primo caso
tradotto nel seguente codice:
12:
7CEA
7CEC
7CEE
7CF0
7CF2
7CF4

0E03
2403
6E03
0E00
2004
6E04

a = a + 3;
MOVLW 0x3
ADDWF a, W, ACCESS
MOVWF a, ACCESS
MOVLW 0x0
ADDWFC 0x4, W, ACCESS
MOVWF 0x4, ACCESS

mentre il secondo caso, con l'operatore di assegnazione composto si ha il seguente codice


Assembly:
14:
7CF6
7CF8
7CFA
7CFC

0E03
2601
0E00
2202

a+= 3;
MOVLW 0x3
ADDWF a, F, ACCESS
MOVLW 0x0
ADDWFC 0x2, F, ACCESS

Oltre a quanto mostrato sopra, come possibile operatore di somma alcune volte viene
utilizzato il doppio ++, che ha lo scopo di incrementare la variabile di uno.
Consideriamo il seguente segmento di codice:
int main (void) {

176

unsigned char i=0;


// Dopo la somma i vale 1
i = i + 1;
}

Un altro modo per effettuare questo tipo di somma in maniera snella per mezzo
dell'operatore incremento ++, come riportato nel seguente codice:
int main (void) {
unsigned char i=0;
// Dopo la somma i vale 1
i++;
}

In maniera analoga all'operatore ++ esiste anche l'operatore di decremento --. Un


semplice esempio di utilizzo riportato nel seguente codice.
int main (void) {
unsigned char i=0;
// Dopo l'incremento i vale 1
i++;

// Dopo il decremento i vale 0


i--;

Nel caso particolare di variabili a singolo byte, l'istruzione di incremento e decremento si


traduce in una sola istruzione Assembly, infatti i PIC18, come molti altri microcontrollori,
possiede come istruzioni base l'operazione d'incremento ( INCF) e decremento (DECF) di
un registro senza far uso del registro speciale accumulatore. Analizziamo il seguente
codice:
#include <xc.h>
#include "PIC18F4550_config.h"
int main (void) {
unsigned int i = 0;
unsigned char y = 0;
// Sommo 1 ad un intero
i = i + 1;
// Incremento di un intero
i++;
// Sommo 1 ad un char
y = y + 1;

177

// Incremento di un char
y++;
// Ciclo infinito
while (1) {
}
}

Dal codice Assembly possibile vedere come vengono tradotte le istruzioni di somma
ed incremento per mezzo dell'operatore ++:
12:
13:
7CE6
7CE8
7CEA
7CEC
7CEE
7CF0
14:
15:
16:
7CF2
7CF4
17:
18:
19:
7CF6
7CF8
20:
21:
22:
7CFA

0E01
2402
6E02
0E00
2003
6E03

// Sommo 1 ad un intero
i = i + 1;
MOVLW 0x1
ADDWF i, W, ACCESS
MOVWF i, ACCESS
MOVLW 0x0
ADDWFC 0x3, W, ACCESS
MOVWF 0x3, ACCESS

4A02
2A03

// Incremento di un intero
i++;
INFSNZ i, F, ACCESS
INCF 0x3, F, ACCESS

2804
6E04

// Sommo 1 ad un char
y = y + 1;
INCF y, W, ACCESS
MOVWF y, ACCESS

2A04

// Incremento di un char
y++;
INCF y, F, ACCESS

possibile subito vedere che il caso peggiore si ha quando si deve sommare 1 ad un


intero, ovvero i= i + 1. Questo come spiegato legato al fatto che un intero
rappresentato da due byte, dunque la sua gestione pi laboriosa del caso di un
incremento di un byte. La cosa migliora nel caso in cui invece di sommare 1 si fa uso
dell'operatore d'incremento ovvero i++, riducendo il codice Assembly ad oltre la met.
Qualora la nostra variabile sia un char invece di un intero, si ha che sommare un
numero non laborioso quanto il caso d'incremento di un intero, inoltre quando si
incrementa un char per mezzo dell'operatore ++ si ha che il codice Assembly ottenuto
per mezzo di una sola istruzione, ovvero l'incremento diretto del registro!
L'analisi appena svolta non un ragionamento da prendere come verit assoluta,
ovvero che l'incremento di 1 genera sempre un codice Assembly migliore della somma di 1.
A seconda del compilatore utilizzato e livello di ottimizzazioni, il codice che si ottiene pu
essere diverso. Quanto detto dovrebbe anche aprire gli occhi in termini di ottimizzazione,
ovvero che per ottimizzare un codice non basta cambiare il sorgente solo per sentito dire,
ma bisogna sempre controllare il codice Assembly per vedere come viene tradotto il
codice sorgente. Normalmente, a meno di non voler accelerare una particolare parte di
codice, non ci si trova frequentemente a dover analizzare il codice Assembly. Nel testo ne
faccio spesso uso solo per agevolare la spiegazione e fornire maggiori informazioni sugli
178

strumenti messi a disposizione dall'ambiente di sviluppo.


Detto questo cerchiamo di complicarci la vita dicendo che l'operatore ++ pu essere
messo sia prima che dopo la variabile, ovvero si pu scrivere sia y++ che ++y, ottenendo
risultati diversi! Vediamo un esempio:
// Inizializzo le variabili
charA = 0;
charB = 0;
// Incremento dopo la variabile
charB = charA++ + 1;
// Inizializzo le variabili
charA = 0;
charB = 0;
// Incremento prima della variabile
charB = ++charA + 1;

Come visibile, escluse le inizializzazioni delle variabili charA e charB, quello che si ha
che charB assegnato il risultato della somma di 1 pi l'incremento di charA, una volta
ottenuto con ++ posto prima della variabile, e una volta posto dopo. Ovviamente il
risultato deve essere uguale a 2 in tutti e due i casi!
...ed qui che avvengono i problemi!
Nel primo caso, charB vale 1 mentre nel secondo caso charB vale 2!
La ragione legata al fatto che il ++ prima o dopo effettivamente permette
d'incrementare la variabile subito (ovvero prima della somma) o dopo la somma. Questo
se non tenuto in considerazione pu portare a gravi errori. Un modo semplice per evitare
tali errori far uso di una sola modalit d'incremento ++i o i++, una vale l'altra, e
scrivere l'istruzione in una riga di codice a parte, ovvero non all'interno di espressioni di
alcun tipo. In questo modo si garantisce che quello che ci si aspetta sia quello che si
ottiene. Dal codice Assembly possibile vedere quanto ora spiegato, ovvero come
l'istruzione INCF sia eseguita dopo la somma (primo caso) o prima della somma (secondo
caso).
29:
30:
7CBC
7CBE
7CC0
7CC2
31:
7CC4
7CC6
7CC8
7CCA
32:
33:
34:
7CCC

0E00
6E02
0E00
6E01

// Inizializzo le variabili
charA = 0;
MOVLW 0x0
MOVWF 0x4, ACCESS
MOVLW 0x0
MOVWF charA, ACCESS
charB = 0;
MOVLW 0x0
MOVWF 0x2, ACCESS
MOVLW 0x0
MOVWF charB, ACCESS

0E01

// Incremento dopo la variabile


charB = charA++ + 1;
MOVLW 0x1

0E00
6E04
0E00
6E03

179

7CCE
7CD0
7CD2
7CD4
7CD6
7CD8
7CDA
35:
36:
37:
7CDC
7CDE
7CE0
7CE2
38:
7CE4
7CE6
7CE8
7CEA
39:
40:
41:
7CEC
7CEE
7CF0
7CF2
7CF4
7CF6
7CF8
7CFA

2403
6E01
0E00
2004
6E02
4A03
2A04

ADDWF charA, W, ACCESS


MOVWF charB, ACCESS
MOVLW 0x0
ADDWFC 0x4, W, ACCESS
MOVWF 0x2, ACCESS
INFSNZ charA, F, ACCESS
INCF 0x4, F, ACCESS

0E00
6E02
0E00
6E01

// Inizializzo le variabili
charA = 0;
MOVLW 0x0
MOVWF 0x4, ACCESS
MOVLW 0x0
MOVWF charA, ACCESS
charB = 0;
MOVLW 0x0
MOVWF 0x2, ACCESS
MOVLW 0x0
MOVWF charB, ACCESS

4A03
2A04
0E01
2403
6E01
0E00
2004
6E02

// Incremento prima della variabile


charB = ++charA + 1;
INFSNZ charA, F, ACCESS
INCF 0x4, F, ACCESS
MOVLW 0x1
ADDWF charA, W, ACCESS
MOVWF charB, ACCESS
MOVLW 0x0
ADDWFC 0x4, W, ACCESS
MOVWF 0x2, ACCESS

0E00
6E04
0E00
6E03

Un altro caso in cui l'operatore d'incremento potrebbe creare problemi all'interno


dell'istruzione condizionali if () che verr illustrata a breve.
Vediamo ora qualche dettaglio anche sugli altri operatori matematici.
L'operazione di divisione e moltiplicazione sono operazioni molto complesse e per la
loro esecuzione richiedono molto tempo e memoria del PIC. In alcuni casi queste
operazioni possono essere sostituite con shift a destra (divisione) o shift a sinistra
(moltiplicazione) ma solo qualora l'operazione sia una potenza di due. Alcuni esempi
verranno riportati a breve quando si introdurranno gli operatori bitwise.
I PIC18 diversamente dai suoi predecessori ad 8 bit, possiedono al loro interno un
moltiplicatore hardware 8x8 per mezzo del quale possibile ottimizzare il codice per lo
svolgimento delle moltiplicazioni. In particolare una moltiplicazione tra due byte pu
essere svolta in un solo ciclo istruzione. Naturalmente questi dettagli non sono molto
importanti quando si scrive in C, ma bene tenere a mente che il compilatore far uso di
questo hardware per ottimizzare il codice per risolvere le moltiplicazioni e divisioni.
Per quanto riguarda la divisione bene notare che, salvo casi specifici l'operazione
deve essere svolta tra variabili di tipo float. Se le variabili non dovessero essere di tipo
float potrebbe avvenire qualcosa di strano ma i dettagli saranno descritti nel prossimo
paragrafo, dove verr introdotto il concetto di casting delle variabili.
L'unico operatore strano tra quelli citati (trascurando il trauma per gli incrementi, che si
180

era pensato di gestire facilmente), rappresentato dall'operatore % che restituisce il resto


della divisione tra interi, ovvero sia il dividendo che il divisore devono essere interi. Non
c' nulla di complicato dietro questo simbolo infatti gli esempi sotto dovrebbero chiarire
il tutto.
intA = 0;
intB = 10;
// Divisione tra interi
intA = intB % 5;
intA = 0;
// Divisione tra interi
intA = intB % 3;

Nel primo caso, dal momento che intB vale dieci e viene diviso per un suo
sottomultiplo, si ha che il resto 0, dunque intA vale 0. Nel secondo caso dal momento
che intB vale 10 e viene diviso per 3, si ha resto 1, dunque intA vale 1.
Ultima nota va data riguardo alla priorit degli operatori. Ogni operatore ha una
propria priorit all'interno di un'espressione matematica ma dal momento che spesso
difficile tenere a mente le priorit sempre bene far uso delle parentesi tonde in modo da
raggruppare ed ordinare espressioni complesse.
a = b * c + 5;

meglio scriverla:
a = (b * c) + 5;

piuttosto che far affidamento al fatto che la moltiplicazione viene svolta comunque per
prima. Nel caso in cui si voglia fare prima la somma invece obbligatorio scrivere:
a = b * (c + 5);

Nel caso di espressioni complesse bene creare della variabili intermedie, il cui nome
deve essere espressivo della loro funzione. Per esempio per il calcolo della velocit della
bicicletta l'espressione potrebbe essere:
speed = (numero_giri_ruota * DIAMETRO_RUOTA)/(num_base_tempo *

BASE_TEMPO);

potrebbe essere scritta meglio come:


spazio_percorso = numero_giri_ruota * DIAMETRO_RUOTA;
tempo_percorso = num_base_tempo * BASE_TEMPO;
speed = spazio_percorso / tempo_percorso;

Si noti che in questo caso il tutto risulta molto pi leggibile, per cui risulta
particolarmente pratico nel caso in cui si debbano risolvere dei problemi complessi.
181

L'utilizzo di variabili intermedie viene normalmente pagato con un utilizzo


maggiore della memoria RAM e spesso anche un tempo di esecuzione maggiore.
In base ai casi bisogna valutare come scrivere l'espressione. Come consiglio
generale sempre bene partire dalla forma pi chiara e successivamente
ottimizzare il codice, se e solo se, necessario.
Dalle formule e dal nome delle variabili ci si accorge che in realt l'operazione svolta
facendo uso anche di alcune costanti. Il modo con cui vengono scritte le variabili e
costanti molto utile, poich permette di discriminarle a colpo d'occhio. Come detto le
costanti sono tutte maiuscole, mentre le variabili hanno la caratteristica di essere scritte in
minuscolo, almeno la loro parte iniziale. Altra pratica in nomi composti quella di usare o
un under score _ per unire i nomi o scrivere il secondo nome con l'iniziale maiuscola, come
nel seguente esempio:
tempoPercorso = numBaseTempo * BASE_TEMPO;

Questa pratica per nominare le variabili anche seguita per nominare le funzioni. In
ultimo si noti l'importanza nel dare alle variabili un nome significativo. Questo permette
di ridurre i commenti mantenendo il codice pi pulito ma sempre molto leggibile. In un
certo qual modo un nome della variabile potrebbe racchiudere il commento o spiegazione
della variabile stessa.

Casting delle variabili


Per Casting si intende una promozione o un cambio del tipo di una variabile in un altro
tipo. La sua utilit e pericoli a cui pu condurre sono difficilmente spiegabili in parole,
dunque meglio vedere il tutto tramite esempi. Come inizio, al fine di tranquillizzare gli
animi inquieti, sappiate che qualora svolgiate operazioni tra variabili tutte dello stesso tipo
le sorprese sono poche...ma il rischio di perdere informazioni sempre in agguato!
Prendiamo in considerazione il seguente esempio:
intA = 9 / 4;
floatA = 9 / 4;

Quando si scrive un numero come riportato nell'esempio, il compilatore lo considera


come intero, dunque la prima operazione una divisione tra interi e il risultato intero. Se
facciamo 9/4 con la calcolatrice (meglio se a mente) viene 2.25, per dal momento che il
nostro risultato memorizzato in un intero, il fatto che la variabile intA valga 2 non
sorprende molto, infatti il risultato viene apparentemente troncato.
Quello che non ci si aspetta (e qui state gi capendo che sto per dire qualcosa di strano)
che pur mettendo il risultato in una variabile di tipo float, questo ancora 2!
Il problema infatti non sta sul valore sinistro (come viene spesso nominato in inglese
left value o lvalue), ma su quello destro. Il compilatore quando deve svolgere l'operazione di
divisione prende le due variabili di tipo int, ovvero a 16 bit, e svolge l'operazione. Per
come sono strutturati i numeri interi, la parte decimale gi persa, dunque quello che
viene caricato nella variabile floatA effettivamente il risultato della divisione tra interi.
182

Per evitare che la divisione dia risultato 2 anche quando la variabile per il risultato di
tipo float, bisogna promuovere, ovvero fare il Casting di uno dei due numeri del valore
destro (o del dividendo o del divisore), ovvero:
floatA = (float) 9 / 4;

Dall'esempio si vede subito che fare il Casting significa mettere tra parentesi il tipo in
cui vogliamo convertire la nostra variabile (o costante in questo caso). A questo punto il
compilatore quando trova l'operazione vede che la divisione tra un float ed un int,
dunque nel fare la divisione, al fine di gestire i calcoli, utilizzer la struttura dati pi
complessa ovvero float.
Lo stesso risultato si sarebbe ottenuto qualora nel fare la divisione, o il dividendo o il
divisore gi di tipo float:
float floatA = 9;
floatB = floatA / 4;

In questo caso floatB vale 2.25 come nel caso in cui era stata fatta la promozione
volontaria a float.
Nel caso specifico, avendo fatto uso di solo costanti e non di variabili intero, piuttosto
che far uso del Casting, si potrebbe scrivere:
floatA = 9.0 / 4;

Ovvero una delle costanti deve essere dichiarata con il punto decimale. In questo modo la
costante viene ritenuta float ed il risultato 2.25 anche se non abbiamo fatto alcun
Casting. Nel caso di variabili il Casting l'unica soluzione per convertire un intero in float.
Alcune volte quando tutte le variabili sono di tipo float e si sta facendo una divisione
di cui non si ha interesse nella parte decimale, ovvero si vuole solo la parte intera della
divisione, pu ritornare utile fare un Casting ad int:
float floatA = 9;
floatB = (int) floatA / 4;

In questo caso floatB, grazie al Casting, ci permette di avere come risultato 2. Dunque
non tutti i mali vengono per nuocere, ma bisogna certamente stare attenti. Da quanto
spiegato sembra che il pericolo stia solamente nell'utilizzo dei float e con la divisione,
mentre in realt ogni volta che facciamo operazioni con strutture dati differenti, ovvero
tipi diversi, c' il rischio che a causa di una qualunque manipolazione delle variabili ci
siano delle perdite d'informazione. Un esempio lo abbiamo visto anche nel caso dei
puntatori, in cui il puntatore e la variabile puntata devono essere dello stesso tipo.
Vediamo ora un esempio tra char e int.
intA = 10;

183

charA = intA;
intA = 300;
charA = intA;

Nel primo caso carichiamo 10 in una variabile intera e poi carichiamo tale variabile int
in una char. Quello che ci si aspetta che charA valga 10. Effettivamente questo il
valore di charA. A questo punto sembra che tale operazione possa essere fatta in maniera
innocua, per ripetendo l'operazione con il valore di 300 vediamo che charA vale questa
volta 44!
Effettivamente 300 in binario si scrive 00000001 00101100, ovvero richiede due byte,
cosa che un int effettivamente ha. Nel momento in cui si carica il valore di un intero in
una variabile char, solo il primo byte viene caricato dunque solo i primi 8 bit, ovvero
00101100, cio il nostro 44. In questo caso abbiamo dunque perso delle informazioni ma
il Casting non ci avrebbe aiutato. Quello che avremo dovuto fare era quello di mettere il
risultato in un intero invece che in un char.
Questo secondo esempio serve anche per mettere in guardia nel caso in cui si facciano
operazioni complesse in cui valori intermedi possono causare l'overflow di qualche
variabile. Errore tipico potrebbe essere quello della moltiplicazione tra variabili, in cui il
valore intermedio pu facilmente eccedere il valore massimo della variabile.

Operatori logici
Frequentemente all'interno di un programma ci si trova a dover prendere delle
decisioni in base al particolare valore di una variabile. Al fine di poter effettuare dei test
sulle variabili, oltre a particolari parole chiave che vedremo a breve, sono necessarie anche
le operazioni logiche o operatori logici.
Gli operatori logici rappresentano quegli operatori che permettono al programma di
rispondere a delle domande con una risposta positiva, ovvero 1 logico, o negativa, ovvero
0 logico. Tali operatori sono:
||
&&
==
!=
<=
>=
>
<

:
:
:
:
:
:
:
:

operatore logico OR
operatore logico AND
operatore logico di uguaglianza
operatore logico diverso
operatore logico minore o uguale
operatore logico maggiore o uguale
operatore logico maggiore
operatore logico minore

Tali operatori saranno discussi in maggior dettaglio quando si parler delle istruzioni
condizionali, ovvero quelle istruzioni che permettono al programma di gestire le domande
poste per mezzo degli operatori logici. Come verr messo in evidenza parlando
dell'istruzione if (...) la domanda A uguale a B si pone scrivendo if (A==B) e non
if (A=B).

184

Operatori bitwise
Gli operatori bitwise permettono di manipolare un registro (variabile) variandone i
singoli bit. Tali operatori sono quelli che permettono di svolgere operazioni secondo
l'algebra booleana. Gli operatori bitwise sono:
operatore binario AND
operatore binario OR
operatore binario XOR
operatore complemento a 1 (i bit vengono invertiti)107
shift a sinistra
shift a destra
Come visibile and ed or binario sono molto simili a quelli logici, se non per il fatto che il
simbolo deve essere ripetuto solo una volta.
&
|
^
~
<<
>>

:
:
:
:
:
:

Vediamo a scopo riassuntivo le tabelle della verit dei vari operatori, applicati nel caso di
due ingressi. In Tabella 8 riportata la tabella della verit dell'operatore AND. La tabella
della verit dell'operatore AND (&) si pu leggere come:
L'uscita vale 1 solo nel caso in cui sia A che B valgono 1.
Input A

Input B

A&B

Tabella 8: Tabella della verit dell'operatore bitwise AND.

Da un punto di vista pratico scrivendo:


input_A = 0b01110001;
input_B = 0b01000001;
risultato = input_A & input_B;

si ha che il risultato, 01000001, ovvero solo i bit il cui valore 1 in entrambe le variabili
rimangono ad 1. Da questo si capisce che una possibile applicazione dell'operatore &
proprio quello di maschera, ovvero permette di azzerare i bit di cui non si ha interesse.
Quando si scrive un numero in formato binario 0b00000011 bisogna sempre
accertarsi si inserire il numero corretto di cifre dopo 0b. Qualora si dovessero
scrivere un numero maggiore di 0 o 1, il programma si potrebbe comportare in
maniera anomala a causa di troncamenti del numero.

107

Il simbolo ~ possibile inserirlo come carattere ASCII numero 126. Tenere premuto ALT, digitare 126 e poi rilasciare ALT.

185

In Tabella 9 riportata la tabella della verit dell'operatore OR. La tabella della verit
dell'operatore logico OR (|) si pu leggere come:
L'uscita vale 1 nel caso in cui o A o B valgono 1.
Input A

Input B

A|B

Tabella 9: Tabella della verit dell'operatore bitwise OR.

Scrivendo il seguente codice:


input_A = 0b01110001;
input_B = 0b00000100;
risultato = input_A | input_B;

Si ha che il risultato vale 01110101. Si capisce che l'operatore OR permette d'impostare


ad 1 un bit in un registro senza influenzare il valore degli altri bit.
In Tabella 10 riportata la tabella della verit dell'operatore XOR. L'operatore XOR
simile all'operatore OR ma la tabella della verit si legge:
L'uscita vale 1 nel caso in cui o A o B valgono 1 ma non tutti e due.
Input A

Input B

A ^B

Tabella 10: Tabella della verit dell'operatore bitwise XOR.

Scrivendo il seguente codice:


input_A = 0b01110001;
input_B = 0b00000101;
risultato = input_A ^ input_B;

Si ha che il risultato vale 01110100, ovvero i bit che hanno entrambi il valore 1 sono posti
a 0. Un tipico uso dell'operatore XOR il lampeggio di un LED.
Scrivendo per esempio:
LED ^= 0x01;

186

dove LED potrebbe essere definita come una costate per un pin qualunque del
microcontrollore, come per esempio:
#define LED PORTDbits.RD0

ogni volta che si esegue l'istruzione:


LED ^= 0x01;

il valore del pin viene invertito, infatti supponendo che inizialmente LED sia posto a 0,
facendo 0 ^ 1 si ha 1 (LED acceso). La seconda volta che si esegue l'istruzione si ha 1 ^ 1,
il risultato vale 0, per cui il LED si spegne. Continuando il discorso e mettendo un ritardo
tra un'esecuzione e l'altra dell'istruzione, si ha il lampeggio del LED.
In Tabella 11 riportata la tabella della verit dell'operatore NOT. La tabella
dell'operatore NOT (~) si legge:
L'uscita vale 1 se l'ingresso vale 0 mentre vale 0 se l'ingresso vale 1.
Input A

~A

Tabella 11: Tabella della verit dell'operatore bitwise NOT.

In altre parole l'operatore NOT inverte il valore dei bit nel registro.
Scrivendo il seguente codice:
input_A = 0b01110001;
risultato = ~input_A;

Si ha che il risultato vale 10001110, ovvero ogni bit risulta invertito.


Diversamente dagli operatori logici appena descritti gli operatori di shift intervengono
sulla variabile causando uno spostamento di un bit verso sinistra o verso destra, dei bit
che compongono il valore numerico originale. Il bit che viene inserito uno zero mentre
il bit che esce viene perduto. Spostare verso sinistra di un bit equivale a moltiplicare per 2,
mentre spostare verso destra di un bit equivale a dividere per 2.
Se l'operazione di divisione o moltiplicazione viene fatta su di un numero intero pari ad
una potenza di due bene utilizzare gli operatori di shift visto che richiedono ognuno un
solo ciclo istruzione e non causano la perdita del resto, visto che numeri potenze di due
diviso 2 darebbero come resto 0. Qualora il numero non sia una potenza di 2 sempre
possibile far uso degli operatori shift, ma bisogna tenere a mente che il resto viene
perduto. Utilizzando l'operatore shift si riesce a risparmiare tempo di esecuzione e spazio
in memoria. Vediamo un esempio estremo in cui si voglia dividere per 4 una variabile di
187

tipo unsigned

char:

charA = 16;
// Divisione classica
charB = charA /4;
charA = 16;
// Divisione per mezzo dello shift
charB = charA >> 2;

Dal momento che la divisione per 4 ovvero una potenza di due possibile utilizzare
lo shift a destra, di due posizioni, quale passo equivalente alla divisione per due.
Controllando il codice Assembly possibile vedere che effettivamente lo shift ha permesso
di rendere l'implementazione della divisione pi snella, permettendo di raggiungere
dunque maggiori velocit di esecuzione e risparmiare memoria.
12:
7F40
7F42
7F44
7F46
13:
14:
15:
7F48
7F4A
7F4C
7F4E
7F50
7F52
7F54
7F56
7F58
7F5A
7F5C
16:
17:
7F5E
7F60
7F62
7F64
18:
19:
20:
7F66
7F68
7F6A
7F6C

6E0A
0E10
6E0C
500A

dividendo_A = 16;
MOVWF 0xA, ACCESS
MOVLW 0x10
MOVWF dividendo_A, ACCESS
MOVF 0xA, W, ACCESS

C00C
F001
6A02
0E00
6E04
0E04
6E03
ECB8
F03F
5001
6E0B

// Divisione classica
risultato = dividendo_A /4;
MOVFF dividendo_A, dividend
NOP
CLRF 0x2, ACCESS
MOVLW 0x0
MOVWF 0x4, ACCESS
MOVLW 0x4
MOVWF divisor, ACCESS
CALL 0x7F70, 0
NOP
MOVF dividend, W, ACCESS
MOVWF risultato, ACCESS

6E0A
0E10
6E0C
500A

dividendo_A = 16;
MOVWF 0xA, ACCESS
MOVLW 0x10
MOVWF dividendo_A, ACCESS
MOVF 0xA, W, ACCESS

400C
42E8
0B3F
6E0B

// Divisione per mezzo dello shift


risultato = dividendo_A >> 2;
RRNCF dividendo_A, W, ACCESS
RRNCF WREG, F, ACCESS
ANDLW 0x3F
MOVWF risultato, ACCESS

Dal codice Assembly possibile vedere che per fare la divisione per 4 si effettuato uno
shift a destra di due posti (ovvero eseguita RRNCF per due volte) ottenendo come risultato
4. La divisione per 4 facendo uso dell'operatore matematico / richiede molte pi
istruzioni ovvero tempo di esecuzione.
Se ripetete l'esempio dividendo per 16 invece di 4 ci si accorge di una cosa strana,
188

l'operazione di shift non viene tradotta pi facendo uso dell'istruzione


SWAPF e poi l'azzeramento dei 4 bit pi significativi.
23:
7F60
7F62
7F64
7F66
24:
25:
26:
7F68
7F6A
7F6C

6E0A
0E10
6E0C
500A

dividendo_A = 16;
MOVWF 0xA, ACCESS
MOVLW 0x10
MOVWF dividendo_A, ACCESS
MOVF 0xA, W, ACCESS

380C
0B0F
6E0B

// Divisione per mezzo dello shift


risultato = dividendo_A >> 4;
SWAPF dividendo_A, W, ACCESS
ANDLW 0xF
MOVWF risultato, ACCESS

RRNCF

bens con

Questo esempio dovrebbe mettervi in guardia ancora una volta che non bisogna mai
ottimizzare il codice alla ceca, soprattutto quando sono presenti delle costanti. In
particolare l'esempio mette in evidenza che spesso il compilatore piuttosto sveglio, visto
che riassume l'esperienza e trucchi di molti programmatori esperti.
Ripetendo ancora una volta il nostro esempio facendo uso di variabili di tipo float si ha:
floatA = 16;
// Divisione classica
floatB = floatA /4;
floatA = 16;
// Divisione per mezzo dello shift
floatB = (unsigned int) floatA >> 2;

In questo caso necessario effettuare un Casting della variabile float ad intero, poich
l'operatore shift pu essere utilizzato solo per variabili di tipo intero o char. Controllando
il codice Assembly (non riportato per brevit) si pu vedere che in ambedue le
implementazioni sono necessarie molte decine di istruzioni. Dunque se si pensava di
ottimizzare il codice per mezzo dell'utilizzo dello shift si sarebbe fatto un buco
nell'acqua108. In casi come questo dove non si ha il beneficio voluto bene far uso della
forma del codice di pi facile lettura, ovvero:
floatB = floatA /4;

piuttosto che della seconda:


floatB = (unsigned int) floatA >> 2;

Anche questo esempio dovrebbe essere una lampadina da utilizzare nel vostro
bagaglio d'esperienza, nel caso in cui vogliate ottimizzare il codice.
Riprendiamo ora l'esempio discusso nel paragrafo del Casting:
intA = 300;
108

Compilatori differenti possono ottimizzare in maniera diversa, rendendo o meno l'utilizzo dello shift una soluzione valida per
ottimizzare il codice.

189

charA = intA;

In tale esempio si era messo in evidenza che trasferire un intero troppo grande in un
avrebbe portato alla perdita dei dati. Supponiamo ora di voler evitare la perdita di
dati, facendo uso di due variabili char:
char

intA = 300;
charA = intA;
charB = intA >> 8;

Grazie allo shift possibile recuperare il byte che andava perduto e caricarlo in charB.
Tale operazione risulta molto utile nel caso in cui si voglia per esempio memorizzare il
valore di un intero in memoria EEPROM, visto che la EEPROM composta da
locazioni di memoria da un byte. Altra situazione in cui potrebbe tornare utile
un'operazione di questo tipo potrebbe essere nel caso in cui si voglia inviare un itero sulla
porta seriale, visto che si pu trasmettere un solo byte alla volta.
Questo codice se pur funzionante non mette in evidenza il fatto che vi una
trasformazione in char dunque bene commentare tale operazione o far uso di un Casting
esplicito:
intA = 300;
charA = (unsigned char) intA;
charB = (unsigned char) (intA >> 8);

Fare il Casting non render il codice Assembly pi complesso, visto che implicitamente
comunque fatto, ma mette in evidenza che stiamo trasformando il nostro intero in char.
bene notare che in questa linea di codice l'operazione di shift messa tra parentesi.
charB = (unsigned char) (intA >> 8);

Se non si mettesse l'operazione tra parentesi, il Casting verrebbe applicato ad intA


perdendo dunque l'informazione del secondo byte, prima ancora di fare lo shift.

Istruzione condizionale while ( )


Frequentemente in un programma ci si trova nella situazione di dover eseguire in
maniera continua una parte di codice, fino al verificarsi di una certo evento o situazione.
Usando una terminologia anglosassone si dice che il programma esegue un loop, ovvero
una sequenza di istruzioni ripetute. Il controllo in maniera continua del verificarsi di un
determinato evento prende il nome di polling. Un loop potrebbe per esempio essere
rappresentato da un controllo software della pressione di un tasto.
Da quanto detto si capisce che l'istruzione while permette di fare in un certo qual
modo un controllo e ripetere una parte di programma. La sintassi dell'istruzione while
(...) ovvero finch (), :
while (espressione_logica) {

190

// Codice da eseguire fino a quando l'espressione logica valida


}

All'interno delle parentesi tonde contenuta l'espressione logica che se verificata


permette l'esecuzione del gruppo di istruzioni all'interno delle parentesi graffe.
L'espressione logica verificata se assume un valore diverso da 0, mentre risulta non
verificata se vale 0. Una volta eseguite le istruzioni contenute tra le parentesi graffe viene
eseguito nuovamente il controllo dell'espressione logica. Se l'espressione nuovamente
verificata viene nuovamente eseguito il blocco di istruzioni tra le parentesi graffe,
altrimenti il programma continua con la prima istruzione successiva alle parentesi graffe.
Si capisce che se l'espressione logica non sia verificata al suo primo controllo, il blocco di
istruzioni del while non verrebbe mai eseguito. Facendo uso di una rappresentazione
grafica, ovvero per mezzo del diagramma di flusso noto anche come flow chart, si pu
rappresentare il ciclo while come mostrato in Figura 88:

1
Blocco istruzioni

Espressione
Logica

Figura 88: Diagramma di flusso dell'istruzione while.

Con il ciclo while possibile ottenere dei cicli infiniti qualora l'espressione sia sempre
verificata. Il modo pi semplice per ottenere un ciclo infinito si ha semplicemente
scrivendo:
while (1) {
// Istruzioni da eseguire all'infinito
}

Questa istruzione stata gi utilizzata molte volte in maniera pi o meno


inconsapevole. Un altro modo equivalente potrebbe essere:
while (123) {
// Istruzioni da eseguire all'infinito
}

Un altro modo potrebbe ancora essere:

191

while (a=5) {
// Istruzioni da eseguire all'infinito
}

In questo caso infatti, il risultato dell'espressione 5 cio diversa da 0, dunque equivale


ad una condizione sempre vera. Si ricorda che a=5 non un operazione logica di
verifica per constare se a sia pari a 5, bens un'assegnazione. L'operazione logica
dovrebbe avere il formato a==5 e non a=5.
Si capisce che gli ultimi due modi sono un po' bizzarri anche se effettivamente validi. In
particolare l'ultimo esempio potrebbe in realt essere il risultato di uno sbaglio derivante
dal voler scrivere l'espressione logica:
while (a==5) {
}

// Istruzioni da eseguire

Nel caso in cui un loop diventi infinito, ovvero la condizione logica dovesse
essere sempre verificata, bene controllare con attenzione l'espressione logica e
verificare che non si siano commessi errori come per esempio l'utilizzo
dell'operatore assegnazione = piuttosto che l'operatore logico ==. Il
compilatore potrebbe generare delle Warning nel caso sospetti errori.
Una volta all'interno di un ciclo while, vi sono due modi per uscirne. Il primo modo
quello spiegato precedentemente, ovvero l'espressione logica non viene pi verificata.
Questo tipo di uscita non permette per di uscire da un ciclo infinito come quelli
precedentemente descritti, o comunque avere delle uscite premature dovute per esempio
al verificarsi di errori. In questo caso pu risultare comoda l'istruzione break. Quando
viene eseguita l'istruzione break il programma procede con la prima istruzione successiva
alle parentesi graffe, dunque esce dal ciclo while senza troppe domande. L'istruzione
break pu essere utilizzata anche all'interno di cicli while che non siano infiniti,
permettendo al programmatore di avere altre condizioni di uscita dal ciclo. Vediamo un
semplice esempio di utilizzo di un ciclo while :
#include <xc.h>
#include "PIC18F4550_config.h"
int main (void) {
// Variabile usata per il conteggio da visualizzare su PORTD
unsigned char numero = 0;
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;

192

// Imposto PORTC tutti ingressi


LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
while (numero < 16) {
// Visualizzo in uscita il valore di numero
LATD = numero;
// Incremento della variabile numero
numero++;
}
// Ciclo infinito
while (1) {
}
}

La parte iniziale del programma non richiede particolari spiegazioni. Dal momento che
la variabile numero inizializzata a 0, il controllo nel ciclo while viene verificato e si entra
nel loop.
while (numero < 16)

All'interno del loop avviene l'incremento della variabile numero:


numero++;

Il loop viene ripetuto fino a quando la variabile numero minore di 16 ovvero fino a 15.
Tale valore anche l'ultimo valore visualizzato sui LED ovvero 0x0F, in binario
00001111. Eseguendo il programma, l'esecuzione talmente veloce che viene visualizzato
solo il valore 00001111 come se il conteggio non sia avvenuto. Per vedere il conteggio
necessario effettuare il Debug del programma ed eseguire le varie istruzioni passo passo.
Ora vediamo un altro esempio molto simile al precedente in cui si utilizza l'istruzione
per avere un'uscita prematura dal ciclo while.

break

#include <xc.h>
#include "PIC18F4550_config.h"
int main (void) {
// Variabile usata per il conteggio da visualizzare su PORTD
unsigned char numero;

193

// Imposto PORTA tutti ingressi


LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// Inizializzazione della variabile numero
numero = 0;
while (numero < 16) {
// Visualizzo in uscita il valore di numero
LATD = numero;
// Incremento della variabile numero
numero++;
// Controllo extra sulla variabile numero
if (numero == 10) {
break;
}
}
// Ciclo infinito
while (1) {
}
}

Il nuovo esempio praticamente identico al precedente se non per l'aggiunta di un


controllo sul valore della variabile numero dopo il suo incremento. Il controllo viene
effettuato con l'istruzione if, i dettagli su questa istruzione verranno dati a breve ma
intuitivamente fa un controllo sulla variabile numero e se vale 10 effettua il break dal loop.
Questo significa che questa volta il conteggio si ferma a 10 e non pi a 15. Si noti che il
conteggio si ferma a 10 ma a fine programma LATD 9 ovvero 00001001, infatti
raggiunto il valore 10 il registro LATD non pi aggiornato.
Abbiamo visto che il ciclo while ci permette di effettuare un controllo sulla variabile
prima di entrare nel loop, e solo se il controllo viene verificato viene eseguito il loop al
meno una volta. Ci sono casi in cui si pu desiderare che il blocco istruzioni sia eseguito
almeno una volta e solo dopo avvenga il controllo. Per questi casi, anche se il ciclo while
potrebbe essere modificato per supportare questo caso, c' una variante del while
dedicata al caso, ovvero:
194

do {
// Codice da eseguire
} while ( espressione_logica)

Da un punto di vista logico rimane tutto uguale, ma l'istruzione while posta alla fine
del blocco istruzione da eseguire, che come detto sono eseguite certamente almeno una
volta. Una rappresentazione per mezzo di un diagramma di flusso riportata in Figura 89.

Blocco istruzioni

Espressione
Logica

Figura 89: Diagramma di flusso dell'istruzione do - while.

Il ciclo for ( )
Come abbiamo appena visto per l'istruzione while, i programmi difficilmente sono
lineari, ovvero una semplice sequenza di istruzioni da compiere dall'inizio alla fine. Spesso
l'esecuzione del programma viene interrotta per fare dei test o ripetere delle porzioni del
programma stesso. Ripetere una sezione del programma equivale a fare dei salti
all'indietro in modo da riposizionare il registro PC all'inizio del ciclo.
Per eseguire un numero di volte prefissato una determinata istruzione, il modo pi
semplice per mezzo costrutto for(), il quale permette appunto di eseguire un numero
definito di volte una certa operazione o insieme di operazioni. La sua sintassi :
for (espressione1; espressione2; espressione3) {
// Blocco da ripetere
}

195

Si pu vedere che il ciclo for composto da un blocco con parentesi graffe al cui
interno sono scritte l'insieme di istruzioni da ripetere. Prima del blocco da ripetere, vi la
parola chiave for e tre espressioni tra parentesi109:
espressione1: Usata per l'inizializzazione della variabile di controllo del loop.
espressione2: Controllo della condizione di fine loop.
espressione3: Operazione da svolgere sulla variabile per il controllo del loop.
Da quanto appena detto si capisce che, sebbene un modo semplice per ripetere una
sequenza di istruzioni sia per mezzo del costrutto for, in realt questo comporta molte
pi istruzioni e complessit se paragonato al costrutto while. Il diagramma di flusso di un
ciclo for riportato in Figura 90.

Inizializzazione

Espressione
Logica

1
Incremento
o altra operazione

Blocco istruzioni

Figura 90: Diagramma di flusso del costrutto for.

Vediamo di capirci qualcosa con il seguente esempio :


#include <xc.h>
#include "PIC18F4550_config.h"
// Valore massimo per il conteggio
#define MAX_VALUE 86
int main (void){
109

Ogni espressione pu essere in realt un gruppo di espressioni, ma si sconsiglia di utilizzare tale tecnica poich di pi
difficile lettura.

196

// Variabile per il conteggio


unsigned char i;
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
for (i=0; i < MAX_VALUE; i++) {
}

LATD = i;

// Ciclo infinito
while (1) {
}

La parte iniziale del programma non richiede particolari commenti. Si noti solo che si
voluto definire una costante MAX_VALUE, la quale viene utilizzata come limite per il
controllo del loop. Il suo utilizzo non obbligatorio, ma ritorna utile in applicazioni reali,
permettendo di cambiare il numero di volte che viene ripetuto il loop. Si noti inoltre che
PORTD stata impostata come output, in modo da visualizzare il valore i.
Iniziamo a vedere in maggior dettaglio le espressioni del ciclo for. La prima
espressione inizializza la variabile i, precedentemente dichiarata, questa utilizzata per il
conteggio delle volte per cui deve essere ripetuto il loop. Normalmente le variabili di
conteggio vengono inizializzate a 0 ma in base alle esigenze possibile iniziare da un
valore differente. La seconda espressione effettua ad ogni ciclo un controllo sullo stato
della variabile per vedere se la condizione verificata. In questo caso volendo fare un
ciclo con MAX_VALUE iterazioni si posto i = 0 come inizio e i < MAX_VALUE come fine.
La terza espressione del ciclo for il tipo di conteggio che si vuole avere, in questo
caso, partendo da 0 e volendo raggiungere MAX_VALUE bisogna incrementare di 1, dunque
si scritto i++. Se il punto di partenza fosse stato MAX_VALUE e quello di arrivo fosse stato
>0 si sarebbe dovuto scrivere i--110, ovvero:
for (i=MAX_VALUE; i > 0; i--) {
}
110

LATD = i;

In questo caso si parlato solo di i++ e i-- ma in realt si possono scrivere anche altre espressioni.

197

Le istruzioni che si vogliono ripetere per MAX_VALUE volte sono contenute all'interno
delle parentesi graffe. In questo caso si ha la sola istruzione che scrive il valore di i che si
scrive nel registro LATD. Per poter vedere il conteggio sui LED posti sulla PORTD
necessario eseguire il programma passo passo, ovvero in fase di Debug.
Sebbene il conteggio al contrario possa sembrare semplicemente un'alternativa al
caso di incremento, viene in realt utilizzato in applicazioni a batteria al fine di
risparmiare energia. Infatti per un microprocessore pi facile contare al
contrario e vedere se si arrivati a zero che non contare in vanti e vedere se si
arrivati al valore desiderato.
Vediamo un semplice esempio in cui si mette a confronto un semplice loop while, for sia
in avanti che indietro, usando una variabile i di tipi unsigned char.
// Esempio di ciclo for in avanti
for (i=0; i < MAX_VALUE; i++) {
LATD = i;
}
// Esempio di ciclo for indietro
for (i= MAX_VALUE; i > 0; i--) {
LATD = i;
}
// Esempio di ciclo while in avanti
i = 0;
while (i<MAX_VALUE){
LATD = i;
i++;
}
// Esempio di ciclo while indietro
i = MAX_VALUE;
while (i>0){
LATD = i;
i--;
}

L'esempio interessante perch mette a confronto i due costrutti for e while, ed in


particolare l'esigenza del while di avere un'inizializzazione della variabile per il conteggio e
relativo incremento. Visualizzando il codice Assembly si ha:
34:
35:
7FAA
7FAC
7FAE
7FB0
7FB2
7FB8
7FBA
7FBC
7FBE
36:

6E01
0E00
6E02
5001
D003
2A02
0E55
6402
D7FA

// Esempio di ciclo for in avanti


for (i=0; i < MAX_VALUE; i++) {
MOVWF 0x1, ACCESS
MOVLW 0x0
MOVWF i, ACCESS
MOVF 0x1, W, ACCESS
BRA 0x7FBA
INCF i, F, ACCESS
MOVLW 0x55
CPFSGT i, ACCESS
BRA 0x7FB4
LATD = i;

198

7FB4
7FB6
37:
38:
39:
40:
7FC0
7FC2
7FC4
7FC6
7FC8
7FCE
7FD0
7FD2
41:
7FCA
7FCC
42:
43:
44:
45:
46:
7FD4
7FD6
7FD8
7FDA
47:
7FDC
7FE4
7FE6
7FE8
48:
7FDE
7FE0
49:
7FE2
50:
51:
52:
53:
7FEA
7FEC
7FEE
7FF0
54:
7FF2
7FFA
7FFC
55:
7FF4
7FF6
56:
7FF8
57:

C002
FF8C

6E01
0E56
6E02
5001
D003
0602
6602
D7FB
C002
FF8C

6E01
0E00
6E02
5001
D003
0E55
6402
D7FA
C002
FF8C
2A02

6E01
0E56
6E02
5001
D003
6602
D7FB
C002
FF8C
0602

MOVFF i, LATD
NOP
}
// Esempio di ciclo for indietro
for (i= MAX_VALUE; i > 0; i--) {
MOVWF 0x1, ACCESS
MOVLW 0x56
MOVWF i, ACCESS
MOVF 0x1, W, ACCESS
BRA 0x7FD0
DECF i, F, ACCESS
TSTFSZ i, ACCESS
BRA 0x7FCA
LATD = i;
MOVFF i, LATD
NOP
}
// Esempio di ciclo while in avanti
i = 0;
MOVWF 0x1, ACCESS
MOVLW 0x0
MOVWF i, ACCESS
MOVF 0x1, W, ACCESS
while (i<MAX_VALUE){
BRA 0x7FE4
MOVLW 0x55
CPFSGT i, ACCESS
BRA 0x7FDE
LATD = i;
MOVFF i, LATD
NOP
i++;
INCF i, F, ACCESS
}
// Esempio di ciclo while indietro
i = MAX_VALUE;
MOVWF 0x1, ACCESS
MOVLW 0x56
MOVWF i, ACCESS
MOVF 0x1, W, ACCESS
while (i>0){
BRA 0x7FFA
TSTFSZ i, ACCESS
BRA 0x7FF4
LATD = i;
MOVFF i, LATD
NOP
i--;
DECF i, F, ACCESS
}

Il codice lascia piuttosto delusi visto che al livello di ottimizzazione utilizzano non si
notano grandi differenze. Ciononostante se si contano il numero di istruzioni dentro il
blocco che viene effettivamente ripetuto, si pu notare che il ciclo for richiede circa 10
istruzioni contro 6 per il ciclo while. Questo mostra come un ciclo while possa tornare
pi leggero, soprattutto in cicli che non contengono molte istruzioni da ripetere e devono
199

essere ripetuti molte volte.


Si fa notare che le varie espressioni interne al ciclo for possono essere pi di una. Per
esempio nell'espressione di inizializzazione si potrebbero inizializzare due variabili, come
anche avere pi incrementi di variabile. Ciononostante l'espressione logica da verificare
deve essere solo una anche se di tipo pi complesso. Per esempio si potrebbe scrivere:
for (x=0, y=0; (x<5) & (y<3);

x++, y++) {

Nonostante sia possibile una forma di programmazione di questo tipo, bene limitarne
l'uso visto che il codice diventa pi difficile da comprendere.
Vediamo un altro semplice esempio in cui si fa uso del ciclo for e in cui vengono
utilizzati anche gli Array, in modo da prendere dimestichezza con entrambi e vedere come
inserire anche un ritardo. In questo programma sono anche illustrate alcune pratiche di
programmazione che bene non usare, anche se il programma funziona comunque senza
problemi.
#include <xc.h>
#include "PIC18F4550_config.h"
#define MAX_VALUE 10
void main (void){
// Variabile per il Conteggio e Delay
unsigned char i;
unsigned int j;
// Definizione dell'Array
unsigned char mioArray [MAX_VALUE];
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// Inizializzazione dell'Array
for (i = 0; i < MAX_VALUE; i++)
mioArray[i] = i;

200

for (i=0; i < MAX_VALUE; i++) {


// Ritardo
for (j =0; j < 64000; j++);
}

LATD = mioArray[i];

// Ciclo infinito
while (1) {
}

Anche in questo caso non c' molto da dire per la parte iniziale del programma. Si noti
solo che la nostra costante MAX_VALUE stata adesso utilizzata in pi punti, dunque il
cambio del suo valore permette di aggiornare l'intero programma senza dover cambiare
pi punti del codice sorgente, questa proprio l'utilit delle costanti. L'Array
inizializzato utilizzando un ciclo for, per mezzo del quale possibile caricare il valore
dell'indice all'interno della variabile indicizzata dell'Array.
// Inizializzazione dell'Array
for (i = 0; i < MAX_VALUE; i++)
mioArray[i] = i;

Tale parte del codice meglio scriverla in questo modo:


// Inizializzazione dell'Array
for (i = 0; i < MAX_VALUE; i++) {
mioArray[i] = i;
}

ovvero facendo uso delle parentesi graffe. In questo modo si mette in evidenza che nel
ciclo for viene svolta solo questa operazione. Vediamo ora il nostro nuovo loop :
for (i=0; i < MAX_VALUE; i++) {
// Ritardo
for (j =0; j < 64000; j++);
}

LATD = mioArray[i];

L'inizializzazione del primo loop, ovvero ciclo for, praticamente uguale al


precedente, in particolare si fa nuovamente uso della variabile i, la quale viene
nuovamente inizializzata a 0. All'interno del ciclo possibile vedere che, piuttosto che
porre il valore dell'indice i direttamente su LATD, si scrive il valore dell'Array che viene
scansionato cella per cella per mezzo del loop stesso. In ogni modo ogni cella i dell'Array,
come sappiamo dalla nostra inizializzazione, ha il valore di i. La differenza in questo loop,
rispetto agli esempi precedenti, dovuto al fatto che presente al suo interno un secondo
loop, il cui scopo semplicemente di contare senza fare nulla, ovvero perdere solo
tempo...in questo caso utile per rendere visualizzabile il conteggio senza dover eseguire il
201

programma in modalit passo-passo. Si noti che il ciclo for possiede il punto e virgola alla
fine della sua dichiarazione, visto che non possiede nessuna istruzione. Questa linea di
codice in realt meglio scriverla:
for (j =0; j < 64000; j++) {
// Non faccio nulla
}

In questo modo si mette chiaramente in evidenza che il ciclo rappresenta solo un


ritardo. Infatti quando si ha un ciclo di questo tipo facile fraintendere che l'istruzione
successiva venga eseguita 64000 volte! Altra pratica da evitare in cicli annidati l'utilizzo
di variabili dal nome i,j,x,y,z... Immaginate una matrice a due dimensioni che venga
scansionata n volte e chiamare gli indici i,x,y!

Quale codice si legge meglio?

Questo:
for (i = 0; i < 10; j++)
for (x = 0; x < 100; x++)
for (y = 0; y < 100; y++) {
mat[x][y] = (i * 3) + x;
}

o questo:
for (num_scan = 0; num_ scan < NUMERO_SCANSIONI_MAX; num_scan)
for (riga = 0; riga < MAX_RIGA; riga++)
for (colonna = 0; colonna < MAX_COLONNA; colonna++) {
matrice[riga][colonna] = (num_scan * 3) + riga;
}

In questo secondo caso, sebbene si faccia uso di nomi pi lunghi e il codice possa
sembrare pi lungo, viene in realt tradotto in Assembly come il codice con variabili meno
leggibili. Un'ultima nota sul ciclo for riguarda il caso particolare in cui non scritta
nessuna espressione, come sotto riportato:
for ( ; ;) {
//ciclo infinito
}

Quello che si ottiene un ciclo infinito, ovvero il microcontrollore esegue all'infinito il


blocco di istruzioni del ciclo for. Il ciclo risulta infinito poich non c' nessuna
condizione da verificare. Il suo comportamento praticamente uguale all'istruzione
while (1).

202

Allo stesso modo del ciclo while, l'istruzione break permette di interrompere il
ciclo ed uscire in maniera prematura. Oltre all'istruzione break, presente
l'istruzione continue, che permette di saltare il resto del codice presente nel
ciclo, sia esso for o while, e riprendere il codice dall'esecuzione del controllo
dell'istruzione condizionale.

Istruzione condizionale if ( )
In ogni programma di fondamentale importanza poter controllare una variabile o un
particolare stato, e decidere se fare o meno una determinata operazione. In C possibile
porre domande e decidere, facendo uso dell'istruzione if () ovvero se (...). Per questa
istruzione sono presenti due diverse sintassi, la prima :
if (espressione_logica) {
}

// Programma da eseguire se l'espressione logica verificata

Per mezzo di questa sintassi possibile eseguire una parte di programma se


l'espressione logica all'interno delle parentesi tonde verificata. Per espressione logica si
intende una qualunque espressione ottenuta per mezzo degli operatori logici. In
particolare un'espressione logica si dice verificata se pari ad un qualunque valore diverso
da 0, mentre per espressione logica non verificata si intende un valore pari a 0. Le
espressioni logiche tradizionali, precedentemente viste, ritornano 1 se l'espressione logica
verificata 0 altrimenti. Come per il ciclo for (...) anche per l'istruzione if (...) il
programma da eseguire contenuto all'interno di parentesi graffe. Il diagramma di flusso
per l'istruzione semplice if riportato in Figura 91.

Espressione
Logica

Blocco istruzioni

Figura 91: Diagramma di flusso del costrutto if.

La seconda sintassi :
203

if (espressione_logica) {
// Programma da eseguire se l'espressione logica verificata
} else {
// Programma da eseguire se l'espressione logica non verificata
}

Per mezzo della seconda sintassi, in cui presente anche la parola chiave else,
possibile eseguire un secondo blocco di istruzioni qualora l'espressione logica non
verificata, il secondo blocco introdotto proprio per mezzo della parola chiave else
(altrimenti).
Il diagramma di flusso per l'istruzione semplice if riportato in Figura 92.

Espressione
Logica

Blocco istruzioni

Blocco istruzioni

Figura 92: Diagramma di flusso del costrutto if - else.

Vediamo qualche semplice esempio con la prima sintassi:


if ( i == 3) {
LATD = i;
}

In questo esempio PORTD riflette in uscita il valore di i solo se i uguale a 3.


Dunque se i, nel momento in cui viene effettuato il controllo vale 2, il blocco di istruzioni
all'interno delle parentisi graffe non viene eseguito. Si fa notare che per verificare che i sia
uguale a 3 bisogna scrivere i==3 e non i=3. Questo tipo di errore tipico se si ha
esperienza di programmazione con il Basic o Pascal. Il C non ci viene in generale in aiuto
nella risoluzione di questi problemi, anche se sono presenti compilatori che rilasciano una
Warning nel caso in cui vedono assegnazioni sospette come espressioni logiche. Il C infatti
permette al programmatore di scrivere anche i=3 come nel seguente esempio:
204

if ( i = 3) {
LATD = i;
}

Il compilatore non segnala nessun errore e il programma viene anche eseguito dal PIC;
il problema sta nel fatto che quando viene eseguita l'operazione if (i=3) l'espressione
logica sempre verificata poich maggiore di 1, ovvero 3, dunque per il C vera.
Dunque come effetto collaterale si ha che PORTD viene impostata a 3 ogni volta che
viene eseguita l'istruzione if (i=3).
Vediamo ora un esempio completo in cui si effettua la lettura di un pulsante collegato
tra massa e RB4 e si pilotano dei LED sulla PORTD.
#include <p18f4550.h>
#include "PIC18F4550_config.h"
int main (void){
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// Abilita i resistori di pull-up sulla PORTB
INTCON2bits.RBPU = 0x00;
// Ciclo infinito
for (;;) {
if (PORTBbits.RB4 == 0) {
// Ho premuto il pulsante su 0 su RB4
LATD = 0x0F;

}
else {

// Il pulsante aperto
LATD = 0xF0;
}

205

Questa volta il programma diventa interessante...comincia ad essere un po' pi utile.


Per permettere la lettura dei pulsanti necessario abilitare i resistori di pull-up sulla
PORTB, dove sono appunto collegati i pulsanti. Quando si legge lo stato logico di un
pulsante o un interruttore sempre necessario avere un resistore di pull-up o di pulldown111. In questo caso dal momento che la PORTB possiede al suo interno dei resistori
di pull-up...perch non sfruttarli?! Per poter attivare i resistori di pull-up si esegue
l'istruzione:
INTCON2bits.RBPU = 0x00;

che setta a 0x00 il bit RBPU all'interno del registro INTCON2. Questa opzione abilita i
resistori di pull-up su tutti i pin della porta B settati come ingressi.
Per poter leggere il pulsante si provveduto a realizzare un numero infinito di letture 112
sul pin RB4 di PORTB facendo uso del ciclo for senza parametri, tanto per non usare il
tipico while (1).
All'interno del ciclo infinito si effettuato il controllo del bit RB4 di PORTB per
mezzo della variabile PORTBbits.RB4. Poich sono stati attivati i resistori di pull-up e il
pulsante collegato verso massa si ha che normalmente il valore di RB4 pari a 1 logico.
Se il controllo effettuato dall'if vale 0 viene eseguito il blocco di istruzioni dell' else,
dunque PORTD viene impostato con il valore 0xF0, ovvero i LED collegati sui quattro
bit pi significativi sono accesi, tali LED rimangono accesi fino a quando non si preme il
pulsante su RB4. Quando si preme il pulsante, il pin RB4 vale 0 logico (collegato a massa),
dunque la condizione if viene verificata e PORTD viene impostato a 0x0F, ovvero si
accenderanno i LED sulla PORTD associati ai bit meno significativi. Poich il ciclo for
infinito, RB4 viene continuamente testato, dunque quando si rilascia il pulsante, PORTD
viene impostato nuovamente a 0xF0.
Normalmente quando si leggono dei pulsanti la procedura ora utilizzata non
sufficiente, anche se si ha l'impressione che tutto funzioni correttamente. Infatti quando si
legge un pulsante sempre bene accertarsi che il pulsante sia stato effettivamente
premuto e in particolare non interpretare una singola pressione come pi pressioni. Infatti
quando si preme un pulsante si vengono a creare degli spike, ovvero l'ingresso del PIC ha
un treno di 0 e 1, che se non opportunamente filtrati possono essere interpretati come
pressioni multiple del pulsante stesso.
Per filtrare l'ingresso a cui collegato il pulsante, si inserisce generalmente una pausa
dopo aver rilevato la pressione del pulsante stesso e si effettua poi una seconda lettura per
essere certi che il pulsante stato effettivamente premuto. Questa tecnica pu essere
implementata sia per mezzo di hardware esterno che per via software, ed nota come
111

112

Un resistore di pull-up collega un ingresso a Vcc, mentre un resistore di pull-down collega un ingresso a massa. Questi
resistori, o l'uno o l'altro risultano indispensabili quando si deve leggere uno stato di un pulsante o un interruttore. Infatti
quando il pulsante/interruttore aperto l'ingresso rimarrebbe fluttuante ovvero ad un livello logico indeterminato, mentre
per mezzo del resistore l'ingresso vincolato o a Vcc o a GND. L'utilizzo dell'uno o dell'altro dipende da come si collega il
pulsante. Se si collega il pulsante a massa, bisogna usare un resistore di pull-up mentre nel caso in cui si collega il pulsante a
Vcc, si bisogna usare un resistore di pull-down.
La tecnica di leggere continuamente lo stato di un ingresso per catturarne una sua variazione di stato viene detta polling.
Questa si contrappone alla tecnica dell'Interrupt (interruzione) in cui il microcontrollore libero di svolgere altre operazioni
invece di leggere continuamente un ingresso, poich viene avvisato (Interrupt) quando l'ingresso subisce una variazione.

206

filtro antirimbalzo. Nel nostro esempio il filtro implementato via software, visto che
possibile risparmiare componenti esterni.
Nel seguente esempio si incrementa una variabile e la si pone in uscita ad ogni pressione
del pulsante posto sul pin RB4:
#include <xc.h>
#include "PIC18F4550_config.h"
int main (void){
// Variabile usata per creare un conteggio fittizio di pausa
unsigned int i;
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// Abilita i resistori di pull-up sulla PORTB
INTCON2bits.RBPU = 0x00;
// Ciclo infinito
for (;;) {
if (PORTBbits.RB4 == 0) {
// Pausa che filtra gli spike
for (i=0;i<32000; i++) {
}
// Controllo nuovamente il pulsante per vedere
// se ancora premuto
if (PORTBbits.RB4 == 0) {

}
}

// Ho premuto il pulsante su 0 su RB4


LATD++;

Questo programma almeno all'inizio molto simile al precedente. Unica differenza


207

l'introduzione della variabile i utilizzata per implementare una pausa per mezzo di un
ciclo for. possibile notare che all'interno del ciclo infinito il filtro antirimbalzo viene
realizzato nel seguente modo: si effettua prima la lettura del pulsante, qualora questo
risulti attivo c' la piccola pausa (conteggio fittizio) 113. Dopo il filtraggio viene nuovamente
riletto il pulsante per vedere se ancora premuto. Qualora sia ancora premuto vuol dire
che effettivamente si premuto il pulsante. In questo caso quello che viene fatto
incrementare il registro di uscita LATD, per cui si vede in uscita il conteggio binario da 0
a 255. Una volta che LATD arriva a 255 il conteggio riprende da 0. Inoltre per come
impostato il programma, tenendo premuto il pulsate, il conteggio viene fatto in
automatico (come quando si imposta l'ora dell'orologio digitale) e premendo il pulsante
troppo rapidamente la pressione viene ignorata114.
Qualora il conteggio (pausa) del ciclo for dovesse essere tolto, si pu notare
come a causa degli spike del pulsante il conteggio viene fatto quasi come se il
microcontrollore stia dando i numeri. L'incremento sempre di 1 ma vengono
contati tutti gli spike...creando cosi apparenti incrementi a passi di 50 e oltre...ma
in maniera casuale!
Si fa notare che spesso i pulsanti non vengono letti in polling piuttosto si fa uso delle
interruzioni. Questo argomento per oggetto di un altro Capitolo.
Frequentemente si ha l'esigenza di testare pi condizioni al fine di prendere una
decisione, in particolare se si devono controllare due condizioni logiche e decidere se
accendere un LED, un motore o qualsiasi altro dispositivo, ci si potrebbe porre la
domanda: condizione_1 e condizione_2 sono entrambe verificate? O ancora, si
verificato solo un evento?
Per rispondere alla prima domanda si potrebbe fare in questo modo, utilizzando due
in successione:

if

if (condizione_1 == 1)
if (condizione_2 == 1){
}

// Programma da eseguire se le condizioni sono entrambe verificate

Un altro modo che si potrebbe utilizzare per mezzo dell'operatore logico AND
ovvero:

&&,

if ((condizione_1 == 1) && (condizione_2 == 1)) {


}

// Programma da eseguire se le condizioni sono entrambe verificate

Si noti che ogni singola espressione posta all'interno di parentesi tonde. In particolare
possibile utilizzare l'operatore && anche con pi termini, ma bene, per ragioni di
113
114

In generale per ottenere un buon filtro antirimbalzo si raccomandano circa 5-10ms di attesa.
Si capisce che estendendo di molto il filtro antirimbalzo si pu creare la funzione tipica dei pulsanti ON-OFF, ovvero di
non accendere o spegnere il sistema se non si tiene premuto il pulsante per almeno 1-2 secondi. In questo modo si filtrano
oltre che agli spike, anche le pressioni accidentali.

208

leggibilit non eccedere con la fantasia. Oltre all'operatore && si pu utilizzare anche
l'operatore OR ||, o altro operatore logico o combinazione degli stessi.
Vediamo qualche dettaglio riguardo all'utilizzo della parola chiave
appena descritti:

else

negli esempi

if ((condizione_1 == 1) && (condizione_2 == 1)) {


// Programma da eseguire se le condizioni sono entrambe verificate
} else {
// Codice eseguito se solo una condizione o nessuna sono verificate.
}

In questo caso capire quando viene eseguito il blocco else piuttosto semplice poich
abbiamo a che fare con un solo if. Il blocco else viene eseguito se la condizione_1 e
condizione_2 non sono verificate, ovvero una sola condizione o nessuna delle due
verificata.
Riprendendo il primo esempio, in cui si invece fatto utilizzo di due if:
if (condizione_1 == 1)
if (condizione_2 == 1){
// Programma da eseguire se le condizioni sono entrambe verificate
} else {
// Programma da eseguire se la condizione 1 vera e la 2 no!
}

Il nostro blocco else, apparentemente posto come il precedente ha un ruolo diverso,


infatti appartiene al blocco del secondo if e non del primo! Per tale ragione le istruzioni
del blocco else vengono eseguite qualora la condizione_1 verificata e la condizione_2
non verificata, il che diverso da quello ottenuto precedentemente! Riscrivendo il
codice in questo modo:
if (condizione_1 == 1) {
if (condizione_2 == 1){
// Programma da eseguire se le condizioni sono entrambe verificate
} else {
// Programma da eseguire se la condizione 1 vera e la 2 no!
} else {
// Programma da eseguire se la condizione 1 falsa.
// Lo stato della 2 indifferente!
}

In questo secondo esempio, stata inserita la parentesi graffa anche per il primo if,
creando in questo modo una chiara evidenza a chi appartiene ogni else, l'indentazione
209

aiuta molto alla comprensione. Questo codice si sarebbe potuto scrivere anche in questo
secondo modo, ma lo sconsiglio poich ancor meno leggibile del primo.
if (condizione_1 == 1)
if (condizione_2 == 1){
// Programma da eseguire se le condizioni sono entrambe verificate
} else {
// Programma da eseguire se la condizione 1 vera e la 2 no!
}
else {
// Programma da eseguire se la condizione 1 falsa.
// Lo stato della 2 indifferente!
}

Da quanto detto si capisce che utilizzare due if non proprio come utilizzare
l'operatore AND, se non quando condizione_1 e condizione_2 sono entrambe verificate.
Un problema dell'utilizzo degli if in successione legata al fatto che possono rendere il
codice poco leggibile per tale ragione, in alcuni casi, piuttosto che concatenare molti if
meglio scrivere pi blocchi indipendenti, fare uso degli operatori logici, o se possibile far
uso del costrutto switch.

Istruzione condizionale switch ( )


Quando ci sono molte condizioni da verificare o controllare, pu ritornare utile, quale
alternativa a molti if concatenati, l'istruzione condizionale switch. La sintassi
dell'istruzione switch la seguente:
switch (variabile) {
case valore:
// Istruzioni da eseguire
break;
case valore:
// Istruzioni da eseguire
break;
case valore:
// Istruzioni da eseguire
break;
case valore:
// Istruzioni da eseguire
break;
default:

// Istruzioni da eseguire

Dopo la parola chiave

switch

posta tra parentesi tonde la variabile da controllare.


210

Tutti i casi da verificare sono posti al fianco della parola chiave case. Il valore che si vuole
controllare deve essere un valore costante ed intero, gi noto a priori, ovvero non si pu
porre come valore un'altra variabile. Questo fatto esclude l'utilizzo dell'istruzione
condizionale case qualora si vogliano fare confronti tra variabili. In questo caso si deve
infatti utilizzare l'istruzione condizionale if. Il diagramma di flusso per l'istruzione switch
riportato in Figura 93.

Espressione
Logica

Blocco istruzioni
+ break

Espressione
Logica

Blocco istruzioni
+ break

0
Blocco istruzioni
default

Figura 93: Diagramma di flusso del costrutto switch.

Il valore usato per il confronto pu essere scritto come intero, o come carattere; per
esempio:
case 128:

o anche
case 'b':

La costante potrebbe anche essere definita per mezzo della direttiva #define e si
potrebbe utilizzare il suo nome al posto del valore numerico. Questo secondo approccio
in generale quello consigliato in applicazioni professionali.

211

In alcuni casi si tentati di usare il costrutto switch per confrontare delle


stringhe. Questo non non supportato. Per poter effettuare il confronto tra
stringhe bisogna realizzare del codice ad hoc o fare uso di funzioni dedicate
fornite dalle librerie Microchip.
Qualora il confronto tra la variabile e la costante verificato, vengono eseguite tutte le
istruzioni tra i due punti e la parola chiave break. Tale parola chiave permette al
programma di continuare dopo il blocco di istruzioni individuato dalle parentesi graffe
associate all'istruzione switch. Qualora non si metta la parola chiave break, ed lecito
ometterla, il programma continuerebbe a controllare le altre condizioni. Si capisce che ci
possono tranquillamente essere applicazioni in cui questo sia effettivamente voluto.
Ciononostante, persone con esperienza in BASIC potrebbero omettere l'istruzione break
per distrazione e non per esigenza. Infatti il linguaggio BASIC non richiede l'istruzione
break, che in un certo qual modo eseguita implicitamente. Nel caso in cui nessuno dei
case verificato, viene eseguito il codice che segue la parola chiave default. Si noti che
in questo caso non presente la parola chiave break poich non comunque presente
altro codice, ovvero l'esecuzione del programma riprenderebbe comunque dopo le
parentesi graffe associate al blocco switch. In particolare, qualora non si abbia la necessit
di eseguire nessuna operazione di default si pu omettere il blocco relativo.
Vediamo ora un esempio in cui si leggono i pulsanti posti tra RB4 e RB7 e si visualizza
il pulsante premuto per mezzo dei LED.
#include <xc.h>
#include "PIC18F4550_config.h"
#define
#define
#define
#define

LED1
LED2
LED3
LED4

#define
#define
#define
#define

BT1
BT2
BT3
BT4

LATDbits.LATD0
LATDbits.LATD1
LATDbits.LATD2
LATDbits.LATD3
0b11100000
0b11010000
0b10110000
0b01110000

int main (void){


// Variabile per la lettura pulsanti
unsigned char button;
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;

212

// Imposto PORTD tutte uscite


LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// Abilita i resistori di pull-up sulla PORTB
INTCON2bits.RBPU = 0x00;
// Ciclo infinito
while(1){
button = PORTB;
// Maschera per i bit d'interesse
button = button & 0xF0;
// Controllo del tasto premuto
switch(button) {
case BT1:
LED1 = 0x01;
break;
case BT2:
LED2 = 0x01;
break;
case BT3:
LED3 = 0x01;
break;
case BT4:
LED4 = 0x01;
break;

}
}

default:
LATD = 0x00;

Al fine di poter testare il programma necessario rimuovere il programmatore.


Se cosi non si dovesse fare i pulsanti BT3 e BT4 sono riconosciuti sempre
premuti, causando l'esecuzione del caso di default .

Il programma oltre alle direttive classiche, fa in questo caso utilizzo della direttiva
#define per definire le costanti che sono utilizzate all'interno del blocco switch:
In particolare sono definite le costanti per la posizione dei LED
#define LED1 LATDbits.LATD0

213

#define LED2 LATDbits.LATD1


#define LED3 LATDbits.LATD2
#define LED4 LATDbits.LATD3

e le costanti che determinano il valore di PORTB, alla pressione dei tasti:


#define
#define
#define
#define

BT1
BT2
BT3
BT4

0b11100000
0b11010000
0b10110000
0b01110000

possibile vedere che il pulsante BT1 equivale a porre a 0 il bit 5 ovvero RB4. Questo

dovuto al fatto che il pulsante ha un resistore di pull-up ovvero quando aperto il pin
RB4 viene letto come 1, mentre premendo il pulsante, ovvero collegando il pin RB4 a
massa, RB4 viene letto come 0.
Questa volta la lettura in polling dei pulsanti e il loro controllo stato posto all'interno
del ciclo while (1) che dagli altri esempi sembrava apparentemente utile solo per
bloccare il programma. All'interno del ciclo viene letta la PORTB e vengono azzerati i 4
bit meno significativi:
button = PORTB;
button = button & 0xF0;

La ragione per cui si azzerano i 4 bit meno significativi discende dal fatto che i pin non
utilizzati della PORTB, ovvero i quattro bit meno significativi, sono definiti come input.
Per fare questo si fa uso dell'operatore AND &; in particolare facendo button & 0xF0
vengono posti a 0 i bit meno significativi mentre si mantengono al proprio valore i bit
relativi ai pulsanti.
Una volta effettuata la lettura e pulizia della variabile button, si effettua il confronto tra
la variabile e le varie possibilit prese in considerazione, ovvero la pressione del singolo
pulsante. Questa operazione viene ripetuta in maniera continua per mezzo del ciclo while
all'interno del quale si messo l'intero codice. Caricando il codice nella scheda di
sviluppo, possibile vedere che i LED 0,1,2,3 sono inizialmente spenti, visto che non si
preme nessun pulsante. Questo discende dal fatto che button vale 11110000, dunque il
confronto con le costanti BT1-BT4 non viene verificato, per cui viene eseguita l'istruzione
all'interno del blocco default, in cui si spengono tutti i LED. Premendo un pulsante alla
volta si accende il LED associato al pulsante, il quale rimane acceso fino a quando si tiene
premuto il pulsante. Premendo in contemporanea un secondo pulsante si pu vedere che
viene eseguito il codice di default poich il programma non riconosce pi la pressione
del singolo pulsante.
Facciamo ora una digressione. Nel caso in cui si abbia l'esigenza di svolgere
un'operazione su di un registro e porre il risultato nel registro stesso, possibile far uso
dell'operatore di assegnazione composto:
button &= 0x0F;

Come visto, la ragione per cui si scrive il codice in questo modo per dare un aiuto al
compilatore permettendo di raggiungere un codice ottimizzato. Vediamo come viene
214

eseguita l'operazione AND nel primo caso, ovvero quello standard:


49:
50:
7FC6
7FC8
7FCA

// Maschera per i bit d'interesse


button = button & 0xF0;
MOVF button, W, ACCESS
ANDLW 0xF0
MOVWF button, ACCESS

5001
0BF0
6E01

Effettivamente quello che ci si aspetta, visto che abbiamo una variabile unsigned
viene infatti eseguita l'operazione AND tra la costante e 0xF0 facendo uso
dell'accumulatore. Il risultato viene poi posto dall'accumulatore nella variabile button.
Vediamo ora come viene tradotto il codice aiutando il compilatore ad ottimizzare:
char,

49:
50:
7FC8
7FCA

// Maschera per i bit d'interesse


button &= 0xF0;
MOVLW 0xF0
ANDWF button, F, ACCESS

0EF0
1601

effettivamente l'operazione di AND viene fatta direttamente con la variabile button,


risparmiando un'istruzione. Nel caso del compilatore C18 interessante notare che aiutare
il compilatore avrebbe generato il seguente codice Assembly:
55:
00CA
00CC
00CE
00D0
00D2

0E00
90DF
92DF
94DF
96DF

button &= 0xF0;


MOVLW 0
BCF 0xfdf, 0, ACCESS
BCF 0xfdf, 0x1, ACCESS
BCF 0xfdf, 0x2, ACCESS
BCF 0xfdf, 0x3, ACCESS

Volevamo ottimizzare il codice sapendo che per sentito dire potevamo ottimizzarlo e
quello che abbiamo ottenuto un codice peggiore, almeno con il C18. interessante
vedere che il C18, piuttosto che fare l'AND pone a zero i bit meno significativi della
variabile button, che era proprio quello che volevamo fare usando l'AND. Quello che
successo che molte delle ottimizzazioni in realt possono perdere di significato quando
si ha a che fare con variabili unsigned char, ovvero di un solo byte.
Ancora una volta si mette in evidenza che non bisogna ottimizzare il codice senza
verificare i risultati e benefici che si ottengono con l'ottimizzazione. La sintassi:
button &=0x0F

pu risultare di pi difficile lettura. Dunque se non si hanno ragioni per ottimizzare il


codice meglio preferire la sintassi:
button = button & 0x0F

Anche se abbiamo avuto un risultato migliore si ricorda che compilatori differenti o


anche versioni differenti potrebbero ottimizzare il codice in maniera da perdere
l'ottimizzazione sperata.

215

Riassunto
In questo capitolo si introdotta la sintassi del C e relative parole chiave. Le principali
parole chiave sono state introdotte per mezzo di esempi mostrando il codice C e relativa
traduzione in codice Assembly. L'aver mostrato il codice Assembly ha permesso di
evidenziare come particolari costrutti e sintassi possano preferirsi rispetto ad altri
permettendo di raggiungere livelli di ottimizzazione differente.
Ogni esempio ha mostrato come la chiarezza del codice possa aiutare la fase di Debug
e debba essere favorita al tentativo di ottimizzare un codice senza averne l'effettiva
esigenza. In particolare sempre preferibile una soluzione semplice e chiara piuttosto che
una soluzione geniale e poco comprensibile. Nel caso si voglia proprio mostrare il proprio
genio sempre bene commentare accuratamente il codice, visto che il tempo arrugginisce
spesso la mente, impedendo la comprensione delle soluzioni precedentemente utilizzate.

Punti chiave ed approfondimenti


ASCII: Il codice ASCII (American Standard Code for Information Interchange) rappresenta uno
standard Americano per mezzo del quale, usando un valore numerico a 7 bit, viene
associato ad ogni numero un carattere. Tale standard ideato nel 1961 da Bob Bemer
(IBM) stato successivamente standardizzato dall'ente ISO (International Organization for
Standardization) sotto lo standard ISO646, facendo del codice ASCII uno standard
internazionale ufficiale.
Puntatore: Il puntatore rappresenta un tipo particolare di variabile, il cui contenuto
rappresenta un indirizzo di memoria. Un puntatore ha due tipologie di accesso, o si scrive
il contenuto del registro, ovvero si cambia il valore dell'indirizzo di memoria, o si scrive
nel registro di memoria indirizzato dal puntatore.
Array: Un Array rappresenta un insieme di variabili dello stesso tipo. L'accesso di ogni
variabile raggruppata in un Array possibile per mezzo di un indice. In particolare
definito un Array di dimensione N, l'indice permesso tra 0 e N-1.
Stringa: Una Stringa rappresenta un insieme di caratteri terminati dal carattere \0.
Essendo un Array di caratteri, non corretto parlare di tipo di variabile, sebbene alcuni
linguaggi di programmazioni supportino nativamente il tipo String.
Struttura: Una struttura rappresenta un tipo particolare di variabile definibile dall'utente.
Fornisce il vantaggio di poter creare delle variabili il cui formato possa meglio
rappresentare la particolare grandezza di interesse che si vuole memorizzare al suo
interno. Sebbene permetta di creare un buon livello di astrazione e semplificare la
comprensione del programma, il suo utilizzo pu rallentare l'accesso alle variabili stesse.
Casting: Il Casting delle variabili permette di cambiare il tipo di una variabile in un altro
tipo. Il compilatore effettua frequentemente Casting impliciti, ma al verificarsi degli stessi
rilascia delle Warning. Il Casting, sebbene permetta il corretto svolgimento delle operazioni,
deve essere sempre di tipo esplicito, in maniera da evitare che eventuali scelte del
compilatore possano creare effetti collaterali. Frequentemente Casting non desiderati
216

potrebbero portare alla perdita delle informazioni dei registri coinvolti in una determinata
operazione. Questo stesso effetto si potrebbe per verificare anche nel caso contrario,
ovvero quando sarebbe necessario un Casting ma non lo si fa.

Domande ed Esercizi
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.

Descrivere un esempio di utilizzo della parola chiave sizeof.


Descrivere l'utilizzo della variabile di tipo bit.
Spiegare perch si sconsiglia l'uso del tipo bit.
Spiegare perch importante specificare se una variabile di tipo con o senza segno.
Spiegare perch il codice Assembly associato ad operazioni con variabili float risulta
particolarmente complesso.
Dove devono essere dichiarate le variabili globali?
Cosa comporta quando una variabile dichiarata __persistent?
Spiegare la differenza tra dichiarare ed inizializzare una variabile.
Scrivere due esempi di inizializzazione di un Array con una stringa.
Che differenza c' tra un Array di caratteri e un Array contenente una stringa?
Scrivere un codice che dati due Array connettenti due stringhe, accenda un LED se le due
stringhe sono uguali e accenda un secondo LED se le due stringhe sono diverse.
Scrivere un codice che dato un Array di unsigned char di dimensione 11 venga inizializzata
con la stringa Ciao Mondo facendo uso di un puntatore all'Array stesso.
Che differenza c' tra lo scrivere ++i e i++?
Spiegare gli effetti collaterali nell'usare un puntatore di un tipo per puntare una variabile di
tipo diverso.
Spiegare l'utilit dell'operatore bitwise &.
Compilare il programma riportato sotto e verificare il tempo di ritardo introdotto dal ciclo
for.
#include <xc.h>
#include "PIC18F4550_config.h"
int main (void){
// Pausa che filtra gli spike
for (i=0;i<32000; i++) {
}

}
17.
18.
19.
20.
21.

while (1) {
}

Ripetere l'esercizio sopra cambiando il livello di ottimizzazione del compilatore e compilare


facendo uso di due versioni di compilatori differenti.
Ripetere l'esercizio cambiando il ciclo for con un decremento della variabile piuttosto che
un incremento.
Spiegare la differenza tra un ciclo while e un ciclo do while.
Scrivere un codice che faccia lampeggiare un LED facendo uso dell'operatore bitwise ^.
Riscrivere il codice di esempio del test relativo all'istruzione switch accendendo il LED sul
217

22.

pin RD4 alla pressione contemporanea del tasto BT1 e BT2.


Riscrivere l'esempio dell'esercizio precedente facendo uso dell'istruzione if e confrontare
la dimensione del codice ottenuto per mezzo dell'istruzione if. Commentare il risultato.

218

Capitolo VII

VII

Le Funzioni
I programmi di esempio visti fino a questo momento non hanno presentato molte
difficolt, ed oggettivamente non svolgevano neanche compiti entusiasmanti. Un
programma o applicazione reale, contiene spesso molte centinaia o migliaia di righe di
codice. Poterlo organizzare in maniera opportuna permette di ottenere una chiarezza che
favorisce sia la sua scrittura che la fase di Debug. A questo scopo il C supporta le
funzioni, che come vedremo permettono di suddividere il codice in blocchi funzionali e
concettuali, permettendo di organizzare il software proprio con l'ordine a cui siamo ormai
abituati.

Schema elettrico
Per poter seguire gli esempi presentati in questo Capitolo, richiesto realizzare su
Breadboard lo schema elettrico di Figura 94 o far uso delle schede di sviluppo impostate
come descritto nel paragrafo successivo.

Figura 94: Schema elettrico di esempio del Capitolo VII.

219

Impostare le schede di sviluppo


Al fine di poter seguire gli esempi mostrati in questo Capitolo necessario impostare la
scheda di sviluppo utilizzata come descritto nei paragrafi seguenti.

Freedom II
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom II deve avere i Jumper posizionati nel seguente modo:

Figura 95: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

Freedom Light
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom Light deve avere i Jumper posizionati nel seguente modo:

Figura 96: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

La scheda Freedom Light possiede i LED solo sui pin RD0 e RD1. Per poter
visualizzare gli altri LED necessario montarne altri sul connettore di
espansione, facendo per esempio uso della scheda di espansione Breadboard
PJ7011 o limitare il Debug su due soli LED.

220

Impostazioni software
Gli esempi mostrati non richiedono alcuna impostazione software particolare oltre alla
semplice creazione di un progetto.
Gli esempi mostrati sono spiegati nel dettaglio, mostrando il codice Assembly,
per cui, a seconda delle impostazioni delle ottimizzazioni e della versione del
compilatore utilizzato, possono essere presenti delle differenze tra il codice
mostrato e quello che otterrete in fase di compilazione.

Cosa sono le funzioni


Abbiamo gi visto la funzione main quale funzione principale che viene chiamata
all'avvio del nostro programma. Si compreso che la funzione main altro non che un
gruppo di istruzioni delimitato da parentesi graffe. Qualora tutto il nostro programma sia
scritto all'interno della sola funzione main, si renderebbe il codice particolarmente
complesso e di difficile lettura. Per mezzo delle funzioni, ogni programma pu essere
scritto in maniera molto pi snella e leggibile e al tempo stesso si ha la possibilit di
riutilizzare codice gi scritto sotto forma di librerie.
La funzione un insieme di istruzioni identificate da un nome e alla quale possibile
passare un certo insieme di variabili ovvero parametri. La funzione pu inoltre, a seguito
di una elaborazione dati, restituire un risultato. La sintassi per una funzione :
tipo_ restituito nome_funzione (tipo1 var1, , tipoN varN) {
}

Si pu subito osservare che la funzione pu avere un numero qualunque di parametri


in ingresso ma restituisce un solo valore di un determinato tipo. Questo non significa che
pu restituire un solo risultato, se infatti si fa uso di una struttura, Array o di un puntatore,
si pu restituire indirettamente pi di un semplice valore numerico.
Se paragoniamo la sintassi della funzione main con quella generale, si pu notare che
la funzione main non ha nessuna variabile in ingresso115.
Vediamo un esempio pratico in cui si crea una funzione per fare la somma. Si capisce
che il valore restituito che ci si aspetta deve essere il risultato, ovvero la somma, mentre i
parametri che necessario passare alla funzione sono gli addendi. Nell'esempio
prendiamo in considerazione il caso di una somma tra due interi, i quali per loro natura
genereranno un risultato che a sua volta un intero. Dal momento che la somma
potrebbe generare anche un numero negativo, bene usare un tipo signed char.

115

Questo non vero in ANSI C, dove la funzione main pu ricevere variabili in ingresso e restituire un valore. Le variabili in
ingresso sono rappresentate dal testo che si scrive sulla linea di comando quando si lancia il programma stesso da una shell
di comando. Il valore di uscita in generale un valore che segnala un eventuale codice di errore.

221

#include <xc.h>
#include "PIC18F4550_config.h"
// Funzione per sommare due numeri interi
signed char sommaInteri (signed char add1, signed char add2) {
signed char somma;
somma = add1+add2;

// Restituisco la somma
return(somma);

int main (void){


// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// Effettuo la somma tra 3 e 7
LATD = sommaInteri (3,7);
// Ciclo infinito
while(1){
}
}

Come di consueto la parte iniziale del programma relativa alle direttive non cambiata,
ciononostante possibile vedere che prima della funzione main stata dichiarata la
funzione sommaInteri.
signed char sommaInteri ( signed char add1, signed char add2) {
signed char somma;
somma = add1+add2;
// Restituisco la somma

222

return(somma);
}

Questa potrebbe anche essere riscritta:


signed char sommaInteri ( signed char add1, signed char add2)

// Restituito la somma
return(add1+add2);
}

In questo modo si risparmia anche una variabile. Il fatto che la funzione sommaInteri
sia definita prima della funzione main non obbligatorio, ma si ritorner a breve su
questo argomento. Vediamo qualche dettaglio sulla definizione della funzione:
signed char sommaInteri ( signed char add1, signed char add2)

possibile vedere che si dichiarato, come tipo restituito, un signed char. Questo

discende dal fatto che la somma tra due interi sempre un intero 116 siano essi positivi o
negativi. I due addendi sono dichiarati all'interno delle parentesi tonde. Questa
dichiarazione a tutti gli effetti una dichiarazione di variabile ma come si vedr nel
paragrafo sulla visibilit delle variabili queste variabili sono rilasciate non appena la
funzione viene terminata, ovvero la memoria RAM utilizzata viene lasciata disponibile per
altro utilizzo. Le variabili dichiarate tra parentesi ospitano i valori degli addendi che
vengono inseriti nel momento della chiamata della funzione stessa. Il corpo della funzione
praticamente identico alla funzione main, ovvero si possono dichiarare altre variabili e si
pu scrivere tutto il codice che si vuole. Una differenza la presenza dell'istruzione
return ( ) (in realt una funzione) che permette di restituire il risultato della somma.
Qualora la funzione non abbia nessun valore restituito, ovvero il valore restituito sia void,
come potrebbe essere per una funzione di ritardo, la funzione return () non risulta
necessaria. In particolare se si dichiarasse il valore restituito di tipo void e si facesse
comunque uso della funzione return () verrebbe generato un errore:
error: (204) void function can't return a value

Allo stesso modo se si dichiare una funzione con un tipo restituito non void ma non si
richiama la funzione return ( ), si ha:
warning: (343) implicit return at end of non-void function

All'interno delle parentesi tonde della funzione return () si deve porre una variabile o
costante che sia dello stesso tipo di quella che stata dichiarata per la restituzione della
funzione. In particolare il valore restituito il risultato della nostra funzione.
Dopo un'istruzione return () si ha l'uscita dalla funzione e il programma riprende dal
punto in cui la funzione stata chiamata. All'interno di ogni funzione possono essere
presenti anche pi punti di uscita, ovvero return () ma solo uno quello che
effettivamente far uscire dalla funzione stessa. Se la funzione non restituisce alcun valore,
si ha l'uscita dalla funzione quando il programma giunge alle parentesi graffe. Quando
questo avviene il programma riprende dall'istruzione successiva alla chiamata della
116

In questo esempio non si sta considerando il caso in cui il risultato possa eccedere il valore massimo consentito da un intero.

223

funzione. Da quanto detto riguardo lo Stack Pointer e lo Stack Memory si capisce che la
chiamata ad una funzione equivale a posizionare il Program Counter al punto della memoria
in cui definita la funzione stessa. Lo Stack Pointer viene incrementato e l'indirizzo di
ritorno, ovvero l'indirizzo successivo a quello dal quale avviene la chiamata alla funzione,
viene memorizzato all'interno della Stack Memory. Questo significa che all'interno di una
funzione possibile richiamare altre funzioni ma bisogna tenere sempre a mente del limite
imposto dalla Stack Memory disponibile117.
La chiamata di una funzione avviene nel seguente modo:
LATD = sommaInteri (3,7);

Si noti che nel caso specifico si sono passati come parametri due costanti, ma si
possono pure passare due variabili di tipo intero. Dal momento che la funzione restituisce
un risultato bisogna scrivere sulla sinistra della funzione un'assegnazione, o comunque la
funzione deve essere parte di una espressione il cui risultato viene assegnato ad una
variabile; se cosi non si facesse il risultato verrebbe perso. Nel nostro caso il valore della
somma, pari a 10 (00001010), viene posto in uscita alla PORTD. Il valore restituito viene
spesso usato anche per notificare un codice di errore in maniera da avvisare chi chiama la
funzione, del fatto che si verificato un errore. Normalmente un valore restituito
negativo o nullo equivale alla presenza di un errore. Un valore maggiore di 0 equivale ad
una corretta esecuzione delle operazioni contenute nella funzione. In alcuni casi non si ha
interesse a gestire gli errori, anche se buona abitudine farlo, perci il valore restituito
pu non essere assegnato a nessuna variabile.
Come detto ogni funzione deve essere dichiarata prima della funzione main ma questo
non obbligatorio. Se si dichiara la funzione o funzioni dopo la funzione main il
compilatore fornisce un avviso o errori poich compilando la funzione main si trova ad
usare la funzione sommaInteri che non stata precedentemente dichiarata...ma che
presente dopo la funzione main. In particolare se si ripete il programma precedente
semplicemente spostando la funzione sommaInteri dopo la funzione main, si ottengono i
seguenti messaggi:
main.c:32: warning: (361) function declared implicit int
main.c:41: error: (984) type redeclared
main.c:41: error: (1098) conflicting declarations for variable
"sommaInteri" (main.c:32)

Per evitare i messaggi di errore, prima della funzione main bisogna scrivere il prototipo
di funzione ovvero una riga in cui si avvisa il compilatore che la funzione sommaInteri
che utilizzata all'interno della funzione main dichiarata dopo la funzione main stessa.
La dichiarazione di un prototipo di funzione si effettua semplicemente per mezzo del
nome della funzione stessa e il tipo di variabili presenti, terminando il tutto per mezzo di
un punto e virgola, ovvero senza il corpo del programma associato alla funzione. Il
prototipo di funzione e relativo spostamento della funzione sommaInteri riportato nel
seguente esempio:

117

Si faccia riferimento al Capitolo relativo all'architettura dei PIC18 per maggiori informazioni.

224

#include <xc.h>
#include "PIC18F4550_config.h"
// Prototipo della funzione per sommare due numeri interi
signed char sommaInteri (signed char add1, signed char add2);
int main (void){
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// Effettuo la somma tra 3 e 7
LATD = sommaInteri (3,7);
// Ciclo infinito
while(1){
}

// Funzione per sommare due numeri interi


signed char sommaInteri (signed char add1, signed char add2) {
signed char somma;
somma = add1+add2;

// Restituisco la somma
return(somma);

Il fatto di dichiarare il prototipo di funzione permette al compilatore di effettuare il


controllo del tipo di variabili che vengono passate alla funzione stessa.
Sebbene il prototipo della funzione stato dichiarato con il nome delle variabili al suo
interno, questo non necessario.
signed char sommaInteri (signed char add1, signed char add2);

225

Il prototipo della funzione richiede infatti il solo tipo delle variabile e potrebbe dunque
essere scritto nel seguente modo:
signed char sommaInteri (signed char, signed char);

Per convenzione il nome delle funzioni inizia spesso con la lettera minuscola.
Eventualmente parole composte possono essere scritte con la lettera maiuscola
come nel caso di sommaInteri. Un altro modo per scrivere il nome delle
funzioni facendo uso dell'undescore _ ovvero somma_interi.
Spiegata la funzione SommaInteri vi sarete resi certo conto di qualcosa di strano sul
tipo di variabili scelte. Cosa succederebbe facendo la somma 127 + 127?

Visibilit delle variabili


Introdotte le funzioni, il nostro programma pu essere suddiviso in molti file o
blocchi. Questo se da un lato permette di dividere e conquistare un qualunque problema,
apre alcune problematiche relativamente alla dichiarazione delle variabili. A seconda di
dove si dichiara una variabile questa pu o meno essere utilizzata, ovvero essere visibile,
in determinate parti del programma. Per visibilit o scope di variabili si intende le parti di
programma dove possibile utilizzare una variabile precedentemente dichiarata.
Una variabile viene detta globale quando pu essere utilizzata in qualunque parte del
programma. Generalmente questo tipo di variabili sono dichiarate nel caso in cui
l'informazione in esse contenute debba essere condivisa da pi moduli ovvero funzioni di
programma118. Per dichiarare una variabile globale bisogna effettuare una dichiarazione al
di fuori della funzione main come nel seguente esempio:
// Variabile globale
signed int temperatura_sistema = 0;
int main (void) {
// Programma da eseguire...
if (temperatura_sistema > 37) {
}

attivaAllarme ();

Quando una variabile globale, vuol dire che il Linker associa una determinata quantit
di memoria RAM fissando un indirizzo fisso accessibile da ogni parte del programma. La
variabile risulta accessibile da ogni parte del programma proprio perch il suo indirizzo
118

In linguaggi di programmazione ad oggetti l'utilizzo di variabili globali sconsigliato in quanto crea delle relazioni di
dipendenza tra classi.

226

rimane non varia.


Quando una variabile viene dichiarata all'interno della funzione main, risulta accessibile
solo da parti di programma interne alla funzione main stessa ma non da funzioni esterne.
Ad una variabile interna alla funzione main, come per le variabili globali gli viene
assegnata della RAM e questa rimane fissa durante tutto il tempo di esecuzione del
programma da parte del microcontrollore, ma comunque accessibile solo dalla funzione
main.
Quando una variabile dichiarata all'interno di una funzione generica, come per
esempio la variabile intera somma dichiarata all'interno della funzione sommaInteri degli
esempi precedenti, questa visibile solo all'interno della funzione stessa. In particolare la
RAM che viene allocata per la variabile viene rilasciata (resa disponibile) una volta che la
funzione termina le sue operazioni; questo significa che la variabile cessa di esistere.
Accedere ad una variabile locale di una funzione che stata rilasciata non genera, a livello
di compilazione, alcun Errore o Warning ma pu comportare effetti collaterali. Questo
dovuto al fatto che accedendo ad una variabile rilasciata, il valore che si ottiene pu in
realt appartenere ad un'altra variabile temporanea 119.
Poich una variabile dichiarata all'interno della funzione main non visibile all'interno
di altre funzioni e viceversa, possibile utilizzare anche nomi di variabili utilizzati in altre
funzioni. Nonostante sia possibile avere nomi di variabili uguali, grazie al fatto che le
funzioni non riescono a vedere le variabili interne ad altre funzioni, questa non una
buona pratica di programmazione visto che pu creare facilmente confusione. Questa
possibilit dovrebbe essere sfruttata solo per variabili molto semplici come le variabili di
indice utilizzate nei cicli for o while ma bene sempre non eccedere. Il caso peggiore si
ha quando si usa in una variabile locale con lo stesso nome di una variabile globale.
Alcune volte si pu avere l'esigenza di contare degli eventi ogni volta che si richiama
una funzione, per esempio il numero di volte che la funzione viene chiamata. Ma come
possibile contare degli eventi se le variabili interne ad una funzione vengono poi
cancellate?
Un modo per risolvere questo tipo di problema dichiarare una variabile globale e
utilizzarla per il conteggio interno alla funzione. Infatti tale variabile non appartenendo
alla funzione non viene cancellata al termine dell'esecuzione della funzione stessa, ed ad
ogni accesso avrebbe sempre il valore relativo all'ultimo incremento.
Un altro modo per raggiungere lo scopo dichiarare la variabile static, ovvero statica.
Questo significa che la variabile pur appartenendo alla funzione non viene cancellata alla
fine dell'esecuzione del programma associato alla funzione stessa. Dunque ogni volta che
la funzione viene richiamata, questa pu accedere sempre alla stessa variabile
precedentemente dichiarata static.
Una variabile dichiarata static viene inizializzata una sola volta nel momento
della sua dichiarazione, quindi successive chiamate alla funzione non
inizializzano nuovamente la variabile.

119

Alcune volte pur accedendo al contenuto di una variabile rilasciata il valore letto quello esatto, ma questo solo dovuto al
caso specifico e potrebbe non essere ripetibile.

227

Vediamo il seguente esempio, in cui la funzione conteggio grazie alla variabile statica
conta il numero di chiamate effettuate alla funzione stessa.
#include <xc.h>
#include "PIC18F4550_config.h"
// Funzione che conta le sue chiamate
unsigned char conteggio (void) {
// Dichiarazione variabile statica
static unsigned char contatore=0;
contatore++;
return (contatore);
}
int main (void){
// Variabile per il ciclo for
unsigned char i;
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
for (i = 0; i < 8; i++) {
}

conteggio ();

LATD = conteggio ();


// Ciclo infinito
while(1){
}
}

228

La dichiarazione della funzione fatta come nel caso precedente prima della funzione
main. possibile osservare che nel nostro caso non viene passata alcuna variabile.
unsigned char conteggio (void) {
// Dichiarazione variabile statica
static unsigned char contatore=0;
contatore++;
return (contatore);
}

La variabile per il conteggio delle chiamate della funzione stessa dichiarata


semplicemente ponendo la parola chiave static prima della dichiarazione stessa.

static

static unsigned char contatore=0;

In questo caso l'inizializzazione a zero fatta direttamente con la dichiarazione della


variabile stessa, garantendo che sia eseguita una sola volta.
Il resto della funzione non fa altro che incrementare il valore del contatore e restituirlo
in uscita. Nell'esempio la funzione conteggio viene chiamata 8 volte per mezzo di un
ciclo for:
for (i = 0; i < 8; i++) {
}

conteggio ();

Dopo il ciclo
PORTD.

for

il valore restituito dalla funzione

conteggio()

viene scritto su

LATD = conteggio ();

Dal momento che per scrivere il valore restituito si chiamata nuovamente la funzione
si ha che il valore finale 9 ovvero 00001001. E' interessante notare che la funzione
conteggio () pur restituendo un valore pu essere richiamata senza dover
necessariamente leggere il suo valore.
Pu essere interessante rimuovere ora la parola chiave static e vedere che in uscita si
ha sempre il valore 00000001. La ragione legata al fatto che la variabile conteggio viene
inizializzata sempre a zero non essendo pi static .
Dagli esempi fin ora visti, si fatto restituire alla funzione sempre un solo valore, ma
come detto ci possono essere dei casi in cui si abbia l'esigenza di restituire variabili pi
complesse o un gruppo di variabili. In questa situazione si potrebbero usare i puntatori al
fine di restituire il puntatore ad un Array o una struttura. Al fine di garantire che lo spazio
di memoria RAM assegnato all'Array o struttura non sia perso, necessario dichiarare tali
strutture come statiche. Nel caso di un Array se non si dovesse dichiararlo come static si
avrebbe una Warning:
229

warning: (365) pointer to non-static object returned

Un esempio potrebbe essere il seguente, in cui si scrivono dei valori nell'Array e poi si
restituisce l'Array, o meglio il puntatore a quest'ultimo, per poi scrivere il suo contenuto
sulla PORTD.
#include <xc.h>
#include "PIC18F4550_config.h"
// Prototipo di funzione
unsigned char * inizializza_array (void);
int main (void){
unsigned int delay = 0;
unsigned int i = 0;
unsigned char *pArray;
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
pArray = inizializza_array();
// Ciclo infinito
while(1){
for (i=0; i<=10; i++){
for (delay=0; delay<64000; delay++){
//Semplice Delay
}
}

LATD = pArray[i];

}
}

230

unsigned char * inizializza_array (void) {


static unsigned char mio_array[11] ;
unsigned char index = 0;
for (index = 0; index <= 10; index++) {
mio_array[index] = index * 2;
}
// Restituisco il puntatore
return(mio_array);
}

Il programma non fa nulla di interessante ma certamente pi complesso se messo a


confronto con gli altri. In particolare bene notare come si possa restituire un puntatore:
unsigned char * inizializza_array (void)

La forma non dovrebbe sorprendere molto. bene anche notare che l'Array dichiarato
come static:
static unsigned char

mio_array[11] ;

La funzione di inizializzazione non fa altro che scrivere la tabellina del 2 all'interno


dell'Array e restituire il suo indirizzo di inizio, il quale viene salvato nella variabile pArray,
ovvero:
pArray = inizializza_array();
pArray

dichiarata come puntatore:

unsigned char *pArray;

per cui scrivendo:


pArray[i]

possibile accedere ad ogni suo elemento.


Quanto appena visto rappresenta un modo per far restituire ad una funzione pi valori,
ciononostante, come vedremo a breve, passando delle variabili per riferimento possibile
indirettamente restituire pi valori anche nel caso in cui si dovesse dichiarare come void il
valore di ritorno della funzione! In particolare l'utilizzo dei puntatori mostra come sia
possibile da parte di una funzione esporre o rendere disponibile una variabile che
altrimenti sarebbe nascosta nella funzione stessa.

231

Passaggio di variabili per valore e riferimento


Negli esempi di funzioni mostrati fino ad ora si messo in evidenza che per passare dei
valori numerici ad una funzione, affinch ne faccia uso, basta dichiarare delle variabili
durante l'implementazione della funzione stessa. Quello che non si messo in evidenza
che le variabili cosi dichiarate sono locali alla funzione stessa, per cui variare il suo valore
non comporta l'aggiornamento della variabile che stata passata. Se per esempio si ha:
unsigned char numero_a = 20;
//codice generico
resetta_valore (numero_a);

e si implementa la funzione :
void resetta_valore (unsigned char registro){
registro = 0;
}

scrivendo 0 nella variabile registro non comporta l'aver scritto 0 nella variabile
numero_a. Quanto detto rappresenta il cosiddetto passaggio di una variabile per valore,
ovvero viene creata una variabile locale alla funzione, in questo caso nominata registro,
e al suo interno viene caricato il valore della variabile numero_a. Nella funzione
resetta_valore, non si a conoscenza dell'indirizzo della variabile numero_a per cui
scrivere nella variabile registro non equivale a scrivere nella variabile numero_a.
Per avere la possibilit di scrivere nella variabile numero_a dalla funzione
resetta_valore, necessario passare la variabile per riferimento e non per valore. Da
quanto detto si capisce che passare la variabile per riferimento equivale a passare
l'indirizzo della variabile stessa, ovvero la funzione deve avere come variabile in ingresso
un puntatore.
Riscrivendo la funzione:
void resetta_valore (unsigned char * registro){
*registro = 0;
}

e riscrivendo il programma principale in maniera da fornire alla funzione l'indirizzo della


nostra variabile:
unsigned char numero_a = 20;
//codice generico
resetta_valore (&numero_a);

232

si ottiene il risultato desiderato, ovvero di porre a 0 la variabile


dalla funzione:

numero_a

direttamente

*registro = 0;

Da quanto detto si capisce che passare pi variabili per riferimento potrebbe anche
essere considerato un modo per restituire pi valori, superando il limite di un solo valore
di ritorno. In questo modo il valore di ritorno principale potrebbe essere utilizzato per la
gestione degli errori. Vediamo qualche dettaglio in pi per comprendere le due possibilit
con cui possibile passare dei valori ad una funzione.
#include <xc.h>
#include "PIC18F4550_config.h"
// Prototipo di funzione
unsigned char funzione_test
numero_b);

(unsigned

char

numero_a,

unsigned

char

int main (void){


unsigned int delay = 0;
unsigned int i = 0;
unsigned char numero = 0;
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
for (i=0; i<10; i++){
for (delay=0; delay<64000; delay++){
//Semplice Delay
}
LATDbits.LATD7 = funzione_test (i, &numero);
LATD |= numero;
}
// Ciclo infinito
while(1){

233

unsigned char
numero_b) {

funzione_test

(unsigned

char

numero_a,

unsigned

char

unsigned char errore = 1;


*numero_b = numero_a;
if (numero_a > 3){
errore = 0;
}
return (errore);
}

Il programma di esempio mostra come sia possibile passare due valori alla funzione, in
particolare uno per valore e uno per riferimento. Inoltre la funzione restituisce anche un
errore nel caso in in cui il valore della variabile numero_a sia maggiore di 3 (numero
magico!). La chiamata alla funzione viene effettuata fornendo l'indirizzo della variabile
numero, visto che tale variabile passata per riferimento.
LATDbits.LATD7 = funzione_test (i, &numero);

Dopo la chiamata alla funzione, il valore dell'errore viene visualizzato sul pin 7 di
PORTD mentre i bit meno significativi visualizzano il conteggio:
LATDbits.LATD7 = funzione_test (i, &numero);
LATD |= numero;

In particolare la variabile numero incrementata all'interno della funzione, assegnando alla


stessa la variabile numero_a.
Dal momento che il passaggio di una variabile per riferimento non richiede di
allocare ulteriore memoria RAM ad ogni chiamata della funzione stessa, risulta
essere un metodo pi veloce e richiede meno risorse RAM. In particolare,
risparmiando dei cicli di clock, in applicazioni Ultra Low Power, si raccomanda di
passare le variabili per riferimento e non per valore.

Scriviamo una libreria personale


L'utilizzo delle funzioni permette di risolvere applicazioni complesse concentrandosi su
singoli problemi, inoltre, come detto, risulta anche un modo per riutilizzare il codice. Per
fare questo si pu semplicemente copiare ed incollare una funzione da un programma in
un altro o cosa migliore creare una propria libreria che contiene una certa categoria di
funzioni. Si pu per esempio fare una libreria per controllare un modulo LCD o una
libreria per controllare la comunicazione con un integrato particolare. Per poter scrivere
234

una libreria basta creare un altro file, come si fatto per il file main.c e dargli un nome per
esempio funzioniLCD.c ed aggiungerlo al progetto. Il file creato in questo modo in
realt parte del progetto stesso e non una vera e propria libreria. Per trattarla come una
libreria basta in realt cancellare il file dal nostro progetto ed includerlo con la direttiva
#include. Quando si include un file con la direttiva #include si hanno due formati, il
primo :
#include <nome_file>

dove si scrive il file da includere tra i due simboli di minore e maggiore. Un secondo
formato :
#include "nome_libreria"

Come fatto notare in precedenza, al fine di permettere al compilatore di trovare il file,


necessario includere il percorso dove si trova il file stesso, all'interno degli include path. Nel
caso particolare in cui la libreria risieda all'interno della directory del nostro progetto basta
scrivere:
#include "nome_file"

infatti il percorso della directory madre (dove risiede il progetto) automaticamente


incluso tra quelli di ricerca. Anche l'includere il file per mezzo della direttiva #include
non fa in realt del file una vera e propria libreria, ma concettualmente ci siamo quasi.
L'utilizzo di file multipli non solo consigliato per raccogliere una certa classe di
funzioni ma anche per spezzare il programma principale. Infatti, in programmi complessi
si potrebbe creare un file che contenga solo la dichiarazione delle variabili ed includerlo
come precedentemente detto. Inoltre buona abitudine scrivere le funzioni in altri file
seguendo un criterio di appartenenza e scrivere sul file principale solo lo scheletro del
programma. Per un robot si potrebbe scrivere per esempio un file per le sole variabili, un
file con le funzioni di controllo del movimento, un file per la gestione degli occhi, un file
per il controllo delle interfacce grafiche e via discorrendo.
Quando si fa uso di file multipli bisogna sempre tenere conto della dichiarazione del
prototipo di funzione, in maniera da evitare i messaggi di Warning. In particolare si vedr a
breve che per lo scopo viene creato un altro file, nominato header file.

Perch scrivere una libreria


Abbiamo gi visto che ogni programma C pu svilupparsi su di un unico file o ancor
meglio, organizzare il tutto in pi file. Un file esterno pu essere incluso all'interno del
progetto per mezzo della direttiva #include o per mezzo della finestra dei file di progetto.
Questa tecnica pu essere utilizzata senza problemi qualora si abbiano poche funzioni
specifiche per un'applicazione, ma quando le funzioni sono specifiche per un determinato
dispositivo o famiglia di applicazioni, ovvero possono tornare utili in molteplici situazioni,
bene creare una libreria, ovvero una raccolta di funzioni scritte solo ed esclusivamente
per un determinato dispositivo. Un esempio potrebbe essere una libreria per gestire
display LCD o una comunicazione seriale RS232 e via dicendo.
235

Si capisce subito che l'utilit di una libreria sta nel fatto che fornisce tutte le funzioni (o
almeno questa sempre la speranza) di cui si pu aver bisogno per gestire la nostra
periferica. Se la libreria ben fatta permette anche di raggiungere un livello di astrazione
per l'utilizzo della periferica tale per cui non bisogna sapere molto della periferica stessa.
Oltre a raccogliere le funzioni, una libreria ha l'utilit di rappresentare un file comune
per diverse applicazioni, permettendo di aggiornare un unico file qualora si dovesse essere
trovato un errore o si volessero aggiungere nuove funzioni. Se infatti si dovesse creare per
ogni programma un file c con le funzioni per la gestione di un display LCD,
semplicemente copiando ed incollando le funzioni da un altro progetto precedentemente
scritto, si avrebbero due copie delle funzioni. Facendo ulteriori copia ed incolla, il numero
di copie delle nostre funzioni diventerebbe tale che se si volesse modificare una sola
funzione dovremo andare a modificare uno per uno i sorgenti dei vari progetti. Creando
una libreria si ha un'unica raccolta che tutti i progetti possono includere, dunque ogni
modifica si ripercuote automaticamente su ogni progetto. Nonostante si possa modificare
la libreria in un solo punto, si deve comunque di ricompilare ogni progetto che fa uso
della libreria.
Un ultimo aspetto associato all'utilizzo delle librerie, e questo vale in generale, come
anche gli altri punti, quello di permettere di mantenere il codice sorgente nascosto. Per
utilizzare una libreria si pu infatti necessitare solo del file header .h con i prototipi delle
funzioni e il file .lib ovvero il file oggetto con il codice compilato. Il file sorgente .c non
necessario poich compilato nel file .lib. In questo modo molte societ nascondono il file
sorgente permettendo di implementare librerie ottimizzate che i competitori avranno
difficolt a copiare. Nel caso di semplici applicazioni osservando il codice Assembly
possibile risalire pi o meno al codice originale ma richiede comunque un certo sforzo. Le
librerie LuarTec non sono scritte con l'intento di nascondere il codice, per cui sono
fornite con il file header .h e il file sorgente .c.

Le direttive
Come detto una direttiva non rappresenta un'istruzione che il compilatore tradurr in
linguaggio macchina, bens una guida per il compilatore o meglio pre-compilatore (ovvero
colui che viene prima del compilatore). In particolare le direttive servono per far capire al
pre-compilatore come organizzare la compilazione che verr effettuata dal compilatore.
Abbiamo gi incontrato alcune direttive che per completezza sono ripetute in maniera da
avere una lista pi completa. Le direttive che sono descritte sono solo alcune di quelle
disponibili, in particolare sono le pi usate. Ogni direttiva come gi visto introdotta dal
carattere # e non termina con il solito punto e virgola necessario per le istruzioni C.
Vediamo le direttive:

#include

La direttiva #include permette di includere un file esterno all'interno del file in


cui viene scritta. La sua sintassi differente a seconda del percorso a cui si vuol far
riferimento.
#include <nome_file>

Questo formato viene utilizzato per includere file, siano essi .c , .h o altri formati
236

in cui siano scritti contenuti riconoscibili dal compilatore. Il file viene ricercato tra i
percorsi di ricerca impostati nelle propriet del progetto, alla voce Include Search Path.
Qualora si voglia fare riferimento ad un percorso assoluto o alla directory dove si
trova anche il nostro progetto, si pu far uso del formato:
#include "percorso_file/nome_file"

La parte del percorso file pu anche essere rimossa, qualora non si abbia interesse
ad un percorso assoluto. In questo caso i percorsi che sono utilizzati per la ricerca
del file sono prima la directory corrente e successivamente gli Include Search Path. In
generale si sconsiglia di utilizzare i percorsi assoluti in modo da rimanere pi
flessibili in termini di salvataggio ed esecuzione del progetto.
Nel testo ho seguito la pratica di usare il formato di inclusione con <
Microchip, mentre il formato " " per le librerie LaurTec.

>

per le librerie

#define

La direttiva #define, pu essere utilizzata per vari scopi. Normalmente viene


utilizzata per definire delle costanti o assegnare un nome particolare ad un pin del
PIC.
#define MAX_VALUE 12
#define LED LATDbits.LATD4

Nel primo esempio si definisce la costante MAX_VALUE pari al valore 12. L'utilizzo
della costante MAX_VALUE permette di utilizzare tale nome, ovvero valore, in
qualunque parte del programma, ma nel caso in cui si dovesse cambiare il suo valore
baster cambiare la riga di definizione.
Nel secondo esempio si assegna il nome LED al pin LATDbits.LATD4. In
questo modo qualora si voglia accendere il LED si pu scrivere:
LED = 0x01;

piuttosto che:
LATDbits.LATD4 = 0x01;

Questo risulta molto utile nei casi in cui la posizione del LED dovesse cambiare.
In tale circostanza basterebbe infatti cambiare solo la riga in cui viene definito il
nome LED e non tutti i punti del codice dove viene effettivamente usato.
La direttiva #define pu essere anche utilizzata per definire semplicemente un
nome senza dare un valore:
#define LED

Questo risulta utile per esempio nel caso si usi la direttiva #ifndef.

#ifndef

La direttiva

#ifndef

permette di far compilare il codice che segue, qualora una


237

determinata costante non sia stata definita. Il codice che deve essere compilato deve
essere terminato dalla direttiva #endif. Questa funzione ritorna molto utile per
evitare di includere due volte uno stesso file. Qualora un file sia incluso due volte si
ha infatti il problema di codice duplicato ed in particolare di nomi di funzioni e
variabili duplicate. Questa situazione crea in generale molti errori di compilazione,
facilmente identificabile dal fatto che il compilatore segnala la presenza di variabili e
funzioni gi dichiarate. Un esempio in cui la direttiva #ifndef permette di
controllare ed evitare inclusioni multiple potrebbe essere:
#ifndef LIBRERIA_LCD
#define LIBRERIA_LCD
// Codice della mia libreria o funzioni
#endif

Dall'esempio si pu vedere che la direttiva effettua un test sull'esistenza o meno


del nome LIBRERIA_LCD. Se questo non risulta essere stato dichiarato da nessun'altra
parte vuol dire che il file non stato mai incluso prima e dovr dunque essere
compilato. Dopo il controllo viene definito il nome LIBRERIA_LCD in modo tale che
includendo una seconda volta il file da qualche altra parte, questo non verr pi
compilato. Dopo aver definito il nome d'interesse per il controllo del nostro codice,
possibile inserire il codice che deve essere compilato. Il termine del codice da
compilare deve essere segnalato dalla direttiva #endif.
Il blocco if appena introdotto pu essere ripetuto numerose volte all'interno del
nostro programma, creando in questo modo una compilazione ottimizzata a
seconda delle impostazioni esterne o definite dall'utente.

#endif

Tale direttiva viene utilizzata per terminare un blocco di programma identificato


dalla direttiva #ifndef.

#warning

La direttiva
codice.

#warning

permette di aggiungere dei messaggi di Warning al nostro

#warning Questo il messaggio che visualizzo

Il messaggio di Warning pu essere utilizzato qualora si voglia avvisare


l'utilizzatore di una libreria di un possibile problema derivante per esempio da un
compilatore non supportato o la mancanza di parametri. Come gli atri messaggi di
Warning bene fare in modo che un eventuale messaggio visualizzato da una libreria
venga propriamente compreso ed eliminato in modo opportuno.

#error

La direttiva #error pu essere utilizzata per generare un errore ed evitare che la


compilazione venga portata a termine. Un esempio il seguente.
#error Questo il messaggio che visualizzo per l'errore

238

pu ritornare particolarmente utile qualora un parametro fondamentale non sia


del valore giusto o non sia stato definito. In questo caso bene che la compilazione
venga interrotta visto che non possibile garantire la corretta esecuzione del
programma stesso, ovvero l'utilizzo della libreria.

Progetto 1 Creare una Libreria


La realizzazione di una libreria non molto complicata e alla luce delle nuove
conoscenze si hanno tutti gli strumenti per realizzarne di professionali.
In questo progetto non spiegata la fase di ingegnerizzazione che precede la realizzazione
della libreria, ovvero la schematizzazione del problema al fine di comprendere le esigenze
e specifiche del progetto.

Specifiche del progetto


Realizzare una libreria che permetta di leggere e scrivere un byte alla volta, nella
memoria EEPROM interna al microcontrollore.
Funzione di scrittura
L'indirizzo della cella di memoria in cui scrivere viene passato per valore, come anche il
dato. La funzione deve restituire 1 se l'operazione eseguita con successo, 0 altrimenti. Il
formato della funzione deve essere :
unsigned char write_internal_EEPROM (unsigned char address, unsigned char
data);

Funzione di lettura
L'indirizzo della cella di memoria da cui leggere viene passato per valore. Il valore
restituito dalla funzione deve essere il valore letto dall'indirizzo di memoria richiesto. Il
formato della funzione deve essere:
unsigned char read_internal_EEPROM (unsigned char address);

Sviluppo del progetto


Per realizzare una libreria si procede come per un progetto normale. Per il nome del
progetto bene usare il nome della libreria, nel nostro caso intEEPROM.
buona pratica avere un'unica cartella dove sono salvati tutti i progetti di
libreria, magari dividendo solo i file sorgenti dagli header file, come per esempio
fatto nella libreria LaurTec.

Successivamente bisogna creare un file.c con il nome della nostra libreria, nel nostro
caso intEEPROM.c e un file .h (noto come header file) intEEPROM.h.
Nel file .c viene scritto il codice vero e proprio con cui si implementano le funzioni,
mentre nel file .h sono scritti solo i prototipi delle funzioni con loro descrizione.
Questa pratica utilizzata anche nella programmazione C e C++ per PC o altri
239

microcontrollori, ed dunque indipendente dal fatto che stiamo programmando in C per


XC8.
Normalmente per sapere come utilizzare una libreria si fa riferimento al suo header file.
Per tale ragione la documentazione delle funzioni scritta in questo file. Nel nostro caso,
rispettando le specifiche si potrebbe avere:
#ifndef intEEPROM_H
#define intEEPROM_H
#ifdef __XC8
#include <xc.h>
#endif
/**
* This function writes a byte inside the internal EEPROM.
*
* @param data Byte to write [min: 0, max: 255]
*
* @param address Address where the byte must be written [min: 0, max:
255]
*
* @return status 1: The byte has been properly written
*
0: The byte has not been properly written
*
* @warning During the writing process the Interrupts are disabled. The
old status is restored afterward.
*
*/
unsigned char write_internal_EEPROM (unsigned char address, unsigned char
data);

/**
* This function reads a byte from the internal EEPROM.
*
*
* @param address Address where the byte must be read [min: 0, max: 255]
*
* @return data Value read from the internal EEPROM.
*
*/
unsigned char read_internal_EEPROM (unsigned char address);
#endif

La libreria inizia con alcune direttive:


#ifndef intEEPROM_H
#define intEEPROM_H

Questo permette di avere pi inclusioni dello stesso file, senza avere conflitti ed errori
di compilazione, infatti come detto, il codice viene incluso e compilato una sola volta.
Il nome del flag dichiarato per l'inclusione spesso uguale al nome della libreria con
l'aggiunta di _H, ma un nome vale l'altro, si deve solo garantire che nel progetto non
siano presenti definizioni uguali.
240

Successivamente viene scritto il seguente codice:


#ifdef __XC8
#include <xc.h>
#endif

Questo permette alla libreria di essere compatibile sia con il vecchio compilatore C18 che
con il nuovo XC8. In particolare viene controllato se il nome __XC8 definito. Qualora
lo sia vorrebbe dire che stiamo compilando facendo uso del compilatore XC8. Infatti
__XC8 automaticamente definito dall'ambiente di sviluppo. In particolare se il
compilatore XC8, viene incluso il file xc.h.
Dopo queste prime direttive, sono definiti i prototipi di funzione. Ogni funzione
preceduta da un commento in cui si riporta una breve descrizione della mansione svolta
dalla funzione e la descrizione dei parametri che bisogna passare e che vengono restituiti.
In questo modo l'utilizzatore ha tutte le informazioni necessarie per un suo corretto
utilizzo. Qualora si dovessero avere dei problemi con la funzione e il suo utilizzo, l'header
file un buon punto per iniziare la ricerca della documentazione sulle funzioni fornite. La
documentazione scritta facendo uso di un formato un po' particolare, ovvero conforme
a Doxygen. Doxygen uno strumento che permette di estrarre la documentazione
direttamente dal codice, evitando di dover scrivere file diversi per la documentazione.
Estrarre la documentazione direttamente dal codice, in un certo qual modo garantistiche
che sia sempre aggiornata, maggiori dettagli su Doxygen sono dati a breve.
La dichiarazione delle due funzioni seguita dalla direttiva #endif, ovvero la fine del
codice associato alla dichiarazione dei prototipi di funzioni.
Nel file .c scritto il sorgente vero e proprio. In questo file, diversamente dal file .h
non vengono normalmente scritte le informazioni associate alla funzione. Infatti se si
dovessero utilizzate librerie precompilate, si potrebbe non avere affatto il codice sorgente,
ovvero i file.c e in pi si avrebbe il problema di avere due punti in cui dover aggiornare la
documentazione. Il file .c rappresenta un file sorgente tradizionale come quelli creati con
la funzione main, ma in questo caso si implementano solo le funzioni dichiarate
nell'header file, ovvero non abbiamo nessuna funzione main.
Un modo per implementare le funzioni potrebbe essere:
#ifdef __XC8
#include <xc.h>
#endif
#include "intEEPROM.h"
//************************************************************
//
write_internal_EEPROM function implementation
//************************************************************
unsigned char write_internal_EEPROM (unsigned char address, unsigned char
data) {
// Flag used to store the GIE value

241

unsigned char flagGIE = 0;


// Flag used to store the GIEH value
unsigned char flagGIEH = 0;
// Flag used to store the GIEL value
unsigned char flagGIEL = 0;
// Set the address that will be written
EEADR = address;
// Set the data that will be written
EEDATA = data;
// EEPROM memory is pointed
EECON1bits.EEPGD = 0;
#ifdef _PIC18
// EEPROM access enable
EECON1bits.CFGS = 0;
#endif
// Enable write
EECON1bits.WREN = 0x01;
// Check and store the Interrupt Status
if (INTCONbits.GIE == 1) {
INTCONbits.GIE = 0;
flagGIE = 1;
}
#ifdef _PIC18
if (INTCONbits.GIEH == 1) {
INTCONbits.GIEH = 0;
flagGIEH = 1;
}
if (INTCONbits.GIEL == 1) {
INTCONbits.GIEL = 0;
flagGIEL = 1;
}
#endif

// Start the writing enabling sequence


EECON2 = 0x55;
EECON2 = 0xAA;
// Initiate writing process
EECON1bits.WR = 0x01;
// Wait the end of the writing process
while (EECON1bits.WR);
// Restore the previous interrupt status
if (flagGIE == 1) {
INTCONbits.GIE = 1;
}

242

#ifdef _PIC18
if (flagGIEH == 1) {
INTCONbits.GIEH = 1;
}
if (flagGIEL == 1) {
INTCONbits.GIEL = 1;
}
#endif
// Disable the writing process
EECON1bits.WREN = 0x00;
// Check if the data has been properly written,
// a simple read back is done
if (read_internal_EEPROM (address) == data) {
return (1);
} else {
}

return (0);

}
//************************************************************
//
read_internal_EEPROM Function Implementation
//************************************************************
unsigned char read_internal_EEPROM (unsigned char address) {
unsigned char data = 0;
// Set the memory address that will be read
EEADR = address;
// EEPROM memory is pointed
EECON1bits.EEPGD = 0;
#ifdef _PIC18
// EEPROM access enable
EECON1bits.CFGS = 0;
#endif
// Initiate reading
EECON1bits.RD = 0x01;
// Data is read from the register
data = EEDATA;
return (data);
}

Si pu subito notare che due semplici funzioni di lettura e scrittura possono essere
piuttosto complicate qualora debbano essere compilate in ambiente C18, XC8 e come
anche PIC16 e PIC18. Questa esigenza non contemplata nelle specifiche per cui non
243

sarebbe stato necessario ma in una libreria di uso pratico in cui si responsabili delle
specifiche, bisogna cercare di coprire anche esigenze extra che permettano alla libreria di
essere robusta. In realt le specifiche appena aggiunte non rendono la libreria pi robusta
ma semplicemente di pi ampio utilizzo. Guardando nel dettaglio la libreria ci sono per
alcuni dettagli in pi che rendono la libreria robusta contro particolari eventi ma si
comprenderanno solo alla fine del prossimo Capitolo.
All'interno del file .c viene incluso l'header file:
#include "intEEPROM.h"

Questo permette di includere il file header nel quale sono dichiarati i prototipi di
funzione e si sono inclusi gli altri file necessari alla compilazione. Ogni altro file
necessario deve essere incluso nel file di header, ed indirettamente viene incluso nel file
sorgente. Il non seguire questa pratica non crea nessun problema, ma un modo ordinato
per mostrare i file inclusi in un solo punto e segue la pratica di programmazione in C e
C++. Il programma in C ripete i passi gi spiegati quando stata introdotta la memoria
EEPROM, il cui codice era stato per scritto in Assembly. Unica differenza sta nella
funzione di scrittura in cui si gestito il caso di disabilitazione delle interruzioni e
confronto del byte scritto con quello da scrivere.
Arrivati a questo punto si potrebbe compilare il codice al fine di creare un file binario.
Sebbene questo era quanto avevo fatto in passato con il compilatore C18, al fine di restare
compatibile con la pratica della programmazione dei sistemi embedded C e C++, in realt
dal momento che nella stessa famiglia PIC18 ci possono essere vari modelli non
compatibili tra loro che richiederebbero una ricompilazione delle libreria ho iniziato a
preferire il lasciare la libreria non compilata.

Utilizziamo la nostra Libreria


Al fine di testare la libreria appena creata, vediamo ora un semplice esempio:
#include <xc.h>
#include "intEEPROM.h"
#include "intEEPROM.c"
#include "PIC18F4550_config.h"
int main (void){
unsigned char data = 0xAA;
unsigned char address = 0x00;
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;

244

// Imposto PORTC tutti ingressi


LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// Scrivo e visualizzo il dato dalla EEPROM interna
if (write_internal_EEPROM (address, data) == 1) {
LATD = read_internal_EEPROM (address);
} else {
LATD = 0xFF;
}
while(1){
}

La libreria scritta come esempio appartiene alle librerie LaurTec per PIC18 per cui tra i
percorsi da includere al fine di compilare l'esempio in maniera corretta, bisogna scrivere
tra le propriet del progetto:
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.2.0\conf
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.2.0\inc
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.2.0\src

bene notare che si sono inclusi entrambi i file .h e .c.


#include "intEEPROM.h"
#include "intEEPROM.c"

Questo non pratica comune visto che dovrebbe bastare includere il solo file .h.
L'includere il solo file .h richiederebbe per un file di libreria precompilato e il dover
includere il percorso del file di libreria. Inoltre si dovrebbero avere file multipli
precompilati di libreria per i vari microcontrollori, rendendo l'aggiornamento della libreria
piuttosto complicato. Come detto in precedenza, preferisco ora lasciare la compilazione
della libreria in fase di progetto. Per tale ragione, al fine di testare la libreria sempre
necessario scrivere un semplice programma di esempio nel quale includere la libreria
stessa.
Nel caso si debbano includere le librerie Microchip, basta includere il solo file .h.
Infatti Microchip fornisce i file di libreria precompilati che sono inclusi in
maniera trasparente. Nella guida al compilatore XC8 si sconsiglia comunque di
creare file precompilati per le ragioni citate sopra.

245

Includendo entrambi i file .h e .c si ha ogni volta la compilazione della libreria,


garantendo che venga compilata sempre per il PIC in uso, senza necessitare di di file
multipli. Un altro commento potrebbe essere legato al fatto che il file .h potrebbe non
essere necessario visto che viene incluso anche nel file .c. Questo vero ed possibile
farlo, ma per chiarezza dell'inclusione dei file preferisco scrivere sempre entrambi.
In ultimo sempre bene tenere a mente che solo il file .h protetto da doppia
inclusione e non il file .c. Volendo si potrebbe proteggere anche il file .c da una doppia
inclusione, ma ho preferito mantenere la pratica standard del proteggere il solo file .h. Per
tale ragione bisogna fare attenzione ad includere un solo file .c. Qualora ci dovessimo
sbagliare non un grosso problema, visto che il compilatore ci segnala con decine di
errori le doppie inclusioni!

Documentazione del codice


Sviluppare del software pu essere un'attivit piuttosto lunga e coinvolgere molte
persone. Scrivere del codice in maniera chiara particolarmente utile per non creare
frustrazioni nel team. Un altro aspetto che spesso si sottovaluta, pur realizzando del
codice per delle applicazioni personali che probabilmente ci troveremo a distanza di
tempo a dover rimettere le mani su del codice scritto molto tempo prima. Commentare il
codice torna particolarmente utile per agevolare la comprensione del codice stesso.
Oltre al commentare il codice bene non sottovalutare l'opportuna documentazione
delle funzioni create per svolgere particolari compiti. Infatti, spesso, se tutto funziona a
dovere non si interessati a vedere come una funzione adempia alla proprio compito,
bens sapere semplicemente che parametri passare e che valore viene restituito. Sebbene si
possa creare un documento a parte per la documentazione, si potrebbe avere la difficolt
nel mantenere allineata la documentazione e il codice, infatti si spesso pigri e alcune
volte la documentazione, all'interno di un team, anche scritta da altre persone.
Per tale ragione, al fine di garantire una documentazione sempre aggiornata bene che
questa sia scritta direttamente nel codice.
A supporto di questa filosofia sono nati diversi strumenti, e il pi noto senza
dubbio Doxygen. Doxygen uno strumento esterno a MPLAB X e deve essere eseguito
fornendogli le coordinate sul dove trovare il codice per il quale deve creare la
documentazione.
Si capisce che affinch Doxygen possa capire quale commenti estrarre, si deve
rispettare una sintassi durante la scrittura dei commenti. Non entrando nei dettagli e
spiegando solo la documentazione delle funzioni si ricordano le seguenti regole:

La documentazione di una funzione deve precedere il prototipo della funzione e


usare il seguente formato per i commenti:
/**
*/
unsigned int somma_interi (unsigned char A, unsigned char B);

246

Questo si differenzia da un commento standard multilinea per il fatto che ha due


asterischi all'inizio.

La descrizione della funzione deve essere il primo commento da scrivere.


/**
Questa funzione svolge la somma tra due interi
*/
unsigned int somma_interi (unsigned char A, unsigned char B);

I parametri della funzione devono essere introdotti dalla parola chiave @param
/**
Questa funzione svolge la somma tra due interi
@param A primo addendo [min. 0 max. 255]
@param B secondo addendo [min. 0 max. 255]
*/
unsigned int somma_interi (unsigned char A, unsigned char B);

Il parametro restituito deve essere introdotto dalla parola chiave @return


/**
Questa funzione svolge la somma tra due interi
@param A primo addendo [min. 0 max. 255]
@param B secondo addendo [min. 0 max. 255]
@return somma tra i due addendi passati alla funzione
*/
unsigned int somma_interi (unsigned char A, unsigned char B);

Nel caso siano presenti nelle note, warning possibile aggiungerle facendo uso
delle parole chiavi @note e @warning.
/**
Questa funzione svolge la somma tra due interi
@param A primo addendo [min. 0 max. 255]
@param B secondo addendo [min. 0 max. 255]
@return somma tra i due addendi passati alla funzione
@warning Gli addendi devono essere solo numeri maggiori di 0
@note Una qualunque nota...
*/
unsigned char somma_interi (unsigned char A, unsigned char B);

247

bene notare che i vari parametri sono anche descritti da un valore massimo e
minimo. Questo non obbligatorio ma un buon aiuto per il programmatore.
Sebbene abbia scritto frequentemente la parola deve essere, in realt la sintassi di
Doxygen piuttosto flessibile e sono supportati diversi formati.
Prima di eseguire Doxygen necessario impostare alcune impostazioni, in particolare
dove trovare i file sorgenti e quali file documentare. possibile anche scegliere il formato
della documentazione, che spesso semplicemente HTML. Doxygen utilizzato per
documentare codice sorgenti scritti in molti linguaggi di programmazione ed di fatto
quasi uno standard. Alcuni IDE, grazie al Parser interno, durante la scrittura del nome di
una funzione visualizzano anche la descrizione della funzione e dei parametri, prelevando
le informazioni direttamente dalla documentazione in formato Doxygen.
Nel caso delle librerie LaurTec, un esempio di impostazioni di Doxygen, facendo uso
dell'interfaccia grafica per Windows, riportato in Figura 97.
La documentazione estratta in formato HTML, pu essere letta facendo uso di un
qualunque Browser, come riportato in Figura 98. Per maggiori dettagli sull'utilizzo di
Doxygen si rimanda alla documentazione ufficiale che possibile trovare al sito
www.doxygen.org .

Figura 97: Interfaccia grafica per le impostazioni di Doxygen in ambiente Windows .

248

Figura 98: Esempio di documentazione estratta dal codice sorgente della libreria intEEPROM.

Riassunto
In questo capitolo si messo in evidenza l'importanza di organizzare in maniera
corretta un programma, in particolare mostrando come scrivere delle funzioni e
organizzare il tutto in librerie. Si sono mostrati anche altri esempi di utilizzo di puntatori,
in particolare per il valore restituito dalla funzione e i parametri in ingresso. Alcuni dettagli
sono stati forniti per quanto riguarda il passaggio di variabili per valore e per riferimento,
mostrando alcuni vantaggi e svantaggi nell'utilizzare l'uno o l'altro metodo.

Punti chiave ed approfondimenti


Variabile static: Una variabile definita static all'interno di una funzione, ha la
caratteristica di avere un indirizzo fisso durante l'esecuzione del programma. In
particolare le risorse assegnate alla variabile non sono rilasciate al termine dell'esecuzione
della funzione. La loro inizializzazione avviene una sola volta alla prima chiamata della
funzione.
Scope di una variabile: Per scope di una variabile o visibilit, si intende lo spazio
all'interno del programma, all'interno del quale una variabile pu essere richiamata ovvero
risulta visibile. All'interno di uno stesso scope non possibile avere variabili con lo stesso
nome, mentre possibile qualora lo scope sia differente. Nel caso di variabile locale con
lo stesso nome di una variabile globale, la variabile locale ha la precedenza sulla globale.
249

Variabile globale: Una variabile si definisce globale quando il suo scope rappresentato
da tutto il programma, ovvero visibile da ogni funzione. Per dichiarare una variabile in
modo globale bisogna definire la stessa prima della funzione main. Allo stesso modo di
una variabile definita static, una variabile globale possiede un indirizzo fisso durante
tutta l'esecuzione del programma.
Header file: L'header file, rappresenta il file con estensione .h all'interno del quale sono
definiti i vari prototipi di funzione. Contiene generalmente anche la documentazione delle
funzioni stesse, rappresentando il primo punto in cui guardare nel caso in cui si cerchino
informazioni sulla funzione e suoi parametri. La divisione in file .h e .c viene fatta spesso
per le librerie, fornendo un modo per separare i prototipi di funzione, dal codice sorgente
vero e proprio. Qualora la libreria sia fornita in formato compilato (binary file), il file header
rappresenta l'unica fonte di documentazione visto che il codice sorgente non leggibile,
in particolare deve essere incluso insieme al file .bin.
Doxygen: Doxygen un'applicazione multi-piattaforma che pu essere utilizzata per
estrarre la documentazione direttamente dal codice sorgente. La documentazione inclusa
nel file header, al fine di poter essere estratta da Doxygen, deve essere scritta seguendo una
particolare sintassi. L'utilizzo di Doxygen divenuto uno standard per la documentazione
di molte applicazioni software e supporta molti linguaggi di programmazione.

250

Domande ed Esercizi
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.

11.

12.

Descrivere i vantaggi derivanti dall'utilizzo di una funzione.


Per quale ragione consigliabile non creare un file precompilato della libreria e preferire
piuttosto la compilazione della stessa nel progetto in cui usata?
Le considerazioni del punto 2 varrebbero anche se si scrivesse un programma in C per PC?
In quali circostanze una variabile interna ad una funzione potrebbe essere definita static?
Descrivere la differenza tra il passaggio di una variabile per valore e per riferimento.
Perch in un'applicazione Ultra Low Power si preferisce passare una variabile per riferimento
piuttosto che per valore?
In quali casi pu tornare utile far restituire un puntatore ad una funzione?
Se una funzione dovesse restituire il puntatore ad un Array non definito come statico, che
problemi si potrebbero avere?
Qual' l'utilit nel documentare il codice sorgente ed evitare di creare una documentazione
separata per le funzioni?
Realizzare una funzione che accentando una stringa di 10 caratteri, passata per riferimento,
ne restituisca una data dalla somma di quella data pi la stringa - OK. Realizzare un
piccolo esempio completo con la funzione main e la chiamata alla funzione creata.
Realizzare una funzione che accetta come parametro una variabile definita come struttura
rettangolo (composta dei campi base e altezza) e restituisca l'area del rettangolo. Realizzare
un piccolo esempio completo con la funzione main e la chiamata alla funzione creata.
Realizzare una funzione che accetta una stinga passata per riferimento e ne alteri il suo
contenuto invertendo l'ordine dei caratteri stessi. Realizzare un piccolo esempio completo
con la funzione main e la chiamata alla funzione creata.

251

Capitolo VIII

VIII

Le Interruzioni
Quanto studiato nei Capitoli precedenti ci permette gi di scrivere applicazioni
piuttosto complesse, ma certamente ci si resi conto che manca qualcosa che permetta di
armonizzare varie parti del programma, sebbene queste possano essere scritte in funzioni.
Nella programmazione dei microcontrollori un ruolo importantissimo coperto dalla
interruzioni, che permettono appunto di interrompere il programma principale e di saltare
in altre parti della nostra applicazione. In questo Capitolo si vedremo i dettagli su cosa sia
un'interruzione e l'importanza delle stesse nello sviluppo di un'applicazione software.

Schema elettrico
Per poter seguire gli esempi presentati in questo Capitolo, richiesto realizzare su
Breadboard lo schema elettrico di Figura 99 o far uso delle schede di sviluppo impostate
come descritto nel paragrafo successivo.

Figura 99: Schema elettrico di esempio del Capitolo VIII.

252

Impostare le schede di sviluppo


Al fine di poter seguire gli esempi mostrati in questo Capitolo necessario impostare la
scheda di sviluppo utilizzata, come descritto nei paragrafi seguenti.

Freedom II
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom II deve avere i Jumper posizionati nel seguente modo:

Figura 100: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

Freedom Light
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo capitolo la
scheda di sviluppo Freedom Light deve avere i Jumper posizionati nel seguente modo:

Figura 101: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

La scheda Freedom Light possiede i LED solo sui pin RD0 e RD1. Per poter
visualizzare gli altri LED necessario montarne altri sul connettore di
espansione, facendo per esempio uso della scheda di espansione Breadboard
PJ7011 o limitare il Debug su due soli LED.

254

Impostazioni software
Gli esempi mostrati non richiedono alcuna impostazione software particolare oltre alla
semplice creazione di un progetto.

Che cosa sono le interruzioni?


Abbiamo visto durante la fase della simulazione di un programma che la nostra
applicazione viene eseguita passo passo, incrementando il Program Counter. Dagli esempi
visti si capisce che un programma viene sempre eseguito in maniera progressiva
eseguendo un passo alla volta. A seconda della presenza o meno di un salto condizionale
derivante da istruzioni if od altro, il Program Counter viene incrementato o decrementato
in maniera opportuna. Altro caso in cui il Program Counter viene alterato in maniera
inusuale potrebbe essere la chiamata di una funzione. Infatti in questo caso il valore del
Program Counter viene memorizzato nello Stack prima di fare il salto all'indirizzo di
memoria dove inizia la funzione. Il valore del Program Counter relativo all'indirizzo
dell'istruzione che dovr essere eseguita al ritorno dalla funzione, viene ripristinato in
automatico alla fine della funzione stessa. Sebbene la chiamata di una funzione possa
portare il PC ad essere cambiato con un valore diverso dal tipico passo in avanti o
indietro eseguito in un ciclo di programma, la chiamata della funzione nota a priori ed
parte dell'algoritmo creato.
Ci sono casi in cui il nostro programma potrebbe dover chiamare delle funzioni
indipendentemente dallo stato del programma principale. Se per esempio il programma
principale sta facendo lampeggiare un LED come potrebbe gestire la pressione di un
pulsante o dei dati inviati alla porta seriale?
Molto semplice, farebbe quello che fa un essere umano, ovvero lascerebbe quello che sta
facendo e cercherebbe di adempiere alle attivit richieste dagli altri eventi.
Quello appena descritto proprio un'interruzione o interrupt, o meglio un evento che
potrebbe causare un'interruzione.
Un'interruzione non altro che una pausa forzata dall'attivit principale al fine di
compiere delle azioni richieste da altri eventi.
L'interruzione non permette di compiere azioni in parallelo, ma permette di
gestire in maniera rapida eventi che potrebbero richiedere attenzione in un
qualunque momento, indipendente dalla monotonia dell'applicazione principale.

Le interruzioni
Da quanto fin ora spiegato si capisce che i microcontrollori hanno la possibilit di
essere utilizzati in sistemi molto complessi in cui sia richiesto di svolgere molte operazioni
in contemporanea. In realt qualunque microcontrollore, per quanto veloce sia, svolger
un'operazione alla volta; le sue velocit di esecuzione sono tali per da dare l'impressione
che siano svolte molte operazioni in contemporanea. Ogni qual volta si abbia l'esigenza
di svolgere pi operazioni bene strutturare il programma in modo da poter gestire il
tutto in maniera snella e funzionale. La tecnica del Polling potrebbe creare qualche
255

problema poich si perde molto tempo a controllare periferiche che magari sono inattive.
La gestione di pi funzioni o processi notevolmente migliorata per mezzo dell'utilizzo
degli interrupt, ovvero segnali inviati dall'hardware quando viene a verificarsi un
determinato evento. I PIC18 possiedono, per ogni modulo hardware interno varie
modalit e tipologie d'interrupt in maniera da permettere l'utilizzo dell'hardware non solo
per mezzo della tecnica del Polling ma anche per mezzo degli interrupt. Per esempio la
PORTB per i pin RB4-RB7 genera un evento d'interrupt qualora ci sia il cambio del valore
logico sul pin, che potrebbe essere causato per esempio dalla pressione di un pulsante.
Un'interruzione di cui si fatto inconsapevolmente uso e presente in tutti i
microcontrollori quella che si viene a generare con il segnale di Reset. Infatti il Reset
rappresenta un'interruzione che in particolare interrompe la normale esecuzione del
programma facendolo iniziare nuovamente da capo. Altre interruzioni tipiche sono quelle
che vengono generate da periferiche interne, come per esempio il convertitore analogico
digitale (ADC), l'USART, i Timer, le linee sulla PORTB e altro ancora.
Questi tipi d'interruzione, a differenza dell'interruzione generata dal Reset non fanno
iniziare il programma da capo ma lo fanno continuare a partire da un punto specifico del
programma stesso; questo punto viene detto vettore d'interruzione (interrupt Vector)
Quando avviene un'interruzione da parte delle periferiche, prima di saltare al vettore
d'interruzione, l'Hardware120 si preoccupa di salvare tutte le informazioni necessarie per
poter riprendere dal punto in cui il programma stato interrotto. Allo stesso modo delle
funzioni, come visto in precedenza, il PC viene salvato nello Stack in maniera da
riprendere il programma dal punto in cui stato interrotto 121.
Il PIC18F4550 come ogni PIC18, possiede due livelli d'interruzione, ovvero due vettori
di interruzione. Il vettore d'interruzione ad alta priorit posizionato all'indirizzo di
memoria 0x08 e il vettore d'interruzione a bassa priorit posizionato all'indirizzo di
memoria 0x18.
Ogni interruzione a bassa priorit pu interrompere il normale flusso del programma
permettendo di svolgere la particolare funzione associata con la gestione delle stessa. Le
istruzioni ad alta priorit possono interrompere sia il normale flusso del programma sia
l'esecuzione della routine associata alle interruzioni a bassa priorit. Da questa possibilit
si capisce per quale ragione questa seconda interruzione viene detta ad alta priorit. Lo
svolgimento della funzione associata alle interruzioni ad alta priorit non pu essere
invece interrotta n da un'interruzione ad alta priorit n da un'interruzione a bassa
priorit. Ciononostante, nel caso si dovesse premere il tasto di Reset anche la gestione di
un'interruzione ad alta priorit verrebbe interrotta per far iniziare il programma
nuovamente da capo; il Reset in un certo qual modo ad altissima priorit.
Il verificarsi di un'interruzione, dopo essere stata accettata, causa il salvataggio del
registro PC e il suo cambio in modo da puntare l'interrupt Vector di competenza. Oltre a
questa operazione vengono in realt salvati i registri WREG, BSR e lo Status Register. La
modalit di salvataggio degli stessi differisce a seconda del tipo d'interruzione. Si capisce
intanto che il posto dove vengono salvati deve essere differente per i due tipi
d'interruzione, perch se cosi non fosse l'interruzione ad alta priorit potrebbe causare la
perdita di dati associati ad un'interruzione a bassa priorit.
Nel caso d'interruzione a bassa priorit i registri ora citati vengono salvati nello Stack
per mezzo di normali operazioni MOVFF, MOVWF e MOVF. Nel caso in cui si verifichi
120
121

In realt anche il software interviene spesso per il salvataggio di variabili particolari che altrimenti non verrebbero salvate.
L'eventuale istruzione caricata nell'ISR viene comunque terminata.

256

un'interruzione ad alta priorit i registri WREG, BSR, STATUS vengono salvati in


automatico nei registri shadow, ovvero ombra, per mezzo di un solo ciclo di clock. Da
quanto detto si capisce che il tempo richiesto per iniziare lo svolgimento della routine
associata ad un'interruzione diverso nel caso in cui si tratti di un'interruzione a bassa o
alta priorit, in particolare quelle ad alta priorit sono gestite pi velocemente. Tale
intervallo di tempo noto come Latency (tempo di latenza) ed un parametro di
fondamentale importanza nel caso in cui si stia realizzando un sistema Real Time122.
Per la gestione delle interruzioni necessario impostare i seguenti registri:

RCON
INTCON
INTCON2
INTCON3
PIR1, PIR2
PIE1, PIE2
IPR1, IPR2

In realt a seconda delle periferiche che sar necessario gestire solo alcuni registri
dovranno essere propriamente impostati, in particolare solo alcuni bit. Ciononostante il
registro RCON ed INTCON hanno al loro interno i bit fondamentali per la gestione delle
interruzioni. All'interno di tali registri sono presenti tutti i bit ovvero le impostazioni
necessarie per la gestione delle interruzioni 123. Per maggiori informazioni sui singoli
registri si rimanda al Datasheet del PIC utilizzato.
Ogni tipo d'interruzione che pu essere generata da una periferica, possiede tre bit di
controllo posizionati in punti diversi nei registri ora citati:

Bit di Enable
Bit per il livello Priorit
Bit di Flag

Il bit di Enable permette di abilitare l'interruzione della periferica stessa. Infatti non
sempre si vuole che un evento generi un'interruzione del programma principale. A
seconda della gestione di una periferica si potrebbe gestire la stessa sia in Polling che per
mezzo delle interruzioni. Il bit di Enable abilita la possibilit di generare l'interruzione,
ovvero di poter interrompere il flusso del programma principale, ma necessario
impostare anche il livello di priorit. Abbiamo infatti visto che i PIC18 possiedono due
livelli di priorit. Una corretta scelta del livello di priorit permette o meno di armonizzare
la gestione delle periferiche nella particolare applicazione che si sta scrivendo.
Il bit di Flag segnala il fatto che l'evento che pu generare un'interruzione si
verificato. Per esempio il Flag relativo alla ricezione di un byte sulla porta seriale (UART)
viene abilitato ogni qual volta che si riceve un byte.
122

123

Si ricorda che un sistema Real Time non un sistema necessariamente veloce, ma un sistema che garantisce lo svolgimento
di determinate operazioni o compiti in un tempo predefinito. Conoscere il tempo di latenza permette di valutare il tempo
richiesto ad una routine prima di essere chiamata. Oltre al tempo di latenza possono poi intervenire altri ritardi derivanti
dalla gestione dei Task da parte del Sistema Operativo.
Il modulo USB un po' un mondo a parte per quanto riguarda le interruzioni. La sua gestione esula dallo scopo di tale testo,
per cui si rimanda al Datasheet per ulteriori informazioni.

257

Il bit di Flag vengono attivati indipendentemente dal fatto che la periferica abbia
o meno le interruzioni abilitati, per cui possono essere usati anche per mezzo del
Polling per controllare il verificarsi di un certo evento. La generazione di
un'interruzione da parte del Flag richiede che le interruzioni della periferica siano
abilitate.
Oltre a questi tre bit associati ad ogni tipologia d'interruzione, sono presenti anche altri
bit per la gestione globale delle interruzioni. I bit per la gestione globale delle interruzioni
sono i bit GIE e PEIE del registro INTCON e il bit IPEN del registro RCON. I PIC18
pur potendo gestire le interruzioni ad alta e bassa priorit possono anche lavorare in
modalit compatibile con i PIC16. Per lavorare in modalit compatibile bisogna impostare
a 0 il bit IPEN, questo per altro il valore di default.
Quando IPEN vale zero, il bit GIE permette di abilitare in maniera globale le
interruzioni mentre PEIE permette di abilitare/disabilitare le interruzioni delle
periferiche. Quando i bit valgono 1 la funzione abilitata, mentre quando valgono 0
disabilitata. Quando il PIC18 lavora in maniera compatibile con i PIC16 l'Interrupt Vector
posto all'indirizzo 0x08, cio come se fosse abilitata l'interruzione ad alta priorit.
Per lavorare in maniera compatibile sono richieste le seguenti istruzioni:
// Abilito modalit compatibile (di default vale gi 0)
RCONbits.IPEN = 0;
// Abilito l'interrupt globale
INTCONbits.GIE = 1;
// Abilito interrupt per periferiche
INTCONbits.PEIE = 1 ;

In questa configurazione i bit per selezionare la priorit non devono essere impostati,
poich di default valgono 1, ovvero abilitati per funzionare ad alta priorit.
Nel caso si voglia fare uso delle funzionalit avanzate d'Interrupt offerte dall'architettura
PIC18, bisogna settare i bit precedenti in maniera differente. In particolare necessario
porre ad 1 il bit IPEN. Quando questo bit viene abilitato i bit IPEN e GIE del registro
INTCON assumono un altro significato. Il bit GIE, anche nominato GIEH, permette, se
posto ad 1, di abilitare le interruzioni ad alta priorit. Il bit PEIE, anche nominato GIEL,
se posto ad 1, abilita tutte interruzioni a bassa priorit. In questo caso si hanno i due
vettori d'interruzione posti all'indirizzo 0x08 (alta priorit) e 0x18 (bassa priorit). Quando
si lavora con i due livelli di priorit necessario impostare i bit dei registri di priorit IPR
e IPR2. Per abilitare le interruzioni con i due livelli di priorit si deve procedere come
segue:
// Abilito modalit interruzione a due livelli alta e bassa
RCONbits.IPEN = 1;
// Abilito gli interrupt ad alta priorit
INTCONbits.GIEH = 1;
// Abilito gli interrupt a bassa priorit
INTCONbits.GIEL = 1 ;

258

Prima di abilitare le interruzioni, consigliabile aver precedentemente abilitato


ed impostato tutte le periferiche che dovranno generare gli interrupt. Questo
valido anche nella modalit compatibile PIC16. In particolare si consiglia di
porre a 0 il Flag delle interruzioni d'interesse, in maniera da evitare che
all'attivazione delle stesse venga richiesta un'interruzione derivante da un vecchio
valore del Flag.
I vettori delle interruzioni sono 0x08 o 0x18 a seconda del tipo d'interruzione o
modalit abilitata. Si capisce che a partire da questi indirizzi non possibile scrivere
l'intero programma di gestione delle interruzioni, si pensi ad esempio che tra il vettore ad
alta priorit e bassa priorit sono presenti solo 16 locazioni di memoria. Per tale ragione
quello che si fa posizionare dei salti, che dai vettori d'interruzione posizionano il
programma (ovvero il Program Counter) alle relative funzioni di gestione dell'interruzione
nominate Interrupt Service Routine (ISR). Quanto appena detto viene fatto in automatico dal
compilatore ma bisogna dichiarare le funzioni in maniera opportuna a seconda del livello
di priorit utilizzato. In particolare la dichiarazione delle funzioni delle interruzioni,
tenendo abilitato l'opzione CCI, :
//*******************************************
//
ISR Alta Priorit
//*******************************************
__interrupt (high_priority) void ISR_alta (void) {
//Programma di gestione dell'interruzione
}
//*******************************************
//
ISR Bassa Priorit
//*******************************************
__interrupt (low_priority) void ISR_bassa

(void) {

//Programma di gestione dell'interruzione


}

In particolare si deve far uso della macro


__interrupt

specificando il livello dell'interruzione d'interesse:

high_priority
low_priority

Fatto questo bisogna specificare il nome della funzione che utilizzeremo per la gestione
dell'interruzione. Come si vede dalla dichiarazione sopra, entrambi le funzioni devono
essere dichiarate con parametri di chiamata e di restituzione di tipo void. Il nome della
funzione pu essere arbitraria, ma dal momento che viene specificato il livello di
259

interruzione utilizzato, il compilatore sa a quale vettore delle interruzioni si sta facendo


riferimento.
All'interno delle funzioni appena dichiarate, dal momento che vengono richiamate per
la gestione delle interruzioni da parte di periferiche differenti, al fine di discriminare la
periferica che ha generato l'interruzione, necessario controllare i Flag degli interrupt
associati alle periferiche.
Ogni volta che viene identificata la periferica che ha generato l'interrupt, bisogna
porre il relativo Flag delle interruzioni a 0 al fine di permettere la gestione di altre
interruzioni. Qualora non si dovesse resettare il Flag verrebbero generate in
maniera continua delle interruzioni.
Per i Flag delle interruzione, come per gli altri campi dei registri, il nome dei bit lo
stesso utilizzato nel Datasheet. In particolare per cambiare il valore del bit bisogna
scrivere:
NOME_REGISTRObits.nomeflag

Per esempio per modificare il bit GIE presente nel registro INTCON si pu scrivere:
INTCONbits.GIE = 1;

Vediamo un esempio in cui si voglia gestire la pressione del tasto BT1 collegato alla
linea RB4 del microcontrollore. In particolare si gestir tale interruzione in modalit
compatibile con i PIC16, ovvero facendo uso dell'Interrupt Vector posto all'indirizzo 0x08
(alta priorit).
#include <xc.h>
#include "PIC18F4550_config.h"
// Funzione per la gestione dell'interruzione
__interrupt (high_priority) void ISR_alta (void) {
// Indice per il ciclo di pausa
unsigned int i;
// Controllo che l'interrupt sia stato generato da PORTB
if (INTCONbits.RBIF == 1 ) {
//pausa filtraggio spike
for (i=0; i<10000; i++){
}
// Controllo la pressione di RB4
if (PORTBbits.RB4 == 0) {
// Inverto lo stato del LED 0
LATDbits.LATD0 = ~LATDbits.LATD0;
}

260

// Resetto il flag d'interrupt per permettere nuove interruzioni


INTCONbits.RBIF = 0;
}

int main (void){


// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// Abilita i resistori di pull-up sulla PORTB
INTCON2bits.RBPU = 0x00;
// Abilito le interruzioni su PORTB
INTCONbits.RBIE = 1;
// Abilito modalit compatibile (di default vale gi 0)
RCONbits.IPEN = 0;
// Resetto il flag d'interrupt prima di attivare le interruzioni
INTCONbits.RBIF = 0;
// Abilito l'interrupt globale
INTCONbits.GIE = 1;
// Abilito l'interrupt periferiche
INTCONbits.PEIE = 1 ;
// Ciclo infinito
while(1){
// non faccio nulla...
}
}

In questo esempio si vede che possibile gestire la pressione di un pulsante senza che
il microcontrollore stia di continuo a leggere il valore del pin a cui collegato il pulsante.
In particolare la funzione main praticamente uguale alla funzione main degli altri
progetti. Unica differenza che prima di entrare nel ciclo while a non far nulla, si
261

impostano ed abilitano le interruzioni. Per poter utilizzare i pulsanti mostrati nello schema
di Figura 99 necessario attivare i resistori di pull-up interni al PIC:
// Abilita i resistori di pull-up sulla PORTB
INTCON2bits.RBPU = 0x00;

Dopo aver attivato i resistori di pull-up si abilitano le interruzioni associate ai pin della
PORTB, ovvero si setta il bit RBIE del registro INTCON:
INTCONbits.RBIE = 1;

Una volta impostato l'hardware per la gestione delle periferiche, vengono abilitate le
interruzioni:
// Resetto il flag d'interrupt prima di attivare le interruzioni
INTCONbits.RBIF = 0;
// Abilito modalit compatibile (di default vale gi 0)
RCONbits.IPEN = 0;
// Abilito l'interrupt globale
INTCONbits.GIE = 1;
// Abilito l'interrupt periferiche
INTCONbits.PEIE = 1 ;

Si pu notare come il Flag delle interruzioni sia resettato prima di abilitare le


interruzioni stesse. Questo serve per evitare che eventuali interruzioni in attesa non siano
eseguite all'avvio delle stesse124.
Fatto questo il programma entra in un ciclo infinito while (1) e non fa nulla. In
applicazioni reali si potrebbe mandare in stato di Sleep il microcontrollore in modo da
risparmiare energia. Si ricorda infatti che le interruzioni sono tra gli eventi che permettono
di risvegliare il PIC dallo stato di Sleep. Oltre a dormire, il PIC potrebbe anche svolgere
altre operazioni, quali per esempio fare il Polling su altre periferiche...o fischiare!
La funzione per la gestione delle interruzioni la seguente:
// Indice per il ciclo di pausa
unsigned int i;
// Controllo che l'interrupt sia stato generato da PORTB
if (INTCONbits.RBIF == 1 ) {
//pausa filtraggio spike
for (i=0; i<10000; i++){
}
// Controllo la pressione di RB4
if (PORTBbits.RB4 == 0) {
// Accendo il LED 0
LATDbits.LATD0 = ~LATDbits.LATD0;
124

Nel caso specifico il filtro inserito nei pulsati annullerebbe eventuali interruzioni visto che al controllo del pulsante non
verrebbe trovato alcun pulsante premuto.

262

}
// Resetto il flag d'interrupt per permettere nuove interruzioni
INTCONbits.RBIF = 0;
}

Inizialmente si dichiarata la variabile i utilizzata come nostro solito per un filtraggio


degli spike. Da quanto appena visto si capisce che una funzione per la gestione delle
interruzioni non differisce da una normale funzione.
All'interno della funzione si controlla se l'interrupt stato generato dalla pressione di un
pulsante ovvero dalla variazione di stato dei bit della PORTB. Per fare questo si controlla
il bit RBIF del registro INTCON. In questo caso, essendo PORTB l'unico hardware
abilitato ad interrompere non sarebbe in realt necessario fare questo controllo, ma per
chiarezza e generalizzazione abbiamo fatto tale test.
if (INTCONbits.RBIF == 1 )

Una volta assodato che l'interrupt stato generato dalla pressione di un pulsante, viene
posto un filtro per eliminare gli spike, semplicemente facendo un conteggio a vuoto. Un
conteggio di questo tipo viene detto bloccante, poich durante il conteggio non
possibile fare altre cose.
Dopo la pausa viene controllato se il pulsante ancora premuto, qualora dovesse
essere ancora premuto il pin RD0, collegato con il LED 0, viene invertito di stato per
mezzo dell'operatore bitwise ~.
LATDbits.LATD0 = ~LATDbits.LATD0;

Dunque se il LED spento viene acceso, mentre se acceso viene spento. Una volta
svolta l'operazione di gestione dell'interrupt viene resettato il bit RBIF.
INTCONbits.RBIF = 0;

Si noti che tale istruzione appartiene al blocco if e non alla funzione principale.
Questo fondamentale qualora si gestiscano anche altre interruzioni. Infatti se si pulissero
tutti i flag d'interruzione alla fine della funzione di gestione dell'interruzione, qualora una
periferica avesse generato un interrupt durante l'esecuzione di istruzioni associate ad
un'altra interruzione, tale informazione verrebbe persa; dunque ogni flag deve essere
posto a 0 all'interno del blocco if che ha controllato il suo stato. A fine gestione di un
interrupt, qualora qualche altra periferica avesse richiesto un interruzione, potr essere
gestita mettendo nuovamente in attesa il programma principale. Infatti ogni interruzione
abilitata sia essa ad alta o bassa priorit risulta a priorit pi alta rispetto all'esecuzione del
programma principale.
Dal momento che non sono ancora note molte periferiche interne al PIC, non sono
presentati altri esempi sulla gestione delle interruzioni abilitando sia l'alta che la bassa
priorit. Esempi con ambedue le modalit attive saranno comunque mostrati nei prossimi
Capitoli.

263

Considerazioni sull'uso delle ISR


Utilizzare le interruzioni alcune volte porta confusione al programmatore non abituato
al suo utilizzo, visto che il programma perde quella linearit concettuale che spesso aiuta
nella comprensione del programma stesso. Dall'altro lato i programmatori pi esperti
spesso sottovalutano altri aspetti. Vediamo alcune cose da tenere a mente quando si
scrivono le Iterrupt Service Routine.
Uso delle variabili volatile
Le Interrupt Service Routine escluso il fatto che hanno un indirizzo fisso al quale si
sposta il PC al verificarsi di un determinato evento d'interruzione, sono
praticamente delle funzioni normali. Particolarit messa in evidenza nel paragrafo
precedente legata al fatto che non possibile passare alcun parametro in ingresso e
non restituiscono alcun valore. Naturalmente comunque possibile restituire dei
valori facendo uso delle variabili globali, ovvero di variabili che sono visibili sia alle
funzioni di ISR che al programma principale.
Qualora si dovessero usare variabili globali si deve avere l'accortezza di dichiarare la
variabile globale di tipo volatile. Se per esempio per dichiarare una variabile
globale temperatura, si scrivesse fuori dalla funzione main:
unsigned int temperatura = 0;

Questa variabile cosi definita potrebbe essere utilizzata dalla funzione main come
anche da qualunque altra funzione. Qualora tale variabile sia utilizzata dalla funzione
di gestione delle interruzione (qualunque sia la priorit) eventuali assunzioni che il
compilatore potrebbe fare per ottimizzare il codice non varrebbero pi, poich la
variabile temperatura potrebbe essere variata in qualunque momento dalla funzione
d'interrupt.
Spesso il compilatore cerca infatti di ottimizzare il codice cambiando l'ordine delle
istruzioni o utilizzando il contenuto di un vecchio registro piuttosto che ripetere
un'operazione che presumibilmente porterebbe allo stesso risultato.
Se il risultato di un'operazione potesse essere cambiato da un ISR si capisce che il
suo valore potrebbe cambiare inaspettatamente, infatti il compilatore non pu per
esempio prevedere quando verr premuto un pulsante o si ricever un dato sulla
porta seriale.
Per tale ragione, al fine di mettere in guardia il compilatore e obbligarlo a non
fare assunzioni di alcun tipo nei confronti della variabile d'interesse, bisogna
dichiarare la variabile in questo modo.
volatile unsigned int temperatura = 0;

ISR semplici e veloci


Sebbene le ISR siano delle funzioni come le altre, bene che le operazioni svolte
all'interno delle stesse siano pi semplici possibili ovvero permettano l'esecuzione
dell'ISR nel tempo pi breve possibile. Si fatto infatti notare che durante
l'esecuzione dell'ISR non si possono gestire ulteriori eventi d'interruzione a meno
che il livello di priorit interruzione sia superiore a quello in esecuzione. In
264

applicazioni Real Time, non avere il controllo del tempo di esecuzione potrebbe
facilmente causare dei problemi. Si pensi per esempio che un ISR impieghi 1ms e si
garantisce ad un cliente che entro 2ms si riesce a servire un'interruzione di un'altra
periferica; fin qui tutto bene. Ma se la nostra ISR dovesse diventare troppo
complessa, potremmo facilmente eccedere i 2ms uscendo dunque fuori specifica. Al
fine di garantire che il tempo dell'ISR non vari al variare della complessit dell'ISR
consigliabile impostare delle variabili base nell'ISR e svolgere le operazioni pi
pesanti al di fuori dell'ISR stessa.
Tipiche cose da evitare sono l'utilizzo di funzioni bloccanti all'interno di un ISR,
come per esempio funzioni di delay, usate per esempio nell'esempio precedente.
Altre tecniche per permettere lo svolgimento di operazioni al di fuori dell'ISR per
mezzo delle macchine a stati, ovvero un programma le cui operazioni da svolgere
sono scandite non direttamente dagli eventi ma dagli stati, i quali vengono invece
cambiati dagli eventi.
Per esempio un sistema molto semplice con due soli stati ON e OFF (acceso e
spento) come nel caso dell'esempio precedente, potrebbe essere scritto:
#include <xc.h>
#include "PIC18F4550_config.h"
#define STATE_OFF
#define STATE_ON
#define STATE_BUTTON_PRESSED

0x00
0x01
0x02

volatile unsigned char state = STATE_OFF;


//*********************************************
// Funzione per la gestione dell'interruzione
//*********************************************
__interrupt (high_priority) void ISR_alta (void) {
// Controllo che l'interrupt sia stato generato da PORTB
if (INTCONbits.RBIF == 1 ) {
state = STATE_BUTTON_PRESSED;

// Resetto il flag d'interrupt per permettere nuove interruzioni


INTCONbits.RBIF = 0;

}
//*********************************************
// Funzione principale
//*********************************************
int main (void){
unsigned int i;
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;

265

TRISB = 0x10;
// Imposto PORTC tutti ingressi
LATC = 0x00;
TRISC = 0xFF;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// Abilita i resistori di pull-up sulla PORTB
INTCON2bits.RBPU = 0x00;
// Abilito le interruzioni su PORTB
INTCONbits.RBIE = 1;
// Abilito modalit compatibile (di default vale gi 0)
RCONbits.IPEN = 0;
// Resetto il flag d'interrupt prima di attivare le interruzioni
INTCONbits.RBIF = 0;
// Abilito l'interrupt globale
INTCONbits.GIE = 1;
// Abilito l'interrupt periferiche
INTCONbits.PEIE = 1 ;
// Ciclo infinito
while(1){
switch (state) {
case STATE_BUTTON_PRESSED:
//pausa filtraggio spike
for (i=0; i<10000; i++){
}
// Controllo la pressione di RB4
if (PORTBbits.RB4 == 0) {
if (LATDbits.LATD0 == 1){
state = STATE_OFF;
} else {
state = STATE_ON;
}

}
break;

case STATE_ON:
LATDbits.LATD0 = 1;
break;

case STATE_OFF:
LATDbits.LATD0 = 0;
break;

266

Reset del Flag delle interruzioni


Abbiamo visto che all'interno dell'ISR necessario ripulire il Flag delle
interruzioni una volta identificato lo stesso. La domanda che ci si pu porre
quando sia meglio riporre il flag delle interruzioni a 0, ovvero subito dopo aver
identificato il flag pari ad 1 o alla fine dell'esecuzione della parte di programma
associata all'interruzione. In generale entrambi gli approcci sono validi e le ragioni
che portano l'una o l'altra scelta sono spesso le seguenti:

bene pulire il Flag delle interruzioni subito dopo l'aver identificato il flag delle
interruzioni stesse. In questo modo eventuali ulteriori interruzioni da parte della
stessa periferica possono essere identificate anche durante l'esecuzione dell'ISR.
Infatti se durante l'esecuzione dell'ISR un nuovo evento di interruzioni si dovesse
verificare, porrebbe il Flag delle interruzioni ad 1 ed uscendo dall'ISR verrebbe
nuovamente eseguita la funzione delle interruzioni senza aver perso l'evento
stesso.
bene pulire il Flag solo alla fine delle istruzioni nel caso in cui si debba garantire
che altre interruzioni siano ignorate se si stanno eseguendo ancora le operazioni
associate all'interruzione precedente.

Sequenza di esecuzione delle interruzioni


Nel caso in cui si dovessero presentare due interruzioni dello stesso istante,
ovvero ciclo istruzione, per discriminare quale delle due interruzioni debba essere
eseguita per prima, verrebbe effettuato il controllo sul livello di priorit. Se per
esempio la pressione di un tasto avviene in contemporanea alla ricezione di un dato
sulla porta seriale a seconda di quale periferica sia assegnata al livello di interruzione
ad alta priorit verrebbe eseguita prima l'ISR per la pressione del pulsante o della
porta seriale.
Nel caso in cui durante l'esecuzione di un ISR si dovessero verificare altre due
interruzioni dello stesso livello di priorit, uscendo dall'ISR verrebbe nuovamente
generato un'interruzione. L'ordine di esecuzione all'interno della stessa ISR verrebbe
a dipendere dall'ordine con cui vengono controllati i Flag delle interruzioni. Per tale
ragione l'ordine con cui vengono controllati i Flag delle interruzioni, oltre alla
suddivisione delle periferiche in alta e bassa priorit, copre un ruolo particolarmente
importante.
Quando si parla di interruzioni che avvengono nello stesso istante, si pensa che la
probabilit che ci accada sia piuttosto bassa. Questo non in realt vero, visto che
stesso istante significa semplicemente che nel momento del controllo dei flag delle
interruzioni pi flag valgono 1. Nel caso in cui si sta eseguendo un ISR la probabilit
che si verifichino altre interruzioni non improbabile. Ciononostante solo al
termine dell'ISR verrebbero controllati i flag delle interruzioni, e pi di un flag
potrebbe valere 1, per cui come se sono avvenuti nello stesso istante.
Interrupt Service routine per ogni livello e periferica
Negli esempi mostrasti si sono dichiarate le Iterrupt Service Routine solo a partire
dal Capitolo in questione. Questo naturalmente giustificato da scopi didattici visto
che era inutile introdurre ISR non usati portando solo confusione. Nell'esempio di
267

questo Capitolo si sono viste le ISR ma si sono controllati solo i flag relativi alle
periferiche d'interesse. In applicazioni professionali si effettua spesso il controllo dei
flag delle interruzioni anche di periferiche non abilitate, in maniera da poter
individuare eventuali problemi ed eventualmente decidere se effettuare un Reset di
sistema al fine di far ripartire il sistema da uno stato noto.

Riassunto
In questo Capitolo abbiamo visto come utilizzare le interruzioni al fine di armonizzare
eventi da parte dei moduli interni al microcontrollore. Si sono visti i due livelli di priorit
utilizzabili per poter gestire due gruppi di periferiche ai quali viene assegnato un livello di
importanza diverso. Il Capitolo, sebbene non si sia introdotta la gestione multipla di
interruzioni, ha messo in evidenza diverse considerazioni da tenere a mente quando si
scrivono applicazioni con le interruzioni. In particolare si introdotta la variabile di tipo
volatile al fine di limitare problemi con l'utilizzo delle variabili globali.

Punti chiave ed approfondimenti


Interruzione: L'interruzione rappresenta una sospensione dell'esecuzione principale del
programma per permettere la gestione di eventi associati a periferiche interne al
microcontrollore o esterne.
Livelli di priorit: Le interruzioni interne ai PIC18 si possono raggruppare in alta o
bassa priorit a seconda delle impostazioni della periferica stessa. Le interruzioni a bassa
priorit possono interrompere l'esecuzione dell'applicazione principale ma non sono
interrotte da altre interruzioni a bassa priorit. Le interruzioni ad alta priorit possono
interrompere sia il programma principale che l'esecuzione di una ISR a bassa priorit.
Interrupt Vector: L'Interrupt Vector rappresenta l'indirizzo di memoria al quale si sposta il
Program Counter al verificarsi di un evento di interruzione. A seconda che sia ad alta o a
bassa priorit, il PC si sposta rispettivamente all'indirizzo 0x08 o 0x18. A partire da questi
indirizzi viene normalmente scritta l'istruzione GOTO per puntare l'Interrupt Service Routine
associata al livello d'interruzione.
Interrupt Service Routine: L'Interrupt Service Routine rappresenta la funzione associata al
livello d'interruzione d'interesse. Al suo interno contiene i vari controlli per determinare
quale periferica ha causato l'interruzione. L'esecuzione delle istruzioni associate all'evento
dell'interruzione possono essere sia interne che esterne alla funzione stessa, a seconda
dell'approccio di programmazione utilizzato.
Variabile volatile: Una variabile definita volatile permette di impedire al compilatore di
effettuare ottimizzazioni sulla variabile stessa, evitando che possa assumere valori
sbagliati. Il suo utilizzo viene spesso associato alle variabili globali che possono essere
variate sia dall'ISR che il resto del programma.

268

Domande ed Esercizi
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.

11.

Che cos' un'interruzione?


Elencare tre possibili fonti d'interruzione del microcontrollore.
Qual' l'evento d'interruzione a pi alta priorit?
Per quale ragioni variabili globali usate all'interno del programma e le ISR sono definite
volatile?
Descrivere i vantaggi che si hanno nell'avere due livelli di priorit rispetto al singolo livello
di priorit dei PIC16.
Spiegare il criterio con cui il microcontrollore gestisce Interruzioni multiple dello stesso
livello.
Per quale ragione il tempo di latenza di un'interruzione ad alta priorit pi bassa di quella
a bassa priorit?
Cosa sono i registri shadow?
Per quale ragioni pu tornare utile gestire le interruzioni per mezzo di stati logici, evitando
di scrivere il programma nelle ISR?
Scrivere un programma che permetta di accendere 4 LED ognuno assegnato ad un
pulsante. Alla pressione di un pulsante si deve accendere il LED assegnato al pulsante. Alla
nuova pressione del pulsante il LED si deve spegnere. Gestire il tutto per mezzo di
interruzioni.
Riscrivere il programma del punto precedente gestendo il tutto per mezzo di stati di
sistema, evitando di scrivere il programma di gestione dei LED direttamente all'interno
dell'ISR, se non limitatamente al fatto del cambio di stato.

269

Capitolo IX
Utilizziamo un Display alfanumerico LCD
In questo Capitolo si introduce una periferica esterna al PIC, ovvero il display
alfanumerico LCD. Il suo utilizzo cosi frequente nonch importante, che bene
parlarne subito. Infatti il modulo LCD torner utile anche negli esempi che seguiranno,
fornendo un'interfaccia grafica sulla quale visualizzare i dati. I display LCD alfanumerici
permettono infatti con pochi soldi di creare un'interfaccia grafica professionale,
permettendo ai nostri programmi di colloquiare con l'utilizzatore finale. In particolare, al
fine di mantenere la discussione quanto pi semplice possibile, l'utilizzo del display viene
mostrato facendo uso della libreria LaurTec. In questo modo, con poche righe,
saluteremo nuovamente il mondo!

Schema elettrico
Per poter seguire gli esempi presentati in questo Capitolo, richiesto realizzare su
Breadboard lo schema elettrico di Figura 102 o far uso delle schede di sviluppo impostate
come descritto nel paragrafo successivo.

Figura 102: Schema elettrico di esempio del Capitolo IX.

270

Impostare le schede di sviluppo


Al fine di poter seguire gli esempi mostrati in questo Capitolo necessario impostare la
scheda di sviluppo utilizzata, come descritto nei paragrafi seguenti.

Freedom II
Al fine di poter eseguire in maniera corretta l'esempio presentato in questo capitolo la
scheda di sviluppo Freedom II deve avere i Jumper posizionati nel seguente modo:

Figura 103: Impostazioni dei Jumper per il Progetto presentato nel Capitolo .

Freedom Light
Al fine di poter eseguire in maniera corretta l'esempio presentato in questo capitolo la
scheda di sviluppo Freedom Light deve avere i Jumper posizionati nel seguente modo:

Figura 104: Impostazioni dei Jumper per il Progetto presentato nel Capitolo .

Al fine di poter visualizzare correttamente i dati sul display LCD, bisogna


regolare in maniera opportuna il trimmer di contrasto presente su entrambe le
schede di sviluppo.

272

Impostazioni software
I programmi, facendo uso della libreria grafica LCD_44780.h richiede che siano
propriamente impostati i percorsi per la sua inclusione, ovvero:
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\conf
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\inc
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\src

Per semplificare la scrittura del programma, i vari esempi presentati fanno uso della
seguente funzione di inizializzazione delle varie porte di ingresso e uscita:
//*************************************
//
Implementazione della funzione
//*************************************
void board_initialization (void) {
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi, RC1 come output
LATC = 0x00;
TRISC = 0b11111101;
// Imposto PORTD tutte uscite, RD0 come ingresso
LATD = 0x00;
TRISD = 0b00000001;

// Imposto PORTE tutti ingressi


LATE = 0x00;
TRISE = 0xFF;

La funzione di inizializzazione non riportata nei vari esempi ma aprendo i progetti


allegati con il testo contenuta nel progetto stesso.

273

Descrizione dell'hardware e sue applicazioni


I display alfanumerici LCD sono ormai molto popolari e permettono con modica spesa
di aggiungere un pizzico di professionalit ad ogni circuito. Per mezzo di tali display
inoltre possibile realizzare un'ottima interfaccia macchina utente grazie ai men che
possibile scrivere direttamente sul display. In commercio sono presenti molti tipi di
display alfanumerici LCD di varie dimensioni, quelle pi note ed utilizzate sono 8x1, 8x2,
16x1, 16x2, 20x2, 40x4, dove il primo numero indica il numero dei caratteri 125 che
possibile scrivere su ogni riga, mentre il secondo rappresenta il numero di righe
disponibili. Ogni LCD possiede almeno un controllore che permette la comunicazione tra
il microcontrollore e il display LCD. Sono presenti diversi tipi di controllori con diversi
tipi set di istruzioni necessarie per la comunicazione con il controllore stesso. Il pi
utilizzato senza dubbio il controllore HD44780 della Hitachi. Sono presenti anche altre
sigle d'integrati realizzati da altre case costruttrici ma che sono compatibili con questo
controllore; infatti il protocollo di comunicazione di tale controllore divenuto uno degli
standard per il controllo dei display alfanumerici.
Il controllore HD44780, possiede una memoria interna in cui sono memorizzati i
caratteri che possono essere scritti sul display, il codice assegnato ad ogni carattere lo
stesso usato per il codice ASCII, salvo i codici speciali, dunque scrivendo un carattere o
un intero sul display questo viene interpretato secondo la tabella del codice ASCII. La
Figura 105126 mostra i caratteri standard supportati dal controllore HD44780.

Figura 105: Caratteri standard supportati dal controllore HD44780 .


125

126

Ogni carattere contenuto all'interno di una piccola matrice di punti per mezzo dei quali si ottiene la forma del carattere
stesso o anche di un simbolo.
La Figura estratta dal Datasheet Hitachi, relativo al controllore HD44780.

274

Per esempio se si volesse scrivere il simbolo , per determinare il valore numerico da


scrivere bisogna unire il codice binario presente sulla prima riga e colonna della
Figura 105, ovvero 0111 1110, ovvero il valore decimale 126. Questo carattere diverso
dal codice ASCII 126, rappresentato da ~.
Oltre al set di caratteri standard si ha anche la possibilit di creare dei caratteri personali
e scriverli all'interno della memoria del controllore. I caratteri scritti possono poi essere
richiamati per la loro visualizzazione. Per ulteriori informazioni sulle caratteristiche e
funzionalit del controllore HD44780 si rimanda al relativo Datasheet.
Ogni display LCD possiede varie linee di controllo in particolare un bus per la
trasmissione dei dati composto da 8 linee, una linea di Enable127, una linea R/W per
scrivere o leggere sul/dal controllore e una linea RS per distinguere l'invio di un comando
da un carattere. Oltre a queste linee, necessarie per il controllo del display, presente il
pin per il contrasto, ovvero per rendere pi o meno scura la scrittura sul display. Il
controllore, al fine di risparmiare pin sul PIC pu essere utilizzato in modalit 4 bit
piuttosto che a 8 bit, ovvero si pu far uso di sole 4 linee dati.
La piedinatura dei display generalmente standard ma potrebbe variare da quella sotto
riportata, in particolare i pad delle varie linee potrebbero essere localizzate in alto a
sinistra piuttosto che in basso a destra, l'ordine dei pin rimane in genere invariato.
Alla piedinatura proposta sotto fanno spesso eccezione i pin per la retroilluminazione che
si possono trovare alla destra o sinistra dei pad. In Figura 106 riportato un esempio di
Display LCD 16x2.

Figura 106: Esempio di Display LCD 16x2 con retroilluminazione

127

pin 1 = GND (il pin 1 generalmente indicato sul display stesso)


pin 2 = Vcc (+5V)
pin 3 = Contrasto
pin 4 = RS
pin 5 = R/W (collegato a massa in applicazioni in cui non si legge dal controllore)
pin 6 = E
pin 7 = DB0 (non usato in modalit 4 bit)
pin 8 = DB1 (non usato in modalit 4 bit)
pin 9 = DB2 (non usato in modalit 4 bit)
pin 10 = DB3 (non usato in modalit 4 bit)
pin 11 = DB4 (D0 in modalit 4 bit)
pin 12 = DB5 (D1 in modalit 4 bit)
pin 13 = DB6 (D2 in modalit 4 bit)

Le linee di Enable possono anche essere due ma per il 16x2, 16x1 si ha una sola linea di Enable.

275

pin 14 = DB7 (D3 in modalit 4 bit)


pin 15 = LED+
pin 16 = LED-

Il compilatore XC8 fornisce una libreria dedicata per il controllo dei display LCD ma
per ragioni di semplicit e uniformit delle librerie LaurTec, nel Capitolo far uso della
libreria che ho personalmente realizzato128 ovvero la libreria inclusa nei file:
LCD_44780.h
LCD_44780.c

La libreria LCD_44780 preimpostata per lavorare correttamente con la scheda


Freedom II, ma con pochi semplici passi, pu essere adattata per funzionare con
qualunque applicazione 4 bit129. Le linee di cui si parlato sopra possono essere infatti
collegate ad un qualunque pin del PIC lasciando ampia libert.

Utilizzo della libreria LCD


Per poter utilizzare la libreria LCD_44780 bisogna includere i file LCD_44780.h
LCD_44780.c. Dal momento che la libreria fa uso della libreria delay, anche necessario
includere il file delay.h e delay.c130.
La libreria gi ottimizzata per lavorare correttamente con le schede di sviluppo Freedom
II e Freedom Light, ciononostante pu essere facilmente adeguata ad altre esigenze,
ovvero riadattata per eventuali collegamenti del modulo LCD a pin diversi da quelli
presentati nello schema elettrico di Figura 102.
Dal momento che la libreria piuttosto flessibile, a scopo precauzionale, ho previsto la
generazione di alcune Warning nel caso si utilizzino le impostazioni standard. Se infatti si
includono solo i file:
#include "LCD_44780.h"
#include "LCD_44780.c"

Compilando i nostri programmi si avranno i seguenti messaggi di Warning:


Warning
Warning
Warning
Warning
Warning
Warning
Warning
Warning

[2105]
[2105]
[2105]
[2105]
[2105]
[2105]
[2105]
[2105]

LCD_D0 has been not defined, LATDbits.LATD4 will be used


LCD_D1 has been not defined, LATDbits.LATD5 will be used
LCD_D2 has been not defined, LATDbits.LATD6 will be used
LCD_D3 has been not defined, LATDbits.LATD7 will be used
LCD_RS has been not defined, LATDbits.LATD2 will be used
LCD_E has been not defined, LATDbits.LATD3 will be used
LCD_RW has been not defined, LATDbits.LATD1 will be used
LCD_LED has been not defined, LATCbits.LATC1 will be used

Questi messaggi stanno ad indicare che le varie costanti interne non sono state
128
129

130

Il file di libreria disponibile al sito www.LaurTec.it.


La libreria LaurTec oltre che supportare il display in formato 4 bit fornisce una libreria dedicata per il controllo di moduli
LCD facendo uso del protocollo I2C ed un I/O extender esterno.
Se non si dovesse includere la libreria delay di avrebbero degli errori di compilazione derivanti dall'impossibilit da parte del
compilatore di trovare le relative funzioni.

276

impostate e per cui saranno usate quelle di default. In particolare quelle di default
rappresentato proprio quelle per poter lavorare correttamente con Freedom II e Freedom
Light.
Come detto bene sempre compilare un programma senza alcun messaggio di
Warning, dunque la libreria prevede un modo per non far visualizzare tali messaggi. Il
modo consiste nel comunicare alla libreria l'intenzione di utilizzare i valori di default. Per
fare questo necessario definire il nome LCD_DEFAULT prima della chiamata della libreria,
ovvero:
#define LCD_DEFAULT
#include "LCD_44780.h"
#include "LCD_44780.c"

In questo modo i controlli interni della libreria sapranno che l'utente consapevole che
i valori utilizzati dalla libreria sono quelli di default.
Oltre alla libreria necessario impostare, per mezzo dei registri TRISx del PIC, i vari
pin utilizzati dalla libreria stessa in modo opportuno. In particolare i pin utilizzati dalla
libreria sono:

RD1: Deve essere impostato come Output. Linea RW.


RD2: Deve essere impostato come Output. Linea RS.
RD3: Deve essere impostato come Output. Linea E.
RD4: Deve essere impostato come Output. Linea D4.
RD5: Deve essere impostato come Output. Linea D5.
RD6: Deve essere impostato come Output. Linea D6.
RD7: Deve essere impostato come Output. Linea D7.

RC1: Deve essere impostato come Output. Controllo LED retroilluminazione.

Dunque TRISD deve essere impostato caricando il valore 0000000x dove x


indifferente e in particolare pu variare a seconda dell'applicazione. Se non usato bene
impostarlo come input ovvero ad 1. Il registro TRISC deve essere impostato con il valore
xxxxxx0x dove le x dovranno essere impostate a seconda dell'applicazione mentre il bit 1
deve valere 0. In particolare se gli altri pin non sono utilizzati bene impostarli come
input, ovvero ad 1.
Per poter propriamente utilizzare il display LCD sulle schede di sviluppo, potrebbe
essere necessario impostare propriamente dei Jumper. Maggiori dettagli sulla
configurazione sono riportati nel relativo paragrafo di inizio Capitolo.
Vediamo ora come impostare la libreria nel caso in cui la si voglia utilizzare in sistemi
differenti. All'interno del file LCD_44780 sono dichiarate le seguenti costanti:
#ifdef _PIC18
#define LCD_D0 LATDbits.LATD4
#define LCD_D1 LATDbits.LATD5
#define LCD_D2 LATDbits.LATD6

277

#define
#define
#define
#define
#define
#endif

LCD_D3 LATDbits.LATD7
LCD_RS LATDbits.LATD2
LCD_E LATDbits.LATD3
LCD_RW LATDbits.LATD1
LCD_LED LATCbits.LATC1

ed
#ifndef _PIC18
#define LCD_D0 PORTDbits.RD4
#define LCD_D1 PORTDbits.RD5
#define LCD_D2 PORTDbits.RD6
#define LCD_D3 PORTDbits.RD7
#define LCD_RS PORTDbits.RD2
#define LCD_E PORTDbits.RD3
#define LCD_RW PORTDbits.RD1
#define LCD_LED PORTCbits.RC1
#endif

I due diversi blocchi vengono a dipendere dal fatto che la libreria supporta sia i PIC18 che
i PIC16. Nel caso in cui si stia usando un PIC18, cosa che dovrebbe essere, visto il libro
che state leggendo, dovrete modificare il primo blocco di costanti. Nel caso in cui state
usando un PIC16, dovrete cambiare il secondo blocco.
Ogni costante fa riferimento ad un pin del PIC. proprio questo riferimento che deve
essere aggiornato; si noti che la modalit supportata solo quella a 4 bit. Nel caso dei
PIC18 si fa uso del registro LATD, mentre nel caso dei PIC16 si fa uso del registro
PORTD, non essendo presente il registro ausiliario LATD.
La costante LCD_D0, ovvero il bit D0, rappresentato in realt dal bit D4 del display,
infatti i bit del display D0-D3 non sono utilizzati, quelli utilizzati in modalit 4 bit sono
solo i bit D4-D7. Il bit RW non viene utilizzato dalla libreria se non per il fatto che viene
posto a 0; per tale ragione in molte applicazioni tale pin collegato a massa. L'ultimo bit
da impostare il bit LCD_LED ovvero il bit che controlla il LED di retroilluminazione.
La libreria supporta un semplice Output, ma la linea RC1 del PIC18F4550 ha l'opzione di
avere un segnale PWM in uscita, fornendo dunque la possibilit di regolare la
retroilluminazione del display stesso. Questa opzione deve per essere gestita al di fuori
della libreria stessa.
Nel caso si decidesse di modificare la libreria si consiglia di crearne una copia e
modificare la stessa, lasciando invariata quella originale, la quale potr essere
ripristinata in ogni momento.

Vediamo ora le funzioni che sono offerte dalla libreria LCD_44780.h.

278

ID

Funzione

Descrizione

LCD_initialize

Permette d'inizializzare il display LCD.

LCD_clear

Pulisce le righe del Display.

LCD_cursor

1=ON cursor 0=OFF cursor; 1=ON blinking 0=OFF Blinking.

LCD_backlight

Accende e spegne il LED di retroilluminazione.

LCD_home

Riposiziona il cursore all'inizio del display.

LCD_goto_line

Posiziona il cursore ad una determinata riga .

LCD_goto_xy

Posiziona il cursore ad una determinata riga e colonna.

LCD_shift

Trasla le righe a destra o sinistra per un numero di volte variabile.

LCD_shift_cursor

Sposta il cursore a destra o sinistra per un numero di volte variabile.

10

LCD_write_char

Scrive un carattere sul display.

11

LCD_write_string

Scrive una variabile sul display.

12

LCD_write_integer

Scrive un intero sul display, convertendo l'intero in stringa.

13

LCD_write_message

Scrive una stringa costante sul display.

Tabella 12: Funzioni disponibili nella libreria LCD_44780.

La libreria supporta, per ogni funzione, il nome LCD sia come all'inizio che alla
fine del nome stesso. Per esempio LCD_initialize e initialize_LCD. In
particolare il primo formato torna utile quando non si ricorda il nome della
funzione, basta infatti scrivere LCD e premere Control + Space per avere i
suggerimenti di ogni funzione inclusa nella libreria.
Per facilitare l'utilizzo della libreria sono definite le seguenti costanti:
#define LCD_LEFT 0
#define LCD_RIGHT 1
#define LCD_TURN_ON_LED 1
#define LCD_TURN_OFF_LED 0
#define LCD_TURN_ON_CURSOR 1
#define LCD_TURN_OFF_CURSOR 0
#define LCD_BLINKING_ON 1
#define LCD_BLINKING_OFF 0

Per la documentazione completa delle funzioni, far riferimento alla


documentazione ufficiale. Variazioni della libreria non sono riportate su questo
testo per cui sempre bene far riferimento alla documentazione contenuta
nell'header file o quella creata con Doxygen. Le funzioni disponibili potrebbero
essere maggiori di quelle riportate in Tabella 12.

279

Leggiamo finalmente Hello World


Vediamo come utilizzare il display in un esempio, visto che quanto detto potrebbe
sembrare pi complicato di quanto in realt non sia. Il nome delle funzioni scelte
permette di leggere il codice come se fosse commentato, mostrando come la scelta del
nome delle funzioni risulti un ottimo aiuto per rendere il codice facilmente leggibile.
Iniziamo con un primo esempio in cui risalutiamo il mondo...questa volta lo leggendolo!
#include <xc.h>
#include "PIC18F4550_config.h"
#define LCD_DEFAULT
#include "LCD_44780.h"
#include "LCD_44780.c"
#include "delay.h"
#include "delay.c"
//*************************************
//
Costanti del programma
//*************************************
#define FRASE_1 "Hello World 1"
//*************************************
//
Prototipo di funzione
//*************************************
void board_initialization (void);
//*************************************
//
Programma principale
//*************************************
int main (void){
board_initialization ();
// Inizializzo il display LCD con quarzo a 20MHz
LCD_initialize (20);
LCD_backlight (TURN_ON_LED_LCD);
LCD_write_message (FRASE_1);
LCD_goto_line (2);
LCD_write_message ("Hello World 2");
// Ciclo infinito
while (1) {
}

Si osservi che la libreria stata inclusa definendo il nome


eliminare i messaggi di Warning.

LCD_DEFAULT,

in modo da

280

#define LCD_DEFAULT
#include "LCD_44780.h"
#include "LCD_44780.c"

Tra le impostazioni dei pin non c' nulla di nuovo, ma si osservino le impostazioni dei
pin della PORTD e PORTC contenuti della funzione di inizializzazione
board_initialization.
// Imposto PORTC tutti ingressi, RC1 come output
LATC = 0x00;
TRISC = 0b11111101;
// Imposto PORTD tutte uscite, RD0 come ingresso
LATD = 0x00;
TRISD = 0b00000001;

Una volta inizializzati i pin possibile inizializzare il nostro display LCD e scrivere il
messaggio Hello World 1 sulla prima riga.
LCD_initialize (20);
LCD_backlight (TURN_ON_LED_LCD);
LCD_write_message (FRASE_1);

Il valore 20 usato nella funzione di inizializzazione rappresenta il valore della frequenza


del clock utilizzato, ovvero di 20MHz. Qualora si dovesse utilizzare il PLL interno
bisogna usare il valore della frequenza in uscita al PLL diviso il valore del Postscaler
impostato per mezzo dei bit CPUDIV.
Il messaggio visualizzato sulla prima riga definito per mezzo della costante FRASE_1:
#define FRASE_1 "Hello World 1"

Questo viene fatto spesso, in maniera da avere le frasi scritte in un unico posto. Non
inusuale che sia creato anche un file .h separato solo per la definizione delle frasi o
messaggi.
Dopo aver scritto il primo messaggio sulla prima riga si pu scrivere sulla seconda,
semplicemente con l'aggiunta delle due righe:
LCD_goto_line (2);
LCD_write_message ("Hello World 2");

la funzione:
LCD_goto_line (2);

qualora si stia usando un display con 4 righe, permette anche di posizionare il cursore
sulla riga 3 e 4.

281

Se i messaggi non dovessero essere visualizzati bene controllare il contrasto in


maniera opportuna e controllare che il display LCD sia propriamente abilitato.
Un display che non viene propriamente inizializzato mantiene la prima riga nera.

Il display LCD permette di scrivere frasi sul display, ma come visto nell'esempio
precedente il modo con cui si pu usare la funzione write_message_LCD pu essere
leggermente differente in base a come viene definito il messaggio stesso. Infatti si pu
scrivere sia:
LCD_write_message (FRASE_1);

che
LCD_write_message ("Hello World 2");

Sebbene apparentemente differenti, il messaggio in realt un valore costante noto al


tempo di compilazione e salvato in memoria flash. La cosa diversa qualora il messaggio
da visualizzare sia salvato in RAM. In questo secondo caso la funzione
write_message_LCD non pi idonea e bisogna usare la funzione write_string_LCD.
Vediamo un esempio:
#include <xc.h>
#include "PIC18F4550_config.h"
#define LCD_DEFAULT
#include "LCD_44780.h"
#include "LCD_44780.c"
#include "delay.h"
#include "delay.c"
//*************************************
//
Prototipo di funzione
//*************************************
void board_initialization (void);
//*************************************
//
Programma principale
//*************************************
int main (void){
unsigned char frase [ ] = "Hello World 2";
board_initialization ();
// Inizializzo il display LCD con quarzo a 20MHz
LCD_initialize (20);
LCD_backlight (TURN_ON_LED_LCD);
LCD_write_message ("Hello World 1");

282

LCD_goto_line (2);
LCD_write_string (frase);
// Ciclo infinito
while (1) {
}
}

Un messaggio scritto in RAM rappresenta pi propriamente una stringa ovvero un Array


di caratteri, per cui deve essere definita come:
unsigned char frase [ ] = "Hello World 2";

Si ricorda che non scrivendo la dimensione dell'Array il compilatore crea un Array di


dimensione opportuna ovvero pari al numero di caratteri pi uno, al fine di contenere il
carattere speciale di fine stringa. Una volta definita la stringa possibile visualizzarla nel
seguente modo:
LCD_write_string (frase);

Detto questo vi potreste chiedere quando usare un messaggio costante o un stringa. La


risposta viene in generale a dipendere dalle specifiche, ma le stringhe costanti sono usate
nel caso in cui non si abbia la necessit di cambiarle dinamicamente mentre le stringhe
agevolano le applicazioni in cui si necessiti maggiore flessibilit. Tipici esempi potrebbero
essere il caso in cui la stringa contenga il valore di una misura o si stia scrivendo un menu
multilingua.
Vediamo un ultimo esempio in cui si fa uso di una struttura per memorizzare il nome e il
cognome di una persona e si effettua una piccola manipolazione di Array.
#include <xc.h>
#include "PIC18F4550_config.h"
#define LCD_DEFAULT
#include "LCD_44780.h"
#include "LCD_44780.c"
#include "delay.h"
#include "delay.c"
//*************************************
//
Prototipi di funzione
//*************************************
void board_initialization (void);
void copy (unsigned char * dest, const unsigned char * parola);
//*************************************
//
Variabile personale
//*************************************

283

typedef struct {
unsigned char nome [20];
unsigned char cognome [20];
} persona;
int main (void){
// Variabile tizio di tipo persona
persona tizio;
board_initialization();
// Inizializzo il display LCD con quarzo a 20MHz
LCD_initialize (20);
LCD_backlight (TURN_ON_LED_LCD);
// Scrivo i dati nella variabile tizio
copy (tizio.nome, "Mauro");
copy (tizio.cognome, "Laurenti");
LCD_write_string (tizio.nome);
LCD_goto_line (2);
LCD_write_string (tizio.cognome);
while (1) {
}

//*************************************
//
Implementazione della funzione
//*************************************
void copy (unsigned char * dest, const unsigned char * parola) {
while(*parola) {
// Copio il carattere dentro l'array
*dest = *parola;

// Incremento il puntatore
parola++;
dest++;

// Inserisco il carattere di fine stringa


*dest = '\0';

L'inizio del programma simile al primo esempio, nulla di nuovo. In particolare oltre
alle inclusioni dei file gi visti si dichiara il prototipo della funzione copy e la struttura dati
persona. Riguardo alla funzione copy basta sapere che viene utilizzata per copiare una
stringa costante, tipo Ciao Mamma all'interno del nostro Array di caratteri.
typedef struct {

284

unsigned char nome [20];


unsigned char cognome [20];
} persona;

All'inizio del
tizio :

main

viene utilizzata la struttura

persona

per dichiarare una variabile

// Variabile tizio di tipo persona


persona tizio;

Il programma diviene poi come il precedente, ovvero si inizializzano i vari pin del PIC
in modo da poter utilizzare propriamente la libreria e si inizializza il display LCD.
Successivamente viene richiamata la funzione copy, utilizzata per copiare una stringa
all'interno della nostra struttura.
// Scrivo i dati nella variabile tizio
copy (tizio.nome, "Mauro");
copy (tizio.cognome, "Laurenti");

La ragione per cui deve essere richiamata la funzione legata al fatto che il C, non
supportando in maniera nativa le stringhe, non permette istruzioni del tipo:
nome = "Piero"

Per poter scrivere un nome o una qualunque frase all'interno di un Array necessario
scrivere elemento per elemento, ovvero carattere per carattere, il nome o frase. Per
agevolare il programmatore la Microchip fornisce la libreria standard C string.h, questa
contiene varie funzioni ad hoc per le stringhe. In questo programma di esempio ho
preferito scrivere la funzione copy piuttosto che usare la libreria string.h in modo da
comprendere come poter manipolare un Array di caratteri.
Come detto la variabile tizio possiede i due campi nome e cognome, quindi possibile
accedere i singoli caratteri di questi due campi per mezzo di questa sintassi:
a = tizio.nome[2];
b = tizio.cognome[3];

Questo permette di copiare nelle variabili a e b rispettivamente il terzo e il quarto


carattere dei due campi nome e cognome (si ricordi che il primo carattere di un Array
alla posizione 0), a e b devono essere due variabili dichiarate come caratteri:
unsigned char a;
unsigned char b;

Oltre a quanto scritto in precedenza possibile anche scrivere:


d = tizio.nome;

Questa volta d non deve essere semplicemente un carattere! Infatti con questa sintassi
senza parentesi quadre, come spiegato in precedenza, si intende l'indirizzo di memoria
285

dove inizia il nostro Array 131 tizio.nome, ovvero abbiamo a che fare con un puntatore.
Per cui la variabile d deve essere definita come:
unsigned char * d;

Dopo questa breve spiegazione ritorniamo al nostro programma, in particolare


cerchiamo di capire come funziona la nostra funzione copy.
Per far funzionare la nostra funzione necessario indicare la posizione del nostro
Array, dunque necessario passare il suo indirizzo semplicemente scrivendo tizio.nome
o tizio.cognome. Si ricorda che il passaggio dell'indirizzo di una variabile noto come
passaggio di parametri per riferimento. Come secondo campo necessario passare la
nostra stringa costante che contiene il nostro nome o il nostro cognome. Rivediamo la
dichiarazione del prototipo della funzione copy:
void copy (unsigned char * dest, const unsigned char * parola);

possibile notare che la prima variabile della funzione copy rappresenta proprio un

puntatore ad un Array, questo se si seguito il ragionamento precedente spero non


sorprenda. La seconda variabile ancora un puntatore a caratteri, ma di tipo costante,
ovvero memorizzati nella memoria programma (Flash). Si capisce che se la funzione
avesse dovuto copiare un Array in un altro Array anche la seconda variabile sarebbe stata
un puntatore a unsigned char. Per supportare entrambi i casi sarebbe necessario scrivere
una seconda funzione.
Qualora si dovesse provare a dichiarare il secondo parametro come semplice puntatore a
caratteri, ovvero senza const, si avrebbe un messaggio di Warning:
warning: (359) illegal conversion between pointer types

La funzione copy la seguente:


void copy (unsigned char * dest, const unsigned char * parola) {
while(*parola) {
// Copio il carattere dentro l'array
*dest = *parola;

// Incremento il puntatore
parola++;
dest++;

// Inserisco il carattere di fine stringa


*dest = '\0';

Al suo interno viene effettuato un ciclo while che termina quando il valore puntato dal
131

I questo caso si ha una struttura, ma la cosa sarebbe stata equivalente con un semplice Array di caratteri chiamato nome
piuttosto che un Array nome interno alla struttura tizio.

286

puntatore parola vale '\0', ovvero alla fine della stringa. Fino a che tale valore diverso
da tale carattere vengono eseguite le istruzioni interne al ciclo.
La prima istruzione nel ciclo copia il carattere puntato da parola nell'indirizzo puntato
da dest, ovvero si copia un elemento dall'origine alla destinazione. L'istruzione successiva
incrementa l'indirizzo contenuto nella variabile parola in modo da puntare il carattere
successivo della parola d'origine. Allo stesso modo viene incrementato l'indirizzo
contenuto nella variabile dest in modo da poter copiare il nuovo carattere nella cella
successiva, il ciclo si ripete fino a che la parola non termina.
Usciti dal ciclo, all'ultimo elemento puntato da dest, si scrive il valore '\0', infatti tale
valore non viene trasferito poich il ciclo while termina quando questo viene trovato
all'interno della parola, come detto fondamentale che questo valore sia mantenuto.
In ultimo, una volta che il nome e cognome sono caricati all'interno della nostra
struttura, si possono scrivere semplicemente facendo uso della funzione di libreria
LCD_write_string, al quale viene passato il puntatore dell'Array:
LCD_write_string (tizio.cognome);

Progetto 2 Creare un menu multilingua


Specifiche del progetto
Realizzare un menu multilingua che supporti l'italiano, inglese e il tedesco. Il menu
deve consistere di due voci:

Luce
Lingua

Luce pu assumere il valore accesa e spenta, mentre il valore di Lingua assume il nome
delle tre lingue. I tre menu nelle tre diverse lingue sono:
Italiano

Luce: [accesa] [spenta]


Lingua: [italiana]

Inglese

Light: [ON] [OFF]


Language: English

Tedesco

Licht: [Ein] [Aus]


Sprache: Deutsch

Il passaggio da un menu ad un altro deve avvenire attraverso la pressione del tasto BT1
mentre il cambio del parametro deve avvenire per mezzo del tasto BT2.
287

La voce del menu selezionato deve essere selezionato da una freccia .

Sviluppo del progetto


#include <xc.h>
#include "PIC18F4550_config.h"
#define LCD_DEFAULT
#include "LCD_44780.h"
#include "LCD_44780.c"
#include "delay.h"
#include "delay.c"
//*************************************
//
Definizioni Lingue
//*************************************
const unsigned char *italian[]= {"Lingua
: ","IT","Luce : ", "Accesa",
"Spenta"};
const unsigned char *english[] = {"Language: ","EN ","Light: ", "ON
",
"OFF
"};
const unsigned char *german[] = {"Sprache : ","DE ","Licht: ", "Ein
",
"Aus
"};
#define
#define
#define
#define
#define

MESSAGE_0
MESSAGE_1
MESSAGE_2
MESSAGE_3
MESSAGE_4

0x00
0x01
0x02
0x03
0x04

const unsigned char **language;


//*************************************
//
Definizioni stati
//*************************************
#define STATE_LIGHT_OFF 0x00
#define STATE_LIGHT_ON
0x01
#define STATO_LUCE_MAX 0x02
#define
#define
#define
#define

STATE_LANGUAGE_ITALIANO
STATE_LANGUAGE_ENGLISH
STATE_LANGUAGE_DEUTSCH
STATO_LANGUAGE_MAX

#define STATE_MENU_1
#define STATE_MENU_2
#define STATE_MENU_MAX

0x00
0x01
0x02
0x03

0x01
0x02
0x03

unsigned char state_light = STATE_LIGHT_OFF;


unsigned char state_language = STATE_LANGUAGE_ITALIANO;
unsigned char state_menu = STATE_MENU_1;
//*************************************
//
Definizioni costanti
//*************************************

288

#define ACTIVATED
#define DEACTIVATED

0x01
0x00

#define PRESSED

0x00

#define SPIKES_DELAY 10000


//*************************************
//
Prototipo di funzione
//*************************************
void board_initialization (void);
//*************************************
// Funzione per la gestione dell'interruzione
//*************************************
__interrupt (high_priority) void ISR_alta (void) {
// Indice per il ciclo di pausa
unsigned int i;
// Controllo che l'interrupt sia stato generato da PORTB
if (INTCONbits.RBIF == ACTIVATED ) {
//pausa filtraggio spikes
for (i=0; i<SPIKES_DELAY; i++){
}
// Controllo la pressione di BT1
if (PORTBbits.RB4 == PRESSED) {
state_menu++;

if (state_menu == STATE_MENU_MAX)
state_menu = STATE_MENU_1;

// Controllo la pressione di BT2


if (PORTBbits.RB5 == PRESSED) {
if (state_menu == STATE_MENU_2){
state_light++;
if (state_light == STATO_LUCE_MAX)
state_light = STATE_LIGHT_OFF;
}
if (state_menu == STATE_MENU_1){
state_language++;

if (state_language == STATO_LANGUAGE_MAX)
state_language = STATE_LANGUAGE_ITALIANO;

}
// Resetto il flag d'interrupt per permettere nuove interruzioni
INTCONbits.RBIF = DEACTIVATED;
}

289

//*************************************
//
Programma principale
//*************************************
int main (void){
board_initialization ();
// Inizializzo il display LCD con quarzo a 20MHz
LCD_initialize (20);
// Ciclo infinito
while (1) {
//*************************************
//
Controllo Lingua
//*************************************
if (state_language == STATE_LANGUAGE_ITALIANO) {
language = ( unsigned char **) &italian;
}
if (state_language == STATE_LANGUAGE_ENGLISH) {
language = ( unsigned char **) &english;
}
if (state_language == STATE_LANGUAGE_DEUTSCH) {
language = ( unsigned char **) &german;
}
LCD_goto_xy (2,1);
LCD_write_string ((unsigned char *) language[MESSAGE_0]);
LCD_write_string ((unsigned char *) language[MESSAGE_1]);
//*************************************
//
Controllo Cursore
//*************************************
if (state_menu == STATE_MENU_1){
LCD_goto_xy (1,1);
LCD_write_char (126);
LCD_goto_xy (1,2);
LCD_write_char (' ');
} else {
LCD_goto_xy (1,1);
LCD_write_char (' ');
LCD_goto_xy (1,2);
LCD_write_char (126);
}
//*************************************
//
Controllo Luce
//*************************************
LCD_goto_xy (2,2);
LCD_write_string ((unsigned char *) language[MESSAGE_2]);
if (state_light == STATE_LIGHT_ON) {
LCD_backlight (LCD_TURN_ON_LED);
LCD_write_string ((unsigned char *) language[MESSAGE_3]);
} else {

290

}
}

LCD_backlight (LCD_TURN_OFF_LED);
LCD_write_string ((unsigned char *) language[MESSAGE_4]);

//*************************************
//
Implementazione della funzione
//*************************************
void board_initialization (void) {
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi, RC1 come output
LATC = 0x00;
TRISC = 0b11111101;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// Abilita i resistori di pull-up sulla PORTB
INTCON2bits.RBPU = 0x00;
// Abilito le interruzioni su PORTB
INTCONbits.RBIE = 1;
// Abilito modalit compatibile (di default vale gi 0)
RCONbits.IPEN = 0;
// Resetto il flag d'interrupt prima di attivare le interruzioni
INTCONbits.RBIF = 0;
// Abilito l'interrupt globale
INTCONbits.GIE = 1;
// Abilito l'interrupt periferiche
INTCONbits.PEIE = 1 ;
}

Analisi del progetto


Il progetto presentato, diversamente dagli altri, certamente pi organico, e per la
prima volta cerca di utilizzare quante pi regole di buona programmazione possibile,
anche se qualcuna, per ragioni di semplicit la si ancora evitata. Interessante vedere la
definizione delle varie lingue:

291

const unsigned char *italian[]= {"Lingua


: ","IT","Luce : ", "Accesa",
"Spenta"};
const unsigned char *english[] = {"Language: ","EN ","Light: ", "ON
",
"OFF
"};
const unsigned char *german[] = {"Sprache : ","DE ","Licht: ", "Ein
",
"Aus
"};

bene notare che l'Array multidimensionale, infatti ogni lingua rappresenta un


puntatore ad Array. La posizione di ogni parola deve essere la stessa in ogni Array. Nel
caso specifico, al fine di semplificare la pulizia dello schermo, si anche provveduto a fare
in modo che ogni parola tradotta avesse la stessa lunghezza, ovvero si sono posti spazi
vuoti alle parole pi corte.
La gestione del menu implementata per mezzo di tre variabili di stato, che sono
propriamente incrementate durante l'ISR.
unsigned char state_light = STATE_LIGHT_OFF;
unsigned char state_language = STATE_LANGUAGE_ITALIANO;
unsigned char state_menu = STATE_MENU_1;

bene vedere i vari dettagli del programma al fine di comprendere il tutto e fare
propri alcuni aspetti di programmazione. Si consiglia di simulare anche il programma o
parte di esso, per la verifica e studio dell'Array multidimensionale che contiene le stringhe
del menu. In particolare per la gestione degli Array, si fatto anche uso di un puntatore a
puntatore.

Riassunto
In questo Capitolo abbiamo visto come utilizzare il modulo LCD basato su controllore
HD44780. Grazie all'utilizzo della libreria LaurTec i vari programmi di esempio sono
piuttosto compatti e chiari. Il progetto finale ha messo in evidenza come un programma
possa diventare pi organico anche se apparentemente le funzioni svolte non sono molte.
Il progetto mostrato mette anche in evidenza l'utilizzo dei puntatori in applicazioni pi
complesse e come il loro utilizzo permetta di cambiare facilmente lingua semplicemente
cambiando l'indirizzo dell'Array indirizzato.

Punti chiave ed approfondimenti


LCD: Liquid Crystal Display ovvero Display a Cristalli Liquidi. Rappresenta una tecnologia
per la realizzazione di display che sfrutta la polarizzazione dei cristalli al fine di creare filtri
della luce, creando, nel caso dei semplici display bianco e nero, zone buie o trasparenti alla
luce.
HDC44780: Il chip HD44780 un controllore per display a cristalli Liquidi, progettato
dalla Hitachi. Il suo set di comandi divenuto uno standard di fatto, portando altri
produttori alla realizzazione di controllori con nome diverso ma compatibili con lo stesso.

292

Domande ed Esercizi
1.
2.
3.
4.
5.
6.

7.

8.

Che cos' modulo LCD?


Di cosa si occupa il controllore HD44780?
Com' possibile scrivere caratteri personali sul display LCD basato su controllore
HD44780?
Quali possono essere le cause della mancata visualizzazione di una scritta sul display?
Cambiare il Progetto presentato eliminando il ritardo nella ISR, per la gestione dei pulsanti.
Scrivere un programma che visualizzi sul display il pulsante premuto. Se per esempio viene
premuto il pulsante BT1 deve essere visualizzato BT1 Premuto, mentre rilasciando il
pulsante sul display deve comparire la scritta Nessun pulsante premuto, avendo
l'accortezza di usare due linee.
Riscrivere l'esempio precedente aggiungendo il caso di pressione multipla di pulsanti, nel
cui caso si deve scrivere sul display: Pi pulsanti sono stati premuti, facendo scorrere il
messaggio sul display.
Realizzare una funzione che permetta di visualizzare un numero reale sul display LCD e
scrivere un semplice progetto che ne faccia uso.

293

Capitolo X
Utilizziamo un Display Grafico LCD
In questo Capitolo si introduce nuovamente una periferica esterna al PIC. Il suo
utilizzo particolarmente frequente, estendendo le possibilit di un Display Alfanumerico,
ovvero il Display Grafico. Il Capitolo prende come esempio i Display Grafici basati su
controllore KS0108B ed una risoluzione 128x64. Sebbene sia di tipo bianco e nero e la
risoluzione non paragonabile con i classici cellulari che si soliti avere in tasca, le
applicazioni e tipologia di dati che possibile visualizzare, fornisce un ulteriore strumento
professionale alle nostre applicazioni.

Schema elettrico
Per poter seguire gli esempi presentati in questo Capitolo, richiesto realizzare su
Breadboard lo schema elettrico di Figura 107 o far uso delle schede di sviluppo impostate
come descritto nel paragrafo successivo. Si noti che il Display Grafico non presente
sulle schede di sviluppo presentate, per cui il montaggio del Display GLCD deve essere
esterno.

Figura 107: Schema elettrico di esempio del Capitolo X.

294

Impostare le schede di sviluppo


Al fine di poter seguire gli esempi mostrati in questo Capitolo necessario impostare la
scheda di sviluppo utilizzata, come descritto nei paragrafi seguenti. Oltre alle impostazioni
mostrate si consiglia inoltre di scollegare il display LCD montato sulla scheda.

Freedom II
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom II deve avere i Jumper posizionati nel seguente modo:

Figura 108: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

Freedom Light
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom Light deve avere i Jumper posizionati nel seguente modo:

Figura 109: Impostazioni dei Jumper per i Progetti presentati nel Capitolo .

Visti i consumi della retroilluminazione si consiglia di collegare la stessa ad


un'alimentazione separata. In particolare consigliabile ricavare +5V
dall'alimentazione Vin, facendo uso di un secondo regolatore lineare, evitando in
questo modo di assorbire corrente eccessiva da quello montato sulla scheda di
sviluppo.

296

Impostazioni software
Gli esempi mostrati non richiedono alcuna impostazione software particolare oltre alla
semplice creazione di un progetto.
Il programma facendo uso della libreria grafica GLCD_KS0108B.h richiede che siano
propriamente impostati i percorsi per la sua inclusione, ovvero:
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\conf
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\inc
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\src

Per semplificare la scrittura del programma, i vari esempi presentati, fanno uso della
seguente funzione di inizializzazione delle varie porte di ingresso e uscita:
//*************************************
//
Implementazione della funzione
//*************************************
void board_initialization (void) {
// Imposto PORTA
LATA = 0x00;
TRISA = 0x07;
// Set Analog Input come I/O
ADCON1 = 0x0F;
// Imposto PORTB
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC
LATC = 0x00;
TRISC = 0xFD;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;

// Imposto PORTE RE0, RE1, RE2 Output


LATE = 0x00;
TRISE = 0xF8;

La funzione di inizializzazione non riportata nei vari esempi ma aprendo i progetti


allegati con il testo contenuta nel progetto stesso.

297

Descrizione dell'hardware e sue applicazioni


Sul mercato sono disponibili numerose tipologie di Display Grafici. Tra i pi noti si
ricordano quelli basati sul controllore KS0108B. Il controllore grafico KS0108B, prodotto
dalla Samsung, sebbene relativamente anziano, viene ancora frequentemente utilizzato
grazie alla sua reperibilit, facilit d'uso e costi ridotti. Il controllore possiede tutta
l'elettronica per potersi interfacciare con un microcontrollore e poter decodificare i
comandi necessari per poter controllare un display GLCD monocromatico. Grazie alla
retroilluminazione tipicamente bianca, verde o blu si possono avere colori differenti, ma
pur sempre bicromatici. Il controllore sebbene decodifichi i comandi per il controllo del
display e rappresenti l'interfaccia del display grafico stesso, al fine di controllare
effettivamente il Display a cristalli liquidi, necessario abbinarlo al controllore KS0107B.
Il controllore KS0108B possiede al suo interno 512 Byte di memoria RAM ovvero
4096 bit. Spesso i display GLCD che si trovano in commercio hanno una risoluzione
128x64 pixel ovvero con una risoluzione sull'asse delle X pari a 128 punti mentre sull'asse
delle Y hanno una risoluzione di 64 punti. Moltiplicando semplicemente la risoluzione X
ed Y si capisce che un display grafico 128x64 possiede 8192 punti. Dal momento che il
controllore KS0108B possiede una memoria RAM interna di 4096 bit, per poter
controllare un display GLCD con risoluzione 128x64 necessario usare due controllori
KS0108B, ai quali necessario abbinare un controllore KS0107B. Uno schema a blocchi
di un modulo GLCD riportato in Figura 110132.

Figura 110: Schema a blocchi di un display GLCD 128x64 basato su controllore KS0108B.

Sul mercato possibile trovare GLCD con risoluzione maggiore ottenuti con tre
controllori KS0108B. Da quanto detto si capisce che pilotare un Display Grafico basato
su controllore GLCD KS0108B consiste nel pilotare due controllori. Il Display Grafico
suddiviso in due parti come riportato in Figura 111. La parte I controllata dal primo
controllore mentre la parte II controllata dal secondo controllore. Da questa prima
rappresentazione si capisce anche che se si volesse disegnare un punto con coordinata X
pari a 80, bisogna scrivere nel secondo controllore con coordinata X pari a 15. Di questo
132

Immagine prelevata dal Datasheet del Controllore KS0108B.

298

non ci si dovr preoccupare troppo perch la libreria utilizzata, tratter con questi dettagli
mentre l'utilizzatore pu utilizzare funzioni semplici.

Figura 111: Divisione del display grafico 128x64 .

Sebbene la Figura 111 mostri che la risoluzione di ogni settore di 64x64 pixel, per
accedere ad ogni punto non basta semplicemente inviare dei comandi per un punto di
coordinate x,y compreso tra 0 e 63. Il controllore KS0108B divide infatti ogni settore in 8
pagine, come riportato in Figura 112. Ogni pagina possiede a sua volta una risoluzione
64x8 e pu essere scritta solo 8 punti alla volta (verticalmente).

Figura 112: Divisione dei settori in Pagine.

Questa divisione, che si ritrova anche in altri controllori, discende dal fatto che per
ogni pixel non associato un byte ma un solo bit, per cui un byte controlla 8 bit. Per tale
ragione 8 punti il numero minimo di pixel che possibile scrivere. Per poter disegnare
un pixel necessario dunque impostare, il controllore (settore), la pagina e l'indirizzo
(nominato Y address). Dal momento che possibile scrivere solo 8 punti alla volta, si
capisce che, qualora si voglia modificare un solo pixel, necessario prima leggere il valore
del byte in cui raggruppato il nostro pixel e solo successivamente scrivere il valore del
nuovo pixel senza alterare il valore degli altri.
Sebbene per ogni pixel si debba posizionare controllore, pagina e indirizzo, qualora si
debbano scrivere pi punti consecutivi si pu sfruttare il fatto che l'indirizzo interno al
controllore viene automaticamente incrementato ad ogni scrittura. In questo modo fin
tanto che non si debba cambiare controllore o pagina, si pu scrivere l'indirizzo iniziale e
procedere da sinistra verso destra alla scrittura degli altri bit, evitando di dover effettuare
299

una scrittura d'indirizzo per ogni pixel.


Si noti che l'asse delle X (asse orizzontale) viene puntato da Y address del
controllore, la ragione semplicemente legata al fatto che il Display dovrebbe
essere visto posizionato in verticale, per cui la risoluzione la si dovrebbe leggere
come 64x128 e non 128x64.
Ogni controllore KS0108B si interfaccia con un microcontrollore con le seguenti linee
di controllo:

DB0-DB7 : Data bus per leggere e scrivere dati.

E : Linea di Enable, usata per scandire le fasi di lettura e scrittura.

R/W' : Linea Read/Write, utilizzata per selezionare la fase di lettura, se impostata


ad 1, o la fase di scrittura se impostata a 0.

D/I' : Linea Dati/Istruzione, utilizzata per specificare se l'operazione sul Bus


DB0-DB7 rappresenta un movimenti Dati o Istruzione (Dati se D/I' pari ad 1,
mentre Istruzione se D/I' pari a 0).

RST' : Linea di Reset. Se impostata a 0 pone in stato di Reset i controllori.

CS1, CS2 : Tali linee rappresentano i Chip Select dei due controllori. In particolare
le linee precedentemente spiegate sono usate dall'uno o l'altro controllore a
seconda del Chip Select abilitato.
Si fa notare che la nomenclatura utilizzata in questa descrizione pu variare
leggermente a seconda del Datasheet utilizzato ma dalla descrizione della
funzione del pin stesso si pu facilmente individuare la corrispondenza con
quelli appena descritti.

300

La sequenza di scrittura dati descritta in Figura 113133.

Figura 113: Sequenza di scrittura in un controllore KS0108B.

mentre la sequenza di lettura riportata in Figura 114134.

Figura 114: Sequenza di lettura in un controllore KS0108B.

I vari tempi associati alle sequenze scrittura/lettura, sono riportati in Tabella 13.

133
134

Immagine prelevata dal Datasheet del Controllore KS0108B.


Immagine prelevata dal Datasheet del Controllore KS0108B.

301

Parametro

Simbolo

Min.

Tipico

Max.

Unit

Ciclo E (Enable)

tCYC

1000

--

--

ns

E livello alto

PWEH

450

--

--

ns

E livello basso

PWEL

450

--

--

ns

E tempo di salita (rise time)

tr

--

--

25

ns

E tempo di discesa (fall time)

tf

--

--

25

ns

Setup Time Indirizzo

tAS

140

--

--

ns

Hold Time Indirizzo

tAH

10

--

--

ns

Data Setup Time

tDSW

200

--

--

ns

Data Delay Time

tDDR

--

--

320

ns

Data Hold Time (Write)

tDHW

10

--

--

ns

Data Hold Time (Read)

tDHR

20

--

--

ns

Tabella 13: Tempi associati alle sequenze scrittura/lettura su un controllore KS0108B.

Qualora i tempi di scrittura lettura non sono rispettati possibile che i comandi non
vengano propriamente riconosciuti, per cui l'inizializzazione o la rappresentazione di
oggetti sullo schermo avviene in maniera disordinata (punti mancanti o punti in posizioni
errate). Una mancata inizializzazione possibile verificarla osservando un effetto neve
sullo schermo, ovvero i pixel dello schermo hanno un valore casuale che riflette il
contenuto random della memoria RAM interna al controllore (alcune volte possibile
vedere una correlazione tra i pixel accesi). La sequenza d'inizializzazione del Display,
ovvero dei controllori, tra le varie operazioni compiute, ha il compito di porre a 0x00 o
0xFF il contenuto della memoria RAM, il che equivale a pulire lo schermo.
Al fine di poter utilizzare propriamente il Display sono presenti altre linee/pin, in
particolare per il LED di retroilluminazione, pin per il contrasto e pin di alimentazione. La
piedinatura tipica per un Display GLCD basato su controllore KS0108B riportata in
Tabella 2 (normalmente il pin 1 riportato sulla serigrafia del PCB sul quale montato il
display):
Per il controllo della griglia per polarizzare i cristalli interni necessaria una
tensione negativa. Alcuni Display possiedono il convertitore DC-DC o charge
pump, direttamente sul modulo, richiedendo per cui un solo trimmer esterno.
Alcuni Modelli di display richiedono invece una tensione negativa esterna, la
quale deve essere opportunamente ricavata dai 5V (normalmente disponibili).

302

PIN

Simbolo

Livello

Funzione

VSS

--

Ground

VDD

--

Power Supply (+5V 5%)

VO

--

Tensione per pilotaggio LCD

D/I

H/L

H: Data Input
L: Istruzione

R/W

H/L

H: Data Read
L: Data Write

H, H -> L

7-14

DB0-DB7

H/L

Enable

15

CS1

Chip Select 1

16

CS2

Chip Select 2

17

RST

Reset

18

VOUT

--

Tensione uscita per pilotaggio LCD

19

LED+

--

LED retroilluminazione

20

LED-

--

LED retroilluminazione

Data Bus

Tabella 14: Piedinatura tipica per un GLCD basato su controllore KS0108B.

La piedinatura riportata fa riferimento al display LCD 64128A LED della


ELECTRONIC DATA TECH. La piedinatura potrebbe variare a seconda del
modello utilizzato, per cui fare sempre attenzione al Datasheet del display che si
sta utilizzando.

Utilizzo della libreria GLCD


La libreria grafica descritta in questo Capitolo stata scritta per PIC18 ed testata su
PIC18F4550 con quarzo 20MHz. pensata per poter essere utilizzata senza dover tenere
a mente il funzionamento del controllore KS0108B e poter quindi mantenere l'attenzione
sugli aspetti principali dell'applicazione.
Come ogni libreria descritta fino ad ora, composta di un file header in cui sono
dichiarate diverse costanti e i prototipi delle funzioni. L'implementazione delle funzioni
riportata nel file sorgente .c. Entrambi i file devono essere inclusi al fine di poter
propriamente compilare un progetto. I percorsi in cui sono contenuti i file devono essere
aggiunti ai percorsi di sistema utilizzati dal compilatore. In particolare i file da includere
sono:
#include "GLCD_KS0108B.h"
#include "GLCD_KS0108B.c"

Nel file header sono presenti le informazioni relative al collegamento del modulo
GLCD. Sebbene sia possibile variare a proprio piacimento i collegamenti rispetto allo
schema elettrico presentato in Figura 107 necessario mantenere il Data bus su un'unica
303

porta. In particole, come mostrato dalle seguenti dichiarazioni, necessario anche


specificare il nome degli altri registri associati alla porta stessa.
//**************************************************
//
GLCD pin constants
//**************************************************
#define
#define
#define
#define
#define
#define
#define
#define
#define
#define

GLCD_DATA_WRITE
GLCD_DATA_READ
GLCD_DATA_DIRECTION
GLCD_D_I
GLCD_R_W
GLCD_E
GLCD_CS1
GLCD_CS2
GLCD_RST
GLCD_LED

LATD
PORTD
TRISD
LATAbits.LATA3
LATAbits.LATA4
LATAbits.LATA5
LATEbits.LATE0
LATEbits.LATE1
LATEbits.LATE2
LATCbits.LATC1

Questo risulta necessario poich il data bus bidirezionale ed dunque necessario


variare le impostazioni della porta utilizzata al fine di permettere sia la scrittura che la
lettura sul data bus. Il cambio di direzione viene effettuato direttamente dalle funzioni che
lo richiedono per cui non necessario avere altri dettagli. Oltre alla porta per il Data bus
sono definiti anche i vari bit di controllo per la gestione del modulo grafico. I pin suggeriti
nella libreria sono tali da non creare conflitti o perdita di risorse qualora si faccia uso della
scheda di sviluppo LaurTec, ma possibile usare anche altre linee.
Nel caso si decidesse di modificare la libreria si consiglia di crearne una copia e
modificare la stessa, lasciando invariata quella originale, avendo sempre a
disposizione la copia originale.
All'interno dell'header file sono dichiarate tutte le funzioni utilizzate.
Sebbene il C non permetta di proteggere funzioni interne, dichiarandole private, come
invece possibile in C++, le funzioni sono comunque raggruppate in pubbliche e private.
Le funzioni pubbliche sono quelle ad alto livello che possibile usare, mentre quelle
raggruppate e descritte come private non dovrebbero essere usate visto che entrano nel
dettaglio delle specifiche del controllore, in un certo qual modo, per l'utilizzo finale del
Display GLCD come se non ci fossero.
Vediamo ora le funzioni che sono offerte dalla libreria GLCD_KS0108B.

304

ID

Funzione

Descrizione

GLCD_initialize

Permette d'inizializzare il display GLCD.

GLCD_clear

Pulisce il Display.

GLCD_backlight

Accende o spegne la retroilluminazione del Display.

GLCD_set_display

Accende o Spegne il Display (Low Power Mode)

GLCD_set_vertical_offset

Permette di traslare l'immagine del Display.

GLCD_plot_xy

Disegna un punto nelle coordinate X, Y.

GLCD_draw_horizontal_line

Disegna una linea orizzontale.

GLCD_draw_vertical_line

Disegna una linea verticale.

GLCD_draw_window

Disegna una Finestra vuota da usare come bordo.

10

GLCD_draw_box

Disegna un box pieno.

11

GLCD_draw_picture

Disegna un'immagine a schermo intero.

12

GLCD_write_char

Scrive un carattere sul Display.

13

GLCD_write_string

Scrive una stringa (Array di caratteri) sul Display.

14

GLCD_write_message

Scrive un messaggio costante sul Display.

15

GLCD_write_integer

Scrive un intero sul Display.

16

GLCD_set_font

Permette di impostare in font usato dalle funzioni di scrittura.

Tabella 15: Funzioni disponibili nella libreria GLCD_KS0108B.

Per facilitare l'utilizzo della libreria sono definite le seguenti costanti:


#define GLCD_TURN_ON_LED
#define GLCD_TURN_OFF_LED

0b00000001
0b00000000

#define GLCD_TURN_ON_DISPLAY
#define GLCD_TURN_OFF_DISPLAY

0b00000001
0b00000000

#define GLCD_HIGH
#define GLCD_LOW

0b00000001
0b00000000

#define GLCD_ENABLE
#define GLCD_DISABLE

0b00000001
0b00000000

#define GLCD_ENABLE_RESET
#define GLCD_DISABLE_RESET

0b00000000
0b00000001

#define GLCD_FILLING_WHITE 0x00


#define GLCD_FILLING_BLACK 0xFF

Per la documentazione completa delle funzioni, far riferimento alla


documentazione ufficiale. Variazioni della libreria non sono riportate su questo
testo per cui sempre bene far riferimento alla documentazione contenuta nel
file header o quella creata con Doxygen. Le funzioni disponibili potrebbero essere
di pi di quelle riportate in Tabella 15.
bene notare che avendo a che fare con un display grafico, al fine di visualizzare degli
oggetti o semplici caratteri alfanumerici, necessario specificare le coordinate x,y di
origine dell'oggetto stesso. La libreria semplifica il sistema di coordinate emulando un
display grafico ad un unico controllore come riportato in Figura 115. Questo permette di
avere una pi facile visualizzazione degli oggetti in un sistema di riferimento grafico a cui
305

siamo pi abituati.

Figura 115: Modellizzazione del display grafico emulato dalla libreria.

Facendo riferimento all'esempio riportato a fine articolo possibile avere una migliore
comprensione sul concetto di origine dell'oggetto, posto all'angolo in basso a sinistra
dell'oggetto stesso.
Prima di poter utilizzare una qualunque funzione della libreria necessario
richiamare la funzione di inizializzazione del modulo GLCD.

Il controllore KS0108B diversamente dai display alfanumerici e alcuni controllori


grafici, non possiede i font di caratteri memorizzai nel controllore stesso, per cui
necessario implementarli e memorizzarli all'interno della memoria Flash del
microcontrollore utilizzato. La libreria attualmente dispone di un unico font delle
dimensioni 5x7. Tale font memorizzato all'interno del seguente Array:
const unsigned char font_5x7[96][5]

per mezzo del


#define GLCD_ENABLE_FONT_5x7

possibile includere la tabella del font (normalmente la tabella inclusa).


//**************************************************
// Font Tables and Pictures
//**************************************************
#define GLCD_ENABLE_FONT_5x7
#define GLCD_ENABLE_LOGO_1

Per non includere la tabella dei font e risparmiare memoria Flash, basta commentare la
riga in cui viene definito ENABLE_FONT_5x7.
Allo stesso modo dei font possibile includere delle immagini, a loro volta incluse in
Array memorizzati nella memoria Flash. Per abilitare o meno le immagini possibile
306

commentare o meno il #define associato al logo d'interesse.


Variare le dimensioni del font non varia le dimensioni del font disponibile. Una
nuova tabella deve essere creata al fine di supportare altri font.

Per realizzare i font sono disponibili molti programmi gratuiti.


Per realizzare una immagine da caricare Display necessario prima realizzare un'immagine
delle dimensioni 128x64 (per esempio usando Bitmap di Windows). Successivamente
bisogna importare l'immagine all'interno degli appositi programmi di conversione.
Al fine di rendere l'utilizzo della libreria quanto pi facile possibile ho evitato di descrivere
troppi dettagli relativi al suo funzionamento, ciononostante esorto a leggere i sorgenti
della libreria. Spero che il livello di astrazione non abbia creato troppa confusione ma gli
esempi dovrebbero risolvere il problema.

Disegniamo Hello World


Vediamo come utilizzare il Display grafico in un esempio. L'esempio nuovamente un
Hello World ma con qualche aspetto grafico in pi, possibile grazie all'uso del Display
GLCD.
#include <xc.h>
#include "PIC18F4550_config.h"
#include "GLCD_KS0108B.h"
#include "GLCD_KS0108B.c"
#include "delay.h"
#include "delay.c"
//*************************************
//
Prototipo della funzione
//*************************************
void board_initialization (void);
//*************************************
//
Programma principale
//*************************************
int main (void){
unsigned int i = 0;
board_initialization ();
GLCD_initialize ();

307

GLCD_backlight (GLCD_TURN_ON_LED);
while (1) {
GLCD_draw_picture (logo_1);
GLCD_draw_window ( 0, 0, 127, 63, GLCD_FILLING_BLACK);
GLCD_draw_window ( 2, 2, 123, 59, GLCD_FILLING_BLACK);
delay_ms (2000);
GLCD_clear (GLCD_FILLING_WHITE);
GLCD_write_message (25,28,"Loading...

%");

for (i = 0; i < 100 ; i+= 5) {


GLCD_write_integer (90,28, i, 2);
delay_ms (100);
}
GLCD_clear (GLCD_FILLING_WHITE);
GLCD_write_message (30,28,"Hello World");
delay_ms (300);
GLCD_backlight (GLCD_TURN_OFF_LED);
delay_ms (300);
GLCD_backlight (GLCD_TURN_ON_LED);
delay_ms (2000);
}
}

Il programma comincia inizializzando la scheda e il modulo GLCD. Successivamente si


disegna un'immagine sul Display, semplicemente passando il nome dell'Array che lo
contiene, ovvero il suo indirizzo all'interno della memoria Flash.
GLCD_draw_picture (logo_1);

Come seconda cosa vengono disegnati due rettangoli, ovvero finestre.


GLCD_draw_window ( 0, 0, 127, 63, FILLING_BLACK);
GLCD_draw_window ( 2, 2, 123, 59, FILLING_BLACK);

Si noti che sono passati vari parametri. Facendo riferimento alla documentazione:
/**
* This function plots an empty window either with black or white colour
borders.
* Origin and length and height can be set by the user.
*
* @param x_origin Window X origin [0..127].
*
* @param y_origin Window Y origin [0..63].
*
* @param length Window length [0..127].
*
* @param length Window height [0..63].
*
* @param filling_color Filling color allows to set the dot either on
white or black color [GLCD_FILLING_WHITE, GLCD_FILLING_BLACK]

308

*
* @return void
*
* @warning It is user responsibility to make sure that the window length
and height are valid, depending on x_origin, y_origin.
*
*/
void GLCD_draw_window (unsigned char x_origin, unsigned char y_origin,
unsigned char length, unsigned char height, unsigned char filling_color);

si pu vedere che i primi due valori rappresentano le coordinate x,y del punto di
riferimento dell'oggetto stesso, ovvero la sua origine. Come detto questo fa riferimento al
punto in basso a sinistra. Successivamente si passano i parametri relativi alla lunghezza e
all'altezza. In ultimo si definisce il colore della cornice stessa, che pu essere solo:
#define GLCD_FILLING_WHITE 0x00
#define GLCD_FILLING_BLACK 0xFF

Diversamente dal Display LCD, anche quando si scrive un messaggio bisogna fornire
l'origine della stringa stessa, ovvero x ed y:
GLCD_write_message (25,28,"Loading...

%");

Progetto 3 Rappresentare dati in un istogramma


Specifiche del progetto
Realizzare un programma che permetta di visualizzare i dati contenuti in un Array definito
come:
unsigned char data_buffer [4] = {25, 10,45, 20};

per mezzo di un istogramma. I quatto byte, rappresentano l'ampiezza di quattro diversi


segnali presenti agli ingressi Analogici del microcontrollore. I segnali sono identificati dal
numero del canale a cui appartengono, ovvero da CH1 a CH4. La rappresentazione
grafica dell'istogramma deve seguire il Layout di Figura 116.

Figura 116: Layout dell'istogramma.

Sviluppo del progetto


#include <xc.h>
#include "PIC18F4550_config.h"

309

#include "GLCD_KS0108B.h"
#include "GLCD_KS0108B.c"
#include "delay.h"
#include "delay.c"
#define DISPLAY_WIDTH 128
#define
#define
#define
#define

MAIN_WINDOW_ORIGIN_X 0
MAIN_WINDOW_ORIGIN_Y 0
MAIN_WINDOW_LENGTH 127
MAIN_WINDOW_HEIGHT 63

#define INFO_WINDOW_ORIGIN_X 0
#define INFO_WINDOW_ORIGIN_Y 12
#define INFO_WINDOW_LENGTH 127
#define NUMBER_OF_CH 4
#define
#define
#define
#define
#define

CHX_ORIGIN_Y
CH1_ORIGIN_X
CH2_ORIGIN_X
CH3_ORIGIN_X
CH4_ORIGIN_X

2
8
40
73
104

#define COLUMN_WIDTH 15
//*************************************
//
Prototipi delle funzioni
//*************************************
void board_initialization (void);
void draw_main_window (void);
void plot_histogram (unsigned char *data);
//*************************************
//
Programma principale
//*************************************
int main (void){
unsigned char data_buffer[4] = {25, 10,45, 20};
board_initialization ();
GLCD_initialize ();
GLCD_backlight (GLCD_TURN_ON_LED);
draw_main_window ();
plot_histogram (data_buffer);

while (1) {
}

//*************************************
//
Implementazione della funzione
//*************************************

310

void draw_main_window (void){


unsigned char i = 0;
GLCD_clear (GLCD_FILLING_WHITE);
GLCD_draw_window ( MAIN_WINDOW_ORIGIN_X, MAIN_WINDOW_ORIGIN_Y,
MAIN_WINDOW_LENGTH, MAIN_WINDOW_HEIGHT, FILLING_BLACK);
GLCD_draw_horizontal_line
(
INFO_WINDOW_ORIGIN_X,
INFO_WINDOW_ORIGIN_Y, INFO_WINDOW_LENGTH, FILLING_BLACK);
GLCD_write_message
GLCD_write_message
GLCD_write_message
GLCD_write_message

(CH1_ORIGIN_X,
(CH2_ORIGIN_X,
(CH3_ORIGIN_X,
(CH4_ORIGIN_X,

CHX_ORIGIN_Y,"CH1");
CHX_ORIGIN_Y,"CH2");
CHX_ORIGIN_Y,"CH3");
CHX_ORIGIN_Y,"CH4");

for (i=0; i<NUMBER_OF_CH; i++){


GLCD_draw_vertical_line
0,INFO_WINDOW_ORIGIN_Y,FILLING_BLACK);
}
}

(i*(DISPLAY_WIDTH)/NUMBER_OF_CH,

//*************************************
//
Implementazione della funzione
//*************************************
void plot_histogram (unsigned char *data) {
GLCD_draw_box
data[0], FILLING_BLACK);
GLCD_draw_box
data[1], FILLING_BLACK);
GLCD_draw_box
data[2], FILLING_BLACK);
GLCD_draw_box
data[3], FILLING_BLACK);

(CH1_ORIGIN_X,INFO_WINDOW_ORIGIN_Y,COLUMN_WIDTH,
(CH2_ORIGIN_X,INFO_WINDOW_ORIGIN_Y,COLUMN_WIDTH,
(CH3_ORIGIN_X,INFO_WINDOW_ORIGIN_Y,COLUMN_WIDTH,
(CH4_ORIGIN_X,INFO_WINDOW_ORIGIN_Y,COLUMN_WIDTH,

Analisi del progetto


Il programma non si presenta pi complesso del primo esempio ma mette in evidenza
come possa tornare utile definire delle costanti al fine di disegnare delle tabelle o pulsanti.
In questa maniera eventuali cambiamenti tornano pi facili. In particolare il programma
stato anche diviso in pi funzioni, in maniera da dividere gli aspetti grafici. Qualora si
abbiano diverse schermate o menu, pu tornare utile avere delle funzioni dedicate alle
diverse schermate e dividere le stesse anche in parti comuni.
La funzione per disegnare l'istogramma piuttosto semplice e consiste nel disegnare
dei box colorati di opportuna ampiezza. In questo semplice progetto, non avendo ancora
l'esigenza di variare il diagramma nel tempo, ovvero avendo solo dei valori costanti, non
ha senso scrivere funzioni pi complesse. Nel caso in cui la funzione plot_histogram
dovesse disegnare dei valori variabili nel tempo contenuti nell'Array data_buffer,
richiederebbe qualche accorgimento in pi.

311

Riassunto
In questo Capitolo abbiamo visto come utilizzare il modulo GLCD basato su
controllore KS0108B spiegando in dettaglio come viene realizzato in pratica il Display
stesso. In particolare si messa in evidenza l'esigenza dell'uso di due controllori al fine di
ottenere una risoluzione pari a 128x64 pixel. L'uso della libreria presentata ha mostrato
nuovamente come l'uso della stessa permetta di realizzare un sistema complesso, senza
dover entrare nei dettagli di ogni sub modulo.

Punti chiave ed approfondimenti


Pixel: Elemento grafico base, ovvero punto. Ogni elemento grafico pi complesso
rappresentato dall'unione continua o meno di pi pixel.

Domande ed Esercizi
1.
2.
3.
4.
5.
6.
7.

8.

Che cos' modulo GLCD?


Spigare le funzioni di ogni blocco interno al controllore KS0108B.
Per quale motivo ad ogni byte corrispondono 8 pixel?
Spiegare a parole la procedura logica necessaria per poter disegnare un punto della
dimensione di un pixel.
Scrivere una funzione che permetta di disegnare un segmento con una qualunque
inclinazione, ricevendo in ingresso il punto di inizio e di fine del segmento stesso.
Scrivere una funzione che permetta di disegnare una circonferenza, ricevendo in ingresso le
coordinate del centro e la lunghezza, in pixel, del raggio.
Scrivere un programma che permetta di realizzare un menu di selezione come quello
presentato nel Progetto del capitolo IX, ma la cui selezione del menu avvenga
evidenziando l'intera linea e non con il simbolo .
Riscrivere la funzione plot_histogram presentata nel Progetto, al fine di poter accettare
Array variabili nel tempo. In particolare come test definire due Array
unsigned char data_buffer[4] = {25, 10,45, 20};

e
unsigned char data_buffer[4] = {10, 30,20, 40};

passare i due Array alla funzione plot_histogram a distanza di 1000ms in maniera ciclica.
9.

Trovare tre metodi differenti per implementare la funzione plot_histogram e verificarne


la loro efficienza (di esecuzione) nella rappresentazione di un Array variabile nel tempo.

312

Capitolo XI

XI

Utilizziamo l'EUSART interna al PIC


La possibilit di comunicare sempre alla base di ogni sistema embedded.
Normalmente i sistemi embedded utilizzano la porta seriale RS232 o LAN per poter
effettuare il Debug del sistema durante la fase di sviluppo o comunicare con quest'ultimo
durante la normale esecuzione di un'applicazione. In questo Capitolo si introduce la porta
seriale e il suo utilizzo con il protocollo RS232.

Schema elettrico
Per poter seguire gli esempi presentati in questo Capitolo, richiesto realizzare su
Breadboard lo schema elettrico di Figura 117 o far uso delle schede di sviluppo impostate
come descritto nel paragrafo successivo. I collegamenti mostrati sul connettore DB9
implementano gi una connessione Null-Modem, per cui, per il collegamento con il PC si
pu usare un cavo per modem normale.

Figura 117: Schema elettrico di esempio del Capitolo XI.

313

Impostare le schede di sviluppo


Al fine di poter seguire gli esempi mostrati in questo Capitolo necessario impostare la
scheda di sviluppo utilizzata, come descritto nei paragrafi seguenti.

Freedom II
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo capitolo la
scheda di sviluppo Freedom II deve avere i Jumper posizionati nel seguente modo:

Figura 118: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

Freedom Light
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom Light deve avere i Jumper posizionati nel seguente modo:

Figura 119: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

Freedom Light non possiede il transceiver MAX232 e il connettore RS232, per cui
devono essere montati esternamente. Per poter testare tutti i LED, se necessario,
richiesto montare anche i relativi LED aggiuntivi.

314

Impostazioni software
Gli esempi mostrati non richiedono alcuna impostazione software particolare oltre alla
semplice creazione di un progetto.
Alcuni esempi fanno uso delle librerie LaurTec, per cui necessario impostare i percorsi
di libreria riportati di seguito:
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\conf
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\inc
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\src

Per la libreria Microchip usart.h, necessario impostare il percorso:


[percorso radice]\Microchip\xc8\v1.21\include\plib

La versione nel percorso potrebbe variare in base a quella utilizzata.

Il protocollo RS232
Lo standard RS232 rappresenta un protocollo di comunicazione che permette senza un
eccessivo hardware, la comunicazione tra due sistemi elettronici. Lo standard descrive
una trasmissione asincrona e in modalit full-duplex. Il protocollo impone dei vincoli sia
di natura meccanica che elettrica, oltre che al formato dei dati stessi.
Il connettore utilizzato nei Personal Computer di due tipi, il DB25 maschio e il DB9
maschio. Ai suoi albori il connettore utilizzato era il DB25 rimpiazzato successivamente
con il DB9. In particolare lo standard RS232 associato al connettore DB25 mentre lo
standard RS232C associato al connettore DB9. Relativamente ai segnali base, i due
standard non differiscono.
I nomi che il sistema operativo d alle seguenti porte COM1 e COM2, ma non
inusuale avere nomi anche con un numero superiore a 100.
In Tabella 16 sono riportate, in funzione della numerazione che visibile sulla parte
frontale del connettore stesso, il significato dei vari pin, sia nel caso di connettore DB25
che del connettore DB9.
DB25

DB9

in/out

Sigla

TX Trasmissione Dati

RX Ricezione Dati

RTS Request To Send

CTS Clear To Send

20

DTR Data Terminal Ready

DSR Data Set Ready

22

RI Ring Indicator

DCD Data Carrier Detect

GND Ground (massa)

Tabella 16: Corrispondenza pin e funzione.

315

E' possibile subito osservare che sono presenti pi delle due semplici linee di
trasmissione e ricezione. Questo dovuto al fatto che nel protocollo RS232 sono presenti
anche regole per Handshaking (stretta di mano) ovvero linee che permettono al
trasmettitore e al ricevitore di avere informazioni dirette l'uno dell'altro. I modem fanno
uso di queste linee, ma si pu ottenere una trasmissione full-duplex con il solo utilizzo
delle linee TX, RX e GND.
Alcune volte le altre linee di output vengono utilizzate per prelevare l'alimentazione del
dispositivo collegato al computer. Questo viene fatto per esempio dai mouse seriali 135.
Bisogna comunque stare attenti e placare ogni entusiasmo poich questo non significa che
si possa alimentare ogni circuito. Le correnti che possibile prelevare dalle linee di output
sono dell'ordine del mA e non neppure specificato sul manuale, per cui non bene
affidarsi a questa alimentazione a meno di non richiedere correnti molto inferiori al mA.
Vediamo ora a cosa servono le altre linee non di trasmissione.

DTR: La linea DTR viene attivata quando il PC pronto per il collegamento con
una periferica esterna.

DSR: La periferica esterna segnala al PC che pronta al collegamento attivando


DSR.

RTS: La linea RTS viene posta alta dal PC quando pronto per la trasmissione
dati verso la periferica esterna.

CTS: La linea CTS viene attivata in risposta al segnale RTS quando la periferica
pronta per ricevere dati. Prima di questo evento il PC non inizia la trasmissione.

DCD: Il segnale DCD pensato appositamente per i modem. Questo viene


attivato quando il modem collegato al nostro PC rileva la trasmissione dell'altro
modem collegato...chiss dove!

RI: Il segnale RI viene attivato dal modem quando rileva il trillo di una chiamata
telefonica.

Quanto fin ora descritto rappresenta la parte pi grammaticale del protocollo, ovvero
la descrizione dei segnali e il loro significato in una trasmissione. In realt le linee che si
sono appena descritte sono controllabili anche via software il che permette di utilizzarle
anche con un significato differente creando cosi un proprio protocollo.
Quello che per non potremmo variare sono le specifiche elettriche a cui devono
soddisfare i segnali utilizzati per la trasmissione dei dati.
Questo discende semplicemente dal fatto che l'hardware per il protocollo RS232
progettato per soddisfare queste specifiche. In particolare i livelli di tensione sono
riportati in Tabella 17.

135

Anche i mouse USB prendono l'alimentazione direttamente dal computer, ma in questo caso la presa USB prevede di suo le
linee di alimentazione.

316

Livello Logico

Trasmettitore

Ricevitore

da +5V a +15V

da +3V a +25V

da -5V a -15V

da -3V a -25V

non definito

da -3V a +3V

Tabella 17: Corrispondenza livelli logici e tensioni.

Come possibile osservare l'intervallo di tensione che caratterizza ogni livello logico
molto ampio e differisce dai livelli di tensione disponibili sul microcontrollore.
Per tale ragione tra il microcontrollore e il connettore necessario avere un traslatore di
livello adatto, ovvero un RS232 Transceiver. Per convertire il livello logico TTL (5V) a
RS232 il tipico integrato utilizzato il MAX232, come mostrato in Figura 117.
Confrontando i livelli di tensione di questo protocollo con lo standard TTL in cui i livelli
di tensione sono compresi tra 0V (0 logico) e 5V (1 logico) possibile notare che un
livello di tensione alto corrisponde ad uno zero logico.
Nonostante l'ampiezza dell'intervallo di tensioni ammissibili nel protocollo RS232,
generalmente si usano livelli di tensione compresi tra 12V.
In ultimo, prima di fare un esempio di trasmissione utilizzando le linee TxD e RxD,
bene ricordare alcune modalit con cui possibile accorgersi di un errore.
Supponiamo di dover trasmettere il byte 10010011, sia che si trasmetta serialmente un bit
alla volta sia che si trasmetta tutti i bit in parallelo, c' sempre la possibilit che qualche bit
possa cambiare di livello prima di giungere a destinazione a causa del rumore elettrico.
L'errore, di suo, non sempre eliminabile, ma si pu limitare la possibilit che questo si
verifichi. Per limitare il verificarsi di un errore si provvede alla schermatura del cavo e al
suo intreccio in modo da limitare gli effetti nocivi dovuti ad eventuali campi
elettromagnetici che si possono accoppiare con il nostro cavo. Nonostante questi
accorgimenti un errore in trasmissione o ricezione pu ancora avvenire.
Subentrano a questo punto le tecniche per la correzione. Queste presumono in primo
luogo che sia possibile accorgersi dell'errore. Per fare questo necessario aggiungere,
come si dice in gergo, della ridondanza, ovvero degli ulteriori bit che permettano di fare
dei controlli sulle informazioni ricevute.
Maggiore l'informazione ridondante che viene aggiunta, maggiore la probabilit di
accorgersi e correggere l'errore. Con questo si vuole dire che nonostante si faccia uso di
ridondanza, il verificarsi di un errore pu ancora una volta solo essere limitato.
Il protocollo RS232 prevede la possibilit di utilizzare le seguenti tecniche per la
rilevazione degli errori :

Parit pari
Parit dispari
Mark
Space

In particolare possibile anche non abilitare nessun controllo. Questi controlli


permettono solo di verificare se i dati in arrivo sono corretti o meno, non permettendo
alcuna correzione da parte del ricevitore. Inoltre, tali tecniche, essendo piuttosto semplici
possono facilmente essere ingannate.
Se il ricevitore si dovesse accorgere di un eventuale errore nella ricezione dei dati,
317

l'unico modo per effettuare una correzione richiedere al trasmettitore di ritrasmettere


l'informazione affetta da errori. Vediamo in dettaglio come funziona la parit pari e la
parit dispari che sono quelle pi comunemente utilizzate.
Per parit pari si intende che il dato ricevuto deve possedere sempre un numero pari di
1. Quindi se si vuole trasmettere il byte 11001000 affinch sia verificata la parit pari
necessario aggiungere un 1 che rappresenter la ridondanza aggiunta. Dunque la nuova
sequenza realmente trasmessa 110010001.
Se si volesse trasmettere il byte 10001000 bisognerebbe aggiungere uno 0 e si
otterrebbe dunque 100010000. La parit dispari consiste nell'inserire un eventuale 1 al fine
di ottenere un numero dispari di 1.
Da quanto esposto si capisce che se un disturbo esterno dovesse far variare
contemporaneamente un numero pari di bit n la parit pari n la parit dispari si
accorgerebbe di nulla.
Le modalit mark e space consistono rispettivamente nell'aggiungere in maniera fissa
un 1 o uno zero.
Se nessuna delle sopra citate metodologie di rilevazione di errori viene abilita non viene
aggiunto nessun bit di ridondanza.
Vediamo ora un esempio di trasmissione che fa uso del protocollo RS232. Supponiamo
di voler trasmettere il byte 10001100. In questo caso non si considera abilitata nessuna
modalit di rilevazione dell'errore. Secondo il protocollo RS232 la sequenza deve essere
trasmessa a partire dal bit meno significativo 136 (bit a destra) fino al bit pi significativo 137.
Si capisce dunque che eventuali bit di parit essendo messi in coda al byte sono posti al
fianco del bit pi significativo.
Dal momento che i protocollo RS232 di tipo asincrono necessaria qualche strategia
per sincronizzare la fase di trasmissione con quella di ricezione. La tecnica adottata
quella di mantenere la linea TX a livello logico 1 (tensione inferiore 138 a -5V) per poi
trasmettere un bit di start oltre alla normale sequenza. Se collegassimo un oscilloscopio
alla linea di trasmissione potremmo vedere la sequenza d'invio del byte. In Figura 120
riportato quello che verrebbe visualizzato.

Figura 120: Andamento della tensione sulla linea TX.


136
137
138

Noto anche LSB (Least Significant Bit)


Noto anche come MSB (Main Significant Bit)
Si ricorda che -6 inferiore a -5.

318

possibile osservare che prima dell'inizio della trasmissione, la linea posizionata a


-12V. Quando la trasmissione inizia, il livello passa a +12V per la durata di un bit.
L'ampiezza temporale di un bit viene a dipendere dalla velocit di trasmissione. Al
rilevamento del bit di start il ricevitore inizia a leggere il segnale. La lettura del valore di un
bit viene effettuata nel mezzo poich questo il punto pi probabile per trovare
un'informazione corretta.
Dopo il bit di start la tensione rimane alta per altri due bit per poi passare per altri due
bit a -12V. Successivamente ripassa a +12V per la durata di tre bit per poi passare
nuovamente a -12V per la durata di un bit. Questo bit rappresenta l'ultimo della sequenza
voluta.
Si pu osservare che il livello di tensione rimane poi fisso a -12V. Qualora l'ultimo bit
fosse stato uno zero logico il livello sarebbe comunque 139 sceso a -12V. Questa situazione
rappresenta il punto di partenza per una nuova sequenza ovvero per un nuovo bit di start.
Il passaggio finale a -12V viene chiamato bit di stop. In particolare possibile impostare il
numero di bit di stop a 1, 1.5 o 2. L'eventuale bit di parit avrebbe preceduto il, o i bit di
stop. La lunghezza della parola che possibile inviare pu essere di 6,7,8,9 bit 140. Le
velocit che possono essere impostate e variano a seconda dell'USART utilizzata, in
particolare il protocollo RS232 prevede una frequenza massima fino a 20Kb/s (20000 bit
per secondo) mentre molti PC prevedono comunque frequenze fino a 115Kb/s. Alcuni
microcontrollori supportano frequenze di trasmissione anche superiori a 1Mb/s
supportando il protocollo RS485.
Per mezzo della porta seriale RS232 possibile collegare direttamente due PC o due
microcontrollori. Questo tipo di collegamento prende il nome di null-modem.
Per effettuare la connessione null-modem non bisogna semplicemente collegare pin to pin
le due porte seriali dei PC, perch se cosi si facesse non verrebbe trasmesso nulla.
In particolare dovrebbe risultare intuitivo che la linea di trasmissione di un PC deve
essere collegata alla linea di ricezione dell'altro PC e viceversa.
Gi con questi due soli collegamenti si ottenuto un cavo null-modem funzionante. Un
miglioramento, se necessario pu essere ottenuto collegando la linea RTS con la linea
CTS mentre la linea DTR con la linea DSR e DCD.

Descrizione del modulo EUSART


Un qualunque dispositivo hardware dopo aver elaborato un segnale o informazione, sia
essa di natura digitale o analogica, deve poter comunicare per mezzo di un'interfaccia di
output i risultati ottenuti. I display LCD sono una valida soluzione per visualizzare i dati,
ma alcune volte l'elaborazione finale dei dati, o loro visualizzazione, pu avvenire per
mezzo di un altro dispositivo che pu trovarsi fisicamente distante dal primo. Sorge
dunque il problema della trasmissione dei dati da un dispositivo ad un altro.
Per brevi tratte si tende a far prevalere una comunicazione parallela laddove
effettivamente i dati da trasmettere siano digitali. Con trasmissione parallela si intende
l'utilizzo di pi linee dati 141 (fili) per trasmettere l'informazione. Si pensi per esempio alla
139

140

141

Il livello scende automaticamente a -12V se la trasmissione gestita da hardware dedicato. Se la trasmissione implementata
per mezzo di un software bisogna abbassare il livello a -12V manualmente.
Non tutte le USART supportano tutte le lunghezze (forato dati). La lunghezza di 7 ed 8 bit generalmente quella standard
poich permette di inviare i caratteri ASCII e un byte completo.
Si specificato linee dati poich generalmente nella trasmissione dell'informazione sono presenti anche delle linee di

319

comunicazione tra il PIC e il modulo LCD; questo tipo di collegamento permette velocit
particolarmente elevate. Nonostante le apparenti brevi distanze che separano il
microprocessore dalle varie periferiche, necessario attendere un tempo non nullo
affinch tutti i bit che costituiscono l'informazione da trasmettere arrivino alla fine del
bus. Per tale ed altre ragioni la trasmissione parallela non viene utilizzata per lunghe
distanze, anche se in realt la massima distanza viene a dipendere pure dalla massima
frequenza con la quale si vuole trasmettere l'informazione. A tal proposito si pensi alla
porta parallela utilizzata per la stampante, che pu raggiungere distanze dei 3m senza
molti problemi. Questo non sarebbe possibile se si volesse trasmettere le informazioni alla
stampante alla stessa velocit con cui il microprocessore pu leggere o scrivere nella
memoria RAM.
Per lunghe distanze si tende ad utilizzare una trasmissione seriale; per trasmissione
seriale si intende che l'informazione pu viaggiare su un'unica linea dati 142. In questa classe
di trasmissioni rientrano per esempio lo standard RS232 e RS485, entrambi gestiti per
mezzo del modulo interno al PIC nominato EUSART (Enhanced Universal Synchronous
Asynchronous Receiver Transmitter).
Normalmente i nomi dei moduli per gestire la RS232 e RS485 sono nominati USART o
UART a seconda delle opzioni disponibili, in particolare la S sta per trasmissione
Sincrone. Molti PIC gestiscono per alcune opzioni aggiuntive per migliorare il loro
utilizzo in applicazioni particolari, per cui stata aggiunta la E.
Sia che la trasmissione sia parallela o seriale si parla di trasmissioni simplex, half-duplex e
full-duplex. Con questi nomi si caratterizza ulteriormente la tipologia della trasmissione
stessa. In particolare si ha una trasmissione simplex quando i dati viaggiano in un unico
senso, ovvero si invia un'informazione senza preoccuparsi di nessuna risposta da parte del
ricevitore. Con questo tipo di trasmissione non dunque possibile sapere se
l'informazione effettivamente giunta a destinazione ed inoltre non c' un vero scambio
di informazioni143.
Per trasmissione half-duplex si intende una trasmissione che pu avvenire in entrambi i
sensi ma non contemporaneamente. Il fatto che la trasmissione non debba avvenire
contemporaneamente pu essere dovuta alla presenza di un solo cavo per la trasmissione
dati o poich le risorse hardware non permettono di ottenere una comunicazione
bidirezionale nello stesso intervallo di tempo. Un esempio tipico rappresentato dal
protocollo RS485.
In ultimo vi la trasmissione full-duplex che permette una comunicazione in entrambi i
sensi e in contemporanea, come per esempio il protocollo RS232. Questo non
necessariamente significa che siano presenti due linee dati, una per la trasmissione e una
per la ricezione, infatti con la cosiddetta multiplazione di frequenza, utilizzata per esempio
in telefonia, possibile trasmettere pi informazioni in contemporanea su un unico cavo.
Due altri sottogruppi di trasmissioni che possibile individuare sia tra quelle parallele
che quelle seriali l'insieme delle trasmissioni sincrone e quelle asincrone. Per
trasmissione sincrona si intende che assieme alle linee dati viene trasmesso anche un

142

143

controllo necessarie per la corretta trasmissione delle informazioni.


Utilizzando segnali LVDS (Low Voltage Differential Signal) o differenziali di altro tipo, possibile effettuare trasmissioni
parallele a distanze piuttosto lunghe ma le problematiche tecniche associate al ritardo dei bit ( skew) non sono poche, in ogni
modo si cerca sempre di ridurre il numero di cavi necessari.
Un esempio di trasmissione simplex che sfrutta i canali radio rappresentata dalla televisione e dalla radio stessa. Questo
tende ad essere sempre meno vero con l'avvento dei decoder e delle TV via cavo.

320

segnale di Clock in modo da sincronizzare le varie fasi di trasmissione e ricezione dati 144,
questo pu o meno essere fornito per mezzo di linea dedicata. Con trasmissioni sincrone,
avendo a disposizione una base dei tempi, possibile raggiungere velocit di trasmissione
pi alte che non nel caso asincrono. Anche se letteralmente asincrono vuol dire senza
sincronismo, non significa che questo non sia presente. Per asincrono si intende solo che
il sincronismo non viene trasmesso per mezzo di un Clock presente su una linea ausiliaria.
Nelle trasmissioni asincrone, sia il trasmettitore che il ricevitore sono preimpostati a
lavorare ad una certa velocit. Quando il ricevitore si accorge che il trasmettitore ha
iniziato a trasmettere, conoscendo la sua velocit, sapr interpretarlo, lo start bit del
protocollo RS232 serve proprio per permettere la sincronizzazione in una trasmissione
asincrona.
Per l'invio di dati sono presenti molti tipi di trasmissioni sia seriali che parallele, come
anche in ogni lingua affinch i due interlocutori si possano capire necessario che parlino
la stessa lingua. Ogni idioma ha una struttura grammaticale che permette una corretta
trasmissione dell'informazione. In termini tecnici si parla di protocollo per intendere
l'insieme di regole e specifiche che il trasmettitore e il ricevitore devono avere affinch si
possano comprendere. Le varie regole si traducono per lo pi in impostazioni del PIC
stesso ma coinvolgono anche alcuni aspetti elettrici, per cui sono spesso necessari dei
transceiver ad hoc, a seconda del protocollo che si vuole utilizzare.
Infatti il modulo EUSART e protocollo RS232 sono due cose distinte, in particolare il
protocollo RS232 impone alcune regole di tipo meccanico (connettore) ed elettrico al fine
da garantire uno standard sul cosiddetto physical layer (livello fisico) ed anche sul formato
dei dati. Il modulo EUSART rispetta il formato dei dati che il protocollo RS232 richiede,
ma per poter rispettare l'intero protocollo necessario aggiungere il connettore
opportuno145 e soprattutto un traslatore di livello (transceiver) per convertire i segnali TTL
del PIC in RS232 e viceversa. Lo stesso modulo EUSART potrebbe essere utilizzato
anche per una trasmissione RS485, cambiando semplicemente il transceiver o anche LIN.
Per cui a parit di impostazioni del modulo EUART, si potrebbe soddisfare sia il
protocollo RS232 che RS485, semplicemente cambiando il transceiver.

144
145

Si veda per esempio il Tutorial sul Bus I2C.


Nulla vieta di utilizzare altri connettori, ma per poter comunicare con periferiche che rispettano il protocollo, sar necessario
avere un adattatore.

321

Registri di configurazione del modulo EUSART


I registri interni al PIC18F4550 per la configurazione del modulo EUSART sono:

Nome
TXSTA
RCSTA
BAUDCON

TXSTA
RCSTA
BAUDCON
SPBRGH
SPBRG
BIT7

BIT6

BIT5

BIT4

BIT3

BIT2

BIT1

BIT0

CSRC

TX9

TXEN

SYNC

SENDB

BRGH

TRMT

TX9D

SPEN

RX9

SREN

CREN

ADDEN

FERR

OERR

RX9D

ADOVF

RCIDL

RXDTP

TXCKP

BRG16

WUE

ABDEN

SPBRGH

Generatore di Baud Rate Byte pi significativo.

SPBRG

Generatore di Baud Rate Byte meno significativo.

Il contenuto dei registri presentati nel capitolo sono forniti per semplificare la
comprensione delle librerie e del programma ma non rappresentano una fonte
alternativa al Datasheet. A seconda del modello del PIC utilizzato i registri
descritti potrebbero essere differenti o avere bit con significato differente.

Configurazione dei pin


Al fine di utilizzare il modulo EUART necessario impostare ad 1 i bit 6 e 7 del
registro TRISC, ovvero relativi ai pin RC6 e RC7.

La linea RC6 rappresenta la linea TX del modulo EUART.


La linea RC7 rappresenta la linea RX del modulo EUART.

All'abilitazione del modulo, la linea TX viene automaticamente impostata come uscita.

322

Registro TXSTA: Transmit Status and Control Register


R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R-1

R/W-0

CSRC

TX9

TXEN

SYNC

SENDB

BRGH

TRMT

TX9D

Bit 7

Bit 6

Bit 5

Bit 4

Bit 3

Bit 2

Bit 1

Bit 0

Leggenda
R = Readable bit
-n = Value at POR
Bit 7

W = Writable bit
1 = Bit is set

U = Unimplemented bit read as 0


0 = Bit is cleared

S : Settable bit
x = Bit is unknown

CSRC : Clock Source Select bit


Modalit asincrona
Ignorato
Modalit sincrona
1 : Master Mode
0 : Slave Mode

Bit 6

TX9 : 9 bit Transmit enable bit


1 : Trasmissione ad 9 bit
0 : Trasmissione ad 8 bit

Bit 5

TXEN : Transmit enable


1 : Trasmissione abilitata
0 : Trasmissione disabilitata

Bit 4

SYNC : EUASART Mode Select bit


1 : Modalit sincrona
0 : Modalit asincrona

Bit 3

SENDB : Send Break Character bit


Modalit Asincrona
1: Invia bit sincronizzazione alla prossima trasmissione
0: Trasmissione bit completata
Modalit Sincrona
Ignorato

Bit 2

BRGH : High Baud Rate Select bit


Modalit Asincrona
1: High Speed
0: Low Speed
Modalit Sincrona
Ignorato

Bit 1

TRMT : Transmit Shift Register Status bit


1 : TSR vuoto
0 : TSR pieno

Bit 0

TX9D : 9th bit of Transmit Data


Pu essere usato sia come bit di parit che di indirizzo

323

Registro RCSTA: Receive Status and Control Register


R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R-0

R-0

R/W-0

SPEN

RX9

SREN

CREN

ADDEN

FERR

OERR

RX9D

Bit 7

Bit 6

Bit 5

Bit 4

Bit 3

Bit 2

Bit 1

Bit 0

Leggenda
R = Readable bit
-n = Value at POR

W = Writable bit
1 = Bit is set

Bit 7

SPEN : Serial Port Enable Pin


1 : Porta Seriale abilitata
0 : Porta Seriale disabilitata

Bit 6

RX9 : 9 bit Receive enable bit


1 : Ricezione a 9 bit
0 : Ricezione a 8 bit

Bit 5

SREN : Single Receive Enable bit

U = Unimplemented bit read as 0


0 = Bit is cleared

S : Settable bit
x = Bit is unknown

Modalit Asincrona
Ignorato
Modalit Sincrona
1 : Ricezione singola abilitata
0 : Ricezione singola disabilitata
Bit 4

CREN : Continuous Receive Enable bit


Modalit Asincrona
1 : Ricevitore abilitato
0 : Ricevitore disabilitato
Modalit Sincrona
1 : Abilita ricezione continua fino a quando CREN posto a 0
0 : Ricezione continua disabilitata

Bit 3

ADDEN : Address Detect Enable bit


Modalit Asincrona
1: Abilita riconoscimento degli indirizzi
0: Disabilita il rilevamento degli indirizzi
Modalit Sincrona
Ignorato

Bit 2

FERR : Framing Error bit


1: Errore di Frame
0: Non presente alcun errore di Frame

Bit 1

OERR : Overrun Error bit


1 : Errore di Overrun
0 : Non presente alcun errore di Overrun

Bit 0

RX9D : 9th bit of Received Data


Valore del nono bit ricevuto

324

Registro BAUDCON: Receive Status and Control Register


R/W-0

R-1

R/W-0

R/W-0

R/W-0

U-0

R/W-0

R/W-0

ADOVF

RCIDL

RXDTP

TXCKP

BRG16

WUE

ABDEN

Bit 7

Bit 6

Bit 5

Bit 4

Bit 3

Bit 2

Bit 1

Bit 0

Leggenda
R = Readable bit
-n = Value at POR

W = Writable bit
1 = Bit is set

Bit 7

ADOVF : Auto baud rate Acquisition Rollover Status bit


1 : Evento di BRG Rollover
0 : No BRG Rollover

Bit 6

RCIDL : Receive Operation Idle Status bit


1 : Operazione di ricezione in Idle
0 : Operazione di ricezione attiva

Bit 5

RXDTP : Received Data Polarity Select bit

U = Unimplemented bit read as 0


0 = Bit is cleared

S : Settable bit
x = Bit is unknown

Modalit Asincrona
1 : I dati RX sono invertiti
0 : I dati RX non sono invertiti
Modalit Sincrona
1 : Clock CK invertito
0 : Clock CK non invertito
Bit 4

TXCKP : Clock and Data Polarity Select bit


Modalit Asincrona
1 : Dati TX invertiti
0 : Dati TX non invertiti
Modalit Sincrona
1 : Clock CK invertito
0 : Clock CK non invertito

Bit 3

BRG16 : 16 Bit Baud Rate Register Enable bit


1: Generatore baud rate a 16 bit
0: Generatore baud rate a 8 bit

Bit 2

Non implementato

Bit 1

WUE : Wake up Enable


Modalit Asincrona
1 : RX viene campionato ed un Interrupt viene generato sul fronte di discesa
0 : RX non controllato o rilevato fronte di salita
Modalit Sincrona
Ignorato

Bit 0

ABDEN : Auto baud rate Detect Enable bit


Modalit Asincrona
1 : Abilita la misura del baud rate al prossimo carattere
0 : Misura del baud rate disabilitato o completato
Modalit Sincrona
Ignorato

325

Libreria per l'utilizzo del modulo EUSART


Per utilizzare il modulo EUART si possono configurare direttamente i registri
precedentemente descritti o far uso della libreria Microchip:
usart.h

Dal momento che la libreria valida indipendentemente dal PIC18 utilizzato, si far uso
di quest'ultima. Vediamo un riassunto delle funzioni principali della libreria usart.h:
Funzioni

Descrizione

char BusyxUSART(void)

Controlla se l'USART occupata.

void ClosexUSART(void )

Chiude l'USART.

char DataRdyxUSART(void)

Controlla se sono stati ricevuti dati.

void OpenxUSART(unsigned char, unsigned int)

Inizializza l'USART.

char ReadxUSART( void )

Legge un dato dal buffer di ricezione.

void WritexUSART(char)

Trasmette un dato in uscita.

Tabella 18: Funzioni principali della libreria standard usart.h

Si noti subito che ogni funzione possiede una x prima della parola USART. La x deve
essere cambiata con il numero del modulo interno al PIC. I PIC18 possiedono infatti, a
seconda del modello, fino a 3 USART interne. Nel caso di modelli con una sola USART,
la x va tolta.
char BusyUSART ( void )

Per mezzo di questa funzione possibile controllare lo stato di trasmissione


dell'USART. La funzione restituisce il valore 1 se l'USART sta trasmettendo il dato
altrimenti restituisce il valore 0. Questa funzione pu essere utilizzata per controllare la
fine della trasmissione di un byte.
Parametri:
void
Restituisce:
1: Il modulo sta ancora trasmettendo il dato
0: Il modulo libero per trasmettere altri dati
Esempio:
// Aspetta la fine della trasmissione dati
while (BusyUSART());
// Continua dopo la trasmissione

void CloseUSART ( void )

326

Per mezzo di questa funzione viene chiuso il modulo USART precedentemente aperto.

Parametri:
void
Restituisce:
void
Esempio:
// Chiudi il modulo USART
CloseUSART();

char DataRdyUSART ( void )

Per mezzo di questa funzione possibile controllare se nel buffer di ricezione


dell'USART presente almeno un byte. Se presente un dato viene restituito il valore 1
altrimenti se non presente nessun dato viene restituito il valore 0.
Parametri:
void
Restituisce:
1: Il modulo ha un byte nel buffer di ricezione
0: Non sono stati ricevuti dati
Esempio:
// Aspetta per la ricezione di dati
while (!DataRdyUSART());
// Continua dopo la ricezione del dato

void OpenUSART ( unsigned char config, unsigned int spbrg)

Per mezzo di questa funzione possibile aprire il modulo USART ed impostare i


parametri di trasmissione, ricezione ed eventuali interruzioni. Per fare questo bisogna
riempire i due campi della funzione OpenUSART. Il primo valore dato da un AND
bitwise di varie costanti. Il secondo valore associato al registro BRG che permette
d'impostare la frequenza di trasmissione.
Parametri:
config: Il primo valore della funzione impostato per mezzo delle seguenti costanti,
unite tra loro per mezzo dell'AND bitwise &. Le costanti d'interesse sono:
327

Interruzione di Trasmissione:
USART_TX_INT_ON
Interruzione TX ON
USART_TX_INT_OFF Interruzione TX OFF
Interruzione in ricezione:
USART_RX_INT_ON
Interruzione RX ON
USART_RX_INT_OFF Interruzione RX OFF
Modalit USART:
USART_ASYNCH_MODE
USART_SYNCH_MODE

Modalit Asincrona
Modalit Sincrona

Larghezza dati:
USART_EIGHT_BIT
USART_NINE_BIT

8-bit
9-bit

Modalit Slave/Master:
USART_SYNC_SLAVE Modalit Slave sincrona (si applica solo in modalit sincrona)
USART_SYNC_MASTER Modalit Master sincrona (si applica solo in modalit sincrona)
Modalit di ricezione:
USART_SINGLE_RX
Ricezione singola
USART_CONT_RX
Ricezione multipla
Baud rate:
USART_BRGH_HIGH
USART_BRGH_LOW

Baud rate alto


Baud rate basso

spbrg: Il secondo valore da passare alla funzione spbrg che permette di impostare la
frequenza di trasmissione. Tale valore varia a seconda della frequenza del quarzo che si sta
utilizzando e se si sta utilizzando un alto baud rate o meno. Alto baud rate si ha quando il
flag BRGH impostato ad 1 mentre un basso baud rate si ha con BRGH impostato a 0.
Questi valori sono assegnati dalla funzione OpenUSART per mezzo delle costanti
USART_BRGH_HIGH e USART_BRGH_LOW. Per decidere il valore della variabile spbrg si pu
far uso delle tabelle riportate sui Datasheet del microcontrollore che si sta utilizzando. In
Tabella 19 sono riportate quelle di maggior interesse ovvero per il caso asincrono alto
baud rate e basso baud rate.
Le prime due Tabelle fanno riferimento all'opzione basso baud rate; ogni colonna delle
Tabelle fa riferimento a diverse frequenze del quarzo. Le ultime due Tabelle fanno
riferimento al caso sia selezionata l'opzione alto baud rate; anche in questo caso le colonne
fanno riferimento a diversi valori di quarzo.
Per poter utilizzare il modulo EUSART i seguenti bit devono essere cosi
impostati:

328

Il bit SPEN146 (RCSTA bit 7 deve essere impostato ad 1)


Il registro TRISC bit 7 deve essere impostato ad 1
Il registro TRISC bit 6 deve essere impostato ad 1
Il modulo EUSART, alla sua abilitazione, imposta come uscita il bit RC6 (TX)
del registro TRISC. Qualora si faccia uso della libreria Microchip, non bisogna
preoccuparsi del bit SPEN, il quale viene propriamente settato all'apertura del
modulo EUSART.
Esempio:
//
//
//
//
//

Quarzo 20MHz
Formato 8 bit
1 bit di stop
Baud rate 19200bit/s
Interruzioni disattive

OpenUSART( USART_TX_INT_OFF &


USART_RX_INT_OFF &
USART_ASYNCH_MODE &
USART_EIGHT_BIT &
USART_CONT_RX &
USART_BRGH_HIGH,

64);

Si noti che per evitare di scrivere tutte le impostazioni su di una linea si formattato il
tutto su pi linee. Per il C questo non un problema, poich il compilatore, per capire il
termine della riga cerca il punto e virgola. L'approccio con AND di parametri molto
usato, normalmente lo si implementa per mezzo di una struttura enum all'interno della
quale si definiscono le costanti da utilizzare.

146

Il bit SPEN abilita il modulo seriale ovvero l'EUSART (Serial Port Enable).

329

SYNC = 0, BRGH = 0, BRG16 = 0


Baud Rate
bps

Fosc = 40 MHz

Fosc = 20 MHz

Fosc = 10 MHz

Fosc = 8 MHz

Actual
Rate

Error
%

SPBRG

Actual
Rate

Error
%

SPBRG

Actual
Rate

Error
%

SPBRG

Actual
Rate

Error
%

SPBRG

300

1200

1221

1.73

255

1202

0.16

129

1201

-0.16

103

2400

2441

1.73

255

2404

0.16

129

2404

0.16

64

2403

-0.16

51

9600

9615

0.16

64

9766

1.73

31

9766

1.73

15

9615

-0.16

12

19200

19531

1.73

31

19531

1.73

15

19531

1.73

57600

56818

-1.36

10

62500

8.51

52083

-9.58

115200

125000

8.51

104167

-9.58

78125

-32.18

SYNC = 0, BRGH = 0, BRG16 = 0


Baud Rate
bps

Fosc = 4 MHz

Fosc = 2 MHz

Actual
Rate

Error
%

SPBRG

300

300

0.16

1200

1202

0.16

2400

2404

9600

8929

19200

20833

57600
115200

Fosc = 1 MHz

Actual
Rate

Error
%

SPBRG

Actual
Rate

Error
%

SPBRG

207

300

-0.16

51

1201

-0.16

103

300

-0.16

51

25

1201

-0.16

12

0.16

25

2403

-6.99

-0.16

12

8.51

62500

8.51

62500

-45.75

SYNC = 0, BRGH = 1, BRG16 = 0


Baud Rate
bps

Fosc = 40 MHz

Fosc = 20 MHz

Fosc = 10 MHz

Fosc = 8 MHz

Actual
Rate

Error
%

SPBRG

Actual
Rate

Error
%

SPBRG

Actual
Rate

Error
%

SPBRG

Actual
Rate

Error
%

SPBRG

300

1200

2400

2441

1.73

255

2403

-0.16

207

9600

9766

1.73

255

9615

0.16

129

9615

0.16

64

9615

-0.16

51

19200

19231

0.16

129

19231

0.16

64

19531

1.73

31

19230

-0.16

25

57600

58140

0.94

42

56818

-1.36

21

56818

-1.36

10

55555

3.55

115200

113636

-1.36

21

113636

-1.36

10

125000

8.51

SYNC = 0, BRGH = 1 BRG16 = 0


Baud Rate
bps

Fosc = 4 MHz

Fosc = 2 MHz

Fosc = 1 MHz

Actual
Rate

Error
%

SPBRG

Actual
Rate

Error
%

SPBRG

Actual
Rate

Error
%

SPBRG

300

300

-0.16

207

1200

1202

0.16

207

1021

-0.16

103

1201

-0.16

51

2400

2404

0.16

103

2403

-0.16

51

2403

-0.16

25

9600

9615

0.16

25

9615

-0.16

12

19200

19231

0.16

12

57600

62500

8.51

115200

125000

8.51

Tabella 19: Valori del registro SPBRG.

330

char ReadUSART ( void )

Per mezzo di questa funzione possibile leggere un byte ricevuto dalla porta seriale; il
valore letto, ovvero restituito dalla funzione, di tipo char.
Parametri:
void
Restituisce:
Dato letto
Esempio:
unsigned char data = 0;
// Aspetta per la ricezione di dati (tecnica del polling)
while (!DataRdyUSART());
// Leggo il dato ricevuto
data = ReadUSART ();

void WriteUSART ( char data )

Per mezzo di questa funzione possibile scrivere un dato in uscita alla porta seriale. Il
dato deve essere di tipo char quindi di lunghezza non superiore a 8 bit.
Parametri:
data: Dato da inviare
Restituisce:
void.
Esempio:
unsigned char data = 0x0A;
// Invio il dato
WriteUSART(data);

Per ulteriori informazioni sulle altre funzioni disponibili nella libreria usart.h si rimanda
alla documentazione ufficiale della Microchip. Tra le funzioni non trattate in questo
paragrafo ma che possono ritornare utili, si ricorda la famiglia di funzioni put e get che
permettono di scrivere e leggere, singoli caratteri o stringhe, sia da variabili che da
stringhe costanti.

331

Esempio di utilizzo dell'EUSART in polling


Come gi visto in esempi precedenti, il controllo continuo del verificarsi di un evento
viene definito polling. Tale tecnica pu essere molto semplice e pratica qualora il PIC non
debba svolgere altri compiti. Nell'esempio che segue si realizza un semplice sistema
collegato al PC per mezzo della porta seriale, che permette al testo che viene scritto con la
tastiera di venir visualizzato sul display LCD. Ancora una volta possibile vedere come
grazie all'utilizzo delle librerie il programma risulta piuttosto semplice.
Per poter scrivere il testo dal PC sulla scheda di sviluppo si fatto uso del programma
RS232 Terminal147. Questo deve essere impostato per operare sulla porta COM seriale
dove connessa la scheda e deve avere i seguenti parametri:
Configurazione del terminale

Formato: 8 bit
Baud Rate: 19200 bit/s
Stop bit: 1 bit stop
Bit di parit: 0 bit
Controllo Flusso: Nessuno

Tale configurazione deve essere utilizzata anche nel caso in cui si faccia uso di altri
terminali per il controllo della porta seriale.
#include <xc.h>
#include "PIC18F4550_config.h"
#include <usart.h>
#define LCD_DEFAULT
#include "LCD_44780.h"
#include "LCD_44780.c"
#include "delay.h"
#include "delay.c"
int main (void){
// Variabile per salvare il dato restituito
unsigned char data = 0;
// Inizializzo PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Inizializzo PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Inizializzo PORTC
LATC = 0x00;
TRISC = 0b11111101;
// Inizializzo PORTD tutte uscite
147

Il programma RS232 Terminal pu essere scaricato gratuitamente dal sito www.LaurTec.it .

332

LATD = 0x00;
TRISD = 0x00;
// Inizializzo PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
LCD_initialize (20);
LCD_backlight (LCD_TURN_ON_LED);
//
//
//
//
//

Configura l'USART
8 bit
19200 bit/s
1 bit stop
0 bit parit

OpenUSART( USART_TX_INT_OFF &


USART_RX_INT_OFF &
USART_ASYNCH_MODE &
USART_EIGHT_BIT &
USART_CONT_RX &
USART_BRGH_HIGH,
64 );
// Invio la stringa al terminale
putrsUSART ("...start writing: ");
// Invio la stringa all'LCD
LCD_write_message ("...start writing");
// Attendo di ricevere un dato dal PC
while(!DataRdyUSART( ));
LCD_clear ();
// Ciclo infinito
while(1) {
// Leggo il dato dal buffer di ricezione
data = ReadUSART();
LCD_write_char (data);
// Invio il carattere al terminale (Echo)
WriteUSART (data);
// Attendo che il dato venga trasmesso
while (BusyUSART());
// Se premo Back Space pulisco lo schermo
if(data == 0x08) {
LCD_clear ();
}
// Se premo Esc termino l'applicazione
if(data == 0x1B) {
break;
}
// Attendo di ricevere un dato dal PC
while(!DataRdyUSART( ));
}

333

// Chiudo l'USART
CloseUSART();
LCD_clear ();
LCD_write_message ("RS232 : Closed");
// Ciclo infinito
while (1) {
}
}

Arrivati a questo punto del testo inutile ripetere sempre le stesse cose, pi
interessante andare al sodo!
Come prima cosa necessario collegare la scheda di sviluppo al PC per mezzo della porta
seriale ed avviare il programma RS232 Terminal. Una volta programmata la scheda di
sviluppo, se tutto impostato propriamente, viene mostrato il messaggio in Figura 121.

Figura 121: RS232 Terminal all'avvio del programma.

Dopo la visualizzazione del messaggio, il programma si mette in attesa di ricevere dei


caratteri dalla tastiera, controllando in maniera continua il bit:
PIR1bits.RCIF

per mezzo della macro della libreria Microchip:


DataRdyUSART()

334

ovvero:
// Attendo di ricevere un dato dal PC
while(!DataRdyUSART( ));

Il Flag RCIF, lo stesso utilizzato dalle interruzioni, ed indica la ricezione di un dato


nel registro di ricezione. Una volta rilevato un carattere dalla tastiera, viene ripulito lo
schermo LCD e si entra in un loop infinito, dal quale si esce solo quando il carattere
premuto ESC (ovvero codice ASCII 0x1B). Entrati nel loop, il carattere ricevuto viene
letto dal buffer di ricezione e viene scritto sul display LCD e inviato al Terminal, in modo
da visualizzare il carattere ricevuto (effetto echo).
Il carattere letto, come detto viene controllato per vedere se il tasto ESC, che fa
uscire dal loop infinito o il tasto Back Space che permette di cancellare il display. Dopo i
controlli si attende nuovamente di ricevere un nuovo carattere.
Si noti che la prima attesa di lettura del carattere posta fuori dal loop while in modo
da poter scrivere sullo schermo la scritta ...start writing, per poi cancellarla solo alla
ricezione del primo carattere.
Per testare caratteri speciali in ricezione, possibile usare lo strumento ASCII Table di
RS232 Terminal come riportato in Figura 122, richiamabile dal menu Tool.

Figura 122: Strumento ASCII Table.

Esempio di utilizzo dell'EUSART con interruzione


Ogni volta che il microcontrollore deve eseguire pi attivit, gestire le periferiche in
polling non sempre la cosa migliore, visto che facile che le varie attivit possano
interferire tra loro. In questo secondo esempio si rivede quanto mostrato in precedenza
335

ma facendo uso delle interruzioni.


Concettualmente non cambia nulla ma il programma stato riorganizzato riflettendo l'uso
delle interruzioni.
#include <xc.h>
#include "PIC18F4550_config.h"
#include <usart.h>
#define LCD_DEFAULT
#include "LCD_44780.h"
#include "LCD_44780.c"
#include "delay.h"
#include "delay.c"
//*************************************************
//
Gestione Interrupt
//*************************************************
__interrupt (high_priority) void ISR_alta (void) {
// Variabile che conterr i dati ricevuti
unsigned char data;
// Flag per controllare la ricezione del primo carattere
static char firstTime = 0;
// Controllo che l'interrupt sia stato generato dall'USART
if (PIR1bits.RCIF == 1 ) {
// Resetto gli Interrupt EUSART
PIR1bits.RCIF = 0;
// Controllo la ricezione del primo carattere
if (firstTime == 0) {
clear_LCD ();

// Memorizzo il fatto che ho gi pulito il display


firstTime = 1;

// Leggo il dato dal buffer di ricezione


data = ReadUSART();
LCD_write_char(data);
// Invio il carattere al terminale
WriteUSART (data);
// Attendo che il dato venga trasmesso
while (BusyUSART());
// Se premo Back Space pulisco lo schermo
if(data == 0x08) {
LCD_clear ();
}

336

// Se premo Esc termino l'applicazione


if(data == 0x1B) {
LCD_clear ();
// Chiudo l'USART
CloseUSART();
// Ripulisco LCD prima di riscrivere
LCD_clear ();
// L'USART stata chiusa
LCD_write_message ("RS232 : Closed");

}
}

// Disabilito l'interrupt globale


INTCONbits.GIE = 0;

//*************************************************
//
Programma Principale
//*************************************************
int main (void){
// Inizializzo PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Inizializzo PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Inizializzo PORTC
LATC = 0x00;
TRISC = 0b11111101;
// Inizializzo PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Inizializzo PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
LCD_initialize (20);
LCD_backlight (LCD_TURN_ON_LED);
//
//
//
//
//
//

Configura l'USART
8 bit
19200 bit/s
1 bit stop
0 bit parit
Interruzione RX abilitata

OpenUSART( USART_TX_INT_OFF &


USART_RX_INT_ON &
USART_ASYNCH_MODE &
USART_EIGHT_BIT &
USART_CONT_RX &
USART_BRGH_HIGH,

337

64 );
// Invio la stringa al terminale
putrsUSART ("...start writing: ");
// Invio la stringa all'LCD
LCD_write_message ("...start writing");
// Resetto gli Interrupt vecchi
PIR1bits.RCIF = 0;
// Abilito modalit compatibile (di default vale gi 0)
RCONbits.IPEN = 0;
// Abilito l'interrupt globale
INTCONbits.GIE = 1;
// Abilito interrupt per periferiche
INTCONbits.PEIE = 1 ;
// Ciclo infinito
while (1) {
}
}

possibile vedere che il programma principale, ovvero la funzione main ha il compito

di inizializzare l'EUSART, scrivere i messaggi ed abilitare le interruzioni in modalit


compatibile (ovvero alta priorit).
Alla routine di gestione dell'Interrupt viene lasciato il compito di scrivere il carattere sul
display e controllare se il tasto premuto ESC o Back Space. Questa volta non essendo
stato utilizzato il trucco precedente del controllo del byte prima di entrare nel loop di
controllo di ricezione dati, si creata una variabile come flag 148, per memorizzare la
ricezione del primo carattere. In questo modo alla ricezione dei successivi caratteri il
display non viene ripulito. Si noti che la variabile dichiarata static in modo da
mantenere il valore assegnato tra una chiamata e l'altra della routine di gestione
dell'Interrupt. Alla pressione del tasto ESC non viene pi eseguita l'istruzione break bens
il codice per disattivare la periferica e le interruzioni. Notate come le applicazioni
cominciano a divenire sempre pi utili o quantomeno complesse!
Come visto, in questo esempio si fatto uso di numeri magici (magic number),
infatti i pulsanti non sono riconosciuti per mezzo di confronti con costanti ma
per mezzo di confronti con valori numerici. Il valore Back Space vale in
particolare 0x08 che per sua sfortuna proprio pari al valore del vettore delle
interruzioni. Questo significa che se si volesse cambiare il valore 0x08 del
pulsante Back Space bene non farlo con Tool automatici (Trova e cambia in...).
Questi cambierebbero anche il valore del vettore delle interruzioni. In questo caso non
essendo presenti molti numeri 0x08 non un grave problema, ma il non aver utilizzato
una costante sin dal principio potrebbe portare problemi in fase di Refactoring del software
qualora il numero dovesse comparire in pi punti del programma.

148

Variabili che hanno il compito di memorizzare un determinato stato, quale per esempio acceso/spento vengono spesso
chiamate Flag.

338

Progetto 4 Controllo remoto di LED


Specifiche del progetto
Realizzare un programma che permetta di attivare i LED1-LED8 alla ricezione del
codice 0x55 0xyy, dove yy rappresenta il LED da accendere. In particolare il primo bit
permette di controllare il LED1, il secondo bit il LED2 e via dicendo.
Il programma permette in un certo qual modo di poter accedere 8 carichi diversi per
mezzo del PC. In particolare sostituendo i LED con dei Relay (opportunamente pilotati) si
possono controllare carichi di vario tipo.
Configurare la porta seriale nel seguente modo:

Formato: 8 bit
Baud Rate: 19200 bit/s
Stop bit: 1 bit stop
Bit di parit: 0 bit
Controllo Flusso: Nessuno

Configurare RS232 Terminal nel seguente modo:


Disabilitare Terminal mode dal menu :
Settings Terminal Mode

Attivare il formato Hex dal Tab:


Write Settings Data Format

Sviluppo del progetto


#include <xc.h>
#include "PIC18F4550_config.h"
#include <usart.h>
//*************************************************
//
Definizione costanti
//*************************************************
#define HEADER_FILE 0x55
#define DEACTIVATED 0x00
#define ACTIVATED 0x01
#define HEADER_NOT_RECEIVED 0x00
#define HEADER_RECEIVED 0x01
//*************************************************
//
Gestione Interrupt

339

//*************************************************
__interrupt (high_priority) void ISR_alta (void) {
// Variabile che conterr i dati ricevuti
unsigned char data;
// Flag per controllare la ricezione del primo carattere
static unsigned char header_file_Flag = HEADER_NOT_RECEIVED;
// Controllo che l'interrupt sia stato generato dall'USART
if (PIR1bits.RCIF == ACTIVATED ) {
// Resetto gli Interrupt EUSART
PIR1bits.RCIF = DEACTIVATED;
// Leggo il dato in ingresso
data = ReadUSART();
// Controllo la ricezione del primo carattere
if (header_file_Flag == HEADER_NOT_RECEIVED) {

if (data == HEADER_FILE) {
// Memorizzo la ricezione dell'header byte
header_file_Flag = HEADER_RECEIVED;
}
} else {
LATD = data;
header_file_Flag = HEADER_NOT_RECEIVED;
}

}
//*************************************************
//
Programma Principale
//*************************************************
int main (void){
// Inizializzo PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Inizializzo PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Inizializzo PORTC
LATC = 0x00;
TRISC = 0xFF;
// Inizializzo PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Inizializzo PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// Configura l'USART
// 8 bit
// 19200 bit/s

340

// 1 bit stop
// 0 bit parit
// Interruzione RX abilitata
OpenUSART( USART_TX_INT_OFF &
USART_RX_INT_ON &
USART_ASYNCH_MODE &
USART_EIGHT_BIT &
USART_CONT_RX &
USART_BRGH_HIGH,
64 );
// Resetto gli Interrupt vecchi
PIR1bits.RCIF = DEACTIVATED;
// Abilito modalit compatibile (di default vale gi 0)
RCONbits.IPEN = 0;
// Abilito l'interrupt globale
INTCONbits.GIE = 1;
// Abilito interrupt per periferiche
INTCONbits.PEIE = 1 ;
// Ciclo infinito
while (1) {
}
}

Analisi del progetto


Il programma piuttosto semplice ma riflette l'applicazione tipica del voler pilotare dei
carichi per mezzo della porta seriale di un PC. In particolare si osservi che per accendere il
LED si fa uso di un codice che inizia con 0x55, seguito poi dal valore numerico da
rappresentare sulla PORTD ovvero i LED. La presenza del codice d'inizio permette di
rendere il sistema pi robusto contro eventuali disturbi elettrici. Infatti se si dovesse
presentare un disturbo, se questo non dovesse creare il codice 0x55 non verrebbero
controllati i LED. Inviando invece direttamente il codice per il controllo dei LED, un
eventuale disturbo verrebbe riflesso direttamente sui LED creando dei problemi. Per
rendere il sistema ancora pi robusto si potrebbe rendere il codice di partenza, spesso
nominato di header, anche a pi byte. I valori tipici usati in ambito delle telecomunicazioni
sono 0x55 e 0xAA, vista la simmetria degli stessi e la bassa probabilit di essere generati
dal rumore. Questi codici offrono inoltre il vantaggio di permette un pi facile Clock
Recovering, nel caso di comunicazioni sincrone, in cui il clock non abbia una linea dedicata
ma debba essere estratto dalla linea dati.
Facendo uso di RS232 Terminal, al fine di poter inviare i dati in maniera opportuna,
necessario impostare il formato Hex e disabilitare la modalit Terminal. In particolare
facendo uso dello strumento Macros, richiamabile dal menu:
Tools

Macros

possibile impostare dei pulsanti per l'accensione dei vari LED, come riportato in
Figura 123.

341

Figura 123: Strumento Macro.

In particolare possibile scrivere il codice di Header nella casella dedicata nel campo
Prepend Data, in maniera che venga inviato alla pressione di ogni pulsante. Ad ogni
pulsante possibile associare un nome ed impostare il valore da inviare. Nel caso
specifico si associato un LED a pulsante per cui ad ogni codice inviato per il campo yy
viene attivato un solo bit.

Riassunto
In questo Capitolo abbiamo visto il funzionamento del modulo EUSART utilizzandolo
in applicazioni basate sul protocollo RS232. In particolare si visto come controllare una
comunicazione seriale sia facendo uso della tecnica del polling che delle interruzioni. Si
introdotto anche l'applicazione RS232 Terminal, al fine di poter gestire la porta seriale dal
lato PC e poter controllare il sistema Hardware senza dover realizzare GUI dedicate.

Punti chiave ed approfondimenti


Protocollo di comunicazione: Un protocollo di comunicazione rappresenta un insieme
di regole che permette a pi sistemi di scambiare ed interpretare propriamente
informazioni.
Trasmissione Asincrona: Una trasmissione di definisce Asincrona quando il clock non
vine inviato durante la trasmissione dei dati ma generato localmente. Il valore del clock
deve essere in generale noto a priori o ricavato con opportuni artifici software o hardware.
Trasmissione Sincrona: Una trasmissione di definisce Sincrona quando il clock viene
342

fornito durante la trasmissione dei dati per cui il ricevitore pu adattarsi a varie frequenze.
Trasmettere un clock non necessariamente implica una linea dedicata allo stesso, infatti i
dati possono inglobale tale informazione. Nonostante il clock sia fornito dal sistema che
trasmette i dati, il ricevitore in generale pu comunque avere un clock locale che viene
modificato in funzione del clock ricevuto.
Trasmissione Full Duplex: Una trasmissione di definisce Full Duplex quando il sistema
supporta la trasmissione e ricezione dei dati in contemporanea. In generale questo
necessita di una linea dedicata alla trasmissione ed una per la ricezione ma ci sono
comunque tecniche per mezzo delle quali potrebbe essere sufficiente una sola linea dati.
Trasmissione Half Duplex: Una trasmissione di definisce Half Duplex quando il sistema
supporta la trasmissione e ricezione dei dati in tempi separati.
Trasmissione Simplex: Una trasmissione di definisce Simplex quando il sistema
trasmette informazioni ad uno o pi ricevitori, senza avere la possibilit di sapere se la
ricezione sia avvenuta o meno.

Domande ed Esercizi
1.
2.
3.
4.
5.
6.
7.

8.

Che cos' il modulo EUSART?


Quali sono i vantaggi e gli svantaggi di un protocollo seriale e parallelo?
Descrivere brevemente il protocollo RS232.
Descrivere brevemente il protocollo RS485.
Descrivere brevemente il protocollo LIN.
Quali sono i vantaggi nell'usare il protocollo RS485 rispetto al protocollo RS232?
Realizzare un programma che permetta di far comunicare due PIC18F4550 per mezzo delle
linee TX ed RX. In particolare premendo un pulsante su una scheda si accenda un LED
sull'altra. Si definisca un semplice protocollo che permetta di gestire fino a 4 LED ed 4
pulsanti.
Modificare il progetto presentato nel Capitolo permettendo di visualizzare sul Terminal
l'eventuale pressione di un pulsante. In particolare premendo il tasto BT1 sia visualizzato il
messaggio BT1 has been pressed, mentre rilasciando il tasto venga visualizzato BT1 has been
released. Si scriva il programma al fine di gestire fino a 4 pulsanti.

343

Capitolo XII

XII

Utilizziamo il modulo I2C interno al PIC


Molti microcontrollori, tra cui i PIC18, possiedono oltre alla USART, un modulo I2C
per comunicare con periferiche esterne. In questo Capitolo si introdurr brevemente il
bus I2C e le sue applicazioni; successivamente verr spiegata la modalit per impostare il
modulo interno ai PIC18 al fine di poter utilizzare molteplici periferiche esterne e
permettere anche una comunicazione tra due microcontrollori.

Schema elettrico
Per poter seguire gli esempi presentati in questo Capitolo, richiesto realizzare su
Breadboard lo schema elettrico di Figura 124 o far uso delle schede di sviluppo impostate
come descritto nel paragrafo successivo.

Figura 124: Schema elettrico di esempio del Capitolo XII.

344

Impostare le schede di sviluppo


Al fine di poter seguire gli esempi mostrati in questo Capitolo necessario impostare la
scheda di sviluppo utilizzata, come descritto nei paragrafi seguenti.

Freedom II
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom II deve avere i Jumper posizionati nel seguente modo:

Figura 125: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

Freedom Light
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom Light deve avere i Jumper posizionati nel seguente modo:

Figura 126: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

Freedom Light non possiede il la memoria EEPROM e il Real Time Clock


Calendar, per cui per eseguire e testare i relativi esempi necessario montare i
relativi componenti sulla Breadboard.

346

Impostazioni software
Gli esempi mostrati non richiedono alcuna impostazione software particolare oltre alla
semplice creazione di un progetto.
Alcuni esempi fanno uso delle librerie LaurTec, per cui necessario impostare i percorsi
di libreria riportati di seguito:
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\conf
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\inc
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\src

Per la libreria Microchip i2c.h, necessario impostare il percorso:


[percorso radice]\Microchip\xc8\v1.21\include\plib

La versione della libreria inclusa nel percorso potrebbe variare in base a quella utilizzata.

Il protocollo I2C
Il protocollo I2C uno standard brevettato dalla Philips (ora NXP). Venne ideato nel
1980 (Il brevetto scaduto) per superare le difficolt inerenti all'utilizzo di Bus paralleli
per la comunicazione tra un'unit di controllo e le varie periferiche.
In quegli anni, tra le soluzioni maggiormente adottate per permettere la comunicazione
tra un microcontrollore e delle periferiche esterne vi era la comunicazione parallela. Per
tale ragione il bus149 su cui doveva viaggiare l'informazione era costituito da molti fili.
Fin quando bisogna collegare una sola periferica al microcontrollore i problemi legati alla
presenza di molte linee possono essere tenuti sotto controllo. Qualora le periferiche
dovessero essere pi di una, far giungere il Bus ad ogni periferica pu diventare un
problema.
Un semplice Bus ad otto linee comporta comunque la presenza ad ogni integrato di
almeno altrettanti pin necessari per la comunicazione. Questo significa che le dimensioni
dell'integrato vengono ad essere dipendenti dalla dimensione del bus stesso. Ci comporta
che lo stesso PCB (Print Circuit Board) sul quale deve essere montato l'integrato sar pi
grande e quindi pi costoso.
Questi problemi vengono interamente superati dal Bus I 2C, che permette una
comunicazione tra periferiche con due sole linee 150; questa la ragione per cui non raro
avere integrati con Bus I2C a soli otto pin.
Il Bus I2C uno standard seriale che a differenza del protocollo RS232, permette di
collegare sullo stesso Bus un numero elevato di periferiche, ognuna individuata da un proprio
indirizzo, in gergo tale comunicazione si definisce multipoint.

Un notevole vantaggio dei dispositivi che fanno uso del Bus I2C quello della loro
semplicit d'uso. Infatti tutte le regole del protocollo che bisogna rispettare per una
corretta comunicazione vengono gestite a livello hardware, dunque il progettista non si
deve preoccupare di nulla.
Da quando nato, tale protocollo stato aggiornato al fine di adeguarlo alle diverse
149
150

Per Bus si intende semplicemente un insieme di linee sui cui viaggia un segnale elettrico.
Alle due linee bisogna comunque aggiungere la linea di massa comune.

347

esigenze che il mondo dell'elettronica andava mostrando.


Tutte le modifiche apportate sono sempre state compatibili dall'alto verso il basso,
ovvero gli integrati che soddisfano gli ultimi standard possono comunicare sempre con gli
integrati della generazione precedente.
La prima versione del bus I2C permette di trasmettere fino a 100Kbit/s (modalit
standard (Standard Mode). Questa velocit stata portata a 400Kbit/s nelle modifiche
apportate nel 1992 (Fast Speed Mode). Nel 1998 la velocit stata portata fino a 3.4Mbit/s
(High Speed Mode).
Non necessariamente gli integrati di ultima generazione devono rispettare la
modalit ad alta velocit.

Le periferiche che fanno uso del Bus I 2C per comunicare con un'unit di controllo sono
tipicamente: memorie EEPROM, RAM, Real Time Clock Calendar (RTC), LCD,
potenziometri digitali, convertitori A/D, sintonizzatori radio, controller per toni DTMF,
periferiche generiche per estendere il numero degli ingressi o delle uscite (PCF8574,
MCP2300x), sensori per la temperatura, controllori audio e molto altro.
Un altro vantaggio del Bus I2C quello di poter aggiungere o togliere delle periferiche
dal Bus senza influenzare il resto del circuito. Questo si traduce in una facile scalabilit
verso l'alto del sistema, ovvero si pu migliorare un sistema aggiungendo nuove
caratteristiche senza dover toccare l'hardware151.

Specifiche elettriche del bus I2C


Il Bus I2C un bus seriale che necessita di sole due linee nominate SDA (Serial Data) e SCL

(Serial Clock) pi la linea di massa. Ambedue le linee sono bidirezionali 152. La prima
utilizzata per il transito dei dati che sono in formato ad 8 bit, mentre la seconda
utilizzata per trasmettere il segnale di Clock necessario per la sincronizzazione della
trasmissione. Il Bus I2C permette la connessione di pi periferiche sullo stesso Bus ma
permette la comunicazione tra due soli dispositivi alla volta.
Chi trasmette le informazioni chiamato trasmettitore mentre chi le riceve chiamato
ricevitore. L'essere il trasmettitore o il ricevitore non una posizione fissa, ovvero, un
trasmettitore pu anche divenire ricevitore in una differente fase della trasmissione dati.
In ogni comunicazione invece fissa la posizione del cosiddetto Master (Padrone) e del
cosiddetto Slave (Schiavo). Il Master il dispositivo che inizia la comunicazione ed lui a
terminarla, lo Slave pu solo ricevere o trasmettere informazioni su richiesta del Master.
Non tutti i dispositivi possono essere dei Master del bus I2C. Per esempio una memoria
per il mantenimento dei dati non sar un Master del Bus, mentre ragionevole supporre
che un microcontrollore lo possa essere153.
Su uno stesso bus inoltre possibile la presenza di pi Master, ma solo uno alla volta
151

152
153

Naturalmente il software dell'unit di controllo deve essere cambiato affinch possa riconoscere la nuova periferica. Quanto
detto non vale se il software predisposto per accettare la nuova periferica, la quale pu esser dunque inserita senza alcuna
modifica n hardware n software.
Dal momento che la linea dati bidirezionale si ha che il sistema half-duplex.
Molti microcontrollori integrano al loro interno l'hardware necessario per la gestione del bus I2C sia in modalit Master che
Slave.

348

ricoprir questo ruolo. Se per esempio due microcontrollori iniziano una comunicazione,
anche se potenzialmente potrebbero essere ambedue dei Master, solo uno lo sar, in
particolare il Master sar quello che ha iniziato la comunicazione, mentre l'altro sar uno
Slave.
Ogni periferica inserita nel Bus I2C possiede un indirizzo che sul Bus la individua in
modo univoco. Questo indirizzo pu essere fissato dal produttore in sede di
fabbricazione o fissato dal progettista, spesso solo parzialmente. L'indirizzo costituito da
7 bit nelle versioni standard o da 10 bit nelle versioni estese.
Nel caso di indirizzamento a 7 bit si ha potenzialmente la possibilit di indirizzare 128
periferiche mentre nel caso di 10 bit si ha la possibilit di indirizzare fino a 1024
periferiche. Il numero di periferiche ora citate non sono tutte raggiungibili dal momento
che alcuni indirizzi, come si vedr, sono riservati a funzioni speciali.
Nel caso in cui l'indirizzo che l'integrato ha all'interno del bus I 2C venga fissato
dall'industria, comporta che su un Bus non possono essere presenti due integrati dello
stesso tipo.
Questa soluzione viene generalmente scelta per quelle periferiche come il Real Time
Clock Calendar, ovvero per gli orologi. E' ragionevole infatti presumere che in un circuito,
e in particolare sullo stesso bus, sia presente un solo orologio che mantenga ora e data.
Tale scelta ha il vantaggio di permettere di avere un integrato con meno pin. Se proprio
si dovesse avere la necessit di inserire due orologi 154, o comunque due periferiche con
stesso indirizzo necessario dividere il bus in due, questo pu per esempio essere
ottenuto con l'integrato PCA9544 della NXP.
Qualora si utilizzino delle memorie esterne, un eventuale limite come per l'orologio
potrebbe essere notevole. Per tale ragione in questo caso sia ha la possibilit di impostare
l'indirizzo dell'integrato intervenendo su alcuni bit. Ad esempio la memoria 24LC256155
della Microchip, possiede tre pin in uscita nominati A0, A1, A2, per mezzo dei quali
possibile impostare i tre bit meno significativi dell'indirizzo che caratterizza la memoria.
Questo significa che possibile mettere fino ad otto memorie dello stesso tipo sullo
stesso Bus. Un limite sul numero massimo di periferiche che possibile connettere sul
Bus dunque imposto dall'indirizzo, ovvero il numero dei pin assegnati all'indirizzo.
Un altro vincolo a cui non sempre si pensa, imposto dalla capacit totale della linea che
deve essere limitata a non pi di 400 pF. Il valore di questa capacit viene a dipendere dal
numero di dispositivi e dalla lunghezza del bus stesso. Potenzialmente, dal momento che
una linea tipicamente introduce una capacit parassita di circa 80 pF/m, potr essere
lunga fino a 5m, ma queste distanze non sono in realt quelle per cui pensato il
protocollo. Bisogna inoltre tenere a mente che ogni dispositivo aggiunto sul Bus
introduce ulteriore capacit a causa del buffer di ingresso e uscita. I pin del package
dell'integrato possono inoltre aggiungere altri 1-2pF, per cui moltiplicando solo questo
valore per il numero potenziale di periferiche, si capisce che il numero massimo
piuttosto teorico ed fornito per permettere di segmentare le periferiche in maniera
opportuna e non per avere realmente 128 o 1024 integrati su uno stesso Bus.
Il limite imposto dalla capacit viene a dipendere anche dalla velocit con cui devono
avvenire le transizioni dei bit dal livello basso al livello alto (rise time).
Qualora si debbano raggiungere distanze elevate, o il numero dei dispositivi supera la
capacit totale permessa sul Bus, possibile spezzare il Bus, in due o pi parti, per mezzo
154

155

Il PCF8583 possiede un pin, nominato A0, per impostare il bit meno significativo dell'indirizzo. Dunque possibile inserire
due Real Time Clock Calendar sullo stesso bus.
La Microchip produce una vasta gamma di memorie che supportano il protocollo I2C.

349

di ripetitori (Repeater) quali il PCA9515, PCA9516 e il PCA9518 della NXP. Per mezzo di
questi integrati possibile avere 400 pF per ogni segmento del Bus.
Da quanto appena esposto si comprende che un Bus che possa funzionare a 100Kbit/s
non necessariamente deve lavorare a tale frequenza. il Master, opportunamente
impostato, a scandire il sincronismo e dunque la velocit di trasmissione.
La necessit di dividere il Bus in due la si pu avere anche nel caso in cui siano presenti
dispositivi con diversi standard di velocit o livelli di tensione.
La divisione del Bus necessaria solo nel caso in cui si voglia far comunicare le
periferiche a pi alta velocit sfruttandole al massimo della velocit. Nel caso ci si adegui
alla periferica pi lenta non necessario dividere il Bus. Gli integrati che sfruttano il
protocollo I2C possono lavorare a diverse tensioni, dai tipici 5V a 3.3V ed anche 2.5V 156
permettendo consumi cosi ridotti da poter essere montati anche su dispositivi portatili
alimentati a batteria.
Ultima nota, prima di vedere come avviene una comunicazione sul Bus I2C, riguarda le
linee SDA e SCL. Queste linee devono essere implementate per mezzo di uscite open
drain/open collector o emularle. Questa nota particolarmente importante qualora si voglia
implementare il protocollo I2C interamente via Software, ovvero senza sfruttare moduli
dedicati. Tale caratteristica rende necessaria una resistenza di pull-up per ogni linea, ovvero
di una resistenza collegata tra la linea e Vcc, come riportato in Figura 127. Questo
significa che quando le linee SDA e SCL non sono utilizzate, sono a livello alto.
Valori tipici per le resistenze di pull-up sono compresi tra 2K e 10K. Il primo valore
utilizzato per una frequenza del Bus fino a 400Kbit/s mentre il secondo per velocit fino
di 100Kbit/s.
Il valore corretto viene comunque a dipendere, oltre che dalla frequenza di
trasmissione, anche dalla capacit totale di linea.
Nel caso di trasmissioni a 100Kbit/s la resistenza pull-up sufficiente fino a capacit
totali di linea pari a 400 pF. Nel caso di velocit a 400 Kbit/s la resistenza di pull-up
sufficiente fino a capacit totali di linea non superiori a 200 pF, mentre per velocit a 3.4
Mbits/s la resistenza idonea fino a capacit non superiori a 100 pF.
Cosa significa questo limite? Il problema della capacit di linea legato al tempo di
salita che si riesce ad ottenere per una variazione dal livello logico basso a livello logico
alto. Nel caso in cui si faccia uso di una resistenza, i fronti di salita hanno un andamento
esponenziale, tipico della carica e scarica di un condensatore.

156

Il livello di tensione di lavoro tende ormai a spostarsi a 1.8V.

350

Figura 127: Collegamento dei resistori di pull-up e dettaglio dell'indirizzamento di dispositivi dello stesso tipo.

All'aumentare della capacit, il tempo di salita tende ad aumentare, a tal punto da non
rispettare pi le specifiche elettriche I 2C. Per porre rimedio a questo limite bisogna porre,
in sostituzione della resistenza di pull-up, un generatore di corrente costante.
Tale generatore, avendo una corrente indipendente dal livello di carica della capacit di
linea, permette di ottenere dei fronti di salita la cui pendenza rimane costante 157. La
differenza dei fronti di salita ottenuti nel caso di resistenza di pull-up e di generatore
costante sono riportati in Figura 128.

157

Figura 128: Differenza tra i fronti d'onda nel caso si usi una resistenza di pull-up e di un generatore di corrente.
Questo pu essere un modo per ottenere un segnale a forma triangolare. Normalmente si preferisce per generare un
segnale ad onda quadra e poi provvedere all'integrazione di quest'ultimo.

351

Comunicazioni sul bus I2C


Vediamo ora come avviene una comunicazione su di un Bus I 2C. Come detto solo le
periferiche che possono essere dei Master possono avviare una comunicazione.
Supponiamo che un microcontrollore voglia leggere da una memoria esterna, collegata
al Bus I2C, dei dati precedentemente memorizzati.
Le fasi che devono essere seguite sono le seguenti:
1.
2.

3.
4.
5.
6.
7.

8.

Il Master controlla se le linee SDA e SCL non siano attive, ovvero siano poste
ambedue a livello alto158.
Se il Bus libero, il master invia il messaggio che fa capire alle altre periferiche che
il Bus ora occupato. Le altre periferiche si metteranno allora in ascolto per
comprendere con chi il Master ha intenzione di comunicare.
Il Master provvede all'invio del segnale di sincronizzazione sulla linea SCL, che
rappresentato da un onda quadra, non necessariamente periodica.
Il Master invia l'indirizzo della periferica con la quale vuole parlare.
Il Master segnala poi se la comunicazione che vuole intraprendere verso la
periferica di lettura o scrittura.
Il Master attende la risposta da parte della periferica che nella chiamata ha
riconosciuto il suo indirizzo. Se nessuna periferica risponde, il Master libera il Bus.
Dopo l'avvenuto riconoscimento della periferica, il Master inizia lo scambio dei
dati. Lo scambio avviene inviando pacchetti di 8 bit. Ad ogni pacchetto si deve
attendere il segnale che avvisa dell'avvenuta ricezione.
Quando la trasmissione terminata il Master libera il Bus inviando un segnale di
stop.

Vediamo in dettaglio come vengono realmente ottenute le varie fasi.

158

Fase 1-2
L'hardware del microcontrollore, dedicato alla gestione dell'interfaccia I 2C,
controlla le linee SDA e SCL per un tempo superiore al massimo periodo di
trasmissione consentito dall'hardware. Se il bus risulta libero, il Master invia la
sequenza di Start, che consiste nel portare la linea SDA a livello basso quando SCL
a livello alto. L'andamento delle due linee SDA e SCL, per segnalare lo Start,
riportata in Figura 129.

Il livello alto, in termini di tensione, viene a dipendere dagli integrati che si sta utilizzando. Se Vcc=3.3V, i resistori di pull-up
porteranno le linee SDA e SCL al livello pari a 3.3. Diversamente se l'alimentazione ottenuta con 5V il livello alto di 5V.

352

Figura 129: Sequenza di Start.

Dopo l'invio della sequenza di Start, il bus considerato occupato.

Fase 3
Dopo la transizione della linea SDA da alto a basso, il Master invia il segnale di
sincronizzazione per le altre periferiche. A differenza della sequenza di Start e di
Stop, la linea SDA assume un valore valido solo se la linea SCL a livello basso.
Questo vuol dire che non sono ammesse transizioni di livello della linea SDA
durante il livello alto della linea SCL, se non da parte del Master per inviare un
nuovo Start o uno Stop.

Fase 4-5
Il formato dell'indirizzo pu essere sia a 7 bit che a 10 bit, per semplicit si
considera prima il formato a 7 bit.
I 7 bit dell'indirizzo vengono inviati dal bit pi significativo al bit meno
significativo. In coda a questo indirizzo viene aggiunto un bit per segnalare se il
Master vuole intraprendere, con la periferica individuata da tale indirizzo, una
comunicazione di scrittura o di lettura. In particolare se tale bit 0 vuol dire che il
Master vuole scrivere sulla periferica, se il bit 1 vuol dire che il Master vuole
leggere dalla periferica. Il formato del byte (8 bit) riportato in Figura 130 a). Nel
caso in cui l'indirizzo della periferica individuato da 10 bit, si capisce che
necessario inviare un secondo byte. Per mantenere la compatibilit con il formato a
7 bit per necessario qualcosa che permetta alle periferiche in ricezione di capire
che il primo byte non rappresenta in realt il formato d'indirizzo a 7 bit. Questo lo si
ottenuto proibendo alcuni indirizzi nel formato a 7 bit.

353

Figura 130: Formato dell'indirizzo a 7 bit (a) e a 10 bit (b).

Se gli indirizzi proibiti159 dovessero essere utilizzati vorrebbe dire che il primo
byte serve per indirizzare una periferica con indirizzo a 10 bit.
Gli indirizzi proibiti che individuano il formato d'indirizzamento a 10 bit sono
rappresentati dalle combinazioni 1111xxx, dove x pu valere sia 0 che 1.
In particolare la prima x vicino all'1 viene posta a 0 cosi con il primo byte si inviano
solo i due bit pi significativi dell'indirizzo a 10 bit.
L'ottavo bit rappresenta come nel caso ad indirizzamento a 7 bit la condizione di
lettura o scrittura. Gli altri 8 bit, dell'indirizzo a 10 bit, sono inviati con il secondo
byte. Il formato dell'indirizzamento a 10 bit riportato in Figura 130 b).

159

160

161

Fase 6
L'invio dell'indirizzo a 7 bit e della modalit del colloquio (lettura/scrittura),
avviene grazie ad otto transizioni, da livello alto a basso, della linea SCL. Al nono
impulso della linea SCL il Master si aspetta una risposta di un bit da parte della
periferica che ha chiamato160. La risposta della periferica chiamata consiste nel
mantenere a livello basso la linea SDA, per la durata di un ciclo SCL. In gergo si dice
che il Master attende l'Acknowledge da parte della periferica chiamata. Una sola
periferica risponder alla chiamata del Master.
Nel caso di indirizzamento a 10 bit si pu anche avere che pi periferiche
rispondano al primo byte, ma solo una risponder anche al secondo.
Qualora la periferica non sia presente il Master libera il bus permettendo ad
eventuali altri Master di prenderne il controllo.

Fase 7
Dopo l'avvenuto riconoscimento, avviene lo scambio dei dati verso la periferica,
nel caso di scrittura, o dalla periferica al Master, in caso di lettura. In una
comunicazione si possono avere sia fasi di scrittura che di lettura. Si prenda ad
esempio una fase di lettura da memoria. In primo luogo bisogner scrivere dei byte
per impostare l'indirizzo che si vuole leggere, poi si potr effettivamente leggere il
byte161. Ad ogni invio di un byte necessario l'Acknowledge del byte inviato o
ricevuto. In particolare se il Master invia un byte allo Slave si aspetta, dopo l'ottavo
bit, un bit basso sulla linea SDA. Se lo Slave sta inviando un byte al Master si aspetta

In realt sono presenti altri gruppi d'indirizzi proibiti che individuano altre funzioni speciali o sono stati lasciati per future
estensioni dello standard I2C.
Si capisce che colui che scrive il software deve conoscere a priori gli indirizzi delle periferiche montate o che possono essere
montate.
Le memorie permettono istruzioni speciali per saltare la fase d'indirizzamento qualora si debbano leggere pi indirizzi
consecutivi. Comunque l'indirizzo d'inizio dovr essere inviato.

354

che quest'ultimo invii un bit alto dopo aver ricevuto il byte. La mancanza
dell'Acknowledge determina un errore di comunicazione.

Fase 8
Quando la comunicazione terminata, il Master libera il Bus, inviando la
sequenza di Stop.
Questa consiste nella transizione dal livello basso ad alto della linea SDA, quando la
linea SDL alta. In Figura 131 riportata la sequenza di Stop generata dal Master.
Se il Master deve effettuare un'altra comunicazione con un altro Slave, piuttosto che
liberare il bus e rischiare di perdere il diritto del controllo, pu inviare una nuova sequenza
di Start. Il vecchio Slave comprender che la comunicazione con lui terminata.

Figura 131: Sequenza di Stop.

Le potenzialit dell'interfaccia I2C consistono nel fatto di poter disporre di una larga
variet di dispositivi che possiedono al loro interno l'hardware necessario per la gestione
automatica del protocollo.

Descrizione del modulo I2C


Il PIC18F4550 come molti altri microcontrollori PIC18 possiede il modulo interno
MSSP, ovvero il Master Synchronous Serial Port. Tale modulo supporta la trasmissione seriale
di dati e pu essere configurato sia per la trasmissione I2C che per la trasmissione SPI
(che vedremo in un altro Capitolo). Il particolare il modulo MSSP pu essere configurato
per operare nelle seguenti modalit I2C:

Master
Multi-Master Mode
Slave

La modalit Multi-Master rappresenta il caso in cui nello stesso Bus siano presenti pi
Master, per cui necessario controllare che non avvenga nessun conflitto durante la
scrittura dei dati sul Bus.
355

I vari registri associati al modulo MSSP assumono un significato differente in base alla
configurazione del modulo stesso, ovvero se lo stesso impostato come modulo I2C o
SPI, ciononostante il nome dei registri rimane lo stesso. Il Diagramma a blocchi del
modulo MSSP in configurazione I2C riportato in Figura 132.

Figura 132: Diagramma a blocchi del modulo MSSP in configurazione I2C.

Dal Diagramma a blocchi possibile vedere il registro per la trasmissione seriale dei
dati, ovvero SSPSR. Tale registro non direttamente scrivibile, infatti il suo accesso
controllato per mezzo di un sistema a doppio Buffer, rappresentato dal registro SSPBUF.
In particolare per scrivere nel registro SSPSR, quello che bisogna fare scrivere nel
registro SSPBUF. Dal lato di ricezione, SSPBUF viene automaticamente scritto con il
contenuto del registro SSPSR, una volta che viene riempito da un nuovo byte, sia esso il
valore di un dato o un registro, in particolare SSPSR rappresenta uno shift register, ovvero
registro a scorrimento.
Nel Diagramma a blocchi possibile vedere anche il registro per il riconoscimento
dell'indirizzo, necessario nel caso in cui il microcontrollore dovesse essere impostato
come Slave. In particolare il contenuto del registro SSPSR viene confrontato con il
registro SSPADD, che contiene il valore dell'indirizzo della periferica. Il valore di tale
registro pu essere singolo o rappresentare un gruppo, configurabile per mezzo del
registro Address Mask. I bit da mascherare, ovvero ignorare sono contenuti nel registro
SSPCON2, ovvero nei bit 2-5 del relativo registro. Il valore dell'indirizzo rappresentato,
nel caso dello standard a 7 bit dal primo byte trasmesso. A livello software per
riconoscere se il valore del byte ricevuto sia un Byte indirizzo o dati, possibile usare il bit
A/D' contenuto nel registro SSPSTAT. In maniera analoga, per distinguere se l'accesso al
modulo I2C sia per una lettura o scrittura di dati, possibile usare il bit R/W', sempre del
registro SSPSTAT.
356

Nel caso in cui il modulo MSSP sia configurato come Master, il diagramma a blocchi che
meglio lo rappresenta riportato in Figura 133.

Figura 133: Diagramma a blocchi del modulo MSSP in configurazione I2C Master.

In questo nuovo dettaglio del modulo, possibile notare la presenza delle funzioni per
il controllo della sequenza di Start, di Stop, collisione e Acknowledge, che avviene
controllando le linee SCL e SDA. In particolare la linea dati e clock possiede un buffer
con Trigger di Schmitt, il quale permette di limitare eventuali disturbi derivanti dal rumore
presente sulle rispettive linee di comunicazione.

Registri di configurazione del modulo I2C


I registri interni al PIC18F4550 per la configurazione del modulo MSSP in modalit
I2C sono:

SSPCON1
SSPCON2
SSPSTAT
SSPBUF
SSPSR (non accessibile direttamente)
SSPADD

357

Nome

BIT7

BIT6

BIT5

BIT4

BIT3

BIT2

BIT1

BIT0

GIE/GIEH

PEIE/GIEL

TMR0IE

INT0IE

RBIE

TMR0IF

INT0IF

RBIF

PIR1

SPPIF

ADIF

RCIF

TXIF

SSPIF

CCP1IF

TMR2IF

TMR1IF

PIE1

SSPPIE

ADIE

RCIF

TXIF

SSPIE

CCP1IE

TMER2IE

TMR1IE

IPR1

SPPIP

ADIP

RCIP

TXIP

SSPIP

CCP1IP

TMR2IP

TMR1IP

PIR2

OSCFIF

CMIF

USBIF

EEIF

BCLIF

HLVDIF

TMR3IF

CCP2IF

PIE2

OSCFIE

CMIE

USBIE

EEIE

BCLIE

HLVDIE

TMR3IE

CCP2IE

IPR2

OSCFIP

CMIP

USBIP

EEIP

BCLIP

HLVDIP

TMR3IP

CCP2IP

TRISC

TRISC7

TRISC6

TRISC2

TRISC1

TRISC0

TRISD7

TRISD6

TRISD5

TRISD4

TRISD3

TRISD2

TRISD1

TRISD0

INTCON

TRISD
SSPBUF

Buffer di ricezione e trasmissione del modulo MSSP

SSPADD

Indirizzo del modulo I2C in modalit Slave


Registro di configurazione del Clock in modalit Mater

SSPCON1

WCOL

SSPOV

SSPEN

CKP

SSPM3

SSPM2

SSPM1

SSPM0

SSPCON2

GCEN

ACKSTAT

ACKDT

ACKEN

RCEN

PEN

RSEN

SEN

SSPSTAT

SMP

CKE

D/A'

R/W'

UA

BF

Il contenuto dei registri presentati nel Capitolo sono forniti per semplificare la
comprensione delle librerie e dei programmi, ma non rappresentano una fonte
alternativa al Datasheet. A seconda del modello del PIC utilizzato i registri
descritti potrebbero essere differenti o avere bit con significato differente.

Configurazione dei pin


Al fine di utilizzare il modulo I2C necessario impostare ad 1 i bit 0 e 1 del registro
TRISB, ovvero relativi ai pin RB0 e RB1.

La linea RB0 rappresenta la linea SDA del modulo I2C.


La linea RB1 rappresenta la linea SCL del modulo I2C.

All'abilitazione del modulo, le linee SDA e SCL sono automaticamente impostate come
Open Drain.
Oltre alla configurazione dei singoli pin necessario avere i resistori di pull-up al fine di
permettere il corretto funzionamento del sistema. Sul Bus devono essere presenti solo due
resistori di pull-up, uno per linea. Nel caso in cui due sistemi aventi i resistori di pull-up
siano collegati tra loro, in generale necessario disattivare/eliminare i resistori da un
sistema. Nel caso in cui il valore dei resistori dovesse essere elevato (circa 10K) potrebbe
non essere necessario, ma sempre bene verificare la qualit del segnare ed avere due soli
resistori nel sistema finale.
La configurazione dei pin riportata valida solo per il PIC18F4550 e per la
maggior parte dei PIC18 con modulo USB integrato. I PIC18 senza modulo
USB possiedono le linee SDA ed SCL rispettivamente sui pin RC4 ed RC3, RD2
ed RD3. Ci sono Modelli per cui tramite Configuration bit possibile configurare
anche altri pin alternativi.
358

Registro SSPSTAT: MSSP Status Register


R/W-0

R/W-0

R-0

R-0

R-0

R-0

R-0

R-0

SMP

CKE

D/A

R/W

UA

BF

Bit 7

Bit 6

Bit 5

Bit 4

Bit 3

Bit 2

Bit 1

Bit 0

Leggenda
R = Readable bit
-n = Value at POR
Bit 7

W = Writable bit
1 = Bit is set

U = Unimplemented bit read as 0


0 = Bit is cleared

S : Settable bit
x = Bit is unknown

SMP : Slew rate Control bit


Modalit Master e Slave
1 : Slew Rate Control bit Disabilitato (100KHz e 1MHz)
0 : Slew rate Control bit Abilitato (400KHz)

Bit 6

CKE : 9 bit Transmit Enable bit


Modalit Master e Slave
1 : Abilita gli ingressi specifici SMBus
0 : Disabilita gli ingressi specifici SMBus

Bit 5

D/A : Data/Address bit


Modalit Master (Riservato)
Modalit Slave
1: L'ultimo byte ricevuto era un dato
0: L'ultimo byte ricevuto era un Indirizzo

Bit 4

P : Stop bit
1: Indica che stata rilevata la sequenza del bit di Stop
0: Bit di Stop non rilevato

Bit 3

S : Start bit
1: Indica che stata rilevata la sequenza del bit di Start
0: Bit di Start non rilevato

Bit 2

R/W : Read Write Information bit


Modalit Slave
1: Read
0: Write
Modalit Mater
1: Trasmissione in corso
0: Trasmissione non in corso

Bit 1

UA : Update Address (10 bit mode only)


1 : Indica la necessit di aggiornare il registro SSPADD
0 : Il registro SSPADD non necessita di essere aggiornato

Bit 0

BF : Buffer Full Status Bit


In Modalit TX
1 : SSPBUF pieno
0 : SSPBUF vuoto
In Modalit RX
1 : SSPBUF pieno (non include ACK e Stop bit)
0 : SSPBUF vuoto (non include ACK e Stop bit)

359

Registro SSPCON1: MSSP Control Register


R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

WCOL

SSPOV

SSPEN

CKP

SSPM3

SSPM2

SSPM1

SSPM0

Bit 7

Bit 6

Bit 5

Bit 4

Bit 3

Bit 2

Bit 1

Bit 0

Leggenda
R = Readable bit
-n = Value at POR
Bit 7

W = Writable bit
1 = Bit is set

U = Unimplemented bit read as 0


0 = Bit is cleared

S : Settable bit
x = Bit is unknown

WCOL : Write Collision Detect Bit


Modalit Master TX
1 : Tentativo illegale di scrivere nel registro di trasmissione SSBUF (Prerequisiti I2C non rispettati)
0 : Nessuna Collisione
Modalit Slave TX
1 : Tentativo illegale di scrivere nel registro di trasmissione SSBUF, mentre il dato precedente ancora in trasmissione
0 : Nessuna Collisione

Bit 6

SSPOV : Receive Overflow Indicator bit


Modalit RX
1 : Un byte stato ricevuto mentre SSBUF contiene ancora il vecchio byte
0 : Nessun Overflow.
Modalit TX
Bit Ignorato

Bit 5

SSPEN : Master Synchronous Port Enable bit


1: Abilita la porta seriale abilitando opportunamente le linee SDA e SCL.
0: Disabilita la porta seriale e configura le linee SDA e SCL come normali I/O.

Bit 4

CKP : SCK Release Control Bit


Modalit Slave
1: Rilascia il Clock
0: Mantiene il Clock Low (Clock Stretch)
Modalit Master
Bit Ignorato

Bit 3-0

SSMP3:SSPM0 : Master Synchronous Serial Port Mode Select bits


1111: I2C Slave Mode, Indirizzamento a 10 bit (Interrupt Start e Stop bit abilitato).
1110: I2C Slave Mode, Indirizzamento a 7 bit (Interrupt Start e Stop bit abilitato).
1011: Modalit Master Controllata via Firmware (Slave Idle).
1000: Modalit Master clock = Fosc/(4* (SSPADD+1)).
0111: Modalit Slave con indirizzamento a 10 bit.
0110: Modalit Slave con indirizzamento a 7 bit.

360

Registro SSPCON2: MSSP Control Register 2 (I2C in modalit Master)


R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

GCEN

ACKSTAT

ACKDT

ACKEN

RCEN

PEN

RSEN

SEN

Bit 7

Bit 6

Bit 5

Bit 4

Bit 3

Bit 2

Bit 1

Bit 0

Leggenda
R = Readable bit
-n = Value at POR
Bit 7

W = Writable bit
1 = Bit is set

U = Unimplemented bit read as 0


0 = Bit is cleared

S : Settable bit
x = Bit is unknown

GCEN : General Call Enable bit


Ignorato in Modalit Master

Bit 6

AKSTAT : Acknowledge Status bit (Solo Modalit RX)


1 : Acknowledge non ricevuto dallo Slave
0 : Acknowledge ricevuto dallo Slave

Bit 5

ACKDT : Acknowledge Data bit


1: Not Acknowledge
0: Acknowledge

Bit 4

ACKEN : Acknowledge Sequence Enable


1: Avvia la sequenza Acknowledge
0: Sequenza Acknowledge in Idle

Bit 3

RCEN : Receive Enable bit (Solo Modalit RX)


1: Abilita la Modalit RX
0: Ricevitore in stato Idle

Bit 2

PEN : Stop Condition Enable bit


1: Avvia la sequenza di Stop Bit
0: Condizione Stop in stato Idle

Bit 1

RSEN : Repeat Start Condition Enable bit


1 : Avvia sequenza di invio dati multipla.
0 : invio dati multipli in stato Idle.

Bit 0

SEN : Start Condition Enable/Stretch Enable bit


1 : Avvia la sequenza del bit di Start
0 : Condizione di Start in stato Idle

361

Registro SSPCON2: MSSP Control Register 2 (I2C in modalit Slave)


R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

GCEN

ACKSTAT

ACKDT

ACKEN

RCEN

PEN

RSEN

SEN

Bit 7

Bit 6

Bit 5

Bit 4

Bit 3

Bit 2

Bit 1

Bit 0

Leggenda
R = Readable bit
-n = Value at POR
Bit 7

W = Writable bit
1 = Bit is set

U = Unimplemented bit read as 0


0 = Bit is cleared

S : Settable bit
x = Bit is unknown

GCEN : General Call Enable bit


1 : Abilita l'interruzione alla ricezione del General Call Address 0000h
0 : General Call disabilitato

Bit 6

AKSTAT : Acknowledge Status bit


Non usato in Modalit Slave

Bit 5-2

ADMSK5-ADMSK2 : Slave Address Mask Select bit


1: Mascheramento del bit del registro SSPADD, abilitato
0: Mascheramento del bit del registro SSPADD, disabilitato

Bit 1

ADMSK1 : Address Mask Select bit


Indirizzamento a 7 bit
1 : Mascheramento solo di SSPADD <1:0> abilitato
0 : Mascheramento solo di SSPADD <1:0> disabilitato
Indirizzamento a 10 bit
1 : Mascheramento di SSPADD <1:0> abilitato
0 : Mascheramento di SSPADD <1:0> disabilitato

Bit 0

SEN : Stretch Enable bit


1 : Clock Stretching abilitato
0 : Clock Stretching disabilitato

362

Libreria per l'utilizzo del modulo I2C


Nonostante abbia detto che il Bus I2C sia un modo semplice per comunicare con delle
periferiche per mezzo delle due sole linee SDA e SCL, impostare ogni registro del
microcontrollore pu risultare laborioso, in particolare si pu facilmente incorrere in
impostazioni errate che possono portare al malfunzionamento del modulo I2C. Per
questa ragione l'utilizzo di librerie gi collaudate pu tornare utile, per cui si spiegher
come impostare il modulo per mezzo della libreria della Microchip. Il file da includere per
la libreria i2c.h. All'interno di questa libreria sono presenti le funzioni per aprire e
chiudere il modulo e controllore i vari stati del modulo stesso. La libreria supporta anche i
PIC con pi moduli I2C, per cui il nome delle funzioni descritte pu variare in base al
numero del modulo che si sta utilizzando. Nel caso sia presente un solo modulo vale la
nomenclatura delle funzioni sotto riportate.
La libreria Microchip supporta altre funzioni non descritte nel seguente
paragrafo. Per maggiori dettagli possibile far riferimento alla documentazione
ufficiale presente nella directory docs, che possibile trovare nella directory
principale del compilatore XC8.
Vediamo le funzioni e Macro principali che sono presenti nella libreria i2c.h. La x del
nome della funzione va sostituita con il numero del modulo utilizzato, come detto, sono
infatti presenti PIC con pi moduli I2C. In particolare se il PIC possiede un solo modulo
I2C il nome della funzione OpenI2C.
Funzione

Descrizione

void OpenI2Cx (unsigned char, unsigned char )

Apre il modulo I2Cx.

void CloseI2Cx (void)

Chiude il modulo I2Cx.

void IdleI2Cx (void)

Controlla che il bus sia in stato di Idle (non ci sono attivit in corso).

StartI2Cx ( )

Avvia la sequenza del bit di Start (Macro).

StopI2Cx ( )

Avvia la sequenza del bit di Stop (Macro).

NotAckI2Cx ( )

Avvia la sequenza del bit di Not Acknowledge (Macro).

unsigned char WriteI2Cx (unsigned char)

Scrivi un Byte nel buffer di uscita.

unsigned char ReadI2Cx (void)

Legge un byte dal buffer in ingresso.

Tabella 20: Funzioni e Macro disponibili nella libreria I2C.

void OpenI2Cx (unsigned char sync_mode, unsigned char slew)

Questa funzione deve essere eseguita prima di utilizzare qualunque funzione della
libreria I2C, infatti permette di attivare il modulo per un suo corretto utilizzo. Tale
funzione potrebbe non essere richiamata solo nel caso in cui la libreria del
dispositivo I2C utilizzata, fornisca un'altra funzione per attivare ed impostare il
modulo I2C.
Parametri:
sync_mode:

SLAVE_7

I2C Modalit Slave con indirizzo a 7 bit.


363

SLAVE_10 I2C Modalit Slave con indirizzo a 10 bit.


MASTER
I2C Modalit Master.
slew:

SLEW_OFF Ottimizzazione slew rate disabilitata per lavorare a 100 KHz.


SLEW_ON Ottimizzazione slew rate abilitata per lavorare a 400 KHz.

Restituisce:
void.
Oltre ad attivare il modulo necessario impostare la frequenza di lavoro del
modulo stesso. Questo possibile farlo per mezzo del registro SSPADD, il cui valore
determina appunto la frequenza operativa che verr utilizzata qualora il modulo sia
impostato come Master. L'equazione da usare per il calcolo del valore da inserire in
SSPADD :
SSPADD=

F OSC
1
4F BUS

Dove FOSC rappresenta la frequenza del quarzo mentre FBUS rappresenta il valore
della frequenza che si vuole avere per la trasmissione dei dati sul bus I2C.
void CloseI2Cx (void)

Questa funzione deve essere eseguita per richiudere il modulo I2C. Tale funzione
potrebbe non essere mai richiamata all'interno del programma, qualora non si avesse
mai l'esigenza di chiudere il modulo.
Parametri:
void.
Restituisce:
void.
void IdleI2Cx (void)

Questa funzione permette di controllare che non sia in corso alcuna attivit sul
bus ed in particolare siano terminate le operazioni interne al modulo. Questa
funzione deve essere richiamata per evitare che si verifichino sovrapposizioni nella
sequenza delle operazioni svolte dal modulo. Ogni operazione deve essere svolta
solo al termine della precedente. La funzione di tipo bloccante e termina solo
quando il modulo I2C pu svolgere altre operazioni, per questa ragione non accetta
n restituisce alcun valore.
Parametri:

364

void
Restituisce:
void
StartI2C ()

L'operazione di Start, rappresenta una macro che permette di avviare la sequenza


necessaria per il bit di Start. In particolare il codice :
#define StartI2C()

SSPCON2bits.SEN=1;while(SSPCON2bits.SEN)

Parametri:
void
Restituisce:
void
StopI2C ()

L'operazione di Stop, rappresenta una macro che permette di avviare la sequenza


necessaria per il bit di Stop. In particolare il codice :
#define StopI2C()

SSPCON2bits.PEN=1;while(SSPCON2bits.PEN)

Parametri:
void
Restituisce:
void
NotAckI2Cx ()

L'operazione di Not Acknowledge, rappresenta una macro che permette di avviare


la sequenza necessaria per il bit di Acknowledgment. In particolare il codice :
#define NotAckI2C()
SSPCON2bits.ACKDT=1; SSPCON2bits.ACKEN=1;
while(SSPCON2bits.ACKEN)

Parametri:

365

void
Restituisce:
void
unsigned char WriteI2Cx (unsigned char)

La funzione permette di scrivere un singolo Byte sul bus I2C.


Parametri:
Byte da inviare sul bus.
Restituisce:
0 : Il byte stato inviato correttamente.
-1: Si verificato un evento di collisione sul bus (solo in modalit Master).
-2: Acknowledgment negativo ricevuto.
unsigned char ReadI2Cx (void)

La funzione permette di leggere un byte ricevuto dal modulo I2C.


Parametri:
void
Restituisce:
Byte ricevuto dal modulo I2C.
Si fa in ultimo presente che generalmente il modulo I2C mutuamente esclusivo con il
modulo SPI. Il modulo SPI un modulo molto semplice per comunicare con molte altre
periferiche di tipologia simile all'I2C. Il modulo I2C lo si preferisce per piccole distanze e
frequenze di trasmissioni non elevate. Il modulo SPI viene preferito per lunghe distanze
(facendo magari uso di transceiver) e frequenze di trasmissione elevate. Il modulo SPI pu
essere utilizzato per frequenze dell'ordine di 10MHz senza problemi 162.

162

Sebbene molti siano portati a trasmissioni sempre pi veloci, bisogna tenere a mente che all'aumentare delle frequenze in
gioco il layout del PCB diventa sempre pi importante e le considerazioni da tenere a mente non sono banali. Il sistema che
si progetta non deve essere visto solo da una prospettiva software ma anche hardware. In particolari errori di trasmissione
potrebbero derivare da un layout non corretto e non da problemi software.

366

Se si controlla l'implementazione delle funzioni dedicate al modulo I2C, ci si


accorge che sono presenti diversi loop bloccanti, ovvero che prevengono
l'esecuzione del software qualora non si verifichi un determinato evento. Questo,
spesso causa di blocchi software soprattutto alla scrittura dei primi programmi
di esempio.

Esempio Master - Slave, comunicazione tra due PIC


Per poter comprendere l'utilizzo delle funzioni della libreria appena spiegata e del
protocollo I2C, bene vedere un esempio in cui si utilizzano le varie funzioni effettuando
una comunicazione tra due PIC18. In particolare un PIC18 deve essere programmato
come Master mentre il secondo deve essere programmato come Slave.

Programma per il Master


L'applicazione dal lato del Master legge in continuazione i pulsanti collegati al
microcontrollore ed invia il valore del tasto premuto al microcontrollore Slave. La
sequenza di invio dei dati rispetta la sequenza specificata nel Datasheet.
#include <xc.h>
#include <i2c.h>
#include "PIC18F4550_config.h"
#define SLAVE_ADDRESS 0xA6
#define READ 0x01
#define WRITE 0x00
#define BUTTON_MASK

0xF0

#define
#define
#define
#define

BUTTON_1
BUTTON_2
BUTTON_3
BUTTON_4

0x01
0x02
0x04
0x08

#define
#define
#define
#define

BUTTON_1_PRESSED
BUTTON_2_PRESSED
BUTTON_3_PRESSED
BUTTON_4_PRESSED

0b11100000
0b11010000
0b10110000
0b01110000

//*************************************
//
Prototipi delle funzioni
//*************************************
void board_initialization (void);
void write_data (unsigned char data);
//*************************************
//
I2C Master
//*************************************
int main(void) {

367

board_initialization ();
//Inizializzazione del modulo I2C
//Baudrate 400kHz @20MHz
OpenI2C(MASTER,SLEW_OFF);
SSPADD = 12;
//Controllo della pressione del pulsante
while (1) {
switch (PORTB & BUTTON_MASK) {
case BUTTON_1_PRESSED : write_data (BUTTON_1);
LATD = 0x00;
break;
case BUTTON_2_PRESSED : write_data (BUTTON_2);
break;
case BUTTON_3_PRESSED : write_data (BUTTON_3);
break;
case BUTTON_4_PRESSED : write_data (BUTTON_4);
break;

}
}
}

//*************************************
//
Implementazione della funzione
//*************************************
void board_initialization (void) {
// Imposto PORTA
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC
LATC = 0x00;
TRISC = 0xFD;
// Imposto PORTD
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE
LATE = 0x00;
TRISE = 0xFF;
//Attivo i restitori di pull-up su PORTB
INTCON2bits.RBPU = 0;
}
//*************************************
//
Implementazione della funzione
//*************************************
void write_data (unsigned char data){

368

unsigned char dummy_read;


//Controllo che il bus non abbia attivita' in corso
IdleI2C();
StartI2C();
//Lettura fittizia per cancellare il buffer in ingresso
dummy_read = SSPBUF;
// Invio del primo Byte (indirizzo) - Scrittura
while (WriteI2C( SLAVE_ADDRESS | WRITE )) {
data = SSPBUF;
SSPCON1bits.WCOL=0;
}
// Invio del secondo Byte (data)
while(WriteI2C (data));
IdleI2C();
StopI2C();
}

Programma per lo Slave


L'applicazione dal lato dello Slave, legge in continuazione i dati ricevuti sul Bus, i quali
contengono il valore del pulsante premuto. La sequenza di ricezione dei dati rispetta la
sequenza specificata nel Datasheet.
#include <xc.h>
#include <i2c.h>
#include "PIC18F4550_config.h"
#define SLAVE_ADDRESS 0xA6
#define READ 0x01
#define WRITE 0x00
#define BYTE_TO_SEND 0x05
//*************************************
//
Prototipi delle funzioni
//*************************************
void board_initialization (void);
//*************************************
//
I2C Slave
//*************************************
int main(void) {
unsigned char address;
unsigned char data;
board_initialization ();

369

// Inizializzazione del modulo I2C come Slave


OpenI2C(SLAVE_7,SLEW_OFF);
// Indirizzo del modulo Slave
SSPADD = SLAVE_ADDRESS;
while (1) {
//Aspetto l'arrivo del primo byte (Indirizzo)
while(DataRdyI2C()==0);
address = ReadI2C();
//controllo se l'operazione e' di scrittura o lettura
if (SSPSTATbits.R_W == WRITE){
//Aspetto l'arrivo del secondo byte (Data)
while(DataRdyI2C()==0);
data = ReadI2C();
//Visualizzo il valore ricevuto
LATD = data;
}
//Attesa del bit di Stop (fine trasmissione del master)
while(SSPSTATbits.S!=1);
}
}
//*************************************
//
Implementazione della funzione
//*************************************
void board_initialization (void) {
// Imposto PORTA
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC
LATC = 0x00;
TRISC = 0xFD;
// Imposto PORTD
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE
LATE = 0x00;
TRISE = 0xFF;

//Attivo i restitori di pull-up su PORTB


INTCON2bits.RBPU = 0;

370

Esempio di lettura di una memoria EEPROM I2C


Qualora si debba salvare in maniera permanente molte informazioni e la memoria
EEPROM interna al PIC non sufficiente, possibile far uso di memorie EEPROM
esterne, oltre che utilizzare la memoria flash riservata per il programma. Le memorie
EEPROM esterne hanno spesso interfacce per Bus SPI e I2C, ma visto che stiamo
parlando del protocollo I2C far riferimento a quest'ultimo tipo di memoria.
La libreria i2c.h della Microchip permette di leggere e scrivere direttamente le memorie
EEPROM ma possiede il limite di poter gestire memorie che possiedono un solo byte per
l'indirizzamento della locazione di memoria. Per tale ragione memorie del tipo 24LC32,
24LC64 e cos via non possono essere lette o scritte. Come detto la libreria i2c.h fornisce
per tutte le funzioni generiche per il corretto funzionamento del modulo.
Basandomi sulla libreria i2c.h ho realizzato una nuova libreria i2cEEPROM.h per
mezzo della quale possibile leggere e scrivere in memorie EEPROM I2C in cui si hanno
due byte per l'indirizzamento della memoria interna, per esempio Freedom II possiede la
memoria 24LC32, per cui richiesta tale libreria. Per il suo corretto funzionamento
necessario includere il file:
i2cEEPROM.h
i2cEEPROM.c
delay.h
delay.c

La libreria i2cEEPROM.h richiede che il modulo I2C sia stato gi propriamente


impostato per il corretto funzionamento. In particolare tra le funzioni disponibili nella
libreria presente una funzione ad hoc per inizializzare il modulo I2C, senza doversi
preoccupare dei registri interni del modulo I2C. Vediamo ora le funzioni che sono
presenti nella libreria i2cEEPROM.h.
Funzione

Descrizione

void I2C_EEPROM_initialize_(unsigned char, unsigned int)

Inizializza il modulo I2C.

signed char I2C_EEPROM_write (unsigned char, unsigned int , unsigned char )

Scrive un byte nella cella di memoria indirizzata

signed char I2C_EEPROM_write_check (unsigned char, unsigned int , unsigned Scrive un byte nella cella di memoria indirizzata e
char )
controlla che sia stata scritto propriamente
signed char I2C_EEPROM_read ( unsigned char , unsigned int , unsigned char )

Legge un byte dalla cella di memoria indirizzata

Tabella 21: Funzioni disponibili nella libreria.

void I2C_EEPROM_initialize(unsigned
int baud_rate_KHz)

char

crystal_frequency_MHz,

unsigned

Questa funzione permette di inizializzare il modulo I2C e la funzione delay


senza dover entrare nel dettaglio dei registri. Basta infatti specificare il valore del
quarzo, o meglio frequenza di clock usata per la CPU, e il baud rate richiesto sul bus
I2C.
Parametri:

371

crystal_frequency_MHZ: Tale parametro rappresenta la frequenza del cristallo


utilizzato, o meglio la frequenza del clock fornita alla CPU. Questa potrebbe
differire dalla frequenza del cristallo qualora si faccia uso del PLL o dei divisori
interni. La frequenza deve essere espressa in MHz e deve essere un valore intero.
baud_rate_KHZ: Tale valore rappresenta il baud rate desiderato sul bus.
Restituisce:
void
Esempio:
I2C_EEPROM_initialize(20, 400);

signed char I2C_EEPROM_write (unsigned char control, unsigned int address,


unsigned char data)

Questa funzione permette di scrivere un byte all'interno della locazione di


memoria EEPROM indirizzata.
Parametri:
control: Tale valore rappresenta il parametro di controllo che identifica la memoria.
Questo valore lo si trova scritto sul Datasheet della memoria utilizzata. Molte
memorie I2C EEPROM della Microchip possiedono come valore di controllo il
valore 10100xxx dove le x rappresentano il valore dei pin d'indirizzo esterni. Per
esempio Freedom II possiede i pin d'indirizzo della memoria EEPROM collegati a
massa, cio pari a 0; dunque il valore del byte di controllo 0xA0 (10100000).
address: Tale valore rappresenta l'indirizzo della locazione di memoria dove
scrivere il byte d'interesse. Tale indirizzo viene scomposto internamente alla
funzione in due byte. L'utilizzatore deve accertarsi che l'indirizzo sia sempre nei
limiti della memoria utilizzata.
data: Tale valore rappresenta il byte che deve essere scritto all'interno della
memoria.
Restituisce:
1: Il byte stato scritto senza errori (il controllo del valore non effettuato)
-1: Errore Bus Collision
-2: Errore Not Ack Error
-3: Errore Write Collision
Esempio:
errore = I2C_EEPROM_write(0xA0, 0x00, 0x55);

372

signed char I2C_EEPROM_write_check (unsigned char


address, unsigned char data)

control, unsigned int

Per ragioni di velocit, la funzione precedente, anche se riporta diverse condizioni


di errore, non controlla se il byte sia stato propriamente scritto. In applicazioni in
cui bisogna avere un buon livello di robustezza bene controllare che il byte scritto
sia proprio quello voluto, al prezzo di essere pi lenti. In particolare il controllo
consiste semplicemente nel leggere il byte scritto e confrontarlo con il valore
desiderato. Tale controllo risulta anche molto utile in quelle applicazioni in cui si fa
uso di molti cicli di scrittura. Si ricorda infatti che le memorie EEPROM hanno un
numero limitato, seppur grande, di cicli di scrittura. Dal momento che tale funzione
fa utilizzo della libreria delay.h 163 necessario includere il file delay.h e delay.c
presenti nella libreria LaurTec per PIC.
Parametri:
control: Tale valore rappresenta il parametro di controllo che identifica la memoria.
Questo valore lo si trova scritto sul Datasheet della memoria utilizzata. Molte
memorie I2C EEPROM della Microchip possiedono come valore di controllo il
valore 10100xxx dove le x rappresentano il valore dei pin d'indirizzo esterni. Per
esempio Freedom II possiede i pin d'indirizzo della memoria EEPROM collegati a
massa, cio pari a 0; dunque il valore del byte di controllo 0xA0 (10100000).
address: Tale valore rappresenta l'indirizzo della locazione di memoria. Tale
indirizzo viene scomposto internamente alla funzione in due byte. L'utilizzatore
deve accertarsi che l'indirizzo sia sempre nei limiti della memoria utilizzata.
data: Tale valore rappresenta il byte che deve essere scritto all'interno della memoria
Restituisce:
1: Il byte stato scritto senza errori (il controllo del valore effettuato)
-1: Errore Bus Collision
-2: Errore Not Ack Error
-3: Errore Write Collision
-4: Il valore scritto e quello voluto differiscono
Esempio:
errore = I2C_EEPROM_write_check (0xA0, 0x00, 0x55);

signed char I2C_EEPROM_read(unsigned char control, unsigned int address,


unsigned char *data)

Questa funzione permette di leggere un byte dalla locazione di memoria


EEPROM indirizzata.
163

La funzione delay utilizzata per introdurre l'attesa necessaria al ciclo di scrittura per essere completato. Il tempo richiesto,
specificato nel Datasheet, dell'ordine di qualche ms.

373

Parametri:
control: Tale valore rappresenta il parametro di controllo che identifica la memoria.
Questo valore lo si trova scritto sul Datasheet della memoria utilizzata. Molte
memorie i2c EEPROM della Microchip possiedono come valore di controllo il
valore 10100xxx dove le x rappresentano il valore dei pin d'indirizzo esterni. Per
esempio Freedom II possiede i pin d'indirizzo della memoria EEPROM collegati a
massa, cio pari a 0; dunque il valore del byte di controllo 0xA0 (10100000).
address: Tale valore rappresenta l'indirizzo della locazione di memoria. Tale
indirizzo viene scomposto internamente alla funzione in due byte. L'utilizzatore
deve accertarsi che l'indirizzo sia sempre nei limiti della memoria utilizzata.
data: Tale valore rappresenta l'indirizzo della variabile di tipo
all'interno della quale viene scritto il valore letto.

unsigned char,

Restituisce:
1: Il byte stato scritto senza errori (il controllo del valore non effettuato)
-1: Errore Bus Collision
-2: Errore Not Ack Error
-3: Errore Write Collision
Esempio:
unsigned char data;
signed char errore;
errore = I2C_EEPROM_read(0xA0, 0x00, &data);

Si noti che la variabile data all'interno della quale viene scritto il dato letto, viene
passata per mezzo dell'operatore & che permette di passare l'indirizzo della memoria data.

Vediamo ora un esempio in cui si scrive un byte nella memoria EEPROM e lo si legge
subito dopo per visualizzarlo sui LED.
#include <xc.h>
#include "PIC18F4550_config.h"
#include "i2cEEPROM.h"
#include "i2cEEPROM.c"
#include "delay.h"
#include "delay.c"
#define ADDRESS
#define DATA_TO_STORE

0x00
0xA3

374

#define CONTROL_BYTE

0xA0

int main (void){


// Variabile per salvare il dato restituito
unsigned char data = 0;
// Imposto PORTA tutti ingressi
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB tutti ingressi
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC tutti ingressi, RC1 come output
LATC = 0x00;
TRISC = 0b11111101;
// Imposto PORTD tutte uscite
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE tutti ingressi
LATE = 0x00;
TRISE = 0xFF;
// Inizializza il modulo I2C a 400KHz @20MHz
I2C_EEPROM_initialize(20, 400);
I2C_EEPROM_write (CONTROL_BYTE, ADDRESS, DATA_TO_STORE);
// Aspetto che il dato sia propriamente scritto in EEPROM
delay_ms (10);
I2C_EEPROM_read (CONTROL_BYTE, ADDRESS, &data);
LATD = data;
// Ciclo infinito
while (1) {
}
}

Il programma inizia includendo i file di libreria, si noti che oltre al file di libreria
i2cEEPROM.h si incluso anche il file di libreria delay.h. Il resto delle impostazioni
piuttosto standard. Come prima istruzione si richiama la seguente funzione:
// Inizializza il modulo I2C a 400KHz @20MHz
I2C_EEPROM_initialize (20, 400);

Questo richiesto per impostare correttamente la funzione delay.h e il modulo I2C per
lavorare a 400 KHz.
Solo dopo aver inizializzato il modulo I2C possibile utilizzare la libreria
i2cEEPROM.
375

I2C_EEPROM_write (CONTROL_BYTE, ADDRESS, DATA_TO_STORE);

In questo esempio si ignora il valore restituito. Dopo aver scritto il byte


nell'indirizzo di memoria ADDRESS della EEPROM 24LC32, si esegue un
ritardo di 10ms. Tale ritardo necessario per garantire che il byte sia scritto propriamente.
Se si eseguisse una lettura prima di tale tempo 164 si rischierebbe di leggere un valore non
corretto. Tale tempo non richiesto tra scritture successive ad indirizzi diversi. Una volta
attesa la scrittura del byte, viene effettuata la sua lettura.
DATA_TO_STORE

I2C_EEPROM_read (CONTROL_BYTE, ADDRESS, &data);

Si osservi che il valore viene scritto all'interno della variabile data, la quale deve essere
passata per riferimento, ovvero deve essere passato il suo indirizzo. Si ricorda infatti che i
valori passati ad una funzione sono per valore, ovvero la funzione crea delle variabili
temporanee all'interno delle quali pone i valori passati. Questo quello che in generale
voluto, ma nel nostro caso dovendo scrivere il valore nella variabile passata necessario
sapere il suo indirizzo reale e non costruire una variabile temporanea. Per fare questo la
funzione accetta un puntatore al tipo di variabile che si vuole modificare (nel nostro caso
unsigned char). Per passare l'indirizzo della variabile si fa uso dell'operatore & prima
della variabile, da non confondere con l'operatore and. Nel caso si volesse passare
l'indirizzo di un Array, come detto, si sarebbe fatto uso del nome dell'Array senza
parentesi quadre e non dell'operatore &. Tale apparente complicazione stata utilizzata
poich il valore restituito utilizzato per segnalare eventuali errori. In ultimo, il valore
della variabile data viene scritto sui LED.

Esempio di utilizzo di un Clock Calendar I2C


Un'altra periferica frequentemente utilizzata sul bus I2C il Real Time Clock Calendar. La
scheda di sviluppo Freedom II possiede il Real Time Clock Calendar della NXP (Philips)
PCF8563. Viste le numerose impostazioni che la periferica richiede, ho deciso di scrivere
direttamente una libreria dedicata, per utilizzare la libreria bisogna includere i file:
PCF8563.h
PCF8563.c

In Tabella 16 sono riportate le funzioni presenti all'interno della libreria PCF8563,


come visibile dal numero, la libreria offre molta flessibilit. possibile notare che si
fatto uso di una nomenclatura delle funzioni simile a quella utilizzata nell'ambito della
programmazione Linux. Se da un lato il nome diviene piuttosto lungo, dall'altro bisogna
riconoscere che il nome della funzione piuttosto autoesplicativo.

164

Il Datasheet della memoria 24LC32 mette come valore massimo 5ms. Si scelto di usare 10ms quale forma di garanzia per
un corretto funzionamento.

376

Funzione

Descrizione

void PCF8563_initialize (unsigned char , unsigned int )

Inizializza il modulo I2C e PCF8563 per il corretto funzionamento.

signed char RTCC_set_seconds (unsigned char);

Imposta i secondi dell'orario corrente.

unsigned char RTCC_get_seconds (void);

Restituisce i secondi dell'orario corrente.

signed char RTCC_set_minutes (unsigned char);

Imposta i minuti dell'orario corrente.

unsigned char RTCC_get_minutes (void);

Restituisce i minuti dell'orario corrente.

signed char RTCC_set_hours (unsigned char);

Imposta le ore dell'orario corrente.

unsigned char RTCC_get_hours (void);

Restituisce le ore dell'orario corrente.

unsigned char* RTCC_get_time_seconds (void);

Restituisce l'orario corrente con i secondi, formato HH:MM.ss.

unsigned char* RTCC_get_time (void);

Restituisce l'orario corrente senza i secondi, formato HH:MM.

signed char RTCC_set_days (unsigned char);

Imposta il giorno della data corrente.

unsigned char RTCC_get_days (void);

Restituisce il giorno della data corrente.

signed char RTCC_set_day_of_the_week (unsigned char );

Imposta il giorno della settimana.

signed char RTCC_get_day_of_the_week (void);

Restituisce il giorno della settimana.

signed char RTCC_set_months (unsigned char);

Imposta il mese della data corrente.

unsigned char RTCC_get_months (void);

Restituisce il mese della data corrente.

signed char RTCC_set_years (unsigned char);

Imposta l'anno della data corrente.

unsigned char RTCC_get_years (void);

Restituisce l'anno della data corrente.

unsigned char* RTCC_get_date (void);

Restituisce la data corrente, formato DD/MM/YY.

signed char RTCC_set_minutes_alarm (unsigned char, unsigned Imposta i minuti dell'allarme.


char);
signed char RTCC_set_hours_alarm (unsigned char, unsigned Imposta le ore dell'allarme.
char);
signed char RTCC_set_days_alarm (unsigned char, unsigned
char);

Imposta il giorno per l'allarme.

signed char RTCC_set_day_of_the_week_alarm (unsigned char, Imposta il giorno della settimana per l'allarme.
unsigned char );
signed char RTCC_enable_alarm_interrupt (void);

Abilita l'interrupt per la sveglia (allarme).

signed char RTCC_disable_alarm_interrupt (void);

Disabilita l'interrupt per sveglia (allarme).

unsigned char RTCC_is_alarm(void);

Controlla se l'allarme attivo.

signed char RTCC_increment_minutes (void);

Incrementa i minuti dell'orario corrente.

signed char RTCC_increment_hours (void);

Incrementa le ore dell'orario corrente.

signed char RTCC_increment_years (void);

Incrementa gli anni della data corrente.

signed char RTCC_increment_months (void);

Incrementa i mesi della data corrente.

signed char RTCC_increment_days (void);

Incrementa i giorni della data corrente.

Tabella 22: Funzioni disponibili nella libreria PCF8563.

Piuttosto che vedere il funzionamento di ogni funzione, per la cui descrizione si


rimanda alla documentazione allegata alla libreria LaurTec o al file header della libreria
stessa. Ogni funzione di tipo set e comunque ogni funzione che restituisce una variabile
signed char, restituisce il seguente codice di errore:
Ritorna:
1: Il byte stato scritto senza errori (il controllo del valore non effettuato)
-1: Errore Bus Collision
377

-2: Errore Not Ack Error


-3: Errore Write Collision
Il formato dei secondi, minuti, ore, giorno, mese e anno sono BCD, ovvero per
scrivere il giorno 22 non si pu scrivere:
RTCC_set_days(22);

Facendo per uso del formato esadecimale si pu scrivere direttamente:


RTCC_set_days (0x22);

Le funzioni RTCC_get_time, RTCC_get_time_seconds e RTCC_get_date, ritornano


puntatori a una stringa di caratteri ASCII contenente l'orario. La stringa pu essere
direttamente stampata su di un display LCD alfanumerico o grafico.
Vediamo ora un esempio in cui si realizza un orologio con data. Il programma
permette inoltre di cambiare orario e data per mezzo dei pulsanti BT1-BT4.
L'orologio funziona nel seguente modo:
Una volta avviato viene visualizzata una data ed un orario di default (30/12/2008).
Questi possono essere cambiati per mezzo dei pulsanti BT1-BT4.

Premendo BT2 si incrementa l'anno.


Premendo BT3 si incrementa il mese.
Premendo BT4 si incrementa il giorno.
Tenendo premuto BT1:
Premendo BT3 si incrementano i minuti.
Premendo BT4 si incrementano le ore.

#include <xc.h>
#include <i2c.h>
#include "PIC18F4550_config.h"
#include "PCF8563.h"
#include "PCF8563.c"
#define LCD_DEFAULT
#include "LCD_44780.h"
#include "LCD_44780.c"
#define DELAY_INTERRUPT_BLOCKING
#include "delay.h"
#include "delay.c"
#define
#define
#define
#define

BT1
BT2
BT3
BT4

0b11100000
0b11010000
0b10110000
0b01110000

378

//*************************************
//
Prototipi delle funzioni
//*************************************
void board_initialization (void);
//******************************************
//
Gestione Interrupt
//******************************************
// Funzione per la gestione dell'interruzione
__interrupt (high_priority) void ISR_alta (void) {
unsigned char button;
// Controllo che l'interrupt sia stato generato da PORTB
if (INTCONbits.RBIF == 1 ) {
//pausa filtraggio spike
delay_ms (30);
button = PORTB;
button = button & 0xF0;
// Controllo del tasto premuto
switch(button) {
case BT1 & BT3:
RTCC_increment_minutes();
break;

case BT1 & BT4:


RTCC_increment_hours ();
break;
case BT2:
RTCC_increment_years ();
break;
case BT3:
RTCC_increment_months ();
break;
case BT4:
RTCC_increment_days ();
break;

// Resetto il flag d'interrupt per permettere nuove interruzioni


INTCONbits.RBIF = 0;

}
//******************************************
//
Programma Principale
//******************************************
int main (void){
board_initialization ();
// Inizializza il modulo I2C a 400KHz @20MHz
PCF8563_initialize(20,400);

379

// Ritardo di attesa per il cristallo da 32KHz


// Il cristallo e' impostato a 20MHz dalla funzione precedente
delay_ms(500);
LCD_initialize (20);
LCD_backlight (LCD_TURN_ON_LED);
// Inizializzo la data
RTCC_set_days(0x30);
RTCC_set_months (0x12);
RTCC_set_years (0x08);
// Inizializzo l'ora
RTCC_set_hours (0x02);
RTCC_set_minutes (0x56);
RTCC_set_seconds (0x33);
// Resetto il flag d'interrupt per cancellare vecchi eventi
INTCONbits.RBIF = 0;
// Abilito le interruzioni su PORTB
INTCONbits.RBIE = 1;
// Abilito modalit compatibile (di default vale gi 0)
RCONbits.IPEN = 0;
// Abilito l'interrupt globale
INTCONbits.GIE = 1;
// Abilito l'interrupt periferiche
INTCONbits.PEIE = 1 ;
// Ciclo infinito
while (1) {
LCD_write_message ("Time :

");

LCD_write_string (RTCC_get_time());
LCD_goto_line(2);
LCD_write_message ("Date :

");

LCD_write_string (RTCC_get_date ());


LCD_home ();
}

//*************************************
//
Implementazione della funzione
//*************************************
void board_initialization (void){
// Imposto PORTA
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB

380

LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC
LATC = 0x00;
TRISC = 0xFD;
// Imposto PORTD
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE
LATE = 0x00;
TRISE = 0xFF;
//Attivo i restitori di pull-up su PORTB
INTCON2bits.RBPU = 0;
}

Per poter far funzionare il programma correttamente, a fine programmazione,


necessario rimuovere il programmatore. Infatti il programmatore condivide le
linee dati usati dai pulsanti BT3 e BT4.

Il programma funziona, quindi tutto bene...


Dopo aver compilato ed eseguito il programma con successo si pu andare avanti
pensando di aver compreso tutto. Forse qualche domanda sorta su qualche parte del
codice, come per esempio:
#define DELAY_INTERRUPT_BLOCKING

Procediamo per per gradi.


Il programma sebbene funzioni correttamente non stato scritto in maniera ottimale
per permettere applicazioni pi complesse. Infatti l'ISR non stata ottimizzata dal punto
di vista temporale, ovvero sebbene lineare alla comprensione, potrebbe richiedere troppo
tempo per la sua esecuzione. Infatti i filtri dei pulsanti dell'orologio sono inclusi nell'ISR.
Oltre a questo aspetto, che in applicazioni semplici potrebbe essere anche non
considerato, l'aver inserito la funzione delay all'interno dell'ISR apre delle problematiche
molto pi complesse tipiche di sistemi multitasking come i sistemi operativi.
Il ritardo in se, non un problema, infatti usando le macro per i ritardi offerte dalla
Microchip il problema del multitasking non si verrebbe a verificare. Infatti le librerie delay
Microchip introducono ad ogni chiamata un pezzo di codice indipendente per creare il
ritardo stesso. La libreria LaurTec invece, aggiungendo l'opzione di poter cambiare in
maniera dinamica la frequenza del cristallo con la quale impostare la libreria stessa apre
delle problematiche di utilizzo.
La funzione delay infatti usata all'interno delle funzioni per il controllo LCD al fine
di generare i segnali di controllo per il modulo. Allo stesso tempo la funzione delay viene
usata nell'ISR, la quale pu interrompere in maniera asincrona (non deterministica) le
varie funzioni di controllo del modulo LCD. Il problema nel verificarsi di un Interrupt sta
381

nel fatto che la funzione delay ha delle variabili interne che vengono alterate in due punti
diversi, rendendo il programma imprevedibile. In particolare incrementando e
decrementando le variabili interne in maniera incontrollata si possono verificare delle
condizioni di stallo legate al fatto che determinate condizioni possono non essere pi
verificate, per cui il programma non procede oltre.
Per tale ragione, usando la funzione delay sia nel main che nell'ISR necessario
dichiarare la seguente riga di codice prima della libreria delay:
#define DELAY_INTERRUPT_BLOCKING

In questo modo la chiamata alla funzione delay_ms blocca le interruzioni di ogni


periferica fino all'esecuzione del ritardo. Nel caso in cui la funzione delay sia usata solo
nel main o nell'ISR, non si ha la necessit di creare dei delay bloccanti per le interruzioni.
Questo problema, messo in luce per la funzione delay in realt valido per ogni
funzione che possa venire usata in pi parti del programma in contemporanea, o meglio
in pi parti, senza che sia terminata l'esecuzione della precedente chiamata. Uno scenario
di questo tipo viene a verificarsi nel caso di chiamate della funzione dall'ISR e dal main o
da parte di due task (o Thread) differenti, nel caso in cui si stia utilizzando un sistema
operativo. Una funzione che possa essere utilizzata in vari punti o da pi task, viene detta
Thread Save.

Riassunto
In questo Capitolo abbiamo visto il funzionamento del modulo I2C, descrivendo il
protocollo stesso. In particolare si mostrato come utilizzare direttamente la libreria
Microchip al fine di gestire la comunicazione Master Slave tra due microcontrollori. Oltre
alla libreria base si sono introdotte anche le librerie per la gestione EEPROM e del Real
Time Clock Calendar, mostrando come l'utilizzo delle librerie possa rendere il programma
particolarmente chiaro e di facile realizzazione. In ultimo si sono accennate brevemente
le problematiche ad applicazioni multitasking, ovvero la chiamata di una funzione in pi
punti del programma.

Punti chiave ed approfondimenti


Protocollo I2C: Il protocollo I2C un protocollo seriale ideato dalla Philips (ora NXP)
per risolvere le problematiche di comunicazione tra periferiche e microprocessore,
evitando le numerose linee di comunicazione tipiche dell'Address Bus e del Data Bus,
richieste in sistemi digitali a microcontrollore o microprocessore.
Periferica Master: Nel protocollo I2C una periferica viene definita Master se pu iniziare
una comunicazione sul Bus.
Periferica Slave: Nel protocollo I2C una periferica viene definita Slave se pu iniziare
una comunicazione sul Bus solo previa interrogazione da un Master. Uno Slave potrebbe
richiedere ad un Master di essere interrogato, facendo uso di linee interruzioni dedicate.
Le linee delle interruzioni non fanno parte dello standard I2C e rappresentano solo una
382

soluzione per rendere una periferica Slave pi intelligente.

Domande ed Esercizi
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.

Quali sono le ragioni per cui stato introdotto il protocollo seriale I2C?
Quali sono le differenze tra il protocollo I2C e il protocollo RS232?
Quali sono le differenze tra il protocollo I2C e il protocollo RS485?
possibile per i PIC18 eseguire un codice direttamente da una memoria esterna
EEPROM? (spiegare la risposta)
possibile per un PIC18F4550 realizzare un RTCC senza far uso di un PCF8563 esterno?
(descrivere due soluzioni, una basata su un solo quarzo ed una basata su due)
Che problemi si potrebbero avere richiamando una funzione in pi parti del programma in
maniera asincrona?
Che cosa significa che una funzione Thread Safe?
Riscrivere il programma del RTCC facendo uso di una state machine, evitando i ritardi
all'interno dell'Interrupt Service Routine.
Modificare la libreria EEPROM al fine che possa funzionare anche per EEPROM ad un
solo byte d'indirizzo.
Realizzare una sveglia, estendendo il programma di esempio, in cui sia possibile impostare
un orario per la sveglia.

383

Capitolo XIII

XIII

Utilizziamo il modulo SPI interno al PIC


Tra le interfaccie seriali pi importanti, oltre al modulo UART e I2C, bisonga ricordare
l'interfaccia SPI. La sua semplicit d'uso la porta spesso ad essere preferita al protocollo
I2C, ma come vedremo a breve, a seconda delle applicazioni, utilizzare l'una o l'altra pu
portare alcuni vantaggi. In questo Capitolo si introdurr brevemente l'interfaccia SPI e le
sue applicazioni; successivamente verr spiegato il modulo integrato nei PIC18 al fine di
poter utilizzare molteplici periferiche esterne.

Schema elettrico
Per poter seguire gli esempi presentati in questo Capitolo, richiesto realizzare su
Breadboard lo schema elettrico di Figura 134 o far uso delle schede di sviluppo impostate
come descritto nel paragrafo successivo.

Figura 134: Schema elettrico di esempio del Capitolo XIII.

384

Impostare le schede di sviluppo


Al fine di poter seguire gli esempi mostrati in questo Capitolo necessario impostare la
scheda di sviluppo utilizzata, come descritto nei paragrafi seguenti.

Freedom II
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom II deve avere i Jumper posizionati nel seguente modo:

Figura 135: Impostazioni dei Jumper per il Progetti presentati nel Capitolo.

Freedom Light
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom Light deve avere i Jumper posizionati nel seguente modo:

Figura 136: Impostazioni dei Jumper per il Progetti presentati nel Capitolo.

Il DAC MCP4822 non presente sulle schede di sviluppo e deve per tanto
essere montato sulle schede di sviluppo esterne o su Breadboard. Per montaggi
in cui sia richiesto un bus di lunghezza di circa 10cm o pi, si consiglia di
mettere un resistore in serie ad ogni linea dell'interfaccia SPI, del valore di
120ohm, in prossimit della scheda di sviluppo.

386

Impostazioni software
Gli esempi proposti fanno uso delle librerie LaurTec, per cui necessario impostare i
percorsi di libreria riportati di seguito:
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\conf
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\inc
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\src

Per la libreria Microchip spi.h, necessario impostare il percorso:


[percorso radice]\Microchip\xc8\v1.21\include\plib

La versione della libreria inclusa nel percorso potrebbe variare in base a quella utilizzata.

Il protocollo SPI
Il protocollo SPI o meglio l'interfaccia SPI, venne originariamente ideata dalla
Motorola (ora Freescale) a supporto dei propri microprocessori e microcontrollori. A
differenza dello standard I2C ideato dalla Philips, linterfaccia SPI non stata mai
standardizzata; ciononostante divenuta di fatto uno standard. Il non avere delle regole
ufficiali ha portato allaggiunta di molte caratteristiche ed opzioni che vanno
opportunamente selezionate ed impostate al fine di permettere una corretta
comunicazione tra le varie periferiche interconnesse.
Linterfaccia SPI descrive una comunicazione singolo Master singolo Slave, ed di
tipo sincrono e full-duplex. Il clock viene trasmesso con una linea dedicata (non
necessariamente una trasmissione sincrona ha una linea dedicata al clock) ed possibile
sia trasmettere che ricevere dati in contemporanea. In Figura 137 riportato uno schema
base di collegamento tra due periferiche che fanno uso dellinterfaccia SPI.

Figura 137: Schema base di collegamento tra periferiche facenti uso dellinterfaccia SPI.

Dalla Figura 137 possibile subito notare quanto appena detto, ovvero che la
comunicazione avviene generalmente tra un Master ed uno Slave. Linterfaccia presenta
inoltre 4 linee di collegamento (esclusa la massa comunque necessaria), per cui lo standard
SPI anche noto come 4 Wire Interface. compito del Master avviare la comunicazione e
fornire il clock allo Slave. La nomenclatura delle varie linee presenti nellinterfaccia SPI
normalmente la seguente:

MOSI : Master Output Slave In


MISO : Master Input Slave Output
387

SCLK : Serial Clock (generato dal Master)


SS : Slave Select (Selezione dello Slave)

Oltre a questa nomenclatura, pi o meno standard, sono presenti altre sigle. Per
esempio i microcontrollori quali i PIC18 della Microchip o gli MSP430 della Texas
Instruments, fanno uso di una nomenclatura leggermente differente ma comunque
intuitiva.
Per esempio la linea MOSI viene anche nominata:

SDO (Serial Data Out), DO (Data Out), DOUT e SO (Serial Out)

La linea MISO viene anche nominata:

SDI (Serial Data In), DI (Data In), DIN e SI (Serial In)

La linea di Clock viene anche nominata:

CLK, SCK (Serial Clock), SCK.

La linea di Enable viene anche nominata:

CS (Chip Select), CE (Chip Enable)

Gi da questa prima spiegazione si capisce che non si ha un vero standard. Tornando


alla Figura 137 possibile notare anche la direzione di ogni linea dati. Sebbene sia il
Master a dover avviare la trasmissione dati, uno Slave potrebbe richiedere lavvio di una
comunicazione per mezzo di una linea dinterruzione (non prevista dallinterfaccia SPI). Il
Master, alla richiesta di una interruzione andrebbe a leggere la periferica.
Si noti che la comunicazione SPI prevede la presenza di un SS (Slave Select), per cui
anche se la comunicazione avviene tra un solo Master e un solo Slave, il Master pu
selezionare lo Slave con cui effettuare la comunicazione, sia per scrivere che leggere dati.
Qualora siano presenti pi Slave, il loro collegamento generalmente effettuato come in
Figura 138.
Come visibile si ha ancora un unico Master, che ha questa volta il compito di
selezionare lo Slave con il quale avviare la comunicazione; infatti un solo Slave alla volta
deve essere attivo. Questo collegamento possibile solo se la periferica Slave supporta
lopzione di avere la linea MISO di tipo three states o floating (alta impedenza). In
particolare lo stato di alta impedenza deve essere impostato ogni qual volta lo Slave risulti
disattivo. Questo richiesto poich tutti i MISO sono collegati in parallelo, e non cosa
buona avere uscite in parallelo che abbiano livelli logici differenti (si potrebbero avere dei
cortocircuiti che porterebbero alla rottura dei dispositivi stessi).

388

Figura 138: Collegamento di pi periferiche facendo uso dellinterfaccia SPI.

Unaltra osservazione da tenere a mente sia sulla Figura 137 che Figura 138 che la
linea SS attiva bassa. Seppur questo non uno standard, i microcontrollori della
Motorola vennero pensati con questa opzione.
Il segnale Slave Select (SS) non sempre presente nei microcontrollori che possiedono
linterfaccia SPI e per la sua implementazione necessario distinguere due casi:
Il microcontrollore impostato per lavorare come Master
Lo Slave Select deve essere opportunamente abilitato prima dellavvio della
comunicazione con lo Slave. Il suo corretto utilizzo pu cambiare a seconda della
modalit di trasmissione utilizzata (si veda di seguito). Frequentemente i
microcontrollori in modalit Master fanno uso di semplici I/O generici che
vengono assegnati uno per periferica.
Il microcontrollore impostato per lavorare come Slave
Lo Slave Select ha lo scopo di abilitare il microcontrollore al fine di permettergli
una corretta comunicazione. Quando il segnale SS non presente si fa spesso uso di
un ingresso con interruzione, in maniera da poter prontamente individuare la
richiesta del Master di avviare la comunicazione. Si ricorda che il segnale SS, qualora
pi Slave dovessero condividere la linea MISO, quando disabilitato, dovrebbe
portare luscita MISO dello Slave in uno stato di alta impedenza.

Linterfaccia SPI e la sua implementazione


Un modulo di un microcontrollore che implementi linterfaccia SPI si presenta come
un registro a scorrimento (Shift Register) sia in una periferica Master che Slave. La
connessione tra Master e Slave, mettendo in evidenza i registri a scorrimento, si presenta
come in Figura 139.
389

Figura 139: Connessione tra Master e Slave mettendo in evidenza i registri a scorrimento interni.

La rappresentazione per mezzo di un solo registro a scorrimento, seppur semplice,


rappresenta in maniera piuttosto realistica linterfaccia SPI. La dimensione del registro a
scorrimento non specificata, ma spesso la sua dimensione di un solo byte. In
microcontrollori a 16 e 32 bit, per la dimensione stessa dei registri interni al
microcontrollore, si pu spesso avere lopzione di selezionare la dimensione del registro,
usando le dimensioni comuni 8, 16, 32 (alcune volte si trova anche il formato a 7 bit ma
questa unopzione raramente usata).
Vediamo ora di analizzare come avviene la trasmissione di un byte. Prima di avviare
una comunicazione, il Master attiva la linea SS relativa allo Slave con cui vuole effettuare
la comunicazione e successivamente fornisce il clock alla frequenza con cui avverr la
trasmissione. Dopo lattivazione dello Slave, i bit interni al registro a scorrimento del
Master vengono traslati allesterno (linea MOSI) a partire dal bit pi significativo MSB
(Main Significant Bit). Il bit traslato entra nel registro dello Slave, il quale a sua volta inizia a
svuotare il proprio registro inviando il bit pi significativo attraverso la linea MISO. La
comunicazione termina quando lottavo bit viene trasmesso.
Da quanto detto si capisce che se il Master volesse leggere dallo Slave deve comunque
inviare un dato fittizio allo Slave. Allo stesso modo, se il Master volesse solo impostare lo
Slave o inviare solo Dati, ricever comunque dei dati fittizi dallo Slave, a meno di ignorare
e non collegare la linea MISO.
Rispetto a quanto appena detto ci sono naturalmente delle eccezioni, in particolare la
linea SS potrebbe non essere usata qualora nel sistema si abbia un solo Slave. In alcune
periferiche, quali per esempio gli ADC (Analog to Digital Converter), la linea SS quasi
sempre obbligatoria, visto che oltre a rappresentare la linea Slave Select, svolge spesso la
mansione di avvio conversione (Start Conversion). In situazioni come queste potrebbe
essere necessario ritardare il Clock prima di poter effettivamente leggere il risultato della
conversione.
Leventuale mancanza della linea SS, nel caso dei microcontrollori MSP430, viene
chiamata 3 Wire communication, ovvero comunicazione a 3 fili, che si contrappone al fatto
che linterfaccia SPI nota come 4 Wire communication. Questa nomenclatura facile per
confonderla con la pi nota 3 Wire communication in cui la linea SS presente ma le linee
MOSI e MISO sono unite in una unica linea per formare una comunicazione half-duplex.
Questo causa una diminuzione delle informazioni che possono viaggiare nel sistema, vista
la natura half-duplex del bus. Solo a scopo di completezza si ricorda che esiste anche la
comunicazione 1 Wire, ideata dalla Dallas-Maxim, ma che non ha quasi nulla in comune
con linterfaccia SPI, se non per il fatto di essere un protocollo seriale.
Per complicare un attimo la situazione bene tenere a mente che molti
390

microcontrollori hanno lopzione di trasmettere il byte in uscita non a partire dal bit pi
significativo ma da quello meno significativo. Come ogni opzione utilizzata si deve avere
la premura di controllare che lo Slave sia compatibile con tale opzione. La pratica di
trasmettere il bit dal meno significativo a quello pi significativo viene normalmente
utilizzata nella comunicazione 3 Wire (half-duplex).
Come detto la rappresentazione con un solo registro a scorrimento non una
semplificazione a solo scopo didattico, infatti viene spesso utilizzata. Ciononostante molti
microcontrollori offrono anche un registro a scorrimento per direzione di trasmissione,
come anche un buffer dati al fine da poter caricare il dato ricevuto e permettere una
nuova ricezione limitando eventuali perdite d'informazione. Il modulo SPI spesso
supportato da interruzioni in maniera da poter armonizzare lutilizzo dello stesso con il
resto delle attivit della CPU.
Si noti che quanto descritto non fa riferimento ad alcun protocollo o formattazione dei
dati. Il byte trasmesso un semplice byte e il suo significato dipender dallapplicazione.
Questo in contrapposizione con standard come lI2C e CAN, in cui i vari byte possiedono
un determinato significato. La mancanza di un protocollo fa dellinterfaccia SPI una
soluzione semplice da implementare anche senza lausilio di un modulo dedicato. Daltro
lato la mancanza di un protocollo di supervisione, impedisce allinterfaccia SPI di sapere
se il Master effettivamente collegato ad uno Slave; questo nello Standard I2C
implementato per mezzo del meccanismo di Acknowledgment. Per mezzo dellinterfaccia
SPI comunque possibile controllare la presenza di uno Slave, ma necessario
implementarlo, per esempio creando un byte domanda del tipoci sei?
Una volta compreso come realizzato un modulo con interfaccia SPI, possibile
comprendere il terzo modo con cui si possono collegare tra loro pi Slave. In Figura 140
riportato il cosiddetto collegamento in daisy chain degli Slave.

Figura 140: Collegamento in daisy chain di tre Slave.

Dalla Figura 140 si capisce che la catena di Slave ottenuta collegando luscita MISO
del primo Slave allingresso MOSI del Secondo Slave e cosi via. Lultimo Slave avr il
MISO collegato effettivamente al Master. La catena di Slave viene attivata da una sola
391

linea SS, quindi si ha la possibilit di risparmiare diversi pin altrimenti necessari per ogni
singolo Slave. Per comunicare con un dispositivo necessario questa volta inviare sempre
tre byte (uno per periferica) anche se il dispositivo con cui si vuole colloquiare il primo
(in lettura obbligatorio, mentre in sola scrittura si potrebbe evitare). Questa
configurazione, seppur attraente non permette un numero indefinito di periferiche. A
seconda della frequenza del segnale di clock e dalla tipologia delle periferiche utilizzate
potrebbe essere necessario dover rispettare dei tempi minimi e massimi di lettura o
scrittura oltre i quali non possibile andare.
La tecnica daisy chain la stessa utilizzata dallinterfaccia JTAG (Joint Test Action Group)
molto simile allinterfaccia SPI, ovvero basata su registri a scorrimento. Linterfaccia
JTAG divenuta uno standard per simulare circuiti integrati montati su sistemi complessi,
ma viene sempre pi frequentemente utilizzata anche per programmare i dispositivi quali
microcontrollori, FPGA e CPLD.

Modalit di trasmissione SPI


Da quanto fin ora letto, ci si sar resi conto che linterfaccia SPI non molto
complicata. Proprio per tale ragione riscuote un certo successo. Unica sua difficolt sta
nel fatto che non presente uno standard, per cui bisogna fare attenzione alle
impostazioni del Master e dello Slave. In aggiunta a quanto detto sopra, linterfaccia SPI
pu essere impostata per trasmettere o ricevere in quattro modalit differenti. La modalit
selezionata deve essere la stessa sia per il Master che per lo Slave. Dal momento che lo
Slave spesso non ha modo di cambiare modalit, il Master a doversi adeguare.
Le quattro modalit appartengono in un certo qual modo allo standard SPI, visto che
sono descritte nei Datasheet della Motorola (Freescale).
Le quattro modalit vengono normalmente impostate per mezzo di due parametri (spesso
implementati con due bit), nominati CPOL (Clock Polarity) e CPHA (Clock Phase). La
polarit consiste semplicemente nellavere o meno una porta NOT sulla linea di Clock,
ovvero quando CPOL = 0 si ha il clock normale, mentre quando CPOL=1 si ha il
clock invertito. Questo comporta che tutte le operazioni che avvengono sul fronte di
salita, qualora CPOL sia impostato su 1, avverranno sul fronte di discesa. Allo stesso
modo, le operazioni che avvengono normalmente su fronte di discesa, impostando
CPOL=1, avverranno sul fronte di salita.
Il parametro CPHA, permette dimpostare la fase del campionamento, ovvero quando i
dati devono essere letti (campionati). In Tabella 23 riportata la corrispondenza tra i
valori dei parametri CPOL, CPHA e la relativa modalit associatagli.
Modalit

CPOL

CPHA

Tabella 23 : Corrispondenza tra modalit e parametri CPOL, CPHA.

Si fa presente che alcuni microcontrollori, come per esempio i PIC18 e MSP430 hanno
il significato di CPHA invertito. Quando uno Slave richiede quindi la modalit 0 secondo
lo standard Freescale CPOL=0 e CPHA=0, per alcuni microcontrollori potrebbe essere
392

CPOL=0 e CPHA=1. Frequentemente il nome dei bit non lo stesso per cui bisogna
prestare ulteriore attenzione. Vediamo in maggior dettaglio le varie modalit.
In Figura 141 riportato il diagramma temporale nel caso in cui CPHA=1.

Figura 141: Diagramma temporale con il parametro CPHA=1.

Si noti che il caso CPOL=0 e CPOL=1 sono inclusi nello stesso diagramma e
comporta, come detto, solo linversione dei fronti sul quale avviene una determinata
operazione. Nella trattazione che segue si considera il caso CPOL=0 (modalit 1). La
trasmissione dei dati avviene secondo la seguente sequenza:

Il Master attiva lo Slave Select.


Il Master attiva il Clock.
Sul fronte di salita del clock il bit MSB dei registri a scorrimento interni, viene
posto sulla linea MOSI (dal Master) e sulla linea MISO (dallo Slave).
Sul fronte di discesa il Master e lo Slave campionano (sampling) il bit e lo traslano
nel registro interno.
Sul nuovo fronte di salita il nuovo bit viene posto sulle linee MOSI e MISO.
Il discorso continua fino allultimo bit.

In questa modalit e nella 3 (CPOL=1, CPHA=1) la linea Slave Select rimane attiva per
tutta la trasmissione. Se ulteriori byte devono essere inviati, la linea SS pu rimanere
ininterrottamente attiva, cosa non vera se CPHA = 0. Questo significa che moduli con
interfaccia SPI con registri a 8 bit, possono leggere e scrivere periferiche che hanno
registri a scorrimento 16 bit (come anche 3 o pi byte), ma solo se si fa uso della modalit
1 o 3. In Figura 142 riportato il caso in cui CPHA sia uguale a 0. Come nellanalisi
precedente, il caso CPOL=0 e CPOL=1 sono inclusi nello stesso diagramma temporale.

393

Figura 142: Diagramma temporale con il parametro CPHA=0.

Come per la Figura 141, anche in questo caso si analizza il caso CPOL=0 ovvero la
modalit 0.

Il Master attiva lo Slave Select.


Sul fronte di discesa della linea SS il bit MSB dei registri a scorrimento interni,
viene rispettivamente messo sulla linea MOSI (dal Master) e MISO (dallo Slave).
Il master attiva il Clock.
Sul fronte di salita il Master e lo Slave leggono (campionano) il bit e lo traslano nel
registro interno.
Sul nuovo fronte di discesa il nuovo bit viene posto sulle linee MOSI e MISO.
Il discorso continua fino allultimo bit.

Per la modalit 0 e 2 necessario che ad ogni invio di byte la linea SS venga disattivata
e riattivata. Infatti il bit MSB viene posto rispettivamente sulle linee MOSI e MISO sul
fronte di discesa della linea stessa. In queste modalit si capisce che non si pu fare a
meno della linea SS e che Master con registri interni a 8 bit possono colloquiare solo con
Slave che hanno il registro interno di ugual dimensione.

Frequenza di trasmissione e livelli elettrici dellinterfaccia SPI


Arrivati a questo punto, dopo la carrellata di opzioni e accortezze vi sarete certamente
resi conto che in tutta questa descrizione, pur avendo detto che il Master deve fornire il
Clock, non ho fatto alcun cenno alla frequenza di trasmissione. Effettivamente,
diversamente da uno standard asincrono come il protocollo RS232, linterfaccia SPI non
ha una frequenza operativa prefissata o standard, ed in particolare lavorando sui fronti del
clock non nemmeno necessario che abbia valori particolari (per esempio non c un
valore minimo formale sotto il quale non si pu andare). Questa peculiarit discende dal
fatto che il clock fornito dal Master e lo Slave si adegua a questultimo. In uno Standard
asincrono come il protocollo RS232, dal momento che il clock non viene fornito,
necessario che Master e Slave lavorino ad una determinata frequenza nota a priori (escluso
il caso di utilizzo di algoritmi di auto baud rate).
Applicazioni con interfaccia SPI fanno uso di comunicazioni a frequenze che partono
da poche decine di KHz fino ad arrivare a decine di MHz (80MHz-100MHz). Il limite
394

massimo viene a dipendere dalle periferiche di cui si sta facendo uso. Per esempio molti
microcontrollori supportano frequenze massime fino a 10MHz. Per frequenze pi alte si
fa spesso uso di macchine a stati implementate dentro CPLD o FPGA, che permettono di
raggiungere le frequenze massime sopra citate, che per altro potrebbero anche essere
superate. Anche DSP di fascia alta permettono di superare il limite dei 10MHz che
caratterizza molti microcontrollori. Il limite massimo della frequenza in generale legato
alla frequenza di clock e dalla tecnologia con cui vengono realizzati gli integrati.
Linterfaccia SPI, oltre a non specificare la frequenza operativa non impone alcun
vincolo sulle tensioni che caratterizzano i livelli logici. Ciononostante si capisce che due
dispositivi al fine di comunicare tra loro debbano lavorare con gli stessi livelli logici,
altrimenti non si potrebbero capire. Qualora due dispositivi lavorino a tensioni diverse,
per esempio uno a 3.6V e uno a 5V, necessario far uso di traslatori di livello. Valori
tipici di tensione sono: 1.8V, 3.3V, 3.6V e 5V.

Applicazioni dellinterfaccia SPI


Sebbene linterfaccia SPI sembri un po disordinata a causa della mancanza di uno
standard, il suo utilizzo, una volta letto il Datasheet dello Slave al quale bisogna adeguarsi,
piuttosto facile, per cui utilizzata in molte applicazioni.
Lapplicazione madre stata quella di estendere gli ingressi e le uscite di un
microprocessore e microcontrollore, ma il suo utilizzo si subito esteso ad altre
applicazioni. Sistemi tipici che fanno uso dell'interfaccia SPI sono: Real Time Clock
Calendar, memorie RAM, memorie EEPROM, amplificatori a guadagno variabile,
controllori video, convertitori analogico digitali (ADC), convertitori Digitali Analogico
(DAC), display LCD Alfanumerici e grafici.
Nello sviluppo delle applicazioni, sebbene sia il Master a comandare la comunicazione,
come detto, in realt lo Slave ad imporre le regole. Lo Slave, di qualunque natura esso
sia, potr infatti comunicare con un Master in una determinata modalit che non pu
essere in generale cambiata. Per tale ragione, durante la scrittura del software si deve avere
la premura di impostare il Master (frequentemente un microcontrollore) al fine di poter
correttamente comunicare con lo Slave. Infatti il Master, diversamente dallo Slave ha la
possibilit di essere programmato al fine di potersi adattare allo Slave. Da questo discende
che in uno stesso Bus si potrebbero avere Slave che fanno uso di opzioni e modalit di
comunicazione differenti. Naturalmente nel caso di uno Salve realizzato con un
microcontrollore, si ha pi flessibilit.

Il protocollo I2C e SPI a confronto


Le applicazioni dellinterfaccia SPI sembrerebbero essere le stesse del protocollo I2C,
ed in effetti molte applicazioni potrebbero essere sviluppate usando luno o laltro
standard. Ciononostante ci sono dei pregi e difetti che possono rendere una soluzione
pi attraente dellaltra:
Vantaggi dellinterfaccia SPI
Linterfaccia SPI full-duplex.
Si possono raggiungere frequenze di trasmissione elevate.
Interfaccia ed utilizzo semplici (mancanza di un protocollo).
Facilit di isolare galvanicamente le linee unidirezionali.
Lunghezza arbitraria dei dati da trasmettere.
395

Svantaggi dellinterfaccia SPI


Utilizzo di un numero maggiore di linee.
Mancanza di un protocollo e conferma di trasmissione (alcune volte un
vantaggio).
Mancanza dindirizzamento delle periferiche (escluso segnale SS)
Alcune volte si legge che tra gli svantaggi dellinterfaccia SPI rispetto ad altri protocolli
(non solo il protocollo I2C), ci sia anche il fatto che non possa raggiungere elevate
distanze, questo non corretto. Sebbene linterfaccia SPI sia stata pensata per il
collegamento tra un Master e uno Slave a brevi distanze (stessa scheda), facendo uso di
driver possibile estendere la distanza alla pari di altri protocolli come per esempio il
protocollo RS232, RS485. Facendo uso dei driver RS232 e RS485, linterfaccia SPI
raggiunge le stesse distanze dei suddetti protocolli. In particolare il protocollo RS485
descrive il physical layer di una comunicazione, per cui questo pu essere adottato anche
per linterfaccia SPI, visto che non fa uso di uno specifico physical layer.

396

Descrizione del modulo SPI


Il PIC18F4550 come molti altri microcontrollori PIC18 possiede il modulo interno
MSSP, ovvero il Master Synchronous Serial Port. Tale modulo, come visto, oltre a poter
essere configurato come modulo I2C, pu essere configurato anche come modulo SPI.
Le due modalit sono mutuamente esclusive, ovvero o si imposta il modulo come I2C o
come SPI. Qualora si voglia supportare entrambi necessario scegliere un
microcontrollore con due moduli MSSP o implementare un protocollo via software 165. In
Figura 143 riportato il modulo MSSP in configurazione SPI.

Figura 143: Modulo MSSP in configurazione SPI.

Si pu notare la presenza del registro a scorrimento SSPSR e la presenza del buffer


SSPBUF. Il modulo MSSP supporta l'interfaccia SPI in tutte e quattro le modalit
descritte ed in particolare supporta sia la modalit a 3 che 4 linee. La linea SS pu essere
disattivata qualora non sia utilizzata. La linea SDI per l'ingresso dei dati, possiede un
buffer con Trigger di Schmitt al fine di rimuovere disturbi sulla linea e rendere dunque la
165

Qualora ci si trovi ad implementare un protocollo via software si preferisce in generale implementare l'interfaccia SPI
piuttosto che il protocollo I2C, che in generale richiede maggior attenzioni.

397

trasmissione dei dati pi affidabile.


Il modulo SPI pu essere configurato sia in modalit Master che Slave per cui la linea
SCK deve essere impostata come Output o Input a seconda dei casi. In particolare nel
caso del Master la linea SCK deve essere impostata come Output, visto che il clock viene
fornito dal Master. Nel caso dello Slave la linea SCK deve essere invece impostata come
Input. Per questa semplice differenza, il Master e lo Slave hanno un comportamento
diverso in caso il microcontrollore sia in stato di SLEEP. Nel caso del Master, entrando in
SLEEP viene interrotta la comunicazione, che riprende dal punto interrotto nel momento
in cui il microcontrollore si risveglia. Nel caso dello Slave il modulo, anche se il
microcontrollore in stato di SLEEP e i clock interni sono disattivi, pu ricevere
comunque dati e generare un Interrupt all'arrivo di un byte. Questo discende dal fatto che
nel caso della modalit Slave, il clock viene fornito dal Master.
Alla ricezione di ogni byte, ovvero quando il byte ricevuto viene trasferito dal registro
SSPSR al registro SSPBUF, viene settato il Flag SSPIF del registro PIR1. Nel caso in cui
le interruzioni siano attivate, viene generata un'interruzione con la relativa priorit
impostata.

Registri di configurazione del modulo SPI


I registri interni al PIC18F4550 per la configurazione del modulo MSSP in modalit
SPI sono:

Nome
INTCON

SSPCON1
SSPSTAT
SSPBUF
SSPSR (non accessibile direttamente)
BIT7

BIT6

BIT5

BIT4

BIT3

BIT2

BIT1

BIT0

GIE/GIEH

PEIE/GIEL

TMR0IE

INT0IE

RBIE

TMR0IF

INT0IF

RBIF

PIR1

SPPIF

ADIF

RCIF

TXIF

SSPIF

CCP1IF

TMR2IF

TMR1IF

PIE1

SSPPIE

ADIE

RCIF

TXIF

SSPIE

CCP1IE

TMER2IE

TMR1IE

IPR1

SPPIP

ADIP

RCIP

TXIP

SSPIP

CCP1IP

TMR2IP

TMR1IP

TRISA

TRISA7

TRISA6

TRISA5

TRISA4

TRISA3

TRISA2

TRISA1

TRISA0

TRISB

TRISB7

TRISB6

TRISB5

TRISB4

TRISB3

TRISB2

TRISB1

TRISB0

TRISB

TRISC7

TRISC6

TRISC2

TRISC1

TRISC0

SSPBUF

Buffer di ricezione e trasmissione del modulo MSSP

SSPCON1

WCOL

SSPOV

SSPEN

CKP

SSPM3

SSPM2

SSPM1

SSPM0

SSPSTAT

SMP

CKE

D/A'

R/W'

UA

BF

Il contenuto dei registri presentati nel Capitolo sono forniti per semplificare la
comprensione delle librerie e del programma ma non rappresentano una fonte
alternativa al Datasheet. A seconda del modello del PIC utilizzato i registri
descritti potrebbero essere differenti o avere bit con significato differente.

398

Configurazione dei pin


Al fine di utilizzare il modulo SPI necessario impostare come ingressi o uscite i pin
associati alle linee SDI, SDO, SCK, SS

La linea RB0 rappresenta la linea SDI del modulo SPI.


La linea RB1 rappresenta la linea SCK del modulo SPI.
La linea RC7 rappresenta la linea SDO del modulo SPI.
La linea RA5 rappresenta la linea SS del modulo SPI.

Prima di abilitare il modulo e poter avviare la comunicazione bisogna propriamente


impostare le linee di comunicazione. Bisogna in particolare distinguere due casi, ovvero il
caso Master e Slave:
Master
Impostare la linea RB0 come ingresso (disabilitare l'ingresso analogico).
Impostare la linea RB0 come uscita (disabilitare l'ingresso analogico).
Impostare la linea RC7 come uscita.
Impostare la linea RA5 come uscita (disabilitare l'ingresso analogico).
Slave

Impostare la linea RB0 come ingresso (disabilitare l'ingresso analogico).


Impostare la linea RB0 come ingresso (disabilitare l'ingresso analogico).
Impostare la linea RC7 come uscita.
Impostare la linea RA5 come ingresso (disabilitare l'ingresso analogico).

In particolare tra i due diversi casi, le linee SCK e SS assumono un valore diverso, vista la
differente funzione delle linee.
La configurazione dei pin riportata valida solo per il PIC18F4550 e per la
maggior parte dei PIC18 con modulo USB integrato. La posizione delle linee
potrebbe cambiare a seconda del PIC utilizzato.

399

Registro SSPSTAT: MSSP Status Register


R/W-0

R/W-0

R-0

R-0

R-0

R-0

R-0

R-0

SMP

CKE

D/A

R/W

UA

BF

Bit 7

Bit 6

Bit 5

Bit 4

Bit 3

Bit 2

Bit 1

Bit 0

Leggenda
R = Readable bit
-n = Value at POR
Bit 7

W = Writable bit
1 = Bit is set

U = Unimplemented bit read as 0


0 = Bit is cleared

S : Settable bit
x = Bit is unknown

SMP : Slew rate Control bit


Modalit SPI Master
1 : Data In campionato alla fine di output time
0 : Data In campionato al centro di output time
Modalit SPI Slave
Il bit deve essere posto a 0 quando si in modalit Slave.

Bit 6

CKE : Selezione del Clock del modulo SPI


1 : La trasmissione avviene alla transizione del clock da Active ad Idle
0 : La trasmissione avviene alla transizione del clock da Idle ad Active

Bit 5

D/A : Data/Address bit


Utilizzato solo in modalit I2C.

Bit 4

P : Stop bit
Utilizzato solo in modalit I2C.

Bit 3

S : Start bit
Utilizzato solo in modalit I2C.

Bit 2

R/W : Read/Write Information bit


Utilizzato solo in modalit I2C.

Bit 1

UA : Update Address (10Bit mode only)


Utilizzato solo in modalit I2C.

Bit 0

BF : Buffer Full Status Bit


In Modalit TX
1 : SSPBUF pieno, ricezione completata.
0 : SSPBUF vuoto, ricezione non completa.

400

Registro SSPCON1: MSSP Control Register


R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

WCOL

SSPOV

SSPEN

CKP

SSPM3

SSPM2

SSPM1

SSPM0

Bit 7

Bit 6

Bit 5

Bit 4

Bit 3

Bit 2

Bit 1

Bit 0

Leggenda
R = Readable bit
-n = Value at POR
Bit 7

W = Writable bit
1 = Bit is set

U = Unimplemented bit read as 0


0 = Bit is cleared

S : Settable bit
x = Bit is unknown

WCOL : Write Collision Detect Bit (solo in trasmissione)


1 : SSBUF scritto prima che la trasmissione del byte precedente terminata. Deve essere resettato via Software,
0 : Nessuna Collisione.

Bit 6

SSPOV : Receive Overflow Indicator bit


Modalit SPI Slave
1 : Un byte stato ricevuto mentre SSBUF conteneva ancora il vecchio byte. Il Byte in ingresso deve essere sempre letto anche se il
Master deve solo inviare dati.
0 : Nessun Overflow.

Bit 5

SSPEN : Master Synchronous Port Enable bit


1: Abilita la porta seriale, le line SCK, SDO, SDI, SS devono essere opportunamente impostate.
0: Disabilita la porta seriale.

Bit 4

CKP : Selezione della polarit del Clock


1: Lo stato Idle equivale al livello alto del clock.
0: Lo stato Idle equivale al livello basso del clock.

Bit 3-0

SSMP3:SSPM0 : Master Synchronous Serial Port Mode Select bits


0101: Modalit SPI Slave, SS pin disabilitato.
0100: Modalit SPI Slave, SS pin abilitato.
0011: Modalit SPI Master, clock = TMR2 out/2.
0010: Modalit SPI Master, clock = Fosc/64.
0001: Modalit SPI Master, clock = Fosc/16.
0000: Modalit SPI Master, clock = Fosc/4.

401

Libreria per l'utilizzo del modulo SPI


Nonostante abbia detto che l'interfaccia SPI sia un modo semplice per comunicare con
delle periferiche per mezzo delle due sole linee SDI e SDO, SCK, SS, impostare ogni
registro del microcontrollore pu risultare laborioso. Per questa ragione l'utilizzo di
librerie gi collaudate pu tornare utile, per cui si spiegher come impostare il modulo per
mezzo della libreria della Microchip. Il file da includere per la libreria del modulo SPI
spi.h. All'interno di questa libreria sono presenti le funzioni per aprire e chiudere il
modulo e controllore i vari stati del modulo stesso. La libreria supporta anche i PIC con
pi moduli SPI, per cui il nome delle funzioni descritte pu variare in base al numero del
modulo che si sta utilizzando. Nel caso sia presente un solo modulo vale la nomenclatura
delle funzioni sotto riportate.
La libreria Microchip supporta altre funzioni non descritte nel seguente
paragrafo. Per maggiori dettagli possibile far riferimento alla documentazione
ufficiale presente nella directory docs, che possibile trovare nella directory
principale del compilatore XC8.
Vediamo le funzioni e Macro principali che sono presenti nella libreria spi.h. La x del
nome della funzione va sostituita con il numero del modulo utilizzato. Sono infatti
presenti PIC con pi moduli SPI. In particolare se il PIC possiede un solo modulo SPI il
nome della funzione OpenSPI.
Funzione

Descrizione

void OpenSPIx (unsigned char, unsigned char, unsigned Apre il modulo SPIx.
char )
void CloseSPIx (void)

Chiude il modulo SPIx.

unsigned char WriteSPIx (unsigned char)

Invia un byte sul buffer SSPBUF.

unsigned char ReadSPIx ( void )

Legge il buffer SSPBUF.

Tabella 24: Funzioni e Macro disponibili nella libreria I2C.

void OpenSPI1( unsigned char sync_mode, unsigned char bus_mode, unsigned


char smp_phase)

Questa funzione deve essere eseguita prima di utilizzare qualunque funzione della
libreria SPI, infatti permette di attivare il modulo per un suo corretto utilizzo. Tale
funzione potrebbe non essere richiamata solo nel caso in cui la libreria del
dispositivo SPI utilizzata, fornisca un'altra funzione per attivare ed impostare il
modulo SPI.
Parametri:
sync_mode: Contiene i parametri utilizzati per la configurazione del registro
SSPCON1.
SPI_FOSC_4
402

SPI_FOSC_16
SPI_FOSC_64
SPI_FOSC_TMR2
SLV_SSON
SLV_SSOFF
bus_mode: Modalit del modulo SPI
MODE_00
MODE_01
MODE_10
MODE_11
smp_phase: Delezione del campionamento
SMPEND
SMPMID
Restituisce:
void
void CloseSPIx (void)

Questa funzione deve essere eseguita per richiudere il modulo SPI. Tale funzione
potrebbe non essere mai richiamata all'interno del programma, qualora non si avesse
mai l'esigenza di chiudere il modulo.
Parametri:
void
Restituisce:
void
unsigned char WriteSPIx (unsigned char data_out)

Questa funzione permette di inviare i dati scrivendo direttamente sul buffer


SSPBUF.
Parametri:
data_out: Byte da inviare sul bus.
Restituisce:

403

0: Nessuna collisione in trasmissione.


-1: Collisione durante la scrittura del byte.
unsigned char ReadSPIx (void)

La funzione permette di leggere un singolo byte dal buffer. La libreria mette


anche a disposizioni funzioni per leggere byte multipli, ma altro non sono che
letture singole del registro SSPBUF.
Parametri:
void
Restituisce:
Byte letto dal registro SSPBUF.

Si fa in ultimo presente che generalmente il modulo SPI mutuamente esclusivo con il


modulo I2C.
Se si controlla l'implementazione delle funzioni dedicate al modulo SPI, ci si
accorge che sono presenti diversi loop bloccanti, ovvero che prevengono
l'esecuzione del software qualora non si verifichi un determinato evento. Questo,
spesso causa di blocchi Software soprattutto alla scrittura dei primi programmi
di esempio.

Esempio Master - Slave, comunicazione tra due PIC


Per poter comprendere l'utilizzo delle funzioni di libreria e del protocollo SPI bene
vedere un esempio in cui si utilizzano le varie funzioni effettuando una comunicazione tra
due PIC18. In particolare un PIC18 deve essere programmato come Master mentre il
secondo deve essere programmato come Slave. La linea SDO del master deve essere
collegata alla linea SDI dello Slave e viceversa, mentre le linee di clock possono essere
collegate tra loro.
Nel caso si utilizzi Freedom II con versione inferiore a 4 necessario togliere
l'integrato MAX232 dalla scheda, al fine di evitare conflitti sulla linea RC7. Nel
caso di linee di trasmissione superiori a 10cm bene prevedere una resistenza in
serie da 120ohm su ogni linea dell'interfaccia SPI. Per lunghezze superiori a 2030cm bene prevedere anche protezioni ESD e ulteriori filtri EMI.

404

Programma per il Master


L'applicazione dal lato del Master legge in continuazione i pulsanti collegati al
microcontrollore ed invia il valore del tasto premuto al microcontrollore Slave.
#include <xc.h>
#include <spi.h>
#include "PIC18F4550_config.h"
#define BUTTON_MASK

0xF0

#define
#define
#define
#define

BUTTON_1
BUTTON_2
BUTTON_3
BUTTON_4

0x01
0x02
0x04
0x08

#define
#define
#define
#define

BUTTON_1_PRESSED
BUTTON_2_PRESSED
BUTTON_3_PRESSED
BUTTON_4_PRESSED

0b11100000
0b11010000
0b10110000
0b01110000

//*************************************
//
Prototipi delle funzioni
//*************************************
void board_initialization (void);
void write_data (unsigned char data);
//*************************************
//
SPI Master
//*************************************
int main(void) {
board_initialization ();
//Inizializzazione del modulo SPI
//Baudrate Fosc/16 @20MHz -> 312.5 KHz
OpenSPI(SPI_FOSC_64,MODE_01,SMPMID);
//Controllo della pressione del pulsante
while (1) {
switch (PORTB & BUTTON_MASK) {
case BUTTON_1_PRESSED : write_data (BUTTON_1);
LATD = 0x00;
break;
case BUTTON_2_PRESSED : write_data (BUTTON_2);
break;
case BUTTON_3_PRESSED : write_data (BUTTON_3);
break;
case BUTTON_4_PRESSED : write_data (BUTTON_4);
break;
}

405

//*************************************
//
Implementazione della funzione
//*************************************
void board_initialization (void) {
// Imposto PORTA
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB
LATB = 0x00;
TRISB = 0b11111101;
// Imposto PORTC
LATC = 0x00;
TRISC = 0b01111111;
// Imposto PORTD
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE
LATE = 0x00;
TRISE = 0xFF;
// Tutte le porte Analogiche sono impostate come I/O
ADCON1 = 0x0F;

//Attivo i restitori di pull-up su PORTB


INTCON2bits.RBPU = 0;

//*************************************
//
Implementazione della funzione
//*************************************
void write_data (unsigned char data){
unsigned char dummy_read;
WriteSPI(data);
dummy_read = ReadSPI ();
}

Programma per lo Slave


L'applicazione dal lato del Slave legge in continuazione i dati ricevuti sul Bus, i quali
contengono il valore del pulsante premuto.
#include <xc.h>
#include <spi.h>
#include "PIC18F4550_config.h"
#define BUTTON_MASK
#define BUTTON_1
#define BUTTON_2
#define BUTTON_3

0xF0

0x01
0x02
0x04

406

#define BUTTON_4
#define
#define
#define
#define

0x08

BUTTON_1_PRESSED
BUTTON_2_PRESSED
BUTTON_3_PRESSED
BUTTON_4_PRESSED

0b11100000
0b11010000
0b10110000
0b01110000

//*************************************
//
Prototipi delle funzioni
//*************************************
void board_initialization (void);
//*************************************
//
SPI Slave
//*************************************
int main(void) {
board_initialization ();
//Inizializzazione del modulo SPI
//Baudrate Fosc/16 @20MHz -> 312.5 KHz
OpenSPI (SLV_SSOFF,MODE_01,SMPMID);
//Controllo della pressione del pulsante
while (1) {
LATD = ReadSPI();
}
}
//*************************************
//
Implementazione della funzione
//*************************************
void board_initialization (void) {
// Imposto PORTA
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB
LATB = 0x00;
TRISB = 0b11111111;
// Imposto PORTC
LATC = 0x00;
TRISC = 0b01111111;
// Imposto PORTD
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE
LATE = 0x00;
TRISE = 0xFF;
// Tutte le porte Analogiche sono impostate come I/O
ADCON1 = 0x0F;

407

//Attivo i restitori di pull-up su PORTB


INTCON2bits.RBPU = 0;

Progetto 5 Generatore di tensione di riferimento


Specifiche del progetto
Si realizzi un generatore di tensione di riferimento con precisione 1mV per mezzo del
DAC166 MCP4822. Le tensioni di riferimento in uscita devono essere selezionate per
mezzo di 3 pulsanti e devono essere:

1.5V
1.8V
3.3V

Per mezzo di un quarto pulsante si deve porre l'uscita in alta impedenza, ovvero spegnere
l'uscita. Reprimendo il pulsante, l'uscita deve essere riposta all'ultimo valore di tensione
prima dello spegnimento dell'uscita.
Tra un'accensione ed un altra si preveda un ritardo di un 1 secondo.
Lo stato dell'uscita deve essere segnalato dall'accensione e spegnimento di un LED.

Sviluppo del progetto


#include <xc.h>
#include "PIC18F4550_config.h"
#include "MCP4822.h"
#include "MCP4822.c"
#include "delay.h"
#include "delay.c"
//*************************************
//
Riferimenti di tensione
//*************************************
#define
#define
#define
#define

REFERENCE_0000
REFERENCE_1500
REFERENCE_1800
REFERENCE_3300

0
1500
1800
3300

//*************************************
//
Costanti
//*************************************
#define STATUS_ON 0x01
166

Il DAC, ovvero Digital to Analog Converter, permette di convertire un segnale digitale in uno analogico. In particolare in
base al numero di bit (risoluzione) e il valore di riferimento usato, permette di controllare il livello analogico con un valore di
precisione pari al valore di riferimento diviso il numero di sequenze numeriche supportate (2 RISOLUZIONE). Il passo minimo
viene definito quanto.

408

#define STATUS_OFF 0x00


#define STATUS_LED LATDbits.LATD0
#define WAIT_TIME 1000
#define DEBOUNCE_TIME 10
//*************************************
//
Prototipi delle funzioni
//*************************************
void board_initialization (void);
int main (void) {
unsigned char system_status = STATUS_OFF;
unsigned int last_voltage = REFERENCE_0000;
board_initialization ();
MCP4822_initialize(MCP4822_CLOCK_FOSC_64);
STATUS_LED = STATUS_OFF;
while (1) {
//*************************************
//
Pulsante BT1
//*************************************
if (PORTBbits.RB4 == 0) {
delay_ms (DEBOUNCE_TIME);

if (PORTBbits.RB4 == 0) {
MCP4822_set_amplitude (REFERENCE_1500, DAC_A);
last_voltage = REFERENCE_1500;
}

//*************************************
//
Pulsante BT2
//*************************************
if (PORTBbits.RB5 == 0) {
delay_ms (DEBOUNCE_TIME);

if (PORTBbits.RB5 == 0) {
MCP4822_set_amplitude (REFERENCE_1800, DAC_A);
last_voltage = REFERENCE_1800;
}

//*************************************
//
Pulsante BT3
//*************************************
if (PORTBbits.RB6 == 0){
delay_ms (DEBOUNCE_TIME);

409

if (PORTBbits.RB6 == 0){
MCP4822_set_amplitude (REFERENCE_3300, DAC_A);
last_voltage = REFERENCE_3300;
}
}
//*************************************
//
Pulsante BT4
//*************************************
if (PORTBbits.RB7 == 0 && system_status == STATUS_ON){
delay_ms (DEBOUNCE_TIME);
if (PORTBbits.RB7 == 0){
MCP4822_set_options (MCP4822_GAIN_2, MCP4822_SHUTDOWN_ON,
MCP4822_DAC_A);
MCP4822_set_amplitude (REFERENCE_0000, DAC_A);
STATUS_LED = STATUS_OFF;
system_status = STATUS_OFF;
delay_ms (1000);
}

if (PORTBbits.RB7 == 0 && system_status == STATUS_OFF){


delay_ms (DEBOUNCE_TIME);
if (PORTBbits.RB7 == 0 ){
MCP4822_set_options (MCP4822_GAIN_2, MCP4822_SHUTDOWN_OFF,
MCP4822_DAC_A);
set_amplitude_MCP4822 (last_voltage, DAC_A);

STATUS_LED = STATUS_ON;
system_status = STATUS_ON;
delay_ms (1000);

}
}

//*************************************
//
Implementazione della funzione
//*************************************
void board_initialization (void) {
// Imposto PORTA
LATA = 0x00;
TRISA = 0xFF;
// Imposto PORTB
LATB = 0x00;
TRISB = 0xFF;
// Imposto PORTC
LATC = 0x00;
TRISC = 0xFF;

410

// Imposto PORTD
LATD = 0x00;
TRISD = 0x00;
// Imposto PORTE
LATE = 0x00;
TRISE = 0xFF;
// Tutte le porte Analogiche sono impostate come I/O
ADCON1 = 0x0F;
//Attivo i resistori di pull-up su PORTB
INTCON2bits.RBPU = 0;
}

Analisi del progetto


Le specifiche richiedono l'utilizzo del DAC MCP4822, il quale, non essendo presente
sulle schede di sviluppo, deve essere montato esternamente. Il DAC MCP4822 possiede
due uscite, ma con una sola si riesce a svolgere il compito richiesto.
Si osservi che le varie porte del microcontrollore non sono impostate visto che si fa uso
della libreria MCP4822, la quale imposta propriamente i vari pin necessari per l'interfaccia
SPI.

Riassunto
In questo Capitolo abbiamo visto il funzionamento del modulo SPI, mostrando come
con i due soli registri SSPCON1 e SSPSTAT sia possibile configurare un modulo seriale
di particolare importanza. L'interfaccia SPI si presta infatti alla configurazioni di molti
dispositivi complessi come ADC, DAC ma anche per leggere e scrivere memorie Flash,
EEPROM. Il Capitolo ha anche mostrato come poter utilizzare l'interfaccia SPI per poter
mettere in comunicazione due microcontrollori ed utilizzare il DAC a doppia uscita
MCP4822.

Punti chiave ed approfondimenti


Interfaccia SPI: L'interfaccia I2C uno standard seriale ideato per risolvere le
problematiche di comunicazione tra periferiche e microprocessore, evitando le numerose
linee di comunicazione tipiche dell'Address Bus e del Data Bus. In particolare lo scopo dello
standard anche quello di mantenere lo stesso particolarmente semplice, se paragonato al
protocollo I2C.
Periferica Master: Nel protocollo SPI una periferica viene definita Master se pu iniziare
una comunicazione sul Bus. Il Master fornisce anche il clock per lo Slave.
Periferica Slave: Nel protocollo SPI una periferica viene definita Slave se pu iniziare
una comunicazione sul Bus solo previa interrogazione da un Master. Uno Slave potrebbe
411

richiedere ad un Master di essere interrogato, facendo uso di linee interruzioni dedicate.


Le linee delle interruzioni non fanno parte dello standard e rappresentano solo una
soluzione per rendere una periferica Slave pi intelligente.

Domande ed Esercizi
1.
2.
3.
4.
5.
6.
7.
8.

Quali sono le ragioni per cui stato introdotto il protocollo seriale SPI?
Quali sono le differenze tra il protocollo SPI e il protocollo I2C?
Quali sono le massime velocit raggiungibili dal protocollo SPI?
Quali sono le massime distanze raggiungibili dal protocollo SPI?
Che cos' un DAC?
Modificare l'esempio Master e Slave presentato, permettendo la visualizzazione da parte del
Master dei pulsanti premuti dallo Slave.
Modificare il Progetto, facendo uso delle interruzioni per il controllo dei tasti premuti.
Modificare il Progetto presentato inserendo altri tre pulsanti per il DAC_B, estendendo il
sistema a due uscite.

412

Capitolo XIV

XIV

Utilizziamo i Timer interni al PIC


In questo Capitolo vengono introdotti i Timer interni al PIC mostrando applicazioni e
particolarit di ognuno. Sebbene un Timer serva apparentemente solo per contare,
vedremo a breve che le applicazioni realizzabili per mezzo di quest'ultimi va oltre il
semplice conteggio. Il Capitolo aggiunge inoltre nuovi esempi per gli Interrupt a doppio
livello, estendendo dunque la pratica sugli stessi.

Schema elettrico
Per poter seguire gli esempi presentati in questo Capitolo, richiesto realizzare su
Breadboard lo schema elettrico di Figura 144 o far uso delle schede di sviluppo impostate
come descritto nel paragrafo successivo.

Figura 144: Schema elettrico di esempio del Capitolo XIV.

413

Impostare le schede di sviluppo


Al fine di poter seguire gli esempi mostrati in questo Capitolo necessario impostare la
scheda di sviluppo utilizzata, come descritto nei paragrafi seguenti.

Freedom II
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom II deve avere i Jumper posizionati nel seguente modo:

Figura 145: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

Freedom Light
Al fine di poter eseguire in maniera corretta gli esempi presentati in questo Capitolo la
scheda di sviluppo Freedom Light deve avere i Jumper posizionati nel seguente modo:

Figura 146: Impostazioni dei Jumper per i Progetti presentati nel Capitolo.

414

Impostazioni software
Gli esempi proposti fanno uso delle librerie LaurTec, per cui necessario impostare i
percorsi di libreria riportati di seguito:
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\conf
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\inc
[percorso radice della libreria]\LaurTec_PIC_libraries_v_3.3.0\src

Per la libreria Microchip timer.h, necessario impostare il percorso:


[percorso radice]\Microchip\xc8\v1.21\include\plib

La versione della libreria inclusa nel percorso potrebbe variare in base a quella utilizzata.

Descrizione ed applicazione dei Timer


Giunti a questo punto siamo sempre pi consapevoli che per mezzo di un
microcontrollore possibile fare molte cose in contemporanea o meglio in successione
ma con l'apparenza che siano svolte contemporaneamente. Per permettere questo
spesso necessario organizzare il programma in maniera opportuna e sfruttare le
interruzioni, dove necessario, per poter creare quella fluidit di esecuzione che crea
quell'apparente esecuzione Real Time, ovvero di esecuzione in tempo reale. In tale
contesto la dicitura Real Time potrebbe essere mal intesa; in ambito professionale una
semplice fluidit del programma che non crea disagio all'utilizzatore, viene in generale
definita Soft Real Time. Si pensi per esempio al mouse del PC, questo normalmente si
muove in maniera fluida e non crea disagio all'utilizzatore; ciononostante non inusuale
che in alcuni casi si muova a scatti. Per tale ragione il mouse non realmente una
periferica Real Time, ed in particolare il Software che la gestisce risponde alla definizione di
Soft Real Time. Periferiche Real Time o meglio Hard Real Time sono quelle periferiche il cui
Software permette di eseguire determinati compiti in un tempo stabilito (definito in gergo
professionale come time budget). Qualunque cosa accada, questo tempo viene rispettato
garantendo che le azioni o risultati derivanti dal servizio prestato dal Software siano
sempre forniti in tempo utile e in forma corretta. Tra le tecniche per raggiungere tali livelli
di performance vi l'utilizzo degli Interrupt 167 che permette ad una periferica importante
di interrompere la normale esecuzione del programma. Una seconda tecnica utilizzata
per mezzo di Timer, che permettono di assegnare determinati tempi di esecuzione a dei
processi o funzioni168.
I Timer interni al PIC sono effettivamente quello che ci si aspetta che siano ovvero dei
sistemi per contare. All'interno dei PIC18 sono presenti fino a 6 Timer a seconda del
modello del PIC utilizzato. Nel PIC18F4550 sono presenti 4 Timer nominati Timer0,
Timer1, Timer2, Timer3. Il Timer0 rappresenta quello per uso generico e sar quello
descritto in maggior dettaglio nei paragrafi ed esempi che seguiranno. Gli altri Timer
anche se possibile utilizzarli per applicazioni generiche, sono spesso utilizzati per
167
168

I tempo di latenza uno dei parametri fondamentali nell'analisi del time budget.
I Timer vengono spesso utilizzati nei sistemi operativi per la realizzazione dello scheduling dei processi. Nel seguente
Capitolo non si affronteranno le problematiche associate a questo utilizzo poich la trattazione richiede conoscenze che
vanno oltre la semplice lettura di un Datasheet o un libro.

415

applicazioni specifiche.
Per esempio il Timer1 pu essere utilizzato come oscillatore secondario ed in
particolare possedendo la circuiteria per far oscillare un quarzo a bassa frequenza, pu
essere utilizzato per far oscillare un quarzo da 32768KHz, utilizzato per la realizzazione di
un Real Time Clock Calendar. Il Timer2 e Timer3 sono spesso associati con i moduli CCP
utilizzati per la generazione del segnale PWM. Quando un Timer viene associato ad una
periferica non pu essere utilizzato in applicazioni generiche salvo forte restrizioni.
Normalmente i Timer vengono fatti lavorare per mezzo di Interrupt, in maniera tale da
avere un conteggio senza bloccare il resto del programma. Infatti i Timer sono
indipendenti dall'esecuzione del programma, ovvero una volta impostati non richiedono
nessun'altra azione da parte del programma. Se sono attivate le interruzioni il Timer
interrompe il programma principale e sar solo a questo punto che il programma deve
gestire l'evento associato al Timer. Ogni Timer, indipendentemente dal modulo a cui
viene associato, permette di generare un'interruzione di overflow al passaggio da 0xFF a
00 (se ad 8 bit) o da 0xFFFF a 0x0000 (se a 16 bit).
Per esempio, nel caso del Timer0, per abilitare le interruzioni necessario abilitare il bit
TMR0IE del registro INTCON. All'interno dello stesso registro anche presente il bit
TMR0IF che permette di visualizzare l'evento d'interruzione associato al Timer1. Si
ricorda che tale bit deve essere posto a 0 via software al termine della routine di gestione
dell'interruzione. In ultimo a seconda della modalit d'interruzione che viene utilizzata
bisogna impostare il bit TMR0IP. Bisogna impostare il bit a 0 per abilitarlo ad interruzioni
a bassa priorit, mentre deve essere impostato ad 1 per utilizzare le interruzioni ad alta
priorit o modalit compatibile. Si ricorda che tale bit di default vale 1, ovvero la
periferica gestita ad alta priorit. Questo permette di ignorare tale bit quando viene
utilizzata la modalit compatibile PIC16 che equivale a trattare le interruzioni ad alta
priorit, ovvero con l'Interrupt Vector impostato posto all'indirizzo 0x08.

Descrizione del modulo Timer0


Il Timer0 il Timer pensato per applicazioni generiche e non associato direttamente
a nessun modulo interno. Il Timer0 ha le seguenti caratteristiche:

Il Timer pu lavorare in modalit ad 8 o 16 bit.


I registri del Timer possono essere sia letti che scritti.
Possibilit di utilizzare il Prescaler.
Sorgente di Clock selezionabile (interna od esterna).
Selezione del fronte dell'incremento.
Possibilit di generare un Interrupt per overflow.

In Figura 147 riportato lo schema a blocchi del modulo Timer0 in modalit ad 8 bit
mentre in Figura 148 riportato lo schema a blocchi in modalit a 16 bit.

416

Figura 147: Schema a blocchi del Timer0 in modalit 8 bit.

Figura 148: Schema a blocchi del Timer0 in modalit 16 bit.

Dallo schema a blocchi subito possibile vedere che sulla sua sinistra possibile
selezionare la sorgente del clock che permette l'incremento del contatore. In particolare
possibile selezionare una sorgente esterna ovvero entrante dal pin TOKI o la sorgente
interna Fosc/4 ovvero la frequenza di esecuzione delle istruzioni. Successivamente
presente un percorso diretto verso un secondo multiplexer di selezione, ed un percorso
attraverso un Prescaler ovvero un divisore di frequenza. Questo permette rallentare la
frequenza principale, sia essa quella interna o esterna. La divisione pu essere selezionata
da 2 a 256 con passi di potenze di 2, ovvero, 2, 4, 8, 16, 32, 64, 128, 256. Successivamente
presente una linea di ritardo di due cicli di clock; questo permette di sincronizzare il
segnale con il clock interno. Il clock che si viene a generare dopo questo percorso andr
ad incrementare il contatore Timer0, sia che sia impostato a 8 bit ovvero con conteggio
massimo fino a 255, sia che sia impostato in modalit a 16 bit, ovvero con conteggio
massimo fino a 65535. Lo schema a blocchi a 16 bit risulta leggermente pi complesso
poich il secondo registro associato al Timer0, ovvero TMR0H, deve essere scritto e letto
in maniera opportuna in maniera da garantire il corretto funzionamento del Timer stesso.
In particolare il registro TMR0H rappresenta un registro ombra, dal momento che non
possibile leggere e scrivere direttamente il suo contenuto.
Per leggere il registro contatore dato dall'unione TMR0H e TMR0L si deve leggere prima
il registro TMR0L. Alla lettura del registro TMR0L, il registro ombra TMR0H viene
automaticamente caricato con il valore reale del registro TMR0 per cui si pu leggere in
maniera accurata il valore del registro all'istante in cui si avviata la lettura del registro
417

TMR0L. In maniera simile, per poter scrivere nel registro a 16 bit dato dall'unione
TMR0H e TMR0L, si procede prima alla scrittura del registro TMR0H, il cui valore viene
per trasferito solo alla scrittura del registro TMR0L. In questo modo si garantisce che i
16bit vengono caricati nello stesso ciclo di clock.
Si fa notare che al fine di garantire la corretta esecuzione temporale dei tempi
impostati, il Prescaler viene azzerato ogni volta che si scrive nei registri del Timer0. Il suo
Reset non cambia per il rapporto di divisione che stato impostato.
Scrivere nel contatore TMR0 inibisce il conteggio per due cicli di clock. Questa
perdita sistematica del clock pu essere compensata caricando il valore
opportuno all'interno del Timer.

Registri di configurazione del modulo Timer0


I registri interni al PIC18F4550 per la configurazione del modulo Timer0 sono:

T0CON
TMR0L
TMR0H

In Tabella 25 sono riportati i vari registri e bit associati al Timer0.


Nome

BIT7

BIT6

BIT5

BIT4

BIT3

TMR0L

Registro Timer0 Low

TMR0H

Registro Timer0 High

INTCON

GIE/GIEH PEIE/GIEL

INTCON2

BIT2

BIT1

BIT0

TMR0IE

INT0IE

RBIE

TMR0IF

INTIF

RBIF

RBPU

INTEDG0

INTEDG1

INTEDG2

TMR0IP

RBIP

T0CON

TMR0ON

T08BIT

T0CS

T0SE

PSA

T0PS2

T0PS1

T0PS0

TRISA

TRISA6

TRISA5

TRISA4

TRISA3

TRISA2

TRISA1

TRISA0

Tabella 25: Registri associati al Timer0.

Il contenuto dei registri presentati nel Capitolo sono forniti per semplificare la
comprensione delle librerie e dei programmi, e non rappresentano una fonte
alternativa al Datasheet. A seconda del modello del PIC utilizzato i registri
descritti potrebbero essere differenti o avere bit con significato differente.

Configurazione dei pin


Al fine di utilizzare il modulo Timer0 qualora si faccia uso di un clock esterno, bisogna
impostare la linea RA4 come ingresso.

La linea RA4 rappresenta la linea T0CKI.


418

Registro T0CON: Timer0 Control Register


R/W-1

R/W-1

R/W-1

R/W-1

TMR0ON

T08BIT

T0CS

Bit 7

Bit 6

Bit 5

Leggenda
R = Readable bit
-n = Value at POR

R/W-1

R/W-1

R/W-1

R/W-1

T0SE

PSA

T0PS2

T0PS1

T0PS0

Bit 4

Bit 3

Bit 2

Bit 1

Bit 0

W = Writable bit
1 = Bit is set

Bit 7

TMR0ON : Timer0 ON/OFF bit


1 : Abilita il Timer0
0 : Disabilita il Timer0

Bit 6

T08BIT : Timer0 8-Bits/16Bits Control bit


1 : Il Timer0 configurato a 8 bit.
0 : Il Timer0 configurato a 16 bit

Bit 5

T0CS : Bit di selezione del Clock.


1 : Transizione sul pin T0CKI
0 : Clock d'istruzione interna

Bit 4

T0SE : Selezione del fronte del Timer0


1 : Incrementa sul fronte di discesa del pin T0CKI
0 : Incrementa sul fronte di salita del pin T0CKI

Bit 3

PSA : Bit di assegnamento del Prescaler


1: Il Prescaler non assegnato al Timer0
0: Il Prescaler assegnato al Timer0

Bit 2-0

T0PS2-T0PS0 : Valore del Prescaler


111 : 1:256 Valore del Prescaler
110 : 1:128 Valore del Prescaler
101 : 1:64 Valore del Prescaler
100 : 1:32 Valore del Prescaler
011 : 1:16 Valore del Prescaler
010 : 1:8 Valore del Prescaler
001 : 1:4 Valore del Prescaler
000 : 1:2 Valore del Prescaler

U = Unimplemented bit read as 0


0 = Bit is cleared

S : Settable bit
x = Bit is unknown

419

Descrizione del modulo Timer1


Il Timer1 un Timer a 16 bit, ed pensato per applicazioni pi specifiche se
paragonato al Timer0. Il Timer1 ha le seguenti caratteristiche:

Timer a 16 bit.
I registri del Timer possono essere sia letti che scritti.
Possibilit di utilizzare il Prescaler.
Sorgente di Clock selezionabile (interna od esterna).
Possibilit di generare un Interrupt per overflow.
Evento speciale modulo CCP.
Stato del clock (T1RUN).

In Figura 149 riportato lo schema a blocchi del modulo Timer1.

Figura 149: Schema a blocchi del Timer1.

Alla sinistra dello schema a blocchi presente la possibilit di selezionare la sorgente


del clock che permetter l'incremento del contatore. In particolare possibile selezionare
una sorgente esterna ovvero entrante dal pin T13CKI o la sorgente interna Fosc/4
ovvero la frequenza di esecuzione delle istruzioni. Oltre a questa opzione presente un
buffer interno che permette l'oscillazione di un quarzo esterno a bassa frequenza,
tipicamente da 32KHz (32768Hz). Questa opzione fornisce la possibilit di realizzare un
Real Time Clock Calendar con la stessa precisione di RTCC esterni, come per esempio il
PCF8563. In particolare il clock generato per mezzo del cristallo esterno da 32KHz pu
anche essere utilizzato come clock di sistema, disponibile anche quando il clock principale
dovesse essere disabilitato da modalit a basso consumo.
420

I contatore del Timer1 composto dall'unione di due registri ad 8 bit, TMR1L e


TMR1H, in particolare il registro TMR1 rappresenta l'ombra del registro TMR1H. Per
leggere il registro contatore dato dall'unione TMR1H e TMR1L si deve leggere prima il
registro TMR1L. Alla lettura del registro TMR1L, il registro ombra TMR1H viene
automaticamente caricato con il valore reale del registro TMR1 per cui si pu leggere in
maniera accurata il valore del registro all'istante in cui si avviata la lettura del registro
TMR1L. In maniera simile, per poter scrivere nel registro a 16 bit dato dall'unione
TMR1H e TMR1L, si procede prima alla scrittura del registro TMR1H, il cui valore viene
per trasferito solo alla scrittura del registro TMR1L. In questo modo si garantisce che i
16 bit vengono caricati nello stesso ciclo di clock.

Registri di configurazione del modulo Timer1


I registri interni al PIC18F4550 per la configurazione del modulo Timer1 sono:
T1CON
TMR1L
TMR1H

In Tabella 26 sono riportati i vari registri e bit associati al Timer1.


Nome

BIT7

BIT6

BIT5

BIT4

BIT3

TMR1L

Registro Timer1 Low

TMR1H

Registro Timer1 High

INTCON

GIE/GIEH PEIE/GIEL

TMR0IE

INT0IE

RBIE

BIT2

BIT1

BIT0

TMR0IF

INTIF

RBIF

PIR1

SPPIF

ADIF

RCIF

TXIF

SSPIF

CCP1IF

TMR2IF

TMR1IF

PIE1

SSPPIE

ADIE

RCIF

TXIF

SSPIE

CCP1IE

TMER2IE

TMR1IE

IPR1

SPPIP

ADIP

RCIP

TXIP

SSPIP

CCP1IP

TMR2IP

TMR1IP

T1CON

RD16

T1RUN

T1CKPS1

T1CKPS0

T1OSCEN

T1SYNC

TMR1CS

TMR1ON

TRISA

TRISA6

TRISA5

TRISA4

TRISA3

TRISA2

TRISA1

TRISA0

Dopo un Reset

Tabella 26: Registri associati al Timer1.

Il contenuto dei registri presentato nel capitolo sono forniti per semplificare la
comprensione delle librerie e del programma ma non rappresentano una fonte
alternativa al Datasheet. A seconda del modello del PIC utilizzato i registri
descritti potrebbero essere differenti o avere bit con significato differente.

Configurazione dei pin


Al fine di utilizzare il modulo Timer1 qualora si faccia uso di un clock esterno, bisogna
impostare la linea RC0 come ingresso. Nel caso si attivi l'oscillatore interno, le linee
T1OSO e T1OSI sono impostate automaticamente come ingressi.

La linea RC0 rappresenta la linea T1OSO/T13CKI.


La linea RC1rappresenta la linea T1OSI.
421

Registro T1CON: Timer1 Control Register


R/W-0

R-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

R/W-0

RD16

T1RUN

T1CKPS1

T1CKPS0

T1OSCEN

T1SYNC

TMR1CS

TMR1ON

Bit 7

Bit 6

Bit 5

Bit 4

Bit 3

Bit 2

Bit 1

Bit 0

Leggenda
R = Readable bit
-n = Value at POR

W = Writable bit
1 = Bit is set

U = Unimplemented bit read as 0


0 = Bit is cleared

Bit 7

RD16 : Seleziona la modalit di lettura/scrittura a 16 bit


1 : Abilita la lettura/scrittura del Timer in modalit a 16 bit.
0 : Disabilita la lettura/scrittura del Timer in modalit a 16 bit

Bit 6

T1RUN : Bit di stato del System Clock


1 : Il clock si sistema prelevato dal Timer
0 : Il clock si sistema non prelevato dal Timer

Bit 5-4

T1CKPS1:T1CKPS0: Selezione del Prescaler


11 : 1:8 Valore del Prescaler
10 : 1:4 Valore del Prescaler
01 : 1:2 Valore del Prescaler
00 : 1:1 Valore del Prescaler

Bit 3

T1OSCEN : Abilita l'oscillatore esterno del Timer


1 : Oscillatore esterno abilitato
0 : Oscillatore esterno disabilitato

Bit 2

T1SYNC : Sincronizzazione del Clock esterno

S : Settable bit
x = Bit is unknown

TMR1CS = 1
1: Non sincronizzare i