;**********************************************************************
; UART to I2C Bridge
; 2022-02-25 RSP
; Target: PIC12F1822
;
; Pin Diagram                --------- 
;                    Vdd -> |1       8| <- Vss
;                    RxD -> |2 A5 A0 7| -> RxReady  (PGD)
;                    TxD <- |3 A4 A1 6| <> SCL      (PGC)
;        (MCLR)    /Lock -> |4 A3 A2 5| <> SDA
;                            ---------
; Interface:
;   Serial up to about 19200 (using HFINTOSC (2%), so high baud rate performance is not good)
;   32 byte transmit buffer, overflow is NAK'ed
;   80 byte receive buffer, overflow is ignored
;   RxReady goes high when Rx data is ready
;   Reading data when none is ready results in NAK
;   I2C address and serial speed settings are configurable via I2C stream data
;   Ground /Lock pin to inhibit settings changes 
;**********************************************************************

    list       p=12F1822      ; list directive to define processor
    #include   <p12F1822.inc> ; processor specific variable definitions
    errorlevel -302           ; suppress register bank warnings

;------------------------------------------------------------------------------
; HELPER MACROS
;------------------------------------------------------------------------------

MOVLF macro zLITERAL, zREG
    movlw   zLITERAL
    movwf   zREG
    endm

MOV   macro zSrc,zDst
    movf    zSrc,w
    movwf   zDst
    endm

#define B0 0
#define B1 1
#define B2 2
#define B3 3
#define B4 4
#define B5 5
#define B6 6
#define B7 7

#DEFINE MASK(b) (1 << b)

;------------------------------------------------------------------------------
; CHIP CONFIGURATION
;------------------------------------------------------------------------------    

    __CONFIG _CONFIG1, 0x3FFF & _FOSC_INTOSC & _WDTE_OFF & _PWRTE_OFF & _MCLRE_OFF & _CP_ON & _CPD_OFF & _BOREN_OFF & _CLKOUTEN_OFF & _IESO_OFF & _FCMEN_OFF
    __CONFIG _CONFIG2, 0x3FFF & _WRT_OFF & _PLLEN_ON & _STVREN_OFF & _BORV_19 & _LVP_OFF

#define LOCK_PIN PORTA,B3
#define RDY_PIN  LATA,B0

#define I2C_ADDR 8 ; default I2C address for new chip

; SPBRG settings
#define BRG_2400  207
#define BRG_4800  103
#define BRG_9600   51
#define BRG_19200  25

;------------------------------------------------------------------------------
; VARIABLE DEFINITIONS
;
; The 12F1822 has 128 bytes of RAM
;   80 bytes in Bank 0
;   32 bytes in Bank 1
;   16 bytes of bankless access RAM
;------------------------------------------------------------------------------

ACCESS_RAM  UDATA_SHR  0x70 ; bankless, 16 bytes

ee_i2c   RES 1  ; i2c address
ee_spbrg RES 1  ; serial speed setting

rx_ch    RES 1  ; temp

tx_ch    RES 1  ; temp

; queue for i2c -> serial xmit
tx_index RES 1  ; head position in tx_buf
tx_count RES 1  ; chars in tx_buf

; queue for serial receive -> i2c
rx_index RES 1  ; head position in rx_buf
rx_count RES 1  ; chars in rx_buf
rx_data  RES 1  ; i2c preload data

; settings command stream processor
command_bp      RES 1 ; command receiver state
command_chksum  RES 1 ; command checksum accumulator
command_addr    RES 1 ; address parameter
command_data    RES 1 ; data parameter
command_ch      RES 1 ; temporary variable
command_temp    RES 1 ; temporary variable

;- - - - - - - - - - - - - - - - - - - - - - - -

BANK0       UDATA       0x20    ; 80 bytes

#define RXBUFSIZ 80 ; receive buffer uses all of bank0
rx_buf  RES RXBUFSIZ

;- - - - - - - - - - - - - - - - - - - - - - - -

BANK1       UDATA       0xA0    ; 32 bytes

#define TXBUFSIZ 32 ; transmit buffer uses all of bank1
tx_buf  RES TXBUFSIZ

;------------------------------------------------------------------------------
; EEPROM INITIALIZATION
;
; The 12F1822 has 256 bytes of non-volatile EEPROM, starting at address 0xF000
;------------------------------------------------------------------------------

