;**********************************************************************
; Dual IN-1 Nixie Backpack Controller
; 
; 2021-12-16 RSP 
; 2022-01-07 M113 PCB
;**********************************************************************
; Use:              PIC16F1615
;                     ------ 
;             Vdd -> |1   14| <- Vss
;          1A RA5 <- |2   13| -> RA0 xx  PGD
;          1D RA4 <- |3   12| -> RA1 xx  PGC
;        MCLR RA3 xx |4   11| -> RA2 RX @9600  
;          1B RC5 <- |5   10| <- RC0 10C
;          1C RC4 <- |6    9| -> RC1 10A
;         10B RC3 <- |7    8| -> RC2 10D
;                     ------ 
; Interface:
;   Serial @ 9600
;   ATID=x to set unit address, x=A..Z
;   3 character commands, ASCII
;     1: unit address, a-z or A-Z, lowercase does a fade, @(instant) or *(fade) addresses all units
;     2: 10's digit, 0-9, anything else turns off the digit
;     3:  1's digit, 0-9, anything else turns off the digit
;   Diagnostic LED indicates activity
;**********************************************************************
    
    list      p=16F1615           ; list directive to define processor
    #include <p16F1615.inc>       ; processor specific variable definitions

    errorlevel  -302              ; suppress bank select warnings

    __CONFIG _CONFIG1, _FOSC_INTOSC & _PWRTE_OFF & _MCLRE_OFF & _CP_OFF & _BOREN_OFF & _CLKOUTEN_OFF & _IESO_OFF & _FCMEN_OFF
    __CONFIG _CONFIG2, _WRT_OFF & _ZCD_OFF & _PLLEN_ON & _STVREN_OFF & _BORV_LO & _LPBOR_OFF & _LVP_OFF
    __CONFIG _CONFIG3, _WDTE_OFF

#define DIAG_PIN LATA,B0          ; diagnostic LED, indicates activity
#define BEAT_BIT      B1          ; 500Hz heartbeat

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

NOPS macro zCount ; repeat nop's (125ns each)
  variable i
i = 0
    while i < zCount
    nop
i += 1
    endw
  endm

;------------------------------------------------------------------------------
; Memory allocation
    
ACCESS_RAM  UDATA_SHR   0x70  ; bankless, 16 bytes

unit_id     RES 1 ; stored in EEPROM

matches     RES 1 ; "ATID=" detector

command     RES 1 ; current command, ASCII ID char (a..z, A..Z)

next_digits    RES 1 ; command digits, BCD
current_digits RES 1 ; current display, BCD
old_digits     RES 1 ; fade display, BCD

pgm_flags   RES 1 ; program state
#define FADE        B0
#define GOT_COMMAND B1
#define GOT_10S     B2

diag_timer  RES 1 ; diagnostic LED timer

fade_cycles RES 1 ; PWM cycles remaining
pwm_index   RES 1 ; PWM width

port_data   RES 1 ; high side drive pins, positive logic
port_tris   RES 1 ; low side drive pins, negative logic
#define _C10   B0 ; port C B0..B5
#define _A10   B1
#define _D10   B2
#define _B10   B3
#define _C1    B4
#define _B1    B5
#define _D1    B6 ; port A B4,B5
#define _A1    B7

temp        RES 2 ; temp storage

;- - - - - - - - - - - - - - - - - - - - - - - -
     
; Bank 12 (last bank of SRAM)
 
BANK12     UDATA       0x620  ; 48 bytes

#define port_A_blanking pin_conrol_regs+0
#define port_C_blanking pin_conrol_regs+1
#define port_A_data     pin_conrol_regs+2
#define port_C_data     pin_conrol_regs+3
#define port_A_tris     pin_conrol_regs+4
#define port_C_tris     pin_conrol_regs+5
pin_conrol_regs RES 6 ; transfer buffer

last_A_data RES 1 ; last values written to LAT/TRIS registers
last_C_data RES 1
last_A_tris RES 1
last_C_tris RES 1

;------------------------------------------------------------------------------
; RESET VECTOR
;------------------------------------------------------------------------------
 
RESET_VECTOR    CODE    0x0000       ; processor reset vector
        goto    start                ; go to beginning of program

;------------------------------------------------------------------------------
; INTERRUPT SERVICE ROUTINE
;
; Automatically saves/restores W, STATUS, BSR, FSR, and PCLATH
;------------------------------------------------------------------------------

ISR             CODE    0x0004
    
INTERRUPT ; 3-5 cycles latency
    retfie

;------------------------------------------------------------------------------
; MAIN PROGRAM
;------------------------------------------------------------------------------

