; 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

        constant USING_WAIT_MACROS = 1
        include ../include/handy.inc

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


; 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

.gpr1
; Place this value into the same bank as TRISA and TRISC to speed up
; testing and setting the tristates.
SENDVAL         res     1


slave:  code                    ; relocatable


; main entry point: set up listener
;
; INPUTS: none
; EXPECT: Bank 1
; EXIT:   Bank 0
;
 global I2C_S_listen
I2C_S_listen:
        ; Certain pins can be digital *or* analog. Configure SDA/SCL,
        ; for *digital* input.
        disable_analog  SCL_PORT, SCL
        disable_analog  SDA_PORT, SDA

        ; Set up the selected pins for *input* (aka HIGH-Z)
        bsf     SCL_TRISTATE, SCL
        bsf     SDA_TRISTATE, SDA

        ; Copy the node's I2C address from EEPROM into W.
        read_eeprom NODE_ADDRESS

        ; End this section of code in Bank 0
        bank0s

        ; Now we can stash away the address we read out of EEPROM.
        movwf   ADDRESS

        ; exit in Bank 0
        return


; SDA transitioned to LOW. Look for START condition, and perform initial
; processing to determine the operation target.
;
; EXPECT: Bank 0
; EXIT:   Bank 0
;
; RETURN VALUES:
;
;   Z is SET: the I2C bus is now IDLE (any/all transactions have been skipped)
;   Z is CLEAR: self has been addressed; further calls necessary
;               NOTE: it is possible that several transactions have been
;                     skipped, and we finally reached one addressing self.
;
;   When Z is CLEAR:
;     C is SET: MASTER wants 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)
;
;   ### below is unclear, given various conditions above
;   SCL is LOW, just after our ACK was delivered
;   SDA is unknown
;
 global I2C_S_detect_START
I2C_S_detect_START:

        ; 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 (for some other node).
        ;
        ; Note: The first SDA transition (a START) would jump to here,
        ;       and appropriate handling/skip should already be
        ;       in-process. However, it is *possible* that we missed
        ;       the SCL transition to LOW because the app was busy
        ;       doing something else. Best we can do is wait for the
        ;       busy to return to the IDLE state.
        ;
        ;       If this is going to be "typical" for the app, then the
        ;       Master device should use the "slow start" described in
        ;       Section 3.2.12 ("START byte") of the I2C Specification.
        ;       We'll see the REPEATED START, then examine the address
        ;       to see if it matches self.
        ;
        ;       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_until_idle

        ; FALLTHRU

; When caller has identified the START condition, it can call directly
; to this entrypoint.
;
; Entry: SDA is LOW, SCL may be HIGH or (already) LOW.
; Exit: see I2C_S_detect_START
;
 global I2C_S_saw_START
I2C_S_saw_START:
        ; 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.

        ; ### above might not be true. we haven't disabled interrupts.

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

        ; register W == 0. Z is SET.

        ; Read R/W bit into the CARRY flag.
        wait_SCL_LOW
        wait_SCL_HIGH
        ; ### t(HIGH) seems to be 4.0 µs (Table 10)
        ; t(HIGH) minimum (8.0 µs) until SCL transitions LOW. We have that
        ; time period, plus t(VD;ACK) (3.45 µs) to latch SDA for an ACK.
        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
        ; SCL is LOW. SDA is released.

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

skip_until_idle:
        ; Wait for the LSB to finish its transmission.
        wait_SCL_LOW

        call    I2C_skip
        ; STOP/RESTART condition is in Z. SCL is HIGH.

        ; If Z is CLEAR (REPEATED-START was seen), then read an address
        ; to see if the next transmission identifies self.
        bnz     repeated_start

        ; Z is SET (STOP condition was seen). The I2C bus is IDLE.
        return


; ### should this wait? set return params for no byte arrived? (ie. bus
; ### goes idle, or a repeated start)
; ### timeouts on wait?
; Read one byte from the I2C bus, and ACK the reception.
;
; EXPECT: Bank 0
; EXIT:   Bank 0
;
 global I2C_S_read
I2C_S_read:
        ; Grab the byte from the bus.
        read_byte

        ; Got the opcode. ACK the thing.
        send_ACK

        ; OPCODE is returned in W.
        return


; Read one byte, and clock-stretch before sending ACK.
;
; EXPECT: Bank 0
; EXIT:   Bank 0
;
 global I2C_S_read_stretch
I2C_S_read_stretch:
        read_byte

        ; Wait for the Master to bring SCL LOW, then we'll keep it there.
        wait_SCL_LOW
        ; ### we could unroll the assertion macro, as we know the port
        ; ### latch already has SCL as LOW.
        assert_SCL_LOW
        return


; End the clock-stretch, and send an ACK.
;
; EXPECT: Bank 0
; EXIT:   Bank 0
;
 global I2C_S_end_stretch