; +0 = i2c address
; +1 = SPBRG value

;------------------------------------------------------------------------------
; RESET VECTOR
;------------------------------------------------------------------------------

RESET_VECT  CODE    0x0000  ; processor reset vector

    bra     START           ; When using debug header, first inst. may be passed over by ICD2.  
    bra     START

;------------------------------------------------------------------------------
; INTERRUPT SERVICE ROUTINE
;
; The 12F1822 automatically saves/restores W, STATUS, BSR, FSRs, and PCLATH
;------------------------------------------------------------------------------

ISR         CODE    0x0004

; the only enabled interrupt is SSP (I2C)
; latency for SSP interrupt is 3-5 cycles = 625ns max @32MHz

    banksel SSP1STAT        ; handle i2c read vs write
    btfss   SSP1STAT, R_NOT_W
    bra     i2c_write

i2c_read
    btfsc   SSP1CON3, ACKTIM ; in ACK sequence?
    bra     i2c_read_ack
    ; reading data
    MOV     rx_data, SSP1BUF ; prepare data for i2c master
    bsf     SSP1CON1, CKP    ; release clock, master can now start receiving (clocking out data)
    btfss   SSP1STAT, D_NOT_A
    bra     i2c_read_done
    ; prepare next rx_data
    clrf    rx_data         ; send 0 if que is empty
    movf    rx_count, f     ; queue should not be empty (count still includes the byte we just sent)
    bz      i2c_read_empty
    ; bump queue pointer
    decf    rx_count, f     ; count the byte we just sent
    incf    rx_index, f     ; bump index to next byte in queue
    movlw   RXBUFSIZ        ; check for wrap
    xorwf   rx_index, w
    btfsc   STATUS, Z
    clrf    rx_index        ; wrap
    ; handle empty queue (turn off ready signal)
    movf    rx_count, f     ; is queue empty now?
    bnz     i2c_read_next   ;   no, prefetch next
i2c_read_empty
    banksel LATA
    bcf     RDY_PIN         ; turn off ready pin
i2c_read_done
    banksel PIR1
    bcf     PIR1, SSP1IF    ; reset interrupt source
    retfie                  ; RETurn From IntErrupt

i2c_read_ack
    ; send NAK if no data is ready
    movf    rx_count, f
    btfsc   STATUS, Z
    bsf     SSP1CON2, ACKDT ; set NACK
    btfss   STATUS, Z
    bcf     SSP1CON2, ACKDT ; set ACK
    clrf    SSP1BUF         ; required or I2C locks up
    bsf     SSP1CON1, CKP   ; release clock
    bra     i2c_read_done

i2c_read_next
    ; prefetch data from queue
    movf    rx_index, w     ; data is at rx_buf[ rx_index ]
    addlw   rx_buf
    movwf   FSR1L           ; now pointing to data
    MOV     INDF1, rx_data  ;   fetch from buffer, and leave it in the buffer
    bra     i2c_read_done


i2c_write
    btfss   SSP1CON3,ACKTIM ; in ACK sequence?
    bra     i2c_write_done  ;   no, ignore interrupt
    MOV     SSP1BUF, tx_ch  ; read whatever i2c master sent, clears BF
    ; NAK if buffer is already full
    movlw   TXBUFSIZ
    xorwf   tx_count, w
    btfsc   STATUS, Z
    bsf     SSP1CON2, ACKDT ; set NACK
    btfss   STATUS, Z
    bcf     SSP1CON2, ACKDT ; set ACK
    bsf     SSP1CON1, CKP   ; release clock
    ; ignore address
    btfss   SSP1STAT, D_NOT_A
    bra     i2c_write_done
    ; store data to transmit queue
    movf    tx_index, w     ; store data to tx_buf[ tx_index + tx_count ]
    addwf   tx_count, w
    addlw  -TXBUFSIZ        ; Z|C if wrap
    bz      i2c_write_wrap_handled
    btfss   STATUS, C
    addlw   TXBUFSIZ        ; no wrap, restore value
i2c_write_wrap_handled
    addlw   tx_buf
    movwf   FSR1L           ; now pointing to buffer
    MOV     tx_ch, INDF1    ;  store to buffer
    incf    tx_count, f     ; count as added to buffer
i2c_write_done
    banksel PIR1
    bcf     PIR1, SSP1IF    ; reset interrupt source
    retfie                  ; RETurn From IntErrupt

