LEDboard/144

This is a 9×16 LED matrix I made by hand back in 2004, with LEDs I had left over from another, even sillier project. It has a USB interface powered by a PIC16C765 microcontroller, one of the first to feature a built-in USB device interface.

This video post is something of a eulogy for the project. I have been trying to simplify and unclutter my life lately, and to that end I’ve been having to recycle, donate, or throw out a lot of things. This includes old projects of mine. This matrix was a fun way to spend a couple weekends seven years ago, but since then it’s just been taking up space in my apartment. So, before throwing it out, I thought I’d take a few pictures and make a silly video.

For what it’s worth, the firmware and PC source code (navi-misc/ledboard/usb144) are still available.


;################################################################################
;
; display.asm - Implements the LED matrix driver itself
;
; The LED board project
; Copyright (C) 2004 Micah Dowty <micah@navi.cx>
;
;  This library is free software; you can redistribute it and/or
;  modify it under the terms of the GNU Lesser General Public
;  License as published by the Free Software Foundation; either
;  version 2.1 of the License, or (at your option) any later version.
;
;  This library is distributed in the hope that it will be useful,
;  but WITHOUT ANY WARRANTY; without even the implied warranty of
;  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;  Lesser General Public License for more details.
;
;  You should have received a copy of the GNU Lesser General Public
;  License along with this library; if not, write to the Free Software
;  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
;
;###############################################################################

	errorlevel -226		; suppress gpasm's silly include file warning

#include <p16c765.inc>
#include "macros.inc"
#include "../include/ledboard_protocol.h"

	errorlevel -302		; supress "register not in bank0, check page bits" message
	errorlevel -306		; ..and the page boundary warning

	global	display_poll
	global	display_init
	global	display_request_flip
	global	display_poll_flip
	global	display_seq_write_byte
	global	display_seek

	global	back_fsr
	global	back_status
	global	scan_rate
	global	pwm_cycles
	global	pwm_table

	extern	temp
	extern	temp2

	;; Hardware definitions
	#define ROW_DRIVE_0	TRISA, 1
	#define ROW_DRIVE_1	TRISA, 2
	#define ROW_DRIVE_2	TRISA, 3
	#define ROW_DRIVE_3	TRISA, 4
	#define ROW_DRIVE_4	TRISA, 5
	#define ROW_DRIVE_5	TRISE, 0
	#define ROW_DRIVE_6	TRISE, 1
	#define ROW_DRIVE_7	TRISE, 2
	#define ROW_DRIVE_8	TRISC, 0
	#define COLUMNS_LOW	PORTD
	#define COLUMNS_HIGH	PORTB


bank0	udata

write_pointer	res	1	; The current position in writing to the backbuffer
scan_state	res	2	; Function pointer for the scanner thread's state

scan_rate	res	2	; 16-bit big-endian TMR1 adjustment used to control scanning speed.
				; Higher values make it go faster, improving refresh rate
				; but possibly increasing flicker due to USB activity.

pwm_cycles	res	1	; Number of scan cycles used for PWM, sets our PWM resolution

pwm_table	res	0x10	; Table mapping LED brightness codes to PWM values

pwm_row		res	0x10	; PWM values for the current LED row

unbanked udata_shr

display_flags	res	1
pwm_iter	res	1	; Iterates from 0 up to pwm_cycles, inclusive

front_fsr	 res	1	; FSR pointing to the current front buffer
front_status	 res	1	; STATUS pointing to the current front buffer
back_fsr	 res	1	; FSR pointing to the current back buffer
back_status	 res	1	; STATUS pointing to the current back buffer

flip_counter	res	1	; Count number of page flips for display_poll_flip
	
#define FLAG_FLIP_REQUEST	display_flags, 0

bank1	udata
vram_buffer_1	res	LEDBOARD_VRAM_SIZE

bank2	udata
vram_buffer_2	res	LEDBOARD_VRAM_SIZE

prog3	code


;*****************************************************************************************
;************************************************************************** Entry Points *
;*****************************************************************************************

