Some friends asked me to do an electronic dice, just for fun. I thought this was the
occasion to write a simple yet interesting program for the usual PIC 16F84 chip.
Thus I wrote such a program and decided to publish the entire project, including
the assembler source, for educational purposes.
The dice will simply show a random number, 1 to 6, using 7 leds put in the same
shape as the dots on a dice faces. When the user push a button, a new random number
is generated and shown. After about ten seconds of inactivity the leds are switched off,
to save the battery.
Since in this application the timing is not critical, we can save the crystal and
run the chip on the internal RC oscillator. The frequency of oscillation will depends
on the resistor and capacitor used, as well as the voltage and the ambient temperature.
At 25° and the rated 4.5 V, the clock in this circuit will be about 620 kHz,
giving an istruction rate of 155 kHz (6.45 µs per istruction).
Port A of the chip will not be used at all, while the first pin of port B (RB0/INT)
will be used to connect a push button, normally open.
The other seven pins of port B (RB1 - RB7) will drive the seven leds.
The power to the circuit will be given by 3xAA cells, so 4.5 V, but you can try
also with three or four NiMH rechargable cells (3.6 - 4.8 V).
The clock will change depending on the voltage used: the higher the voltage, the slower
the clock. This will only change slightly the timeout used to switch off the leds, all other features
being exactly the same.
The are several ways to generate a random number to simulate the rolling of a dice.
I simply make the main program increment a variable at the maximum possibile speed. This infinte
loop requires 3 istruction cycles for every increment, thus we will have about 52,000 increments
per second. When we push the button, we generate an interrupt and the interrupt service routine
will read the current counter value and will use it to generate a random number in the range 0
to 5. This number will be shown by the leds as a number in the range 1 to 6.
To generate the random number, the program will look at the 3 least significant bits of the
counter (value 0 to 7). If we have an invalid value (6 or 7), the program will look at the 3 bits
on the left and if we have another invalid value, another byte is used. So the tmr0 register
is taken (it is the timer counter) and here also we look for the 3 least significat bits and
then, if needed, to the 3 immediately on their left.
A wrong generation has thus a probability of about 0.4% and in
this case we will simply need to push again the button to have a valid number.
Of course we could make a better algorythm, but I think this is good enough for an aducational
project and beware of the counters manipulations, because it is very easy to obtain
a not uniform distribution of the six numbers!
To signal a wrong generation, I make the seven leds blink all at the same time.
Another problem, very secondary however, is that pushbuttons tend to give many pulses when
we press them once, until the mechanical parts stabilize. One can decide to simply let
the button give all these pulses, so that many random generations would occur within a
few milliseconds (only the last stable one will be visibile on the leds) but I decided
to provide a protection for this: when we press the button, all subsequent pulses
are disabled for about 300 ms.
Since there are three different operations linked to time passing (blinking the leds,
re-enabling the button and switching off the leds), we need that also
Timer 0 generate an interrupt. This timing interrupt will increment a second counter,
used to know when we have to do the different operations.
Thus, basically the whole program is contained in the interrupt service routine, called
either by a button pressure or by a timeout of Timer 0.
You can find further details on the assembler code, that I commented heavily for
educational purpose. I think it is possibile to learn a lot of things about PICs
from this simple code.
What follows is the commented source code for the dice program (dice.asm).
You can compile it with the freeware suite MPLAB that you can download
from the Microchip site.
; DICE.ASM - Electronic dice with a PIC16F74(A) and 7 leds - (c) Vinicio Coletti 2005 ; First of all we must inform the assembler program and the development environment ; about what chip we are using in this source LIST P=16F84A ; then we load an include file with many symbolic definitions that simplify ; the writing of the program; for example if we want to stop all interrupts ; we must clear the most significant bit in the interrupt control register ; writing: bcf 11,7 which is quite obscure, whilst using the mnemonic ; definitions we could write: bcf INTCON,GIE that is easier to understand INCLUDE <p16f84a.inc> ; the other important thing to do initially is to define the bits ; of the configuration word, which are stored on the chip in the ; programming phase and are not accessible by the program; ; here we say: the power on delay is ON, code protect is OFF, ; watchdog timer is OFF, the oscillator we use is RC __CONFIG _PWRTE_ON & _CP_OFF & _WDT_OFF & _RC_OSC ; now this is somewhat optional: we say we want to be informed of all errors ; during compile (this is a message to the compiler) ERRORLEVEL 1 ; another information to the compiler: the numbers we write in the source ; are to be considered in decimal notation, if not otherways stated RADIX DEC ; now we begin to define the variables used by the program ; the user RAM bank in the 16f84(a) begins at address 0x0c and ; ends at address 0x4f for a total of 68 bytes ; we define several variables, all of 1 byte each (the default) ; simply listing their names; the first will be at address 0x0c ; the second at 0x0d and so on; we need only seven bytes! cblock 0x0c count ; counter used to generate the random number flag ; contains several 1 bit flags nint ; timer interrupts counter, each unit about 82 ms save_w ; save w register during interrupts save_status ; save status register during interrupts num ; stores the generated random number 0-5 try ; counter of the generation tries endc ; we need two different 1 bit flags and to simplify their access ; we define two symbolic names for them #define f_ledon flag,0 ; it is 1 if leds are ON #define f_blink flag,1 ; it is 1 if leds must blink ; at this point we can begin to write the real program istructions! ; but first we need to say to the compiler which is the address ; of the first istruction; it will be ZERO because on power on ; reset, the program counter is set to 0; this is the start of all ; PIC programs org 0 ; now lets begin the program; we can put here only 4 istructions ; at addresses 0,1,2,3 because at address 4 there is the interrupt vector! ; we begin by clearing the register of timer0 and the two i/o ports ; this is not mandatory, but I like it :-) clrf TMR0 clrf PORTA clrf PORTB ; then we must jump over the interrupt vector and continue ; to main program goto begin ; this is the interrupt vector! whenever an interrupt occurs, ; the program counter is loaded with the address 4, so the ; program execution jumps here; I put a goto to the real ; interrupt service routine goto inter ; here there is the continuation of the main program ; first of all we must configure some important ; registers and we begin with the i/o ports ; since the special registers TRISA and TRISB are in the high bank of RAM ; we need first to set the bit RP0 in the STATUS register begin bsf STATUS,RP0 ; in every port definition 0=output pin and 1=input pin ; port A is not used and set to all inputs movlw 0xff movwf TRISA ; port B is all outputs, except the first pin, used for the button movlw 1 movwf TRISB ; now we set the OPTION register, where each bit sets a different feature: ; - internal pull-up resitors on port B are enabled ; - external interrupt in on falling edge (1 to 0 transition) ; - istruction clock is from internal oscillator ; - prescaler is assigned to timer 0 ; - prescaler value is 64 (1 timeout every about 105 ms) ; for clarity the value is specified in binary notation movlw B'00000101' movwf OPTION_REG ; now we go back to bank 0 of RAM, where we have our variables bcf STATUS,RP0 ; we clear (that is set to 0) some variables, as needed clrf count clrf flag clrf nint ; now we enable the interrupts, setting three bits in the INTCON register ; they are the external interrupt enable (INTE), the timer 0 interrupt ; enable (T0IE) and the general interrupt enable (GIE) ; to be shorter we do it loading a single value in INTCON movlw B'10110000' movwf INTCON ; now we are finished with the initialitazion and we can begin the ; main program, which in this case consists simply in an infinite ; loop where the variable count in incremented 0 to 255 and then ; it rolls back to 0 and so on ad libitum... ; the value of count will be used to generate the random numbers ; and all the processing will happen in the interrupt service routine loop incf count,f goto loop ; here is the interrupt service routine! all things will happen here ; first of all we MUST save the current STATUS and W registers ; the istruction MOVF must be avoided, because in alters the status! inter movwf save_w swapf STATUS,W movwf save_status ; since there are two possibile interupt source in this program, ; first of all we must know who generated this interrupt; ; if it was not the timer, we jump to the button routine btfss INTCON,T0IF goto intbutt ; if we are here, it's not the button, so it's the timer ; first of all we increment the number-of-interrupts variable incf nint,f ; then we clear the timer interrupt flag, to enable it for the next time bcf INTCON,T0IF ; now we should know what we have to do: enabling the button? ; switching off the leds? blinking them? but if the leds are ; already switched off, we must do nothing and exit the routine ; this state is signaled by the f_ledon flag btfss f_ledon goto endint ; here the leds are ON so we should see if the button is already enabled btfsc INTCON,INTE goto int1 ; here the button is disabled, so we check if about 300 ms are passed ; from the last random number extraction, that is 3 timer interrupts movlw 3 subwf nint,w ; if the time passed is shorter, we don't enable the button and ; jump also the led switching routine, that will happen later bnc int2 ; here about 300 ms are passed, so we re-enable the button, if released btfss PORTB,0 goto int1 bcf INTCON,INTF bsf INTCON,INTE ; either button was already enabled or it was enabled now, here we are! ; where we check if about 10 seconds are passed, to switch off leds int1 movlw 100 subwf nint,w ; if less than 10 seconds, we jump to the next routine bnc int2 ; otherwise we clear all port B, where leds are connected ; and we clear also f_ledon to remember that leds are now off clrf PORTB bcf f_ledon ; since we have switched off the leds, they must not blink either ; so we jump directly to the end of the interrupt routine goto endint ; if time was shorter than 10 seconds we arrive here to see ; if leds are in a blinking state, if not we jump to the end int2 btfss f_blink goto endint ; here we must blink the leds; we do so linking the state of bit ; number 1 in the number-of-interrupts variable to the state of ; the leds (1=on 0=off); since this bit changes every 2 ; Timer0 interrupts, this will produce about 210 ms withs leds ON ; then about 210 ms with leds OFF (about 2.4 blinks per second) btfsc nint,1 goto blinkon clrf PORTB goto endint blinkon movlw B'11111110' movwf PORTB goto endint ; here we arrive if the interrupt was generated by a button pressure ; first of all we disable further button interrupts ; they will be re-enabled by the timer about 300 ms later intbutt bcf INTCON,INTE ; then we clear the counter of timer interrupts, ; this is zero time for us! clrf nint ; we set the flag telling that leds are on (either normal or blinking) bsf f_ledon ; we initialize a variable to make 2 tries of random number generation movlw 2 movwf try ; this is the generation loop, ; we look at the 3 least significant bits of count gen movlw 7 andwf count,w movwf num ; if the number is less than 6, it's all ok movlw 6 subwf num,w bnc ok ; if not, we try shifting count 3 bits to the right rrf count,f rrf count,f rrf count,f ; than we get the 3 least significant bits again movlw 7 andwf count,w movwf num movlw 6 subwf num,w bnc ok ; here we are still with a wrong number, so we make a second try ; getting Timer0 counter value, from TMR0 register ; to repeat the try, we put it inside count variable movf TMR0,w movwf count ; we decrement the try variable, first time it will be different ; from zero, so we will go on to the "goto" that brings us back ; to the generation routine; the second time the goto will be jumped decfsz try goto gen ; here both generations failed, so we set the led blink flag ; then we exit the routine; the timer will blink all leds bsf f_blink goto endint ; here the random number is OK, in the range 0 to 5, we reset blink flag ok bcf f_blink ; then we get the generated number and decode it for output movf num,w call decod movwf PORTB ; clear the counter, to avoid correlation with tmr0 value clrf count ; this is the common end for the two interrupt routines ; we restore the STATUS and W registers, then we exit interrupt routine endint swapf save_status,w movwf STATUS swapf save_w,f swapf save_w,w retfie ; this routine decodes the random number 0-5 to the value needed to ; switch on the seven output leds as if they were on the face of a dice ; these values depend on the circuit wiring; ; to decode, we will add the input value in W to the program counter PCL ; in fact, this is a computed-goto ; the "dt" generates a "retlw" istruction, which is a "return" ; loading also a value into the W register; so we simply list ; the six needed values and the routine will return with the selected ; one loaded in the W register ; please note that in longer programs, where a table routine like this ; is placed in a different 256-bytes-page than the rest of the program, ; it would require at the beginning the correct setting of ; PCLATH register decod addwf PCL,f dt B'00010000' dt B'00101000' dt B'10010010' dt B'10101010' dt B'10111010' dt B'11101110' ; and finally we must tell the compiler that the source code ends here end ; and this is the end of the source code of DICE.ASM ; by Vinicio Coletti (c) 2005 - 2015