;AntiKippenLights-Mini (AKL-Mini) - 18 animated LEDs on a microcontroller
;ATtiny24 @ 1 MHz (internal R/C oscillator), BOD disabled to save power
;Fuses: lfuse=0x62, hfuse=0xDF, efuse=0xFF (factory defaults)
;
;Based on "KippenLights special edition with PIC16C54" version 0.2, 2005-01-01
;by Stefan Schuermans <1stein@schuermans.info>
;GNU public license - http://www.gnu.org/copyleft/gpl.html
;
;To disable auto-power-off function, bridge PA0 to ground (J1). This
;configuration is read at power-on (by applying power or pressing the button).
;
;To enable always-on mode, bridge PA0 to PA2 (J2). This configuration is only
;read once after applying power.
;
;PORTS
;  PA0   = No off-timer detect (connect to GND) - disables auto-power-off,
;          Always on detect (connect to PA2) - disables auto-power-off & button
;  PA1   = push-button input (internal pull-up)
;  PA2-7 = LED anodes 1~6
;  PB0-2 = LED group cathodes A~C (A: LEDs 1~6, B: LEDs 7~12, C: LEDs 13~18)
;  PB3   = [Reset]
;
;LED ARRANGEMENT
;                                                15
;     12          08                               16
;   13   11    09   07                  11 12 13 14  17
;  14       10       06                                18
;   15              05                  10               01
;     16          04                                   02
;       17      03                      09 08 07 06  03
;         18  02                                   04
;           01                                   05
;
;  (for the heart arrangement, directions of clockwise
;  and counter-clockwise animations are reversed)
;
;VERSIONS
;  2017-11-09 - Version 1.0.0 - Arne Rossius
;   * complete re-write
;  2019-02-27 - Version 2.0.0 - Arne Rossius
;   * changed from ATtiny2313 to ATtiny24
;   * corrected conflicting use of 'value' register

;button hold-down delay before power-on (1 ms units)
.equ	POWER_ON_DELAY = 50 ;50 ms

;auto-power-off timeout (~8.4 kHz units)
.equ	AUTO_POWER_OFF = 8400 * 60 * 60 * 2 ;approx. 2 hours
;.equ	AUTO_POWER_OFF = 8400 * 5 ;approx. 5 seconds

;===============================================================================

.include "../include/tn24def.inc"

.undef	XL
.undef	XH
.undef	YH
.undef	ZL
.undef	ZH

.def	temp = R16
.def	count = R17
.def	repeat = R18
.def	value = R19
.def	leds = R20
.def	pwm = R21
.def	button_cnt = R22
.def	timeout1 = R23
.def	timeout2 = R24
.def	timeout3 = R25
.def	timeout4 = R26
.def	flags = R27
	.equ	fAlwaysOn = 0
	.equ	fNoAutoOff = 1

;===============================================================================

.dseg
	RAM_Frame: .byte 18 ;one frame of LED data (brightness values 0~63)

;===============================================================================

.cseg
.org 0x000
	rjmp	reset

.org PCI0addr
	;pin change interrupt: button pressed
	reti
	
;===============================================================================

reset:
	;init ports
	ldi	temp, 0xFC ;PORTA
	out	DDRA, temp
	ldi	temp, 0x03
	out	PORTA, temp
	ldi	temp, 0x07 ;PORTB
	out	DDRB, temp
	ldi	temp, 0x0F
	out	PORTB, temp
	
	;disable analog comparator (to save power)
	ldi	temp, 0x80
	out	ACSR, temp
	
	;init stackpointer
	ldi	temp, RAMEND
	out	SPL, temp
	
	;configure pin change interrupt on PA1 (push-button inut)
	ldi	temp, 0x02 ;detect pin change on PA1 only
	out	PCMSK0, temp
	ldi	temp, 1<<PCIE0 ;enable pin change interrupt
	out	GIMSK, temp
	
	;init registers
	clr	flags
	ldi	timeout1, BYTE1(AUTO_POWER_OFF)
	ldi	timeout2, BYTE2(AUTO_POWER_OFF)
	ldi	timeout3, BYTE3(AUTO_POWER_OFF)
	ldi	timeout4, BYTE4(AUTO_POWER_OFF)
	
	;check if always-on mode selected
	sbic	PINA, 0
	rjmp	always_on_end
	sbi	PORTA, 2
	rjmp	PC+1
	sbic	PINA, 0
	ori	flags, 1<<fAlwaysOn | 1<<fNoAutoOff
