release 3
Il microcontrollore PIC16F877A
di Ippolito Perlasca
Presentazione
Questa è una dispensa sul microcontrollore PIC16F877A. Non è completa: manca
la descrizione di alcune periferiche, ma le più importanti ci sono.
Le informazioni valgono anche per gli altri microcontrollori della famiglia PIC16,
tenendo presente che alcune periferiche sono assenti nei modelli di fascia più
bassa e la mappa di memoria può essere ridotta.
Premessa
Con il termine microcontrollore (microcontroller) si intende comunemente un sistema a
microprocessore integrato su un unico chip, che comprende, oltre alla CPU, una memoria di
programma non volatile (nei modelli più vecchi una EPROM o una EEROM, in quelli più recenti
una FLASH), una memoria RAM, generalmente di dimensioni ridotte, per gestire i risultati
intermedi dell'elaborazione e allocare lo stack, e periferici di I/O vari (porte seriali e/o parallele,
contatori/timer, convertitori A/D ecc.). Con queste caratteristiche, è evidente che i microcontrollori
sono stati concepiti soprattutto per applicazioni industriali di controllo, in cui il programma di
gestione, una volta messo a punto, non ha più la necessità di essere modificato (o di esserlo
raramente). Sono applicazioni che gli anglosassoni chiamano embedded, cioè "incorporate" in
prodotti e apparati finiti, che possono andare dagli elettrodomestici "intelligenti", alla domotica, ai
sistemi di comunicazione e sicurezza, ai decoder per la TV satellitare e digitale, alla
strumentazione, all'automazione in campo automobilistico (automotive).
La famiglia PIC16 della Microchip
Tra i numerosi tipi sul mercato, i microcontrollori prodotti dalla Microchip Technology Inc.
americana hanno negli ultimi anni guadagnato ampia diffusione, grazie alla loro concezione
innovativa, che li rende molto versatili e relativamente facili da programmare. La CPU dei
microcontrollori PIC (probabile acronimo di Programmable Integrated Controller), si distacca
dalla struttura di un microprocessore classico, essenzialmente perché è una RISC (Reduced
Instruction Set Computing, elaborazione con insieme di istruzioni ridotto), basata su una struttura
del tipo Harward (dall'università americana presso cui è stata sviluppata). L'architettura Harvard, a
differenza della macchina di Von Neuman classica, ha la memoria di programma e la memoria dati
separate e afferenti a bus diversi; da parte sua, la filosofia RISC consiste sostanzialmente nel
prevedere poche e semplici istruzioni, tutte della stessa lunghezza e (possibilmente) tutte
caratterizzate dallo stesso numero di cicli macchina sia per il fetch (caricamento) che per
l'esecuzione. Questa caratteristica (che, come vedremo, è soddisfatta dai PIC, con l'eccezione delle
istruzioni di salto), unita alla separazione fisica dei bus lungo cui fluiscono istruzioni e dati,
permette di ottenere una sovrapposizione (PipeLining, "incanalamento") della fase di fetch di
un'istruzione con quella di esecuzione della precedente "senza buchi" e quindi molto più efficiente
1
in termini di velocità di quanto sia possibile in una CPU tradizionale, in cui le istruzioni hanno
lunghezza diversa e quindi tempi di fetch diversi. Una ulteriore velocizzazione è dovuta al fatto che
la riduzione del numero di istruzioni, unita a quella dei tipi di indirizzamento, semplifica l'hardware
per la loro decodifica. Nei PIC, un'istruzione richiede quattro cicli di clock (un cosiddetto ciclo
macchina TCY, che è l'unità temporale su cui si svolgono le attività del microcontrollore) per essere
caricata e decodificata, e un altro ciclo macchina per essere eseguita; grazie però al pipelining,
mentre l'istruzione viene caricata, va in esecuzione l'istruzione precedente e, mentre l’istruzione
corrente viene eseguita, viene caricata la successiva (Figura 1)
Figura 1 – Ciclo di clock e ciclo macchina
(OSC1 è il segnale di clock. Il ciclo macchina TCY comprende quattro periodi di clock Q1, Q2, Q3,
Q4. PC sta per program counter, il contatore che contiene in ogni momento l'indirizzo della
prossima istuzione da catturare ed eseguire).
Il meccanismo di pipeling "perde un colpo" solo quando l'istruzione è di salto (Figura 2):
Figura 2 – Flusso "Pipeline" delle istruzioni
Infatti l'esecuzione un'istruzione di questo tipo (Execute 3 nell'esempio di Figura 2) interrompe
l'ordine lineare del programma e l'istruzione successiva, catturata nello stesso ciclo macchina (Fetch
4 in Figura 2), deve essere tolta ("Flush") dalla coda di pipelining senza essere eseguita.
2
In questo modo ogni istruzione richiede un ciclo macchina (4 cicli di clock) per essere eseguita, con
l'eccezione dei salti che richiedono due cicli e della prima istruzione, che ne richiede comunque uno
in più (Il 1° fetch non corrispondente a nessuna esecuzione).
I microcontrollori PIC si dividono in cinque famiglie. Le prime tre sono a 8 bit (parallelismo dei
dati) e si dividono fra loro per il formato e il numero (e quindi la flessibilità) delle istruzioni
famiglia di base (Base-Line PIC10
famiglia medio livello (Mid-Range PIC12, PIC16)
famiglia alto livello (High-End PIC18)
con istruzioni a 12 bit
con istruzioni a 14 bit
con istruzioni a 16 bit
ciascuna di queste tre famiglie mantiene la compatibilità software verso il basso, a livello di
linguaggio di programmazione intermedio (assembly). All'interno della singola famiglia il set di
istruzioni è lo stesso: i vari modelli si distinguono solo per quantità di memoria RAM e programma
(e tecnologia di quest'ultima) velocità e dotazione di periferiche (che però, come vedremo, non
richiedono istruzioni specifiche).
Esistono poi due altre famiglie di microcontrollori a 16 bit di parallelismo dati, una costituita da
microprocontrollori d'uso generale e alte prestazioni, l'altra con capacità DSP (Digital Signal
Processing, elaborazione digitale dei segnali). nel 2007 è stata introdotta un'ulteriore famiglia a 32
bit di parallelismo dati.
IL PIC 16F877A
E' un microcontrollore recente, che si situa all'estremità alta della famiglia Mid-Range. Lo
prendiamo come esempio perché è molto usato, estremamente versatile, dotato di numerosi
periferiche di I/O, utilizzato in molti sistemi didattici e tool di sviluppo facilmente reperibili e poco
costosi, che utilizzano la sua possibilità di lavorare in autoemulazione e in circuit-debugging
(vedremo poi), senza la necessità di costosi emulatori dedicati. E' anche riprogrammabile in
circuito.
In Figura 3 è riportato il pinout, che è lo stesso del PIC16F874A (che ha meno memoria, sia di
programma che dati). E' evidente che numerosi piedini assolvono a funzioni diverse.
3
Figura 3 – Piedinatura PIC16F877A
4
Riportiamo qui le caratteristiche principali del PIC16F877A (per l'elenco completo vedere i
datasheet del costruttore):
























Architettura RISC/Harward (abbiamo già visto cosa vuol dire).
Parallelismo dati di 8 bit.
Set di sole 35 istruzioni di una singola parola di 14 bit, tutte di un solo ciclo macchina (200
ns alla massima frequenza di clock di 20 MHz), tranne le istruzioni di salto che ne
richiedono 2. Il set d'istruzioni è lo stesso di tutta la famiglia Mid-Range PIC16.
Tecnologia CMOS, TTL –compatibile quando alimentato a 5V (ma la tensione di
alimentazione può andare da 2.0 a 5.5 V).
Basso consumo: < 0.6 mA a 4 MHz di clock (senza carichi esterni), < 1 µA in stand-by.
Alta corrente in uscita: 25 mA sink/source.
Interfaccia JTAG a 4 fili per la programmazione. Se i tre piedini d’interfaccia (il quarto è la
massa) non devono essere usati per altri scopi, il microcontrollore può essere riprogrammato
in circuito, e funzionare anche in autoemulazione (cioè funzionare sotto il controllo di un
software in ambiente Windows, che permette di intervenire sui registri del microcontrollore,
fissare un breakpoint, ecc.).
Frequenza di clock dalla continua (esclusa!) a 20 MHz, con varie opzioni di generazione
(gruppo RC, cristallo di quarzo, risuonatore ceramico) selezionabili in fase di
programmazione.
Memoria di programma FLASH di 8192 x 14 bit (quindi fino a 8192 istruzioni, 8 k).
368 byte di RAM, organizzati come registri a 8 bit, alcuni dei quali specializzati.
256 byte di memoria dati non volatile EEPROM (scrivibile, con tempi dell'ordine dei ms,
durante il normale funzionamento da programma.
Stack hardware (cioè separato dalle aree di programma e dati e non accessibile al
programmatore) profondo 8 livelli (si possono "nidificare" fino a 8 chiamate a subroutine
una dentro l'altra).
33 linee di I/O digitale programmabili indipendentemente e organizzate in 5 porte.
3 timer/contatori, due a 8 bit uno a 16.
1 convertitore A/D a 10 bit di risoluzione e 8 ingressi multiplexati.
1 USART (porta seriale asincrona standard).
1 porta seriale sincrona standard SSP e I2C.
1 porta parallela slave con segnali di abilitazione e read/write esterni.
2 comparatori analogici con sorgente di tensione di riferimento interna.
2 moduli Compare/Capture/PWM , per generare eventi (interrupt) su un conteggio
prestabilito del timer a 16 bit, per leggere il timer su un evento esterno, per generare, sempre
in connessione con il timer a 16 bit, segnali PWM per controlli di potenza.
14 possibili sorgenti di interrupt.
Presenza di un WatchDog Timer (WDT, temporizzatore “cane da guardia”), attivabile in
fase di programmazione, che genera un reset ogni circa 18 ms (espandibili a oltre 2 s con un
prescaler). Questa “ripartenza” ciclica del programma è utile per sbloccare il
microcontrollore da eventuali situazioni critiche o di blocco, in cui possa finire a causa di
particolari condizioni esterne (di input dal processo controllato).
Possibilità da programma di mandare il microcontrollore in uno stato di SLEEP (stand-by),
in cui l’esecuzione del programma è sospesa e il consumo ridotto. Dallo SLEEP il PIC esce
su reset esterno (livello basso su MCLR ), su interrupt o reset del WDT.
Possibilità di proteggere il codice di programma contro la lettura, molto importante nelle
applicazioni commerciali.
5
In Figura 4 è riportato lo schema a blocchi interno:
Figura 4 – Organizzazione interna del PIC16F877A
Notare la separazione, tipica dell’architettura Harward, fra la memoria di programma (Flash
Program Memory) e la memoria dati (RAM File Registers, Registri d’Archivio in RAM), nonché
fra Data Bus a 8 bit e Program Bus (Bus Istruzioni a 14). Al solito, una delle caratteristiche
principali di un microprocessore (e di un microcontrollore) è la sua dotazione di registri. Il nostro
amico ne ha 368 di uso generale, 55 specializzati, più un accumulatore o registro di lavoro
(Working Register W), che intrattiene una relazione speciale con l’ALU (Unità Aritmetico-Logica),
nel senso che è operando e possibile destinazione di tutte le operazioni aritmetico-logiche. Tutti
questi registri sono a 8 bit (anche quelli implicati negli indirizzamenti, vedremo poi come è
possibile accedere a tutta la memoria di programma, che richiede 13 bit d’indirizzo: 8192 = 213).
6
Con l’eccezione di W, che non ha indirizzo e a cui le istruzioni si riferiscono implicitamente, tutti
gli altri registri (i cosiddetti RAM File Register, registri RAM di archivio), sono individuati da un
indirizzo a 9 bit da 0 (000h) a 511 (1FFh) e sono suddivisi in 4 blocchi contigui di 128 registri
ciascuno:
Blocco 0 da 000h a 07Fh
Blocco 1 da 080h a 0FFh
Blocco 2 da 100h a 17Fh
Blocco 3 da 180h a 1FFh
Poiché 368 registri di uso generale + 55 specializzati fanno un totale di 423 registri e non 4 x 128 =
512, prima di concludere che i conti non tornano è opportuno dare un’occhiata alla mappa dei
registri d’archivio in Figura 5:
Figura 5 – Mappa dei registri RAM di archivio del PIC16F877A
7
Cominciamo a notare che le locazioni grigie sono in maggioranza non implementate (se lette danno
tutti 0, probabilmente servono a futuri sviluppi) o ( solo un paio) riservate. Consideriamo poi le
locazioni indicate da un nome: sono i 55 registri specializzati, che servono alla gestione della CPU e
dei periferici. Molti nomi però si ripetono: per esempio il registro STATUS (registro di stato)
compare nella stessa posizione in tutti e 4 i blocchi. Non significa che questi registri sono costituiti
da più byte: sono sempre registri a 8 bit, ma sono mappati su più indirizzi: ad esempio, si può
accedere al registro di stato STATUS dall’indirizzo 003h (blocco 0), dall’indirizzo 083h (blocco 1),
da quello 103h (blocco 2) e da quello 183h (blocco 3): notate che sono indirizzi che differiscono fra
loro solo per i due bit alti (di ordine 8 e 7), dei 9 che li costituiscono.
L’indirizzamento della memoria dati 1
La particolare organizzazione dei registri d’archivio pone qualche problema nel loro accesso.
Consideriamo ad esempio l’istruzione MOVWF, che copia il registro di lavoro in un registro di
archivio. Ad esempio:
MOVWF 0x12A ; copia W nel registro di indirizzo 12Ah, che sta nel blocco 2
(Per i particolari, vedere la sezione sulle istruzioni e quella sulla programmazione; tenere presente
che il prefisso 0x in assembly indica un numero esadecimale). Se guardiamo il codice operativo
(codice macchina a 14 bit) di MOVWF (Figura 20), vediamo che è 0000001fffffff, dove fffffff
indicano i sette bit di indirizzo del registro d’archivio di destinazione. Se guardiamo i codici delle
altre istruzioni che accedono a registri di archivio, la cosa si ripete: nel codice ci sono solo sette bit
per specificare il registro. Ma 12Ah = 100101010b, ci sono due bit in più che nell’assemblaggio
(traduzione in linguaggio macchina) vanno persi: il codice operativo di MOVWF 0x12A diventa
infatti:
00000010101010b
dove i bit ombreggiati sono i sette bassi di 12Ah 2 .
1
Da qui in avanti per comprendere quanto segue è necessario avere qualche nozione sulle istruzioni del PIC e sulla sua
programmazione. Tuttavia, per non interrompere il discorso sulla struttura del microcontrollore, rimandiamo ogni volta
che sia necessario ai paragrafi Il set di istruzioni e Descrizione delle istruzioni. Il lettore non si faccia problemi a
saltare avanti e indietro: non sempre un percorso lineare è il modo migliore per comprendere un argomento
(relativamente) complesso.
2
Lo stesso problema c’è anche se usiamo nomi simbolici: se ad esempio abbiamo definito BLOB come 12Ah,
MOVFW BLOB in linguaggio macchina sarà ancora 00000010101010b, anche se BLOB = 12Ah = 100101010b.
8
Come fa allora la CPU a riferirsi al registro giusto? Il meccanismo, piuttosto complicato, è illustrato
Figura 6
Figura 6 – Indirizzamento diretto/indiretto
Cominciamo con il caso dell’indirizzamento diretto, in cui nell’istruzione c’è l’indirizzo del registro
operando (è quello appena visto): i 7 bit bassi (6 – 0) dell’indirizzo vengono presi dal codice
operativo (opcode), i due alti (8 - 9) sono i due bit specializzati RP1:RP0 del registro STATUS
(rispettivamente i bit 6 e 5). In altri termini RP1:RP0 selezionano il banco RAM attivo (00 banco 0,
01 banco 1, 10 banco 2, 11 banco3). Ma attenzione:
RP1:RP0 non vengono settati automaticamente: la loro gestione è a carico del
programmatore, che deve ricordarsi di alzarli/abbassarli a seconda in che banco stia il
registro cui vuole accedere.
Nel nostro esempio, la sequenza d’istruzioni potrebbe essere:
BSF
BCF
MOVWF
STATUS,RP1
STATUS,RP0
0x1AE
; Alza RP1
; Abbassa RP0
; Esegui l’istruzione
Dove BSF, BCF sono istruzioni che rispettivamente alzano e abbassano un bit specificato di un
registro specificato (vedi paragrafo sulle istruzioni); RP1 e RP0 sono definiti come costanti
rispettivamente uguali a 6 e a 5 nel file .inc (vedi programmazione). l'istruzione BCF STATUS,RP0
può essere inutile, se RP0 non è stato alzato prima: RP1:RP0 sono bassi al reset e all’accensione
(cioè il banco di default e lo 0).
9
In linea di massima, dopo aver avuto accesso ai banchi RAM 1 – 3, conviene ritornare in banco 0,
ove ci sono i registri di solito più usati:
BCF
BCF
STATUS,RP1
STATUS,RP0
; Torna al
; banco 0
Un meccanismo simile vale per l’indirizzamento indiretto, cioè per le istruzioni in cui l’indirizzo
del registro operando è variabile, e contenuto in un altro registro. Nei PIC, per rispettare l’esigenza
dell’architettura RISC che tutte le istruzioni abbiano lo stesso formato, non ci sono istruzioni
specifiche a indirizzamento indiretto: esiste un registro indice FSR (mappato su tutti 1 4 blocchi
agli indirizzi 004h, 084h, 104h, 184h) in cui va caricato l’indirizzo del registro su cui si vuole
operare (o meglio, gli 8 bit bassi dei 9 che costituiscono l’indirizzo). Una qualunque istruzione che
opera sui registri d’archivio può operare sul registro puntato da FSR riferendosi al registro fittizio
INDF (indirizzi 000h, 080h, 100h, 180h); se però il registro puntato è in uno dei due blocchi
superiori (cioè ha 1 nel nono bit d’indirizzo), il nono bit, che non ci sta in FSR, va messo nel bit
IRP (MSB del registro STATUS): IRP va cioè alzato (vedi ancora Figura 6).
Ad esempio, a sequenza d’istruzioni (vedi Set di istruzioni):
MOVLW
MOVWF
MOVLW
MOVWF
PORTB
FSR
255
INDF
; Carica in W l’indirizzo POTB
; Passalo in SFR
; Carica in W il dato 255
; Passalo a INDF (in realtà a PORTB)
carica 255 in PORTB, se IRP è basso (default); infatti PORTB è nel blocco 1, e pertanto il MSB del
suo indirizzo è 0.
Ma se vogliamo caricare lo stesso dato in EEDATA, che sta nel blocco 2 (MSB di indirizzo = 1)
dobbiamo scrivere:
BSF
MOVLW
MOVWF
MOVLW
MOVWF
BCF
STATUS, IRP
EEDATA
FSR
255
INDF
STATUS, IRP
; In W e poi in SFR solo gli 8 bit bassi
; dell’indirizzo EEDATA
Il puntamento delle istruzioni
Anche il meccanismo di puntamento delle istruzioni richiede qualche chiarimento: il Program
Counter (PC) è a 13 bit (213 = 8192 locazioni di programma). La parte bassa (8 bit) di PC è PCL,
che è un registro accessibile sia in lettura che scrittura, come d’altra parte tutti i registri d’archivio,
specializzati o no. PCL è mappato in tutti i quattro blocchi, rispettivamente agli indirizzi 002h,
082h, 102h, 182h. La parte alta di PC (5 bit), non è direttamente accessibile, ma si possono
modificare indirettamente attraverso il registro PCLATH , anch’esso mappato sui quattro blocchi
(00Ah, 08Ah, 10Ah, 18Ah). PCLATH è a 8 bit, ma solo i 5 più bassi sono scrivibili e hanno
significato.
Quando un’istruzione modifica PCL (ha cioè PCL come destinazione), i 5 bit alti di PC vengono
copiati dai 5 bit bassi di PCLATH (figura 7, immagine in alto):
10
Figura 7 – Il caricamento del Program Counter in differenti situazioni
Quando invece viene eseguita un salto incondizionato GOTO ADDR, o una chiamata a subroutine
CALL ADDR, dell’indirizzo ADDR (di 13 bit) nel codice operativo dell’operazione trovano posto
solo gli 11 bit inferiori. Ad esempio, GOTO ha codice 101kkkkkkkkkkk, dove gli 11 k
rappresentano appunto il “posto” per tali bit. I due bit mancanti gli arrivano dai bit <4:3> di
PCLATH (Figura 7, immagine in basso); gli altri bit di PCLATH vengono ignorati. Poiché al
reset/accensione PCLATH assume il valore 0, non c’è bisogno di ricordarsi di lui se i salti sono
nella 1a pagina di 211 = 2048 locazioni di memoria (indirizzi 0000h – 07FFh). Se invece si deve
saltare in un’altra pagina, bisogna caricarlo con i bit alti dell’indirizzo:
MOVLW
MOVWF
HIGH ADDRR
PCLTH
GOTO
ADDR
; HIGH è una direttiva che estrae il byte alto di ADDR
; Metti il byte alto di ADDR in PCLATH (i bit <12.11>
; di ADDR vanno nei bit <4:3> di PCLATH)
; In questa istruzione vengono considerati gli 11 bit
; bassi di ADDR.
Il registro di stato
Nella tabella 2.1 del Datasheet è riportata la descrizione sintetica dei 55 registri specializzati,
insieme con la condizione che assumono al reset. La maggior parte di questi registri è connessa alla
gestione dei periferici (vedi oltre). Tra gli altri, un ruolo fondamentale lo ricopre il registro di sato
STATUS (Figura 8):
Figura 8 – Il registro di stato
(R/W sta per bit leggibile/scrivibile, R per bit di sola lettura. I valori segnati sono quelli al reset di
accensione; x indica un valore imprevedibile)
11
Il registro STATUS, che è accessibile da tutti i quattro banchi RAM (indirizzi 003h, 083h, 103h,
183h), è costituito da 8 bit (flag) che vengono influenzati dal risultato di operazioni o da particolari
eventi, e altri che concorrono, come già visto, all’indirizzamento dei registri d’archivio. Ecco il loro
significato, a partire da LSB:
- C (bit 0) Flag di carry: segnala un riporto oltre agli 8 bit (overflow) nelle operazioni di
somma. Vedi scheda 1 per il suo comportamento nelle sottrazioni (che non coincide con
quello di un flag di prestito, come ci si aspetterebbe).
- DC (bit 1) Flag di Digit Carry: segnala un riporto dal nibble (4 bit) basso a quello alto in
un’operazione di somma.
- Z (bit 2) Flag di zero: va alto se il risultato dell’operazione logico/aritmetica precedente è 0,
basso altrimenti. E’ influenzato anche da alcune operazioni di trasferimento (vedi
descrizione delle istruzioni).
- PD (bit 3) Flag di Power Down (riduzione di potenza, standby): va a 1 (inattivo) durante la
temporizzazione di Power-Up o per un’istruzione CLRWDT (azzeramento del watchdog
timer); viene portato a 0 (attivo). dall’istruzione di SLEEP.
- TO (bit 4) Flag di Time Out: va a 1 (inattivo) durante la temporizzazione di Power-Up o per
un’istruzione CLRWDT: viene attivato (0) dal time-out del watchdog timer. In sostanza,
PD e TO permettono, dalle loro combinazioni, di individuare se un reinizio di programma
(reset) è stato determinato da un normale reset hardware su MCLR , se invece si tratta di
un’uscita dal modo SLEEP, o di un time-out del watchdog timer, o ancora di una
combinazione di queste condizioni. Notare che PD e TO sono gli unici bit di STATUs che
possono solo essere letti, ma non scritti.
- RP1, RP0 (bit 6 e 5) Flag di selezione della pagina (blocco) RAM: selezionano il blocco di
registri d’archivio nell’indirizzamento diretto, secondo lo schema:
<RP1:RP0> Blocco indirizzato
Indirizzi
00
Blocco 0
000h – 07Fh
01
Blocco 1
080h – 0FFh
10
Blocco 2
100h – 17Fh
11
Blocco 3
180h – 1FFh
(vedi sopra, Indirizzamento memoria dati).
- IRP (bit 7) Flag di selezione pagine (blocchi RAM) nell’indirizzamento indiretto. 0
seleziona i blocchi 0:1 (indirizzi 000h – 0FFh), 1 i blocchi 2:3 (indirizzi 100h – 1FFh). Vedi
sopra, Puntamento delle istruzioni.
E’ importante notare che STAUS, come altri registri di servizio, è accessibile indipendentemente
dal blocco RAM selezionato, grazie al fatto di essere mappato su quattro indirizzi che differiscono
solo per i due bit superiori (quelli appunto di selezione blocco). Questo è essenziale per poter
arrivare a RP1:RP0: se accedere a STATUS richiedesse il loro settaggio, che si fa attraverso
STATUS…
12
La gestione degli interrupt nel PIC16F877A
Il PIC16F877A ha 3 possibili sorgenti di interrupt, comuni a tutti i microcontrollori della famiglia
PIC16, più tutti quelli che possono essere generati dai periferici specifici dell’877°. Quelle comuni
a tutta la famiglia sono:
Interrupt esterni dal pin RB0/INT.
Interrupt su cambiamento di livello su almeno uno degli ingressi RB7:RB4.
Interrupt su overflow del counter/timer TMR0.
Possono poi generare interrupt il convertitore A/D, la porta seriale, i timer 1 e 2, i moduli
Compare/Capture ecc., per un totale complessivo di 14 sorgenti.
Non esistono interrupt non mascherabili: tutti gli interrupt sono “filtrati” da un flag di abilitazione
generale degli interrupt GIE (bit 7 del registro INTCON), che di default è basso (interrupt
disabilitati).
La logica degli eventi legati a un’interrupt è la seguente: ogni periferica che può chiedere
interruzione ha un “flag d’interruzione” che si alza quando si verificano le condizioni previste per
l’interrupt; la CPU testa i flag d’interruzione dei periferici ogni ciclo Q1 del clock, cioè all’inizio
del ciclo macchina(vedi Figura 9):
Figura 9 – Temporizzazione degli interrupt (esempio dell’interrupt esterno su RB0/INT)
Se ne trova uno alto (e se è quel periferico è abilitato a generare interrupt), l’indirizzo di rientro
viene salvato sullo stack hardware, e il PC caricato con 0004h, locazione dove di conseguenza la
CPU salta, con un ritardo (tempo di latenza) di 2 – 3 cicli macchina. In 0004h deve iniziare la
routine di gestione, o un salto alla routine di gestione.
13
Il vettore d’interruzione è pertanto unico: è il software che può (di solito deve) individuare quale
periferico tra quelli abilitati ha chiesto interrupt interrogando (polling) i loro flag d’interruzione.
Quando il PIC risponde a un’interruzione, ulteriori interrupt vengono disabilitati dall’abbassamento
automatico di GIE. E’ possibile, anche se non molto raccomandabile, permettere la nidificazione
degli interrupt, rialzando GIE (che è scrivibile come tutti i flag di INTCON) all’interno della routine
di gestione.
Al rientro dall’interruzione il flag GIE si rialza automaticamente, riabilitando le interruzioni.
E’ assolutamente importante ricordare che il flag d’interruzione della periferica non si riabbassa
automaticamente: poiché il flag alzato che causa interrupt, ci si deve ricordare di abbassarlo
all’interno della routine di gestione – di solito alla fine della routine – per impedire che al rientro
l’interrupt riparta immediatamente, innescando un loop infinito.
Il meccanismo degli interrupt non è semplicissimo da gestire; per capirlo meglio si può vedere la
Figura 10, che lo traduce in uno schema logico:
Figura 10 – Logica di interruzione (nell’877A manca la coppia di flag GPIF/GPIE)
La richiesta di interrupt alla CPU è un livello alto all’uscita dell’ultimo AND a destra, da cui si vede
che è inibita da GIE = 0
14
Ogni periferico ha un flag d’interruzione, che si alza comunque sempre quando si verificano le
condizioni che potrebbero chiedere interrupt, e un flag di abilitazione specifico. A titolo d’esempio,
consideriamo gli interrupt dalla linea RB0/INT: il flag d’interruzione è INTF e quello d’abilitazione
è INTE. Dallo schema si vede che perché la CPU “senta” l’innalzamento di INTF, non basta che sia
alto GIE, ma deve essere alto anche INTE: i flag di abilitazione specifici dei periferici permettono
quindi di selezionare quali di essi possono chiedere interrupt.
La richiesta d’interrupt da parte dei periferici specifici dell’877A è generata dalla combinazione
ANDs – OR a sinistra, e si vede che perché tale richiesta “passi” è necessaria un’ulteriore
abilitazione rappresentata dal flag PEIE (PEripheral Interrupt Enable). Nel registro principale
collegato agli interrupt – INTCON – ci sono i flag di interrupt e abilitazione interrupt delle tre
sorgenti di base, oltre a GIE e PEIE (Figura 11).
Figura 11
Di INTF e INTE abbiamo già detto. TMR0IF/TMR0IE è la coppia flag d’interrupt/flag
d’abilitazione interrupt relativa al Timer 0, RBIF/RBIE quella relativa all’interrupt da cambiamento
su RB7:RB4. Le coppie relative agli altri periferici sono sparse in altri registri specializzati. Vedere
la documentazione relativa ai singoli periferici. Notare che anche INTCON è mappato in tutti i
banchi di memoria.
Riassumendo, la programmazione degli interrupt richiede i seguenti passi:
In fase di inizializzazione:
1. Alzare il flag di abilitazione interruzione della periferica e, se la periferica è esterna al core,
anche PEIE
2. Se non si è sicuri dello stato del flag d’interruzione della periferica, abbassarlo per sicurezza
3. Alzare il flag di abilitazione generale GIE
Nella routine d’interruzione, che deve iniziare in 0x0004:
1. Salvare STATUS e W in una coppia di registri opportunamente definiti
2. Se le sorgenti d’interruzione possono essere più d’una, controllare i flag d’interruzione per
stabilire quale periferico ha chiamato, ed eseguire di conseguenza le operazioni richieste
dalla gestione del periferico stesso (se il periferico che può chiamare interruzione è uno solo,
il controllo sui flag necessario)
3. Abbassare il flag d’interruzione del perififerico chiamante
4. Recuperare STATUS e W
5. Rientrare dall’interrupt (istruzione RETFIE)
Ecco un esempio abbastanza generale e completo di gestore di interrupt, che può essere facilmente
adattato ad esigenze specifiche. Si suppone che possano chiamare interrupt il Timer 0 (flag
d’interruzione TMR0IF) e l’ingresso RB0/INT (flag d’interruzione INTF). Le routine di servizio dei
due interrupt sono rispettivamente sbr_1 e sbr_2 (che devono terminare con l’istruzione di rientro
15
da subroutine RETURN). Ricordare che il tutto deve essere allocato in 0x004 (mediante una
direttiva org 0x004):
interr
MOVWF W_TEMP
SWAPF STATUS,W
MOVWF STATUS_TEMP
BTFSC
CALL
BTFSC
CALL
BCF
BCF
INTCON,TMR0IF
sbr_1
INTCON,INTF
sbr_2
INTCON,TMR0IF
INTCON,INTF
SWAPF STATUS_TEMP,W
MOVWF STATUS
SWAPF W_TEMP,F
SWAPF W_TEMP,W
RETFIE
;
;
;
;
;
;
;
;
;
;
;
;
;
;
;
;
;
Salva w
e lo stato
(il perché di questa complicazione lo si
capisce al rientro)
Se è interrupt da Timer 0
chiama sbr_1 e rientra
Se è (anche) interrupt da RB0/INT
chiama sbr_2 e rientra
Abbassa i
flag d'interruzione (in realtà solo
uno sarà di solito alto)
Recupera
lo stato
e poi W (con una finezza per evitare di
alterare il flag Z,
che è influenzato da movf, ma non da swapf)
Ritorna dall'interrupt
Al solito fare riferimento a Descrizione delle istruzioni per i particolari. Ricordare in ogni caso che
BTFSC scavalca l’istruzione successiva se il bit specificato del registro specificato è basso, e che
CALL è la chiamata a subroutine. Così il segmento di programma
BTFSC INTCON,TMR0IF
CALL
sbr_1
scavalca l’istruzione CALL sbr_1 se il flag TMR0IF del registro INTCON è basso, cioè
Timer 0 non ha chiamato interrupt; se invece il flag è alto, cioè è Timer 0 ad aver
chiamato, l’istruzione di chiamata viene eseguita e la CPU salta ad eseguire sbr_1.
Notare che il sistema permette di gestire interrupt che si sovrappongano, stabilendo fra
essi una scala di priorità: se, mentre la CPU sta rispondendo all’interrupt ad Timer 0,
si verifica la condizione per un interrupt sull’ingresso RB0/INT, il flag INTF si alza
(indipendentemente da ogni abilitazione), di modo che, al rientro da sbr_1, il test
BTFSC INTCON, INTF
lo trova alto e chiama sbr_2 3 . Notare che l’inverso non avviene: se, durante l’esecuzione
di sbr_2 in risposta all’interrupt da RB0/INT, si verifica una richiesta da Timer 0,
questa viene ignorata, perché TMR0IF non viene più interrogato. Se si vuole escludere la
possibilità di sovrapposizione, si può modificare il gestore così:
interr
ret
MOVWF W_TEMP
SWAPF STATUS,W
MOVWF STATUS_TEMP
BTFSC
GOTO
BTFSC
GOTO
BCF
BCF
SWAPF
MOVWF
SWAPF
SWAPF
RETFIE
; Salva w
; e lo stato
; (il perché di questa complicazione lo si
; capisce al rientro)
INTCON,TMR0IF
; Se è interrupt da Timer 0
sbr_1
; chiama sbr_1
INTCON,INTF
; Se è interrupt da RB0/INT
sbr_2
; chiama sbr_2
INTCON,TMR0IF
; Qui al rientro da sbr_1 o sbr_2: abbassa i
INTCON,INTF
; flag d'interruzione (in realtà solo
; uno sarà di solito alto)
STATUS_TEMP,W
; Recupera
STATUS
; lo stato
W_TEMP,F
; e poi W (con una finezza per evitare di
; alterare il flag Z,
W_TEMP,W
; che è influenzato da movf, ma non da swapf)
; Ritorna dall'interrupt
3
Ciò non è in contraddizione con il fatto che GIE è basso e inibisce uleriori interrupt: la CPU è già in interrupt, quando legge il
secondo flag d’interruzione
16
Qui sbr_1 e sbr_2 vengono chiamate da un salto incondizionato GOTO, e devono terminare
con GOTO ret, in modo da rientrare entrambe nello stesso punto, senza la possibilità che
vengano eseguite entrambe.
Una spiegazione a parte richiede lo strano modo di salvare W e STATUS, e di recuperarli
al rientro. Anzitutto, i registri W_TEMP e STATUS_TEMP è bene che vadano definiti nella
fascia di indirizzi 0x70 – 0x7F, che si “specchia” in tutti i quattro blocchi RAM, in
modo da non doverci preoccupare di che banco è indirizzato quando si verifica
l’interrupt. In secondo luogo, la soluzione che parrebbe ovvia:
MOVWF W_TEMP
MOVF
STATUS,W
MOVWF STATUS_TEMP
; Salvataggio
MOVF
STATUS_TEMP,W
MOVWF STATUS
MOVF
W_TEMP,W
; Recupero
;
;
;
;
non va bene, perché le istruzioni MOVF STATUS_TEMP e W MOVF W_TEMP,W toccano il
flag di zero (vedi Descrizione delle istruzioni) e quindi possono alterare lo stato ripristinato (il flag
di zero fa parte di STATUS). L’unica istruzione che può copiare un registro RAM in W senza
alterare i flag è SWAP (con destinazione W), che però lo fa invertendo i nibble (blocchi di 4 bit)
alto e basso: basta allora usarla complessivamente due volte per ogni salvataggio/recupero. La
sequenza corretta è allora 4 :
MOVWF W_TEMP
SWAPF STATUS,W
MOVWF STATUS_TEMP
; Salvataggio
;
;
;
;
SWAPF
MOVWF
SWAPF
SWAPF
STATUS_TEMP,W
STATUS
W_TEMP,F
W_TEMP,W
; Recupero
Un’ultima precauzione riguarda la disabilitazione generale degli interrupt da programma: la
soluzione ovvia BCF INTCON,GIE è rischiosa; se interviene un interrupt durante l’esecuzione di
questa istruzione, questa viene terminata prima del salto a 0x0004. A questo punto GIE è basso, ma
la CPU ha già accettato l’interrupt (nella prima fase di TCY), per cui salta alla routine di gestione, al
rientro della quale GIE viene alzato di nuovo. Si va sul sicuro controllando che effettivamente GIE
sia andato basso ed eventualmente riprovandoci:
riprova
BCF INTCON,GIE
BTFSC INTCON,GIE
GOTO riprova
Le porte di I/O digitale 5
4
Se nel programma si tocca PCLATH (perché si deve caricare nel Program Counter un indirizzo > FFh o saltare oltre
l'indirizzo 7FFh, vedi paragrafo Il puntamento delle istruzioni) bisogna ricordarsi di salvare anche questo registro (e
poi ripristinarlo). Altrimenti, nel corso della gestione dell'interrupt o al ritorno da esso, possono verificarsi salti errati e
imprevedibili. Vedere a titolo d'esempio le macro pop e push nel programma PWM_2.asm.
5
La traduzione corretta dell’inglese PORT sarebbe “PORTO”, e non “PORTA”. Ma qui ci conformiamo alla
terminologia più diffusa.
17
Il PIC16F877A ha 5 porte di I/O digitale, indicate con PORTA, PORTB, PORTC, PORTD,
PORTE, che costituiscono le connessioni del microcontrollore con il mondo esterno. PORTB,
PORTC e PORTD sono dotate di 8 linee di I/O, PORTA di 6 e PORTE solo di 3, per un totale di
33 linee. Tutte le linee sono programmabili, singolarmente e indipendentemente, come input o
come output e possono essere usate per scopi generici di I/O. Molte di loro sono però in
“comproprietà” con i periferici specializzati (porta seriale, convertitore A/D, comparatori, timer
ecc., vedi sui datasheet la documentazione dei vari periferici, nonché quella sui port di I/O). Tre
linee di PORTB servono in programmazione (Vedi paragrafo sulla Programmazione del PIC).
Alcune linee hanno particolarità elettriche specifiche (pull-up interno, 3-state, capacità analogiche
ecc); tutte però possono comunque essere utilizzate in modalità TTL-compatibile con correnti di
sink/source relativamente alte (25 mA) 6 .
Le porte sono viste come registri d’archivio RAM (figura 5), i cui bit corrispondono alle linee di
I/O (nel caso di PORTA e PORTE corrispondono alle linee esterne rispettivamente i 6 e 3 bit più
bassi, gli altri non hanno significato). Sulle porte sono possibili due operazioni:
Scrittura
La scrittura di un byte su una porta ha l’effetto di emettere i livelli corrispondenti in uscita
sulla linee dichiarate in output (a un bit 1 corrisponde un livello alto, a un bit 0 quello
basso), mentre non ha effetto sulle linee dichiarate in input.
Lettura
La lettura di un byte da una porta legge i livelli in ingresso alle linee dichiarate in input (
livello alto = 1, livello basso = 0), e i livelli scritti precedentemente su quelle dichiarate in
output (queste ultime sono dotate di latch, di modo che le uscite permangono sull’ultimo
valore scritto, fino a una nuova scrittura).
Per dichiarare le linee di una porta in ingresso o in uscita, è necessario scrivere una maschera di un
byte in un registro di controllo corrispondente. I bit posti a 1 corrispondono a linee in ingresso,
quelli posti a 0 a linee in uscita. Ogni porta PORTx ( x = A, B, C, D o E) ha un registro di controllo
associato TRISx. Le porte sono mappate nel blocco 0 di registri RAM, i registri di controllo le
affiancano nel blocco 1: ad esempio, PORTA ha indirizzo 005h = 000000000101b e il suo registro
di controllo TRISB è in 085h = 000010000101b (figura 5). Notare che PORT B e TRISB hanno
una seconda mappatura, rispettivamente nel blocco 2 e nel blocco 4.
Ad esempio, la sequenza di operazioni:
BSF
MOVLW
MOVWF
BCF
STATUS,RP0
‘1111000’b
TRISD
STATUS,RP0
; TRISD è in banco 1
; Torna in banco 0
manda le quattro linee superiori di PORTD (RD7 – RD4) in input, e le quattro inferiori
(RD3 – RD0) in input. Di default (all’accensione o dopo un reset), tutte le linee di tutte le
porte sono in input. La direzione delle linee può essere riprogrammata in ogni momento nel
6
Le linee RA0, RA1, RA2, RA3, RA5 di PORTA e tutte le tre linee di PORTE sono di default ingressi analogici. Per
usarle come I/O digitali bisogna configurare opportunamente i bit PCFG3:PGFG0 del registro ADCON1 (vedi
fig. 18 e successiva tabella, paragrafo sul convertitore AD).
Ricordare che AN7, AN6, AN5 corrispondono a RE2, RE1, RE0 e AN4, AN3, AN2, AN1, AN0 a
RA5, RA3, RA2, RA1, RA0.
18
corso del programma; è un’opportuna avvertenza programmare (lasciare) in input le linee
non utilizzate, per evitare il rischio di cortocircuiti accidentali 7 .
A parte il fatto che, come si è detto, numerose linee di I/O sono multiplexate sui periferici
interni, le principali differenze fra le varie linee sono:
RA4 (linea 4 di PORTA) in ingresso è dotata di trigger di Schmitt, in uscita è opendrain (sullo 0 è connessa a massa, ma su 1 è aperta).
Le linee di PORTB in ingresso possono avere un pull-up interno, attivabile
abbassando il bit RBPU (il 7) del registro di configurazione OPTION_REG (che è
mappato nei blocchi 1 e 3, rispettivamente agli indirizzi 081h e 181h, vedi più avanti
figura 13). Di default i pull-up sono disabilitati.
RB0 (linea 0 di PORTB), quando usata come ingresso di interrupt (INT), e dotata di
trigger di Schmitt.
Le linee di PORTD in ingresso sono dotate di trigger di Schmitt (ma NON quando
PORTD viene usata come Parallel Slave Port, porta parallela asservita).
Le tre linee di PORTD sono anch'esse di tipo trigger, quando lavorano in input.
Le periferiche
Qui di seguito diamo una descrizione sintetica delle periferiche più importanti del PIC16F877A. Per
ulteriori chiarimenti vedere i listati dei programmi d'esempio.
Iniziamo da una periferica che fa parte del "core" del microcontrollore, e che è presente in tutti i
microcontrollori della famiglia PIC16:
Il Timer 0
E' un contatore in avanti a 8 bit, che può essere scritto e letto in ogni momento, dotato di prescaler
programmabile via software. Il clock può essere interno (Fosc/4, cioè stesso periodo del ciclo
macchina TCY) o esterno (pin RA4/T0CKI). Può generare interrupt nell'overflow da FFh a 00h. Con
il clock interno può servire da temporizzatore (timer), con quello esterno funziona da contatore
(counter) di eventi. Lo schema a blocchi semplificato del complesso Timer 0/Prescaler è riportato in
figura 12.
7
Se si va a vedere la struttura interna di una linea di I/O (riferirsi ai datasheets), ci si accorge che i latch di uscita sono
a monte dei gate comandati dai registri TRIS: ciò significa che un PORT può essere scritto anche se non è orientato
(totalmente o parzialmente) in uscita; quando eventualmente il corrispondente registro TRIS viene riprogrammato, i
valori memorizzati nel latch escono sulle linee che vengono mandate in output. Questo vuol dire tra l'altro che una linea
di output può essere mandata in 3-state riprogrammandola in input, senza che ciò comporti la perdita del dato in uscita,
che "ricompare" quando la linea viene riportata in output. Tuttavia, va fatta attenzione all'uso sui PORT delle istruzioni
che leggono-modificano-scrivono, come quelle logico/aritmetiche con destinazione il registro stesso (ad es.
ANDWF PORTB,F) o quelle che modificano singoli bit (BCF e BSF). Supponiamo ad esempio di utilizzare BSF per
settare una linea di uscita di un PORT: il problema è che la CPU prima legge il byte dal PORT, poi cambia il bit
indicato, poi riscrive sul PORT il byte modificato; ma in corrispondenza delle linee dichiarate in ingresso i bit letti sono
quelli che arrivano dall'esterno, e sono questi che vengono poi riscritti nel latch d'uscita, eventualmente modificando i
valori precedenti. Se la programmazione delle linee non cambia, non c'è problema; ma se una linea che prima era in
ingresso viene portata in uscita, il valore che su essa compare è imprevedibile. Evitare quindi di usare questo tipo
d'istuzioni su PORT che debbono cambiare direzione nel corso dell'esecuzione del programma!
19
Figura 12 – Schema semplificato del Timer 0
Il registro di conteggio è TMR0 (mappato agli indirizzi 001h e 101h) e, come si è detto, e di
lettura/scrittura; la configurazione del contatore/timer dipende dai bit <0:5> del registro di opzioni
OPTION_REG (figura 13):
Figura 13 – Registro OPTION_REG (indirizzi 081h, 181h)
TOCS seleziona la sorgente di clock: 0 = clock interno sul ciclo-macchina, 1 = clock esterno da
RA4/T0CKI (default).
TOSE seleziona il fronte attivo del clock esterno: 0 = discesa, 1 = salita (default).
PSA attribuisce il prescaler al Timer 0 (1) o al Watchdog Timer (1, default).
PS2, PS1, PS0 selezionano il rapporto di divisione del prescaler, secondo la tabella qui sotto
riportata. 8
Tabella 1 – Rapporto divisione prescaler TMR0 (e WDT)
(i bit PS2:PS0 sono i bit 2:0 del registro OPTION_REG)
PS2:PS0 Rapporto div. TMR0 Rapporto div. WDT
000
1:2
1:1
001
1:4
1:2
010
1:8
1:4
011
1:16
1:8
100
1:32
1:16
101
1:64
1:32
110
1:128
1:64
111
1:256
1:128
8
Gli altri due bit di OPTION_REG sono RPBU , che è il flag di pull-up su PORTB (di cui si è già parlato) e
INTDEG , che seleziona il fronte di salita/discesa per l'interrupt da RB0/INT (vedi esempi di programmazione).
20
Notare da figura 13 che ,quando il prescaler è attribuito al Watchdog Timer, il Timer 0 prende
direttamente il clock dalla sorgente (interna o esterna), con un rapporto di divisione che è quindi
1:1.
Di per sé, il Timer 0 è sempre abilitato, di default sul clock esterno: si disabilita solo quando la CPU
va in sleep. Al timer sono associati anche due flag del registro di configurazione interrupt
INTCON, di cui abbiamo già parlato (vedi figura 11):
TMR0IF che è il flag d'interruzione che si alza quando il timer "trabocca" (overflow) da 256 (FFh)
a 0. Come tutti i flag d'interruzione non si riabbassa da solo, e va resettato prima di ritornare
dall'interruzione.
TMR0IE che abilita la richiesta d'interruzione da parte del timer (ma non blocca TMR0IF, che si
alza comunque all'overflow, rivedere il paragrafo sulla Gestione degli interrupt). Lo stato attuale
del conteggio può essere letto da TMR0 in qualunque momento. TMR0 può anche essere
scritto,cioè inizializzato a un valore , ma si deve tener presente che una scrittura inibisce
l'incremento di contatore e prescaler di due cicli macchina TCY; inoltre, se il prescaler è assegnato al
Timer 0, una scrittura lo azzera. Ad esempio, se il prescaler (assegnato al timer) è su un rapporto di
divisione 1:2 e il clock è interno, una scrittura su TMR0 ritarda l'incremento del contatore di 4 TCY
(due di inibizione e due di incremento del prescaler, che riparte da 0). In sostanza, dopo la scrittura
di N , con un rapporto r impostato nel prescaler, l'overflow è il conseguente innalzamento di T0IF
avviene dopo a un tempo . Il ritardo massimo (con il prescaler su 256:1) è comunque 256·256 =
65536 cicli macchina TCY (63.536 ms con un quarzo da 4 Mhz). Quando il viene usato il clock
esterno (funzione di conteggio), senza prescaler deve essere rispettato un tempo minimo di durata
dell'impulso (sia per la fase alta che per quella bassa) di 0.5TCY  20 ns ; con il prescaler questo
T  40 ns
, con
tempo può scendere a 10 ns , ma il periodo deve in ogni caso essere almeno CY
N
N fattore di prescaling ( 2  256 ). Per i particolari vedere i datasheet.
Il Timer 1
E' un timer/contatore a 16 bit, il cui conteggio è allocato in due registri TMR1H (byte alto, indirizzo
00Eh) e TMR1L (byte basso, indirizzo 00Fh). Il conteggio va da 0000h a FFFFh (up) e riparte da
0000h. L'overflow è segnalato dall'innalzarsi del flag d'interruzione TMR1IF, bit 0 del registro
PIR1 (00Ch). La richiesta di interrupt è "filtrata" attraverso il flag di abilitazione specifico TMR1IE
(bit 0 di PIE1, 08Ch) e quello PEIE di abilitazione interrupt dai periferici esterni al core (che sta in
INTCON), nonché ovviamente da GIE. Valgono peraltro tutte modalità relative agli interrupt e alla
loro gestione (vedi paragrafo relativo). Timer 1 può funzionare da temporizzzatore (con il ciclo
macchina TCY usato come clock, o come contatore su un clock esterno(fronti di salita su
RC0/T1OSO/T1CKI); nella modalità contatore può anche funzionare come temporizzatore con il
clock (asincrono rispetto al clock di sistema) dato da un quarzo connesso fra RC0/T1OSO/T1CKI e
RC1/T1OSI/CCP2. Timer 1 è dotato di un prescaler con rapporto di divisione da 1:1 a 1:8; a titolo
d'esempio, con una frequenza di clock tipica di 4 Mhz e quindi TCY = 1µs, il prescaler al massimo e
il conteggio che parte da 0 si ottiene un ritardo massimo di 8·216 µs = 524288 µs (poco più di mezzo
secondo).
Il timer può essere resettato dai moduli CCP (vedi) ed è controllato in programmazione dal registro
T1CON (figura 14):
21
Figura 14 – Registro di controllo del Timer 1
Qui di seguito la funzione dei singoli bit:
T1CKPS1, T1CKPS0 fissano il rapporto di divisione del prescaler:
00
1 (default)
01
2
10
4
11
8
T1OSCEN
abilita (1)/disabilita (0, default) l'oscillatore a quarzo esterno.
T1SYNC
controlla la sincronizzazione del clock esterno su quello di sistema
(vedi più sotto). Ignorato se TMR1CS = 0.
TMR1CS
seleziona la sorgente di clock esterna (1) o interna su FOSC/4 (0, default).
TMR1ON
portato a 1 abilita il Timer 1. Di default il timer è disabilitato (0).
L'uso di Timer 1 è immediato come temporizzatore: la lettura/scrittura dei registri di conteggio può
avvenire in qualunque momento, perché il conteggio (come l'operazione stessa) è sincronizzato su
TCY. Lo stesso accade nell'uso come contatore se T1SYNC  0 , perché l'incremento del conteggio
è determinato dal fronte di salita del segnale esterno, ma viene ritardato in modo da essere sincrono
con TCY (ciò significa che l'impulso di clock esterno deve durare più di 2TCY).
Con il clock esterno e in modo asincrono T1SYNC  1 le cose si complicano, perché tra l'accesso
al registro TMR1H e quello aTMR1L il contatore può andare in overflow. La cosa più semplice è
fermare temporaneamente il conteggio abbassando TMR1ON immediatamente prima dell'accesso e
rialzandolo subito dopo. Naturalmente ciò introduce un ritardo nel conteggio di almeno 4TCY (due
per l'accesso ai registri è due per l'abilitazione/disabilitazione). Il modo di conteggio asincrono su
clock esterno è l'unico in cui Timer 1 funziona anche nello stato di sleep.
Il Timer 2
E' un timer che conta i cicli macchina TCY all'indietro (down counter). Al solito il registro di
conteggio (TMR1, indirizzo 011h) è leggibile/scrivibile, ma la caratteristica più importante del
timer è quella di essere dotato di un registro a 8 bit di periodo (PR2, indirizzo 092h), anch'esso
leggibile/scrivibile. Quando il conteggio in TMR1 eguaglia il contenuto di PER2, un comparatore
genera un segnale di reset per il contatore, che riparte da 0 (vedi figura 15):
22
Figura 15 – Struttura di Timer 2
Quindi PR2 determina in effetti il periodo del ciclo di conteggio: dopo rPOST cicli di conteggio, dove
rPOST, con 1 ≤ rPOST ≤ 16, è il valore del rapporto di divisione di un postcaler (vedi sempre figura
15), si alza il flag di interrupt TRM2IF (che è soggetto alle solite condizioni sui flag di interrupt).
Il timer è anche dotato di un prescaler che divide il clock (ciclo macchina) per un rapporto di
divisione . Il reset del timer (TMR2 Ouput in figura 15), che non è accessibile all'esterno, può
essere scelto via software come base dei tempi del modulo SSP (Synchronous Serial Port, Porta
Seriale sincrona).
Timer 2 è controllato dal registro T2CON (figura 16)
Figura 16 – Registro di controllo del Timer 2
TOUTPS3:TOUTPS0 selezionano il rapporto di divisione del postscaler d'uscita: il rapporto (da 1 a
16) è il valore espresso in binario dai quattro bit, aumentato di 1. Default 1.
TMR2ON abilita (1) / disabilita (0, default) il timer.
T2CKPS01, T2CKPS0 selezionano il rapporto di divisione del prescaler:
00
01
1x
1
4
16
Il flag d'interruzione TMR2IF e il rispettivo flag di abilitazione TMR2EI stanno rispettivamente nei
registri PIR1 e PIE1, gli stessi dei flag di Timer 1.
23
Il convertitore A/D
Il convertitore analogico/digitale del PIC16F877A è un convertitore ad approssimazioni successive
con risoluzione a 10 bit (1 parte su 1024) e 8 ingressi multiplexati. Il suo range d'ingresso è fissato
da due tensioni di riferimento VREF+ e VREF-, che possono essere fissate dall'esterno nel range
VSS ≤ VREF- < VREF+ ≤ VDD (sacrifivando uno o due ingressi), o posti internamente a VREF+ = VDD
e VREF- = VSS (di solito VREF- = VSS = 0V). In ogni caso la relazione fra la tensione in ingresso Vi e
il dato numerico a 10 bit N restituito dal convertitore è 9 :
Vi 
VREF   VREF 
N  VREF 
1024
il convertitore è dotato di circuito di track & hold interno. Il risultato della conversione viene posto
in una coppia di registri ADRESH (byte alto, indirizzo 01Eh ) e ADRESL (byte basso, indirizzo
09Eh). Al convertitore sono associati anche due registri di controllo ADCON0 (indirizzo 01Fh) e
ADCON1 (indirizzo 09Fh), i cui bit configurano le varie opzioni del convertitore e gestiscono
l'inizio e la fine della conversione (figura 17 e figura 18):
Figura 17 – Registro ADCON0
ADCS2:ADCS0 selezionano il clock del convertitore (ADCS2 è in ADCON1, vedi oltre):
000 = FOSC/2 (FOSC frequenza clock di sistema)
001 = FOSC/8
010 = FOSC/32
011 = FRC (oscillatore resistenza/capacità interno al modulo AD)
100 = FOSC/4
101 = FOSC/16
110 = FOSC/64
111 = FRC (come 011)
La frequenza prescelta determina il tempo di conversione, che è 12TAD, dove TAD è il periodo del
clock del convertitore. D'altra parte TAD deve essere almeno 1.6 µs; ad esempio, con un clock di
sistema di FOSC = 4 MHz, FOSC/8 = 500 kHz va bene, perché dà TAD = 1/(500 kHz) = 2 µs, ma
FOSC/2 = 1 MHz no, perché TAD = 1µs.
Il clock a oscillatore RC interno (FRC) dà un TAD variabile da 2 µs a 6µs, tipico di 4 µs. La sua
scelta permette di far funzionare il convertitore con la CPU in stato di sleep, che è una soluzione
ottimale per la precisione della conversione, perché sono "spente" le sorgenti di transienti e quindi
di possibili disturbi (clock di sistema, timer, ecc.).
CHS2:CHS0 selezionano il canale d'ingresso da convertire:
000 =
canale 0 (RA0/AN0)
001 =
canale 1 (RA1/AN1)
010 =
canale 2 (RA2/AN2)
011 =
canale 3 (RA3/AN3)
100 =
canale 4 (RA5/AN4)
9
Notare che il fondo scala VREF+ non è raggiungibile, perché il valore massimo possibile per N è 1023.
24
101
110
111
=
=
=
canale 5 (RE0AN5)
canale 6 (RE1/AN6)
canale 7 (RE2/AN7)
Il canale prescelto deve essere dichiarato come input analogico (vedi ADCON1)
GO/ DONE
è il bit di stato della conversione: va portato a 1 per far partire la conversione, e
ritorna automaticamente a 0 quando la conversione è finita.
ADON
abilita (1) / disabilita (0, default) il convertitore. Se non lo si usa è meglio lasciarlo
disabilitato, per non consumare energia.
Figura 18 – Registro ADCON1
ADFM
seleziona il formato del risultato a 10 bit della conversione:
Quando è 1 il risultato è giustificato a destra nella coppia di registri
ADRESH:ADRESL: i due bit MSB sono nei due bit bassi di ADRESH, gli
altri 8 in ADRESL. I 6 bit alti di ADRESH sono a 0.
Quando è 0 (default) il risultato è giustificato a sinistra: i primi 8 bit sono in
ADRESH, gli ultimi due nei due bit alti di ADRESL. I sei bit bassi di
ADRESL sono a 0. In questo caso, se ci si limita a leggere ARESH, si ottiene
una conversione a 8 bit.
ADCS2
contribuisce con ad ADCS1:ADCS0, che stanno in ADCON1 (vedi sopra), a
selezionare la frequenza del clock di conversione. Default 0; con il default 00 di
ADCS1:ADCS0 dà FOSC/2 .
PCFG3:PCFG0
configurano gli ingressi connessi al convertitore come canali analogici,
ingressi di riferimento o input digitali:
25
Tabella 2 – configurazioni ingressi analogico/digitali
I pin usati in ingresso (analogici, di riferimento o digitali) devono essere dichiarati tali nei registri
TRIS corrispondenti.
Quando si seleziona per la prima volta un canale, o lo si cambia, è necessario aspettare un tempo
minimo (detto impropriamente di acquisizione sul datasheet) prima di far partire la conversione, per
permettere al condensatore interno di hold di caricarsi alla tensione d'ingresso. Con una sorgente di
resistenza interna non superiore a 10 kΩ questo tempo minimo è un po' meno di 20 µs. La
conversione dura 12TAD, prima di ripeterla si devono aspettare 2TAD. Ciò significa, su un singolo
canale, un tempo totale di acquisizione (ripetuta) di 14TAD; di fatto questo tempo coincide con il
periodo minimo di campionamento. A titolo d'esempio, con un quarzo da 4 MHz e FOSC/8, si arriva
a 28 µs, che corrisponde a una frequenza di Nyquist di 1/(28 µs) = 36 Khz circa (vedere i datasheet
per i particolari).
La gestione dell'ADC
Il modo più semplice è far partire la conversione alzando il flag GO/ DONE , poi intrappolare il
programma in un ciclo di polling indefinito, aspettando che il flag si riabbassi:
; Lettura da AN0
BSF
STATUS,RP0
MOVLW
b'11111111'
MOVWF
TRISA
MOVLW
b'10001110'
MOVWF
ADCON1
; PORTA in ingresso,
; in particolare RA0/AN0
; Solo RA0/AN0 come input analogico, VREF interne,
; risultato giustificato a destra
;(anche ADCON1 è in bank 1)
26
BCF
MOVLW
MOVWF
MOVLW
MOVWF
delay DECFSZ
GOTO
BSF
ADC
BTFSC
GOTO
STATUS,RP0
b'01000001’
ADCON0
7
0x72
0x72,F
delay
ADCON0,GO_DONE
ADCON0,GO_DONE
ADC
;
;
;
;
;
Ritorna in bank 0 (dove sta ADCON0)
Clock FOSC/8, accendi ADC (ancora in stop)
e seleziona AN0
Ritardo di acquisizione di circa 20 us
Registro contatore
; Ciclo
; di ritardo
; Start di conversione
; Aspetta fine conversione
; Dato pronto in ADRESH:ADRESL
In alternativa si può rilevare la fine conversione su interrupt: il flag d'interruzione del convertitore,
che si alza (come GO/ DONE ) alla fine di conversione è ADIF nel registro PIR1; il relativo flag di
abilitazione (ADIE) si trova in PIE1. Oltre GIE in INTCON va alzato anche PEIE.
Il modo più raffinato è, come già detto, lavorare in sleep. L'inizializzazione del convertitore è come
nell'esempio precedente, ma si deve alzare ADIE e anche PEIE. Fatto partire il convertitore alzando
GO/ DONE , si manda in SLEEP la CPU; quando la conversione finisce, la richiesta di interrupt da
parte del convertitore fa uscire la CPU dallo stato di sleep. Se GIE è abbassato, la CPU riparte
dall'istruzione successiva a quella di SLEEP; se GIE è alzato la CPU va in interrupt è riparte da
0x004. In ogni caso ricordarsi di abbassare il flag d'interruzione ADIF, che non si riabbassa da solo.
La porta seriale universale sincrona/asincrona indirizzabile (USART) 10
Qui ci limitiamo a descriverne il funzionamento asincrono, che è di gran lunga il più usato; in
questa modalità la porta seriale è in grado di implementare una comunicazione full-duplex secondo
lo standard RS232 11 : è un metodo comodo, semplice e robusto per interfacciarsi a un PC (almeno
finché la RS232 non verrà sostituita completamente dallo standard USB), a strumenti e a periferici
di comunicazione (modem). Più in generale, con opportune interfacce a livello fisico, la
comunicazione seriale sincrona funziona su un qualunque canale in grado di trasmettere serialmente
bit (radio, infrarossi, fibre ottiche).
L'USART del PIC16F877A (e degli altri PIC che l'hanno in dotazione), fa capo a due piedini di
PORTC: RC6/TX/CK per l'uscita della sezione di trasmissione e RC7/RX/DT per l'entrata di quella
di trasmissione (i due pin vanno inizializzati rispettivamente come uscita e come ingresso tramite
TRISC).
Il cuore del trasmettitore è costituito da un registro di serializzazione (un PISO), che invia
serialmente il byte contenuto nel registro di trasmissione TXREG (019h), incapsulandolo tra un bit
di start (livello logico 0) e uno si stop (livello logico 1). Il flag d'interruzione TXIF (registro PIR1,
00Ch) associato al modulo trasmettitore segnala che il dato in TXREG è già stato trasferito nel
PISO per la trasmissione, e che quindi si può scrivere in TXREG un nuovo byte da trasmettere,
10
Il PIC16F877A ha un'altra interfaccia seriale: il Master Synchronous Serial Port Module (MSSP),
che serve a gestire le comunicazioni seriali I2C e SPI. Qui per brevità non ne parliamo, ma per un
esempio di sensore interfacciato via I2C si rimanda alla dispensa I2C.pdf .
11
Per una descrizione dello standard RS232 si rimanda alla dispensa seriale.pdf . Da tener presente che lo standard
RS232 prevede una segnalazione bipolare su due livelli di tensione; l'USART del PIC lavora invece su livelli TTL, per
cui, se si vuole comunicare con una porta seriale standard (come quella del PC), bisogna interporre un traslatore di
levello tipo MAX232 o simile. Molto spesso sulle demoboard (ad esempio la PICDEM 2 PLUS della Microchip) il
traslatore di livello è già previsto).
27
senza rischio di sovrascrivere il precedente. TXIF può essere interrogato in polling o generare
interrupt, se il relativo flag di abilitazione TXIE è alto (insieme a PEIE e GIE). Notare che TXIF, a
differenza dei flag d'interruzione degli altri periferici, si resetta automaticamente via hardware
quando il registro TXREG viene scritto.
Il trasmettitore inserisce automaticamente un bit di stop, cioè garantisce che la linea resti a livello 1
per un tempo almeno pari a quello di bit. Se si deve comunicare con un dispositivo che richiede più
di un bit di stop, è sufficiente aspettare di più, inserendo un ritardo software in coda alla
trasmissione di un dato.
Simmetricamente, nel modulo ricevitore il dato seriale (ripulito dai bit di start e di stop) viene
riparallelizzato da un SIPO e caricato nel registro RCREG (01Ah), da dove può essere letto. La
presenza di un dato da leggere è segnalato dall'innalzamento del flag d'interruzione del modulo
ricevente RCIF (che sta anche lui in PIR1).
RCREG è in realtà un registro FIFO a due livelli, di modo che può contenere due dati successivi in
attesa di essere recuperati in due letture successive. RCIF (anche qui a differenza degli altri flag
d'interruzione) si resetta automaticamente quando il FIFO è vuoto, cioè quando tutti (al più due) i
dati ricevuti sono stati letti: RCIF segnala quindi quando c'è un dato nuovo non ancora letto. Il
controllo di RCIF e la susseguente lettura può avvenire in polling, o meglio su interrupt , con RCIE
alto (ovviamente insieme a PEIE e a GIE).
All'USART sono poi associati un registro di stato per il trasmettitore (TXSTA, 098h), e uno per il
ricevitore (RCSTA, 018h), che comprendono alcuni flag indicatori delle attività in corso, nonché
vari bit di configurazione, e un registro SPBRG che serve a determinare la velocità di trasmissione
(baud rate). Vediamoli nei particolari:
Figura 19 – Registro stato di trasmissione
CSRC
non interviene nella trasmissione asincrona.
TX9
seleziona, quando è a 1, la trasmissione in formato 9 bit di dati (invece che i soliti 8);
di solito il nono bit è riservato al bit di parità. Di default TX9 è 0, e la trasmissione è
a 8 bit.
TXEN
abilita (1) /disabilita (0. default) la trasmissione.
SYNC
seleziona il modo di funzionamento sincrono (1) o asincrono (0, default)
dell'USART, sia in trasmissione che in ricezione.
BRGH
seleziona la modalità con cui il contenuto del registro SPBRG determina il baud rate.
Sono possibili il modo alta velocità (!) e bassa velocità (0, default). Sul loro
significato e come scegliere l'uno o l'altro, vedere più sotto la descrizione di SPBGR.
BRGH ha significato solo nel modo asincrono.
TRMT
è il bit di stato dello shift register di trasmissione. Va a 1 quando lo shift register è
vuoto, a zero se il dato precedente non è stato ancora (completamente) trasmesso.
28
Potrebbe essere usato per sapere se si può trasmettere un nuovo dato, ma per questa
funzione è più semplice usare il flag d'interruzione TXIF (vedi oltre).
TX9D
è l'eventuale nono bit di dato, che si "accoda" in trasmissione al byte in TXREG, se
TX9 è alto. Come si è detto, viene di solito usato (quando è usato) per il bit di parità,
che deve essere calcolato via software a cura del programmatore. Un altro possibile
uso è "marcare" il byte trasmesso come indirizzo di un dispositivo destinatario dei
successivi dati (che è poi la ragione per cui l'UART è detta "indirizzabile", vedi
anche ADDEN in RCSTA).
Figura 20 – Registro stato di ricezione
SPEN
è l'abilitazione generale dell'USART, che funziona (in ricezione o in trasmissione o
in entrambe) solo se questo flag è a 1. Valore di default 0 (USART disabilitata).
RX9
seleziona (1) la ricezione dell'eventuale nono bit di dato. Se è a 0 (default), la
ricezione è a 8 bit.
SREN
non interviene nella ricezione asincrona.
CREN
posto a 1 abilita la ricezione continua, a 0 (default) la disabilita. Di solito lo si deve
alzare, perché è raro che si debba ricevere un singolo byte.
ADDEN
flag di abilitazione riconoscimento indirizzo. Ha rilevanza solo nel modo asincrono a
9 bit (RX9 alto): quando ADDEN = 1 un byte ricevuto non viene trasferito nel
registro di ricezione RCREG finché non è 'marcato' dal nono bit a 1, cioè il byte non
è un indirizzo. Quando ADDEN = 0, il trasferimento del byte ricevuto in RCREG
avviene normalmente, e il nono bit (letto da RX9D) deve essere letto come parte del
dato (eventualmente di parità, nel qual caso la sua correttezza va controllata via
software).
FERR
va 1 quando si verifica un errore di frame: dove il ricevitore si aspetta il bit di stop
(livello logico 1) gli arriva un bit a 0. Di solito la causa è un diverso baud rate fra
trasmettitore e ricevitore. Viene resettato se l'errore non si verifica più) leggendo il
registro di ricezione RCREG e ricevendo il successivo byte valido.
OERR
bit di overrun: va a 1 per segnalare che nello shift register di ricezione è arrivato un
nuovo dato prima che i due precedenti siano stati letti (ricordiamo che il registro di
ricezione RCREG è un FIFO a due livelli); di conseguenza il dato nello shift register
non può essere trasferito nel FIFO, che è pieno, e viene perso.
Per resettare OERR si deve abbassare CREN (e poi rialzarlo se si vuole riabilitare la
ricezione continua).
RX9D
contiene l'eventuale nono bit del dato ricevuto.
29
Il registro di Baud Rate SPBRG (099h)
Questo registro controlla il periodo di un contatore pilotato dal clock di sistema, che genera a sua
volta il clock di trasmissione/ricezione seriale. La frequenza di serializzazione, cioè il numero di bit
trasmessi per secondo è il Baud Rate. Nella trasmissione asincrona il byte X contenuto in SPBRG è
legato al Baud Rate B e alla frequenza del cloc di sistema FOSC da una relazione che dipende dal
valore del flag BRGH in TXSTA:
Tabella 3 – Selezione modalità di generazione del Baud Rate
BRGH
Relazione fra X, Baud Rate e FOSC
0 (bassa velocità)
B = FOSC/(64*(X+1))
1 (alta velocità)
B = FOSC/(16*(X+1))
Le formule si possono facilmente invertire, per ricavare il valore X da scrivere in SPBRG; il
problema che X deve essere un intero fra 0 e 255, e non sempre è possibile sceglierlo in modo da
avere esattamente il valore di Baud Rate desiderato. Se entrambe le formule sono applicabili,
conviene sceglier il valore che dà l'errore più contenuto. Nel datasheet sono riportate utili tabelle
con i valori da caricare in SPBRG, per i più comuni valori di clock e di Baud Rate, con gli errori
relativi. Qui limitiamoci a un esempio:
supponiamo di avere FOSC = 4 MHz e di volere un Baud Rate di 19200 (valore standard). La
formula con BRHG = 0 invertita dà
F
4  10 6
X  OSC  1 
 1  2.256
64  B
64  19200
L'intero più vicino è X = 2, che però dà un Baud Rate effettivo di
FOSC
4  10 6
B

 20833
64  ( X  1) 64  3
20833  19200
 0.085  8.5% , che rischia di non essere tollerato dalla
con un errore relativo di
19200
porta seriale di alcuni PC (specie se si usa uno di quegli adattatori seriale/USB per portatili).
Molto meglio in questo caso assumere BRHG = 1:
F
4  10 6
X  OSC  1 
 1  12.021  12
16  B
16  19200
FOSC
4  10 6

 19231 , con un errore di solo:
Il valore effettivo è allora B 
16  ( X  1) 16  13
19231  19200
 0.0016  0.16%
19200
L’uso della porta seriale
L’utilizzazione della porta seriale richiede innanzitutto l’inizializzazione generale (Baud Rate,
modo asincrono, abilitazione della comunicazione) e quella della trasmissione e/o ricezione.
La trasmissione è molto semplice se, come è di solito, la comunicazione è a 8 bit senza controllo di
parità: è sufficiente aspettare in polling indefinito che TXIF si alzi, per indicare che il trasmettitore
è libero, e scrivere il dato da trasmettere su TXREG.
30
La ricezione di solito la si fa in interrupt su RCIF, perché in una comunicazione asincrona, che per
definizione può avvenire a tempi qualunque e indeterminati, non avrebbe senso bloccare la CPU in
attesa di un dato che non si sa quando arriva, La routine di gestione dell’interrupt comprenderà
evidentemente la lettura del dato da RCREG e il suo utilizzo (o memorizzazione). Se nel buffer
FIFO di RCREG ci sono due dati, RCIF non si abbassa alla lettura del primo, per cui al rientro
dall’interruzione, quando GIE si rialza, la CPU riparte di nuovo in interrupt, leggendo anche il
secondo dato; solo a questo punto RCIF si abbassa (automaticamente), e la lettura dell’USART è
per il momento terminata.
Se si vogliono gestire gli errori, è meglio leggere i flag di errore FREE (e OERR) prima di leggere
il dato, perché la lettura resetta FERR.
Nel caso del nono bit (di dato o di parità), in trasmissione la sua scrittura nel flag TX9D va fatta
prima della scrittura degli altri 8 bit in TXREG, perché la scrittura in TXREG fa partire
immediatamente la trasmissione.
Anche in ricezione è più comodo leggere RC9D prima di RCREG, perché RC9D sta in RCSTA,
che come abbiamo visto deve essere letto per primo per ricuperare i flag d’errore.
Un esempio di gestione della porta seriale è riportato in IOSeriale.asm: il byte ricevuto viene
inviato in uscita su PORTD, mentre il dato in ingresso a PORTB viene trasmesso. In pratica è un
sistema di I/O a un byte interfacciabile via RS232 a un qualunque PC dotato di porta seriale. Dal
lato PC si può usare un qualunque programma di lettura/scrittura sulla seriale (Hyperterminal, per
esempio); una buona soluzione, che fa molto “sistema di controllo”, è utilizzare lo strumento
virtuale Labview IOSeriale.vi (eseguibile IOSeriale.exe). Sorgente ed eseguibili sono scaricabili dal
sito Tramontana.
Le procedure descritte in IOSeriale.asm sono assolutamente standard e organizzate in forma
modulare, in modo da poter essere facilmente riciclate in altre applicazioni. Inoltre il programma è
un buon esempio di come dovrebbe essere organizzato e commentato un listato assembly appena
non banale.
; IOSeriale.asm
; Comunicazione full-duplex 19200 baud, 1 bit di start, 1 bit di stop, niente parità.
; Input su PORTB, Output su PORTD
list
#include
p=16f877A,r=dec
<p16f877A.inc>
; Scelta uC
; File di definizioni relative al uC
__CONFIG _CP_OFF & _WDT_OFF & _BODEN_OFF & _PWRTE_ON & _LVP_OFF & _XT_OSC
; Configurazione:
Code Protect
OFF
Watchdog Timer
Brown Out Detect
Power Up Timer
Low Voltage Programming
XTAL Oscillator
;
;
;
;
;
OFF
OFF
ON
OFF
;****** DEFINIZIONE COSTANTI
BRDIV
EQU
12
; Rapporto divisione Baud Rate Generator per
; 19200 Baud con quarzo da 4 MHz e BGRH = 1.
;****** DEFINIZIONE VARIABILI
W_TEMP
STATUS_TEMP
EQU
EQU
0x71
0x72
; Per salvare il contesto (le locazioni 0x070 - 0x07F
; sono "specchiati" nei banchi superiori.
; Attenzione: la locazione 0x70 è usata in autoemulazione!)
;************************************************************************
31
org
0x000
; Qui al reset
goto
start
; Vai all'inizio programma
;****** GESTORE INTERRUPT RICEZIONE SERIALE
ORG
skip
0x004
movwf
swapf
movwf
bcf
W_TEMP
STATUS,W
STATUS_TEMP
STATUS,RP0
btfss
goto
PIR1,RCIF
skip
; Qui all'interrupt
;
;
;
;
;
;
;
;
;
call
service
;
swapf
STATUS_TEMP,W ;
movwf STATUS
;
swapf
W_TEMP,F
;
swapf
W_TEMP,W
;
retfie
;
Salva w
e stato
(se serve, l'uso di swapf si capisce al ripristino)
Chi lo sa in che banco era al momento dell'interruzione
(ma PIR1 è in bank 0)
E' l'interrupt di ricezione seriale?
Se no, vai oltre (o a un'altra routine di servizio, il
controllo è solo in vista di eventuali sviluppi, in cui più ;
periferici possano chiedere interrupt)
Se sì, chiama il servizio
Recupera
lo stato
e poi W (con una finezza per evitare di alterare il flag Z,
che è influenzato da movf, ma non da swapf)
Ritorna dall'interrupt
;****** SERVIZIO SU RICEZIONE DATO SERIALE (Qui emissione dato ricevuto su PORTD
;
e trasmissione del dato in input su PORTB)
service
f_err
go_on
o_err
btfsc
goto
movf
movwf
movf
call
goto
movf
btfss
return
bcf
bsf
return
RCSTA,FERR
f_err
RCREG,W
PORTD
PORTB,W
send
go_on
RCREG,W
RCSTA,OERR
RCSTA,CREN
RCSTA,CREN
;
;
;
;
;
;
;
;
;
;
;
;
;
Se non c'è errore
di frame
leggi il dato in arrivo
e mandalo fuori su PORTD
Leggi in ingresso PORTC
e chiama la trasmissione
Prosegui
Lettura dummy per resettare RCIF (se il buffer è vuoto)
Se non c'è errore di overrun
ritorna
Altrimenti prima resetta la ricezione
(CREN a 0, poi di nuovo a 1) e di conseguenza
anche il flag OERR
;****** ROUTINE DI TRASMISSIONE
send
btfss PIR1,TXIF
goto
send
movwf TXREG
return
; aspetta che sia finita
; la trasmissione del dato precedente
; e trasmetti il nuovo dato (che si suppone in W)
;****** INIZIALIZZAZIONE COMUNICAZIONE SERIALE
;
;
;
;
start
Un po' pedante: molti settaggi sono già di default, ma così è più chiaro.
Allo stesso modo settare i singoli flag, invece che caricare direttamente
nei registri la parola binaria di configurazione, è più lungo ma
più leggibile.
bsf
movlw
movwf
movlw
movwf
bcf
STATUS,RP0
BRDIV
SPBRG
b'10111111'
TRISC
PIE1,TXIE
bcf
bsf
bcf
bsf
bsf
bcf
bcf
bsf
bsf
bcf
bsf
bsf
TXSTA,SYNC
TXSTA,BRGH
TXSTA,TX8_9
TXSTA,TXEN
PIE1,RCIE
STATUS,RP0
RCSTA,RC8_9
RCSTA,CREN
RCSTA,SPEN
PIR1,RCIF
INTCON,PEIE
INTCON,GIE
;
;
;
;
;
;
;
;
;
;
;
;
;
;
;
;
;
;
;
In bank 1 ci sono TRISC, TXSTA, PIE1, SPRBG
Calcola il rapporto
di divisione per il Baud Rate
RC7/RX/DT in input, RC6/TX/CK in output
(gli altri in input per sicurezza)
Disabilita interrupt di trasmissione
(per TX usiamo il polling)
Modo asincrono
Rapporto di divisione Baud rate alto
Trasmissione 8 bit (senza parità)
Abilita trasmissione
Ricezione in interrupt
RCSTA, PIR1, INTCON sono in bank 0
Ricezione su 8 bit (senza parità)
Ricezione continua
Abilita comunicazione seriale
Abbassa flag dato ricevuo, per sicurezza
Abilita interrupt da periferiche
Abilita la CPU a ricevere interrupt
32
;****** INIZIALIZZAZIONE ALTRI PORT
bsf
movlw
movwf
movlw
movwf
bcf
STATUS,RP0
b'11111111'
PORTB
b'00000000'
PORTD
STATUS,RP0
;
;
;
;
;
;
Di nuovo in bank 0
PORTB
in input
PORTD
in output
Torna in bank 0
;****** PROGRAMMA PRINCPALE
loop
goto
loop
; Gira gira, aspettando un dato dalla porta seriale
; (qui di solito qualcosa di più utile!)
end
33
I moduli CCP (Capture/Compare/PWM)
Il PIC16F877A, come molte altre CPU della famiglia, è dotato di due moduli
Capture/Compare/PWM (Cattura/Comparazione/Modulazione a durata d'impulso). Sono strutture
che interagiscono con il Timer 1 (e in modalità PWM anche con il Timer 2) che permettono in
modo relativamente semplice e largamente indipendente dal software di realizzare tre importanti
funzioni:



La cattura e memorizzazione del contenuto corrente (a 16 bit) di Timer 1, al verificarsi di
un fronte di discesa o salita su un ingresso riservato: serve per cronometrare eventi.
La comparazione del contenuto attuale di Timer 1 con un valore a 16 bit memorizzato in
una coppia di registri, e la generazione di un evento (set di un'uscita e/o generazione di un
interrupt) quando tale timer e valore prefissato coincidono: serve a temporizzare eventi, cioè
a generarli a un istante prefissato.
La generazione di un segnale PWM a duty cycle variabile. Su quest'ultima funzione, molto
utile in applicazioni di controllo, ci soffermeremo in particolare.
A ciascun modulo CCP sono associati tre registri a 8 bit: due registri dati CCPRHx e CPPRLy
(x = 1 o 2), visti come parte alta e parte bassa di un unico registro a 16 bit CCPRHx:CCPRLx, e un
registro di controllo CCPxCON. Tutti sono nel primo blocco RAM (blocco 0).
Figura 21 - Registri di configurazione dei moduli CCP
I bit <CCPxX:CCPxY> sono usati in modalità PWM (vedi oltre); i bit <CCPxM3:CCPxM0>
selezionano la modalità di funzionamento del modulo secondo la Tabella 4:
Tabella 4 – Configurazione di modo dei moduli CCP
0000
0100
0101
0110
0111
1000
1001
1010
1011
Modulo disabilitato e in reset.
Cattura (Capture) su ogni fronte di discesa.
Cattura (Capture) su ogni fronte di salita.
Cattura (Capture) ogni 4° fronte di salita.
Cattura (Capture) ogni 16° fronte di salita.
Comparazione (Compare), uscita alta su coincidenza.
Comparazione (Compare), uscita bassa su coincidenza.
Comparazione (Compare), interrupt su coincidenza.
Comparazione (Compare), generazione di evento speciale su
coincidenza (vedi oltre).
11xx Modo PWM.
34
Vediamo le varie modalità in qualche dettaglio:
Modalità cattura (Capture)
In questa modalità in CCPRxH:CCPRxL cattura il contenuto corrente di TIMER 1 quando si
verifica un evento sul piedino di input relativo al modulo , che è RC2/CCP1 per CCP1 e
RC1/T1OSI/CCP2
per CCP2 (attenzione che sembrano scambiati, ma è proprio così! E ricordarsi di programmarli in
input) 12 . In pratica il modulo registra "il momento" in cui si verifica l'evento.
L'evento di per sé può essere di 4 tipi:
Ogni fronte di discesa.
Ogni fronte di salita.
Ogni 4° fronte di salita (tramite un prescaler).
Ogni 16° fronte di salita (tramite un prescaler).
ed è selezionato, insieme alla modalità Capture, dalla configurazione dei bit <CCPxM2:CCPxM0> ,
come da Tabella 4.
Figura 22 – schema a blocchi della funzione Capture
F
Il TIMER 1 deve funzionare in modo timer (clock interno su OSC
, TMR1CS = 0) o, se si vuole
4
utilizzare un clock esterno, in modo sincrono (TMR1CS = 1 e TSYNC  0 , vedi sezione su
TIMER 1)
Quando si verifica una "cattura" (una coincidenza fra TIMER 1 e CCPxH:CCPxL) si alza il flag di
interrupt del modulo, che è rispettivamente CCP1IF - bit 2 del registro PIR1 (0Ch) - per il modulo
CCP1, e CCP2IF – bit 0 del registro PIR2 (0Dh) per CCP2. Il flag, come per tutti i flag
d'interruzione dei periferici, si alza SEMPRE al verificarsi dell'evento, e va riabbassato da software.
Tuttavia, anche qui come per gli altri periferici, l'alzarsi del flag genera interrupt solo se il relativo
flag di abilitazione è alzato (rispettivamente CCP1IE – bit 2 di PIE2 e CCP2IE, bit 0 di PIE2).
Deve essere anche alzato il flag PEIE di abilitazione interrupt dai periferici esterni al core del CPU
(bit 6 di INTCON, vedi figura 10) e naturalmente il flag di abilitazione geerale GIE (bit 7 di
INTCON). In modalità Capture l'interrupt può servire a chiamare una subroutine che legge
CCPRxH:CCPRxL, salvando così l'informazione sull'istante di cattura, prima che sia riscritta da
una cattura successiva 13 .
12
In realtà la cattura può essere anche determinata da una scrittura su RC2 (RC1 per il modulo 2) dichiarata in output.
Alzare il bit equivale a un fronte di salita, abbassarlo a uno di ddiscesa.
13
Il cambiamento di modo di un modulo CCP può causare falsi interrupt: prima di farlo meglio abbassare il relativo
flag di abilitazione e dopo averlo fatto riabbassare il flag di interruzione. In ogni caso conviene "spegnere" il modulo
(clrf CCPxCON) prima di riprogrammarlo.
35
Notare che la programmazione di un modulo in Capture su evento (a) o (b) (ogni singolo fronte),
mette a disposizione una linea d'interrupt aggiuntiva, indipendentemente dall'uso o meno della
funzionalità specifica del modulo: basta usare il flag d'interruzione del modulo come si userebbe
INTF, eventualmente ignorando CCPRxH:CCPRxL e TIMER 1 (o addirittura disabilitando
quest'ultimo).
Modalità comparazione (Compare)
In questa modalità il contenuto di CCPRxH:CCPRxL è costantemente confrontato con il contenuto
di TIMER 1 (che anche qui deve funzionare in modo timer o sincrono): quando i due valori (a 16
bit) coincidono si genera un evento che può essere hardware o software.
Figura 23 – Schema a blocchi della funzione Compare
Come nel caso precedente, il tipo di evento (insieme alla modalita Compare) è selezionato tramite i
bit <CCPxM2:CCPxM0> , sempre secondo la Tabella 4.
L'evento hardware può consistere in:
b. Alzare la linea di uscita del modulo (RC2/CCP1 per il modulo 1
e RC1/T1OSI/CCP2 per il modulo2, ricordarsi di configurarle in uscita!)
c. Abbassare la linea di uscita
Quando il modulo viene programmato in uno di questi due modi, la linea di uscita si porta
inizialmente al livello opposto a quello cui viene forzata quando verifica la coincidenza fra
CCPRxH:CCPRxL e TIMER1 (bassa nel modo a, alta nel modo b). La linea va bassa (se è in
uscita) anche quando il modulo viene disabilitato. Notare che il latch in uscita al modulo è
indipendente da quello del port di uscita C.
La coincidenza è segnalata anche (e in ogni caso)dall'alzarsi del flag d'interruzione del modulo
CCPxIF.
Che questo generi o no un interrupt dipende dalla catena di flag di abilitazione, esattamente come
nel modo Capture.
36
Anche l'evento software può essere di due tipi:
d. Generazione di interrupt software. Il flag d'interruzione si comporta esattamente come
nei casi (a) e (b), che possono anch'essi generare un interrupt. La differenza è che in
questa (e nella successiva) modalità la linea d'uscita rimane invariata.
e. Generazione di un "evento speciale" che è specifico del modulo utilizzato:
CCP1: reset di TIMER 1 (e anche qui innalzamento del flag d'interruzione).
CCP2: reset di Timer 1, flag d'interruzione e in più start del convertitore AD,
se abilitato (innalzamento del bit di controllo GO/ DONE , vedi
sezione sul convertitore A/D).
Il reset del Timer 1 (configurato in modo timer) permette di realizzare un orologio real-time che
"riparte" con un periodo determinato dal valore di CCPRxH:CCPRxL. In modalità timer il clock di
Timer 1 è il ciclo macchina di durata TCY = 4TOSC, per cui il periodo dell'orologio è
T = (CCPRxH:CCPRxL)·4·P·TOSC, dove P è il rapporto di divisione del prescaler del Timer 1
(1, 2 , 4 o 8).
A titolo d'esempio, con clock di 4 MHz e quindi TOSC = 250 ns, il periodo dell'orologio può andare
1·4·1·250 ns = 1 µs a 65535·4·8·250 ns = 0.52428 s. Se si usa il modulo CCP2, grazie alla
possibilità di far partire il convertitore A/D, tale orologio real time può essere usato per gestire con
precisione il campionamento di un segnale analogico.
Modalità PWM
In questa modalità ciascun modulo CCP può generare un segnale PWM, cioè un'onda quadra di
periodo fisso e di duty cycle variabile; la generazione richiede l'intervento del software solo per
l'inizializzazione dei moduli, poi è sufficiente aggiornare il duty cycle quando necessario scrivendo
nel registro CCPRxL (ed eventualmente nei due bit aggiuntivi <CCPxX:CCPxY> di CCPxCON).
La risoluzione con cui è determinato il duty cycle può arrivare a 10 bit.
Lo schema a blocchi della funzione aiuta a capire come funzionano le cose (per la verità non molto
semplici!):
Figura 24 – Schema a blocchi della funzione PWM
37
Il Timer 2 (8 bit) viene incrementato dal clock interno (periodo 1 ciclo macchina TCY o un suo
multiplo determinato dal fattore di prescaler) fino a raggiungere il contenuto del registro PR2: al
ciclo successivo Timer 2 si resetta e riparte (è il comportamento standard di Timer 2, vedi la
relativa sezione). In questo modo si determina il periodo dell'onda quadra generata:
Figura 25 –Segnale PWM
La durata del tempo in cui il segnale è alto (quella che nella figura 25, tratta dalla documentazione
Microchip, è un po' impropriamente indicata come Duty Cycle) è determinata dal confronto del
contenuto di Timer 2 con il contenuto del registro CCPRxL. Al contenuto di CCPRxL si
aggiungono eventualmente i due bit <CCPxX:CCPxY> di CCPxCONn (che possiamo indicare
anche con CCPxCON<5:4>), per arrivare a un dato di controllo del duty cycle di 10 bit massimi.
Per lavorare a 10 bit Timer 2 (che è a 8 bit), è "esteso" premettendogli due bit meno significativi
presi o dai due bit del contatore "nascosto" che divide per 4 il clock di sistema per dare il ciclo
macchina TTY o, se è usato il prescaler di Timer 2, dai due bit più significativi del prescaler stesso.
In altri termini, quando si lavora a 10 bit, la "finezza" con cui si può determinare il duty cycle è 4
volte quella con cui si fissa il periodo dell'onda PWM. Il dato di determinazione del duty cycle può
essere scritto in ogni momento (gli 8 MSB su CCPRxL, i 2 LSB su CCPxCON<5:4>) ma, per
evitare transienti e incertezze, il confronto del Timer 2 ("esteso") avviene con un buffer costituito
DA CCPRxH e da un registro di due bit aggiuntivi che vengono caricati con il dato contenuto in
CCPxRL e CCPxCON<5:4> solo quando Timer 2 si resetta, cioè al termine del periodo corrente
dell'onda. Se si vogliono usare solo 8 bit, basta lasciare CCPxCON<5:4> a 0 (valore di default).
Riassumiamo le relazioni essenziali:
Il periodo del segnale PWM è TPWM  PR2   1  4  P  TOSC
dove PR 2  è il contenuto del registro PR2, TOSC il periodo del clock di sistema e P il rapporto di
divisione del prescaler di Timer 2 (1, 4 o 16, vedi sezione su Timer 2).
Il fattore 4 dipende dal fatto che Timer 2 (o il suo prescaler) s'incrementa sul cicl macchina TCY =
4TOSC, l'1 aggiunto a (PR2) dal fatto che il reset di Timer 2 avviene al ciclo successivo a quello di
successo della comparazione.
La durata della fase alta (impropriamente il duty cycle) è invece:
TH  (CCPRxL : CCPxCON  5 : 4 )  P  TOSC
(qui non c'è il 4 perché al Timer 2 sono premessi i due bit aggiuntivi, il meno significativo dei quali
dei quali cambia al ritmo di P  TOSC ).
38
Di conseguenza la relazione fra duty cycle e contenuto a 10 bit di CCPRxL : CCPxCON  5 : 4  è:
CCPRxL : CCPxCON  5 : 4 
TPWM
4  PR 2   1
La risoluzione effettiva è limitata dal fatto che il duty cycle varia solo finché TH  TPWM , dopo di
ché l'uscita resta fissa alta (duty cycle = 1): considerando che la “grana” con cui è possibile
determinare il duty cycle è P·TOSC.e che di questi grani in TPWM ce ne stanno al massimo:
D
TH

4  PR2   1 
TPWM TPWM
F

 OSC
PTOSC
TOSC
FPWM
Per cui la risoluzione massima in bit è
F
log OSC
F
FPWM
log 2 OSC 
FPWM
log2
(nell’ultimo membro log in base qualunque. Ve le ricordate le proprietà dei logaritmi?).
Concludiamo con la procedura “passo passo” per configurare e generare un segnale PWM:
1. Selezionare la modalità PWM ponendo <CCPxM3:CCPxM0> = 11xx.
2. Scegliere il periodo del PWM scrivendo nel registro PR2 (ricordatevi di considerare il
rapporto di divisione del prescaler, se intendete usarlo).
3. Scegliere il duty cycle scrivendo in CCPRxL ed eventualmente in CCPxCON<5:4>.
Il dato da scrivere è:
CCPRxL : CCPxCON  5 : 4  4  PR2   1  D
4. Configurare il pin del modulo in uscita (abbassare TRISC<2> per CCP1 e TRIS<1> per
CCP2).
5. Fissare il rapporto di prescaler e abilitare Timer 2, scrivendo in T2CON.
6. Configurare il modulo per la modalità PWM scrivendo 11xx in CPPxCON<3:0>.
7. Ripetere il punto 3 ogni volta che si desidera aggiornare il duty cycle.
Osservazione finale
Poiché i due moduli CCP condividono i timer 1 o 2 a seconda della modalità, bisogna prestare
attenzione alle possibili interazioni, riportate in Tabella 5:
Tabella 5 – Interazioni fra i moduli CCP (x = 1, y = 2 o viceversa)
Modalità Modalità Interazione
CCPx
CCPy
Capture Capture Stessa base dei tempi Timer 1
Capture Compare La modalità Compare deve essere configurata per l’evento speciale che resetta
Timer 1
39
Compare Compare Le modalità Compare deve essere configurata per l’evento speciale che resetta
Timer 1
PWM
PWM
I PWM hanno la stessa frequenza e lo stesso ritmo di aggiornamento del duty
cycle (interrupt da Timer2)
PWM
Capture Nessuna
Nella dispensa PWM_2.pdf è descritto un programma (PWM_2.asm) per generare due segnali
PWM indipendenti con risoluzione a 10 bit e frequenza di qualche KHz. Il duty cycle viene
trasmesso al PIC via porta seriale da un programma in Labview.
40
Scheda integrativa n.1
Il ruolo del flag di carry C nelle sottrazioni (e nei confronti)
Il flag C si alza quando un’operazione di somma o incremento genera un riporto (overflow) oltre gli
8 bit. Per capire il suo comportamento nelle sottrazioni, si deve tener presente che le sottrazioni
vengono eseguite dal PIC in complemento a 2 (su 8 bit): sono cioè anch’esse delle somme. Vediamo
la questione in generale:
consideriamo due numeri (senza segno) a n bit A e B. Supponiamo che b n 1b n  2    b 0 sia la
n 1
i
rappresentazione in binario naturale su n bit del numero B. Allora B   b i 2 . Per definizione il
i0
complemento a 2 di B si ottiene complementandone tutti i bit è sommandovi 1, cioè:
B
n 1
n 1
n 1
n 1
i0
i 0
i 0
i 0
 1 - b i  2 i  1   2 i  b i 2 i  1   2 i  1  B
n 1
Ma
 2i  1  2 n ,
come è immediato verificare (la sommatoria è il più grande numero
i0
rappresentabile con n bit, quello che si ottiene ponendo tutti i bit a 1, e che è appunto 2n - 1).
Quindi B  2 n  B .
Si giustifica allora la regola per cui A  B si calcola sommando ad A il complemento a 2 di B.
Infatti: A  B  A  B  2 n , dove, nell’aritmetica “circolare” a n bit, 2 n equivale a 0.
D’altra parte A  B causa riporto oltre gli n bit se e solo se A  B  2 n , cioè se e solo se
A  B  2 n  2 n  A  B  0 . Quindi, abbastanza inaspettatamente, C si alza come effetto di
A  B se e solo se A  B . E questo avviene anche fuori dai limiti in cui il risultato della differenza
è correttamente interpretabile in complemento a 2 (per 8 bit, numeri senza segno fra 0 e 127,
differenza fra -128 e +127).
Queste considerazioni hanno un’importante conseguenza quando il microcontrollore deve
confrontare due numeri. Il confronto fra due numeri A e B avviene facendo la loro differenza
A  B e considerandone l’effetto sui flag Z e C. Per quanto appena visto, i possibili esiti sono:
A>B
A=B
A<B
Z
0
1
0
C
1
1
0
41
Il SET D’ISTRUZIONI DELLA FAMIGLIA PIC16
Avvertenze per leggere quanto segue:





k indica una costante, che può essere a 8 bit (0 ≤ k ≤ 255) quando rappresenta un dato, o a
11 bit (0 ≤ k ≤ 2047) quando rappresenta un indirizzo.
f è una costante a 7 bit (0 ≤ k ≤ 127) che rappresenta i 7 bit bassi dell’indirizzo di un registro
nell’archivio RAM; coincide con l’indirizzo del registro solo se il registro è nel banco RAM
numero 0. Spesso f è rappresentata da un’etichetta numerica che ha il nome simbolico del
registro in questione. Ad esempio STATUS è un’etichetta uguale a 3, che è appunto
l’indirizzo nel blocco 0 del registro di stato. E’ possibile passare a un’istruzione un indirizzo
di registro f superiore a 127, ma i bit superiori al settimo sono ignorati (cioè f è considerato
modulo 128).
Anche se a volte si parla semplicemente di registro f, nel seguito usiamo la convenzione più
corretta di indicare con (f) il registro i cui primi 7 bit d’indirizzo sono specificati da f (e i
restanti da RP1:RP0). Più esattamente ancora, il registro andrebbe indicato con
(RB1:RB0,f).
W indica il registro di lavoro (working register), che non è individuato da un indirizzo,
perché non è nei baanchi RAM d’archivio.
d è una costante che può assumere il valore 0 o 1 e indica la scelta della destinazione di
alcune operazioni fra il registro W (0) e il registro d’archivio di indirizzzo f (1). In questi
casi (d) indica il registro di destinazione. Per facilitare la scrittura delle istruzioni sono
predefinite le costanti W = 0 e F = 1 (e anche w = 0 e f = 1, da non confondersi con
gli omonimi registro W e con indirizzo di un registro d’archivio)
Descrizione delle istruzioni
ADDLW
Addiziona una costante e W
Sintassi:
Operandi:
Operazione:
Flag influenzati:
Descrizione:
[etichetta]
ADDLW
k
0 ≤ k ≤ 255
W+kW
C, DC, Z
Il contenuto del registro W è sommato alla costante a 8 bit k. Risultato in W.
ADDWF
Addiziona W e un registro f
Sintassi:
Operandi:
[etichetta]
ADDWF
f,d
0 ≤ f ≤ 127
d  [0, 1]
W + (f)  (d)
C, DC, Z
Il contenuto del registro W è sommato a quello del registro f. Se d è 0, il
risultato va in W. Se d è 1, il risultato va nel registro f.
Operazione:
Flag influenzati:
Descrizione:
42
ANDLW
Fa l’AND bit a bit fra una costante e W
Sintassi:
Operandi:
Operazione:
Flag influenzati:
Descrizione:
[etichetta]
ANDLW
k
0 ≤ k ≤ 255
W AND k  W
Z
Il contenuto del registro W fa AND bit a bit con la costante a 8 bit. Risultato
in W.
ANDWF
Fa l’AND bit a bit fra W e un registro f
Sintassi:
Operandi:
[etichetta]
ANDWF
f,d
0 ≤ f ≤ 127
d  [0, 1]
W AND (f)  (d)
Z
Il contenuto del registro W fa AND bit a bit con quello del registro f. Se d è 0,
il risultato va in W. Se d è 1, il risultato va nel registro f.
Operazione:
Flag influenzati:
Descrizione:
BCF
Abbassa (clear) un bit specificato del registro f
Sintassi:
Operandi:
Operazione:
Flag influenzati:
Descrizione:
BCF f,b
0 ≤ f ≤ 127
0≤b≤7
0  (f)<b>
Nessuno
Il bit b nel registro f va basso (0).
BSF
Alza (set) un bit specificato del registro f
Sintassi:
Operandi:
[etichetta]
BSF f,b
0 ≤ f ≤ 127
0≤b≤7
1  (f)<b>
Nessuno
Il bit b nel registro f va alto (1).
Operazione:
Flag influenzati:
Descrizione:
43
BTFSC
Test di un bit specificato del registro f, scavalca (skip) se è 0 (cleared)
Sintassi:
Operandi:
Operazione:
Flag influenzati:
Descrizione:
[etichetta]
BTFSC
f,b
0 ≤ f ≤ 127
0≤b≤7
Scavalca se (f)<b> = 0
Nessuno
Se Il bit b nel registro f è 1, viene eseguita l’istruzione successiva.
Se il bit è 0, l’istruzione successiva viene saltata, e in suo luogo viene
eseguita una NOP (operazione nulla).
In ogni caso BTFSC e l’istruzione successiva durano insieme 2 TCY.
BTFSS
Test di un bit specificato del registro f, scavalca (skip) se è 1 (set)
Sintassi:
Operandi:
Operazione:
Flag influenzati:
Descrizione:
[etichetta]
BTFSC
f,b
0 ≤ f ≤ 127
0≤b≤7
Scavalca se (f)<b> = 1
Nessuno
Se Il bit b nel registro f è 0, viene eseguita l’istruzione successiva.
Se il bit è 1, l’istruzione successiva viene saltata, e in suo luogo viene
eseguita una NOP (operazione nulla).
In ogni caso BTFSS e l’istruzione successiva durano insieme 2 TCY.
CALL
Chiamata di una subroutine
Sintassi:
[etichetta]
Operandi:
Operazione:
0 ≤ k ≤ 2047
PC + 1  TOS (TOS = Top Of the Stack, PC = Program Counter)
k  PC<10:0>
(PCLATH)<4:3>  PC<12:11>
Nessuno
Salta a una subroutine. L’indirizzo di rientro PC +1 viene messo sullo stack.
L’indirizzo immediato k, di 11 bit, è caricato nei bit <10:0> del PC. I due bit
superiori di PC (che è a 13 bit) vengono caricati da PCLATH.
CALL dura 2 TCY.
Flag influenzati:
Descrizione:
CALL
k
CLRF
Azzera (clear) un registro f
Sintassi:
Operandi:
Operazione:
[etichetta]
CLRF
f
0 ≤ f ≤ 127
00h  (f)
1Z
Z
Il contenuto del registro f è azzerato e il flag Z va a 1.
Flag influenzati:
Descrizione:
44
CLRW
Azzera (clear) il registro W
Sintassi:
Operandi:
Operazione:
Flag influenzati:
Descrizione:
[etichetta]
CLRW
Nessuno
00h  W
1Z
Z
Il contenuto del registro W è azzerato e il flag Z va a 1.
CLRWDT
Azzera (clear) il timer di Watchdog
Sintassi:
Operandi:
Operazione:
[etichetta]
CLRWDT
Nessuno
00h  WDT
0  WDT prescaler
1  TO
1  PD
TO , PD
Azzera il timer di Watchdog e il suo prescaler. Alza i bit TO (time-out) e
PD (power-down) del registro STATUS.
Flag influenzati:
Descrizione:
COMF
Complementa i bit del registro f
Sintassi:
Operandi:
[etichetta]
COMF
f,d
0 ≤ f ≤ 127
d  [0, 1]
f  (d)
Z
Il contenuto del registro f è complementato. Se d è 0, il risultato va
in W. Se d è 1, il risultato va nel registro f.
Operazione:
Flag influenzati:
Descrizione:

.
Nella descrizione delle istruzioni sono state adottate alcune convenzioni (leggermente modificate
rispetto a quelle nei datasheets) che è bene tener presente:
DECF
Decrementa un registro f
Sintassi:
Operandi:
[etichetta]
DECF
f,d
0 ≤ f ≤ 127
d  [0, 1]
(f) – 1  (d)
Z
Il contenuto del registro f è decrementato di 1. Se d è 0, il risultato va
in W. Se d è 1, il risultato va nel registro f.
Operazione:
Flag influenzati:
Descrizione:
.
45
DECFSZ
Decrementa un registro f, scavalca (ski) se è 0
Sintassi:
Operandi:
[etichetta]
DECFSZ
f,d
0 ≤ f ≤ 127
d  [0, 1]
(f) – 1  (d)
scavalca se risultato = 0
Nessuno
Il contenuto del registro f è decrementato di 1. Se d è 0, il risultato va
in W. Se d è 1, il risultato va nel registro f.
Se il risultato è > 0, viene eseguita l’istruzione successiva.
Se il risultato è 0, l’istruzione successiva viene saltata, e in suo luogo viene
eseguita una NOP (operazione nulla).
In ogni caso DECFSZ e l’istruzione successiva durano insieme 2 TCY.
Operazione:
Flag influenzati:
Descrizione:
.
.
GOTO
Salto incondizionato
Sintassi:
[etichetta]
Operandi:
Operazione:
0 ≤ k ≤ 2047
k  PC<10:0>
(PCLATH)<4:3>  PC<12:11>
Nessuno
Salta all’indirizzo k. L’indirizzo immediato k, di 11 bit, è caricato nei bit
<10:0> del PC. I due bit superiori di PC (che è a 13 bit) vengono caricati da
PCLATH. GOTO dura 2 TCY.
Flag influenzati:
Descrizione:
GOTO
k
INCF
Incrementa un registro f
Sintassi:
Operandi:
[etichetta]
INCF
f,d
0 ≤ f ≤ 127
d  [0, 1]
(f) + 1  (d)
Z
Il contenuto del registro f è incrementato di 1. Se d è 0, il risultato va
in W. Se d è 1, il risultato va nel registro f.
Operazione:
Flag influenzati:
Descrizione:
46
INCFSZ
Incrementa un registro f, scavalca (skip) se è 0
Sintassi:
Operandi:
[etichetta]
INCFSZ
f,d
0 ≤ f ≤ 127
d  [0, 1]
(f) + 1  (d)
Scavalca se risultato = 0
Nessuno
Il contenuto del registro f è incrementato di 1. Se d è 0, il risultato va
in W. Se d è 1, il risultato va nel registro f.
Se il risultato è > 0, viene eseguita l’istruzione successiva.
Se il risultato è 0 (overflow: il registro passa da 255 a 0), l’istruzione
successiva viene saltata, e in suo luogo viene eseguita una NOP (operazione
nulla). In ogni caso DECFSZ e l’istruzione successiva durano insieme 2 TCY.
Operazione:
Flag influenzati:
Descrizione:
.
.
IORLW
Fa l’OR (inclusivo) bit a bit fra una costante e W
Sintassi:
Operandi:
Operazione:
Flag influenzati:
Descrizione:
[etichetta]
IORLW
k
0 ≤ k ≤ 255
W OR k  W
Z
Il contenuto del registro W fa OR bit a bit con la costante a 8 bit. Risultato in
W.
IORWF
Fa l’OR (inclusivo) bit a bit fra W e un registro f
Sintassi:
Operandi:
[etichetta]
IORWF
f,d
0 ≤ f ≤ 127
d  [0, 1]
W OR (f)  (d)
Z
Il contenuto del registro W fa OR bit a bit con quello del registro f. Se d è 0, il
risultato va in W. Se d è 1, il risultato va nel registro f.
Operazione:
Flag influenzati:
Descrizione:
MOVF
Copia un registro f
Sintassi:
Operandi:
[etichetta]
MOVF
f,d
0 ≤ f ≤ 127
d  [0, 1]
(f)  (d)
Z
Il contenuto del registro f viene copiato su una destinazione che dipende da d.
Se d è 0, la destinazione è W. Se d è 1, la destinazione è lo stesso registro f
(in questo caso serve per controllare se il contenuto è 0, perché influenza il
flag di zero Z). In ogni caso, il contenuto originale di f non viene modificato.
Operazione:
Flag influenzati:
Descrizione:
47
MOVLW
Copia una costante in W
Sintassi:
Operandi:
Operazione:
Flag influenzati:
Descrizione:
[etichetta]
MOVLW
k
0 ≤ k ≤ 255
kW
Nessuno
La costante a 8 bit k è copiata in W. Ovviamente la costante non viene
modificata.
MOVWF
Copia W nel registro f
Sintassi:
Operandi:
Operazione:
Flag influenzati:
Descrizione:
[etichetta]
MOVWF
f
0 ≤ f ≤ 127
W  (f)
Nessuno
Il contenuto del registro W è copiato nel registro f.
Il contenuto originale di W non viene modificato.
NOP
Nessuna operazione (operazione nulla)
Sintassi:
Operandi:
Operazione:
Flag influenzati:
Descrizione:
[etichetta]
NOP
Nessuno
Nessuna
Nessuno
Ha il solo effetto di introdurre un ritardo di 1 TCY.
RETFIE
Ritorna da un’interruzione (interrupt)
Sintassi:
Operandi:
Operazione:
[etichetta]
REFFIE
Nessuno
TOS  PC (TOS = Top Of the Stack, PC = Program Counter)
1  GIE
Nessuno
Ritorna da un’interruzione (interrupt). L’indirizzo di rientro viene recuperato
dallo stack e messo nel PC. La CPU è riabilitata ad accettare interrupt,
rialzando il bit GIE (General Interrupt Enable), che si era abbassato
all’accettazione dell’interruzione. RETFIE dura 2 TCY.
Flag influenzati:
Descrizione:
48
RETLW
Ritorna da una subroutine, restituendo il valore di una costante in W
Sintassi:
Operandi:
Operazione:
[etichetta]
RETLW
k
0 ≤ k ≤ 255
kW
TOS  PC (TOS = Top Of the Stack, PC = Program Counter)
Nessuno
Ritorna da una chiamata a subroutine, restituendo nel registro W il dato a 8
bit specificato dalla costante k. L’indirizzo di rientro viene recuperato dallo
stack e messo nel PC. RETLW dura 2 TCY.
Flag influenzati:
Descrizione:
RLF
Ruota a sinistra un registro f attraverso il Carry
Sintassi:
Operandi:
[etichetta]
RLF f,d
0 ≤ f ≤ 127
d  [0, 1]
Vedi descrizione più sotto
C
Il contenuto del registro f viene ruotato a sinistra di un bit attraverso il flag di
carry C (i bit di f slittano di una posizione a sinistra, nel bit 0 entra C e in C
va il bit 7). Se d è 0, il risultato della rotazione va in W (e f resta invariato).
Se d è 1, il risultato resta nel registro f.
Operazione:
Flag influenzati:
Descrizione:
C
Registro f
RETURN
Sintassi:
Operandi:
Operazione:
Flag influenzati:
Descrizione:
Ritorna da una subroutine
[etichetta]
RETURN
Nessuno
TOS  PC (TOS = Top Of the Stack, PC = Program Counter)
Nessuno
Ritorna da una chiamata a subroutine. L’indirizzo di rientro viene recuperato
dallo stack e messo nel PC. RETLW dura 2 TCY.
RRF
Ruota a destra un registro f attraverso il Carry
Sintassi:
Operandi:
[etichetta]
RRF f,d
0 ≤ f ≤ 127
d  [0, 1]
Vedi descrizione più sotto
C
Il contenuto del registro f viene ruotato a destra di un bit attraverso il flag di
carry C (i bit di f slittano di una posizione a destra, nel bit 7 entra C e in C va
il bit 0). Se d è 0, il risultato della rotazione va in W (e f resta invariato). Se d
è 1, il risultato resta nel registro f.
Operazione:
Flag influenzati:
Descrizione:
C
Registro f
49
SLEEP
Sintassi:
Operandi:
Operazione:
Flag influenzati:
Descrizione:
Manda la CPU nello stato di Sleep (Stand By)
[etichetta]
SLEEP
Nessuno
00h  WDT
0  WDT prescaler
1  TO
0 PD
TO , PD
La CPU va in modo Sleep con l’oscillatore di clock bloccato. Viene alzato il
flag di di time-out TO e abbassato quello di power-down PD . Il timer di
watchdog è azzerato insieme al suo prescaler.
SUBLW
Sottrae W da una costante
Sintassi:
Operandi:
Operazione:
Flag influenzati:
Descrizione:
[etichetta]
SUBLW
k
0 ≤ k ≤ 255
k -WW
C, DC, Z
Il contenuto del registro W è sottratto (in complemento a 2) alla costante a 8
bit k. Risultato in W.
SUBWF
Sottrae W da un registro f
Sintassi:
Operandi:
[etichetta]
SUBWF
f,d
0 ≤ f ≤ 127
d  [0, 1]
(f) - W  (d)
C, DC, Z
Il contenuto del registro W è sottratto a quello del registro f. Se d è 0, il
risultato va in W. Se d è 1, il risultato va nel registro f.
Operazione:
Flag influenzati:
Descrizione:
SWAPF
Scambia (swap) i nibble (parole di 4 bit) alto e basso di un registro f
Sintassi:
Operandi:
[etichetta]
SWAPF
f,d
0 ≤ f ≤ 127
d  [0, 1]
(f)<3:0>  (d)<7:4>
(f)<7:4>  (d)<3:0>
Nessuno
Si scambiano fra loro i quattri bit alti e i 4 bassi. Se d è 0, il risultato dello
scambio va in W (e il registro f rimane invariato). Se d è 1, il risultato va nel
registro f.
Operazione:
Flag influenzati:
Descrizione:
50
XORLW
Fa l’OR esclusivo (EXOR) bit a bit fra una costante e W
Sintassi:
Operandi:
Operazione:
Flag influenzati:
Descrizione:
[etichetta]
XORLW
k
0 ≤ k ≤ 255
W EXOR k  W
Z
Il contenuto del registro W fa EXOR bit a bit con la costante a 8 bit. Risultato
in W.
XORWF
Fa l’OR esclusivo (EXOR) bit a bit fra W e un registro f
Sintassi:
Operandi:
[etichetta]
XORWF
f,d
0 ≤ f ≤ 127
d  [0, 1]
W EXOR (f)  (d)
Z
Il contenuto del registro W fa EXOR bit a bit con quello del registro f. Se d è
0, il risultato va in W. Se d è 1, il risultato va nel registro f.
Operazione:
Flag influenzati:
Descrizione:
51
In Figura 20 una tavola riassuntiva delle istruzioni, suddivise per gruppi funzionali, con i codici
operativi e l’influenza sui flag. Nella, nella colonna 14-Bit Opcode, fff ffff indicano i 7 bit
specificati dall’indirizzo f, kkkk kkkk gli 8 bit di una costante di dato 0 ≤ k ≤ 255 e
kkk kkkk kkkk gli 11 bit di una costante d’indirizzo 0 ≤ k ≤ 2043.
Figura 20 – Sommario delle istruzioni
52
In Figura 21 la descrizione del formato del codice macchina delle istruzioni:
Figura 21 – Formato delle istruzioni
Questa dispensa può esser fatta cirolare liberamente.
Vi chiedo solo di non modificarla senza il mio permesso.
Sarò grato di osservazioni, suggerimenti, correzioni.
Bergamo 2007-2009, rivista nel Febbraio 2012
Ippolito Perlasca
53
Scarica

release 3 Il microcontrollore PIC16F877A di Ippolito Perlasca