; BlueBrightnessControl4 - brightness control module for 4 230V AC lamps
; version 0.6.2 date 2007-03-23
; Copyright (C) 2006 Stefan Schuermans <stefan@blinkenarea.org>
; a BlinkenArea project - http://www.blinkenarea.org/

; clock frequency: 8 MHz (calibrated internal RC oscillator)

; PA0: unused (output, low)
; PA1: unused (output, low)
; PA2: reset
; PB0: input from button (input, pull-up enabled, active low)
; PB1: output to status LED (output, active high)
; PB2: output to lamp 1 (output, active low)
; PB3: output to lamp 3 (output, active low)
; PB4: output to lamp 4 (output, active low)
; PB5: programming clock (input, pull-up enabled)
; PB6: programming data output (output, low)
; PB7: programming data input (input, pull-up enabled)
; PD0: serial data input (input, pull-up disabled)
; PD1: unused (output, low)
; PD2: unused (output, low)
; PD3: unused (output, low)
; PD4: unused (output, low)
; PD5: output to lamp 2 (output, active low)
; PD6: unused (output, low)

; serial input: 4800,8,N,1
;   <message> := <control> [<value 1> <value 2> <value 3> <value4>]
;   <control> := control byte (0x80..0xFF)
;                0x80 all lamps off (no values must be present)
;                0x81 linear mapping from grayscale to PWM values
;                0x82 table based mapping from grayscale to PWM values
;   <value N> := grayscale value for lamp N (0x00..0x7F)

; output is done using the 4 PWM channels and using 8 bit fast PWM mode
;   the first PWM output of each timer is inverted and is filled with 0x00 for off and 0xFF for on (outputs are active low)
;   the second PWM output of each timer is not inverted and is filled with 0xFF for off and 0x00 for on (outputs are active low)
;   this results in a phase shift of 1/2 period between the two output of each timer
;   the timers are set up to be phase shifted 1/4 period
;   this results in a phase shift of 1/4 between two outputs (in this order OC0A, OC1A, OC0B, OC1B)



.INCLUDE        "tn2313def.inc"



; IO pins
.equ    PIN_N_BUTTON            =       PINB    ; input from button (active low)
.equ    BIT_N_BUTTON            =       0
.equ    PORT_STATUS             =       PORTB   ; output to status LED (active high)
.equ    BIT_STATUS              =       1
.equ    PORT_N_PWM0             =       PORTB   ; 1st PWM output (active low)
.equ    BIT_N_PWM0              =       2
.equ    PORT_N_PWM1             =       PORTD   ; 2nd PWM output (active low)
.equ    BIT_N_PWM1              =       5
.equ    PORT_N_PWM2             =       PORTB   ; 3rd PWM output (active low)
.equ    BIT_N_PWM2              =       3
.equ    PORT_N_PWM3             =       PORTB   ; 4th PWM output (active low)
.equ    BIT_N_PWM3              =       4
.equ    PIN_SER_IN              =       PIND    ; serial input
.equ    BIT_SER_IN              =       0



; constants

; maximum message length
.equ    MSG_LEN_MAX             =       5

; theshold value for button state (debouncing time in milliseconds)
.equ    BUTTON_THR              =       80

; time the button has priority over the serial data after a press (in 100 milliseconds)
.equ    BUTTON_TIME             =       50

; time of idle serial port that is recognized as a timeout (in 100 milliseconds)
.equ    SER_TIMEOUT             =       50

; synchronization trend limit (value the sync trend must reach before adapting RC oscillator, 20..120)
.equ    SYNC_TREND_LIMIT        =       120



; number of byte to receive next on serial port
.def    RECV_CNT                =       r0

; received data byte
.def    RECV_DATA               =       r16

; counter for detecting milliseconds
.def    MS_CNT                  =       r1

; counter for detecting 100 milliseconds
.def    MS100_CNT               =       r2

; state of button (0=released ... 2*BUTTON_THR=full pressed)
.def    BUTTON_STATE            =       r17

; number of lighting mode that will be enabled at next press of button
.def    BUTTON_MODE             =       r18

; time in 100 milliseconds the button has priority over the serial port
.def    BUTTON_PRIO             =       r3

; time in 100 milliseconds the serial port has been idle
.def    SER_IDLE                =       r4

; number of times to blink in current status output cycle
.def    STATUS_BLINK            =       r5

; counter for status output intervals
.def    STATUS_CNT              =       r6

; tick counter for animation mode
.def    ANIM_TICKS              =       r7

; frame counter for animation mode
.def    ANIM_FRAME              =       r8

; last state of serial port (0x00 for LOW, 0xFF for HIGH) (used for sync)
.def    SER_LAST_STATE          =       r9

; number of times serial port state did not change (used for sync)
.def    SER_UNCHANGED_CNT       =       r10

; trend if running too slow or too fast (compared to speed of serial signal)
.def    SER_SYNC_TREND          =       r11

; variable for bits
.def    BITS                    =       r19
;   animation mode enabled
.equ    BIT_ANIM_ENAB           =       0

; general purpose registers
;   NOT for usage in interrupt
.def    TMP                     =       r20

; registers for interrupts (or when interrupts are disabled)
.def    I_SREG                  =       r12     ; interrupt backup register for SREG
.def    I_TMP                   =       r21     ; general purpose registers for interrupt
.def    I_CNT                   =       r22



