;--------------------------------------------------------------------
; Firmware for bicolour LED bargraph USB Blikenlights module.
;
; I2C address bit 1 is masked, so two addresses are associated with
; the module, one for each LED colour. The base address is read from
; PortA on a reset.
; Data received over I2C is stored, and a flag set to indicate it is
; ready for processing.
; An I2C read sends back EEPROM data, containing a description of the
; function of the address.
;
; The main loop outputs data to the LEDs. The output polarity
; determines which colour LED is being controlled. Switching between
; the two rapidly makes them appear to be both working simultaneously.
;
; If new data is detected, then the difference between new and current
; data is calculated and divided by the time since the last data was
; received. This amount is added to the current output value each time
; period, in order to make it interpolate smoothly between data
; receptions.
;
; If no data has been received for one address for more than TIMEOUT
; periods, then the outputs are turned off and an error LED is lit.
; If no data has been received at all for TIMEOUT periods, then the
; uC goes to sleep until the next I2C event (the error LED stays on
; though).
;--------------------------------------------------------------------
; 
; The following files should be included in the MPLAB project:
;
;	16F690.asm-- Main source code file
;
;	16f690.lkr-- Linker script file
;
;---------------------------------------------------------------------
#include <p16f690.inc>

	errorlevel  -302	;suppress message 302 from list file

	__CONFIG   _CP_OFF & _CPD_OFF & _BOR_OFF & _MCLRE_ON & _WDT_OFF & _PWRTE_OFF & _INTRC_OSC_NOCLKOUT & _FCMEN_OFF & _IESO_OFF 

;---------------------------------------------------------------------
;Constant Definitions
;---------------------------------------------------------------------
#define	NODE_ADDR	0x10	; I2C address of this node
							; Change this value to address that
							; you wish to use.

#define	TIMEOUT		0x20	;If this many timer periods elapse without
							;data reception, an error has occurred.

;---------------------------------------------------------------------
; Variable declarations
;---------------------------------------------------------------------
	udata_shr
WREGsave	res	1		;For saving registers in ISR
STATUSsave	res	1
PCLATHsave	res	1

Arg1		res 1		;Used by various functions
Arg2		res 1
Temp		res	1
#define	Diff1		Arg1	;Aliases to aid readability in function code
#define	Diff2		Arg2
#define	Dividend	Arg1
#define	Divisor		Arg2
#define	DivResult	Temp

LastAddress	res 1		;Last I2C address used to send data to this device

;In the following variables:
;offset0 is for first I2C address
;offset1 is for second I2C address
CurrentBits	res 2		;Current output pattern

DataStatus	res 2		;Info about data
#define	New		0x00	;1 if new data available, 0 otherwise
#define	Dir		0x01	;1 if target > current, 0 otherwise
#define	TO		0x02	;1 if timed out, 0 otherwise

	udata 0x20
NewData		res 2		;Data awaiting processing
Current		res 2		;Current output value
Target		res 2		;Target output
Increment	res 2		;Amount to change Current by at each update

Periods		res 2		;Number of timer periods elapsed between
						;last two data receptions

Count		res 2		;Number of timer periods since last
						;data reception


;---------------------------------------------------------------------
; DoIncrement - Increments (or decrements) current value, stopping
; when the target value is reached.
;---------------------------------------------------------------------
DoIncrement macro	offset
	movf	Increment+offset,W
	btfss	DataStatus+offset,Dir	;Which direction to increment?
	goto	Decrement#v(offset)		;Down

	addwf	Current+offset,F	;Increment current value
	btfsc	STATUS,C			;Exceeded maximum possible value?
	goto	Clamp#v(offset)		;Yes

	movf	Current+offset,W	;Current > Target?
	subwf	Target+offset,W
	btfss	STATUS,C
	goto	Clamp#v(offset)		;Yes
	goto	DoIncEnd#v(offset)
Decrement#v(offset):
	subwf	Current+offset,F
	btfss	STATUS,C			;Decremented below 0?
	goto	Clamp#v(offset)		;Yes

	movf	Target+offset,W		;Target > Current?
	subwf	Current+offset,W
	btfss	STATUS,C
	call	Clamp#v(offset)		;Yes
	goto	DoIncEnd#v(offset)
