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 i0 complemento a 2 di B si ottiene complementandone tutti i bit è sommandovi 1, cioè: B n 1 n 1 n 1 n 1 i0 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 i0 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+kW 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) 1Z 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 1Z 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 kW 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 kW 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 -WW 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