.DSEG
.ORG    0x060



; buffer for data bytes received on serial port
RECV_BUFFER:            .BYTE   MSG_LEN_MAX

; buffer for current PWM values (0x00..0xFF)
PWM_CUR:                .BYTE   4



.CSEG
.ORG    0x000
        rjmp    ENTRY                           ; RESET
        reti                                    ; INT0
        reti                                    ; INT1
        reti                                    ; TIMER1_CAPT
        reti                                    ; TIMER1_COMPA
        reti                                    ; TIMER1_OVF
        reti                                    ; TIMER0_OVF
        rjmp    SER_RECV                        ; USART0_RX
        reti                                    ; USART0_UDRE
        reti                                    ; USART0_TX
        reti                                    ; ANALOG_COMP
        reti                                    ; PC_INT
        reti                                    ; TIMER1_COMPB
        reti                                    ; TIMER0_COMPA
        reti                                    ; TIMER0_COMPB
        reti                                    ; USI_START
        reti                                    ; USI_OVERFLOW
        reti                                    ; EE_READY
        reti                                    ; WDT



; code entry point
ENTRY:
; set system clock prescaler to 1:1
        ldi     TMP,1<<CLKPCE
        out     CLKPR,TMP
        ldi     TMP,0
        out     CLKPR,TMP
; initialize output ports
        ldi     TMP,0x00                        ; PA[01] to output, low
        out     PORTA,TMP
        ldi     TMP,0x02
        out     DDRA,TMP
        ldi     TMP,0xBD                        ; PB[16] to output, low - PB[2-4] to output, high - PB[057] to input, pull-up enabled
        out     PORTB,TMP
        ldi     TMP,0x5E
        out     DDRB,TMP
        ldi     TMP,0x20                        ; PD[1-46] to output, low - PD5 to output, high - PD0 to input, pull-up disabled
        out     PORTD,TMP
        ldi     TMP,0x7E
        out     DDRD,TMP
; initialize stack pointer
        ldi     TMP,RAMEND
        out     SPL,TMP
; enable watchdog (64ms)
        wdr
        ldi     TMP,1<<WDCE|1<<WDE
        out     WDTCR,TMP
        ldi     TMP,1<<WDE|1<<WDP1
        out     WDTCR,TMP
        wdr
; disable analog comparator
        ldi     TMP,1<<ACD
        out     ACSR,TMP
; set up timer 0
        ldi     TMP,1<<WGM01|1<<WGM00           ; timer 0: fast PWM mode PWM
        out     TCCR0A,TMP
        ldi     TMP,1<<CS00                     ; timer 0: fast PWM mode, system clock, prescaler 1:1
        out     TCCR0B,TMP
; set up timer 1
        ldi     TMP,1<<WGM10                    ; timer 1: fast 8 bit PWM mode PWM
        out     TCCR1A,TMP
        ldi     TMP,1<<WGM12|1<<CS10            ; timer 1: fast 8 bit PWM mode, system clock, prescaler 1:1
        out     TCCR1B,TMP
        ldi     TMP,0
        out     TCCR1C,TMP
; disable timer interrupts
        ldi     TMP,0                           ; no timer interrupts (for timer 0 and timer 1)
        out     TIMSK,TMP
; sync timers to be separated by 1/4 period
        ldi     TMP,0
        out     TCNT0,TMP
        out     TCNT1H,TMP
        ldi     TMP,0x43                        ; 3 cycles already elapsed since write to TCNT0
        out     TCNT1L,TMP
; set up usart
;   transmitter disabled
;   receiver enabled
;   receive interrupt enabled
;   4800 baud
;   8N1
; enable usart
        ldi     TMP,0                           ; single speed
        out     UCSRA,TMP
        ldi     TMP,1<<RXCIE|1<<RXEN            ; only receiver enabled, receive interrupt enabled
        out     UCSRB,TMP
        ldi     TMP,1<<UCSZ1|1<<UCSZ0           ; asynchronous, 8N1: 8 data bits, no parity, 1 stop bit
        out     UCSRC,TMP
        ldi     TMP,0                           ; speed = Fosc / (16 * (UBRR + 1))
        out     UBRRH,TMP                       ;       = 8MHz / (16 * (103 + 1))
        ldi     TMP,103                         ;       = 4808bps (4800bps, 0.16% deviation)
        out     UBRRL,TMP
; jump to main program
        rjmp    MAIN



; table for mapping from grayscale values to PWM values
.include "mapping.gen.inc"



; turn on/off PWM machinery and write current PWM values to PWM registers
;  - must be called with interrupts disabled
;  - changes: Z, I_TMP
PWM_OUT:
; set outputs
;  - needed in case pin is disconnected from PWM in next step
        ldi     ZH,HIGH(PWM_CUR)                ; base address
        ldi     ZL,LOW(PWM_CUR)
        ld      I_TMP,Z+                        ; 1st
        sbrs    I_TMP,7
        sbi     PORT_N_PWM0,BIT_N_PWM0
        sbrc    I_TMP,7
        cbi     PORT_N_PWM0,BIT_N_PWM0
        ld      I_TMP,Z+                        ; 2nd
        sbrs    I_TMP,7
        sbi     PORT_N_PWM1,BIT_N_PWM1
        sbrc    I_TMP,7
        cbi     PORT_N_PWM1,BIT_N_PWM1
        ld      I_TMP,Z+                        ; 3rd
        sbrs    I_TMP,7
        sbi     PORT_N_PWM2,BIT_N_PWM2
        sbrc    I_TMP,7
        cbi     PORT_N_PWM2,BIT_N_PWM2
        ld      I_TMP,Z+                        ; 4th
        sbrs    I_TMP,7
        sbi     PORT_N_PWM3,BIT_N_PWM3
        sbrc    I_TMP,7
        cbi     PORT_N_PWM3,BIT_N_PWM3
