; PIC 16F688 code for running an I2C slave
;
; ### license
;
; Conforms to "Rev. 6" of the I2C specification (UM10204).
;
; 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
; NODE_ADDRESS: the EEPROM address containing this node's I2C address.
;               defaults to 0x00.
;

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

        constant PORTS_REQUIRED = 1
        include ../include/i2c.inc

; ### Interrupts appear too slow to respond to the START condition.
; ### Use BUSY_WAIT for now.
 constant BUSY_WAIT = 1

; We pre-program this node's address into the EEPROM
 ifndef NODE_ADDRESS
   constant NODE_ADDRESS = 0x00
 endif

; ----------------------------------

        ; Define this module's variables
.gpr0   udata

; What address should this node use? (read from EEPROM)
; ### should we also have a compile-time constant?
ADDRESS res     1

; ### docco
DATA_BYTE_COUNT res     1

; We'll assume that no operation requires more than 3 bytes of operands
 ifndef MAX_OPERANDS
   constant MAX_OPERANDS = 3
 endif
OPERANDS        res     MAX_OPERANDS

; ### dunno which/both we will need
LAST_OPERAND    equ     OPERANDS + MAX_OPERANDS - 1
END_OPERANDS    equ     OPERANDS + MAX_OPERANDS


slave:  code                    ; relocatable


; main entry point: set up listener
;
; INPUTS: none
; EXPECT: Bank 1
; EXIT:   Bank 0
;
 global I2C_listen
I2C_listen:

        ; copy the node's I2C address from EEPROM into a register
        read_eeprom NODE_ADDRESS
        bank0s
        movwf   ADDRESS
        bank1s

        ; certain pins can be digital *or* analog. when necessary, configure the
        ; pin for *digital* input.
        disable_analog  SCL_PORT, SCL
        disable_analog  SDA_PORT, SDA

        ; set up the selected pins for *input*
        bsf     SCL_TRISTATE, SCL
        bsf     SDA_TRISTATE, SDA

; If we use interrupts, then set them up now. However, interrupts don't appear fast
; enough, so BUSY_WAIT is needed for right now.
 ifndef BUSY_WAIT
        ; Prepare interrupt for SDA
 if SDA_PORT == PORTC
        ; The only PORTC pin allowed is RC5/RX. We trigger this via the UART.
        bsf     PIE1, RCIE
        bank0s
        bsf     RCSTA, SPEN
        bsf     BAUDCTL, WUE
        bsf     INTCON, PEIE
 else
        bsf     IOCA, SDA
        bank0s
        bsf     INTCON, RAIE
 endif
        bsf     INTCON, GIE

 else ; BUSY_WAIT

        ; End this section of code in Bank 0
        bank0s

 endif ; BUSY_WAIT

        ; exit in Bank 0
        return


; End an SDA-transition interrupt. Clear any flags, reset things for another
; interrupt (from sleep), and then RETURN FROM the interrupt.
;
; EXPECT: Bank 0
; EXIT:   -- does not return
;
; ### it appears interrupts don't respond fast enough to an SDA transition.
; ### switching to a busy-wait means this routine is not needed
 ifndef BUSY_WAIT

 global I2C_end_interrupt
I2C_end_interrupt:
 if SDA_PORT == PORTC
        ; The only PORTC pin allowed is RC5/RX, triggering an RCIF interrupt.
        bcf     PIR1, RCIF

        ; ### this code is not properly tested due to interrupt timing issues
        ; Re-enable the EUSART, in order to watch RC5/RX
        bsf     RCSTA, SPEN
        bank1s
        bsf     SCL_TRISTATE, SCL
        bsf     SDA_TRISTATE, SDA
        bank0s

        ; WUE gets cleared after each wakeup, when SDA transitions back to HIGH.
        ; We need to ensure it is *always* set when interrupts are enabled. If
        ; any point exists where it is NOT set, then we SLEEP ... we may never
        ; wake back up.
        bsf     BAUDCTL, WUE
 else
        ; Read PORTA to latch a new value, ending the mismatch which caused RAIF.
        movfw   PORTA

        ; Any PORTA change triggers an RAIF interrupt.
        bcf     INTCON, RAIF
 endif

        ; We're ready for the next SDA transition. Exit the interrupt handler.
        IH_epilogue
        ; NOTREACHED

 endif ; !BUSY_WAIT


; SDA has transitioned [to LOW]. Look for START condition.
;
; EXPECT: Bank 0
; EXIT:   Bank 0
;
; RETURN VALUES:
;
;   Z is SET: the I2C bus is now IDLE
;   Z is CLEAR: self has been addressed
;
;   C is SET: MASTER was to READ-FROM this node
;   C is CLEAR: MASTER wants to WRITE-TO this node
;
;   W is unknown (technically, it is 0, but not part of this contract)
;
;   SCL is LOW, just after our ACK was delivered
;   SDA is unknown
;
 global I2C_SDA_transition
