Appunti sul Pic16F84


Chiedo scusa se l'impaginazione di questa sezione non e' il massimo della vita : si tratta di una pagina che avevo scritto un sacco di tempo fa ed ora, andare a spulciare il codice html per indentare il tutto, sarebbe noiosissimo. Abbiate paziena !

Realizzare delle subroutine di ritardo.

Con registri GPR.

Capita spessissimo in un processo di dover utilizzare delle routine di ritardo per i piu' disparati motivi, vediamo due modi di realizzarle con il PIC16F84F.
Supponiamo di lavorare con una frequanza di clock pari a 2.5 Mhz :

fck = 2.5     Mhz             ;frequenza di clock
tck = 0.4     uS                ;periodo ci clock
ti    = 4 * 0.4 = 1.6 uS    ;tempo per istruzione (non di salto = 4 periodi di clock = 1 ciclo macchina)
ti    = 8 * 0.4 = 3.2 uS    ;tempo per istruzione (di salto = periodi di clock = 2 cicli macchina)

Supponiamo ora di voler realizzare un ritardo tr pari a 1 mS :
Ni (numero di istruzioni da 1 ciclo macchina) = tr / ti ----> 1mS / 1.6 uS = 625 istruzioni
Potrei caricare il valore 625 in un registro e decrementarlo fino al valore 00h. Ad ogni decremento scorre un ciclo macchina pari a 1.6 uS e dopo 625 volte sara' passato circa 1 mS.
Ma i registri del PIC16F84 sono a 8 bit e al massimo possono contenere un valore pari a 255 (ffh).
Possiamo pero' pensare a 625 come 5 * 125, cioe' eseguire 5 ritardi da 1.6 uS per 125 volte, ecco la routine che esegue questa operazione :

......
Count equ 0c
...... Delay_1mS
             Movlw .125         ; 125 in W
             Movwf    Count   ; 125 in Count
Ripeti    Nop                     ; 1.6 uS
             Nop                     ; 1.6 uS
             Decfsz Count       ; 1.6 uS (1.6 uS se non deve saltare, 2 * 1.6 uS se deve saltare ma in questo caso il ciclo e'                                             terminato perche' Count sarebbe a 0)
             Goto Ripeti:         ; 2 * 1.6 uS
             Return                 ; in uscita dalla subroutine altri 2 * 1.6 uS

La somma dei tempi in rosso, che scorrono ad ogni decremento di Count finche esso non e' pari aa 0 vale 8 uS, per cui, 8 uS * 125 = 1 mS.
La subroutine non e' il massimo della precisione ma si presa bene ad essere utilizzata quando non sono richieste tempistiche rigorose.
Vediamo ora un metodo, che fa uso di questa subroutine, per ritardi ancora maggiori.
Definiamo una generica subroutine Delay_XXmS dove XX rappresenta il numero di mS.

........
Count1 equ 0d
........ Delay_XXmS
         Movlw .XX             ; in W il numero di mS
         Movwf     Count1    ; XX in Count1
Ripeti1        Call Delay_1mS     ; delay di 1 uS (2 * 1.6 uS)
                   Decfsz Count1;      ; decrementa e se e' 0 esci (1.6 uS)
                   Goto Ripeti1          ;  se Count <> 0 salta a Ripeti1 (2 * 1.6 uS)
                   Return        

Si deve tenere conto del fatto che ad ogni ciclo di ritardo di 1 mS si verifica un ulteriore ritardo di 8 uS dovuto ai tempi di esecuzione delle istruzioni della subroutine segnati in blu. Inoltre bisogna tenere conto del fatto che chiamando XX volte la subroutine Delay_1ms si ha l'inizializzazione di Count (2 * 1.6 uS) e il return (2 * 1.6 uS). Inoltre in uscita dalla subroutine Delay_XXmS altri 2 * 1.6 uS dovuti alla sua istruzione return.
In conclusione avremo un tempo pari a XX * (1mS + 11.2 uS), piu' ultriori 3.2 uS all'uscita.
E' quindi probabile che per avere un ritardo di XX mS si debbano eseguire XX-YY cicli di questo tipo.

Con Timer0.