; output PWM values to PWM registers
        ldi     ZH,HIGH(PWM_CUR)                ; base address
        ldi     ZL,LOW(PWM_CUR)
        ld      I_TMP,Z+                        ; 1st
        out     OCR0A,I_TMP
        ld      I_TMP,Z+                        ; 2nd (invert: 0xFF -> 0x00, other -> 0xFE - other)
        com     I_TMP
        breq    PWM_OUT_2INV
        dec     I_TMP
PWM_OUT_2INV:
        out     OCR0B,I_TMP
        ld      I_TMP,Z+                        ; 3rd
        out     OCR1AL,I_TMP
        ld      I_TMP,Z+                        ; 4th (invert: 0xFF -> 0x00, other -> 0xFE - other)
        com     I_TMP
        breq    PWM_OUT_4INV
        dec     I_TMP
PWM_OUT_4INV:
        out     OCR1BL,I_TMP
; (dis)connect pins from/to PWM
        ldi     ZH,HIGH(PWM_CUR)                ; base address
        ldi     ZL,LOW(PWM_CUR)
        ld      I_TMP,Z+                        ; 1st
        cpi     I_TMP,0x00
        breq    PWM_OUT_0
        cpi     I_TMP,0xFF
        breq    PWM_OUT_0
        in      I_TMP,TCCR0A                    ; 1st: (inverted) PWM
        sbr     I_TMP,1<<COM0A1|1<<COM0A0
        out     TCCR0A,I_TMP
        rjmp    PWM_OUT_0_END
PWM_OUT_0:
        in      I_TMP,TCCR0A                    ; 1st: no (inverted) PWM
        cbr     I_TMP,1<<COM0A1|1<<COM0A0
        out     TCCR0A,I_TMP
PWM_OUT_0_END:
        ld      I_TMP,Z+                        ; 2nd
        cpi     I_TMP,0x00
        breq    PWM_OUT_1
        cpi     I_TMP,0xFF
        breq    PWM_OUT_1
        in      I_TMP,TCCR0A                    ; 2nd: PWM
        sbr     I_TMP,1<<COM0B1
        out     TCCR0A,I_TMP
        rjmp    PWM_OUT_1_END
PWM_OUT_1:
        in      I_TMP,TCCR0A                    ; 2nd: no PWM
        cbr     I_TMP,1<<COM0B1
        out     TCCR0A,I_TMP
PWM_OUT_1_END:
        ld      I_TMP,Z+                        ; 3rd
        cpi     I_TMP,0x00
        breq    PWM_OUT_2
        cpi     I_TMP,0xFF
        breq    PWM_OUT_2
        in      I_TMP,TCCR1A                    ; 3rd: (inverted) PWM
        sbr     I_TMP,1<<COM1A1|1<<COM1A0
        out     TCCR1A,I_TMP
        rjmp    PWM_OUT_2_END
PWM_OUT_2:
        in      I_TMP,TCCR1A                    ; 3rd: no (inverted) PWM
        cbr     I_TMP,1<<COM1A1|1<<COM1A0
        out     TCCR1A,I_TMP
PWM_OUT_2_END:
        ld      I_TMP,Z+                        ; 4th
        cpi     I_TMP,0x00
        breq    PWM_OUT_3
        cpi     I_TMP,0xFF
        breq    PWM_OUT_3
        in      I_TMP,TCCR1A                    ; 4th: PWM
        sbr     I_TMP,1<<COM1B1
        out     TCCR1A,I_TMP
        rjmp    PWM_OUT_3_END
PWM_OUT_3:
        in      I_TMP,TCCR1A                    ; 4th: no PWM
        cbr     I_TMP,1<<COM1B1
        out     TCCR1A,I_TMP
PWM_OUT_3_END:
; done
        ret



; reception (or receive error) on serial port
;  - serial receive interrupt
SER_RECV:
; interrupt entry
        in      I_SREG,SREG
        push    XH
        push    XL
        push    YH
        push    YL
        push    ZH
        push    ZL
; check for reception
        sbis    UCSRA,RXC
        rjmp    SER_RECV_END                    ; nothing received
; check for errors
        sbic    UCSRA,FE
        rjmp    SER_RECV_ERR                    ; frame error
        sbic    UCSRA,DOR
        rjmp    SER_RECV_ERR                    ; data overrun
; fetch received byte
        in      RECV_DATA,UDR
; uppermost bit set -> first byte of message (control byte)
        sbrc    RECV_DATA,7
        clr     RECV_CNT
; do nothing if behind maximum message length
        ldi     I_TMP,MSG_LEN_MAX
        cp      RECV_CNT,I_TMP
        brsh    SER_RECV_END