Clamp#v(offset):				;Set Current = Target
	movf	Target+offset,W
	movwf	Current+offset
DoIncEnd#v(offset):
	movf	Current+offset,W	;Determine pattern of LEDs to display
	call	NumToBits
	movwf	CurrentBits+offset

	endm

;---------------------------------------------------------------------
; CalcInc - Calculates how much to increment the displayed value by
; at each update in order to reach the target by the time the next
; data arrives.
;---------------------------------------------------------------------
CalcInc		macro	offset
	movf	NewData+offset,W
	movwf	Target+offset	;Set new data as target

	movwf	Diff1			;Calculate required change in Current
	movf	Current+offset,W
	movwf	Diff2
	call	Diff
	movwf	Dividend		;For later division...

	movf	Count+offset,W	;Current count is number of periods
	movwf	Periods+offset	;since last reception
	movwf	Divisor			;For later division...
	clrf	Count+offset	;Reset count

	call	Div8			;Increment = Target / Periods
	movwf	Increment+offset
	bcf		DataStatus+offset,Dir
	movf	Target+offset,W		;Check if Increment should be +ve or -ve
	subwf	Current+offset,W
	btfss	STATUS,C
	bsf		DataStatus+offset,Dir	;Target > Current, Increment is +ve
	bcf		DataStatus+offset,New	;New data processed succesfully

	endm

;---------------------------------------------------------------------
; Update - Updates an address:
; Checks for new data
; Checks for a timeout
; Updates values.
;---------------------------------------------------------------------
Update		macro	offset
	banksel	Count
	btfsc	DataStatus+offset,New	;New data available?
	goto	DoCalcInc#v(offset)		;yes
	movlw	TIMEOUT			;Timed out?
	subwf	Count+offset,W
	btfss	STATUS,C
	goto	DoIncrement#v(offset)	;no
	clrf	Current+offset			;Clean up some stuff if timed out
	clrf	CurrentBits+offset
	clrf	Target+offset
	clrf	Increment+offset
	bsf		DataStatus+offset,TO	;Set timeout flag
	goto	End#v(offset)
DoCalcInc#v(offset):
	bcf		DataStatus+offset,TO	;Clear timeout flag
	CalcInc	offset
DoIncrement#v(offset):
	incf	Count+offset,F
	DoIncrement	offset
End#v(offset):
	endm

;---------------------------------------------------------------------
; Reinit - Initialize all display-related variables
;---------------------------------------------------------------------
Reinit 	macro	offset
	banksel	Current
	clrf	Current+offset
	clrf	Target+offset
	clrf	Increment+offset
	clrf	Periods+offset
	clrf	Count+offset
	clrf	DataStatus+offset
	clrf	CurrentBits+offset

	endm


;---------------------------------------------------------------------
; Vectors
;---------------------------------------------------------------------
STARTUP	 code
	nop			
	goto	Startup
	nop				; 0x0002
	nop				; 0x0003
	goto	ISR		; 0x0004

PROG code


;---------------------------------------------------------------------
; Main Code
;---------------------------------------------------------------------
Startup:
	call	Setup

Main:
	movf	CurrentBits,W	;Load graph 0
	movwf	PORTC
	clrf	PORTA			;Turn on graph 0
	movlw	0xB0			;Delay, to make time from setting PortA
	call	Delay			;to setting PortC relatively small.

	comf	CurrentBits+1,W	;Load graph 1
	movwf	PORTC
	comf	PORTA,F			;Turn on graph 1
	movlw	0xFF			;Slightly longer delay, to make orange
	call	Delay			;LED brighter

	btfss	INTCON,T0IF		;Loop until timer overflows
	goto	Main
	bcf		INTCON,	T0IF	;Begin update cycle

	Update 0
	Update 1

	banksel	PORTB
	bcf		PORTB,5
	movf	DataStatus,W	;Test to see if either address has timed out
	iorwf	DataStatus+1,W
	movwf	Temp
	btfsc	Temp,TO
	bsf		PORTB,5			;One or both have timed out, set error LED

	movf	DataStatus,W	;Test to see if both addresses are timed out
	andwf	DataStatus+1,W
	movwf	Temp
	btfss	Temp,TO
	goto	Main			;no, go back to looping

	clrf	PORTC			;Both timed out, may as well turn off
	clrf	PORTA			;graphs and sleep until something
	sleep					;interesting happens.

	goto	Main			;Loop forever.