display_init
	fpset	scan_state, scanner_thread

	clrf	flip_counter
	
	;; Clear all video buffers
	bzero	vram_buffer_1, LEDBOARD_VRAM_SIZE
	bzero	vram_buffer_2, LEDBOARD_VRAM_SIZE
	bzero	pwm_table, 0x10

	bankisel vram_buffer_1	; Start the front buffer out at vram_buffer_1
	movf	STATUS, w
	movwf	front_status
	movlw	vram_buffer_1
	movwf	front_fsr

	bankisel vram_buffer_2	; ...and the back buffer at vram_buffer_2
	movf	STATUS, w
	movwf	back_status
	movlw	vram_buffer_2
	movwf	back_fsr

	banksel	scan_rate
	movlw	0xFD		; Default scan rate is 0xFD00
	movwf	scan_rate
	clrf	scan_rate+1
	clrf	display_flags	; Clear all display flags
	movlw	0x10		; 16 PWM cycles by default
	movwf	pwm_cycles

	banksel	T1CON		; Start TMR1 running off the instruction clock
	movlw	0x01
	movwf	T1CON
	return


	;; Called frequently from the main loop. It runs our scanning thread a bit
	;; every time TMR1 overflows, adjusting it according to our scan_rate parameter.
display_poll
	banksel	PIR1		; Go away if TMR1 hasn't done anything interesting
	btfss	PIR1, TMR1IF
	return
	bcf	PIR1, TMR1IF

	movf	scan_rate+1, w	; Add our scan_rate adjustment
	addwf	TMR1L, f
	btfsc	STATUS, C
	incf	TMR1H, f
	movf	scan_rate, w
	addwf	TMR1H, f

	fpgoto	scan_state	; Jump to our scanner thread


	;; Request an asynchronous page flip. Has the side effect of resetting
	;; the write pointer.
display_request_flip
	bsf	FLAG_FLIP_REQUEST
	banksel	write_pointer			; Reset the write_pointer
	clrf	write_pointer
	return

	
	;; Return in 'w' the number of page flips that have completed successfully
	;; since the last call to this function.
display_poll_flip
	movf	flip_counter, w
	clrf	flip_counter
	return


	;; Set our write pointer to the given column number, form 'w'.
display_seek
	banksel	write_pointer
	movwf	write_pointer
	return


	; Sequentially write the byte given in 'w' to the backbuffer.
display_seq_write_byte
	movwf	temp
	banksel	write_pointer
	movf	back_status, w
	movwf	STATUS

	movlw	LEDBOARD_VRAM_SIZE	; Test write_pointer - LEDBOARD_VRAM_SIZE
	subwf	write_pointer, w
	btfsc	STATUS, C
	return				; C=1, B=0, LEDBOARD_VRAM_SIZE <= write_pointer

	movf	write_pointer, w	; Write and increment
	addwf	back_fsr, w
	movwf	FSR
	movf	temp, w
	movwf	INDF
	incf	write_pointer, f
	return


	code

;*****************************************************************************************
;*********************************************************************** Scanning Thread *
;*****************************************************************************************

	;; Prepare the pwm_row entry for one column
prepare_pwm_row macro address, column_number, high_nybble
	movf	front_status, w	; Get this LED's nybble
	movwf	STATUS
	movf	front_fsr, w
	addlw	address
	movwf	FSR

	if high_nybble
	swapf	INDF, w
	else
	movf	INDF, w
	endif

	andlw	0x0F		; Index into pwm_table
	addlw	pwm_table
	bankisel pwm_table
	movwf	FSR
	movf	INDF, w		; Store the result in this LED's pwm_row byte
	banksel	pwm_row
	movwf	pwm_row + column_number
	endm


	;; Compare one column against the PWM iterator and right-shift an
	;; inverted bit for its column driver into 'temp'.
	;; Everything in here stays in bank0.
shift_pwm_column macro column_number
	movf	pwm_row + column_number, w
	subwf	pwm_iter, w	; This borrows if our PWM value > pwm_iter.
	rrf	temp, f		; We want the LED to be high if it borrows, so when C=0.
				; We get the invert for free below, when we use comf.
	endm


	;; Output all PWM values for the current row
generate_row_pwm
	banksel	pwm_row
	shift_pwm_column 0x00
	shift_pwm_column 0x01
	shift_pwm_column 0x02
	shift_pwm_column 0x03
	shift_pwm_column 0x04
	shift_pwm_column 0x05
	shift_pwm_column 0x06
	shift_pwm_column 0x07
	comf	temp, w
	movwf	COLUMNS_LOW

	shift_pwm_column 0x08
	shift_pwm_column 0x09
	shift_pwm_column 0x0A
	shift_pwm_column 0x0B
	shift_pwm_column 0x0C
	shift_pwm_column 0x0D
	shift_pwm_column 0x0E
	shift_pwm_column 0x0F
	comf	temp, w
	movwf	COLUMNS_HIGH
	return


	;; Macro to scan one row of our display
