; -------------------------------------
; dd-2.asm      DIGITAL DICE ON A ATTINY13
; Copyright (c) 2011 Mike Shegedin
;               ezdenki.com
;               Released under the GNU General Public License version 2
;
; Chip: ATTiny13A-PU
;
; Revisions
;
; dd-1.asm, 101005
;     First attempt. Was able to manually set individual die segments on left right die
;     and got multiplexing on the hardware working.
;
; dd-1a.asm, 101006
;
; dd-2.asm, 101011
;     First attempt at full function.
;
; dd-2b.asm, 101020
;     Basic functionality. Dice are animated upon startup. Pressing roll button
;     initiates brief roll animation and displays roll.
;     To do:
;        Enable Sleep/Wakeup modes.
;        LED glows dimly when polling the button. Try to make it mostly invisible.
;
; dd-2c.asm, 101021
;     Added sleep functionality
;
; dd-2d.asm, 101027
;     Improved polling timing and blanking
;
; dd-2e.asm, 110923
;     General cleanup of code
;     Improved consistency on sleep routine.
;     Optimized for hardware that includes the one resistor.
;     Reduced ghosting on center LED for hardware w/o resistor.

; dd-2f.asm, 110923
;     Greatly reduced ghosting due to polling by modifying button polling routine
;
; dd-2g.asm, 110925
;     Cleaned up code/comment formatting. No functional changes


; -------------------------------------
;
; Die Segment (DSn) and associated Hardware Pin Definitions:
;
; DS1, center dot, PB0
; DS2, upper-left and lower-right dot, PB1
; DS3, upper-right and lower-left dot, PB2
; DS4, left and right middle dots, PB3
; LR , left/right die select, PB4

; -------------------------------------
; Setting die segments (DSn):
;
; The patterns in which die segments are lit depend on whether the left or right die
; is being accesed, as determeined by LR. LR=0 is the left die, LR=1 is the right.
; To set segments in the left die (LR=0), relative bits must be turned on (1).
; To set segments in the right die (LR=1), relative bits must be turned OFF (0).
;
; Example, set the left die to show a 3 (segments 1 and 2 lit)
;
;   ldi      TMP, (Lon<<DS1) | (Lon<<DS2) | (Loff<<DS3) | (Loff<<DS4) | (Ldie<<LR)
;   out      PORTB, TMP
;
; Example, set right die to show a 4 (segments 2 and 3 lit)
;   ldi      TMP, (Roff<<DS1) | (Ron<<DS2) | (Ron<<DS3) | (Roff<<DS4) | (Rdie<<LR)
;   out      PORTB, TMP
;
; Note that when combining segments, the left die segments should be combined
; with the | (or) operator and the right die segments should be combined with
; the & (and) operator.
; Example: Left 7 (all on) = LD6|LD1 whereas Right 7 (all on) = RD6 & RD1
; -------------------------------------

.nolist
.include "tn13def.inc"
.list

; -------------------------------------
; Interrupt service vectors

.org  0x0000         ; Start of Interrupt vector
      rjmp MyDefs    ; Jump to "main" on startup or reset
.org  INT0addr       ; External Interrupt0
      reti
.org  PCINT0addr     ; Pin Change Interrupt0
      reti           ; Will be used to wake, but normally will be off
.org  TIM0_OVF0addr  ; Overflow0 Interrupt
      reti
.org  EE_RDYaddr     ; EEPROM write complete
      reti
.org  ANA_COMPaddr   ; Analog Comparator Interrupt
      reti
.org  TIM0_COMPAaddr ; Timer/Counter0 Compare Match A
      reti
.org  TIM0_COMPBaddr ; Timer/Counter0 Compare Match B 
      reti
.org  WDTaddr        ; Watchdog Timeout
      reti
.org  ADCaddr        ; ADC Conversion Complete Handle
      reti

; -------------------------------------
; My Defs

MyDefs:

; Register Assignments
.def     ZeroReg=R0    ; Will always have zero
.def     STim0=R1      ; 2 Stages of Sleep Timer
.def     STim1=R2
.def     BtnPress=R5   ; 0xFF=button pressed, 0x00=not pressed
.def     tmp=r16       ; GP register
.def     tmp2=r17      ; GP register
.def     tmp3=r18      ; GP register
.def     LDV=r19       ; Value (decimal) of left die
.def     RDV=r20       ; Value (decimal) of right die
.def     LDP=r21       ; Pattern of left die
.def     RDP=R22       ; Pattern of right die