; store received message byte
        ldi     ZH,HIGH(RECV_BUFFER)            ; base address of message buffer
        ldi     ZL,LOW(RECV_BUFFER)
        clr     I_TMP                           ; add grayscale number as offset
        add     ZL,RECV_CNT
        adc     ZH,I_TMP
        st      Z,RECV_DATA                     ; store received message byte
; next byte
        inc     RECV_CNT
; check for complete message
        lds     RECV_DATA,RECV_BUFFER+0         ; get control byte
        cpi     RECV_DATA,0x80                  ; turn off all lamps
        brne    SER_RECV_NOT_80
        ldi     I_TMP,1                         ; check if complete
        cp      RECV_CNT,I_TMP
        brlo    SER_RECV_END
        ldi	I_TMP,0xFF			; message completed
	mov     RECV_CNT,I_TMP
        rjmp    SER_RECV_OFF			; process off command
SER_RECV_NOT_80:
        cpi     RECV_DATA,0x81                  ; linear mapped grayscales
        brne    SER_RECV_NOT_81
        ldi     I_TMP,5                         ; check if complete
        cp      RECV_CNT,I_TMP
        brlo    SER_RECV_END
        ldi	I_TMP,0xFF			; message completed
	mov     RECV_CNT,I_TMP
        rjmp    SER_RECV_LINEAR			; process linear greyscales
SER_RECV_NOT_81:
        cpi     RECV_DATA,0x82                  ; table based mapped grayscales
        brne    SER_RECV_NOT_82
        ldi     I_TMP,5                         ; check if complete
        cp      RECV_CNT,I_TMP
        brlo    SER_RECV_END
        ldi	I_TMP,0xFF			; message completed
	mov     RECV_CNT,I_TMP
        rjmp    SER_RECV_TABLE			; process table based mapped greyscales
SER_RECV_NOT_82:
        ldi	I_TMP,0xFF			; message completed
	mov     RECV_CNT,I_TMP
        rjmp    SER_RECV_END

; receive error
SER_RECV_ERR:
        in      I_TMP,UDR                       ; read data from USART to clear error
SER_RECV_END:
; interrupt exit
        pop     ZL
        pop     ZH
        pop     YL
        pop     YH
        pop     XL
        pop     XH
        out     SREG,I_SREG
        reti

; turn off all lamps
SER_RECV_OFF:
        clr     SER_IDLE                        ; serial port is not idle
        cbr     BITS,1<<BIT_ANIM_ENAB           ; diable animation mode (on serial timeout)
        ldi     YH,HIGH(PWM_CUR)                ; base address current PWM values
        ldi     YL,LOW(PWM_CUR)
        clr     I_TMP                           ; set all to 0x00
        st      Y+,I_TMP
        st      Y+,I_TMP
        st      Y+,I_TMP
        st      Y+,I_TMP
        rjmp    SER_RECV_OUTPUT                 ; output new PWM values

; linear mapped grayscales
SER_RECV_LINEAR:
        clr     SER_IDLE                        ; serial port is not idle
        sbr     BITS,1<<BIT_ANIM_ENAB           ; enable animation mode (on serial timeout)
        ldi     ZH,HIGH(RECV_BUFFER+1)          ; base address received grayscales
        ldi     ZL,LOW(RECV_BUFFER+1)
        ldi     YH,HIGH(PWM_CUR)                ; base address current PWM values
        ldi     YL,LOW(PWM_CUR)
        ldi     I_CNT,4                         ; process 4 lamps
SER_RECV_LINEAR_LOOP:
        ld      I_TMP,Z+                        ; expand grayscale value from 7 to 8 bits
        lsl     I_TMP
        sbrc    I_TMP,7
        sbr     I_TMP,1<<0
        st      Y+,I_TMP
        dec     I_CNT                           ; loop
        brne    SER_RECV_LINEAR_LOOP
        rjmp    SER_RECV_OUTPUT                 ; output new PWM values

; table based mapped grayscales
SER_RECV_TABLE:
        clr     SER_IDLE                        ; serial port is not idle
        sbr     BITS,1<<BIT_ANIM_ENAB           ; enable animation mode (on serial timeout)
        ldi     YH,HIGH(RECV_BUFFER+1)          ; base address received grayscales
        ldi     YL,LOW(RECV_BUFFER+1)
        ldi     XH,HIGH(PWM_CUR)                ; base address current PWM values
        ldi     XL,LOW(PWM_CUR)
        ldi     I_CNT,4                         ; process 4 lamps
SER_RECV_TABLE_LOOP:
        ld      I_TMP,Y+                        ; translate grayscale value using mapping table
        ldi     ZH,HIGH(MAPPING_TABLE*2)
        ldi     ZL,LOW(MAPPING_TABLE*2)
        add     ZL,I_TMP
        ldi     I_TMP,0
        adc     ZH,I_TMP
        lpm     I_TMP,Z
        st      X+,I_TMP
        dec     I_CNT                           ; loop
        brne    SER_RECV_TABLE_LOOP
        rjmp    SER_RECV_OUTPUT                 ; output new PWM values

; output new grayscale value
SER_RECV_OUTPUT:
; ignore received grayscales if button has got priority
        mov     I_TMP,BUTTON_PRIO               ; branch if not 0
        cpi     I_TMP,0
        brne    SER_RECV_END