Il TMR0 puo' lavorare sia col clock di sistema (gia' pre-diviso per 4) che con un clock esterno collegato al pin Ra4/T0CKI a seconda di come e' settato il bit TOCS (bit 6) sull'OPTION_REG (81h, bank 1).
La frequenza di clock puo' essere ulteriormente divisa attrverso un prescaler programmabile dai bit PS2 (bit 2), PS1 (bit 1) e PS0 (bit 0) dell'OPTION_REG. Quindi fck/4 puo' essere divisa ancora per (2,4,8,16,32,64,128,256).
Se utilizziamo tutto il registro TMR0 precaricandolo a ffh avremo un ritardo pari a :
tr = fck / 4 * PRESCALER * 256

Ma spesso non e' necessario utilizzare 256 decrementi, e piu' generalmente possiamo scrivere che :
tr = fck / 4 * PRESCALER * (256 - x)

Allora imponendo un valore a tr si ricava x :
x = 256 - ((tr * fck) / (4 * PRESCALER))
con tr = 1mS, x = 20 circa per cui il valore che devo precaricare in TMR0 sara' 256-x = 256-20 = 236.

Vediamo ora la routine che esegue il delay di 1 mS con il TMR0 :
......
Movlw B'00000100'    ; configurazione OPTION_REG
......

Delay_1mS
    Movlw .236                 ; in W 236
    Movwf  TMR0            ; W in TMR0
Ripeti   Movf TMR0,W          ; TMR0 in W
    Btfss Status,z               ; se W=0, cioe' TMR0 = 0 si resetta il bit z in STATUS, salta a RETURN
    Goto Ripeti                 ; il decremento di TMR0 e' automatico ad ogni ciclo di clock

Da cui il tempo necessario a TMR0 per azzerarsi e' : 1.6 * 32 * (256  - 236) = 1024 mS
Posso agire sul PRESCALER per ottenere precisioni maggiori o tempi maggiori. Possiamo anche in questo caso fare uso di un registro GPR per ottenere tempi maggiori :


.......
Count equ 0c
.......

Delay_250mS
Movlw .236            ; in W 236 per avere 1 mS
Movwf    TMR0     ; TMR0 preset
Movlw .250            ; in W 250 per avere 250 mS, da notare che se non si imposta queste registro esso parte da 00h
Movvf Count          ; quindi decrementandosi da luogo a 256 step, da ffh a 00h

Rit    Movf    TMR0,W    ; }
        Btfss STATUS,z      ; } 1 mS
        Goto Rit                   ;   }
        Movlw .236             ; ricarico TMR0
        Movwf    TMR0   
        Decfs Count,1           ; decremetno Count
        Goto Rit
        Return

**********************************************************
Direttive al compilatore :
#define

Esiste la possibilita' di programmare i Pic in modo abbastanza astratto, cioe' riuscendo a non tenere conto di come sono fatti internamente, attraverso delle direttive al compilatore.
Una di queste e' la direttiva #define :

#define tasto portb,0