;---------------------------------------------------------------------
; Interrupt Code
;---------------------------------------------------------------------
ISR
	movwf	WREGsave	; Save WREG
	movf	STATUS,W	; Get STATUS register
	banksel	STATUSsave	; Switch banks, if needed.
	movwf	STATUSsave	; Save the STATUS register
	movf	PCLATH,W	;
	movwf	PCLATHsave	; Save PCLATH

	banksel PIR1
	btfss	PIR1,SSPIF	; Is this a SSP interrupt?	
	goto	$			; No, just trap here.
	bcf		PIR1,SSPIF
	call	SSP_Handler	; Yes, service SSP interrupt.

	banksel	PCLATHsave
	movf	PCLATHsave,W
	movwf	PCLATH		; Restore PCLATH
	movf	STATUSsave,W;
	movwf	STATUS		; Restore STATUS
	swapf	WREGsave,F	;
	swapf	WREGsave,W	; Restore WREG
	
	retfie
	
;---------------------------------------------------------------------
; Setup
; Initializes program variables and peripheral registers.
;---------------------------------------------------------------------
Setup:
	banksel	PCON
	bsf		PCON,NOT_POR
	bsf		PCON,NOT_BOR

	banksel ANSELH      ;No need for analogue inputs
	clrf	ANSELH
	clrf	ANSEL

	clrf	PORTA
	clrf	PORTB
	clrf	PORTC
	clrf	PIR1
	banksel	TRISB
	bcf		TRISA,4		;Two outputs for LED reference voltage
	bcf		TRISA,5
	bsf     TRISB,4		;SCL
	bcf		TRISB,5		;Error LED
	bsf     TRISB,6		;SDA
	clrf	TRISC		;PortC is main LED output

	clrwdt					;Clear WDT and prescaler
	banksel	OPTION_REG
	movlw	B'00000111'		;Set TMR0 prescale to 1:16 and
	movwf 	OPTION_REG		;turn on weak pull-ups

;---------------------------------------------------------------------
;Work around for first I2C start-bit being ignored, as per rev. A errata
;Set SSPMSK at the same time
	banksel	SSPCON
	movlw	B'00111001' ;Module enable, clock 
	movwf	SSPCON		;enable, SSPMSK access

	banksel	SSPMSK
	movlw	B'11111101'	;Respond to two addresses
	movwf	SSPMSK

	banksel	SSPCON
	movlw	B'00110110' ;Module enable, clock
	movwf	SSPCON		;enable, 7-bit address I2C slave.
;---------------------------------------------------------------------

	movlw	0x00
	banksel	PORTA		;Read ID bits.
	btfsc	PORTA,2		;Bits are in reverse order on the port
	iorlw	b'00000100'	;Shift them up two places, past R/W bit
	btfsc	PORTA,1		;and the masked address bit
	iorlw	b'00001000'
	btfsc	PORTA,0
	iorlw	b'00010000'
	addlw	NODE_ADDR	;Add minimum allowed I2C address

	banksel	SSPADD
	movwf	SSPADD		;Set I2C address
	clrf	SSPSTAT
		
	banksel	PIE1			;Enable interrupts
	bsf		PIE1,SSPIE
	bsf		INTCON,PEIE		;Enable all peripheral interrupts
	bsf		INTCON,GIE		;Enable global interrupts

	Reinit	0x0
	Reinit	0x1

	return