MAIN_PROG       CODE

start ; default fOSC = 500 KHz
  
;-- set Fosc = 32 MHz fOSC => 8 MHz fCYC

    banksel  OSCCON
    movlw    b'01110000'    ; HFINTOSC (32 MHz with PLL enabled)
    movwf    OSCCON
    ; wait for stable fOSC
    banksel  OSCSTAT
    btfss    OSCSTAT,HFIOFR ; HFINTOSC ready?
    goto     $-1
    btfss    OSCSTAT,PLLR   ; PLL ready?
    goto     $-1
    
;-- configure I/O pins
    
    banksel ANSELA
    MOVLF   b'00000000', ANSELA ; all digital I/O
    banksel ANSELC
    MOVLF   b'00000000', ANSELC

    banksel LATA
    MOVLF   b'00000000', LATA
    banksel LATC
    MOVLF   b'00000000', LATC

    banksel RXPPS               ; configure RxD on RA2 pin
    MOVLF   b'00000010', RXPPS

    banksel TRISA               ; outputs: RA0/RA1=unused, RA4=1D, RA5=1A
    MOVLF   b'00001100', TRISA  ; inputs:  RA3=MCLR, RA2=serial
    banksel TRISC               ; outputs: RC0=10C, RC1=10A, RC2=10D, RC3=10B, RC4=1C, RC5=1B
    MOVLF   b'00000000', TRISC  ; inputs:  none
    
    banksel WPUA                ; enable pull-up on MCLR (open input pin)
    MOVLF   MASK(B3), WPUA
    clrf    WPUC                ; no pullups on PORTC
    banksel OPTION_REG
    bcf     OPTION_REG, NOT_WPUEN    

;-- set up USART for receive only

    BANKSEL TXSTA
    MOVLF   b'00000000', TXSTA  ; async mode, transmit disabled, low speed baud rate
    BANKSEL RCSTA
    MOVLF   b'10010000', RCSTA  ; enable USART module, enable receive
    BANKSEL BAUDCON
    MOVLF   b'00000000',BAUDCON ; 8 bit baud rate generator
    BANKSEL SPBRG
    MOVLF   50, SPBRG           ; 9600 baud

;-- initialize variables

    call    ee_load ; get unit_id
    clrf    pgm_flags
    clrf    matches

    movlw   0xFF            ; BCD blank digits
    movwf   current_digits
    movwf   old_digits

;-- Timer1 + CCP1 is 1000 Hz timebase

    ; fOsc/4 = 8,000,000
    ;   want 1 msec
    ;   need 8,000 divide-by
    banksel T1CON
    MOVLF   b'00010001', T1CON      ; Fcyc clock, 1:2 prescale, timer on
    banksel CCP1CON
    MOVLF   high(4000-1),CCPR1H
    MOVLF    low(4000-1),CCPR1L
    MOVLF   b'10001011', CCP1CON    ; compare mode

;- - - - - - - - - - - - - - - - - - - - - - - -
BIGLOOP
;- - - - - - - - - - - - - - - - - - - - - - - -

;-- listen for comms
    
    BANKSEL RCSTA
    movf    RCSTA, w    ; check for serial errors
    andlw   MASK(FERR) | MASK(OERR)
    bz      RxRDY_check
    ; 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
    movf    RCREG, w    ; read serial data

;-- watch for ATID=x

    movwf   temp            ; save received char in temp
    MOV     matches, temp+1 ; copy match count to another temp
    movlw   'A'
    bz      matchit
    movlw   'T'
    decf    temp+1, f
    bz      matchit
    movlw   'I'
    decf    temp+1, f
    bz      matchit
    movlw   'D'
    decf    temp+1, f
    bz      matchit
    movlw   '='
    decf    temp+1, f
    bz      matchit
    ; set new unit ID; A..Z are valid unit IDs
    movlw   'A'
    subwf   temp, w     ; C=0 if cmd < 'A'
    bnc     mismatch
    movlw   'Z'+1
    subwf   temp, w     ; C=1 if cmd > 'Z'
    bc      mismatch
    MOV     temp, unit_id
    call    ee_update   ; store to EEPROM
    ; turn on diagnostic LED to indicate activity
    banksel LATA
    bsf     DIAG_PIN        ; LED on
    MOVLF   250, diag_timer ; 1/4 sec on time
    bcf     pgm_flags, GOT_COMMAND
    clrf    matches
    bra     BIGLOOP

matchit
    xorwf   temp, w
    bnz     mismatch
    incf    matches, f    
    bra     matchcheck_done