Come per quanto concerne al linguaggio C, questa direttiva non fa altro che dire al compilatore che ogni volta che nel testo del programma incontra la stringa 'tasto' questa deve essere sostituita con la stringa 'portb,0'. In questo modo abbiamo definito un dato astratto, tasto, che sappiamo avere una certa funzionalita'. Durante la stesura del programma possiamo completamente dimenticarci di come la funzione tasto e' implementeata dal pic (in questo caso vi e' un pulsante sul bit 0 di portb) e possiamo concentrarsi solo su di esso in senso lato, cioe' non preoccupandoci del bit dove e collegato e di come fa il pic a riconoscere quella linea come input.
Questi "problemi" li dobbiamo risolvere in altra sede cioe' durante la definizione delle direttive #define, poi, qualunque sia la lunghezza del programma, mai piu'. Vediamo ora un'altra direttiva #define con un esempio di utilizzo nel programma principale :
#define LED porta,0
................
bsf  LED                     ;accende il led
bcf LED                      ;spegne il led
................

Si nota un discreto incremento delle potenzialita' del linguaggio assembler se dotato di direttive.
Addirittura possiamo definire #define di questo genere :
#define ACCENDI  bsf porta,0
#define SPEGNI      bcf porta,0
..........................
ACCENDI                 ; accende il led
nop
nop
SPEGNI                     ; spegne il led
..........................

Direi che l'astrazione e' parecchio elevata !!!

macro

Esistono poi le macro che non sono altro che degli insiemi di istruzioni, anche esse concorrono fortemente all'incremento dell'astrazione.
definizione :

NOME macro parametro1, parametro2, ........., parametro n
istruzione 1
istruzione 2
................
instruzione n
endm

Vediamo qualche esempio :

read macro                             ; in questo caso non passiamo parametri
movf    porta,w
andlw    mask
movwf save
endm

In questo caso innvocando la macro read dal programma principale vengono eseguite tutte le istruzione in essa contenute. La macro e' riusabile lungo tutta la durata del programma.
Dentro ad una macro e' possibile definire delle label ad uso locale per la funzionalita' della macro. Una macro puo' inoltre restituire un risultato in uscita, per fare questo, all'interno della macro, il risultato andra' caricato in un registro il quale verra' poi letto dal programma principale.
Vediamo ora una macro con passaggio di un parametro :

write    macro data         ; passiamo il parametro data, che puo' essere una costante o anche un registro
   movf data,w
   movwf porta
   endm


Vediamo ora una macro che puo' configurare i bit di porta in input o in output, a seconda di come sono i corrispondenti bit del parametro IN_OUT, dinamicamente lugno il programma.

conf_porta  macro    IN_OUT
          bsf status,rp0
          movlw IN_OUT
          movwf  trisa
          bcf status,rp0
          endm

;main
............
conf_porta B'11111111'        ; tutti input
..........
conf_porta B'00000000'        ; tutti output
.........
.........
conf_porta B'10101010'        ; 1 input, 1 output

La potenzialita' della macro rispetto alla subroutine sta nel fatto che alla macro e' possibile passare dei parametri.
Altro esempio :

J_EQ    macro    data,adr ; adr e' una label del programma principale !
    movf    save,w
    sublw    data
    btfss    status,z
    goto    adr             ; salta alla label adr del programma principale
    endm

Un altro esempio ancora, con una supermacro !!!!

IN_DEC    macro    data, rout, adr   ; rout e' il nome di una subroutine !!!!!!
                  local salta                         ; salta viene vista solo dentro la macro, e' una label locale
                  movf save,w
                  sublw data
                  btfss     status,z
                  goto salta                         ; salta dentro la macro a salta
          call rout                            ; chiama la subroutine rotut
          goto adr                           ; salta alla label adr del prg principale
          endm

;main
................
IN_DEC    B'01', INCR, ADR
................
IN_DEC    B'10', DECR,ADR
...............
..............


;subroutine
INCR
............
...........
...........
RETURN

DECR
............
...........
RETURN

***************************************************

La gestione degli interrupt :

Nel Pic16F84 le sorgenti di interrupt possono essere 4, di queste 2 sono esterne e 2 interne.

Interrupt esterno generato da lcambiamento di stato della linea RB0/INT o sul fronte di discesa o sul fronte di salita. La scelta del tipo di fronte viene effettuata andando ad agire sul bit 6  INTEDG dell' OPTION_REG.

Interrupt esterno associato al cambiamento di stato di uno dei pin RB4, RB5, Rb6 e RB7. Queste linee devono tutte essere state definite come input.

Interrupt interno : overflow di TMR), ogni volta che esso passa da ffH a 00H.

Interrupt interno : ciclo di scrittura completo du EEPROM.

Quando viene effettuata una INTR (Interrupt Request) da qualsiasi dei 4 casi precedenti il Program Counter (PC) viene caricato con l'indirizzo 04H al quale si trova la routine di gestione dell'interrupt. La gestione dell'area di Stack e' automatica, non vi e' bisogno di fare PUSH in entrata e POP in uscita. Viene automaticamente caricato nell'area di Stack l'indirizzo di ritorno dall'interrupt al main.  Abbiamo a discposizione un registro apposito per la gestione degli interrupt denominato INTCON. Esso e' costituito da alcuni bit di controllo dell'abilitazione del tipo di interrrupt e di alcuni Flag di controllo. I flag sono :

TOIF : si porta a 1 ogni volta che TMR0 va in overflow, indica un intr proveniente da TMR0

INTF : si porta a 1 quando avviene un intr su RB0/INT anche se l'interrupt in questione non e' stato abilitato

RBIF : si porta a 1 quando avviene un intr su una delle linee da RB4 a RB7

Invece i segnali di controllo delle abilitazioni sono :

