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
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.
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
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.
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