;---------------------------------------------------------------------
; SSP_Handler
;---------------------------------------------------------------------
;	The I2C code below checks for 5 states:
;---------------------------------------------------------------------
;	State 1:  	I2C write operation, last byte was an address byte.
;
;	SSPSTAT bits:  	S = 1, D_A = 0, R_W = 0, BF = 1
;
;	State 2:	  I2C write operation, last byte was a data byte.
;
;	SSPSTAT bits:	  S = 1, D_A = 1, R_W = 0, BF = 1
;
;	State 3:	  I2C read operation, last byte was an address byte.
;
;	SSPSTAT bits:	  S = 1, D_A = 0, R_W = 1, BF = 0
;
;	State 4:	  I2C read operation, last byte was a data byte.
;
;	SSPSTAT bits:	  S = 1, D_A = 1, R_W = 1, BF = 0
;
;	State 5:  Slave I2C logic reset by NACK from master.
;	
;	SSPSTAT bits:  S = 1, D_A = 1, R_W = 0, BF = 0
;
; For convenience, WriteI2C and ReadI2C functions have been used.
;----------------------------------------------------------------------
SSP_Handler:
	banksel	SSPSTAT
	;btfss	SSPSTAT,S		;Commented out due to
	;goto	I2CErr			;start bit not working
	btfsc	SSPSTAT,R_W
	goto	I2CReadCheck

I2CWriteCheck:				;Write operation (R_W = 0)
	banksel	SSPSTAT
	btfss	SSPSTAT,BF
	goto	I2CNACKCheck
	btfss	SSPSTAT,D_A
	goto	State1			;Last byte was address (D_A = 0)
	goto	State2			;Last byte was data (D_A = 1)

I2CNACKCheck:
	btfsc	SSPSTAT,D_A
	goto	State5			;NACK received
	goto	I2CErr

I2CReadCheck:				;Read operation (R_W = 1)
	btfsc	SSPSTAT,BF
	goto	I2CErr
	btfss	SSPSTAT,D_A
	goto	State3			;Last byte was address (D_A = 0)
	goto	State4			;Last byte was data (D_A = 1)

State1:	;Write operation, last byte was an address, buffer is full.
	call	ReadI2C			;Store address for future reference
	movwf	LastAddress
	return
	
State2:	;Write operation, last byte was data, buffer is full.
	call	ReadI2C			;Get the byte from the SSP.
	btfsc	LastAddress, 1	;See which address this byte is for
	goto	NewDataFor1
NewDataFor0:
	banksel	NewData
	movwf	NewData			;Make new byte available
	bsf		DataStatus,New	;Notify of new data availabilty
	return
NewDataFor1:
	banksel	NewData
	movwf	NewData+1			;Make new byte available
	bsf		DataStatus+1,New	;Notify of new data availabilty
	return
	
State3:	;Read operation, last byte was an address, buffer is empty.
	movlw	0x00
	btfsc	LastAddress, 1	;See which address this byte is for
	movlw	0x80			;Address1 starts at offset 0x80
	banksel	EEADR
	movwf	EEADR
	banksel	EECON1
	bsf		EECON1,RD	;Do a read
	banksel	EEDAT
	movf	EEDAT,W

	call	WriteI2C	;Write the byte to SSPBUF
	return
	
State4:	;Read operation, last byte was data, buffer is empty.
	banksel	EEADR
	incf	EEADR,F		;Next EEPROM byte
	banksel	EECON1
	bsf		EECON1,RD	;Read byte
	banksel	EEDAT
	movf	EEDAT,W

	call	WriteI2C	;Write to SSPBUF
	return

State5: ;A NACK was received when transmitting.
	return

I2CErr:	;Do nothing.
	return

;---------------------------------------------------------------------
; WriteI2C
;---------------------------------------------------------------------
WriteI2C:
	banksel	SSPSTAT
	btfsc	SSPSTAT,BF	; Is the buffer full?
	goto	WriteI2C	; Yes, keep waiting.
	banksel	SSPCON		; No, continue.
DoI2CWrite:
	bcf		SSPCON,WCOL	; Clear the WCOL flag.
	movwf	SSPBUF		; Write the byte in WREG
	btfsc	SSPCON,WCOL	; Was there a write collision?
	goto	DoI2CWrite
	bsf		SSPCON,CKP	; Release the clock.
	return

