Ancora LED & interrupt
Sempre senza allontanarci molto dal nosto primo progetto
vediamo ora come far lampeggiare tre led sfruttando la gestione degli
interrupt dei PIC18F, ovviamente sempre in C.
In particolare faremo lampeggiare il LED1 comandandolo
direttamente all'interno della routine di interrupt, il LED2
comandandolo dall'esterno su richiesta della routine di interrupt ed il
LED3 completamente dall'esterno sfruttando pero' un contatore
incrementato sempre dalla routine di interrupt.
Dal menu' Project apriamo
il progetto originario per poi salvarne una copia con nome differente
come base per il nostro secondo progetto. Selezioniamo dunque Project -> Save As e
diamo un nome differente a questo secondo progetto, salvandolo in una
directory differente per mantenere la storia dell'evoluzione del
progetto e non rischiare di cancellare quanto gia' testato e
funzionante. A questo punto il nostro progetto verra' automaticamente
"commutato" verso la nuova directory ed i nuovi files. Se volete per
evitare ogni ulteriore possibile confusione e' possibile rinominare il
file PICcolino_1.C in PICcolino_2a.C e sostituire nel progetto
questo nuovo file al posto dell'originale.
Il sistema piu' comodo per far cio' consiste nel selezionare la finestra del file, poi da menu' File -> Save As salvare PICcolino_1.C come PICcolino_2a.C.
A questo punto nella finestra di progetto
Aggiungere ( Add Files.. ) dapprima il nuove file PICcolino_2a e poi
cancellare quello vecchio PICcolino_1. Il progetto e' ora pronto per
subire le modifiche del caso
La
prima modifica, oltre all'aggiornamento dell'intestazione, consiste
nell'impostazione dei bit di configurazione del PIC. E' vero che di
norma e' possibile impostare tali bit tramite MPLAB dal menu'
Configure -> Configuration Bits , ma
e' buona norma impostare direttamente nel programma la configurazione
dei principali bits, non fosse altro per ribadire quella gia' impostata
in MPLAB.
Il modo piu' semplice per far cio' e' quello di sfruttare una "direttiva" detta #pragma config seguita da un elenco dei bit di configurazione che si intende impostare, ciascuno con il proprio valore simbolico.
Il significato e il nome simbolico dei vari bit di
configurazione puo' essere dedotto dal file " P18F2620.INC " nella
directory ...\mcc18\mpasm\
Occorre
notare che i bit di configurazione possono variare in base al PIC
utilizzato, quidi occorre fare attenzione che il compilatore
conosca questo parametro.
Nel nostro caso vendo fatto precedere questa pseudo istruzione da #include <p18F2620.h> siamo sicuri che la attribuzione dei bit avverra' correttamente.
Nel nostro esempio abbiamo fatto precedere la direttiva #pragma da ina richiesta di compilazione condizionale #if defined(__18F2620) che fa si' che la direttiva venga presa in considerazione solo se risulta gia' definito il simbolo di sistema __18F2620
, cosa sicuramente vera nel nostro caso avendo incluso precedentemente
tutte le definizioni ed i registri relativi al PIC 18F2620 cin la
direttiva #include.
Un simile modo di operare e' utile quando si vogliono ad
esempio testare differenti processori, bastera' per ogni dispositivo
inserire un blocco
#if defined(__18Fxxxx)
#pragma config ........
#endif
.....
#if defined(__18Fxxxx)
#pragma config ........
#endif
e far precedere tale blocco dalla scelta del processore attraverso #include <p18Fxxxx.h>
Procedendo con le modifiche al precedente progetto
definiamo ore alcune costanti utili per la programmazione del clock
principale e per il timer TMR0
Anche
in questo caso si e' optato per una compilazione condizionale che
permettera' di passare in un attimo da un oscillatore principale ad
8MHz ( originale interno al PIC ) ad uno a 32MHz, sfruttando una
moltiplicazione x4 attraverso il circuito di PLL interno al PIC.
Un cambiamento di clock ci costringerebbe a modificare
all'interno del programma finale tutti quei parametri che intervengono
nei circuiti di temporizzazione ( interrupt, Baud rate, ritardi,
lampeggi ecc )
Definendo preventivamente alcune costanti in
funzione del clock si puo' migliorare l'aspetto e la
comprensibilita' del programma, con la sicurezza di non
dimenticarsi di sostituire qualche parametro all'interno del
programma stesso.
Per realizzare il programma di lampeggio LED abbiamo
bisogno di alcune locazioni di memoria per contenere il valore attuale
dei conteggi dei tempi.
Dobbiamo pertanto definire queste variabili ed assegnere ad esse l'indirizzo di memoria ove verranno memorizzate.
Ciascuna variabile potra' avere un nome di fantasia,
composto da un'unica parola ed andra' dichiarata anteponendo al nome il
"tipo" da attribuire alla variabile stessa.
Nel compilatore C18 i tipi previsti sono:
Tipo
|
Dimensione
|
Minimo
|
Massimo |
char
|
8 bits
|
-128 |
127 |
signed char
|
8 bits
|
-128 |
127 |
unsigned char
|
8 bits
|
0 |
255 |
int
|
16 bits
|
-32,768 |
32,767 |
unsigned int
|
16 bits
|
0 |
65,535 |
short
|
16 bits
|
-32,768 |
32,767 |
unsigned short
|
16 bits |
0 |
65,535 |
short long
|
24 bits |
-8,388,608 |
8,388,607 |
unsigned short long
|
24 bits
|
0 |
16,777,215 |
long
|
32 bits
|
-2,147,483,648 |
2,147,483,647 |
unsigned long
|
32 bits
|
0 |
4,294,967,295 |
float
|
32 bits
|
≈
1.17549435 e-38 |
≈ 6.80564693 e+38 |
double
|
32 bits
|
≈
1.17549435 e-38 |
≈ 6.80564693 e +38 |
Non
dimentichiamoci poi di terminare la linea di dichiarazione con un ; e
di aggiungere preferibilmente un commento // a futura memoria
Dopo aver definito la memoria ram passiamo alla gestione degli interrupt.
Come e' noto ( se non e' cosi' andatevi a documentare
all'interno del sito dall'ottimo materiale pubblicato ) nei PIC18 ci
sono due differenti tipi di interrupt, uno ad alta priorita' e l'altro
a bassa priorita'. In sintesi quello ad alta priorita' e'
definito tale in quanto puo' interrompere persino la routine di
interrupt di quello a bassa velocita'. Entrambi se abilitati,
interrompono in maniera asincrona tutto il restante programma. Gli
interrupt come e' noto sono dei segnali generati da particolari eventi
delle periferiche del microprocessore come ad esempio l'overflow di un
contatore / timer o il cambio di stato di un segnale su una porta o la
ricezione di un carattere su una porta seriale ecc..
Nel nostro programma sfrutteremo entrambi gli interrupt,
riservando quello ad alta priorita' per la gestione di un clock a 10mS
circa e quello a bassa priorita' per la ricezione di caratteri sulla
linea seriale.
Come
e' noto dall'esame dei data sheet dei PIC 18 quando si verifica un
interrupt ad alta priorita', il normale flusso del programma viene
interrotto alla fine dell'istruzione in corso ed il controllo, dopo il
salvataggio dei registri esegue un salto alla locazione con indirizzo
0x08. Da questa locazione viene eseguito il programma di interrupt sino
al raggiungimento della istruzione di " ritorno dall'interrupt " che
dopo aver ripristinato i registri al valore che avevano al momento
della chiamata dell'interrupt esegue un salto "indietro" alla
istruzione successiva all'ultima eseguita prima del richiamo
dell'interrupt, facendo continuare il programma dal punto esatto e con
le stesse condizione di quando era stato interrotto.
Sin qui abbiamo solo rinfrescato le nozioni di
"interrupt" valide indipendentemente dal linguaggio di programmazione
utilizzato. Quando si vuol richiamare una routine di interrupt, per
prima cosa dobbiamo predisporre anche nel C un meccanismo che partendo
dalla locazione 0x08 per l'interrupt ad alta priorita' o 0x18 per
quello a bassa priorita' esegua un salto alle effettive routine
posizionate ovunque a piacere nel programma.
Nel nostro caso dovremo innanzitutto fare il modo che
l'istruzione di salto alla routine di interrupt risulti posizionata
all'indirizzo 0x08. Cio' verra' ottenuto con la pseudo istruzione
#pragma code InterruptVectorHigh = 0x8 // indirizzo interrupt alta priorita'
a cui seguira' la vera e propria istruzione di salto alla routine di interrupt
void InterruptVectorHigh (void)
{
_asm
goto
InterruptHandlerHigh
// salta alla gestione
dell'interrupt
_endasm
}
Dunque un interrupt ad alta priorita' saltera' alla locazione 0x08 , denominata simbolicamente InterruptVectorHigh
e da qui verra' rimandato alla funzione InterruptHandlerHigh al cui interno troveranno posto le istruzioni che vorremo far eseguire
dall'interrupt.
Questa lunga precisazione e' sintomatica dell'importanza che va
attribuita agli interrupt il cui meccanismo di funzionamento va ben
compreso prima di addentrarci in piu' complessi programmi.
Del tutto analoga, la gestione dell'interrupt a bassa priorita' che
avremo modo di sperimentare nel prossimo capitolo ma che per
completezza inseriamo gia' da ora nel codice, anche se completamente
vuoto.
Ed
ecco finalmente il codice della routine di interrupt che quasi non si
differenzia da qualsiasi altra funzione del C non fosse altro
dall'essere preceduta da una direttiva
#pragma code
che evidenzia che quanto scritto successivamente e' da interpretarsi come codice istruzioni, e da
#pragma interrupt InterruptHandlerHigh
che istruisce il compilatore di considerare la routine InterruptHandlerHigh come
una ruotine speciale di interrupt, provvedendo dunque al salvataggio
automatico dei registri ed al loro ripristino a fine routine.
L'interrupt ad alta priorita', come apparira' chiaro
dalla nuova routine di inizializzazione dell'hardware che analizzeremo
tra poco, e' associato all'overflow del timer TMR0, dunque la prima
istruzione della routine di interrupt consiste nell'accertarsi che la
segnalazione di interrupr derivi proprio da questa fonte e non da altre
in grado parimenti di generare interrupt ad alta priorita'.
l'istruzione per effettuare tale test e':
if
(INTCONbits.TMR0IF)
// e' un
interrupt da TMR0 ?
{
.
.
}
Il costrutto if(condizione) e' un caso ridotto di quello piu' generale
if(condizione)
{
}
else
{
}
Da notare innanzitutto che ne if(..) ne else
richiedono il classico ; che termina di norma ogni istruzione. I ;
saranno invece indispensabili per ogni istruzione all'interno del
blocco { .. }.
Il significato dell'istruzione e' di per se abbastanza prevedibile; se la condizione risulta VERA allora viene eseguito il blocco di istruzioni tra le succeccive parentesi { ..} altrimenti se e' FALSA viene eseguito il blocco di istruzioni dopo else.
Molto importante risulta capire cosa si intende per "condizione VERA". Oltre alla evidente condizione del bit INTCONbits.TMR0IF del
nostro caso con condizione si intende tutto cio' che restituisce un
risultato logico booleano ( 0 o 1 ), dunque la condizione puo'
risultare una qualsiasi espressione complessa a piacere, incluse
funzioni o altri operatori, purche' il tutto restituisca alla fine un
risultato logico valutabile dall'istruzione if.
Una delle regole fondamentali del C e' infatti che
nella sintassi e' possibile sostituire ad una variabile una qualsiasi
espressione che restituisca lo stesso tipo dati della variabile stessa.
Questo fatto determina in un certo modo la potenza del C, anche se
molto spesso a scapito di una immediatezza di lettura delle istruzioni
che possono annidare al loro interno altre istruzioni al posto di piu'
elementari variabili.
Nei nostri esempi cercheremo di non esagerare con questa possibilita'
anche se in piu' di una occasione utilizzeremo questa possibilita' che
oltre a ridurre le linee di codice sorgente ne aumenta l'efficienza.
Tornando alla nostra routine di interrupt, incontriamo la seconda istruzione
tempo++ ;
L'operatore ++ detto di autoincremento, determina l'incremento di 1
della variabile relativa e la successiva memorizzazione con il nuovo
valore incrementato. Anche in questo caso non bisogna semplicemente
interpretare come la somma del numero intero 1 alla variabile in
questione, ma qualcosa di piu' complesso, funzione di cosa la variabile
stessa rappresenta .... ma ne riparleremo.
Nel nostri caso la variabile intera tempo rappresenta un contatore che puo' essere letto e manipolato dall'esterno della routine di
interrupt.
Proseguendo nell'analisi del listato troviamo
if(tempo_led1++ >=200) // passati 2 sec ?
{
tempo_led1 = 0;
// azzera temporizzatore
led1 = !led1 ;
// inverti stato
led
}
Ora che sappiamo perfettamente come si comporta l'istruzione if siamo
in grado di comprendere cona vogliamo ar fare al nostro programma, ma
attenzione ad interpretarlo correttamente. Come sopra detto
quanto contenuto tra le parentesi ( ) della if ci
deve restituire un risultato logico 0 / 1 o VERO / FALSO che dir si
voglia. Nel nostro caso e' l'operatore >= a svolgere questa
funzione fondamentale in quanto restituira' un valore logico booleano.
Analogamente all'operatore >= esistono altri operatori di confronto
>
// maggiore
>=
// maggiore o uguale
<
// minore
<= // minore o uguale
==
// uguale ( attenzione NON usare = che significa
assegnazione di un valore e non confronto )
!=
// diverso ( ! e' il simbolo di NOT )
Attenzione ancora all'operatore ++ gia' incontrato in precedenza, in
questo caso e' fondamentale interpretare correttamente il comportamento
di tale operatore infatti quando ++ segue la variabile, come nel nostro
caso il compilatore C prima valuta il valore sella variabile e solo
dopo ne effettua l'auto incremento. Contrariamente se avessimo scritto
if(++tempo_led1 >=200) // passati 2 sec ?
avremmo ottenuto dapprima un incremento del contatore tempo_led1 e
solo successivamente una valutazione dell'operatore >=. Poca cosa in
questo caso ma spesso fondamentale per il corretto funzionamento di un
programma.
Come logicamente si puo' supporre accanto all'operatore ++ esiste anche quello -- con analoghe considerazioni.
Rimane ancora da analizzare l'istruzione che fa lampeggiare il led1.
led1 = !led1
;
// inverti stato
led
Questa istruzione legge lo stato logico di led1, lo inverte con l'operatore ! e lo riassegna alla variabile led1 tramite l'operatore =
Tutto qui ! Quelle che sembravano tre banali
istruzioni racchiudevano in se molte delle sintassi che utilizzaremo
largamente nel nostro progetto.
Lascio a voi
interpretare cosa succede con il led2 che non viene comandato
direttamente ma segnala solo con una variabile che e' scaduto il
proprio tempo di lampeggio.
Le ultime righe della routine di interrupt servono a "ricaricare" il
TMR0 con la costante necessaria ad ottenere una temposizzazione di 10mS
ad infine a riabilitare l'interrupt in modo che possa aver luogo un
nuovo richiamo della routine di interrupr allo scarede dei successivi
10mS
TMR0H =
TM0H;
//setta
timer H
TMR0L =
TM0L;
//setta timer L
INTCONbits.TMR0IF =
0;
// azzera interrupt flag
Come gia' detto non ci preoccuperemo per ora dell'interrupt a bassa priorita' di cui
dovremo comunque prevedere una routine che non fa assolutamente nulla.
Guardiamo invece come va modificata la routine di inizializzazione
hardware per gestire la nuova realta' del timer TMR0 gestito in
interrupt.
Innanzitutto si puo' notare come le caratteristiche dell'oscillatore dipendano dalle costanti simboliche CK_TYPE e CK_TUNE
definite in maniera condizionata all'inizio del programma. Non ci
dovremo piu' pertanto preoccupare del loro valore se desideriamo
cambiare la frequenza di clock.
Per far funzionare TMR0 viene poi impostato il registro di controllo T0CON al valore T0CON_K
precedentemente definito in modo da utilizzare in ingresso la frequenza
di oscillazione /4 ulteriormente suddivisa per 8 dal prescaler.
A questo punto per ottenere la corretta temporizzazione
del timer TMR0 occorre impostare i due registri di predivisione TMR0H e
TMR0L con le costanti simboliche sempre definite in funzione della frequenza del clock principale.
Rimane
ora da impostare la gestione degli interrupt di alta e bassa priorita'.
Vi sono diversi registri da impostare ( vedasi data sheet del PIC ).
Occorre infatti impostare il registro di controllo INTCON e INTCON2 mantenendo provvisoriamente disabilitato
l'interrupr globale, ma predisponendo l'abilitazione di quello associato a TMR0.
Viene poi associato l'interrupt di TMR0 alla modalita' alta priorita'.
Nei registro di controllo dell'interrupt RCON viene
poi selezionata la modalita' a doppia priorita' ( alta e bassa ) mentre
nel registro di priorita' delle periferiche viene disabilitata
(provvisoriamente ) l'interrupt a bassa priorita' dell' USART.
Ora tutto e' pronto per essere richiamato dal programma principale.
Dopo il solito richiamo alla routine di inizializzazione dell'hardware si entra nel loop infinito del while (1)
Come e' ovvio non viene effettuato nessun richiamo delle
routines di interrupt essendo queste richiamate automaticamente ed
autonomamente ogni volta che si determina l'evento progettato per
l'interruzione.
Viene invece testato l'effetto dell'interrupt sulle variabili tm_led2 e tempo che vengono manipolate in tale routine.
in particolare con l'istruzione if ( tm_led2 ) viene
testato se l'interrupt , dopo 50 cicli ha settato tale flag ed in
questo caso vengono eseguite le successive istruzioni tra parentesi
graffe.
Riguardo al contatore tempo viene invece testato se questo ha superato il valore 500 e conseguentemente viene commutato il led3.
Vi e' pero' un particolare impostante da
considerare. Il nostro processore e' a 8 bit e pertanto tutte le
istruzioni che richiedono operazioni logico / aritmetiche su
variabili di maggir ampiezza, come ad esempio int, long, float , ecc richiedono necessariamente piu' di una istruzione elementare ( assembler ) per essere portate a compimento.
L'interrupt per sua natura permette di terminare solamente l'istruzione
( elementare ) corrente e dunque puo' accadere che l'interruzione
avvenga proprio nel mezzo di una singola istruzione C.
In genere cio' non comporta nessun problema in quanto l'interrupt
ripristina tutti i registri cosi' come erano prima del richiamo.
Diverso e' se l'istruzione che deve essere eseguita contiene una
variabile manipolata ( ovvero riscritta ) dalla routine di interrupt.
In questo caso puo' accadere che l'interruzione avvenga proprio mentre
il programma principale sta leggendo parte della variabile mente la
restante parte verra' letta ed interpretata dopo l'interrupt, ma
probabilmente differente da quella precedente, dunque ATTENZIONE!
Estremamente improbabile denotera' qualcuno..... Non tanto direi.
Il nostro programma ne e' una prova: Essendo estremamente
compatto e non facendo altro il piu' delle volte che leggere il
valore 16 bit di tempo e
testare se >= a 500, la probabilita' che l'interruzione avvenga
proprio tra la lettura degli 8 bit piu' significativi e degli 8 bit
meno significativi non e' poi cosi' remota.
Per
risolvere il possibile errore occorre dunque interdire momentaneamente l'interrupt che interessa la variabile per ripristinarl subito dopo.
Cosi' e' stato fatto nell'esempio mettendo INTCONbits.GIEH = 0; prima del test di tempo e riabilitando
l'interrupt con INTCONbits.GIEH = 1; dopo il test.
Invito gli scettici a provare a commentare queste istruzioni e a
controllare il lampeggio del led3. Vedrete che saltuariamente si
verificamo dei vistosi ERRORI dovuti appunto all' intreccio tra
interrupt a e main.
A
breve daremo qualche ritocco di restyling, che senza toccare le
funzionalita' del programma gli conferira' un aspetto leggermente piu'
professionale.
La foto che segue mostra il prototipo del nostro PICcolino al lavoro in
unione al sempre valido PICkit 2., foto eseguita dopo la compilazione,
il caricamento e l'avvio del programma sin qui descritto.
Per ora non rendo scaricabile direttamente nella sezione Download i
sorgenti del progetto, lo faro' successivamente per gli scansafatiche.
Per chi vuole davvero imparare a programmare in C o in qualunque altro
linguaggio non c'e' di meglio che scrivere personalmente il programma,
inserendoci inevitabilmente qualche errore per andarne poi a caccia con
i propri mezzi. Vuoi mettere la soddisfazione quando finalmente cio'
che hai costruito con fatica ... funziona !
|