I2C_SDA_transition:
 ifndef BUSY_WAIT
        ; ### unclear whether this code works. interrupts appear too slow for START.
        ; Disable the EUSART. Ensure SCL is tristated as an input.
        bank1s
        bsf     SCL_TRISTATE, SCL
        bsf     SDA_TRISTATE, SDA
        bank0s
        bcf     RCSTA, SPEN
 endif

        ; If SCL is HIGH, then we have a START.
        btfss   SCL_PORT, SCL

        ; SCL is LOW. This is the Master preparing SDA before it
        ; clocks SCL HIGH. But this is being done for somebody else.
        ;
        ; Note: This should never come out of the blue. The first SDA transition
        ;       (a START) moves us to this module for processing. Any further
        ;       transitions should be managed within the code below.
        ;
        ;       That said, checking SCL is a reasonable failsafe. If it is LOW,
        ;       then this is a simple SDA setup for the next bit. We can go
        ;       ahead and start ignoring this transfer (as we should have).
        goto    skip_transfer

        ; Resumption point, when we see a REPEATED START condition. Start pulling
        ; bits off the wire to look for our address.
repeated_start:

        ; SCL is HIGH. SDA is LOW. (REPEATED) START has been seen. Initialize
        ; for reception.

        ; ### set a timeout, to reset if we don't see a proper protocol
        ; ### use the watchdog timer (via SWDTEN). resets the chip. this is the
        ; ### only/simplest method given that (timer) interrupts are disabled
        ; ### while we busy-wait through this communication.

        ; Read the 7-bit address from the I2C bus.
        clrw
        read_bit        0x40
        read_bit        0x20
        read_bit        0x10
        read_bit        0x08
        read_bit        0x04
        read_bit        0x02
        read_bit        0x01

        ; Does the delivered address match ours?
        subwf           ADDRESS, W
        skpz
        ; Not directed at us. Skip this transfer. Wait for the next one.
        goto            skip_transfer

        ; register W == 0

        ; Read R/W bit into the CARRY flag.
        wait_SCL_LOW
        wait_SCL_HIGH
        clrc                    ; WRITE-TO(0)
        btfsc           SDA_PORT, SDA
        setc                    ; READ-FROM(1)

        ; The address matches. We have captured the R/W value. Tell the Master,
        ; reception is successful.
        ; NOTE: the CARRY flag remains unchanged by the send_ACK macro.
        send_ACK

        ; Inform the caller we have been addressed (by clearing the Z flag).
        ; CARRY holds READ-FROM(1)/WRITE-TO(0).
        clrz
        return

; Skip/ignore the rest of this transfer.
skip_transfer:
        ; Now: SCL is HIGH. SDA is NOT-CARE.
        ; We just read ADDR<0> and determined we are not being addressed.
        ; Wait for this bit to finish...
        wait_SCL_LOW

        ; Any amount of addresses, ACKS, data, whatever will pass over the bus.
        ; The START/STOP condition only occurs when SCL is HIGH. Look/wait for that.

resume_skip:
        ; SCL is LOW. SDA is NOT-CARE.
        ; Wait for SCL to hit a HIGH condition.
        wait_SCL_HIGH

        ; SCL is HIGH.
        ; If SDA is HIGH, then we should watch for REPEATED START.
        ; If SDA is LOW, then we should watch for STOP.

        btfsc   SDA_PORT, SDA
        ; SDA is HIGH
        goto    maybe_repeated_start

look_for_STOP:
        ; SCL is HIGH. SDA is LOW. Watch for one of two conditions:
        ; If SCL transitions LOW, then we saw a typical bit transmission.
        ; If SDA transitions HIGH, then we saw a STOP condition
        btfss   SCL_PORT, SCL
        ; SCL just transitioned LOW. Random bit. Continue skipping transitions.
        goto    resume_skip

        ; SCL remains HIGH. Has SDA changed?
        btfss   SDA_PORT, SDA
        ; SDA remains LOW. Keep looking for transitions.
        goto    look_for_STOP

        ; SCL is HIGH. SDA just transitioned HIGH, indicating STOP.
        ; The bus is now IDLE. Notify caller by SETTING the Z flag.
        setz
        return

