; PIC 16F688 code for running an I2C slave
;
; ### license
;
; Conforms to Version 2.1 of the I2C specification
;
; The external build script should set the following:
;
; SCL: the PortA pin number for the SCL bus
; SDA: the PortA pin number for the SDA bus
; SLAVE_VAR_BASE: the address for the slave's working state
;                 this must be in Bank 0.
; DISABLE_COMPARATOR: define to include code to ensure Comparator 1
;                     is disabled when its corresponding pins are used
; NODE_ADDRESS: the EEPROM address containing this node's I2C address.
;               defaults to 0x00.
;

        list p = 16f688
        include p16f688.inc
        include ../include/handy.inc

slave:  code                    ; relocatable


; define the pins. these can/should be set via compilation flags
;
; NOTE: use of PortC is not allowed -- interrupts are not available
; NOTE: PortA3 *may* be used as SCL, but *never* as SDA. in certain
;         applications, it cannot be used as SCL (eg. to force the
;         master to wait before sending more data).
 ifndef SCL
   constant SCL = 0             ; pin 0
 endif
 ifndef SDA
   constant SDA = 1             ; pin 1
 endif


; construct some derived values from the SCL/SDA pins
;
; RA{0:2} map to AN{0:2}. RA{3,5} have no ADC. RA4 is AN3. if ANSEL
; management is not required, we leave $X_ANSEL undefined.
 if SCL == 4
   constant SCL_ANSEL = 3
 else
   if SCL < 3
     constant SCL_ANSEL = SCL
   endif
 endif
 if SDA == 4
   constant SDA_ANSEL = 3
 else
   if SDA < 3
     constant SDA_ANSEL = SDA
   endif
 endif

 ifndef NODE_ADDRESS
   constant NODE_ADDRESS = 0x00
 endif


; figure out where we're going to place our variables
; ### assumed to be in bank0 !!
 ifndef SLAVE_VAR_BASE
   constant SLAVE_VAR_BASE = 0x20
 endif

; The slave uses a state machine, moving states based on interrupts.
STATE   equ     SLAVE_VAR_BASE

; the various states
#define S_IDLE          state_idle
#define S_WAIT_ADDR     state_wait_addr
#define S_WAIT_RW       state_wait_rw
#define S_ACK_WAIT      state_ack_wait
#define S_ACK_RUNNING   state_ack_running
#define S_ACK_DONE      state_ack_done
#define S_WAIT_COMMAND  state_wait_command
#define S_WAIT_DATA     state_wait_data

; the value (address or data) we are acculumating off the wire
ACCUM   equ     SLAVE_VAR_BASE + 0x01

; what address should this node use?
; ### should we also have a compile-time constant?
ADDRESS equ     SLAVE_VAR_BASE + 0x02

; Reading, or writing?
RW_MODE equ     SLAVE_VAR_BASE + 0x03

; We need to save the (potential) DATA<7> value.
DATA7   equ     SLAVE_VAR_BASE + 0x04

; How many bits are left to acquire from the bus?
BCOUNT  equ     SLAVE_VAR_BASE + 0x05


; read the SDA port, placing 0 or 1 into W.
read_SDA: macro
        ; assume Bank 0
        movlw   1
        btfss   PORTA, SDA
        clrw
        endm

; copy the SDA value to a register. REG is {0,1}
; ### we'll likely move to bit storage soon
mov_SDA: macro REG
        ; assume Bank 0
        clrf    REG
        btfsc   PORTA, SDA
        bsf     REG, 0
        endm


return_if_LOW: macro WHICH
        ; assume Bank 0
        btfss   PORTA, WHICH
        return
        endm
return_if_HIGH: macro WHICH
        ; assume Bank 0
        btfsc   PORTA, WHICH
        return
        endm

; Rotate the SDA value into ACCUM's LSB via the Carry bit.
accum_SDA: macro
        ; assume Bank 0
        clrc
        btfsc   PORTA, SDA
        setc
        rlf     ACCUM, F
        endm

; Move to a new state, defined by magical PC offsets.
set_state: macro WHICH
        ; assume Bank 0
        movlw   WHICH - STATE_BASE
        movwf   STATE
        endm

; Jump to WHERE if REG1 == REG2.
jumpeq: macro REG1, REG2, WHERE
        movfw   REG1
        xorwf   REG2, W         ; could use SUBWF
        btfsc   STATUS, Z
        goto    WHERE
        endm


