; Primary application for the switch-debounce boards
;
; ### license
;
; ### docco
;
; This application provides an I2C interface to a board that monitors
; (9) individual switches, pulled-HIGH. If one/more are pulled LOW,
; then pin:ACTIVE is pulled LOW (which has default pull-HIGH output).
; The ACTIVE line is connected to an "upstream" device (eg. Arduino)
; which should notice the change and query this board via I2C to
; discover what happened.
;
; I2C WRITE-TO is not supported.
; I2C READ-FROM will return (2) bytes:
;  * PORTA switch state
;  * PORTC switch state
;
; RA0..5 are switch inputs (weak pull-ups on all, but RA3)
; RC0,1,2 are switch inputs (no internal pull-ups)
; RC3 is the "ACTIVE" line (output) to the upstream manager
; RC4,5 are the I2C interface
;
; ----
;
; This is a good articule about debounce timing:
; https://www.eejournal.com/article/ultimate-guide-to-switch-debounce-part-4/
;
; Important points from above:
;  * Debounce flutter finishes within 20ms.
;  * Humans cannot press/release faster than about 100ms.
;
; Once we signal upstream that data is available for reading, it should
; take maybe 0.2ms to read that data via I2C. Well below the 20ms
; debounce, and likely while the switch is still bouncing.
;
; NOTE: we can ignore switch changes/flutter while transmitting on
; the I2C bus. Further, we can ignore true changes (not the flutter)
; until the debounce time completes from the first detection
; (eg. 20ms ignore). If multiple (human) presses/releases are performed,
; this will occur over a minimum 100ms window (per above), so we'll
; see the change post-debounce-ignore (and then start another
; debounce delay).
;
; ----
;
; At any point in time, a switch input is in one of THREE states:
;
;   1. Stable open
;   2. Stable closed
;   3. Fluttering
;
; Reading during the stable states is our desired condition, and there
; is no concern.
;
; Reading during "flutter" will provide two outcomes:
;
;   a. No change. It bounced, then returned to prior just as the pin
;      state was read. This outcome is theoretical, but will not be
;      observed.
;   b. Change. The pin state is different from prior, which indicates
;      a transition towards a new state.
;
; ----
;
; OPERATION: we only read PORTA and PORTC for switch changes,
; every 20ms. If we miss a change during flutter (eg. recorded
; state was HIGH, and it bounced LOW back to HIGH), then we'll see
; the transition in another 20ms after it settles. Humans cannot
; notice these delays.
;
; ----
;
; Circuit design requires (4) external pull-ups, since some ports
; do not have internal pull-ups: RA3, RC0,1,2. The five other RA*
; ports will use the internal weak pull-ups.
;
; RC3 will be an output and does not require external pull-ups.
; RC4,5 vary between inputs/outputs, for I2C operation.
;

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

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

        __config DEFAULT_CONFIG

        ; see article above
        constant DEBOUNCE_MS = D'20'  ; milliseconds
        constant TIMER_START = (D'32' - DEBOUNCE_MS)*(0x100/D'32')

        ; our ACTIVE output line is RC3
        constant ACTIVE = RC3

; Define this module's variables
.gpr0   udata

; The state of PORTA and PORTC, during our capture process. These
; may flutter, but settle at next-capture. Two flutter events occur
; (on press, AND on release). When the state changes, we need to
; pull ACTIVE to LOW, to tell upstream that a change is available.
;
; Maps to RA0..5 and RC0..2 (9 total)
A_CAPTURED      res 1
C_CAPTURED      res 1

; A simple flag (in LCB) to track if we have changes in the PORTs.
CHANGE_SEEN     res 1


.reset: org  0x0000
        goto main