I2C_S_end_stretch:
        ; NOTE: this function unrolls many macros from i2c.inc and
        ;       send_ACK because of its particular needs for sequencing
        ;       and register banks.

        ; Prepare the ACK by asserting SDA LOW now.
        bcf     SDA_PORT, SDA   ; clear SDA

        ; We were holding SCL LOW, so we merely need to release it, and
        ; wait for the Master to let it go HIGH (where it will read SDA).
        ; Thus, we switch to bank1 and manipulate the output status
        ; of SCL and SDA.
        bank1s

        ; OUTPUT LOW to SDA
        ;
        ; Note: the Master *may* still be driving SDA at this point, but
        ;       it is totally fine if we *also* pull SDA LOW.
        ;
        ; Note: we waited for master to pull SCL LOW before doing the
        ;       stretch. Thus, it should have already tested for arbitration
        ;       failure before now. The Master should no longer care what
        ;       we're doing with SDA.
        bcf     SDA_TRISTATE, SDA

        ; Release SCL to float HIGH
        bsf     SCL_TRISTATE, SCL

        bank0s

        ; Wait for Master to cycle SCL, reading our ACK.
        wait_SCL_HIGH
        wait_SCL_LOW

        ; Return to digital input on SDA. This releases the bus, letting
        ; it float high.
        release_SDA

        return


; Write a single bit to SDA and wait for the Master to cycle SCL.
;
; From the beginning of the macro, SDA reaches its value at:
;   SET: SDA is released on 3rd instruction (1.5 µs)
;   CLEAR: SDA is pulled LOW on 5th instruction (2.5 µs)
;
; The above values are less than t(VD;DAT), assuming the SCL transition to
; LOW occurred just before macro entry. We must *begin* setting the correct
; SDA level by t(VD;DAT). It then has time to transition (t(r) or t(f)),
; then be stable for a minimum of t(SU;DAT) before the Master starts to
; transition SCL to read this value.
;
; t(LOW) is 4.7 µs minimum, when the Master may transition SCL.
; max(t(r), t(f)) is 1.0 µs maximum.
; t(SU;DAT) is 0.25 µs minimum.
; t(VD;DAT) is 3.45 µs (which equals t(LOW) - t(SU;DAT) - max(t(r),t(f)))
;
; EXPECT: Bank 0
; EXIT:   Bank 0
write_one_bit:  macro BIT
        ; Clear SDA in case we need to pull it LOW.
        bcf     SDA_PORT, SDA

        bank1s

        ; Release SDA for now. We may reacquire it.
        bsf     SDA_TRISTATE, SDA

        ; Test the appropriate bit in the value we're sending
        btfss   SENDVAL, BIT

        ; The bit was CLEAR, so acquire SDA to pull it LOW.
        bcf     SDA_TRISTATE, SDA

        ; Bit was HIGH and SDA was already released, or SDA has now been
        ; pulled LOW. Return to Bank 0 to watch SCL.
        bank0s

        ; We need to wait for the Master to read the value.
        wait_SCL_HIGH
        wait_SCL_LOW
        ; Note: t(HD:DAT) == 0 µs, so we can start changing SDA immediately.

        endm


; During a READ-FROM operation on self/slave, this function is used to
; write a byte back to the MASTER.
;
; EXPECT: Bank 0
; EXIT:   Bank 0
;
; RETURN VALUES:
; 
;   Z is SET: NACK was sent by Master. The slave app should stop writing
;             bytes onto the bus, and skip to the next transaction.
;   Z is CLEAR: ACK was sent by Master. Slave app should continue writing
;               bytes onto the bus.
;
 global I2C_S_write
I2C_S_write:
        ; ### similar to I2C_M_send_one, but we don't manage SCL.

        bank1s
        movwf   SENDVAL
        bank0s

        ; Write each BIT to SDA and wait for Master to toggle SCL.
        write_one_bit   7
        write_one_bit   6
        write_one_bit   5
        write_one_bit   4
        write_one_bit   3
        write_one_bit   2
        write_one_bit   1
        write_one_bit   0

        ; Note: SCL is LOW. SDA is unknown. Release SDA so the Master can
        ;       N/ACK our response byte.
        release_SDA

        ; Prepare the Z flag now, for fewer cycles during SCL HIGH.
        clrz

        ; Read the ACK flag
        wait_SCL_HIGH
        ; As soon as SCL transitions HIGH, we can read SDA to determine
        ; the N/ACK value. Update the ZERO flag if needed: ACK(0)/NACK(1).
        btfsc   SDA_PORT, SDA
        setz

        wait_SCL_LOW

        ; Exit: SCL is LOW. SDA is released (and nobody should be holding
        ; it LOW), so SDA will be SET/HIGH-Z
        ;
        ; The Z flag reflects the N/ACK state.

        return


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