;------------------------------------------------------------------------------
; MAIN PROGRAM
;------------------------------------------------------------------------------

START

;-- set Fosc = 32 MHz fOSC => 8 MHz fCYC => 125 nsec

    banksel  OSCCON
    movlw    b'01110000'    ; HFINTOSC (32 MHz with PLL enabled)
    movwf    OSCCON
    
    ; wait for accurate fOSC
    banksel  OSCSTAT
    btfss    OSCSTAT,HFIOFR ; HFINTOSC ready?
    goto     $-1
    btfss    OSCSTAT,PLLR   ; PLL ready?
    goto     $-1

;-- configure I/O pins

    banksel ANSELA
    clrf    ANSELA              ; all digital I/O 
    
    banksel LATA
    clrf    LATA                ; all outputs low

    banksel WPUA
    MOVLF   b'00001000', WPUA   ; enable pull-up on MCLR
    banksel OPTION_REG
    bcf     OPTION_REG, NOT_WPUEN

    banksel TRISA
    MOVLF   b'00101110', TRISA  ; RA5 RxD, RA3(MCLR) option, A1/A2=I2C, A0=INT
    
;-- get settings from EEPROM

    call    ee_load ; gets ee_i2c & ee_spbrg

;-- set up USART

    banksel APFCON
    MOVLF   b'10000100', APFCON ; put Rx on RA5, Tx on RA4

    banksel RCSTA
    MOVLF   b'10010000', RCSTA  ; enable USART module, receive enabled
    MOVLF   b'00100000', TXSTA  ; async mode, transmit enabled, low speed baud rate
    MOVLF   b'00000000',BAUDCON ; 8 bit baud rate generator
    MOV     ee_spbrg, SPBRG     ; set serial speed

;-- set up I2C (slave)

    lslf    ee_i2c, w      ; convert 7 bit I2C address to PIC format
    banksel SSP1ADD
    movwf   SSP1ADD    ; set I2C slave address
    MOVLF   b'00100110', SSP1CON1 ; set I2C slave mode
    bsf     SSP1CON3, AHEN ; enable Address Hold (clock stretching)
    bsf     SSP1CON3, DHEN ; enable Data Hold (clock stretching)
    bsf     SSP1CON3, BOEN ; enable Buffer Overwrite
    MOVLF   b'10000000', SSP1STAT ; disable slew rate for standard mode

    ; enable SPI interrupt
    banksel PIE1
    bsf     PIE1, SSP1IE
    banksel PIR1
    bcf     PIR1, SSP1IF

;-- initialize variables

    clrf    rx_index   ; clear buffers
    clrf    rx_count
    clrf    tx_index
    clrf    tx_count

    clrf    command_bp
    clrf    command_chksum

    clrf    FSR0H      ; foregnd owns INDF0
    clrf    FSR1H      ; interrupt owns INDF1

;-- enable interrupts

    bsf     INTCON, PEIE
    bsf     INTCON, GIE

;- - - - - - - - - - - - - - - - - - - - - - - -
; Bigloop

BIGLOOP

;-- listen for incoming serial data
    
    BANKSEL RCSTA
    movf    RCSTA, w        ; check for serial errors
    andlw   MASK(FERR) | MASK(OERR)
    bz      RxRDY_check     ; no errors, continue
    ; clear errors, ignore data
    bcf     RCSTA, SPEN
    movf    RCREG, w
    movf    RCREG, w
    bsf     RCSTA, SPEN
    bra     listen_done

RxRDY_check
    BANKSEL PIR1
    btfss   PIR1, RCIF      ; serial data ready ?
    bra     listen_done
    BANKSEL RCREG
    MOV     RCREG, rx_ch    ; read serial data
    ; ignore data if queue is full
    movlw   RXBUFSIZ
    xorwf   rx_count, w
    bz      listen_done
    ; store into receive queue
    movf    rx_index, w     ; store data to rx_buf[ rx_index + rx_count ]
    addwf   rx_count, w
    addlw  -RXBUFSIZ        ; Z|C if wrap
    bz      rx_wrap_handled
    btfss   STATUS, C
    addlw   RXBUFSIZ        ; no wrap, restore value