; Constants
.equ     SDELAY=997    ; Short delay value
.equ     LDELAY=7       ; Long delay is number of iterations of short delay
.equ     DS1=PORTB0    ; Define bit numbers on PORTB to be
.equ     DS2=PORTB1    ;   used for LED output segments.
.equ     DS3=PORTB2
.equ     DS4=PORTB3
.equ     LR=PORTB4     ; Left/Right multiplexing bit
.equ     BTN=PORTB0    ; Reset pin used for Roll Button
.equ     Lon=1         ; Turn ON a segment in left die
.equ     Loff=0        ; Turn OFF a segment in left die
.equ     Ron=0         ; Turn ON a segment in right die
.equ     Roff=1        ; Turn OFF a segment in right die
.equ     Ldie=0        ; Indicates Left die (for multiplexing)
.equ     Rdie=1        ; Indicates Right die

; Die patterns for left dice
.equ     LD0=(Loff<<DS1)|(Loff<<DS2)|(Loff<<DS3)|(Loff<<DS4)|(Ldie<<LR)
.equ     LD1=(Lon<<DS1)|(Loff<<DS2)|(Loff<<DS3)|(Loff<<DS4)|(Ldie<<LR)
.equ     LD2=(Loff<<DS1)|(Lon<<DS2)|(Loff<<DS3)|(Loff<<DS4)|(Ldie<<LR)
.equ     LD3=(Lon<<DS1)|(Lon<<DS2)|(Loff<<DS3)|(Loff<<DS4)|(Ldie<<LR)
.equ     LD4=(Loff<<DS1)|(Lon<<DS2)|(Lon<<DS3)|(Loff<<DS4)|(Ldie<<LR)
.equ     LD5=(Lon<<DS1)|(Lon<<DS2)|(Lon<<DS3)|(Loff<<DS4)|(Ldie<<LR)
.equ     LD6=(Loff<<DS1)|(Lon<<DS2)|(Lon<<DS3)|(Lon<<DS4)|(Ldie<<LR)
.equ     LD7=(Lon<<DS1)|(Lon<<DS2)|(Lon<<DS3)|(Lon<<DS4)|(Ldie<<LR)

; Animation patterns for left dice
.equ     LDA0=(Lon<<DS1)|(Lon<<DS2)|(Loff<<DS3)|(Loff<<DS4)|(Ldie<<LR)
.equ     LDA1=(Lon<<DS1)|(Loff<<DS2)|(Loff<<DS3)|(Lon<<DS4)|(Ldie<<LR)
.equ     LDA2=(Lon<<DS1)|(Loff<<DS2)|(Lon<<DS3)|(Loff<<DS4)|(Ldie<<LR)
.equ     LDA3=(Lon<<DS1)|(Loff<<DS2)|(Loff<<DS3)|(Lon<<DS4)|(Ldie<<LR)

; Die patterns for right dice
.equ     RD0=(Roff<<DS1)|(Roff<<DS2)|(Roff<<DS3)|(Roff<<DS4)|(Rdie<<LR)
.equ     RD1=(Ron<<DS1)|(Roff<<DS2)|(Roff<<DS3)|(Roff<<DS4)|(Rdie<<LR)
.equ     RD2=(Roff<<DS1)|(Ron<<DS2)|(Roff<<DS3)|(Roff<<DS4)|(Rdie<<LR)
.equ     RD3=(Ron<<DS1)|(Ron<<DS2)|(Roff<<DS3)|(Roff<<DS4)|(Rdie<<LR)
.equ     RD4=(Roff<<DS1)|(Ron<<DS2)|(Ron<<DS3)|(Roff<<DS4)|(Rdie<<LR)
.equ     RD5=(Ron<<DS1)|(Ron<<DS2)|(Ron<<DS3)|(Roff<<DS4)|(Rdie<<LR)
.equ     RD6=(Roff<<DS1)|(Ron<<DS2)|(Ron<<DS3)|(Ron<<DS4)|(Rdie<<LR)
.equ     RD7=(Ron<<DS1)|(Ron<<DS2)|(Ron<<DS3)|(Ron<<DS4)|(Rdie<<LR)