always_on_end:
	cbi	PORTA, 2

	;sleep
	sbrs	flags, fAlwaysOn
	rcall	power_off
	
;-------------------------------------------------------------------------------

main_loop:
	
	rcall	animation_run_cw
	rcall	animation_flicker
	rcall	animation_worm_ccw
	rcall	animation_blink
	rcall	animation_run_ccw
	rcall	animation_wobble
	rcall	animation_worm_cw
	rcall	animation_glow
	
	rjmp	main_loop
	
;===============================================================================

animation_blink:
	;flash all LEDs on and off 3 times
	ldi	repeat, 3
	
animation_blink_loop:
	ldi	value, 63 ;all LEDs on
	rcall	set_leds
	ldi	count, 50
	rcall	show_frame
	
	ldi	value, 0 ;all LEDs off
	rcall	set_leds
	ldi	count, 50
	rcall	show_frame
	
	dec	repeat
	brne	animation_blink_loop
	ret
	
;--------------------

animation_glow:
	;slowly fade all LEDs on and off 3 times
	ldi	repeat, 3
	ldi	value, 0
	
animation_glow_on:
	rcall	set_leds
	ldi	count, 2
	rcall	show_frame
	inc	value
	cpi	value, 63
	brne	animation_glow_on
	
animation_glow_off:
	rcall	set_leds
	ldi	count, 2
	rcall	show_frame
	dec	value
	nop
	brne	animation_glow_off
	
	dec	repeat
	brne	animation_glow_on
	ret
	
;--------------------

animation_flicker:
	;alternate between odd and even LEDs 10 times
	ldi	repeat, 10
	
animation_flicker_loop:
	ldi	value, 0 ;even LEDs off (odd LEDs on)
	rcall	set_leds_even
	ldi	count, 15
	rcall	show_frame
	
	ldi	value, 63 ;even LEDs on (odd LEDs off)
	rcall	set_leds_even
	ldi	count, 15
	rcall	show_frame
	
	dec	repeat
	brne	animation_flicker_loop
	ret
	
;--------------------

animation_wobble:
	;slowly fade between odd and even LEDs 5 times
	ldi	repeat, 5
	ldi	value, 0
	
animation_wobble_on:
	rcall	set_leds_even
	ldi	count, 2
	rcall	show_frame
	inc	value
	cpi	value, 63
	brne	animation_wobble_on
	
animation_wobble_off:
	rcall	set_leds_even
	ldi	count, 2
	rcall	show_frame
	dec	value
	nop
	brne	animation_wobble_off
	
	dec	repeat
	brne	animation_wobble_on
	ret
	
;--------------------

animation_run_cw:
	;light chaser with 2 dots, clockwise, 2.5 rotations
	ldi	repeat, 5
	
animation_run_cw_start:
	ldi	YL, RAM_Frame
animation_run_cw_loop:
	ldi	value, 0
	rcall	set_leds
	ldi	value, 63
	std	Y+9, value
	st	Y+, value
	ldi	count, 20
	rcall	show_frame
	cpi	YL, RAM_Frame+9
	brne	animation_run_cw_loop
	
	dec	repeat
	brne	animation_run_cw_start
	ret
	
;--------------------

animation_run_ccw:
	;light chaser with 2 dots, counter-clockwise, 2.5 rotations
	ldi	repeat, 5
	
animation_run_ccw_start:
	ldi	YL, RAM_Frame+9