; write current PWM values to PWM registers
        rcall   PWM_OUT
        rjmp    SER_RECV_END



; lighting modes to be activated on button press
;   - format: <PWM lamp 1> <PWM lamp 2> <PWM lamp 3> <PWM lamp 4>
BUTTON_MODES:
        .DB     0x1F, 0x1F, 0x1F, 0x1F, 0x01, 0x00      ; all a little bit on, animation mode enabled
        .DB     0x7F, 0x7F, 0x7F, 0x7F, 0x01, 0x00      ; all half on, animation mode enabled
        .DB     0xDF, 0xDF, 0xDF, 0xDF, 0x01, 0x00      ; all almost full on, animation mode enabled
        .DB     0x7F, 0x00, 0x00, 0x00, 0x01, 0x00      ; 1st half on, animation mode enabled
        .DB     0x00, 0x7F, 0x00, 0x00, 0x01, 0x00      ; 2nd half on, animation mode enabled
        .DB     0x00, 0x00, 0x7F, 0x00, 0x01, 0x00      ; 3st half on, animation mode enabled
        .DB     0x00, 0x00, 0x00, 0x7F, 0x01, 0x00      ; 4nd half on, animation mode enabled
        .DB     0x00, 0x00, 0x00, 0x00, 0x00, 0x00      ; full off, animation mode disabled
BUTTON_MODES_END:



; button has just been pressed
;  - must be called with interrupts enabled
;  - changes: BUTTON_MODE, BUTTON_PRIO, BIT_ANIM_ENAB, Y, Z, TMP
BUTTON_PRESS:
; disable interrupts
        cli
; get address of lighting mode data
        ldi     ZH,HIGH(BUTTON_MODES*2)         ; base address of lighting mode table
        ldi     ZL,LOW(BUTTON_MODES*2)
        mov     TMP,BUTTON_MODE                 ; multiply mode with 6 to get offset
        lsl     TMP
	add	TMP,BUTTON_MODE
        lsl     TMP
        add     ZL,TMP                          ; add offset
        ldi     TMP,0
        adc     ZH,TMP
; next lighting mode
        inc     BUTTON_MODE
        cpi     BUTTON_MODE,(BUTTON_MODES_END*2-BUTTON_MODES*2)/6
        brlo    BUTTON_MODE_NEXT_DONE
        clr     BUTTON_MODE
BUTTON_MODE_NEXT_DONE:
; set new PWM values
        ldi     YH,HIGH(PWM_CUR)                ; base address of current PWM values
        ldi     YL,LOW(PWM_CUR)
        lpm     TMP,Z+                          ; use lighting mode data as current PWM values
        st      Y+,TMP
        lpm     TMP,Z+
        st      Y+,TMP
        lpm     TMP,Z+
        st      Y+,TMP
        lpm     TMP,Z+
        st      Y+,TMP
; enable or disable animation mode
        lpm     TMP,Z+
        sbrs    TMP,0                           ; flag bit 0 cleared -> disable animation mode
        cbr     BITS,1<<BIT_ANIM_ENAB
        sbrc    TMP,0                           ; flag bit 0 set -> enable animation mode
        sbr     BITS,1<<BIT_ANIM_ENAB
; write current PWM values to PWM registers
        rcall   PWM_OUT
; give button priority over serial data for configured time
        ldi     TMP,BUTTON_TIME
        mov     BUTTON_PRIO,TMP
; enable interrupts
        sei
        ret



; button processing
;  - must be called with interrupts enabled
;  - changes: BUTTON_STATE, BUTTON_MODE, BUTTON_PRIO, BIT_ANIM_ENAB, Y, Z, TMP
BUTTON_PROC:
; check if button input is active
        sbis    PIN_N_BUTTON,BIT_N_BUTTON
        rjmp    BUTTON_IN_ACTIVE                ; jump if button input active
; if button state is below BUTTON_THR, keep it at "full released"
        cpi     BUTTON_STATE,BUTTON_THR
        brsh    BUTTON_RELEASED_DEC
        ldi     BUTTON_STATE,0
        ret

BUTTON_RELEASED_DEC:
; decrement button state
        dec     BUTTON_STATE
; if button state is now BUTTON_THR-1 the button has been released
        cpi     BUTTON_STATE,BUTTON_THR-1
        breq    BUTTON_RELEASED_NOW
        ret

BUTTON_RELEASED_NOW:
; set button state to "full released"
        ldi     BUTTON_STATE,0
; do actions to do on button release
        ret

BUTTON_IN_ACTIVE:
; if button state is already at least BUTTON_THR, keep it at "full pressed"
        cpi     BUTTON_STATE,BUTTON_THR
        brlo    BUTTON_PRESSED_INC
        ldi     BUTTON_STATE,2*BUTTON_THR
        ret

BUTTON_PRESSED_INC:
; increment button state
        inc     BUTTON_STATE
; if button state is now BUTTON_THR the button has been pressed
        cpi     BUTTON_STATE,BUTTON_THR
        breq    BUTTON_PRESSED_NOW
        ret

BUTTON_PRESSED_NOW:
; set button state to "full pressed"
        ldi     BUTTON_STATE,2*BUTTON_THR
