Vediamo ora come possiamo utilizzare le istruzioni per creare le strutture base di tutti i linguaggi ad alto livello: selezioni e iterazioni.
Gli esempi proposti sono da considerarsi più che altro dei modelli, che verranno
poi adattati in base alla situazione.
Selezione Semplice (If - Then)
btfsc STATUS,C ; carry = 1 ?? istruzione ; sì |
btfss STATUS,C ; carry = 1 ?? goto EndIf ; no istruzione1 ; sì istruzione2 istruzione3 EndIf .... |
Selezione Doppia (If - Then - Else)
btfss STATUS,C ; carry = 1 ?? goto Else ; no istruzione1 ; sì istruzione2 istruzione3 goto EndIf Else istruzione4 istruzione5 istruzione6 EndIf ... |
Iterazione a cicli fissi
movlw 10 ; 10 cicli movwf 0x0C ; usiamo il registro 0x0C come contatore Loop istruzione1 istruzione2 .... decfsz 0x0C,F ; decrementa 0x0C, se 0 salta l'struzione successiva goto Loop ; altrimenti torna a "Loop" .... |
In questi esempi, si nota che ad alcuni numeri sono state sostituite delle lettere: - STATUS = 0x03 - C = 0x00 - F = 0x01 Questo può essere fatto, in MPLab, con la direttiva EQU: STATUS EQU 0x03 C EQU 0x00 F EQU 0x01 Allo stesso modo possiamo dare ai nostri registri un nome; nell'ultimo esempio potremmo chiamare "Contatore" il registro 0x0C.
Forti di queste conoscenze, possiamo vedere il primo esempio completo:
Il PIC non è in grado di eseguire moltiplicazioni: questo programma esegue la moltiplicazione
5x3 sommando tre volte cinque => 5x3 = 5+5+5
include "P16F84.INC" ; include il file che contiene tutte le EQU del PIC 16F84 DatoA EQU 0x0C ; primo operando nel registro 0x0C DatoB EQU 0x0D ; il secondo in 0x0D Ris EQU 0x0E ; mettiamo il risultato in 0x0E ORG 0x00 ; indica che ci troviamo all'indirizzo 0 ; della memoria programma ; il programma esegue la moltiplicazione 5x3 movlw 0x05 movwf DatoA ; DatoA = 5 movlw 0x03 movwf DatoB ; DatoB = 3 clrf Ris ; Ris = 0 Loop movf DatoA,W ; W = DatoA = 5 addwf Ris,F ; Ris = Ris + W decfsz DatoB,F ; decrementa DatoB (esegue il ciclo DatoB volte) goto Loop ; torna a Loop END ; tutti i programmi devono terminare con END
Putroppo il risultato non può essere visto, se non con il debugger, perchè effettivamente questo
non viene comunicato all'esterno del chip; ma scopriremo come farlo nella prossima lezione.
Come già visto, possiamo usare i registri PORTA e PORTB per modificare lo stato
dei pin o per leggerne il valore; prima, però, bisogna definire quali sono gli ingressi
e le uscite, dobbiamo quindi modificare i registri TRISA e TRISB.
Quest'ultimi si trovano nel banco 1 e per accedervi agiremo sul registro di STATUS:
bsf STATUS,RP0 ; mette a uno il bit RP0, viene selezionato il banco 1
A questo punto è possibilie modificare i due registri: metteremo le due porte tutte
come ingresso, tranne il primo piedino della porta B.
movlw B'11111111' movwf TRISA movlw B'11111110' movwf TRISB
Quindi si torna al banco 0:
bcf STATUS,RP0 ; mette a zero il bit RP0, viene selezionato il banco 0
Ora, agendo sul primo bit di PORTB, con le istruzioni bsf e bcf, cambierà lo stato dell'uscita:
bsf PORTB,0 ; il pin RB0 è a 5V bcf PORTB,0 ; il pin RB0 è a 0V
Esempio pratico
Come primo esempio pratico, realizziamo il programma che fa lampeggiare un LED.
Il codice sorgente completo è disponibile qui, ma vediamolo in dettaglio:
la testata del programma e la configurazione delle porte è già stata illustrata,
quindi esaminiamo il resto del programma.
Main bsf PORTB,0 ; LED acceso call Delay bcf PORTB,0 ; LED spento call Delay goto Main ; torna a Main
Questo è il corpo principale del programma: è costituito da un ciclo infinito:
una volta eseguite le quattro istruzioni, attraverso il goto, ritorna all'inizio e così via in eterno.
Come suggeriscono i commenti, le due istruzioni bsf e bcf accendono e spengono alternativamente il LED,
ma essendo la frequenza di lavoro molto elevata, ai nostri occhi il LED sembrerebbe sempre acceso;
per questo motivo è stata introdotta una routine di ritardo, chiamata Delay.
Si fa uso dell'istruzione call perchè, una volta eseguito il ritardo, si ritorna al corpo principale
del programma.
Vediamo dunque come viene fatto questo ritardo:
Delay clrf Count1 ; azzera i contatori clrf Count2 ; Loop decfsz Count2,F goto Loop decfsz Count1,F goto Loop return ; ritorna al programma principale
Si tratta di due iterazioni a cicli fissi (viste nella lezione precedente) innestate una nell'altra; azzerando i contantori si ottiene
il numero massimo di iterazioni, ovvero 256 ciascuno, per un totale di 256*256 = 65536 cicli.
Il blocco di istruzioni:
decfsz Count2,F goto Loop
decrementa Count2 per 256 volte, finchè non raggiunge lo zero: a questo punto si passa al secondo blocco
che decrementa il registro Count1 una volta, per poi tornare a "Loop" dove il registro Count2 verrà nuovamente
decrementato per 256 volte. Infine quando anche il Count1 raggiunge lo zero, la routine finisce con l'istruzione return.
Mettendo a zero i contatori non si ottiene il minor numero di iterazioni come si potrebbe pensare bensì il massimo, infatti l'istruzione decfsz prima decrementa e solo poi esegue il controllo sullo zero, perciò al primo decremento il contatore varrà 255, saranno quindi 256 le iterazioni totali.
Sappiamo che con un clock a 4Mhz, un ciclo istruzione è di un microsecondo, perciò è possibile calcolare l'entità del ritardo: - 2 per la call - 1 per clrf - 1 per decfsz - 2 per goto, tranne quando viene fatto saltare da decfsz - 2 per il return quindi : 2 + 2 + ((3*256 - 1) + 3)*256 - 1 + 2 = 200ms circa
Lo scopo del programma è quello di riprodurre l'effetto supercar o, se si preferisce,
effetto luci scorrevoli avanti/indietro. In pratica si vuole ottenere la sequenza:
Gli approci per arrivare a questo risultato sono molti e diversi, ma ne esamineremo soltanto uno.
Il sorgente completo è qui.
Il programma consiste in una routine che esegue degli shift verso destra e verso sinistra in modo da creare la sequenza;
un registro tiene traccia della direzione in cui ci stiamo muovendo, quando il primo o l'ultimo LED si
accendono, la direzione viene invertita.
Schema elettrico
Il programma
L'intestazione contiene una piccola novità, che riguarda la direttiva ORG:
ORG 0x0C ; indirizzo 0x0C della RAM Count RES 2 ; riserva 2 byte per il contatore Dir RES 1 ; riserva 1 byte per la direzione
In questo caso, infatti, la direttiva non si riferisce alla memoria programma, ma alla RAM;
poi, attraverso la direttiva RES si possono allocare gli spazi per i nostri registri, indicando il
numero di byte utilizzati. Nel nostro caso il registro Count occuperà i registri 0x0C e 0x0D, mentre a
Dir corrisponderà 0x0E.
Il programma inizia con la classica configurazione delle porte: semplicemente la porta A
viene settata come ingresso (non ci interessa), mentre la porta B è un'uscita. Il blocco di istruzioni
che seguono inizializzano l'uscita e la direzione:
movlw B'00000001' movwf PORTB ; accende solo il primo LED clrf Dir ; Dir = 0 = sinistra
Come sempre, il programma presenta un ciclo infito, rappresentato nel nostro caso dalla label Main:
Main call Delay ; chiama il ritardo bcf STATUS,C ; azzera il carry btfss Dir,0 ; se direzione goto GoLeft ; = 0 va a sinistra
Ad ogni iterazione viene chiamato un piccolo ritardo e viene azzerato il carry: dobbiamo infatti ricordarci
che le istruzioni di rotazione prevedono il passaggio per il carry, è perciò consigliato settarlo secondo
le proprie esigenze prima di eseguire uno shift (nel nostro caso va azzerato).
Successivamente viene eseguito un controllo sul registro Dir per decidere in quale direzione ruotare.
;GoRight ; = 1 va a destra rrf PORTB,F goto Check GoLeft rlf PORTB,F
Questo sono le istruzioni che vengono chiamate per ruotare il contenuto della porta B.
Inifine, attraverso il blocco Check, controlliamo se siamo ad inizio o fine sequenza, in caso affermativo
viene invertita la direzione:
Check btfss PORTB,0 ; se il primo bit btfsc PORTB,7 ; o l'ultimo bit sono settati comf Dir,F ; inverte la direzione
Propongo un altro esempio, forse più semplice, di effetto supercar: supercar2.asm
Abbiamo già visto che esistono due registri (PCL e PCLATH) attraverso i quali è possibile
interagire con il Program Counter; uno dei loro utilizzi è la creazione di
tabelle di dati, o meglio array, e strutture di tipo switch (o case..of).
Tabelle Dati
Sfruttando l'istruzione retlw, possiamo realizzare degli array di costanti: tale istruzione
funziona come return ovvero permette di tornare dopo una chiamata
e, in più, mette una costante in w.
L'istruzione call:
In questo spezzone di codice, quando si giunge alla call, viene effettuato un salto a Label,
le istruzioni vengono eseguite normalmente, infine arrivati a return, si ritorna alla call o
meglio, all'istruzione che la segue.
Andiamo ora a creare la nostra tabella:
retlw B'00000000' retlw B'10000001' retlw B'01000010' retlw B'00100100' retlw B'00011000'
A questo punto interviene il registro PCL attraverso il quale potremo ottenere il valore che
desideriamo dalla tabella:
Tabella addwf PCL,F ; aggiunge W a PCL retlw B'00000000' retlw B'10000001' retlw B'01000010' retlw B'00100100' retlw B'00011000'
Mettendo in W l'indice dell'elemento voluto e aggiungendolo poi a PCL (con addwf), verrà eseguito un salto
quindi retlw ci restituirà il valore; essendo quest'ultima una funzione di ritorno, è indispensabile
utilizzare una chiamata per accedere alla tabella:
.... movlw 3 call Tabella ....
In questo caso verrà restituito il quarto elemento (il n° 3), ovvero B'00100100'.
PCLATH
Come già visto, il Program Counter è un registro di 13bit, perciò PCL (8 bit) non è sufficiente per
accedervi, ma viene impiegato anche il registro PCLATH (5 bit).
Quando si crea una tabella, bisogna tenere conto di questo fatto, ovvero dobbiamo considerare
che andando a modificare il registro PCL, possiamo accedere soltanto a 256 indirizzi di memoria, perciò
è fondamentale che la tabella non si trovi "a cavallo" tra due pagine di, appunto, 256 word.
Lo stesso accorgimento dev'essere fatto nel caso di istruzioni di salto, dove però l'indirizzo è espresso nell'opcode
con 11 bit, perciò vengono utilizzati solo i bit 3 e 4 di PCLATH; in questo caso le pagine sono di 2048 word.
A differenza di PCL, il quale accede direttamente al PC, il registro PCLATH non modifica direttamente
il contatore e non è mai scritto dal micro; viene copiato nel PC soltanto quando PCL viene modificato direttamente o indirettamente.
Scrittura diretta di PCL (movwf, addwf, ecc.) |
Scrittura indiretta di PCL (call, goto) |
Struttura Switch
Sfruttando lo stesso principio delle tabelle dati, possiamo realizzare delle strutture di questo tipo:
switch(w) { case 0 : caso0(); break; case 1 : case1(); break; case ... ; }
semplicemente sostituendo ai retlw dei goto:
Tabella addwf PCL,F goto case0 goto case1 .... break.... ....
caso0 .... goto break caso1 .... goto break caso.. .... ....
dove il break costituisce la fine della struttura (la parte in rosso è codice al di fuori della struttura).
Questo spezzone può essere inserito all'interno del codice, senza l'utilizzo di call; se però la struttura
viene impiegata in più parti del programma, sarebbe bene utilizzare una chiamata, quindi sostituire i goto break
con dei return.
Questo circuito simula il comportamento di un normale decoder per display a 7 segmenti a catodo comune.
Mettendo in ingresso un numero binario di 4 bit (PORTA), si può visualizzarne il valore tramite un display
connesso alla PORTB. Nel collegare il display, si tenga conto che RB0 è il segmento "a" ed in ordine gli altri
fino al segmento "g" in RB6.
Per avere un decoder che funzioni su un display ad anodo comune, è sufficiente invertire tutti gli uni e gli zeri che compaiono
in tabella.
include "P16F84A.INC" RADIX DEC ORG 0x00 bsf STATUS,RP0 clrf TRISB movlw 0xFF movwf TRISA bcf STATUS,RP0 Main movf PORTA,W call Tabella movwf PORTB goto Main Tabella addwf PCL,F ; gfedcba retlw B'0111111' ; 0 retlw B'0000110' ; 1 retlw B'1011011' ; 2 retlw B'1001111' ; 3 retlw B'1100110' ; 4 retlw B'1101101' ; 5 retlw B'1111101' ; 6 retlw B'0000111' ; 7 retlw B'1111111' ; 8 retlw B'1101111' ; 9 retlw B'0110111' ; A retlw B'1111100' ; b retlw B'0111001' ; C retlw B'1011110' ; d retlw B'1111001' ; E retlw B'1110001' ; F END
Normalmente, per scrivere in un registro usiamo l'istruzione:
movwf 0x0C
questo si chiama indirizzamento diretto perchè l'indirizzo a cui ci riferiamo è espresso
direttamente nell'istruzione; esiste, però, un altro metodo di accesso alla RAM, chiamato
"indirizzamento indiretto": questo fa uso di due registri, ovvero FSR e INDF.
Nel primo viene specificato l'indirizzo al quale si vuole accedere; il secondo, invece, non è un vero
e proprio registro, perchè fisicamente non è implementato, ma serve per leggere o scrivere nella locazione di
memoria specificata in FSR.
Vediamo un esempio di come scrivere 0xA3 nel registro 0x0C con entrambi i metodi:
;Indirizzamento Diretto movlw 0xA3 movwf 0x0C |
;Indirizzamento Indiretto movlw 0x0C movwf FSR movlw 0xA3 movwf INDF |
Per un'operazione così banale si ricorre, ovviamente, all'indirizzamento diretto, ma ci sono
situazioni nelle quali l'indirizzamento indiretto è l'unica soluzione.
Array
Una semplice applicazione dell'indirizzamento indiretto, è quello di creare un array di variabili nella
memoria RAM:
Array è una costante e corrisponde all'indirizzo del primo elemento dell'array (in questo caso 0x0C);
aggiungendo poi a questa costante, l'indice dell'elemento che desideriamo, otteniamo il suo indirizzo.
Detto questo, la procedura per leggere/scrivere un elemento dell'array è il seguente:
- w = indice; l'indice deve essere >= 0;
- aggiungere w all'indirizzo dell'array e mettere il risultato in FSR;
- leggere/scrivere l'elemento attraverso INDF;
Per concludere ecco il codice:
Dato EQU 0x0C Array EQU 0x0D ; indirizzo base dell'array Esempio ; esegue array[5] = array[2] + 7 movlw 2 call LeggiArray movlw 7 addwf Dato,F movlw 5 call ScriviArray LeggiArray ; funzione per leggere Array[w] addlw Array movwf FSR movf INDF,W movwf Dato return ScriviArray ; funzione per scrivere in Array[w] addlw Array movwf FSR movf Dato,W movwf INDF return
Alcuni PIC, come il nostro 16F84A, contengono una memoria EEPROM supplementare, la quale ci permette
di salvare dei dati in modo permanente, che non vanno quindi persi quando si toglie l'alimtentazione.
Tale memoria può essere letta o scritta sia in fase di programmazione, sia dal PIC stesso; nel 16F84A è di 64 byte
e per accedervi si usano i quattro registri EEDATA, EEADR, EECON1 ed EECON2.
E' bene tenere a mente che questi quattro registri si trovano in due banchi diversi: i primi due nel banco 0, gli altri due nel banco 1.
EECON1
EEIF : indica che la scrittura della EEPROM è avvenuta (per interrupt); resettato dal software
WRERR : settato quando si è verificato un errore durante la scrittura
WREN : settato per abilitare la scrittura nella EEPROM
WR : inizia la scrittura; resettato dall'hardware
RD : inizia la lettura; resettato dall'hardware
Leggere la EEPROM
Leggere un dato dalla memoria EEPROM è molto semplice: innanzi tutto si scrive l'indirizzo a cui accedere
in EEADR, successivamente si setta il bit RD (EECON1[0]) ed il dato è già disponibile in EEDATA:
; mette in W il contenuto dell'indirizzo 0x1E movlw 0x1E movwf EEADR bsf STATUS,RP0 ; banco 1 bsf EECON1,RD bcf STATUS,RP0 ; banco 0 movf EEDATA,W
Scrivere nella EEPROM
La scrittura della EEPROM è un'operazione un po' più complessa, infatti è presente un sistema di protezione
contro le scritture accidentali.
Il registro EECON2 in realtà non è un registro, viene soltanto impiegato per verificare che la scrittura non sia accidentale;
in particolare bisogna seguire un'essatta sequenza:
- mettere il dato in EEDATA
- scrivere l'indirizzo in EEADR
(queste due operazioni possono essere fatte in un qualunque momento)
- settare il bit WREN
- scrivere 0x55 in EECON2
- scrivere 0xAA in EECON2
- settare il bit WR per iniziare la scrittura
Inoltre la scrittura impiega più cicli istruzione, perciò prima di iniziare una nuova scrittura è indispensabile
controllare che il bit WR sia stato resettato dall'hardware.
; esempio : scrive 0x3A all'indirizzo 0x10 movlw 0x3A movwf EEDATA movlw 0x10 movwf EEADR bsf STATUS,RP0 bsf EECON1,WREN movlw 0x55 movwf EECON2 movlw 0xAA movwf EECON2 bsf EECON1,WR bcf STATUS,RP0
Gli interrupt (o interruzioni) sono un'importante carratteristica dei microprocessori
che permettono la gestione degli eventi esterni.
Un evento può essere moltissime cose, come la pressione di un tasto, la fine di una trasmissione, il cambiamento di
stato di una linea, ecc.; per ricollegarci alla lezione precedente, un evento potrebbe essere
l'avvenuta scrittura della EEPROM.
Normalmente per verificare, ad esempio, il cambiamento di stato di un ingresso (cioè il passaggio da 1 a 0 o viceversa)
dovremmo continuamente controllare quell'ingresso, aspettando che cambi; con le interruzioni, invece, possiamo
eseguire altre operazioni e sarà il PIC a segnalarci che lo stato della linea è cambiato.
Gli interrupt gestibili dal PIC16F84A sono :
- fronte di salita o discesa sul pin RB0
- cambiamento di stato sui pin RB4-RB7
- overflow del timer TMR0 (nella prossima lezione)
- scrittura avvenuta della EEPROM
Interrupt Vector
La routine che gestisce gli interrupt (Interrupt Handler) deve trovarsi all'indirizzo 0x04 (Interrupt Vector), perciò generalmente si usa questo codice:
ORG 0x00 goto Start ORG 0x04 ; istruzioni di gestione interrupt .... .... retfie Start ; inizio programma .... ....
Il registro INTCON
Il registro che gestisce gli interrupt è INTCON, al suo interno sono presenti i bit di abilitazione delle varie fonti
di interruzione e relativi flag.
GIE : abilitazione generale degli interrupt
EEIE : abilita l'interrupt sulla scrittura completata della EEPROM
T0IE : abilita l'interrupt sull'overflow del TMR0
INTE : abilita l'interrupt sul fronte di salita/discesa del pin RB0
RBIE : abilita l'interrupt sul cambiamento di stato dei pin RB4-RB7
T0IF : indica se l'interrupt è dovuto al T0IE
INTF : indica se l'interrupt è dovuto all' INTE
RBIF : indica se l'interrupt è dovuto all' RBIF
Per utilizzare uno o più interrupt bisogna settare il bit GIE e settare i bit di abilitazione degli interrupt
che ci interessa utilizzare.
Quando si verifica una condizione tale da far scattare un'interruzione, il bit GIE viene automaticamente resettato
e viene effettuato il salto all'Interrupt Vector; la routine di gestione è la stessa per tutte le interruzioni
(cioè si trova sempre e solo all'indirizzo 0x04) perciò se sono abilitati più tipi di interruzioni, bisogna controllare
i flag per sapere di quale si tratta.
Il Flag della EEPROM risiede come visto nel registro EECON1.
Finito l'Interrupt Handling si può tornare al programma attraverso l'istruzione retfie che, inoltre, riabilita gli interrupt settando il bit
GIE.
Prima di chiamare questa istruzione, bisogna resettare i flag in modo da permettere all'interruzione di verificarsi nuovamente.
Interrupt su RB0
Questo interrupt permette di "catturare" un fronte di salita o discesa sul piedino RB0; per scegliere quale fronte
è sufficiente agire sul bit INTEDG del registro OPTION (OPTION_REG[6]):
- se 1 : fronte di salita
- se 0 : fronte di discesa
Vediamo ora un esempio completo di come usare un'interruzione per accendere un led alla pressione di un tasto NA, e poi
spegnerlo con un'altra pressione.
Ne approfitto per segnalare la possibilità di abilitare delle resistenze di pull-up interne;
basta resettare il bit NOT_RBPU del registro OPTION (OPTION_REG[7]) per avere un pull-up per ogni pin settato come ingresso della Porta B;
questo ci consente di collegare facilmente dei pulsanti al PIC senza componenti aggiuntivi.
include "P16F84A.INC" ORG 0x00 goto Start ORG 0x04 comf PORTB,F ; inverte lo stato del LED bcf INTCON,INTF ; resetta il flag dell'interrupt retfie Start bsf STATUS,RP0 ; banco 1 movlw B'11111101' ; RB0 ingresso, RB1 uscita movwf TRISB bcf OPTION_REG,NOT_RBPU ; attiva pull-up bcf OPTION_REG,INTEDG ; fronte di discesa bcf STATUS,RP0 ; banco 0 bsf INTCON,INTE ; attiva interrupt su RB0 bsf INTCON,GIE ; abilita gli interrupt Main goto Main END
Se il pulsante non è anti-rimbalzo, il LED potrebbe accendersi o spegnersi in modo anomalo; per ovviare tale problema si protrebbe introdurre un ritardo di qualche millisecondo prima di uscire dall'Interrupt Handler.
Interrupt su RB4-RB7
In questo caso, l'interruzione viene generata ad ogni cambiamento di stato (sia fronte di salita che discesa)
su uno dei pin RB4-RB7; il flag settato è lo stesso per tutti i 4 pin, perciò è compito del programmatore verificare
quale sia il cambiamento avvenuto.
Preservare Status e W
Un interrupt può interrompere il programma principale in qualunque momento, per questo motivo la routine
di gestione delle interruzioni non deve in alcun modo influire sul comportamento del programma.
Qualunque operazione si effettui all'interno dell'Interrupt Handler, va inevitabilmente a modificare i
registri W e STATUS, perciò se questi vengono in qualche modo utilizzati nel software principale, risulta necessario
preservarne il contenuto:
ORG 0x04 movwf W_TEMP swapf STATUS, W movwf STATUS_TEMP .... .... swapf STATUS_TEMP, W movwf STATUS swapf W_TEMP, F swapf W_TEMP, W retfie
In questo snippet viene impiegata l'istruzione swapf perchè non influenza lo STATUS
Sleep
Spesso il PIC si trova in situazioni nelle quali non deve fare nulla, se non attendere un evento esterno; per
questo motivo è stato progettato un sistema di risparmio d'energia che limita il consumo di corrente fino a qualche uA.
Per attivarlo è sufficiente usare l'istruzione sleep che manda il micro in stand-by.
Il PIC può esserere "risvegliato" con uno di questi eventi:
- Reset tramite il pin MCRL'
- Timeout del WDT
- Interrupt su RB0, RB4-RB7 o fine scrittura della EEPROM
Durante lo sleep le periferiche interne vengono disattivate.
Se il risveglio (wake-up) è causato da un interrupt, subito viene eseguita l'istruzione che segue sleep, e solo
poi si passa all'Interrupt Handler; se questa condizione è indesiderata, è sufficiente inserire dopo sleep un nop.