; ================================================================================================
; PROJECT:		SnesHack - A wireless/Wii-enabled SNES and NES controller hack.
; FILENAME:		Receiver.asm
; DESCRIPTION:	Code for the 16F84A receiver firmware.
; COPYRIGHT:	Copyright (c) 2007 Mark Feldman. All Rights Reserved.
; ================================================================================================

	list	p=16F84A
	radix	hex

	#include "C:\Program Files\Microchip\MPASM Suite\p16f84a.inc"

	__config	_HS_OSC & _PWRTE_OFF & _WDT_OFF & _CP_OFF

; ================================================================================================
; constants
; ================================================================================================

FALSE					equ	0
TRUE					equ	(!FALSE)

; used when we've lost the wireless signal. this is the number of good packets in a row we need
; to receive before assuming that we're picking up a good signal again.
VALID_SIGNAL_COUNT		equ 3

; REC_PORT/REC_BIT reads data from the console, it needs to be a pin that can trigger an interrupt.
; SEND_PORT/SEND_BIT sends data to the console on the same line, it needs to be open collector.
; these pins are both connected directly to the console's data line; if this program is ported
; to another type of PIC then there is no reason why they can't be implemented with a single pin,
; provided that pin supports interrupt-on-falling-edge and is open collector output.
REC_PORT				equ	PORTB
REC_BIT					equ 0
SEND_PORT				equ PORTA
SEND_BIT				equ 4

; this pin reads incoming data packets from the wireless receiver module.
WIRELESS_PORT			equ PORTB
WIRELESS_BIT			equ 1

; these pins are used to control the bidirectional red/green LED used to indicate whether or
; not we are receiving a valid wireless signal. they have no other function and can be safely
; removed if needed.
RED_LED_PORT			equ PORTB
RED_LED_BIT				equ 2
GREEN_LED_PORT			equ PORTB
GREEN_LED_BIT			equ 3

; this pin is used to generate a test signal that is held high while a wireless packet is being read.
; it has no other function, and can be safely removed if needed.
TEST_PORT				equ PORTB
TEST_BIT				equ 4

; ================================================================================================
; variables
; ================================================================================================

; variables used by the interrupt handler when receiving console packets
W_TEMP					equ 0Ch				; saves W
STATUS_TEMP				equ 0Dh				; saves STATUS
RECEIVED_PACKET			equ 0Eh				; lets main loop know that a packet was just received
BITS_READ				equ 0Fh				; total number of bits read so far
BITS_TO_SEND			equ	10h				; total number of bits to send to the console
BIT_NUM					equ	11h				; total number of bits in the current byte read/sent
CURRENT_BYTE			equ	12h				; curent byte being sent (so we don't trash the value in INDF)

; scratch variables used by the wireless timing routines
TIMER1					equ	14h
TIMER2					equ	15h

; used when we've lost the wireless signal, counts the number of good packets
; in a row that we've received
PACKET_COUNTER			equ	16h

; counts the number of interrupts we've had since the last valid wireless packet was received.
; when this number exceeds some critical value (hard-coded to 10) then the receiver assumes
; we've lost the wireless signal.
TIMEOUT_COUNTER			equ	17h

; temporary 2-byte buffer used to store button data read in from the wireless packets.
; the data is then copied into the WIRELESS_BUTTONS packet for sending to the console.
WIRELESS_BUTTONS		equ 18h

; an 11-byte packet sent to the console containing the button press states. the first
; two bytes of this structure are filled with the data from WIRELESS_BUTTONS after a
; complete, uncorrupted packet has been received from the wireless module. this
; double-buffering helps avoid random button messages being sent to the controller
; when the wireless signal is disconnected mid-packet.
CONSOLE_BUTTONS			equ 20h

; a general buffer area used to store packet data sent to and received from the console
PACKET_BUFFER			equ 30h

; ================================================================================================
; macros
; ================================================================================================

; sets the open collector output data line low
SEND_0					MACRO
						bcf		SEND_PORT, SEND_BIT
            			ENDM

; sets the open collector output data line high
SEND_1					MACRO
						bsf		SEND_PORT, SEND_BIT
            			ENDM

; initializes FSR to point to a buffer that will be used to store a packet destined for the console
START_PACKET			MACRO	BUFFER_ADDR
						movlw	BUFFER_ADDR
						movwf	FSR
						ENDM

; packs a constant value into the buffer pointed to by FSR and increments the buffer pointer
PACK_CONST				MACRO	CONST
						movlw	CONST
						movwf	INDF
						incf	FSR,F
						ENDM

; finishes off a packet by adding a 1 bit to the end of it and gets the buffer ready to
; send by pointing FSR to the start of it
END_PACKET				MACRO	BUFFER_ADDR, NUM_BYTES
						PACK_CONST 08h
						movlw	BUFFER_ADDR
						movwf	FSR
						movlw	NUM_BYTES*8+1
						ENDM

; reads a button state from an incoming wireless packet, sets the appropriate bit in the
; buffer, waits for the next bit to arrive and then checks to make sure that an interrupt
; hasn't occurred (interrupts throw off the timing, so the rest of the wireless packet has
; to be ignored; skipping a packet doesn't matter though, as we receive at least two wireless 
; packets for every console packet).
READ_WIRELESS_BUTTON	MACRO	ADDR, BIT, REVERSE_POLARITY
						bcf		ADDR, BIT							; clear the buffer bit