; do actions to do on button press
        rcall   BUTTON_PRESS
        ret



; button priority processing
;  - must be called with interrupts enabled
;  - changes: BUTTON_PRIO, TMP
BUTTON_PRIO_PROC:
; decrease BUTTON_PRIO if greater than 0
        cli                                     ; disable interrupts
        mov     TMP,BUTTON_PRIO                 ; branch if 0
        cpi     TMP,0
        breq    BUTTON_PRIO_PROC_NO_DEC
        dec     BUTTON_PRIO                     ; decrement
BUTTON_PRIO_PROC_NO_DEC:
        sei                                     ; enable interrupts
; done
        ret



; serial port timeout detection
;  - must be called with interrupts enabled
;  - changes: SER_IDLE, TMP
SER_TIMEOUT_DETECT:
        ldi     TMP,SER_TIMEOUT                 ; check if timeout
        cp      SER_IDLE,TMP
        brsh    SER_TIMEOUT_DETECT_NO_INC       ;   branch if timeout already detected
        inc     SER_IDLE                        ; increment serial port idle time
SER_TIMEOUT_DETECT_NO_INC:
; done
        ret



; sync to timing of serial line
;  - must be called every 32 microseconds
;  - must be called with interrupts enabled
SER_SYNC:
; sample serial input
        clr     TMP
        sbic    PIN_SER_IN,BIT_SER_IN
        dec     TMP
; check if state of serial input changed
        cp      TMP,SER_LAST_STATE
        brne    SER_SYNC_CHANGE                 ; state changed
; state of serial input did not change
        sbrs    SER_UNCHANGED_CNT,7             ; count number of times the state did not change (limit count at 0x80)
        inc     SER_UNCHANGED_CNT
        ret                                     ; done
SER_SYNC_CHANGE:
; state of serial input changed
;  - 4800 bps -> 208.33 us per bit -> 6.51 samples per bit (sampled every 32 us)
;  - state unchanged 5 or 6 times in a row (both with about 50% probability) for single bits on serial line
;  - conclusion:
;    - <= 3 unchanged samples -> wrong measurement
;    - 4 or 5 unchanged samples -> running too slow
;    - 6 or 7 unchanged samples -> running too fast
;    - >= 8 unchanged samples -> wrong measurement
        mov     SER_LAST_STATE,TMP              ; save new state
        mov     TMP,SER_UNCHANGED_CNT           ; save unchanged count
        clr     SER_UNCHANGED_CNT               ; reset unchanged count
        cpi     TMP,5
        breq    SER_SYNC_SLOW                   ; running too slow
        cpi     TMP,6
        breq    SER_SYNC_FAST                   ; running too fast
        cpi     TMP,4
        breq    SER_SYNC_SLOW                   ; running too slow
        cpi     TMP,7
        breq    SER_SYNC_FAST                   ; running too fast
        ret                                     ; wrong measurement -> done
; accumulate trend if running too slow or too fast
SER_SYNC_SLOW:
        dec     SER_SYNC_TREND
        rjmp    SER_SYNC_TREND_REACT
SER_SYNC_FAST:
        inc     SER_SYNC_TREND
; react if trend gets too low or too high
SER_SYNC_TREND_REACT:
        mov    TMP,SER_SYNC_TREND
        cpi    TMP,SYNC_TREND_LIMIT+1
        brge   SER_SYNC_RC_SLOWER               ; configure to run slower (running too fast at the moment)
        mov    TMP,SER_SYNC_TREND
        cpi    TMP,-SYNC_TREND_LIMIT
        brlt   SER_SYNC_RC_FASTER               ; configure to run faster (running too slow at the moment)
        ret                                     ; done
; configure RC oscillator to run slower
SER_SYNC_RC_SLOWER:
        clr     SER_SYNC_TREND                  ; reset trend
        cli                                     ; disable interrupts
        in      TMP,OSCCAL                      ; read calibration value
        cpi     TMP,0x00+1                      ; decrement if not yet at minimum
        brlo    SER_SYNC_RC_SLOWER_MIN
        dec     TMP
        out     OSCCAL,TMP                      ; write new calibration value
SER_SYNC_RC_SLOWER_MIN:
        sei                                     ; enable interrupts
        ret                                     ; done
; configure RC oscillator to run faster
SER_SYNC_RC_FASTER:
        clr     SER_SYNC_TREND                  ; reset trend
        cli                                     ; disable interrupts
        in      TMP,OSCCAL                      ; read calibration value
        cpi     TMP,0x7F                        ; increment if not yet at maximum
        brsh    SER_SYNC_RC_FASTER_MAX
        inc     TMP
        out     OSCCAL,TMP                      ; write new calibration value
SER_SYNC_RC_FASTER_MAX:
        sei                                     ; enable interrupts
        ret                                     ; done



; get number of times to blink in current status output cycle
;  - must be called with interrupts enabled
;  - changes: STATUS_BLINK, TMP
STATUS_GET_BLINK:
; blink 1 time by default
        ldi     TMP,1
        mov     STATUS_BLINK,TMP
; blink 2 times in case of serial timeout
        ldi     TMP,SER_TIMEOUT
        cp      SER_IDLE,TMP
        brlo    STATUS_GET_BLINK_NO_SER_TIMEOUT
        ldi     TMP,2
        mov     STATUS_BLINK,TMP