scan_row macro prev_port, prev_bit, current_port, current_bit, address
	local	pwm_cycle_loop
	local	pwm_row_done

	;; Prepare the pwm_row buffer for the current row.
	;; We do this before changing row drivers, to reduce the time
	;; when all LEDs are off
	prepare_pwm_row address + 0x00, 0x00, 1
	prepare_pwm_row address + 0x00, 0x01, 0
	prepare_pwm_row address + 0x01, 0x02, 1
	prepare_pwm_row address + 0x01, 0x03, 0
	prepare_pwm_row address + 0x02, 0x04, 1
	prepare_pwm_row address + 0x02, 0x05, 0
	prepare_pwm_row address + 0x03, 0x06, 1
	prepare_pwm_row address + 0x03, 0x07, 0
	prepare_pwm_row address + 0x04, 0x08, 1
	prepare_pwm_row address + 0x04, 0x09, 0
	prepare_pwm_row address + 0x05, 0x0A, 1
	prepare_pwm_row address + 0x05, 0x0B, 0
	prepare_pwm_row address + 0x06, 0x0C, 1
	prepare_pwm_row address + 0x06, 0x0D, 0
	prepare_pwm_row address + 0x07, 0x0E, 1
	prepare_pwm_row address + 0x07, 0x0F, 0

	;; Turn off the last row driver, clear the column drivers, then turn on the
	;; current row. This sequence is necessary to keep the rows isolated.
	banksel	prev_port
	bsf	prev_port, prev_bit
	banksel	COLUMNS_LOW
	clrf	COLUMNS_LOW
	clrf	COLUMNS_HIGH
	banksel	current_port
	bcf	current_port, current_bit

	;; Loop over each PWM cycle...
	banksel	pwm_iter
	clrf	pwm_iter
pwm_cycle_loop

	pscall	generate_row_pwm
	fpsleep	scan_state

	banksel	pwm_iter	; Are we there yet?
	movf	pwm_cycles, w
	xorwf	pwm_iter, w
	pagesel	pwm_row_done
	btfsc	STATUS, Z
	goto	pwm_row_done

	incf	pwm_iter, f
	psgoto	pwm_cycle_loop

pwm_row_done
	endm


	;; Page flip handler. This swaps the front and back
	;; pointers, and increments the flip counter.
page_flip
	bcf	FLAG_FLIP_REQUEST
	swapff	front_fsr, back_fsr
	swapff	front_status, back_status
	incf	flip_counter, f
	return


	;; This is a cooperatively-multitasked thread that scans through the LED matrix.
	;; It's invoked by display_poll each time TMR1 rolls over, and runs until the
	;; next fpsleep.
scanner_thread

	scan_row ROW_DRIVE_8, ROW_DRIVE_0, 0x00
	scan_row ROW_DRIVE_0, ROW_DRIVE_1, 0x08
	scan_row ROW_DRIVE_1, ROW_DRIVE_2, 0x10
	scan_row ROW_DRIVE_2, ROW_DRIVE_3, 0x18

	;; This gets big, switch banks...
	psgoto	scanner_second_half
prog4 code
scanner_second_half

	scan_row ROW_DRIVE_3, ROW_DRIVE_4, 0x20
	scan_row ROW_DRIVE_4, ROW_DRIVE_5, 0x28
	scan_row ROW_DRIVE_5, ROW_DRIVE_6, 0x30
	scan_row ROW_DRIVE_6, ROW_DRIVE_7, 0x38
	scan_row ROW_DRIVE_7, ROW_DRIVE_8, 0x40

	clrwdt			; Pat the watchdog only after a successful scan

	pagesel	page_flip
	btfsc	FLAG_FLIP_REQUEST
	call	page_flip

	psgoto	scanner_thread


	end

;### The End ###

;################################################################################
;
; commands.asm - Implementation of our vendor-specific commands
;
; The LED board project
; Copyright (C) 2004 Micah Dowty <micah@navi.cx>
;
;  This library is free software; you can redistribute it and/or
;  modify it under the terms of the GNU Lesser General Public
;  License as published by the Free Software Foundation; either
;  version 2.1 of the License, or (at your option) any later version.
;
;  This library is distributed in the hope that it will be useful,
;  but WITHOUT ANY WARRANTY; without even the implied warranty of
;  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;  Lesser General Public License for more details.
;
;  You should have received a copy of the GNU Lesser General Public
;  License along with this library; if not, write to the Free Software
;  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
;
;###############################################################################

	errorlevel -226		; suppress gpasm's silly include file warning

#include <p16c765.inc>
#include "usb_defs.inc"
#include "macros.inc"
#include "../include/ledboard_protocol.h"

	errorlevel -302		; suppress "register not in bank0, check page bits" message

	global	CheckVendor
	global	ep0_write_backbuffer

	extern	USB_dev_req
	extern	BufferData
	extern	wrongstate
	extern	temp
	extern	Send_0Len_pkt

	extern	display_request_flip
	extern	display_poll_flip
	extern	display_seek
	extern	display_seq_write_byte

	extern	pwm_cycles
	extern	pwm_table
	extern	scan_rate