mismatch
    clrf    matches
matchcheck_done
    movf    temp, w     ; restore received char

;-- watch for command (3 chars)

    btfss   pgm_flags, GOT_COMMAND
    bra     check_id
    btfss   pgm_flags, GOT_10S
    bra     get_10s
    bra     get_1s

check_id ; check for unit id match
    movwf   command
    movlw   '*'             ; special char check; broadcast w/fade
    xorwf   command,w
    bz      id_match
    movlw   '@'             ; special char check; broadcast instant
    xorwf   command,w
    bz      id_match
    movf    command,w
    bcf     WREG, B5        ; force uppercase
    xorwf   unit_id, w
    bnz     listen_done     ; ignore if mismatch
id_match
    ; id matched, receive 10's digit next
    bsf     pgm_flags, GOT_COMMAND
    bra     listen_done

get_10s
    call    asciiToBCD
    swapf   WREG,w          ; 10's to high nybble
    movwf   next_digits
    bsf     pgm_flags, GOT_10S ; receive 1's digit next
    bra     listen_done
    
get_1s
    call    asciiToBCD      ; 1's to low nybble
    iorwf   next_digits, f  

;-- execute command

    bcf     pgm_flags, GOT_COMMAND
    bcf     pgm_flags, GOT_10S
    ; turn on diagnostic LED to indicate activity
    banksel LATA
    bsf     DIAG_PIN        ; LED on
    MOVLF   50, diag_timer  ; 1/20 sec on time
    ; save old digits
    MOV     current_digits, old_digits
    ; set new digits
    MOV     next_digits, current_digits
    ; turn off old digits after delay if lowercase command
    clrf    pwm_index
    clrf    WREG
    btfsc   command, B5  ; bit set if lowercase
    movlw   15           ;  15 * 16msec = 240msec fade time
    movwf   fade_cycles
listen_done

;-- 1000 Hz timer

    ; wait for timer1
    banksel PIR1
    btfss   PIR1, CCP1IF
    bra     BIGLOOP
    bcf     PIR1, CCP1IF

;-- 1 msec tick

    ; 500Hz heartbeat
    banksel LATA
    movlw   MASK(BEAT_BIT)
    xorwf   LATA, f
    
    ; if diag LED is on, time it out
    btfss   DIAG_PIN
    bra     led_done
    decf    diag_timer, f 
    btfsc   STATUS, Z
    bcf     DIAG_PIN
led_done

;-- update_nixies

    clrf    port_data
    MOVLF   0xFF, port_tris ; all inputs

; 1ms fundamental clock => 4 bit PWM resolution, 62.5 Hz PWM frequency

    ; if (fade_cycles) // fade old -> current
    movf    fade_cycles, w
    bz      nofade
    ; if (pwm_index < fade_cycles) show old else show new
    subwf   pwm_index, w ; borrow (C=0) if pwm_index < fade_cycles
    bcf     pgm_flags, FADE
    btfss   STATUS, C
    bsf     pgm_flags, FADE
    ; if (++pwm_index > 15) { fade_cycles--; pwm_index = 0; }
    incf    pwm_index, f
    btfss   pwm_index, B4
    bra     showit
    decf    fade_cycles, f
    clrf    pwm_index
showit
    btfss   pgm_flags, FADE
    bra     nofade
    ; show old digits
    swapf   old_digits, w  ; 10s
    call    tens
    movf    old_digits, w  ; 1's
    call    ones
    bra     sendit

nofade
    ; else show current digits
    swapf   current_digits, w  ; 10s
    call    tens
    movf    current_digits, w  ; 1's
    call    ones