STATUS_GET_BLINK_NO_SER_TIMEOUT:
; blink 3 times if button has got priority
        ldi     TMP,0
        cp      BUTTON_PRIO,TMP
        breq    STATUS_GET_BLINK_NO_BUTTON_PRIO
        ldi     TMP,3
        mov     STATUS_BLINK,TMP
STATUS_GET_BLINK_NO_BUTTON_PRIO:
; done
        ret



; status output
;  - must be called with interrupts enabled
;  - changes: STATUS_BLINK, STATUS_CNT, TMP
STATUS_OUT:
; count status output intervals
        inc     STATUS_CNT                      ; count from 0 to 15
        ldi     TMP,16
        cp      STATUS_CNT,TMP
        brlo    STATUS_OUT_NO_CLR
        clr     STATUS_CNT
        rcall   STATUS_GET_BLINK                ; get number of times to blink in current status output cycle
STATUS_OUT_NO_CLR:
; turn status LED on or off
        cbi     PORT_STATUS,BIT_STATUS          ; turn off status LED
        sbrc    STATUS_CNT,1                    ; status LED can only be turned on in steps 0, 1, 4, 5, ...
        rjmp    STATUS_OUT_OFF
        mov     TMP,STATUS_CNT                  ; get number of step divided by 4
        lsr     TMP
        lsr     TMP
        cp      TMP,STATUS_BLINK                ; status LED on if smaller than the number of times to blink
        brsh    STATUS_OUT_OFF
        sbi     PORT_STATUS,BIT_STATUS          ; turn on status LED
STATUS_OUT_OFF:
; done
        ret



; animation mode data
;   - format: <PWM lamp 1> <PWM lamp 2> <PWM lamp 3> <PWM lamp 4> <ticks (100ms)> <reserved>
ANIM_DATA:
; off
;       .DB     0x00, 0x00, 0x00, 0x00, 0x1E, 0x00      ; off, 3.0s
; 1 - 4
;       .DB     0x7F, 0x00, 0x00, 0x00, 0x05, 0x00      ; 1st half on, 0.5s
;       .DB     0x00, 0x00, 0x00, 0x00, 0x1E, 0x00      ; off, 3.0s
;       .DB     0x00, 0x7F, 0x00, 0x00, 0x05, 0x00      ; 2nd half on, 0.5s
;       .DB     0x00, 0x00, 0x00, 0x00, 0x1E, 0x00      ; off, 3.0s
;       .DB     0x00, 0x00, 0x7F, 0x00, 0x05, 0x00      ; 3st half on, 0.5s
;       .DB     0x00, 0x00, 0x00, 0x00, 0x1E, 0x00      ; off, 3.0s
;       .DB     0x00, 0x00, 0x00, 0x7F, 0x05, 0x00      ; 4nd half on, 0.5s
;       .DB     0x00, 0x00, 0x00, 0x00, 0x1E, 0x00      ; off, 3.0s
; KITT
;       .DB     0xFF, 0x5F, 0x3F, 0x00, 0x02, 0x00
;       .DB     0x5F, 0xFF, 0x00, 0x00, 0x02, 0x00
;       .DB     0x1F, 0x5F, 0xFF, 0x00, 0x02, 0x00
;       .DB     0x00, 0x1F, 0x5F, 0xFF, 0x02, 0x00
;       .DB     0x00, 0x00, 0xFF, 0x5F, 0x02, 0x00
;       .DB     0x00, 0xFF, 0x5F, 0x1F, 0x02, 0x00
; round running light
       .DB     0xFF, 0x00, 0x3F, 0x5F, 0x03, 0x00
       .DB     0x5F, 0xFF, 0x00, 0x3F, 0x03, 0x00
       .DB     0x3F, 0x5F, 0xFF, 0x00, 0x03, 0x00
       .DB     0x00, 0x3F, 0x5F, 0xFF, 0x03, 0x00
ANIM_DATA_END:



; animation mode
;  - must be called with interrupts enabled
;  - changes: ANIM_TICKS, ANIM_FRAME, Y, Z, TMP
ANIM_MODE:
; exit if no serial timeout (i.e. no animation mode)
        ldi     TMP,SER_TIMEOUT
        cp      SER_IDLE,TMP
        brsh    ANIM_MODE_SER_TIMEOUT
        ret
ANIM_MODE_SER_TIMEOUT:
; exit if button has got priority
        mov     TMP,BUTTON_PRIO
        cpi     TMP,0
        breq    ANIM_MODE_BUTTON_NO_PRIO
        ret
ANIM_MODE_BUTTON_NO_PRIO:
; check if anim mode is enabled
        sbrs    BITS,BIT_ANIM_ENAB
        rjmp    ANIM_MODE_DISABLED
; decrement ticks and exit (if not zero yet)
        mov     TMP,ANIM_TICKS
        cpi     TMP,0
        breq    ANIM_MODE_NEXT_FRAME
        dec     ANIM_TICKS
        ret
ANIM_MODE_NEXT_FRAME:
; show next frame
        inc     ANIM_FRAME
        ldi     TMP,(ANIM_DATA_END*2-ANIM_DATA*2)/6
        cp      ANIM_FRAME,TMP
        brlo    ANIM_MODE_SHOW_FRAME
        clr     ANIM_FRAME