.main:  code                    ; relocatable
main:
        ; Note: bank is ENOCARE. This will force bank 0.
        ; Disable comparators. RA* will be digital inputs.
        disable_comparators
        ; Note: now in bank 0.

        ; Start: mark as no switches have been pressed
        ; (initialize now, while we're in bank0)
        movlw   0x3F            ; 6 bits, all HIGH
        movwf   A_CAPTURED
        movlw   0x07            ; 3 bits, all HIGH
        movwf   C_CAPTURED

        ; Ensure the ACTIVE port will be HIGH when it becomes an OUTPUT.
        bsf     PORTC, ACTIVE

        bank1s
        run_at_8MHz
        no_analog               ; We want digital pins, not analog.

        ; At reset time, all ports are INPUT/HIGH-Z, with weak pull-ups
        ; enabled. Turn RC3 (the ACTIVE pin) into an OUTPUT.
        bcf     TRISC, TRISC3

        ; Prepare the OPTION_REG register:
        ;   ~RAPU:  0    to enable weak pullups on PORTA
        ;   INTEDG: x    (don't care; default is 1)
        ;   T0CS:   0    TIMER0 uses internal instruction clock cycle
        ;   T0SE:   x    (don't care; default is 1)
        ;   PSA:    0    prescaler assigned to TIMER0
        ;   PS<2:0> 111  1:256 scaling, so overflow occurs every 32.8ms
        ;
        ; Note: before untying the prescaler from the WDT (default), the
        ;       WDT should be cleared to avoid an accidental trip.
        clrwdt
        movlw   b'01010111'
        movwf   OPTION_REG

        ; Note: we do not initialize TMR0 to TIMER_START. Whenever it
        ; first fires is just fine, to capture an initial state of the
        ; switch state.

        ; Note: remain in Bank 1

        ; Prepare for I2C slave operation.
        call    I2C_S_listen
        ; Note: now using Bank 0

busy_loop:
        ; ASSUMED: Bank 0.

        ; Has TIMER0 overflowed? If so, then it is time to read
        ; PORTA and PORTC and possibly take action.
        btfsc   INTCON, T0IF
        goto    timer_overflowed ; bit was set

        ; Has the I2C bus just become busy?
        ; Wait for SDA to transition to LOW.
        btfsc   SDA_PORT, SDA
        goto    busy_loop       ; SDA is still HIGH (no activity)

        ; I2C activity has started
        ; FALLTHRU

repeated_start:
        call    I2C_S_detect_START
        ; Note: we are in Bank 0.

        ; Multiple transfers may have occurred (via Repeated Starts).
        ; The Z flag specifies our current state:
        ;   Z is SET: the I2C bus is now IDLE
        ;   Z is CLEAR: self has been addressed.
        bz      busy_loop       ; wait for START

        ; Some transfers may have been skipped, but we have arrived at one
        ; addressing self. We have sent an ACK regarding our address.

        ; CARRY indicates the I2C READ-FROM(1)/WRITE-TO(0) status.
        bc      read_from

        ; This is a WRITE-TO self. We do not accept data/commands, so this
        ; can be ignored. We can "skip" this transmission, which means that
        ; we (and nobody else) will ACK the master's DATA transmission. The
        ; master can deal with that however they like -- not our problem.
        ;
        ; NOTE: we ACK'd the address transmission. Best we can do now is to
        ;       NACK the written bytes (skip does not pull SDA LOW for an
        ;       ACK to the master).

        ; FALLTHRU

finish_transaction:
        ; All work associated with the current (maybe unfinished/ignored)
        ; transaction has been completed.
        ;
        ; Wait for the transaction to finish. Get a STOP or REPEATED-START.
        ;
        ; NOTE: SCL "should" be LOW, but it is possibly HIGH (by now) for
        ;       the first data bit. Use skip_unknown for safety.
        call    I2C_skip_unknown

        ; If Z is CLEAR, then we saw a REPEATED-START. We will need
        ; to examine the next transaction for SELF, and the R/W bit.
        bnz     repeated_start

        ; Z is SET -- the bus is now idle (STOP was seen).
        goto    busy_loop


read_from:
        ; Write the two sets of switch state, for the response.
        movfw   A_CAPTURED
        call    I2C_S_write

        ; If the MASTER sent a NACK, then it is done reading. Finish this
        ; transaction and watch for what is next.
        ; NOTE: the ACTIVE pin is unchanged since the PORTC data is unread.
        bz      finish_transaction

        movfw   C_CAPTURED
        call    I2C_S_write
        ; NOTE: Z specifies result of write back to the master.

        ; ALL data has been read, so return ACTIVE to HIGH.
        bsf     PORTC, ACTIVE
        ; NOTE: does not change Z.

        ; Master sent NACK/DONE. Finish up, read what is next.
        bz      finish_transaction

        ; We have nothing more to send. Just send zeroes.
write_zeroes:
        clrw
        call    I2C_S_write

        ; Master wants more? Keep writing zeroes.
        bnz     write_zeroes

        ; All done. Got a NACK/DONE from the Master.
        goto    finish_transaction


; Our timer has overflowed, so time to look for PORT changes.
timer_overflowed:
        ; ASSUMED: Bank 0.

        ; NOTE: the timer overflow processing is "lengthy". This
        ;   section is (currently) about 32 instructions (16 µS).
        ;   t(HD;STA) is 4.0 µS, so we will certainly miss any
        ;   read that begins during this work.

        ; If the ACTIVE line is still LOW (signalling: read me), then
        ; upstream has not yet read the port data. This is somewhat
        ; problematic, as upstream should have seen our signal.
        btfsc   PORTC, ACTIVE
        ; We haven't signalled a change. See if we should.
        goto    examine_changes

        ; ACTIVE is already LOW. Update the captured values.
        movfw   PORTA
        movwf   A_CAPTURED
        movfw   PORTC
        andlw   0x07            ; just RC0,1,2
        movwf   C_CAPTURED
        goto    reset_timer


examine_changes:
        ; If we see changes, then we need to pull ACTIVE LOW. Use this
        ; flag to record changes on PORTA *or* PORTC.
        clrf    CHANGE_SEEN

        movfw   PORTA
        xorwf   A_CAPTURED, W
        bz      check_portc     ; no changes on PORTA

        ; Note the change.
        bsf     CHANGE_SEEN, 0

        ; Record the change by XOR'ing it into our CAPTURED state.
        xorwf   A_CAPTURED, F
        ; FALLTHRU

check_portc:
        movfw   PORTC
        andlw   0x07            ; just RC0,1,2
        xorwf   C_CAPTURED, W
        bz      alter_active    ; no changes on PORTC

        ; Note the change.
        bsf     CHANGE_SEEN, 0

        ; Record the change by XOR'ing it into our CAPTURED state.
        xorwf   C_CAPTURED, F
        ; FALLTHRU

alter_active:
        ; Did we set the SEEN bit? Skip to reset_timer if not.
        btfsc   CHANGE_SEEN, 0

        ; PORTA and/or PORTC changed. We know ACTIVE is currently HIGH,
        ; as we tested it above. Pull it low to signal upstream to read
        ; the new data.
        bcf     PORTC, ACTIVE
        ; FALLTHRU

reset_timer:
        ; Note: still in Bank 0.

        ; Reset the timer to the proper value for it to rollover in
        ; DEBOUNCE_MS milliseconds.
        movlw   TIMER_START
        movwf   TMR0

        ; Note that the Timer0 Interrupt Flag (T0IF) gets set upon
        ; overflow. Interrupts are not enabled, so the flag gets set
        ; and nothing more happens. But we need to clear it, so that
        ; we can look for it getting raised at the next overflow.
        bcf     INTCON, T0IF

        ; All done. Go look for more work.
        goto    busy_loop


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