bank0	udata

src_ptr		res     1
dest_ptr	res     1
byte_iterator	res     1

	code

;********************************************** Vendor-specific request table

defineRequest	macro	id,	handler
	movf	BufferData+bRequest,w
	xorlw	id
	pagesel	handler
	btfsc	STATUS,Z
	goto	handler
	endm

CheckVendor
	defineRequest	LEDBOARD_CTRL_RANDOM_WRITE3,	request_randomWrite3
	defineRequest	LEDBOARD_CTRL_REQUEST_FLIP,	request_requestFlip
	defineRequest	LEDBOARD_CTRL_SEQ_WRITE12,	request_seqWrite12
	defineRequest	LEDBOARD_CTRL_SEQ_WRITE4,	request_seqWrite4
	defineRequest	LEDBOARD_CTRL_RANDOM_WRITE8,	request_randomWrite8
	defineRequest	LEDBOARD_CTRL_SET_SCAN_RATE,	request_setScanRate
	defineRequest	LEDBOARD_CTRL_SET_PWM_CYCLES,	request_setPwmCycles
	defineRequest	LEDBOARD_CTRL_SET_PWM_ENTRY,	request_setPwmEntry
	defineRequest	LEDBOARD_CTRL_GET_LED_VOLTAGE,	request_getLedVoltage
	defineRequest	LEDBOARD_CTRL_POLL_FLIP,	request_pollFlip
	defineRequest	LEDBOARD_CTRL_STATUS_INTENSITY,	request_statusIntensity

	psgoto	wrongstate		; Not a recognized request


;********************************************** Macros


	; Acknowledge the command with a zero-length packet
returnEmpty		macro
	pagesel	Send_0Len_pkt
	goto	Send_0Len_pkt
	endm

    ; Return 'length' bytes of data copied from buffer
returnBuffer    macro   buffer, length
	local   loop
	movlw   buffer
	banksel src_ptr
	movwf   src_ptr

	banksel BD0IAL
	movf    low BD0IAL,w	; Get the address of the EP0 IN buffer
	banksel dest_ptr
	movwf   dest_ptr	; Start a buffer pointer we'll increment...

	movlw   length
	movwf   byte_iterator
loop

	bankisel buffer
	movf    src_ptr, w
	movwf   FSR

	movf    INDF, w		; Save the referenced byte in temp
	movwf   temp

	movf	dest_ptr, w	; get address of buffer
	movwf   FSR
	bsf	STATUS,IRP	; indirectly to banks 2-3

	movf	temp, w		;  Write the saved byte to our buffer
	movwf   INDF

	incf	src_ptr, f	; Next...
	incf	dest_ptr, f
	banksel byte_iterator
	decfsz  byte_iterator, f
	goto	loop

	banksel BD0IBC
	bsf	 STATUS, RP0
	movlw   length
	movwf   BD0IBC		; set byte count
	movlw   0xc8		; DATA1 packet, DTS enabled
	movwf   BD0IST		; give buffer back to SIE
	return
	endm


    ; Return 1 byte from 'w'
returnByte    macro
	movwf	temp		; Save the byte

	banksel BD0IAL
	movf    low BD0IAL,w	; Get the address of the EP0 IN buffer
	movwf   FSR
	bsf     STATUS,IRP	; indirectly to banks 2-3

	movf    temp, w         ;  Write the saved byte to our buffer
	movwf   INDF

	banksel BD0IBC
	bsf     STATUS, RP0
	movlw   1
	movwf   BD0IBC          ; set byte count
	movlw   0xc8            ; DATA1 packet, DTS enabled
	movwf   BD0IST          ; give buffer back to SIE
	return
	endm


;********************************************** Request handlers

	; Request a page flip at the display's next opportunity.
	; This acknowledges right away, even though the flip itself is asynchronous.
request_requestFlip
	pscall	display_request_flip
	returnEmpty

	
	;; Get the number of asynchronous page flips that have completed
	;; since the last time this request was issued. Returns a 1-byte packet.
request_pollFlip
	pscall	display_poll_flip
	returnByte

	
	; Perform a random 3-byte write to the display, using
	; the low byte of wValue as a column address.