rx_wrap_handled
    addlw   rx_buf
    movwf   FSR0L
    MOV     rx_ch, INDF0    ; store data to buffer
    ; finish queue add & turn on ready signal
    bcf     INTCON, GIE
      ; preload for i2c xmit if 1'st in queue
      movf    rx_count, f   ; nothing in queue?
      btfsc   STATUS, Z
      movwf   rx_data       ; rx_ch -> rx_data
      ; finish
      incf    rx_count, f   ; data added to queue, interrupt now owns it
      banksel LATA
      bsf     RDY_PIN       ; turn on ready pin
    bsf     INTCON, GIE
listen_done

;-- send from transmit queue

    ; anything in the queue?
    movf    tx_count, f     ; transmit queue empty?
    bz      transmit_done
    ; transmit ready?
    banksel TXSTA
    btfss   TXSTA, TRMT     ; transmitter busy?
    bra     transmit_done
    ; fetch from tx queue
    movf    tx_index, w     ; data is at tx_buf[ tx_index ]
    addlw   tx_buf          ; calculate address
    movwf   FSR0L           ; point to data
    movf    INDF0, w        ; fetch data
    ; send it
    banksel TXREG
    movwf   TXREG           ; send serial data

    banksel PORTA
    btfsc   LOCK_PIN        ; ignore commands if Lock pin is grounded
    call    command_stream

    ; remove it from the queue
    bcf     INTCON, GIE
      decf    tx_count, f   ; count it
      incf    tx_index, f   ; bump index
      movlw   TXBUFSIZ      ; check for wrap
      xorwf   tx_index, w
      btfsc   STATUS, Z
      clrf    tx_index      ; wrap
    bsf     INTCON, GIE
transmit_done
    bra     BIGLOOP

;- - - - - - - - - - - - - - - - - - - - - - - -
; command stream
; examines incoming I2C data stream looking for settings
;
;   prolog 13 bytes = 104 bits
;   setting 2 bytes: address, data
;   chksum  1 byte
;   ===== 128 bits total
;
;    0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
;   1c 97 28 f5 83 0b c9 7d a0 11 e1 b4 0f AA DD CC

#define PROLOG_0  0x1C ; stream signature
#define PROLOG_1  0x97
#define PROLOG_2  0x28
#define PROLOG_3  0xF5
#define PROLOG_4  0x83
#define PROLOG_5  0x0B
#define PROLOG_6  0xC9
#define PROLOG_7  0x7D
#define PROLOG_8  0xA0
#define PROLOG_9  0x11
#define PROLOG_10 0xE1
#define PROLOG_11 0xB4
#define PROLOG_12 0x0F

command_stream
    ; match prolog string, extract parameters, and handle checksum
    movwf   command_ch      ; save received char
    ; switch( command_bp )
    MOV     command_bp, command_temp
    movlw   PROLOG_0        
    bz      cmd_match_ch    ; case 0
    decf    command_temp, f
    movlw   PROLOG_1
    bz      cmd_match_ch
    decf    command_temp, f
    movlw   PROLOG_2
    bz      cmd_match_ch
    decf    command_temp, f
    movlw   PROLOG_3
    bz      cmd_match_ch
    decf    command_temp, f
    movlw   PROLOG_4
    bz      cmd_match_ch
    decf    command_temp, f
    movlw   PROLOG_5
    bz      cmd_match_ch
    decf    command_temp, f
    movlw   PROLOG_6
    bz      cmd_match_ch
    decf    command_temp, f
    movlw   PROLOG_7
    bz      cmd_match_ch
    decf    command_temp, f
    movlw   PROLOG_8
    bz      cmd_match_ch
    decf    command_temp, f
    movlw   PROLOG_9
    bz      cmd_match_ch
    decf    command_temp, f
    movlw   PROLOG_10
    bz      cmd_match_ch
    decf    command_temp, f
    movlw   PROLOG_11
    bz      cmd_match_ch
    decf    command_temp, f
    movlw   PROLOG_12
    bz      cmd_match_ch
    decf    command_temp, f
    bz      cmd_addr        ; case 13
    decf    command_temp, f
    bz      cmd_data        ; case 14
    ; it's the chksum character
    movf    command_ch, w
    xorwf   command_chksum, w
    bnz     cmd_reset
    ; execute command
    movf    command_addr, w ; 0=i2c address, 1=SPBRG value, 3=readback
    bz      cmd_set_i2c
    decf    WREG, w
    bz      cmd_set_speed
    decf    WREG, w
    bz      cmd_readback
    ; invalid address