; Animation patterns for right dice
.equ     RDA0=(Ron<<DS1)|(Ron<<DS2)|(Roff<<DS3)|(Roff<<DS4)|(Rdie<<LR)
.equ     RDA1=(Ron<<DS1)|(Roff<<DS2)|(Roff<<DS3)|(Ron<<DS4)|(Rdie<<LR)
.equ     RDA2=(Ron<<DS1)|(Roff<<DS2)|(Ron<<DS3)|(Roff<<DS4)|(Rdie<<LR)
.equ     RDA3=(Ron<<DS1)|(Roff<<DS2)|(Roff<<DS3)|(Ron<<DS4)|(Rdie<<LR)


.cseg

; -------------------------------------
; Reset Code Block

; DiceTable
; These are the 36 roll patterns, 1/1, 1/2, 1/3 all the way to 6/6.
; The last pattern is 7/7. This was for error checking to make sure that the
; routine to convert the clock/counter to the roll was not off by one. You should
; never see a 7/7 roll in normal use.

DiceTable:
.db 1,1,1,2,1,3,1,4,1,5,1,6
.db 2,1,2,2,2,3,2,4,2,5,2,6
.db 3,1,3,2,3,3,3,4,3,5,3,6
.db 4,1,4,2,4,3,4,4,4,5,4,6
.db 5,1,5,2,5,3,5,4,5,5,5,6
.db 6,1,6,2,6,3,6,4,6,5,6,6,7,7


Reset:
   clr      ZeroReg           ; Set ZeroReg to zero

   ldi      TMP, low(RAMEND)  ; Set Stack Pointer to end of RAM
   out      SPL,TMP
