;******************************************************************************
;******************************************************************************
;
;        Electronic Dice Firmware (for the SpikenzieLabs Dice Kit)
;
;                        11/2010 R.S.Pierce
;
;******************************************************************************
;******************************************************************************

; assembly control options

#define ENABLE_EXTENDED_MODES 1 ; 0 (standard dice) or 1 (enable super-dice & double-roller modes)
                                ; 
                                ; When enabled, the mode toggles [through all 4 combinations]
                                ; each time the battery is inserted.
                                ; 
                                ; The current mode is indicated [when the battery is inserted] 
                                ; by a single or double roll of '6'(standard) or '7'(super-dice).
                                ;
                                ; Super-dice adds a 1 in 100 chance of rolling a '7'.

;------------------------------------------------------------------------------
; PROCESSOR DECLARATION
;
; Edit/Compile/link with MPLAB 8.41, MPASM 5.34, MPLINK 4.34
;------------------------------------------------------------------------------

    LIST      P=12F675          ; list directive to define processor
    #INCLUDE <P12F675.INC>      ; processor specific variable definitions
    errorlevel -302             ; suppress register bank warnings

    ; chip fuse configuration:
    ;   - brown-out detect and watchdog must be off [they consume a significant amount of power]
    ;   - MCLR is not used (it's an input pulled up to Vdd, which allows for ICSP)
    ;   - internal (4 MHz) RC oscillator
    ;   - power-up timer on (it can be on or off; it doesn't matter)
    ;   - no code protection
    __CONFIG   _BODEN_OFF & _WDT_OFF & _MCLRE_OFF & _INTRC_OSC_NOCLKOUT & _PWRTE_ON & _CP_OFF & _CPD_OFF

FOSC EQU 4000000                ; oscillator frequency
FCYC EQU FOSC/4                 ; instruction cycle [and Timer clock rate]

;------------------------------------------------------------------------------
; HELPER MACROS
;------------------------------------------------------------------------------

;-- generic

#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)

MOV macro zSRC, zDST
    movf    zSRC, w
    movwf   zDST
    endm

MOVLF macro	zLITERAL, zREG
    movlw	zLITERAL
    movwf	zREG
    endm

MOVLF16 macro zLITERAL, zREG16
    MOVLF   low (zLITERAL), zREG16+0
    MOVLF   high(zLITERAL), zREG16+1
    endm

;-- application specific

MAKE_GP1_ANALOG macro
    banksel ANSEL
    bsf     ANSEL, B1   ; using GP1 (led "BANKA") for analog
    banksel TRISIO
    bsf     TRISIO,B1   ; ...input
    endm

MAKE_GP1_DIGITAL macro
    banksel ANSEL
    bcf     ANSEL, B1   ; using GP1 (led "BANKA") for digital
    banksel TRISIO
    bcf     TRISIO,B1   ; ...output
    endm

;------------------------------------------------------------------------------
; VARIABLE DEFINITIONS
;
; The PIC12F675 has 64 bytes of RAM, all of which is bankless
;------------------------------------------------------------------------------

BANK0   UDATA_SHR 0x20  ; 64 bytes in both banks

pgm_flags RES   1       ; program control bitz
_FLAG_RESERVED  EQU B0  ;   random bit, must be LSB
_FLAG_ADC       EQU B1  ;   roll is in progress (LEDs are off during 1'st 2 timeslices)
_FLAG_TAP       EQU B2  ;   tap has been detected
_FLAG_TAP_PEND  EQU B3  ;   tap detector temp
_FLAG_TAP_LOCK  EQU B4  ;   force application timing even when tap is detected
_FLAG_RESET     EQU B5  ;   reset (vs. restart) indicator
_FLAG_SUPER     EQU B6  ;   super vs. standard dice mode
_FLAG_DOUBLE    EQU B7  ;   double vs. single roll mode
PATTERN  RES    3       ; current LED display pattern, must be align(4)+1
ticks    RES    2       ; timer
RandLo   RES    1       ; random number (16 bit)
RandHi   RES    1       ;   ...
RandTemp RES    1       ; random generator temp
count    RES    1       ; loop counter
roller   RES    2       ; dice roll value = 0..6
throw    RES    2       ; dice roll value = 0..6
souper   RES    2       ; superdice roller (0..99)

;------------------------------------------------------------------------------
; EEPROM INITIALIZATION
;
; The PIC12F675 has 128 bytes of non-volatile EEPROM, starting at address 0x2100
;
; The 1'st cell of EE is used to store the dice operating mode bits.
;------------------------------------------------------------------------------

DATAEE        CODE    0x2100

;------------------------------------------------------------------------------
; OSCILLATOR CALIBRATION VALUE
;
; Internal RC calibration value is placed at location 0x3FF by Microchip as
; a RETLW K instruction, where the K is a literal value to be loaded into 
; the OSCCAL register.  
;------------------------------------------------------------------------------

FACTORY_OSC   CODE    0x03FF

GET_OSC ;retlw  K

;------------------------------------------------------------------------------
; VECTORS
;------------------------------------------------------------------------------

RESET_VECTOR  CODE    0x0000  ; processor reset vector
        GOTO    START         ; go to beginning of program

INT_VECTOR    CODE    0x0004  ; interrupt vector location
        ; not used

;------------------------------------------------------------------------------
; MAIN PROGRAM
;
; The PIC12F675 has 1K of program memory, all in one bank
; The stack is 8 levels deep
;------------------------------------------------------------------------------

MAIN_PROG     CODE

START ; one-time initialization (when battery is inserted)

;------------------------------------------------------------------------------
; OSCCAL RESTORE (not required if internal OSC is not used)
;
; INTOSC is used by this application, but the calibration accuracy is not critical
;------------------------------------------------------------------------------

    call    GET_OSC         ; retrieve factory calibration value
    banksel OSCCAL
    MOVWF   OSCCAL          ; update register with factory cal value 

;------------------------------------------------------------------------------
; Initialize variables
;------------------------------------------------------------------------------

    clrf    roller+0        ; value must be 0..6
    clrf    roller+1

    clrf    souper+0        ; value must be 0..6
    clrf    souper+1

    MOVLF   0x30, RandHi    ; recommended seed value
    MOVLF   0x45, RandLo

;------------------------------------------------------------------------------
; Toggle mode each time battery is inserted
;------------------------------------------------------------------------------

#if ENABLE_EXTENDED_MODES == 1

    ; read 1'st EE cell
    banksel EEADR           ; point to 1'st EE cell
    clrf    EEADR 
    bsf     EECON1,RD       ; read EE
    movf    EEDAT, w

    ; toggle mode
    addlw   b'01000000'     ; mode settings are in the 2 high bits
    movwf   pgm_flags

    ; store back into EE
    movwf   EEDAT           ; value to write (EEADR is still == 0)
    bsf     EECON1,WREN
    movlw   0x55            ; required sequence for EE write, with interrupts disabled
    movwf   EECON2          ;  ...
    movlw   0xAA            ;  ...
    movwf   EECON2          ;  ...
    bsf     EECON1,WR       ;  ...

    ; indicate mode by showing a 6(standard) or 7(super) 
    movlw   5               ; == dice value "6"
    btfsc   pgm_flags, _FLAG_SUPER
    movlw   6               ; == dice value "7"

#else ; standard dice only

    clrf    pgm_flags       ; set single/standard mode
    movlw   5               ; == dice value "6"

#endif

    movwf   throw+0         ; initial display value
    movwf   throw+1
    bsf     pgm_flags, _FLAG_RESET ; force display

;------------------------------------------------------------------------------
; Startup initialization
;------------------------------------------------------------------------------

restart

;-- configure I/O ports

    banksel GPIO
    clrf    GPIO               ; all outputs = 0 (all LEDs off)

    banksel TRISIO
    MOVLF   b'001001',TRISIO   ; GP0 is piezo, GP3 is MCLR, all other pins are LEDs

    banksel CMCON
    MOVLF   b'00000111',CMCON  ; turn comparator off (necessary to make I/O pins digital)

    banksel IOC
    MOVLF   b'000001',IOC      ; enable interrupt-on-change for GP0 pin

;-- configure A/D converter

    banksel ANSEL
    MOVLF   b'0010000', ANSEL  ; ADCS = fOSC/8, all digital I/O [for now]
    banksel ADCON0
    MOVLF   b'10000100',ADCON0 ; right justified, channel AN1, A/D module off [for now]

;-- configure timer 0

    ; 1 MHz clock to TMR0
    ;   using 1:8 prescaler & rollover at 256 = ~488 Hz rollover rate
    ;   with 3-phase display update, net result is ~162 Hz refresh rate
    banksel OPTION_REG
    MOVLF   b'11010010',OPTION_REG

; timimg values
#define TICKS_1SEC 488      ; one second
#define TICKS_ROLL  30      ; roll "spin" pattern flash rate
#define TICKS_DELAY 99      ; delay before showing results
#define ROLL_CYCLES  5      ; number of times to "spin" the dice when roll is started

; single dice mode timing
#define SHOW_TIME   15      ; time to show throw resumts before auto-shutoff (seconds)
#define SHOW_MIN     1      ; minimum lockout time after restart/roll (seconds)

; double dice mode timing
#define TICKS_DOUBLEDIE   244 ; display time per dice
#define TICKS_INTERLBLANK 120 ; delay between dice
#define DOUBLE_CYCLES      10 ; number of times to show both dice

;-- display current mode if this is a reset

    btfsc   pgm_flags, _FLAG_RESET
    goto    bypass

;-- sleep until tap is detected (in extreme low power mode [1uA max, 1.2nA typ])

    banksel GPIO
    movf    GPIO, w         ; clear pin change flag
    bcf     INTCON, GPIF

    bsf     INTCON, GPIE    ; enable wakeup on change

    sleep   ; Zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz

bypass
    
    bcf     pgm_flags, _FLAG_RESET

;-- 1'st tap after sleep shows the last throw value

display

    bcf     pgm_flags, _FLAG_ADC     ; don't use ADC since LED[s] are on

    btfsc   pgm_flags, _FLAG_DOUBLE  ; handle single vs. double roll display
    goto    display_double

    ;--------------------------------
    ; single roll display
    ;--------------------------------

    ; only die #1 is used
    movf    throw,w
    call    get_drive_pattern   ; set up PATTERN[0,1,2]
    ; show result for a while (forced)
    MOVLF16 TICKS_1SEC*SHOW_MIN, ticks
    bsf     pgm_flags, _FLAG_TAP_LOCK
    call    run
    ; show result some more (allow tap to start a new dice roll)
    MOVLF16 TICKS_1SEC*(SHOW_TIME-SHOW_MIN), ticks
    bcf     pgm_flags, _FLAG_TAP
    bcf     pgm_flags, _FLAG_TAP_LOCK
    call    run
    btfss   pgm_flags, _FLAG_TAP
    goto    restart             ; auto-shutoff (timed out)
    goto    throw_the_dice      ; tap was detected

    ;--------------------------------
    ; double roll display
    ;--------------------------------

display_double

; show result (both die) for a while (forced)

    bsf     pgm_flags, _FLAG_TAP_LOCK
    ; die 1
    movf    throw+0,w
    call    get_drive_pattern   ; set up PATTERN[0,1,2]
    MOVLF16 TICKS_DOUBLEDIE, ticks
    call    run
    ; blank before showing second die
    clrf    PATTERN+0           
    clrf    PATTERN+1
    clrf    PATTERN+2
    MOVLF16 TICKS_INTERLBLANK, ticks
    call    run
    ; die 2
    movf    throw+1,w
    call    get_drive_pattern   ; set up PATTERN[0,1,2]
    MOVLF16 TICKS_DOUBLEDIE, ticks
    call    run

; show result (both die) some more (allow tap to start a new dice roll)

    bcf     pgm_flags, _FLAG_TAP
    bcf     pgm_flags, _FLAG_TAP_LOCK
    MOVLF   DOUBLE_CYCLES-1, count ; already showed 'em once

display_loop

    clrf    PATTERN+0           ; blank before showing other die
    clrf    PATTERN+1
    clrf    PATTERN+2
    MOVLF16 TICKS_INTERLBLANK, ticks
    call    run
    btfsc   pgm_flags, _FLAG_TAP
    goto    throw_the_dice

    movf    throw+0,w
    call    get_drive_pattern   ; set up PATTERN[0,1,2]
    MOVLF16 TICKS_DOUBLEDIE, ticks
    call    run
    btfsc   pgm_flags, _FLAG_TAP
    goto    throw_the_dice

    clrf    PATTERN+0           ; blank before showing other die
    clrf    PATTERN+1
    clrf    PATTERN+2
    MOVLF16 TICKS_INTERLBLANK, ticks
    call    run
    btfsc   pgm_flags, _FLAG_TAP
    goto    throw_the_dice

    movf    throw+1,w
    call    get_drive_pattern   ; set up PATTERN[0,1,2]
    MOVLF16 TICKS_DOUBLEDIE, ticks
    call    run
    btfsc   pgm_flags, _FLAG_TAP
    goto    throw_the_dice

    decfsz  count,f             ; continue until timeout or tap is detected
    goto    display_loop
    goto    restart             ; auto-shutoff (timed out)

;-- roll the dice!

throw_the_dice ; a tap during the roll will restart the roll

    ; while the dice is rolling, use the 1'st two display timeslots to "read" a LED that's off
    banksel ADCON0
    bsf     ADCON0, ADON    ; turn on A/D module

    ; loop initialization
    bsf     pgm_flags, _FLAG_ADC      ; enable photoelectric randomization
    bcf     pgm_flags, _FLAG_TAP      ; reset tap detector
    bsf     pgm_flags, _FLAG_TAP_LOCK ; synchronize tap to display
    clrf                   PATTERN+0
    clrf                   PATTERN+1
    MOVLF   ROLL_CYCLES, count

roll_loop ; flash the "spin" pattern

    MOVLF   PATTERN_DIAG1, PATTERN+2
    MOVLF16 TICKS_ROLL, ticks
    call    run

    MOVLF   PATTERN_HORZ,  PATTERN+2
    MOVLF16 TICKS_ROLL, ticks
    call    run

    MOVLF   PATTERN_DIAG2, PATTERN+2
    MOVLF16 TICKS_ROLL, ticks
    call    run

    ; restart roll in sync with LED "spin" effect
    btfsc   pgm_flags, _FLAG_TAP
    goto    throw_the_dice

    decfsz  count, f
    goto    roll_loop

;-- short delay with LEDs off

    clrf                   PATTERN+2
    MOVLF16 TICKS_DELAY, ticks
    bcf     pgm_flags, _FLAG_TAP_LOCK
    call    run
    btfsc   pgm_flags, _FLAG_TAP
    goto    throw_the_dice

;-- roll complete

    banksel ADCON0
    bcf     ADCON0, ADON    ; turn off A/D module

    ; freeze dice roll value
    MOV     roller+0, throw+0
    MOV     roller+1, throw+1

    ; try for a super-roll (yummy)
    btfss   pgm_flags, _FLAG_SUPER
    goto    display
SUPERTHROW macro zIndex
    movf    souper+zIndex,w
    movlw   6               ; == dice roll of "7"
    btfsc   STATUS,Z
    movwf   throw+zIndex
    endm
    SUPERTHROW 0 ; die #1
    SUPERTHROW 1 ; die #2

    goto    display

;------------------------------------------------------------------------------
; Display driver
;
; Entry: ticks   = time to run
;        PATTERN = LED display pattern (3 timeslots)
;        pgm_flags = control settings
;
; Exit:  pgm_flags[_FLAG_TAP] set if a tap was detected

run
    ; reset timebase
    banksel TMR0
    clrf    TMR0            ; also clears prescaler
    bcf     INTCON, T0IF

    bcf     INTCON, GPIF    ; clear piezo pin change detect

    bcf     pgm_flags, _FLAG_TAP_PEND

    MOVLF   PATTERN, FSR    ; init LED pattern index

run_loop ; loop at full code speed

    ; standard dice roller
STDROLL macro zIndex
    call    Random16
    movf    RandLo,w        ; get low bit of random number generator
    xorwf   pgm_flags,w     ;   incorporate photoelectric randomness
    andlw   MASK(B0)
    btfss   STATUS,Z        ; random bit determines roll "speed"
    incf    roller+zIndex,f
    movlw   6               ; wrap 5+1 => 0
    xorwf   roller+zIndex,w
    btfsc   STATUS,Z
    clrf    roller+zIndex
    endm
    STDROLL 0   ; die #1
    STDROLL 1   ; die #2

    ; super-dice roller
SUPERROLL macro zIndex
    movf    roller+zIndex,w ; fold regular roll value into superdice roll value
    addwf   souper+zIndex,f
    movlw   100             ; mod 100
    subwf   souper+zIndex,w
    btfsc   STATUS,C
    movwf   souper+zIndex
    endm
    SUPERROLL 0 ; die #1
    SUPERROLL 1 ; die #2

    ; watch for timer rollover (== 1 tick)
    btfss   INTCON, T0IF
    goto    run_tick_end
    bcf     INTCON, T0IF

;-- timer tick

    ; synchronize to prevent flicker
    btfss   pgm_flags, _FLAG_TAP_PEND  
    goto    run_not_tap
    btfsc   pgm_flags, _FLAG_TAP_LOCK ; application
    goto    run_not_tap
    movlw   PATTERN                   ; display mux
    xorwf   FSR, w
    bz      run_exit
run_not_tap

    ; update display
    movf    INDF, w         ; get next pattern
    banksel GPIO
    movwf   GPIO            ; write to LEDs

    ; introduce randomness using photoelectric effect from a LED
    btfsc   pgm_flags, _FLAG_ADC ; only while LED is known to be off
    call    photorandom

    ; bump pattern index
    incf    FSR, f
    movlw   PATTERN         ; wrap pattern index if necessary
    btfsc   FSR, B2
    movwf   FSR

    ; count down 1 tick
    movlw   -1              ; 16 bit decrement
    addwf   ticks+0, f
    bc      run_tick_end
    movlw   -1
    addwf   ticks+1, f
    bnc     run_exit        ; timer has expired
run_tick_end

    ; watch for tap
    btfss   INTCON, GPIF
    goto    run_loop
    ; tap detected; process it at an appropriate time
    bsf     pgm_flags, _FLAG_TAP_PEND
    bsf     pgm_flags, _FLAG_TAP
    goto    run_loop

run_exit
    MAKE_GP1_DIGITAL        ; restore I/O pin to digital output
    return

;------------------------------------------------------------------------------
; 16 bit random number generator
;
; Logic is from the Microchip application library, rewritten for PIC12 assembler

Random16 ; CRC (pseudo-random number) generator, feedback term = (Q15 xorwf Q14 xorwf Q12 xorwf Q3)

    rlf     RandHi,w
    movwf   RandTemp
    rlf     RandTemp,f
    rlf     RandTemp,f  ; B7 = b12

    rlf     RandHi,w
    xorwf   RandHi,w    ; B7 = xor(b15,14)
    xorwf   RandTemp,f  ; B7 = xor(b15,14,12)

    swapf   RandLo,w
    xorwf   RandTemp,f  ; B7 = xor(b15,14,12,3)

    rlf     RandTemp,f  ; rotate xor(b15,14,12,3)
    rlf     RandLo, f   ;   into LSB of RandHi:Lo
    rlf     RandHi, f

    return

;------------------------------------------------------------------------------
; Helper to "read" a LED
;
; (sense the LED photoelectric effect using 10 bit A/D)

photorandom ; modifies pgm_flags[LSB]

    movlw   PATTERN     ; use pattern index for A/D state machine
    subwf   FSR, w
    bnz     run_photo_not_0
    ; phase 0 = make I/O pin analog, start acquisition
    ;   [all LEDs should be off at this point]
    MAKE_GP1_ANALOG
    return

run_photo_not_0         ; A/D acquisition complete
    addlw   -1
    bnz     run_photo_not_1
    ; phase 1 = start A/D conversion cycle
    ;   [all LEDs should still be off at this point]
    banksel ADCON0
    bsf     ADCON0, GO
    return

run_photo_not_1         ; A/D conversion complete
    ; phase 2 = read A/D result, restore I/O pin to digital
    ;   [one or more LEDs may be on at this point]
    banksel ADRESL
    movf    ADRESL, w   ; get low 8 bits of A/D result
    andlw   MASK(B0)    ;   only using LSB
    xorwf   pgm_flags,f
    MAKE_GP1_DIGITAL    ; restore I/O pin to digital output
    return

;------------------------------------------------------------------------------
; Set up LED drive PATTERN[0,1,2] for die roll value in WREG (0=1, 1=2, ... 5=6, 6=7)

PATTERN_OFF   EQU b'000000'
PATTERN_DOT   EQU b'100000'
PATTERN_DIAG1 EQU b'010000'
PATTERN_DIAG2 EQU b'000100'
PATTERN_HORZ  EQU b'000010'

get_drive_pattern 

    clrf                   PATTERN+1
    clrf                   PATTERN+2

    iorlw   0
    bnz     not_1
; 1
    MOVLF   PATTERN_DOT,   PATTERN+0
    return

not_1
    addlw   -1
    bnz     not_2
; 2
    MOVLF   PATTERN_DIAG1, PATTERN+0
    return

not_2
    addlw   -1
    bnz     not_3
; 3
    MOVLF   PATTERN_DIAG1, PATTERN+0
    MOVLF   PATTERN_DOT,   PATTERN+1
    return

not_3
    addlw   -1
    bnz     not_4
; 4
    MOVLF   PATTERN_DIAG1, PATTERN+0
    MOVLF   PATTERN_DIAG2, PATTERN+1
    return

not_4
    addlw   -1
    bnz     not_5
; 5
    MOVLF   PATTERN_DIAG1, PATTERN+0
    MOVLF   PATTERN_DIAG2, PATTERN+1
    MOVLF   PATTERN_DOT,   PATTERN+2
    return

not_5
    addlw   -1
    bnz     not_6
; 6
    MOVLF   PATTERN_DIAG1, PATTERN+0
    MOVLF   PATTERN_DIAG2, PATTERN+1
    MOVLF   PATTERN_HORZ,  PATTERN+2
    return

not_6
; 7
    MOVLF   PATTERN_DIAG1, PATTERN+0
    MOVLF   PATTERN_DIAG2, PATTERN+1
    MOVLF   PATTERN_HORZ|PATTERN_DOT,PATTERN+2
    return

;------------------------------------------------------------------------------

    END