animation_run_ccw_loop:
	ldi	value, 0
	rcall	set_leds
	ldi	value, 63
	st	-Y, value
	std	Y+9, value
	ldi	count, 20
	rcall	show_frame
	cpi	YL, RAM_Frame
	brne	animation_run_ccw_loop
	
	dec	repeat
	brne	animation_run_ccw_start
	ret
	
;--------------------

animation_worm_cw:
	;worm with fade-out tail, clockwise, 3 rotations
	ldi	repeat, 5
	ldi	value, 0
	rcall	set_leds
	
animation_worm_cw_start:
	ldi	YL, RAM_Frame
animation_worm_cw_loop:
	rcall	fade_leds
	ldi	value, 63
	st	Y+, value
	ldi	count, 20
	rcall	show_frame
	cpi	YL, RAM_Frame+18
	brne	animation_worm_cw_loop
	
	dec	repeat
	brne	animation_worm_cw_start
	ret
	
;--------------------

animation_worm_ccw:
	;worm with fade-out tail, counter-clockwise, 3 rotations
	ldi	repeat, 5
	ldi	value, 0
	rcall	set_leds
	
animation_worm_ccw_start:
	ldi	YL, RAM_Frame+18
animation_worm_ccw_loop:
	rcall	fade_leds
	ldi	value, 63
	st	-Y, value
	ldi	count, 20
	rcall	show_frame
	cpi	YL, RAM_Frame
	brne	animation_worm_ccw_loop
	
	dec	repeat
	brne	animation_worm_ccw_start
	ret
	
;===============================================================================
	
set_leds:
	;set all LEDs to the same brightness value
	sts	RAM_Frame+0, value
	sts	RAM_Frame+1, value
	sts	RAM_Frame+2, value
	sts	RAM_Frame+3, value
	sts	RAM_Frame+4, value
	sts	RAM_Frame+5, value
	sts	RAM_Frame+6, value
	sts	RAM_Frame+7, value
	sts	RAM_Frame+8, value
	sts	RAM_Frame+9, value
	sts	RAM_Frame+10, value
	sts	RAM_Frame+11, value
	sts	RAM_Frame+12, value
	sts	RAM_Frame+13, value
	sts	RAM_Frame+14, value
	sts	RAM_Frame+15, value
	sts	RAM_Frame+16, value
	sts	RAM_Frame+17, value
	ret
	
;--------------------

set_leds_even:
	;set even LEDs to given brightness value, odd LEDs to the inverse
	mov	temp, value
	com	temp
	andi	temp, 63
	sts	RAM_Frame+0, value
	sts	RAM_Frame+1, temp
	sts	RAM_Frame+2, value
	sts	RAM_Frame+3, temp
	sts	RAM_Frame+4, value
	sts	RAM_Frame+5, temp
	sts	RAM_Frame+6, value
	sts	RAM_Frame+7, temp
	sts	RAM_Frame+8, value
	sts	RAM_Frame+9, temp
	sts	RAM_Frame+10, value
	sts	RAM_Frame+11, temp
	sts	RAM_Frame+12, value
	sts	RAM_Frame+13, temp
	sts	RAM_Frame+14, value
	sts	RAM_Frame+15, temp
	sts	RAM_Frame+16, value
	sts	RAM_Frame+17, temp
	ret
	
;--------------------