;---------------------------------------------------------------------
; ReadI2C
;---------------------------------------------------------------------
ReadI2C:
	banksel	SSPBUF
	movf	SSPBUF,W	; Get the byte and put in WREG
	return


;---------------------------------------------------------------------
; Delay - Delays by W * 3 + 1 cycles
;---------------------------------------------------------------------
Delay:
	movwf   Temp
	decfsz  Temp,F
	goto    $-1


;---------------------------------------------------------------------
; Div8 - 8bit integer division. Divides Dividend by Divisor and puts
; the result in W. Truncates result.
; The result is never allowed to be less than one (prevents display
; from becoming stuck if incoming data varies only slightly).
; The result is zero if divisor is zero.
;---------------------------------------------------------------------
Div8:
	clrf	DivResult
	movf	Divisor,W
SubLoop:
	subwf	Dividend,F	;Simply keep subtracting until result < 0
	btfss	STATUS,C
	goto	DivDone
	incf	DivResult,F	;Keep track of number of subtractions
	goto	SubLoop		;Go around once more...
DivDone:
	movf	DivResult,W
	btfsc	STATUS, Z	;Check if result is zero
	retlw	0x01		;Never return less than zero
	return


;---------------------------------------------------------------------
; Diff - Calculates the magnitude of Diff1 - Diff2
;---------------------------------------------------------------------
Diff:
	movf	Diff1,W
	subwf	Diff2,W
	btfsc	STATUS,C	;Result -ve?
	return
	movf	Diff2,W		;Yes. Subtract the other way.
	subwf	Diff1,W
	return

;---------------------------------------------------------------------
; NumToBits - Turns an 8bit number in W into an 8bit bitmask
; Horribly massive table, but might as well fill up all that RAM!
;---------------------------------------------------------------------
NumToBits:
	movwf	Temp
	movlw	LOW Table	;get low 8 bits of address
	addwf	Temp,F		;do an 8-bit add operation
	movlw	HIGH Table	;get high 5 bits of address
	btfsc	STATUS,C	;page crossed?
	addlw	0x1			;yes then increment high address
	movwf	PCLATH		;load high address in latch
	movf	Temp,W		;load computed offset in w reg
	movwf	PCL			;Table offset
Table:
	retlw	0x00	;0 bits set
	retlw	0x00
	retlw	0x00
	retlw	0x00
	retlw	0x00
	retlw	0x00
	retlw	0x00
	retlw	0x00
	retlw	0x00	;0 bits set
	retlw	0x00
	retlw	0x00
	retlw	0x00
	retlw	0x00
	retlw	0x00
	retlw	0x00
	retlw	0x00
	retlw	0x01	;1 bit set
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01	;1 bit set
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01	;1 bit set
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01	;1 bit set
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x01
	retlw	0x03	;2 bits set
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03	;2 bits set
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03	;2 bits set
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03	;2 bits set
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x03
	retlw	0x07	;3 bits set
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07	;3 bits set
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07	;3 bits set
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07	;3 bits set
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x07
	retlw	0x0F	;4 bits set
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F	;4 bits set
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F	;4 bits set
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F	;4 bits set
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x0F
	retlw	0x1F	;5 bits set
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F	;5 bits set
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F	;5 bits set
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F	;5 bits set
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x1F
	retlw	0x3F	;6 bits set
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F	;6 bits set
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F	;6 bits set
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F	;6 bits set
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x3F
	retlw	0x7F	;7 bits set
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F	;7 bits set
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F	;7 bits set
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F	;7 bits set
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0x7F
	retlw	0xFF	;8 bits set
	retlw	0xFF
	retlw	0xFF
	retlw	0xFF
	retlw	0xFF
	retlw	0xFF
	retlw	0xFF
	retlw	0xFF
	retlw	0xFF	;8 bits set
	retlw	0xFF
	retlw	0xFF
	retlw	0xFF
	retlw	0xFF
	retlw	0xFF
	retlw	0xFF
	retlw	0xFF

;Initialize eeprom. First half is for Address0, second half for Address1.
EE		CODE	0x2100
		DE	"Green LED bar graph"

		CODE	0x2180
		DE	"Orange LED bar graph"

	end
