PIC Tutorial Thirteen -
Multiplexed LED's
For
these tutorials you require the 876 or 877 Main Board and
Multiplex LED Board.
Download zipped tutorial files.
These
tutorials demonstrate how to multiplex 64 LED's,
arranged as a matrix of 8x8, we use one port to 'source' the
columns, and another port to 'sink' the rows. So the Multiplex LED board
requires two port connections, one for rows and one for columns - notice
that NEITHER of these have the 0V or 5V pins connected, they aren't used at
all. The technique of multiplexing allows you to individually access any one
LED, or any combination of LED's, with just sixteen output pins.
It's
also worth noting that we're driving the LED's entirely from the PIC, which
is why I chose 150 ohm current limiting resistors, this keeps the current
down to within the PIC's individual pin limits, and also within the overall
chip limits - but both are pushed pretty close!, this is done to get as much
brightness as possible from the LED's, whilst minimising the
component count on the board.
Because of the current requirements the processor board MUST have a 7805
regulator, a 78L05 won't provide enough current.
The
technique used for the display is to show each section in turn, so we
display row 1, then row 2,
then row 3, and continue through the other five rows - as long as we
do this fast enough you can't see any flickering - this obviously restricts
what else the program can be doing, as we must refresh the display regularly
enough to prevent visible flicker. As with the previous 7
segment multiplexing tutorial, I'm again using timer driven interrupts in
order to transparently refresh the LED matrix - and the code is basically
very similar (and simply modified from it).
The
interrupts are generated by Timer2, which is set to give an interrupt
roughly every 16mS. Interrupts in a PIC cause the program to stop what it's
doing, save it's current location, and jump to the 'Interrupt Vector' -
which on a PIC is address 0x0004. The interrupt routine then does what it
needs to do and exits using the 'REFIE' (REturn From IntErrupt)
command - this works rather like a normal 'RETURN' in that it returns the
program to where it was when the interrupt was called. An important point to
bear in mind when using interrupts is that you mustn't change anything the
main program is using - for that reason the first thing we must do is save
various resisters - the W register, the PCLATH register, and the STATUS
register - these are saved in data registers allocated for their use, and
are restored before the routine exits via 'RETFIE'.
The
interrupt routine is shown below, the first 5 lines save the registers
mentioned above, the 'swapf' command is used as it doesn't affect the STATUS
register, there's not much point saving the register if we've already
changed it!. Next we check is the interrupt was generated by the timer or
not, in this case we're only using one interrupt source (TIMER2) - but often
you may be using multiple interrupt sources, if it's not TIMER2 we simply
jump to the EXIT routine, which restores the registers saved and exits
via RETFIE.
Now we
start the actual interrupt routine itself, the first thing we have to do is
reset TIMER2 - once it's triggered an interrupt it turns itself off, so we
turn it back on here (bcf PIR1,TMR2IF). Next we start our display routine,
first we need to find out which display we're updating - in this
case we're selecting one of eight display ROWS in turn, we do this by simply
checking which was the previous display (using seven 'btfss' instructions,
with each one jumping to a different routine to set the new ROW to be
displayed) - I originally tried to do this directly using the PORT register,
but it didn't really work, so I added a GPR (row_pos) which I use to store
the position. We only need seven tests, because if those seven fail it MUST be
the eighth one that was the previous row. The actual values for the eight
columns are
stored in the variables 'zero', 'one', 'two' etc. Each of the eight row
display routines (Do_Zero etc.) first turns OFF the previous row, which will
blank the complete display, then preload the column data from the respective
register. Lastly it turns that row ON, to display the data for that row.
There's probably no need to blank the display before displaying the new
row?, but it ensures that only one row at a time is ever displayed - and it
only costs 400nS extra time with a 20MHz clock.
; Interrupt vector
ORG 0x0004
INT
movwf w_temp ; Save W register
swapf STATUS,W ; Swap status to be saved into W
movwf s_temp ; Save STATUS register
movfw PCLATH
movwf p_temp ; Save PCLATH
btfss PIR1,TMR2IF ; Flag set if TMR2 interrupt
goto INTX ; Jump if not timed out
; Timer (TMR2) timeout every 16 milliseconds
bcf PIR1,TMR2IF ; Clear the calling flag
btfss row_pos, 0 ;check which ROW was last
goto Do_One
btfss row_pos, 1 ;check which ROW was last
goto Do_Two
btfss row_pos, 2 ;check which ROW was last
goto Do_Three
btfss row_pos, 3 ;check which ROW was last
goto Do_Four
btfss row_pos, 4 ;check which ROW was last
goto Do_Five
btfss row_pos, 5 ;check which ROW was last
goto Do_Six
btfss row_pos, 6 ;check which ROW was last
goto Do_Seven
Do_Zero movlw 0xFF
movwf ROW_PORT ;turn off all rows
movwf row_pos
movf zero, w
movwf COL_PORT ;load columns
bcf row_pos, 0
bcf ROW_PORT, 0 ;turn ON row zero
goto INTX
Do_One movlw 0xFF
movwf ROW_PORT ;turn off all rows
movwf row_pos
movf one, w
movwf COL_PORT ;load columns
bcf row_pos, 1
bcf ROW_PORT, 1 ;turn ON row one
goto INTX
Do_Two movlw 0xFF
movwf ROW_PORT ;turn off all rows
movwf row_pos
movf two, w
movwf COL_PORT ;load columns
bcf row_pos, 2
bcf ROW_PORT, 2 ;turn ON row two
goto INTX
Do_Three movlw 0xFF
movwf ROW_PORT ;turn off all rows
movwf row_pos
movf three, w
movwf COL_PORT ;load columns
bcf row_pos, 3
bcf ROW_PORT, 3 ;turn ON row three
goto INTX
Do_Four movlw 0xFF
movwf ROW_PORT ;turn off all rows
movwf row_pos
movf four, w
movwf COL_PORT ;load columns
bcf row_pos, 4
bcf ROW_PORT, 4 ;turn ON row four
goto INTX
Do_Five movlw 0xFF
movwf ROW_PORT ;turn off all rows
movwf row_pos
movf five, w
movwf COL_PORT ;load columns
bcf row_pos, 5
bcf ROW_PORT, 5 ;turn ON row five
goto INTX
Do_Six movlw 0xFF
movwf ROW_PORT ;turn off all rows
movwf row_pos
movf six, w
movwf COL_PORT ;load columns
bcf row_pos, 6
bcf ROW_PORT, 6 ;turn ON row six
goto INTX
Do_Seven movlw 0xFF
movwf ROW_PORT ;turn off all rows
movwf row_pos
movf seven, w
movwf COL_PORT ;load columns
bcf row_pos, 7
bcf ROW_PORT, 7 ;turn ON row seven
INTX
movfw p_temp
movwf PCLATH ; Restore PCLATH
swapf s_temp,W
movwf STATUS ; Restore STATUS register - restores bank
swapf w_temp,F
swapf w_temp,W ; Restore W register
retfie
The
interrupt routine above is complete and working, but it requires
TIMER2 setting up before it can work, this is done in the 'Initialise'
section of the program, which is the first section called when the program
runs. This first turns the analogue inputs off, then sets the direction registers
for the Ports to all outputs, then sets TIMER2 to the correct time, you will
notice there's two lines (with one commented out) for setting the value of
T2CON - if you comment out the second one, and use the first one, it makes
the interrupt routine timing too slow - you can actually see the digits
flickering, first one then the other - worth doing so you can see what's
going on. After that we set up PR2, this sets the value that the timer
counts to before it times out - then we enable TIMER2 interrupts by setting
the TMR2IE flag in register PIE1. This still isn't enough, so finally we
write to the INTCON register to enable 'Peripheral Interrupts' and 'Global
Interrupts'.
Initialise BANKSEL ADCON1 ;disable analogue inputs
movlw 0x06
movwf ADCON1
BANKSEL PORTA
bsf STATUS, RP0 ;select bank 1
movlw b'00000000' ;Set port data directions, data output
movwf ROW_TRIS
movwf COL_TRIS
bcf STATUS, RP0 ;select bank 0
clrf COL_PORT ;turn OFF all LED's
movlw 0xFF
movwf ROW_PORT
call Clear ;clear display registers
; Set up Timer 2.
;movlw b'01111110' ; Post scale /16, pre scale /16, TMR2 ON
movlw b'00010110' ; Post scale /4, pre scale /16, TMR2 ON
movwf T2CON
bsf STATUS, RP0 ;select bank 1
movlw .249 ; Set up comparator
movwf PR2
bsf PIE1,TMR2IE ; Enable TMR2 interrupt
bcf STATUS, RP0 ;select bank 0
; Global interrupt enable
bsf INTCON,PEIE ; Enable all peripheral interrupts
bsf INTCON,GIE ; Global interrupt enable
bcf STATUS, RP0 ;select bank 0
This
is the main section of the first program, as you can see there's not a great
deal to it, all it does it display a 'square' using the outer layer of
LED's, delay 1 second, then 'invert' the display (make lit ones dark, and
dark ones lit), and delay a further second,
then loop back, this give a simple flashing square pattern on the display.
As with the previous interrupt multiplexing tutorial, the display function is
handled totally transparently by the interrupt routine - all we need to do
is place the values required in the eight display data registers.
Main call Square ;display a square
call Delay1Sec
call Invert ;and clear it
call Delay1Sec
goto Main ;endless loop
Tutorial 13.1
This
tutorial flashes an inverting 'square' at 1 second intervals. To display a
pattern, we simply write it to the eight data registers, zero is the bottom
line, and seven is the top. To invert the display we simple XOR the display
registers with 0xFF.
Tutorial 13.2
Where
the previous tutorial used just eight display registers, this second one
uses two sets of eight - one is the same as before (the actual registers
which get displayed), the second is a duplicate set - labelled zero1 to
seven 1. This allows a range of simple effects by changing from one to the
other, and this tutorial gives routines for scrolling in all four
directions, and displaying numeric digits. The code presented scrolls the
digits 0 to 9 from left to right, right to left, bottom to top, and top to
bottom, on the display. It's pretty obvious how the other routines can be
used, just load the second set of registers, and call them as you wish.
Here's a 20 second video clip of the scrolling in action
LED1.WMV
Tutorial 13.3
Following
on from the previous tutorial, this one adds the rest of the ASCII character
set, including upper and lower case letters. The fonts used is based on the
Hitachi text LCD character set - but as I've an extra line I've added true
descenders on those characters which have them. Rather then the previous
simple method of storing the character maps, this tutorial stores them in a
large table - as the table exceeds the normal 256 byte table limit, we use
an extended 16 bit table to provide sufficient room for all the data, this
also overcomes the 256 byte boundary problem. The tutorial itself simply
scrolls the entire character set (starting with a space) from left to right.
Tutorial 13.4
This
is tutorial 13.3 with an extra table added, which is used to store a string
to be displayed - the string scrolls across the display from right to left,
the string can easily be changed, and as before is stored as a large table -
you just need to add a 0x00 at the end to signify the end of the string. The
string is standard ASCII, and the display routine subtracts 0x20 from it to
match the character set. Again, you can see a short video of
this tutorial in action TUT13_4.WMV
|