fade_leds:
	;fade each LED to half its brightness value
	lds	value, RAM_Frame+0
	lsr	value
	sts	RAM_Frame+0, value
	lds	value, RAM_Frame+1
	lsr	value
	sts	RAM_Frame+1, value
	lds	value, RAM_Frame+2
	lsr	value
	sts	RAM_Frame+2, value
	lds	value, RAM_Frame+3
	lsr	value
	sts	RAM_Frame+3, value
	lds	value, RAM_Frame+4
	lsr	value
	sts	RAM_Frame+4, value
	lds	value, RAM_Frame+5
	lsr	value
	sts	RAM_Frame+5, value
	lds	value, RAM_Frame+6
	lsr	value
	sts	RAM_Frame+6, value
	lds	value, RAM_Frame+7
	lsr	value
	sts	RAM_Frame+7, value
	lds	value, RAM_Frame+8
	lsr	value
	sts	RAM_Frame+8, value
	lds	value, RAM_Frame+9
	lsr	value
	sts	RAM_Frame+9, value
	lds	value, RAM_Frame+10
	lsr	value
	sts	RAM_Frame+10, value
	lds	value, RAM_Frame+11
	lsr	value
	sts	RAM_Frame+11, value
	lds	value, RAM_Frame+12
	lsr	value
	sts	RAM_Frame+12, value
	lds	value, RAM_Frame+13
	lsr	value
	sts	RAM_Frame+13, value
	lds	value, RAM_Frame+14
	lsr	value
	sts	RAM_Frame+14, value
	lds	value, RAM_Frame+15
	lsr	value
	sts	RAM_Frame+15, value
	lds	value, RAM_Frame+16
	lsr	value
	sts	RAM_Frame+16, value
	lds	value, RAM_Frame+17
	lsr	value
	sts	RAM_Frame+17, value
	ret
	
;--------------------

show_frame:
	;show current frame 'count' times
	rcall	frame
	dec	count
	brne	show_frame

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

frame:
	;multiplex current frame once (with PWM)
	;63 * 119 = 7497 total cycles (excluding return instruction) => ~7.5 ms
	
	;running from 62 to 0: LED on if pwm < value, 100% on for value = 63
	ldi	pwm, 62
pwm_loop:
	;total loop cycles: 25+3+25+5+25+5+25+2+4 = 119 cycles
	;(1 MHz / 119) = ~8.4 kHz loop frequency
	
	;calculate data for LED group A (25 cycles)
	ldi	leds, 0xC0 ;pull-ups enabled for PA1 and PA0
	lds	temp, RAM_Frame+0
	cp	pwm, temp ;carry set (LED on) if pwm < temp
	ror	leds
	lds	temp, RAM_Frame+1
	cp	pwm, temp
	ror	leds
	lds	temp, RAM_Frame+2
	cp	pwm, temp
	ror	leds
	lds	temp, RAM_Frame+3
	cp	pwm, temp
	ror	leds
	lds	temp, RAM_Frame+4
	cp	pwm, temp
	ror	leds
	lds	temp, RAM_Frame+5
	cp	pwm, temp
	ror	leds
	
	;output (3 cycles)
	out	PORTA, leds ;output LED data for group A
	cbi	PORTB, 0 ;enable LED group A
	
	;calculate data for LED group B (25 cycles)
	ldi	leds, 0xC0
	lds	temp, RAM_Frame+6
	cp	pwm, temp
	ror	leds
	lds	temp, RAM_Frame+7
	cp	pwm, temp
	ror	leds
	lds	temp, RAM_Frame+8
	cp	pwm, temp
	ror	leds
	lds	temp, RAM_Frame+9
	cp	pwm, temp
	ror	leds
	lds	temp, RAM_Frame+10
	cp	pwm, temp
	ror	leds
	lds	temp, RAM_Frame+11
	cp	pwm, temp
	ror	leds
	
	;output (5 cycles)
	sbi	PORTB, 0 ;disable LED group A
	out	PORTA, leds ;output LED data for group B
	cbi	PORTB, 1 ;enable LED group B
	
	;calculate data for LED group C (25 cycles)
	ldi	leds, 0xC0
	lds	temp, RAM_Frame+12
	cp	pwm, temp
	ror	leds
	lds	temp, RAM_Frame+13
	cp	pwm, temp
	ror	leds
	lds	temp, RAM_Frame+14
	cp	pwm, temp
	ror	leds
	lds	temp, RAM_Frame+15
	cp	pwm, temp
	ror	leds
	lds	temp, RAM_Frame+16
	cp	pwm, temp
	ror	leds
	lds	temp, RAM_Frame+17
	cp	pwm, temp
	ror	leds
	
	;output (5 cycles)
	sbi	PORTB, 1 ;disable LED group B
	out	PORTA, leds ;output LED data for group C
	cbi	PORTB, 2 ;enable LED group C
	
	;poll push-button (7 cycles)
	sbic	PINA, 1
	rjmp	button_released
	;button pressed
	sbrs	button_cnt, 7 ;prevent overflow
	inc	button_cnt
	cpi	button_cnt, 127 ;pressed for (127 / 8.4 kHz) = ~15 ms
	brne	button_end
	sbrs	flags, fAlwaysOn
	rcall	power_off
	rjmp	button_end