GIE : consente o meno di abilitare i vari interrupt (usato per mascherare tutti gli interrupt)

TOIE : abilita l'intr su TMR0

INTE : abilita l'intr su RB0/INT

RBIE : abilita l'intr su RB4--RB7

EEIE : abilita l'intr su termine scrittura completa in EEPROM

Il flag EEIF non e' compreso in questoregistro ma si trova nel registro EEICON1.

I flag servono perche' ad ogni intr, se GIE e' abilitato, il PC viene caricato con 04h, a questo punto ho bisogno di sapere chi mi ha rischiesto l'intr altrimenti non so come comportarmi. Ecco l'importanza di avere dei flag da interrogare per sapere quale sorgente fra le 4 a disposizione ha richiesto l'interrupt. Posso avere la necessita' di dover mascherare solo alcuni tipi di interrupt, per questo agiro' sui bit di controllo TOIE, INTE, RBIE, EEIE.

Appena il PC salta a 04h automaticamente GIE viene reserrato per mascherare, ad eventuali ulteriori intr, la routine di gestione dell'intr appena ricevuto. Quando si esce dalla routine bisogna assolutamente resettare il flag della sorgente di interrupt che mi ha fatto l'intr. Questo perche' diversamente, al ritorno al main, con il settaggio automatio di GIE, e quindi trovando ancora quel flag alzato il PC verrebbe nuovamente ricaricato con il vettore 04H e si formerebbe un loop fra il main e la routine dell'interrupt.

Posso avere la necessita', una volta che sto gia' gestendo un primo intr e quindi una volta che sono dentro alla routine di gestione, di dover gestire altri interrupt provenienti da sorgenti diverse. A questo punto appena entrato nella routine di gestione devo settare GIE che era stato automaticamente resettato. Poi, nel caso arrivasee un nuovo intr, il PC punterrebbe nuovamente a 04H dove ci dovra' essere una routine di riconoscimento della sorgente di generazione dell'interrupt ed un conseguente salto alla relativa subroutine Si possono annidare fino a 8 intr.

Quando si entra in una routine di gestione di interrupt occorre :

salvare W
salvare STATUS
gestire l'interrupt
resettare il flag della sorgente di intr
ripristinare STATUS
ripristinare W
retfie

Dall'intr al momento in cui effettivamente il PC passa a 04H trascorrono circa 3-4 cicli macchina per le intr esterne e 2 cicli macchina per le intr interne.
Dallo stato di sleep ci si esce solo con un interrupt o con un reset.

Per sapere effettivamente quale delle linee RB4--RB7 ha richiesto l'inter  occorre tenre in memoria uno storico dello stato di portb e fare un contronto fra il portb attuale e quello precedente ogni volta che si trova il flag RBIF a 1.

*****************************************************************

Lettura e scrittura su EEPROM
Il PIC16F84 ha una EEPROM di 64 byte, da 00h a 3fh) che puo' essere utilizzata in operazioni di lettura e scrittura durante il funzionamento del programma. Per utilizzare la E2PROM devono essere utilizzati quattro registri specifici :

EECON1    bank1
EECON2    bank1
EEADR       bank0
EEDATA    bank0

Il registro EEDATA contiene il dato da scrivere o appena letto dalla EEPROM, in lettura il dato verra' trasferito dalla memoria a EEDATA, in scirttura da EEDATA alla memoria.
Per quanto rigurda la lettura si tratta di una operazione semplice e veloce, la scrittura invece e' piu' complessa e meno veloce.

Il ritardo e' dovuto al fatto che prima di scrivere effettivamente in EEPROM il pic cancella quella locazione per evitare eventuali errori. Lettura e scrittura avvengono per indirizzamento indiretto.

Il registro EEADR viene utilizzato per l'indirizzamento indiretto dell'area EEPROM, e' un registro a 8 bit ma di questi ne vengono utilizzati solo 6 per il fatto che 64 byte sono indirizzabili da 6 bit. (64 = 2^6). Occorrera' quindi prestare la massima attenzione quando si scrive un byte in questo registro, i due bit piu' significativi andranno quindi resettati.

EECON1 e' il vero e proprio resgistro di controllo delle operazioni di lettura e di scrittura sulla e2prom. Vediamo come e' strutturato :

     -    -    -     EEIF    WERR    WREN    WR     RD
     7   6   5        4             3               2           1         0