; main entry point: set up listener
;
; INPUTS: none
;
listen:

; The comparator is normally disabled. Allow applications to ensure
; Comparator 1 is disabled, if the corresponding pin(s) will be used
; for the I2C protocol.
  ifdef DISABLE_COMPARATOR
        ; maybe disable the PortA comparator (if we're using those pins)
        ;  map (Fig 7-3): existing CMCON0 -> target CMCON0
        ;    000 -> 111
        ;    001 -> 101 (breaks caller)
        ;    010 -> 101 (breaks caller)
        ;    011 -> 101 (may break caller)
        ;    100 -> 101 (may break caller)
        ;    101 -> 101
        ;    110 -> 101 (?? confusing C2OUT/RC4 schematic)
        ;    111 -> 111
        ; (101 leaves Comparator 2 on; 111 turns both off)
    if SCL == 0 || SCL == 1 || SDA == 0 || SDA == 1

	; CMCON0 is in Bank 0
        bank0

        movfw   CMCON0
        andlw   0x07            ; mask away the un-interesting bits
        bz      set111          ; it was 000. transition to 111.
        xorlw   0x07
        bz      done            ; it was 111. leave alone.
        movfw   CMCON0
        iorlw   0x05            ; enable bits 0,2
        andlw   0xFD            ; disable bit 1
        movwf   CMCON0          ; atomic change of config
        goto    done

set111:
        movlw   0x07
        iorwf   CMCON0, F       ; 000 -> 111
        ; FALLTHRU

done:
        ; FALLTHRU
    endif ; SCL/SDA tests
  endif ; DISABLE_COMPARATOR

        ; move to the Bank 1 registers
        bank1

        ; copy the node's I2C address from EEPROM into a register
        movlw   NODE_ADDRESS
        movwf   EEADR
        bcf     EECON1, EEPGD   ; select EEPROM
        bsf     EECON1, RD      ; perform the read
        movfw   EEDAT
        movwf   ADDRESS

        ; set up the PortA pins for slave-receive (digital input)
        bsf     TRISA, SCL
        bsf     TRISA, SDA
  ifdef SCL_ANSEL
        bcf     ANSEL, SCL_ANSEL
  endif
  ifdef SDA_ANSEL
        bcf     ANSEL, SDA_ANSEL
  endif

move_to_idle:
        ; NOTE: assuming Bank 1.

        ; enable interrupt on SDA, waiting for START
        bsf     IOCA, SDA

move_to_idle2:
        ; NOTE: assuming Bank 1.

        ; ensure SCL interrupt is disabled. we don't need to watch
        ; that until START has been observed.
        bcf     IOCA, SCL

        bank0s
        set_state S_IDLE

        ; exiting in Bank 0.
        return


interrupt_dispatch:
        bank0

        ; NOTE: we assume this will not cross bank boundaries.
        movfw   STATE
        addwf   PCL, F          ; yay! variant GOTO!

; The above ADDWF adjusts the PC from this location:
STATE_BASE:
; The label has no associated code. kthxbai.


; only SDA interrupts are allowed
state_idle:
        ; NOTE: assuming Bank 0.

        ; SDA will wiggle around while SCL is LOW, as part of normal
        ; communication between other nodes. Just ignore it.
        return_if_LOW   SCL

        ; we just saw a START condition
        set_state S_WAIT_ADDR

        ; Accumulate 7 address bits.
        clrf    ACCUM
        movlw   0x07
        movwf   BCOUNT

        ; time to start watching SCL, and stop watching SDA.
        bank1s
        bsf     IOCA, SCL
        bcf     IOCA, SDA

        return


state_wait_addr:
        ; NOTE: assuming Bank 0.

        ; return if the bit is not set (LOW). we have nothing to do
        ; on the trailing edge
        return_if_LOW   SCL

        ; leading edge, so SDA is now valid. set the carry, according
        ; to SDA, then rotate it into the accumulator.
        accum_SDA
        ; note: carry is now cleared

        ; ### return to IDLE state if the address is already incorrect??

        decfsz  BCOUNT, F
        goto    got_address

        ; We don't have all the bits, so wait for more...
        return

got_address:
        ; We've got all the address bits. Let's see if we're addressed.
        jumpeq  ADDRESS, ACCUM, move_to_wait_rw

        ; The master is not talking to us. Return to the IDLE state.
        bank1s
        goto    move_to_idle

move_to_wait_rw:
        set_state S_WAIT_RW

        return


state_wait_rw:
        ; NOTE: assuming Bank 0.

        ; ignore trailing edges.
        return_if_LOW   SCL

        ; save the R/W mode
        mov_SDA RW_MODE

        return

move_to_ack_wait:
        ; NOTE: assuming Bank 0.

        set_state S_ACK_WAIT

        ; switch SDA to digital OUT, and pull it LOW.
        bcf     PORTA, SDA
        bank1s
        bcf     TRISA, SDA

        return


state_ack_wait:
        ; NOTE: assuming Bank 0.

        ; SCL just transitioned to LOW, corresponding to the trailing
        ; edge of the read/write bit (or possibly the DATA<0> bit).
        ; We now need to skip the leading edge of the ACK clock pulse,
        ; and wait/watch for its trailing edge.

        set_state S_ACK_RUNNING

        return


state_ack_running:
        ; NOTE: assuming Bank 0.

        ; ignore the leading edge of the ACK clock pulse
        return_if_HIGH  SCL

        ; the ACK has been clocked in.
        set_state S_ACK_DONE

        ; switch SDA back to digital IN.
        bank1s
        bsf     TRISA, SDA

        return


state_ack_done:
        ; Note: assuming Bank 0.

        ; SCL is now HIGH, and SDA is at $whatever level.

        ; We need to save the state of SDA right now. If we wait for
        ; the trailing edge to read it, then we might miss the proper
        ; value if the master adjusts it *very* quickly after pulling
        ; SCL to a LOW state.
        mov_SDA DATA7

        ; We now need to watch SDA for (re)START or STOP condition.
        ; Or we will see another SCL transition, indicating we just
        ; got a DATA bit (saved above).

        set_state S_WAIT_COMMAND

        ; Enable interrupts on SDA
        bank1s
        bsf     IOCA, SDA

        return


state_wait_command:
        ; NOTE: assuming Bank 0.
        ; NOTE: this is the only state where both SCL and SDA interrupts
        ;       are enabled. Thankfully, we can easily discover which
        ;       interrupt triggered (SCL must be HIGH to enter this
        ;       state, so if it is *now* LOW... done).

        ; If SCL just went LOW, then we have DATA. Otherwise, it is a
        ; START or STOP condition.
        btfss   PORTA, SCL
        goto    data_command

        ; If SDA is HIGH, then we have a repeated start, which we'll
        ; handle below. If SDA is LOW, then we have a STOP condition,
        ; and we want to disable the SCL interrupt (but leave SDA
        ; interrupts enabled), and move into the IDLE state.
        btfss   PORTA, SDA
        goto    stop_condition

        ; Repeated Start.
        set_state S_WAIT_ADDR

        return

stop_condition:
        bank1s
        goto    move_to_idle2

; During WAIT_COMMAND, we saw SCL go LOW. we're receiving data.
data_command:
        ; NOTE: assuming Bank 0.

        ; SCL transitioned to LOW. That means we just saw DATA<7>.
        ; It was stashed into DATA7, and can be used as the starting
        ; value for our accumulator.

        movfw   DATA7
        movwf   ACCUM
        movlw   0x07            ; wait for 7 more bits
        movwf   BCOUNT

        set_state S_WAIT_DATA

        ; no need for SDA interrupts any more.
        bank1s
        bcf     IOCA, SDA

        return


state_wait_data:
        ; NOTE: assuming Bank 0.

        ; ignore trailing edge of prior data bit's clock pulse
        return_if_LOW   SCL

        ; capture the data into our accumulator
        accum_SDA

        decfsz  BCOUNT, F
        goto    got_data

        ; We don't have all the bits, so wait for more...
        return

got_data:
        ; Got an entire data byte. Process it.
        ; ### how?
        ; ### may want to hold SCL low while processing. compile flag.

        ; Move to ACK_WAIT, and pull SDA LOW.
        goto    move_to_ack_wait


; ### need routines to write data back to the master. obviously, this
; ### will be based on how we handle the incoming data processing.



; stupid assemblers don't realize EOF means *end*
        end