button_released:
	clr	button_cnt
	rjmp	PC+1 ;2 cycles
	nop ;1 cycle
button_end:
	;auto-power-off timeout counter (8 cycles)
	clc ;clear carry
	sbrs	flags, fNoAutoOff
	subi	timeout1, 1
	sbci	timeout2, 0
	sbci	timeout3, 0
	sbci	timeout4, 0
	brne	PC+2
	rcall	power_off
	;extra delay (10 cycles) - button (7) + timeout (8) + 10 = 25 cycles
	rcall	return ;7 cycles
	rjmp	PC+1 ;2 cycles
	nop ;1 cycle
	
	;output (2 cycles)
	sbi	PORTB, 2 ;disable LED group C
	
	;end of PWM loop (4 cycles when looping, 3 when exiting loop)
	subi	pwm, 1
	brcs	PC+2
	rjmp	pwm_loop
	
return:
	ret

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

power_off:
	;button pressed or auto-power-off timeout elapsed: turn off
	
	;disable LED group C
	sbi	PORTB, 2
	
	;set all LED cathodes low (turn on LEDs installed in reverse)
	ldi	temp, 0x03
	out	PORTA, temp
	
	;wait until button released
power_off_wait:
	sbis	PINA, 1
	rjmp	power_off_wait
	
	;pull PA0 low (avoid pull-up current)
	cbi	PORTA, 0
	sbi	DDRA, 0
	
	;enable interrupts (use pin change interrupt for wake-up)
	ldi	temp, 1<<PCIF0 ;clear pending pin change interrupts
	out	GIFR, temp
	sei
	
power_off_sleep:
	;sleep
	ldi	temp, 1<<SM1 | 1<<SE ;power-down mode, sleep enabled
	out	MCUCR, temp
	sleep
	ldi	temp, 1<<SM1 ;disable sleeping
	out	MCUCR, temp
	
	;check if button pressed for a certain time (filter ESD and spikes)
	clr	button_cnt
power_on_loop:
	;delay ~1 ms (999 cycles)
	ldi	temp, 0 ;256 * 3 = 768 cycles delay
	dec	temp
	brne	PC-1
	ldi	temp, 77 ;77 * 3 = 231 cycles delay
	dec	temp
	brne	PC-1
	;poll button
	sbic	PINA, 1
	rjmp	power_off_sleep
	;button pressed
	inc	button_cnt
	cpi	button_cnt, POWER_ON_DELAY
	brlo	power_on_loop

	;power on
	ldi	button_cnt, 255 ;avoid duplicate button press detection
	
	;restore PA0 as input with pull-up
	cbi	DDRA, 0
	sbi	PORTA, 0
	
	;disable interrupts
	cli
	
	;reset power-off timeout
	ldi	timeout1, BYTE1(AUTO_POWER_OFF)
	ldi	timeout2, BYTE2(AUTO_POWER_OFF)
	ldi	timeout3, BYTE3(AUTO_POWER_OFF)
	ldi	timeout4, BYTE4(AUTO_POWER_OFF)
	
	;read PA0 (auto-power-off disable)
	andi	flags, ~(1<<fNoAutoOff)
	sbis	PINA, 0
	ori	flags, 1<<fNoAutoOff
	
	ret
	