sendit

    ; prepare pin controls
    ;  each digit is controlled by 4 pins
    ;  one pin is set HIGH, one LOW, and the other two float (input)
    ;  float all or ground all to turn off a digit
    banksel pin_conrol_regs
    clrf    port_A_blanking ; tris blanking for changed digits
    clrf    port_C_blanking
    MOV     port_A_data, last_A_data ; save for change detection
    MOV     port_C_data, last_C_data
    MOV     port_A_tris, last_A_tris
    MOV     port_C_tris, last_C_tris
    ;   port A - compute new LAT & TRIS
    banksel LATA
    movf    LATA, w         ; preserve other port bits
    andlw   0xCF            ;  we are writing bits 5,4
    movwf   temp
    lsrf    port_data, w    ; shift bits into position
    lsrf    WREG, w
    andlw   0x30            ; isolate bits 5,4
    iorwf   temp, w
    banksel pin_conrol_regs
    movwf   port_A_data     ; LATA data ready
    lsrf    port_tris, w    ; get TRIS bits into position
    lsrf    WREG, w
    iorlw   0x0C            ; set MCLR, serial as inputs
    andlw   0xFC            ; set RA0,RA1 as outputs
    movwf   port_A_tris
    ;   port C maps nicely so the compute is a copy
    MOV     port_data, port_C_data
    MOV     port_tris, port_C_tris
    ; detect changed digits
    movf    last_A_tris, w
    xorwf   port_A_tris, w
    andlw   0x30            ; 1's digit drive pins on PORTA
    bnz     change_1s
    movf    last_C_tris, w
    xorwf   port_C_tris, w
    andlw   0x30            ; 1's digit drive pins on PORTC
    bnz     change_1s
    movf    last_A_data, w
    xorwf   port_A_data, w
    andlw   0x30            ; 1's digit drive pins on PORTA
    bnz     change_1s
    movf    last_C_data, w
    xorwf   port_C_data, w
    andlw   0x30            ; 1's digit drive pins on PORTC
    bz      done_with_1s
change_1s
    MOVLF   0x30, port_A_blanking ; blank 1's digit
    MOVLF   0x30, port_C_blanking
done_with_1s
    movf    last_C_tris, w
    xorwf   port_C_tris, w
    andlw   0x0F            ; 10's digit drive pins on PORTC
    bnz     change_10s
    movf    last_C_data, w
    xorwf   port_C_data, w
    andlw   0x0F            ; 10's digit drive pins
    bz      done_with_10s
change_10s
    MOVLF   0x0F, port_C_blanking ; blank 10's digit
done_with_10s
    ; transfer to pin control registers
    MOVLF   high(pin_conrol_regs), FSR0H
    MOVLF    low(pin_conrol_regs), FSR0L
    ; blank digits that are being changed (float the control pins)
    banksel TRISA
    moviw   FSR0++      ; port_A_blanking
    iorwf   TRISA, f
    moviw   FSR0++      ; port_C_blanking
    iorwf   TRISC, f
    ; transfer to outputs
    banksel LATA
    moviw   FSR0++      ; port_A_data
    movwf   LATA
    moviw   FSR0++      ; port_C_data
    movwf   LATC
    banksel TRISA
    moviw   FSR0++      ; port_A_tris
    movwf   TRISA
    moviw   FSR0++      ; port_C_tris
    movwf   TRISC

    bra     BIGLOOP

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

tens ; turns on appropriate 10's cathode
    andlw   0x0F
    bz      c00
    decf    WREG, w
    bz      c10
    decf    WREG, w
    bz      c20
    decf    WREG, w
    bz      c30
    decf    WREG, w
    bz      c40
    decf    WREG, w
    bz      c50
    decf    WREG, w
    bz      c60
    decf    WREG, w
    bz      c70
    decf    WREG, w
    bz      c80
    decf    WREG, w
    bz      c90
    return

ones ; turns on appropriate 1's cathode
    andlw   0x0F
    bz      c0
    decf    WREG, w
    bz      c1
    decf    WREG, w
    bz      c2
    decf    WREG, w
    bz      c3
    decf    WREG, w
    bz      c4
    decf    WREG, w
    bz      c5
    decf    WREG, w
    bz      c6
    decf    WREG, w
    bz      c7
    decf    WREG, w
    bz      c8
    decf    WREG, w
    bz      c9
    return

c0  bsf     port_data,_B1 ; high side data bit
    bcf     port_tris,_B1 ; high side output enable
    bcf     port_tris,_D1 ; low side output enable
    return

c1  bsf     port_data,_B1
    bcf     port_tris,_B1
    bcf     port_tris,_C1
    return

c2  bsf     port_data,_D1
    bcf     port_tris,_D1
    bcf     port_tris,_B1
    return

c3  bsf     port_data,_D1
    bcf     port_tris,_D1
    bcf     port_tris,_C1
    return

c4  bsf     port_data,_D1
    bcf     port_tris,_D1
    bcf     port_tris,_A1
    return

c5  bsf     port_data,_A1
    bcf     port_tris,_A1
    bcf     port_tris,_C1
    return

c6  bsf     port_data,_A1
    bcf     port_tris,_A1
    bcf     port_tris,_D1
    return

c7  bsf     port_data,_A1
    bcf     port_tris,_A1
    bcf     port_tris,_B1
    return

c8  bsf     port_data,_C1
    bcf     port_tris,_C1
    bcf     port_tris,_B1
    return

c9  bsf     port_data,_C1
    bcf     port_tris,_C1
    bcf     port_tris,_A1
    return