#if REVERSE_POLARITY
						btfss	WIRELESS_PORT, WIRELESS_BIT			; read the wireless port, is it 1?
#else
						btfsc	WIRELESS_PORT, WIRELESS_BIT			; read the wireless port, is it 0?
#endif
						bsf		ADDR, BIT							; yes, so set the buffer bit
						call	wireless_pulse_delay				; wait for the next pulse
						ENDM

; turns the LED off
LED_OFF					MACRO
						bcf		RED_LED_PORT, RED_LED_BIT
						bcf		GREEN_LED_PORT, GREEN_LED_BIT
						ENDM

; sets the LED to red to indicate we have power but not a valid wireless signal
LED_RED					MACRO
						bcf		GREEN_LED_PORT, GREEN_LED_BIT
						bsf		RED_LED_PORT, RED_LED_BIT
						ENDM

; sets the LED to green to indicate we have power and a valid wireless signal
LED_GREEN				MACRO
						bcf		RED_LED_PORT, RED_LED_BIT
						bsf		GREEN_LED_PORT, GREEN_LED_BIT
						ENDM

; these macros are used to generate a test signal that goes high when we're reading
; a packet from the wireless port. used solely for testing on an oscilloscope.

TEST_SIGNAL_ON			MACRO
						bsf		TEST_PORT, TEST_BIT
						ENDM

TEST_SIGNAL_OFF			MACRO
						bcf		TEST_PORT, TEST_BIT
						ENDM

; ================================================================================================
; entry point -	jumps to the initialization and main loop
; ================================================================================================
	org	0000h
		goto	initialization