Il bit RD posto ad 1 abilita la lettura della locazione e2prom puntata da EEADR, alla fine della lettura viene automaticamente resettato dal pic.
Per quanto rigurda la scrittura in e2prom c'e' bisogno di agire su 2 bit , WREN e WR. WREN deve essere posto ad 1 per abilitare la scrittura, pero' al fine di evitare scritture accidentali, per dare il via alla scrittura vera e propria bisogna settare anche il bit WR. WR poi resetta automaticamente alla fine del ciclo di scrittura, WREN no, andra' resettato via software.
Il bit WERR ad 0 ci indica che la scrittura non e' avvenuta correttamente, se e' ad 1 c'e' stato qualche problema. Ovviamente dovrermo essere noi a gestire via software questa eventuaita'. Un caso di errore puo' essere che mentre si scrive in e2prom avviene un reset del pic oppure un reset generato dal watchdog. Siccome in caso di errore EEADR e EEDATA non vengono modificati, si potra' procedere alla riscrittura della loicazione interessata.
EEIF e' il flag di interrupt di fine scrittura, esso va resettato da programma alla fine del ciclo.


EECON2 non e' un registro vero e proprio ed e' utilizzato solo nelle fasi di scrittura della e2prom.
Vediamo ora alcuni esempi :

esempio 1 : lettura di un byte dall locazione e2prom 10h

bcf STATUS,RP0    ;bank0 (e2data e2adr)
movlw    10h             ;
movwf    EEADR     ;10h in EEADR
bsf STATUS,RP0    ;bank1 (e2con1)
bsf EECON1,RD     ;abilita la lettura
bcf STATUS,RP0    ;bank0
movwf EEDATA,W ;in W il dato letto da 10h


esempio 2 : scrittura del contenuto di porta in 10h e2prom

    bcf INTCON,GIE    ;disabilita ogni interrupt
    bcf STATUS,RP0    ;bank0
    movf PORTA,W
    movwf EEDATA      ;PORTA in EEDATA
    movlw    10h
    movwf EEADR
    bsf STATUS,RP1    ;bank1
    bsf EECON1,WREN ;scrittura abilitata
    movlw 55h                   ;    operazioni fisse
    movwf EECON2         ;             "
    movlw  aah                   ;             "
    movwf EECON2         ;             "
    bsf EECON1,WR       ;inizio scrittura
LOOP
    btfsc EECON1,WR
    goto LOOP                   ;attende che WR torni a 0 (fine scrittura)
    bcf EECON1,WREN  ;disabilita scrittura
    bcf EECON1,EEIF      ;disabilita flag di interrupt, operazione fondamentale se non mi interessa gestire EEIF ma ho altri tipi di interrupt da gestire
    bsf INTCON,GIE        ;riabilita gli interrupt


esempio 3 : scrittura in 10h e2prom di PORTA e controllo del dato scitto con gestione di EEIF

FLAG        equ 0ch
S_DATO   equ 0dh

    bcf STATUS,RP0        ;     bank0
    bsf FLAG,1                  ;     flag=0 scrittuara termintata, =1 no
    movlw 10h
    movwf EEADR
    movf PORTA
    movwf EEDATA
    movwf S_DATO         ;una copia del dato in S_DATO per il successivo confronto.
    bsf STATUS,RP0
    bcf INTCON,GIE
    bsf EECON1,WREN
    movlw 55h
    movwf EECON2
    movlw aah
    movwf EECON2
    bsf EECON,WR
    bsf INTCON,GIE
                      ...............................................................................
                             routine di gestione dell'interrupt da EEIF
                          org 04h
                          salva STATUS e W
                          bsf STATUS,RP0
                                  bcf EECON1,EEIF
                                  bcf FLAG,1            ;scrittura terminata
                                  ripristina STATUE e W
                                  retfie
                              ................................................................................
WAIT
    btfsc FLAG,1    ; attende che la routine di intr, cioe' la scittura, sia terminata                      
    goto WAIT
bsf FLAG,0            ;1 lettura ok, 0 non ok
call lettura
movwf EEDATA,W
xorwf S_DATO,W    ;confronto
btfss STATUS,Z
goto ERRORE!!!
.....................
.....................                 ; OK