;  ldi     TMP,high(RAMEND)   ; High end not required on Tiny13
;  out     SPH,TMP

   ldi      TMP, (1<<DS1) | (1<<DS2) | (1<<DS3) | (1<<DS4) | (1<<LR)
   out      DDRB, TMP         ;  Set output pins (BTN will be set as needed)

   ldi      tmp, (0<<CS02 | 0<<CS01 | 1<<CS00)

   out      TCCR0B, tmp       ; Start Clock (TCNT0 register, 8 bit clock)
                              ; 000=no clock, 001=clk w/o prescaler
                              ; 010=1/8, 011=1/64, 100=1/256, 101=1/1024
   
   ldi      tmp, (1<<WGM01)   ; Initialize the CTC Clock (but won't use interrupt)
   out      TCCR0A, tmp

   ldi      tmp, 35           ; Set clock to cycle between 0 to 35
   out      OCR0A, tmp        ; (Current count found in TCNT0)

   clr      STim0             ; Set sleep timer to an appropriate length for the
   ldi      tmp,2             ; opening animation sequence. The wait time is about
   mov      STim1,tmp         ; STim1×45 seconds. Should be long enough to perform
                              ; simple visual diagnostics. STim1=2 gives an intro time
                              ; of about 90 seconds.


; -------------------------------------
; Main Code Block
; Main isn't really the main code. Upon reset, this section performs the opening
; animation while polling the button. Once the button is pressed, it will jump to the
; Roll routine and stay there until the unit sleeps.

OpeningAnimation:

   ldi      LDV, 10           ; Backslash pattern
   ldi      RDV, 12           ; Forward slash pattern

SPLoop:
   rcall    GetDiePatterns


   rcall    longdelay;        ; Hold display for a moment
   rcall    PollButton        ; Poll button

   sbrc     BtnPress, 0       ; BtnPress during delay?
   rjmp     Roll              ; If pressed, jump to Roll Routine

   ldi      LDV, 11           ; 3 across
   ldi      RDV, 11
   rcall    GetDiePatterns    ; Put die patterns in LDP and RDP

   rcall    longdelay;        ; Hold display for a moment
   rcall    PollButton        ; Poll button

   sbrc     BtnPress, 0       ; BtnPress during delay?
   rjmp     Roll              ; If pressed, jump to Roll Routine

   ldi      LDV, 12           ; Forward slash pattern
   ldi      RDV, 10           ; Backslash pattern
   rcall    GetDiePatterns    ; Put die patterns in LDP and RDP

   rcall    longdelay;        ; Hold display for a moment
   rcall    PollButton        ; Poll button
   
   sbrc     BtnPress, 0       ; BtnPress during delay?
   rjmp     Roll              ; If pressed, jump to Roll Routine
   rcall    SleepBnc          ; Bump the sleep counter
   rjmp     OpeningAnimation  ; Jump back to top of this routine
   

; -------------------------------------
; LongDelay
; A momentary delay while maintaining the display of 2 dice as
; defined in LDP and LRP. Does not check for input from the button.

LongDelay:

   push     tmp               ; Save tmp as it may be used by calling functions
   ldi      tmp,LDELAY        ; Number of iterations of this loop to make it long

LDCount:
   rcall    ShortDelay        ; Call ShortDelay
   dec      tmp               ; Decrement counter
   brne     LDCount           ; Loop up if not done
   pop      tmp               ; Restore tmp
   ret

; -------------------------------------
; ShortDelay
; A momentary delay while maintaining the display of 2 dice as
; defined in LDP and LRP. Does not check for input from the button.

ShortDelay:

   ldi      YH, HIGH(SDELAY)  ; 1st level delay
   ldi      YL, LOW(SDELAY)

SDCount1:
   out      PORTB, LDP        ; Output left die
   nop                        ; nops even out left/right brightness
   nop
   nop
   out      PORTB, RDP        ; Output right die

   sbiw      YH:YL, 1         ; Word-level decrement
   brne      SDCount1         ; Loop back up if not zero
   ret                        ; Else return


; -------------------------------------
; PollButton
; Will save the current port status, set the port to input, read the button and then
; restore the port status as it was before it was called.
; Returns BtnPress=0 if not pressed or BtnPress=0xFF if pressed.

PollButton:

   push     tmp               ; Save tmp on stack

   ser      tmp2              ; Assume press (return 1)
                              ; Switch pin to Input. Note that the pullup must
                              ; be enabled before setting the direction otherwise
                              ; a false high will be read on the pin.

   sbi      PORTB, DS1        ; Turn off right-middle LED which is most susceptible
   sbi      PORTB, LR         ; to ghosting from button polling.

   out      DDRB, ZeroReg     ; Set all pins to input
   sbi      PORTB, Btn        ; Enable pullup on btn
   nop
   sbic     PINB, Btn         ; Check button and skip if high (no press)
   clr      tmp2              ; Otherwise pressed, return a 1

   mov      BtnPress, tmp2    ; Load return value into BtnPress

   ser      tmp               ; Change all pins back to output
   out      DDRB, tmp

   pop      tmp               ; Restore tmp
   ret


; -------------------------------------
; Roll
; The actual main loop of the program while the unit is awake. This routine will
; be called each time the button is pressed. Once called, the SleepReset routine
; is called to reset the sleep timer. A short roll animation is displayed in order
; to foil cheaters. The new roll value is taken from the continuously cycling roll
; counter. The new die patterns are loaded. After that, the routine enters a loop
; of displaying the current dice, polling the button, and bumping the sleep timer.
; It will continue doing this as long as the button is not pressed and the sleep
; timer doesn't run out. If the button is pressed, the routine starts from the very
; top, resetting the sleep timer and getting new dice values. Does several loops of
; simply updating the display, and finally polls the input pin one time in order to
; minimize unwanted output due to polling on the shared I/O line.

Roll:

   rcall    SleepReset        ; Just got a roll, so reset the sleep counter
   rcall    RollDelay         ; Give a little animation to foil cheaters!
   rcall    GetRoll           ; Get new roll value.
   rcall    GetDiePatterns    ; Load up dice patterns.

Keep:
   rcall    ShortDelay        ; Hold display a moment
   rcall    PollButton        ; Poll the button.
   sbrc     BtnPress,0        ; Skip if BtnPress=0 (no press)
   rjmp     Roll              ; Otherwise jump up to Roll routine if pressed

   rcall    SleepBNC          ; Tick sleep timer

   rjmp     Keep              ; Loop back to continue displaying current roll


; -------------------------------------
; SleepBnC
; Sleep Bump 'n Check
; Decreases the sleep counter(s) and puts the unit into sleep mode when zeroed.

SleepBnC:

   dec      STim0             ; Decrement stage 1 counter
   brne     NoSleep           ; If not zero, jump to exit point
   clr      STim0             ; Otherwise reset stage 1 counter

   dec      STim1             ; Decrement stage 2 counter
   brne     NoSleep           ; If not zero, jump to exit point

   rjmp     SleepNow          ; If stage 2 counter zero, jump to sleep routine
   
NoSleep:
   ret                        ; Exit point


; -------------------------------------
; SleepNow
; Puts unit to sleep. Shuts off all LED outputs, disables pullups, sets interrupt
; to wake on button press. enables global interrupts, sleeps. When woken, the
; program will resume, just after the "sleep" instruction. At that point global
; and other relevant interrupts will be disabled, and the unit will jump to the
; very top of the Reset routine in order to have a fresh new run. At that point,
; a normal button press should be long enough to get past the initial animation
; routine and simply jump into the next roll.

SleepNow:

   ldi      LDV, 0            ; Shut off all LED outputs
   ldi      RDV, 0
   rcall    GetDiePatterns
   out      PORTB, LDP
   out      PORTB, RDP


; Disable pullups, Enable sleep mode, Sleep => Power Down

   ldi      tmp, (1<<SE | 1<<SM1 | 0<<SM0)
   out      MCUCR, tmp   

; Enable pinchange interrupt on pin0

   ldi      tmp, (1<<PCIE)    ; Enable pin change interrupts
   out      GIMSK, tmp

   ldi      tmp, (1<<PCINT0)  ; Set Pin0 on chip to enable interrupt
   out      PCMSK, tmp

   sei                        ; Enable global interrupts
   sleep                      ; Sleep!

; Wake up from this point.
; First, disable sleep and pin interrupts. Then jump to Roll routine to resume.

   cli                        ; Disable global interrupts

   ldi      tmp, (0<<SE)      ; Disable sleep mode, Sleep => Power Down
   out      MCUCR, tmp   

   ldi      tmp, (0<<PCIE)    ; Disable pin change interrupts
   out      GIMSK, tmp

   ldi      tmp, (0<<PCINT0)  ; Unset Pin0 on chip to enable interrupt
   out      PCMSK, tmp

   rjmp Roll                  ; Exit by jumping directly to Roll routine.



; -------------------------------------
; SleepReset
; Reset sleep timer to 0. Should be called when a button is successfully polled.

SleepReset:

   clr      STim0             ; Clear all sleep timer counter variables
   ldi      tmp,60            ; One unit of wait time is about 2 sec, so, tmp=60
   mov      STim1,tmp         ; yields about a 2 minute delay.

   ret


; -------------------------------------
; RollDelay
; Animates the dice for a few moments in order to prevent overly quick continuous
; rolls. Also acts as a de facto input debounce.

RollDelay:

   ldi      tmp, 5            ;Number of times to loop animation

RDLoop:
   ldi      LDV, 12           ; Display "/" die pattern on left die
   ldi      RDV, 10           ; Display "\" die pattern on right die
   rcall    GetDiePatterns    ; Get die pattern into LDP and RDP
   rcall    LongDelay         ; Show the pattern

   ldi      LDV, 11           ; Display "-" die pattern on left die
   ldi      RDV, 11           ; Display "-" die pattern on right die
   rcall    GetDiePatterns    ; Put die patterns in LDP and RDP
   rcall    LongDelay         ; Show the pattern

   ldi      LDV, 10           ; Display "/" die pattern on left die
   ldi      RDV, 12           ; Display "\" die pattern on right die
   rcall    GetDiePatterns    ; Put die patterns in LDP and RDP
   rcall    LongDelay         ; Show the pattern

   dec      tmp               ; Decrement tmp and loop back up
   brne     RDLoop            ; until done

   ret                        ; Return from routine


; -------------------------------------
; GetRoll
; Reads clock/timer and converts the number to a pair of dice values.
; Timer cycles between 0 and 35, Loads xDV and xDP with numbers from 1 to 6.

GetRoll:

   push     tmp               ; Save tmp to stack

   ldi      ZH, HIGH(DiceTable*2)   ; Point Z to DiceTable. Note that you must
   ldi      ZL, LOW(DiceTable*2)    ; multiply the table reference by 2.
   
   in       tmp, TCNT0        ; Read clock/timer
   lsl      tmp               ; Multiply number by 2
 
   add      ZL, tmp           ; Add offset to Z register
   clr      tmp               ; If there was a carry,
   adc      ZH, tmp           ; add the carry to ZH.

   lpm      LDV, Z+           ; Get first value according to the clock
   lpm      RDV, Z            ; Get second value according to the clock

   pop      tmp               ; Get tmp back from the stack
   ret


; -------------------------------------
; GetDiePatterns
;
; Load die patterns LDP and RDP according to numeric die values
; in LDV and RDV.


GetDiePatterns:

   cpi      LDV, 0            ; Is it a 0?
   brne     LNOT0             ; Jump ahead if not
   ldi      LDP, LD0          ; Otherwise load the "0" pattern
   rjmp     GETRPATTERN       ; Go to right die
LNOT0:
   cpi      LDV, 1
   brne     LNOT1
   ldi      LDP, LD1
   rjmp     GETRPATTERN
LNOT1:
   cpi      LDV, 2
   brne     LNOT2
   ldi      LDP, LD2
   rjmp     GETRPATTERN
LNOT2:
   cpi      LDV, 3
   brne     LNOT3
   ldi      LDP, LD3
   rjmp     GETRPATTERN
LNOT3:
   cpi      LDV, 4
   brne     LNOT4
   ldi      LDP, LD4
   rjmp     GETRPATTERN
LNOT4:
   cpi      LDV, 5
   brne     LNOT5
   ldi      LDP, LD5
   rjmp     GETRPATTERN
LNOT5:
   cpi      LDV, 6
   brne     LNOT6
   ldi      LDP, LD6
   rjmp     GETRPATTERN
LNOT6:
   cpi      LDV, 7
   brne     LNOT7
   ldi      LDP, LD7
   rjmp     GETRPATTERN
LNOT7:                        ; Animation patterns
   cpi      LDV, 10
   brne     LNOT10
   ldi      LDP, LDA0
   rjmp     GetRPattern
LNOT10:
   cpi      LDV, 11
   brne     LNOT11
   ldi      LDP, LDA1
   rjmp     GetRPattern
LNOT11:
   cpi      LDV, 12
   brne     LNOT12
   ldi      LDP, LDA2
   rjmp     GetRPattern
LNOT12:
   cpi      LDV, 13
   brne     LNOT13
   ldi      LDP, LDA3
   rjmp     GetRPattern
LNOT13:
   ldi      LDP, LD7          ; 7 dots most likely means LDV out of range

GETRPATTERN:
   cpi      RDV, 0            ; Is it a 0?
   brne     RNOT0             ; Jump ahead if not
   ldi      RDP, RD0          ; Otherwise load the "0" pattern
   ret                        ; Go to right die
RNOT0:
   cpi      RDV, 1            ; Is right die a 0?
   brne     RNOT1             ; Jump ahead if not
   ldi      RDP, RD1          ; Otherwise load the "0" pattern
   ret                        ; return from call
RNOT1:
   cpi      RDV, 2
   brne     RNOT2
   ldi      RDP, RD2
   ret
RNOT2:
   cpi      RDV, 3
   brne     RNOT3
   ldi      RDP, RD3
   ret
RNOT3:
   cpi      RDV, 4
   brne     RNOT4
   ldi      RDP, RD4
   ret
RNOT4:
   cpi      RDV, 5
   brne     RNOT5
   ldi      RDP, RD5
   ret
RNOT5:
   cpi      RDV, 6
   brne     RNOT6
   ldi      RDP, RD6
   ret
RNOT6:
   cpi      RDV, 7
   brne     RNOT7
   ldi      RDP, RD7
   ret
RNOT7:
   cpi      RDV, 10           ; Animation patterns
   brne     RNOT10
   ldi      RDP, RDA0
   ret
RNOT10:
   cpi      RDV, 11
   brne     RNOT11
   ldi      RDP, RDA1
   ret
RNOT11:
   cpi      RDV, 12
   brne     RNOT12
   ldi      RDP, RDA2
   ret
RNOT12:
   cpi      RDV, 13
   brne     RNOT13
   ldi      RDP, RDA3
   ret
RNOT13:
   ldi      RDP, RD7          ; 7 most likely means RDP out of range
   ret


; -------------------------------------
; End of Program
; -------------------------------------
