TUTORIAL PIC 

 

PIC & C


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

Aggiungi file al progetto

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 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'operatoree 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 !



 

 

Copyright © afg. Tutti i diritti riservati.
Aggiornato il 16/11/11.