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