; Primary application for the relay board
;
; ### license
;
; ### docco
;
; This application provides an I2C interface to an 8-relay board.
;
; WRITE-TO operations are supported to modify the state. READ-FROM is
; enabled to provide current PORT state.
;
; This module uses the terms "Enable" and "Disable" to turn "on" a
; relay. When the ACTIVE_HIGH macro is *defined* (value is irrelevant)
; then "Enable" means that a pin should be set to HIGH to enable the
; relay (turn it on).
;
; OPCODE values are defined with the following bit pattern:
;
;   00EcScPc
;     Enable/Disable pins on a port.
;       E : enable relay (1), or disable (0)
;       S : shift channel mask
;       P : port select: PORTA (0) or PORTC (1)
;       c : channel mask flags
;
;   01xxxxx
;     01000000 : ALL OFF
;     01000001 : reserved
;     0100001x : reserved
;     010001xx : reserved
;     01001xxx : reserved
;     0101xxxx : reserved
;     011cScPc : toggle pins on a port
;
;   10xxxxx
;   11xxxxx
;       reserved
;
;   The valid S/P/c combinations are:
;
;       xxx00001 : RA0
;       xxx01001 : RA1
;       xxx00100 : RA2
;       xxx01100 : RA3
;       xxx10000 : RA4
;       xxx11000 : RA5
;
;       xxx00011 : RC0
;       xxx01011 : RC1
;       xxx00110 : RC2
;       xxx01110 : RC3
;       xxx10010 : RC4
;       xxx11010 : RC5
;
;       PORTA register = 0x05
;       PORTC register = 0x07
;         thus: register = 0x05 | xxxxxxPx
;
;       pin mask = OPCODE & 0x15 ... possibly shifted left once (per S flag)
;

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

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

        __config DEFAULT_CONFIG


; If the Makefile hasn't told us what to do, then we'll use active HIGH by default.
 ifndef ACTIVE_HIGH
  constant ACTIVE_HIGH = 1
 endif

        ; Define this module's variables
.gpr0   udata

; The OPCODE sent in a WRITE-TO request on the I2C bus.
OPCODE  res     1

; Working value for a mask to apply to PORTA/C
MASK    res     1


all_off:        macro
        ; Latch "disabled" to all outputs: RA0/1/2/./4/5 and RC0/1/2/3
 if ACTIVE_HIGH
        clrf    PORTA
        clrf    PORTC
 else
        movlw   0x37
        iorwf   PORTA, F
        movlw   0x0f
        iorwf   PORTC, F
 endif
        endm

goto_if_PORTC:  macro ADDR
        btfsc   OPCODE, 1
        goto    ADDR
        endm

prepare_MASK:   macro
        ; Prepare the bitmask (the "c" bits) to apply to the PORT

        ; W contains the OPCODE. Mask the pins to use.
        andlw   0x15
        movwf   MASK

        ; Potentially shift the MASK, based on the SHIFT bit in the OPCODE
        clrc
        btfsc   OPCODE, 3
        rlf     MASK, F

        endm


.reset: org  0x0000
        goto main


.main:  code                    ; relocatable
main:
        ; Note: bank is ENOCARE. This will force bank 0.
        ; Disable comparators. We're going to use all RA* as OUTPUTS.
        disable_comparators
        ; Now in Bank 0

        ; Start with everything "off"
        all_off

        bank1s
        run_at_8MHz

        ; Ensure AN* are configured as DIGITAL pins.
        clrf    ANSEL

        ; At reset time, all ports are INPUT/HIGH-Z. We need to set the
        ; relay pins to OUTPUT, but leave I2C as INPUT.
        ; (if we set I2C to OUTPUT, then we might short the bus)

        ; Relay pins: RA*, RC0/1/2/3  (RC4/RC5 are I2C)
        clrf    TRISA
        movlw   0xF0
        andwf   TRISC, F

        ; Note: remain in Bank 1

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

busy_loop:
        ; Wait for SDA to transition to LOW.
        btfsc   SDA_PORT, SDA
        goto    busy_loop

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.

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

        ; The Master is writing to self. First byte is the OPCODE. Stretch
        ; the clock so that we don't miss the STOP/REPEAT.
        call    I2C_S_read_stretch
        movwf   OPCODE

        ; ### note: we don't rigorously test the upper two bits.
        ; ### we're assuming the master will send proper opcodes.

        ; Special opcodes? Look for 01xxxxxx.
        btfsc   OPCODE, 6
        goto    special_operation

        ; OPCODE: 00EcScPc (still in W)

        ; Extract the "c" bits for upcoming work.
        prepare_MASK

 if ACTIVE_HIGH
        btfsc   OPCODE, 5       ; Disable (0) should set pin(s) LOW.
 else
        ; Active LOW
        btfss   OPCODE, 5       ; Enable (1) should set pin(s) LOW.
 endif
        goto    set_HIGH

        ; Set a port LOW, by masking off the bit.
        comf    MASK, W

        ; Which PORT are we altering?
        goto_if_PORTC set_LOW_PORTC

        andwf   PORTA, F
        goto    operation_done

set_LOW_PORTC:
        andwf   PORTC, F
        goto    operation_done

set_HIGH:
        movfw   MASK

        ; Which PORT are we altering?
        goto_if_PORTC set_HIGH_PORTC

        iorwf   PORTA, F
        goto    operation_done

set_HIGH_PORTC:
        iorwf   PORTC, F
        ; FALLTHRU

operation_done:
        ; End the clock stretching, send an ACK, then skip until the
        ; next transaction.
        call    I2C_S_end_stretch

        ; We've done everything for this transaction. Wait for a STOP
        ; or REPEATED-START condition, and do whatever comes next.
finish_transaction:

        ; Wait for the end of the request.
        call    I2C_skip

        ; If Z is CLEAR, then this is a REPEATED START, so go see if we
        ; are being addressed again.
        bnz     repeated_start

        ; Z is SET. We saw a STOP condition, and the bus is now idle.
        goto    busy_loop


read_from:
        ; Write PORTA and PORTC as the response.
        movfw   PORTA
        call    I2C_S_write

        ; If the MASTER sent a NACK, then it is done reading. Finish this
        ; transaction and watch for what is next.
        bz      finish_transaction

        movfw   PORTC
        call    I2C_S_write

        ; Master is done. Finish up, read what is next.
        bz      finish_transaction

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

        ; Master wants more? Keep writing zeroes.
        bnz     write_zeroes

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


special_operation:
        ; Note: W contains the OPCODE

        ; ### expand for other opcodes, as needed

        ; OPCODE<5> if set, is the "toggle" operation
        ; (given/assuming OPCODE<7:6> is 01)
        btfsc   OPCODE, 5
        goto    toggle_pins

        ; If OPCODE != 0x40 (ALL_OFF), then ignore it.
        sublw   0x40
        bnz     operation_done

        ; Turn "off" all relays.
        all_off

        goto    operation_done


toggle_pins:
        prepare_MASK
        movfw   MASK

        ; Which PORT are we altering?
        goto_if_PORTC   xor_PORTC

        xorwf   PORTA, F
        goto    operation_done

xor_PORTC:
        xorwf   PORTC, F
        goto    operation_done


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