c00 bsf     port_data,_B10
    bcf     port_tris,_B10
    bcf     port_tris,_D10
    return

c10 bsf     port_data,_B10
    bcf     port_tris,_B10
    bcf     port_tris,_C10
    return

c20 bsf     port_data,_D10
    bcf     port_tris,_D10
    bcf     port_tris,_B10
    return

c30 bsf     port_data,_D10
    bcf     port_tris,_D10
    bcf     port_tris,_C10
    return

c40 bsf     port_data,_D10
    bcf     port_tris,_D10
    bcf     port_tris,_A10
    return

c50 bsf     port_data,_A10
    bcf     port_tris,_A10
    bcf     port_tris,_C10
    return

c60 bsf     port_data,_A10
    bcf     port_tris,_A10
    bcf     port_tris,_D10
    return

c70 bsf     port_data,_A10
    bcf     port_tris,_A10
    bcf     port_tris,_B10
    return

c80 bsf     port_data,_C10
    bcf     port_tris,_C10
    bcf     port_tris,_B10
    return

c90 bsf     port_data,_C10
    bcf     port_tris,_C10
    bcf     port_tris,_A10
    return

;- - - - - - - - - - - - - - - - - - - - - - - -
; ASCII digit to BCD

asciiToBCD ; '0'..'9' to 0..9, anything else to 15

    addlw   -'0'    ; offset '0'..'9' to BCD
    addlw   -10     ; borrow (NC) if 0..9
    btfsc   STATUS,C
    retlw   15      ; not a valid digit
    addlw   10      ; restore value
    return

;------------------------------------------------------------------------------
; Nonvolatile settings storage
;------------------------------------------------------------------------------

#define PFM_ADDRESS 0x1F80 ; high-endurance program flash memory (PFM), 128 bytes

ee_load ; load settings from EEPROM

    banksel PMADRL
    MOVLF    low(PFM_ADDRESS), PMADRL
    MOVLF   high(PFM_ADDRESS), PMADRH
    bcf     PMCON1,CFGS ; Do not select Configuration Space
    bsf     PMCON1,RD   ; Initiate read
    nop
    nop
    movf     PMDATL, w  ; get data
    movwf    unit_id
    ; check for new EEPROM
    incf     WREG, w
    btfss    STATUS, Z
    return
    ; set default ID
    MOVLF    'A', unit_id
    return

ee_update ; write settings to EEPROM

    ; erase row
    banksel PMADRL
    MOVLF    low(PFM_ADDRESS), PMADRL
    MOVLF   high(PFM_ADDRESS), PMADRH
    bcf     PMCON1,CFGS ; Do not select Configuration Space
    bsf     PMCON1,FREE ; Specify an erase operation
    bsf     PMCON1,WREN  ; Enable writes
    MOVLF   0x55, PMCON2  ; Start of required sequence to initiate erase
    MOVLF   0xAA, PMCON2  ;
    bsf     PMCON1,WR  ; Set WR bit to begin erase
    nop ; NOP instructions are forced as processor starts
    nop ; row erase of program memory.
    ; The processor stalls until the erase process is complete
    ; after erase processor continues with 3rd instruction
    bcf     PMCON1,WREN ; Disable writes

    ; write data
    banksel PMADRH 
    MOVLF    low(PFM_ADDRESS), PMADRL
    MOVLF   high(PFM_ADDRESS), PMADRH
    bcf     PMCON1,CFGS ; Do not select Configuration Space
    bsf     PMCON1,WREN  ; Enable writes
    bsf     PMCON1,LWLO  ; Only Load Write Latches
    MOV     unit_id, PMDATL
    clrf    PMDATH
    MOVLF   0x55, PMCON2  ; Start of required sequence to initiate write
    MOVLF   0xAA, PMCON2  ;
    nop     ; NOP instructions are forced as processor
    nop     ; loads program memory write latches
    bcf     PMCON1,LWLO  ; No more loading latches - Actually start Flash program 
    MOVLF   0x55, PMCON2  ; Start of required sequence to initiate write
    MOVLF   0xAA, PMCON2  ;
    bsf     PMCON1,WR  ; Set WR bit to begin write
    nop     ; NOP instructions are forced as processor writes
    nop     ; all the program memory write latches simultaneously
    ; After NOPs, the processor
    ; stalls until the self-write process in complete
    ; after write processor continues with 3rd instruction
    bcf     PMCON1,WREN  ; Disable writes
    return

;------------------------------------------------------------------------------
; END OF PROGRAM
;------------------------------------------------------------------------------
    
    END
