www.viniciocoletti.it > Projects > Dado elettronico

Vinicio Coletti

Dado elettronico


Clicca l'immagine per vedere un filmato

Descrizione

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.

Circuito elettrico


Lista dei componenti

Il programma

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.

Il sorgente Assembler

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