Alcuni conoscenti mi hanno chiesto di realizzare, per puro divertimento, un
dado elettronico e questa mi è parsa l'occasione giusta per scrivere un
semplice programma per il PIC 16F84, da commentare a scopo educativo.
Per questo motivo presento qui sia il progetto che il sorgente in linguaggio
Assembler.
Il dado elettronico mostra il numero estratto, da 1 a 6, utilizzando sette diodi led
messi nella stessa posizione dei punti sulle facce di un vero dado. Ogni volta
che si preme un pulsante, viene estratto e mostrato un nuovo numero. Dopo circa
10 secondi di inattività, i led vengono spenti per risparmiare le batterie.
Dato che in questa applicazione le temporizzazioni non sono critiche, possiamo
evitare di utilizzare un quarzo per il clock del PIC ed usare invece un semplice
circuito RC (resistenza più condensatore). La frequenza di oscillazione dipenderà
dai due componenti scelti, oltre che dalla tensione di alimentazione e dalla
temperatura ambientale. A 25° e con una tensione di 4,5 V il clock dovrebbe
essere di circa 620 kHz, con il quale saranno eseguite 155.000 istruzioni al secondo
(circa 6,45 µs per ogni istruzione).
La Port A del chip non viene usata, mentre il primo pin della Port B (RB0/INT) sarà
collegato ad un pulsante normalmente aperto.
Gli altri sette pin della Port B (RB1 - RB7) saranno collegati ai sette led che
mostrano il numero estratto.
Per l'alimentazione basterà usare tre pile AA in serie, per un totale di 4,5 V, ma
il circuito dovrebbe funzionare bene anche con tensioni un po' più basse, quindi
si possono usare anche tre o quattro ricaricabili al NiMH (3,6 - 4,8 V).
Il clock cambierà un po' a seconda della tensione di alimentazione:
più si aumenta il voltaggio (fino al massimo consentito di 5,5 V) più si riduce la
frequenza di clock. Ciò modificherà solo, e di poco, il tempo dopo il quale i led si
spengono automaticamente, mentre tutto il resto funzionerà esattamente allo stesso modo.
Ci sono vari modi di generare un numero casuale per simulare il lancio di un dado.
Ho fatto semplicemente in modo che il programma principale incrementi di continuo una variabile
alla massima velocità possibile. Questo ciclo infinito richiede 3 cicli di istruzione per ogni
incremento e quindi avremo circa 52.000 incrementi al secondo. Quando si preme il pulsante,
si genererà un interrupt e la sua routine di servizio leggerà il valore corrente del conteggio
e lo userà per generare un numero casuale nella gamma 0 - 5. Tale numero verrà mostrato
sui led come un numero compreso tra 1 e 6.
Per generare un numero casuale, il programma prenderà i 3 bit meno significativi del conteggio
(valore 0 - 7). Se il dato non è valido (numeri 6 e 7), il programma prenderà allora in
considerazione i 3 bit immediatamente a sinistra dei primi e se il dato
sarà ancora errato, proverà lo stesso metodo con un secondo contatore.
Una estrazione errata ha una probabilità di circa lo 0,4%, ma basterà premere di nuovo il
pulsante per estrarre un nuovo numero.
Ovviamente possiamo immaginare degli algoritmi migliori, ma quello proposto basta e
avanza per un programma dimostrativo come questo. Attenzione poi a non cedere alla
tentazione di manipolare in qualche modo il conteggio, perché è molto facile ottenere
come risultato una distribuzione non uniforme dei sei numeri!
Per indicare una estrazione errata, faccio lampeggiare tutti e sette i led contemporaneamente.
Un altro problema, comunque del tutto secondario, è che i pulsanti tendono a dare vari impulsi
consecutivi quando li premiamo una sola volta, finché le parti meccaniche non abbiano
smesso di oscilllare. Si potrebbe decidere di ignorare questo problema, lasciando che il
circuito generi vari numeri casuali uno dopo l'altro, di cui solo l'ultimo sarebbe stabilmente
visibile sui led. Ho deciso invece di evitare questo problema, disabilitando il pulsante
per circa 300 ms, ogni volta che viene premuto.
Siccome ci sono tre operazioni legate allo scorrere del tempo (riabilitare il pulsante,
far lampeggiare i led e spegnerli dopo alcuni secondi), avremo anche bisogno di un interrupt
generato da timer 0. Questo interrupt incrementa un secondo contatore che ci permette di
sapere quando è arrivato il momento di compiere ogni operazione.
Così praticamente tutto il programma consiste nella routine di servizio degli interrupt,
che saranno generati dalla pressione del pulsante o dal timeout di timer 0.
Tutti i dettagli sono riportati negli ampi commenti del programma scritto in assembler,
commenti inseriti proprio a scopo educativo. In effetti penso che si possa apprendere molto
sui PIC a partire da questo semplice programma.
Segue il sorgente assembler del programma Dado (dado.asm).
Può essere compilato con il sistema di sviluppo gratuito MPLAB, prelevabile
dal sito della Microchip.
; DADO.ASM - Dado elettronico con PIC PIC16F84A e 7 led - (c) Vinicio Coletti 2005 ; Prima di tutto dobbiamo dire all'assemblatore ed al sistema di sviluppo ; integrato quale chip stiamo utilizzando LIST P=16F84A ; poi carichiamo un file di tipo include, che contiene molte definizioni che ; semplificano la scrittura del programma; per esempio se volessimo bloccare ; tutti gli interrupt dovremmo utilizzare l'istruzione: bcf 11,7 che è ; piuttosto enigmatica e difficile da capire; ; utilizzando i simboli, potremo invece scrivere: bcf INTCON,GIE che è ; sicuramente molto più comprensibile (se abbiamo studiato un po' i PIC) INCLUDE <p16f84a.inc> ; l'altra cosa importante da fare è definire una serie di bit di ; configurazione che vengono memorizzati nel chip al momento della sua ; programmazione e che non sono accessibili né modificabili dal programma ; con l'istruzione seguente stabiliamo che: il ritardo dopo l'accensione ; è abilitato, la protezione del codice è disabilitata, il watchdog timer ; è disabilitato e l'oscillatore utilizzato è quello di tipo RC __CONFIG _PWRTE_ON & _CP_OFF & _WDT_OFF & _RC_OSC ; quanto segue è opzionale: diciamo all'assemblatore di voler essere ; informati su tutti gli errori trovati nel codice, anche i più piccoli ERRORLEVEL 1 ; poi diciamo sempre all'assemblatore che i numeri inseriti nel programma ; devono essere considerati secondo la notazione decimale, ; se non specificato diversamente RADIX DEC ; A questo punto è ora di definire le variabili utilizzate dal programma ; la RAM utente in questo chip va dall'indirizzo 0x0c fino a 0x4f ; per un totale di 68 byte; le variabili, tutte da 1 byte (lunghezza ; predefinita) vengono definite semplicemente elencando i loro nomi; ; la prima corrisponderà all'indirizzo 0x0c, ; la seconda all'indirizzo 0x0d, ecc.; in tutto ci bastano solo 7 byte! cblock 0x0c cont ; contatore usato per generare i numeri casuali flag ; contiene alcuni flag nint ; conta i timeout del timer, ognuno da circa 105 ms save_w ; salva il registro W durante gli interrupt save_status ; salva il registro STATUS durante gli interrupt num ; memorizza il numero casuale generato tent ; conta i tentativi di generazione del numero casuale endc ; Definiamo qui due nomi simbolici per usare più facilmente due flag #define f_ledon flag,0 ; vale 1 se i led sono accesi #define f_lamp flag,1 ; vale 1 se i led devono lampeggiare ; A questo punto inizia il programma vero e proprio! ; ma prima dobbiamo dire da quale indirizzo devono essere memorizzate ; le istruzioni e questo indirizzo sarà ZERO, perché all'accensione ; il contatore di programma punterà proprio a questo indirizzo org 0 ; Ora iniziamo il programma con tre istruzioni che azzerano il registro ; di timer 0 e le due porte di i/o, cosa che preferisco fare anche ; se non obbligatoria :-) Dovremo poi saltare l'indirizzo 4, ; che contiene il vettore di interrupt! clrf TMR0 clrf PORTA clrf PORTB ; ora saltiamo appunto il vettore di interrupt, con un goto ; che ci porta un po' più avanti goto inizio ; Ora viene il vettore di interrupt, ma che significa? ; Semplice: ogni volta che scatta un interrupt, il programma ; salta all'indirizzo 4, dove io metto un goto che porta ; all'inizio della vera routine di gestione degli interrupt goto inter ; qui continua il programma principale; ; per prima cosa dovremo inizializzare una serie di registri ; ed inizieremo dalle due porte di i/o; ; dato che i registri di controllo TRISA e TRISB ; sono nel banco alto della RAM, dovremo prima attivare il bit RP0 ; del registro STATUS inizio bsf STATUS,RP0 ; nella configurazione dei pin 0=output e 1=input ; la porta A non viene usata e la impostiamo tutta come input movlw 0xff movwf TRISA ; la porta B è tutta di output, eccetto il primo pin, ; dove è collegato il pulsante movlw 1 movwf TRISB ; ora impostiamo il registro OPTION, dove ogni bit stabilisce ; una diversa funzione ; - le resistenze interne di pull-up sulla porta B sono abilitate ; - interrupt esterno alla discesa del segnale (transizione da 1 a 0) ; - il clock per le istruizioni è preso dall'oscillatore interno ; - il prescaler è assegnato a timer 0 ; - il valore del prescaler è 64 (1 timeout ogni circa 105 ms) ; per chiarezza il valore viene scritto in notazione binaria movlw B'00000101' movwf OPTION_REG ; ora torniamo nel banco zero della RAM, dove abbiamo le variabili bcf STATUS,RP0 ; e ne azzeriamo alcune, secondo quanto richiesto dal programma clrf cont clrf flag clrf nint ; ora abilitiamo gli interrupt, attivando tre bit nel registro INTCON ; essi riguardano l'abilitazione degli interrupt esterni (INTE), ; il timeout di Timer0 (T0IE) e l'abilitazione generale interrupt (GIE) ; per brevità si memorizza in un colpo solo tutto il byte movlw B'10110000' movwf INTCON ; ora le operazioni di configurazione sono finite e possiamo scrivere ; il programma principale, che consisterà semplicemente in un ciclo ; infinito che incrementa la variabile CONT ; il suo valore varierà da 0 fino a 255 e poi di nuovo a 0, ecc. ; il valore di CONT sarà usato per generare il numero casuale e ; tutta l'elaborazione avverrà nella routine di gestione interrupt loop incf cont,f goto loop ; Ecco qui la routine di gestione degli interrupt! Qui che avviene tutto ; Innazitutto DOBBIAMO salvare i valori correnti dei registri STATUS e W ; Dobbiamo però evitare l'istruzione MOVF, perché modifica lo status! inter movwf save_w swapf STATUS,w movwf save_status ; dato che ci sono due possibili origini per l'interrupt, ; verifichiamo chi è stato a generarlo e se non è stato il timer ; saltiamo a gestire l'interrupt del pulsante btfss INTCON,T0IF goto intpuls ; se siamo arrivati qui, non è stato il pulsante, ma è stato il timer ; prima di tutto incrementiamo il contatore di interrupt incf nint,f ; poi azzeriamo il flag dell'interrupt da timer, per abilitarlo ; le volte successive bcf INTCON,T0IF ; ora dobbiamo decidere cosa fare: riabilitare il pulsante? ; spegnere i led? farli lampeggiare? ; iniziamo con il chiederci se i led sono già spenti, perché nel caso ; non dovremo fare proprio niente e potremo saltare a fine interrupt ; lo stato dei led è indicato dal flag f_ledon btfss f_ledon goto fineint ; se siamo qui, i led sono accesi e ci chiediamo se il pulsante ; è già abilitato btfsc INTCON,INTE goto int1 ; qui il pulsante è disabilitato, così controlliamo se sono passati ; almeno 300 ms dall'ultima pressione, cioè 3 timeout del timer movlw 3 subwf nint,w ; se è passato meno tempo, non abilitiamo il pulsante e saltiamo ; anche la domanda sullo spegnimento dei led, che avviene piu' tardi bnc int2 ; qui sono passati almeno 300 ms, riabilitiamo pulsante, se rilasciato btfss PORTB,0 goto int1 bcf INTCON,INTF bsf INTCON,INTE ; sia che il pulsante era già abilitato, oppure è stato abilitato ora, ; arriviamo qui! dove ci chiediamo se sono passati almeno 10 secondi int1 movlw 100 subwf nint,w ; se sono passati meno di 10 secondi, saltiamo alla routine successiva bnc int2 ; altrimenti spegniamo tutti i led, azzerando la porta B ; e resettiamo anche il flag che indica se i led sono accesi o meno clrf PORTB bcf f_ledon ; dato che abbiamo spento i led, non importa se dovevano lampeggiare ; o meno e quindi saltiamo direttamente a fine interrupt goto fineint ; arriviamo qui se sono passati meno di 10 secondi o meno di 300 ms ; per chiederci se comunque dobbiamo far lampeggiare i led, secondo lo ; stato del flag f_lamp; se è azzerato, saltiamo a fine interrupt int2 btfss f_lamp goto fineint ; qui dobbiamo far lampeggiare i led e per farlo colleghiamo il suo ; stato a quello del bit 1 nella variabile NINT, che conta gli interrupt ; questo bit cambia quindi stato ogni 2 interrupt, cioè ogni 210 ms ; avremo quindi che i led resteranno accesi 210 ms e spenti 210 ms, per ; circa 2,4 lampi al secondo btfsc nint,1 goto lampon clrf PORTB goto fineint lampon movlw B'11111110' movwf PORTB goto fineint ; e così finisce la routine per gli interrupt del timer ; arriviamo qui se l'interrupt è generato dalla pressione del pulsante ; e innanzi tutto disabiltiamo gli ulteriori interrupt di questo tipo ; sarà il timer a riabilitare il pulsante circa 300 ms dopo intpuls bcf INTCON,INTE ; poi azzeriamo il contatore di interrupt del timer ; questo il il nostro tempo zero! clrf nint ; ora attiviamo il flag per i led accesi, perché comunque li accenderemo bsf f_ledon ; inizializziamo una variabile per fare 2 tentativi di generazione movlw 2 movwf tent ; ora inizia il ciclo di generazione del numero casuale e prendiamo ; i 3 bit meno significativi dalla variabile CONT gen movlw 7 andwf cont,w movwf num ; se il numero è minore di 6, va tutto bene movlw 6 subwf num,w bnc ok ; altrimenti slittiamo CONT di 3 bit verso destra rrf cont,f rrf cont,f rrf cont,f ; e prendiamo ancora i 3 bit meno significativi movlw 7 andwf cont,w movwf num movlw 6 subwf num,w bnc ok ; se arriviamo qui, entrambi i tre gruppi di bit vanno male ; così facciamo un secondo tentativo con il valore di TMR0 (timer) ; che prendiamo e spostiamo in CONT movf TMR0,w movwf cont ; poi decrementiamo il numero di tentativi; la prima volta non si avrà ; nessun salto e faremo quindi il nuovo tentativo di generazione ; mentre la seconda volta il GOTO sarà saltato decfsz tent goto gen ; qui entrambi i tentativi sono falliti, rinunciamo e segnaliamo ; l'estrazione errata attraverso il lampeggio dei led ; basta attivare il flag f_lamp ed il timer li farà lampeggiare bsf f_lamp goto fineint ; qui l'estrazione è andata bene, il numero va da 0 a 5, ; blocchiamo l'eventuale lampeggio ok bcf f_lamp ; poi decodifichiamo il numero per pilotare l'output movf num,w call decod movwf PORTB ; azzeriamo cont, per evitare correlazione con tmr0 clrf cont ; questa è la fine comune delle due routine di interrupt ; recuperiamo i valori di STATUS e di W e terminiamo l'interrupt fineint swapf save_status,w movwf STATUS swapf save_w,f swapf save_w,w retfie ; questa subroutine decodifica il numero 0-5 in modo da fornire il ; corrispondente valore per pilotare i led in output ; questi valori dipendono quindi dal cablaggio del circuito, ; cioè da come sono collegati i vari led ; per effettuare la decodifica si utilizza un GOTO calcolato, aggiungendo ; il valore di input, nel registro W, al contatore di programma (PCL) ; in questo modo il programma salterà avanti da 0 fino a 5 istruzioni ; dove troverà un'istruzione "dt" che genera una dt che è un RETURN ; da subroutine con un valore caricato nel registro W ; in questo modo la routine finisce con in W uno dei valori in tabella ; si noti che se una routine di tabella come questa si trovasse in ; una diversa pagina di 256 istruzioni rispetto alla routine chiamante, ; servierebbe all'inizio settare opportunamente il registro PCLATH decod addwf PCL,f dt B'00010000' dt B'00101000' dt B'10010010' dt B'10101010' dt B'10111010' dt B'11101110' ; ed infine diciamo al compilatore che il codice sorgente è terminato end ; e ciò pone fine al sorgente di DADO.ASM, ; scritto da Vinicio Coletti (c) 2005 - 2015