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

		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

		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


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