ANIM_MODE_SHOW_FRAME:
; disable interrupts
        cli
; get address of animation mode data
        ldi     ZH,HIGH(ANIM_DATA*2)            ; base address of lighting mode table
        ldi     ZL,LOW(ANIM_DATA*2)
        clr     TMP                             ; add offset: 6 times the frame number
        add     ZL,ANIM_FRAME
        adc     ZH,TMP
        add     ZL,ANIM_FRAME
        adc     ZH,TMP
        add     ZL,ANIM_FRAME
        adc     ZH,TMP
        add     ZL,ANIM_FRAME
        adc     ZH,TMP
        add     ZL,ANIM_FRAME
        adc     ZH,TMP
        add     ZL,ANIM_FRAME
        adc     ZH,TMP
; set new PWM values
        ldi     YH,HIGH(PWM_CUR)                ; base address of current PWM values
        ldi     YL,LOW(PWM_CUR)
        lpm     TMP,Z+                          ; use lighting mode data as current PWM values
        st      Y+,TMP
        lpm     TMP,Z+
        st      Y+,TMP
        lpm     TMP,Z+
        st      Y+,TMP
        lpm     TMP,Z+
        st      Y+,TMP
; load tick counter
        lpm     ANIM_TICKS,Z+
; write current PWM values to PWM registers
        rcall   PWM_OUT
; enable interrupts
        sei
; done
        ret

; animation mode disabled - turn off lamps
ANIM_MODE_DISABLED:
; disable interrupts
        cli
; set PWM values to 0
        ldi     YH,HIGH(PWM_CUR)                ; base address of current PWM values
        ldi     YL,LOW(PWM_CUR)
        clr     TMP                             ; turn off lights
        st      Y+,TMP
        st      Y+,TMP
        st      Y+,TMP
        st      Y+,TMP
; write current PWM values to PWM registers
        rcall   PWM_OUT
; enable interrupts
        sei
; done
        ret



; detect when 100 milliseconds have elapsed and then do some stuff
;  - must be called every millisecond
;  - must be called with interrupts enabled
MS100:
; exit if 100 milliseconds have not yet elapsed
        inc     MS100_CNT                       ; milliseconds counter
        ldi     TMP,100                         ; divibe by 100
        cp      MS100_CNT,TMP
        brsh    MS100_ELAPSED
        ret
MS100_ELAPSED:
        clr     MS100_CNT                       ; clear milliseconds counter
; button priority processing
        rcall   BUTTON_PRIO_PROC
; serial port timeout detection
        rcall   SER_TIMEOUT_DETECT
; status output
        rcall   STATUS_OUT
; animation mode
        rcall   ANIM_MODE
; done
        ret



; detect when a milliseconds has elapsed and then do some stuff
;  - must be called every 32 microseconds
;  - must be called with interrupts enabled
MS:
; exit if millisecond has not yet elapsed
        inc     MS_CNT                          ; 32kHz counter
        ldi     TMP,32                          ; divibe by 32
        cp      MS_CNT,TMP
        brsh    MS_ELAPSED
        ret
MS_ELAPSED:
        clr     MS_CNT                          ; clear 32kHz counter
; button processing
        rcall   BUTTON_PROC
; 100 ms stuff
        rcall   MS100
; done
        ret



; main program
MAIN:
; initialization
        clr     BITS
        ldi     TMP,0xFF                        ; no data bytes received yet (i.e. behind a message)
        mov     RECV_CNT,TMP
        sts     PWM_CUR+0,TMP                   ; all current PWM values to zero
        sts     PWM_CUR+1,TMP
        sts     PWM_CUR+2,TMP
        sts     PWM_CUR+3,TMP
        clr     MS_CNT                          ; initialize detection of milliseconds
        clr     MS100_CNT                       ; initialize detection of 100 milliseconds
        clr     BUTTON_STATE                    ; button is not pressed
        clr     BUTTON_MODE                     ; begin with first lighting mode on first button press
        clr     BUTTON_PRIO                     ; button does not have priority
        ldi     TMP,SER_TIMEOUT                 ; serial port has had timeout
        mov     SER_IDLE,TMP
        clr     STATUS_BLINK                    ; do not blink in first status output cycle
        clr     STATUS_CNT
        cbr     BITS,1<<BIT_ANIM_ENAB           ; animation mode disabled
        clr     ANIM_TICKS                      ; start with first frame in animation mode
        clr     ANIM_FRAME
        dec     ANIM_FRAME
        ldi     TMP,0x80                        ; nothing known about last state and number of unchanged samples of serial port
        mov     SER_LAST_STATE,TMP
        mov     SER_UNCHANGED_CNT,TMP
        clr     SER_SYNC_TREND                  ; no trend if running too slow or too fast

; enable interrupts
        sei

MAIN_LOOP:
        wdr

; jump if timer 0 did not overflow yet
        in      TMP,TIFR
        sbrs    TMP,TOV0
        rjmp    MAIN_US32_END
        ldi     TMP,1<<TOV0                     ; clear overflow flag
        out     TIFR,TMP
; sync to timing of serial line
        rcall   SER_SYNC
; 1 ms stuff
        rcall   MS
MAIN_US32_END:

; bottom of main loop
        rjmp     MAIN_LOOP



