SPI to MAX7221 LED Driver

by Marcel Birthelmer

The SPI LED Driver

The purpose of this project was to familiarize myself beyond the blinking-LED stage with programming.

I'm using a p16f648a to implement SPI communication with a Maxim IC Max7221 LED driver, which in turn drives 2 blocks of 2-digit 7-segment LED displays (for a total of 4 digits). The PIC also implements a (rather inefficient) decimal counter.

The SPI protocol

SPI is a rather simple bidirectional communication protocol. It was chosen because I already had the Max7221, and I also intend to use it on future projects. It consists of 4 pins on the client side and 3 pins on the host side which can be shared among all SPI clients, as well as a mechanism to drive the desired client's Chip Select (/CS) pin. The other pins are: SCLK (the clock), MISO (Master-In, Slave-Out), and MOSI (Master-out, Slave-In).

When sending a packet, the order of operations is as follows:

  1. /CS is asserted on the desired client chip
  2. The first bit is pushed out of MOSI (by the host)
  3. SCLK goes high
  4. The client reads the first bit
  5. The first bit is pushed out of MISO (by the client)
  6. SCLK goes low
  7. The host reads the first bit

... and so on for all 16 (in our case) bits. Note that other devices may have a different clock polarity, as well as requiring different timing relations on the first bit. Also note that /CS must be set high before the clock goes high again after the last bit, or data loss will occur (from the MAX7221 data sheet).

The Counter

The counter was simply designed for functionality, without regards to efficiency. On each counting cycle, the unit digit (CTR1) is increased by one. If it equals ten, the digit is reset to 0 and FSR is increased, and the cycle repeats. After 9999, the counter is reset back to 0. Again, this is by no means efficient, as the counter could be contained within 14 bits. However, I simply wanted a counter without working out hex->decimal conversion.

Timing

Delays are created simply by counting down from 255^n to 0, where 1<=n<=3 . Further revisions should probably use a variably scaled timer for the job.

The Circuit

{Ed: See the MAX7221 data sheet for connections to the LED 7-Segement displays} I'm using the p16f648a's internal 4MHz counter (occasionally switched to 48kHz mode for debugging). The pins on the pic are as follows: RA:2 is SPICLK, RB:0 is /CS, MOSI is RB:1, MISO is RB:2. Note that MISO isn't implemented since the MAX7221 doesn't really say anything.

The Code

	LIST P=16F648A, R=HEX
	__FUSES _INTOSC_OSC_CLKOUT & _WDT_OFF & _CP_OFF & _PWRTE_ON
	include "p16f648a.inc"
	
OUT1 EQU 0x20
OUT2 EQU 0x21
IN1 EQU 0x22
IN2 EQU 0x23
TMP1 EQU 0x24
TMP2 EQU 0x25
CNT EQU 0x26
ADDR EQU 0x27
CTR1 EQU 0x28
CTR2 EQU 0x29
CTR3 EQU 0x30
CTR4 EQU 0x31

CS EQU 0
MISO EQU 2
MOSI EQU 1
SPICLK EQU 2

	
	org 0
	goto init
	org 0x10
init:	clrf PORTB ;clear port b
	movlw 0x04
	
	bsf STATUS, RP0
	bcf TRISA, SPICLK
	movwf TRISB; all output except RB2
	bcf STATUS, RP0


	clrf PORTB
	bsf PORTB, CS

	movlw 0x09	; DECODE MODE : DIGITS 1-4
	movwf OUT1
	movlw 0x0F
	movwf OUT2

	call xfer16

	movlw 0x0A ; INADDRSITY : MAX
	movwf OUT1
	movlw 0x0F
	movwf OUT2

	call xfer16

	movlw 0x0B ; SCAN LIMIT : DIGITS 1 - 4
	movwf OUT1
	movlw 0x03
	movwf OUT2
	
	call xfer16
	
	movlw 0x0C ; SHUTDOWN MODE : NORMAL OP
	movwf OUT1
	movlw 0x01
	movwf OUT2

	call xfer16

	clrf CTR1
	clrf CTR2
	clrf CTR3
	clrf CTR4
	
mainloop:	

	movlw CTR1	; load starting address
	movwf FSR


bump:	incf INDF, 1 ; increase current digit
	movlw 0x0F ; restrict to last four bits
	andwf INDF, 0
	xorlw 0x0A ; check if equal to ten
	btfss STATUS, Z
	goto bump_done ; if not, done
	clrf INDF ; else, set to zero
	
	movf FSR, 0 ; check if this was the fourth digit
	xorlw CTR4
	btfsc STATUS, Z ; if so, we're done
	goto bump_done
	incf FSR, 1	; otherwise, go to next digit
	goto bump

bump_done:	

	clrf ADDR
	
	movlw CTR1
	movwf FSR	; start at CTR1
	
pump:	incf ADDR, 1
	movf ADDR, 0	; send address
	movwf OUT1
	movf INDF, 0	; send digit value
	movwf OUT2

	call xfer16
	
	btfsc ADDR, 3	; finish if ADDR==4
	goto finish
	incf FSR, 1		; else send next
	goto pump

finish:	call l0
	goto mainloop

l0:	movlw 0xFF
	movwf 0x42
l0_loop:	decfsz 0x42,1
	goto l0_loop
	return

l1:	movlw 0xFF
	movwf 0x41
l1_loop: call l0
	decfsz 0x41,1
	goto l1_loop
	return

l2:	movlw 0xFF
	movwf 0x40
l2_loop:	call l1
	decfsz 0x40,1
	goto l2_loop
	return


xfer16:	bcf PORTB, CS	; assert /CS
	movf OUT1, 0		
	call xfer8			; send first byte
	movwf IN1
	movf OUT2, 0
	call xfer8			; send second byte
	movwf IN2
	bsf PORTB, CS		; de-assert /CS
	call l0				; wait
	return

xfer8:	movwf TMP1
	movlw 0x8
	movwf CNT

xfer1:	
	rlf TMP1, 1 ;rotate MSB of TMP1 into C
	bcf PORTB, MOSI	;clear output bit
	btfsc STATUS, C ;if rotated-out bit was high,
	bsf PORTB, MOSI ;output high bit
	nop				;keep time constant
	
	bsf PORTA, SPICLK	;bring clock high

	nop
	nop
	nop
	nop
	nop
	nop
	nop
	nop	;8x nop to make clock symmetric
	

	bcf PORTA, SPICLK
	; this would implement MISO, except that we don't need it.
;	bcf STATUS, C		;clear C
;	btfsc PORTB, MISO	;test MISO bit
;	bsf STATUS, C		;set C if MISO bit set
;	nop					;keep time constant
;	rlf TMP2, 1			;rotate carry into TMP2
	
	decfsz CNT, 1
	goto xfer1
	movf TMP2,0			; 'return' TMP2
	
	return
END