cmd_reset
    clrf    command_bp
    clrf    command_chksum
    return

cmd_match_ch ; try to match prolog character
    xorwf   command_ch, w
    bnz     cmd_reset
cmd_accepted_ch
    incf    command_bp, f
    movf    command_ch, w       ; calculate checksum
    addwf   command_chksum, f
    return

cmd_addr ; store command address
    MOV     command_ch, command_addr
    bra     cmd_accepted_ch

cmd_data ; store command data
    MOV     command_ch, command_data
    bra     cmd_accepted_ch

cmd_set_i2c ; set I2C address
    MOV     command_data, ee_i2c
    call    ee_update
    lslf    ee_i2c, w  ; convert 7 bit I2C address to PIC format
    banksel SSP1ADD
    movwf   SSP1ADD    ; set I2C slave address
    bra     cmd_readback

cmd_set_speed ; set serial speed
    MOV     command_data, ee_spbrg
    call    ee_update
    banksel SPBRG
    MOV     ee_spbrg, SPBRG ; set serial speed

cmd_readback ; read out current settings
             ; 00 00 $ i2c brg
    clrf    WREG
    call    store_to_tx_queue
    clrf    WREG
    call    store_to_tx_queue
    movlw   '$'
    call    store_to_tx_queue
    movf    ee_i2c, w
    call    store_to_tx_queue
    movf    ee_spbrg, w
    call    store_to_tx_queue
    bra     cmd_reset

store_to_tx_queue
    movwf   rx_ch
    ; ignore data if queue is full
    movlw   RXBUFSIZ
    xorwf   rx_count, w
    bz      zz_done
    ; store into receive queue
    movf    rx_index, w     ; store data to rx_buf[ rx_index + rx_count ]
    addwf   rx_count, w
    addlw  -RXBUFSIZ        ; Z|C if wrap
    bz      zz_wrap_handled
    btfss   STATUS, C
    addlw   RXBUFSIZ        ; no wrap, restore value
zz_wrap_handled
    addlw   rx_buf
    movwf   FSR0L
    MOV     rx_ch, INDF0    ; store data to buffer
    ; finish queue add & turn on ready signal
    bcf     INTCON, GIE
      ; preload for i2c xmit if 1'st in queue
      movf    rx_count, f   ; nothing in queue?
      btfsc   STATUS, Z
      movwf   rx_data       ; rx_ch -> rx_data
      ; finish
      incf    rx_count, f   ; data added to queue, interrupt now owns it
      banksel LATA
      bsf     RDY_PIN       ; turn on ready pin
    bsf     INTCON, GIE
zz_done
    return

;- - - - - - - - - - - - - - - - - - - - - - - -

ee_load ; load settings from EE
    banksel  EECON1
    bcf      EECON1, CFGS
    bcf      EECON1, EEPGD

    clrf     EEADRL
    bsf      EECON1, RD
    MOV      EEDATL, ee_i2c
    incf     WREG, w        ; test for uninitialized EEPROM
    bz       ee_new
    bcf      ee_i2c, B7     ; force valid I2C address so we can talk to it

    MOVLF    1, EEADRL
    bsf      EECON1, RD
    MOV      EEDATL, ee_spbrg
    return

ee_new ; default settings for new chip
    MOVLF   I2C_ADDR, ee_i2c
    MOVLF   BRG_9600, ee_spbrg

ee_update ; write settings to EE
    banksel  EECON1
    btfsc    EECON1, WR     ; wait for previous write to complete
    goto     $-1
    bcf      EECON1, CFGS
    bcf      EECON1, EEPGD
    bsf      EECON1, WREN

    clrf     EEADRL
    MOV      ee_i2c, EEDATL
    MOVLF     0x55, EECON2
    MOVLF     0xAA, EECON2
    bsf      EECON1, WR

    btfsc    EECON1, WR     ; wait for previous write to complete
    goto     $-1
    MOVLF    1, EEADRL
    MOV      ee_spbrg, EEDATL
    MOVLF     0x55, EECON2
    MOVLF     0xAA, EECON2
    bsf      EECON1, WR

    bcf      EECON1, WREN
    return

;------------------------------------------------------------------------------
; END OF PROGRAM
;------------------------------------------------------------------------------

    END