; ================================================================================================
; interrupt handler -	there's only one type of interrupt used in this program; it gets
;						triggered when the console sends a command. this function reads the
;						command, parses it, dispatches control to the appropriate handler,
;						and re-enables interrupts before returning. timing is critical in this
;						function...we don't have time to jump to a handler, so the handler code
;						has to appear here.
; ================================================================================================
	org	0004h
		
		; get the next packet from the console	  (cycle #)
		movwf	W_TEMP							; ( 2/3) save W register
		swapf	STATUS, W						; ( 3/4) save STATUS register
		movwf	STATUS_TEMP						; ( 4/5)
		movlw	PACKET_BUFFER					; ( 5/6) FSR <- buffer address
		movwf	FSR								; ( 6/7)
		clrf	BIT_NUM							; ( 7/8)
read_bit
		incf	BITS_READ, F					; (8) increment bits read
		bcf		STATUS, C						; (9)		clear carry flag
		btfsc	REC_PORT, REC_BIT				; (10)		is this bit a 1?
		bsf		STATUS, C						; (11)		if yes then set carry bit
		rlf		INDF, F							; (12)		rotate carry bit into the current 

byte
		incf	BIT_NUM, F						; (13) increment current bit number
		btfsc	BIT_NUM, 3						; (14) have we set 8 bits yet?
		incf	FSR, F							; (15) if yes then advance the buffer ptr
		btfsc	BIT_NUM, 3						; (16) have we set 8 bits yet?
		clrf	BIT_NUM							; (17) if yes then reset the current bit number to 0
		nop										; (18)
		nop										; (19)
		btfss	REC_PORT, REC_BIT				; (20) has the data line returned to 0?
		goto	start_bit						; (21) if yes then go read another bit
		btfss	REC_PORT, REC_BIT				; (22) that check is too early, this one is more likely...
		goto	start_bit						; (23) 
		btfss	REC_PORT, REC_BIT				; (22) and sometimes it can appear here...
		goto	start_bit						; (23) 
		btfss	REC_PORT, REC_BIT				; (24) last chance...
		goto	start_bit						; (25) 
		decf	BITS_READ, F					; (34) ok, we've timed out. knock off the last bit
		goto	finished_packet					; (35) (which should be a 1) and we're done.
start_bit
		movfw	BITS_READ						; (3) safety check
		addlw	-22h							; (4)	have we read 34 bits?
		btfss	STATUS, Z						; (5)	if yes then don't read any more
		goto	read_bit						; (6) go read in the next bit

finished_packet
		call	check_4003XXh					; request button states?
		iorlw	0
		btfss	STATUS, Z
		goto	end_interrupt

		call	check_00h						; request id/status?
		iorlw	0
		btfss	STATUS, Z
		goto	end_interrupt

		call	check_41h						; request origins?
		iorlw	0
		btfss	STATUS, Z
		goto	end_interrupt
		
end_interrupt
		bsf		RECEIVED_PACKET, 0				; signal that we've just received a console packet
		clrf	BITS_READ						; we dont' have time to do this in the interrupt initialization
		swapf	STATUS_TEMP, W					; restore STATUS
		movwf	STATUS
		swapf	W_TEMP, F						; restore W
		swapf	W_TEMP, W
		bcf		INTCON, INTF					; clear the pin interrupt flag
		retfie									; return from interrupt

; ================================================================================================
; check_4003XXh -	checks to see if we've received a "request button states" command,
;					if we have then the button buffer is sent to the console.
; ================================================================================================

check_4003XXh
		movfw	BITS_READ						; must be a 24-bit packet
		sublw	18h
		btfss	STATUS, Z
		retlw	0
		movfw	PACKET_BUFFER
		sublw	40h
		btfss	STATUS, Z
		retlw	0
		movfw	PACKET_BUFFER+1					; second byte must be 03h
		sublw	03h
		btfss	STATUS, Z
		retlw	0

		movlw	CONSOLE_BUTTONS					; send the button data packet to the console
		movwf	FSR
		movlw	41h								; = 65 bits
		call	send_packet
		incf	TIMEOUT_COUNTER					; increment the time-out counter
		retlw	1

; ================================================================================================
; check_00h -	checks to see if we've received a "request id/status" command,
;				if we have then send the standard controller id word and status byte are sent
;				to the console
; ================================================================================================

check_00h
		movfw	BITS_READ						; must be an 8-bit packet
		sublw	8h
		btfss	STATUS, Z
		retlw	0
		movfw	PACKET_BUFFER					; first byte must be 00h
		sublw	00h
		btfss	STATUS, Z
		retlw	0
		
		START_PACKET		PACKET_BUFFER		; send id and status
		PACK_CONST	09h
		PACK_CONST	00h
		PACK_CONST	00h
		END_PACKET			PACKET_BUFFER, 3
		call send_packet

		START_PACKET		CONSOLE_BUTTONS		; initialize the buttons packet
		PACK_CONST	00h							; 0     0     0     S     Y     X       B       A
		PACK_CONST	80h							; 1     L     R     Z    D-UP D-DOWN D-RIGHT  D-LEFT
		PACK_CONST	80h							; Joystick X
		PACK_CONST	80h							; Joystick Y
		PACK_CONST	80h							; C-Stick X
		PACK_CONST	80h							; C-Stick Y
		PACK_CONST	00h							; Left shoulder toggle
		PACK_CONST	00h							; Right shoulder toggle
		END_PACKET			CONSOLE_BUTTONS, 8

		retlw	1

; ================================================================================================
; check_41h -	checks to see if we've received a "request origins" command,
;				calibration is easy on this controller...everything's digital.
; ================================================================================================

check_41h
		movfw	BITS_READ								; must be an 8-bit packet
		sublw	8h
		btfss	STATUS, Z
		retlw	0
		movfw	PACKET_BUFFER							; first byte must be 41h
		sublw	41h
		btfss	STATUS, Z
		retlw	0
		
		START_PACKET	PACKET_BUFFER					; send origins/calibration packet
		PACK_CONST		00h
		PACK_CONST		80h
		PACK_CONST		80h
		PACK_CONST		80h
		PACK_CONST		80h
		PACK_CONST		80h
		PACK_CONST		00h
		PACK_CONST		00h
		PACK_CONST		02h
		PACK_CONST		02h
		END_PACKET		PACKET_BUFFER, 0Ah				; 10 bytes in this packet
		call	send_packet
		retlw	1
		

; ================================================================================================
; initialization -	sets up the ports, synchs with the console, enables interrupts and
;					falls through to the wireless_disconnected routine
; ================================================================================================
initialization
		bsf		STATUS, RP0								; select bank 1
		bsf		WIRELESS_PORT, WIRELESS_BIT				; input from the wireless module
		bsf		REC_PORT, REC_BIT						; read data line from the console
		bcf		SEND_PORT, SEND_BIT						; open collector output to the console
		bcf		GREEN_LED_PORT, GREEN_LED_BIT			; LED pin 1
		bcf		RED_LED_PORT, RED_LED_BIT				; LED pin 2
		bcf		TEST_PORT, TEST_BIT						; test point pin
		errorlevel -302
		bcf		OPTION_REG, INTEDG						; select interrupt on falling edge for PORTB, bit0
		errorlevel +302
		bcf		STATUS, RP0								; select bank 0

		TEST_SIGNAL_OFF									; test signal off by default
		clrf	BITS_READ								; we dont' have time to do this in the interrupt initialization
		bsf		SEND_PORT, SEND_BIT						; maintain high output by default
		bcf		INTCON, INTF							; make sure the bit0 interrupt bit is clear
		bsf		INTCON, INTE							; enable external interrupt
		
; ================================================================================================
; wireless_disconnected -	this function is called by the main loop whenever it detects a
;							lost wireless signal. the function disables interrupts (thus breaking
;							off communication with the console) and then sits in a tight loop
;							until it reads several valid packets in a row. a packet is considered
;							valid when we detect the start bit followed by a little less than
;							3.5ms of 0s after the packet.
; ================================================================================================

wireless_disconnected
		bcf		INTCON, GIE								; disable global interrupts
		LED_RED											; show we're in a disconnected state
disconnected_loop
		btfss	WIRELESS_PORT, WIRELESS_BIT				; wait for the start of a wireless pulse
		call	wireless_half_pulse_delay				; advance to the center of the pulse
		movlw	VALID_SIGNAL_COUNT						; we want several good wireless packets in a row
		movwf	PACKET_COUNTER							; 	before we assume we're reconnected
packet_loop
		btfss	WIRELESS_PORT, WIRELESS_BIT				; picking up a 1 on the wireless pin?
		goto	disconnected_loop						; no, so start over
		call	wireless_packet_delay					; yep, could be a start bit. skip over the packet.
		movlw	03Bh									; outer loop
		movwf	TIMER1
		movlw	03Ah
		movwf	TIMER2									; inner loop
		btfsc	WIRELESS_PORT, WIRELESS_BIT				; picking up a 0 on the wireless pin?
		goto	disconnected_loop						; no, so start all over again
		decfsz	TIMER2, F								; dec inner loop
		goto	$-3
		decfsz	TIMER1, F
		goto	$-7										; dec outer loop
		movlw	31h										; burn off a few more cycles to make it 3500 exactly
		movwf	TIMER1
		decfsz	TIMER1, F
		goto	$-1
		decfsz	PACKET_COUNTER, F						; we received a clean packet, decrement the counter
		goto	packet_loop								; if we haven't got them all then loop back for more
		clrf	TIMEOUT_COUNTER							; reset the timeout counter
		bsf		INTCON, GIE								; reenable interrupts		
		LED_GREEN										; show we're picking up a valid wireless signal again

; ================================================================================================
; main_loop -	sits in a tight loop polling the wireless module and updating the buttons array.
;				the main loop often gets interrupted by the console when it requests button data
;				be sent to it. these interrupts are long enough to push the detection of the stop
;				bit past the end of the packet so that a 0 is read instead of a 1. when this
;				happens the entire packet is ignored, and a fresh attempt is made to read in the
;				next wireless packet. control is passed to the disconnected handler when
;				too many bad packets in a row are received.
; ================================================================================================

main_loop
		movfw	TIMEOUT_COUNTER							; get the packet counter
		sublw	0Ah										; have we had at least 10 interrupts since
		btfss	STATUS, C								;	the last wireless packet?
		goto	wireless_disconnected					; yes, so we've lost the wireless signal
		clrf	WIRELESS_BUTTONS						; initialize the bytes we'll used to
		clrf	WIRELESS_BUTTONS+1						; 	double-buffer the button data
		call	wait_for_wireless_packet				; wait for the next start bit to arrive
		sublw	0										; if we timed out then transfer control
		btfsc	STATUS, Z								; 	to the disconnected handler
		goto	wireless_disconnected
		call	wireless_half_pulse_delay				; advance half-way into the pulse
		
		; read each button pulse and store the result in the WIRELESS_BUTTONS array. each iteration 
		; of the READ_WIRELESS_BUTTON macro lasts exactly 625uS, i.e. the length of a wireless bit
		; pulse.
		TEST_SIGNAL_ON
		READ_WIRELESS_BUTTON	WIRELESS_BUTTONS+1, 7, FALSE	; start bit
		READ_WIRELESS_BUTTON	WIRELESS_BUTTONS,   1, FALSE	; B
		READ_WIRELESS_BUTTON	WIRELESS_BUTTONS,   3, TRUE		; Y	
		READ_WIRELESS_BUTTON	WIRELESS_BUTTONS+1, 4, FALSE	; SELECT (Z)
		READ_WIRELESS_BUTTON	WIRELESS_BUTTONS,   4, TRUE		; START
		READ_WIRELESS_BUTTON	WIRELESS_BUTTONS+1, 3, FALSE	; D-UP
		READ_WIRELESS_BUTTON	WIRELESS_BUTTONS+1, 2, TRUE		; D-DOWN
		READ_WIRELESS_BUTTON	WIRELESS_BUTTONS+1, 0, FALSE	; D-LEFT
		READ_WIRELESS_BUTTON	WIRELESS_BUTTONS+1, 1, TRUE		; D-RIGHT
		READ_WIRELESS_BUTTON	WIRELESS_BUTTONS,   0, FALSE	; A
		READ_WIRELESS_BUTTON	WIRELESS_BUTTONS,   2, TRUE		; X
		READ_WIRELESS_BUTTON	WIRELESS_BUTTONS+1, 6, FALSE	; L
		READ_WIRELESS_BUTTON	WIRELESS_BUTTONS+1, 5, TRUE		; R
		TEST_SIGNAL_OFF

		; at this point we should be at the stop bit, unless we've lost the signal or an interrupt
		; has occurred mid-packet. in either case this wireless packet is invalid and so we ignore it.
		; the TIMEOUT_COUNTER will eventually overflow and control will be passed to the disconnected
		; handler.
		btfss	WIRELESS_PORT, WIRELESS_BIT				; we should be in the middle of the stop bit, is it a 1?
		goto	main_loop								; nope, so ignore this packet
		call	wireless_pulse_delay					; skip over the stop bit

		; if we made it this far then we managed to receive a complete wireless packet,
		; including start and stop bits. it's unlikely we've lost the wireless connection,
		; so copy the button data into the CONSOLE_BUTTONS packet so that it gets sent
		; to the console.
save_buttons
		movfw	WIRELESS_BUTTONS
		movwf	CONSOLE_BUTTONS
		movfw	WIRELESS_BUTTONS+1
		movwf	CONSOLE_BUTTONS+1
		clrf	TIMEOUT_COUNTER							; reset the packet counter
		goto	main_loop

; ================================================================================================
; send_packet -	sends a stream of bits to the console. 0 is sent as a 1uS low pulse followed by
;				a 3uS high pulse. 1 is sent as a 3uS low pulse followed by a 1uS high pulse.
;				this function should be called with the number of bits to send loaded into W
;				and the address of the send buffer loaded into FSR. execution time is exactly
;				numbits*20+8 instructions, including the call to and return from function.
; ================================================================================================

send_packet
		movwf	BITS_TO_SEND		; store the number of bytes to send
		clrf	BIT_NUM				; haven't sent any bytes yet
		movfw	INDF				; make a copy of the first byte to send so we don't trash it
		movwf	CURRENT_BYTE
		incf	FSR, F				; and advance the ptr
send_bit
		SEND_0						; 0uS
		nop
		incf	BIT_NUM, F			; add one to the bytes sent counter
		rlf		CURRENT_BYTE, F
		btfsc	STATUS, C
		SEND_1						; 1uS, send the MSB of CURRENT_BYTE to the console
		btfsc	BIT_NUM, 3			; have we sent 8 bits yet?
		goto	advance_byte
		nop
		nop
		nop							; 2uS
		nop
		nop
		nop
		nop
end_bit
		SEND_1						; 3uS, either way, the pulse should be 1 at this stage
bit_sent
		decf	BITS_TO_SEND, F		; one bit less to send
		btfss	STATUS, Z			; finished yet?
		goto	send_bit			; nah
		return						; yup
advance_byte
		movfw	INDF				; make a copy of the first byte to send so we don't trash it
		movwf	CURRENT_BYTE
		incf	FSR, F				; and advance the ptr
		clrf	BIT_NUM				; haven't sent any bytes yet
		goto	end_bit

; ================================================================================================
; timing routines
; ================================================================================================

		; spends roughly 7ms polling the wireless bit to see if a 1 has arrived.
		; returns 1 in W when a pulse is detected, otherwise returns 0 to
		; signify a time-out (i.e. wireless disconnected state)
wait_for_wireless_packet
		movlw	053h
		movwf	TIMER1
		movlw	053h
		movwf	TIMER2
		btfsc	WIRELESS_PORT, WIRELESS_BIT		; wait for the start of a wireless pulse
		retlw	1
		decfsz	TIMER2, F
		goto	$-3
		decfsz	TIMER1, F
		goto	$-7
		retlw	0

		; delays the length of a wireless packet, i.e. exactly 3.5ms
		; including the call-to and return-from. at 20MHz
		; 3.5mS == 17500 cycles
wireless_packet_delay
		movlw	04Bh
		movwf	TIMER1
		movlw	04Bh
		movwf	TIMER2
		decfsz	TIMER2, F
		goto	$-1
		decfsz	TIMER1, F
		goto	$-5
		movlw	6Ah
		movwf	TIMER1
		decfsz	TIMER1, F
		goto	$-1
		nop
		return

		; waits the length of each RF receiver pulse minus 3, i.e. exactly 1247 cycles
		; including the call-to and return-from. the 3 is taken off so that the
		; calling prodecure has a few cycles spare to grab the button state
wireless_pulse_delay
		movlw	0FFh
		movwf	TIMER1
		decfsz	TIMER1, F
		goto	$-1
		movlw	09Eh
		movwf	TIMER1
		decfsz	TIMER1, F
		goto	$-1
		nop
		nop
		return

		; waits the length of half an RF pulse, i.e. exactly 625 cycles,
		; including the call-to and return-from. this is called so that
		; readings are taken in the center of the wireless pulses so
		; that they can accommadate some drift.
wireless_half_pulse_delay
		movlw	0CEh
		movwf	TIMER1
		decfsz	TIMER1, F
		goto	$-1
		nop
		nop
		return

		end