maybe_repeated_start:
        ; SCL is HIGH. SDA is HIGH. Wait for one of them to transition:
        ; If SCL transitions LOW, then we saw a typical bit transmission.
        ; If SDA transitions LOW, then we saw a REPEATED START condition.
        btfss   SCL_PORT, SCL
        ; SCL just transitioned LOW. Random bit. Continue skipping transitions.
        goto    resume_skip

        ; SCL is still HIGH. Has SDA changed?
        btfss   SDA_PORT, SDA
        ; SDA just transitioned LOW. This is a REPEATED START.
        goto    repeated_start

        ; SCL remains HIGH. SDA remains HIGH. Keep waiting...
        goto    maybe_repeated_start


; Read one byte (the OPCODE) from the I2C bus.
;
; EXPECT: Bank 0
; EXIT:   Bank 0
;
 global I2C_receive_opcode
I2C_receive_opcode:
        ; Grab the byte from the bus.
        read_byte

        ; Got the opcode. ACK the thing.
        send_ACK

        ; OPCODE is returned in W.
        return


; Read any/all operands from the I2C bus, immediately after the ACK of the OPCODE.
; SCL is (presumably) LOW upon entry.
;
; Upon return:
;   W == 1: REPEATED START was seen, and the bus remains BUSY
;   W == 0: STOP was seen, and the bus is now IDLE
;
;   NOTE: the Z flag is UNKNOWN upon exit.
;
;   FSR will point at one PAST the last-read operand. If it equals OPERANDS, then
;   no operands were present.
;
; EXPECT: Bank 0
; EXIT:   Bank 0
;
 global I2C_receive_operands
I2C_receive_operands:
        ; Prepare the indirect register for storing the operand bytes.
        movlw           OPERANDS
        movwf           FSR

read_loop:
        ; Note: no clock stretching is necessary here.

        ; SCL is LOW upon entry (or it has *very* recently floated HIGH). We wait
        ; for SCL HIGH, then determine which of three conditions is signalled:
        ;   1) Bit 0x80 of the next operand [SCL will transition LOW]
        ;   2) REPEATED START condition [SDA will transition LOW]
        ;   3) STOP condition [SDA will transition HIGH]
        ;
        ; Grab the SDA value as if we are option (1), then watch for transitions.
        clrw
        wait_SCL_HIGH
        setbit_SDA      0x80

        ; SCL is HIGH. SDA is NOT-CARE.
        ; Enter one of two loops, based on the current SDA state.
        btfss   SDA_PORT, SDA
        ; SDA is LOW. Go look for OPERAND or STOP.
        goto    await_operand_or_STOP

await_operand_or_START:
        ; SCL is HIGH. SDA is HIGH.
        btfss   SCL_PORT, SCL
        ; SCL just transitioned LOW. Option (1). Read rest of the operand byte.
        goto    continue_operand

        ; SCL is still HIGH. See if SDA transitioned LOW (a REPEATED START).
        btfsc   SDA_PORT, SDA
        ; SDA is still HIGH. Keep looking for a transition.
        goto await_operand_or_START

        ; We just saw a REPEATED START.
        retlw   1

await_operand_or_STOP:
        ; SCL is HIGH. SDA is LOW.
        btfss   SCL_PORT, SCL
        ; SCL just transitioned LOW. Option (1). Read rest of the operand byte.
        goto    continue_operand

        ; SCL is still HIGH. See if SDA transitioned HIGH (a STOP condition).
        btfss   SDA_PORT, SDA
        ; SDA is still LOW. Keep looking for a transition.
        goto await_operand_or_STOP

        ; We just saw a STOP
        retlw   0

continue_operand:
        ; We've read the MSB. SCL is LOW. SDA is NOT-CARE.

        ; Wait for SCL to return HIGH, then copy SDA into register W.
        wait_SCL_HIGH
        setbit_SDA      0x40

        ; Read the next (6) bits from the I2C bus.
        read_bit        0x20
        read_bit        0x10
        read_bit        0x08
        read_bit        0x04
        read_bit        0x02
        read_bit        0x01

        movwf           INDF    ; Store indirect via FSR
        incf            FSR, F

        send_ACK
        ; SCL is now LOW.

        ; Go read more operands, or a START/STOP.
        goto            read_loop


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


; Clock stretching is available only if SCL is NOT mapped onto RA3.
 if SCL_PORT != PORTA || SCL != 3

; Turn on the clock stretching (switch SCL to an output, and hold it LOW)
; EXPECT: Bank 0.
 global I2C_begin_clock_stretch
I2C_begin_clock_stretch:
        ; Write LOW to the output latch, then switch it to an output.
        bcf     SCL_PORT, SCL
        bank1s
        bcf     SCL_TRISTATE, SCL
        bank0s
        return

; End the clock stretching (return SCL to an input)
; EXPECT: Bank 0.
 global I2C_end_clock_stretch
I2C_end_clock_stretch:
        bank1s
        bsf     SCL_TRISTATE, SCL
        bank0s
        return

 endif                          ; Test for RA3


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