request_randomWrite3
	banksel	BufferData
	movf	BufferData+wValue, w
	pscall	display_seek

	banksel	BufferData
	movf	BufferData+(wValue+1), w
	pscall 	display_seq_write_byte

	banksel	BufferData
	movf	BufferData+wIndex, w
	pscall 	display_seq_write_byte

	banksel	BufferData
	movf	BufferData+(wIndex+1), w
	pscall 	display_seq_write_byte
	returnEmpty


	; Sequentially write 4 bytes from wValue and wIndex
request_seqWrite4
	banksel	BufferData
	movf	BufferData+wValue, w
	pscall 	display_seq_write_byte

	banksel	BufferData
	movf	BufferData+(wValue+1), w
	pscall 	display_seq_write_byte

	banksel	BufferData
	movf	BufferData+wIndex, w
	pscall 	display_seq_write_byte

	banksel	BufferData
	movf	BufferData+(wIndex+1), w
	pscall 	display_seq_write_byte
	returnEmpty


	; Seek to the column given in the low byte of wValue, write data sent to EP0
request_randomWrite8
	banksel	BufferData
	movf	BufferData+wValue, w
	pscall 	display_seek

	; Don't acknowledge yet, wait for some data on EP0 OUT
	movlw	EP0_WRITE_BACKBUFFER
	banksel	USB_dev_req
	movwf	USB_dev_req
	return


	; Perform a sequential 12-byte write. Start with the 4 bytes in this
	; packet, and prepare to receive an 8-byte data packet.
request_seqWrite12
	banksel	BufferData
	movf	BufferData+wValue, w
	pscall 	display_seq_write_byte

	banksel	BufferData
	movf	BufferData+(wValue+1), w
	pscall 	display_seq_write_byte

	banksel	BufferData
	movf	BufferData+wIndex, w
	pscall 	display_seq_write_byte

	banksel	BufferData
	movf	BufferData+(wIndex+1), w
	pscall 	display_seq_write_byte

	; Don't acknowledge yet, wait for some data on EP0 OUT
	movlw	EP0_WRITE_BACKBUFFER
	banksel	USB_dev_req
	movwf	USB_dev_req
	return


	; Handles the data packets that should follow request_seqWrite12
ep0_write_backbuffer
	banksel BD0OAL
	movf    low BD0OAL,w    	; Get the address of the EP0 OUT buffer
	banksel src_ptr
	movwf   src_ptr                 ; Start a buffer pointer we'll increment...

	banksel	BD0OBC
	movf    BD0OBC, w
	banksel	byte_iterator
	movwf   byte_iterator
ep0loop
	banksel	src_ptr
	movf    src_ptr,w		; get address of buffer
	movwf   FSR
	bsf     STATUS,IRP		; indirectly to banks 2-3
	movf	INDF, w
	pscall	display_seq_write_byte

	banksel	src_ptr
	incf    src_ptr, f              ; Next...
	pagesel	ep0loop
	decfsz  byte_iterator, f
	goto    ep0loop
	return


	; Set the current LED scan rate, from wValue
request_setScanRate
	banksel	BufferData
	movf	BufferData+wValue, w
	banksel	scan_rate
	movwf	scan_rate+1

	banksel	BufferData
	movf	BufferData+(wValue+1), w
	banksel	scan_rate
	movwf	scan_rate
	returnEmpty


	; Set the number of PWM cycles, from wValue low
request_setPwmCycles
	banksel	BufferData
	movf	BufferData+wValue, w
	banksel	pwm_cycles
	movwf	pwm_cycles
	returnEmpty


	; Set one entry in our PWM table
request_setPwmEntry
	banksel	BufferData
	movf	BufferData+wIndex, w
	andlw	0x0F
	addlw	pwm_table
	bankisel pwm_table
	movwf	FSR
	movf	BufferData+wValue, w
	movwf	INDF
	returnEmpty


	;; Read back the LED supply voltage, from AN0.
	;; This is nice and simple with no precharge delay, since
	;; we only have one analog input and it was init'ed in usb_main.
request_getLedVoltage
	banksel	ADCON0
	bsf	ADCON0, GO
	pagesel	adc_loop
adc_loop
	btfsc	ADCON0, NOT_DONE
	goto	adc_loop
	movf	ADRES, w
	returnByte


	;; Set the intensity of the status LED (on CCP1) from the
	;; high 10 bits of wValue.
request_statusIntensity
	banksel	BufferData
	movf	BufferData+(wValue+1), w
	banksel	CCPR1L
	movwf	CCPR1L
	banksel	BufferData
	rrf	BufferData+wValue, f
	rrf	BufferData+wValue, w
	andlw	0x30
	iorlw	0x0F
	banksel	CCP1CON
	movwf	CCP1CON
	returnEmpty

	end

;### The End ###