
;=======================================================================
; This code is written by Leif Engman (leif.engman@home.se) and is used
; in a project called "KBSnoop". The code is provided "as is".
; I cannot guarantee that it works in every situation but I have had no
; problems with it myself.
; If you find any bugs or have problems with it, feel free to email me
; and I will see what I can do to help you.
; You can use this code as you see fit, but you cannot sell anything
; that uses this code (or parts of it) without my permission.
;
; Version history
; ===============
; 000910	v1.00	First public version.
;
;=======================================================================

.include "2313def.inc"

;==========================================================================
;				Constants
;==========================================================================

;=== Port definitions ===
.equ	c_kbinport	= PIND
.equ	c_kboutport	= PORTD
.equ	c_kbddrport	= DDRD
.equ	c_kbclkbit	= 3
.equ	c_kbdatabit	= 2
.equ	c_kbclkmask	= (1<<c_kbclkbit)
.equ	c_kbdatamask	= (1<<c_kbdatabit)

.equ	c_ledinport	= PIND
.equ	c_ledoutport	= PORTD
.equ	c_ledddrport	= DDRD
.equ	c_ledbit	= 4
.equ	c_ledmask	= (1<<c_ledbit)

;=== LED definitions ===
.equ	c_LED_time	= 30		; 30ms

;=== Interrupt definitions ===
.equ	c_gimsk_noint	= 0		; Depends on c_kbdatabit
.equ	c_gimsk_int	= (1<<INT0)<<(c_kbdatabit-2)
					; Depends on c_kbdatabit
.equ	c_mcucr_init	= (1<<ISC01)<<((c_kbdatabit-2)*2)
					; INT=Falling Edge, depends on c_kbdatabit
;=== Timer definitions ===
.equ	c_singletime	= 75		; 75 µs wait each loop, see kbwaitbit
.equ	c_kbtimeout	= 2		; 2 * c_singletime = 150µs at 3.6864MHz
.equ	c_break_time	= 5
.equ	ONE_MS_TIMEOUT	= 58		; One ms timer setup value (at CK/64, 3.6864MHz)

;=== Misc definitions ===
.equ	MAX_BUF_SIZE	= 64

;==========================================================================
;				Registers
;==========================================================================
; (M ) = Variable is changed only in Main program
; ( I) = Variable is changed only in Interrupt routines

.def	_index		= r0		;(MI) Used to fetch data from program memory
.def	_status_save	= r1		;( I) Status flag save during IRQ0
.def	_kbinloop	= r2		;( I)
.def	_kbreceived	= r3		;( I)
.def	_kbtimeoutvar	= r4		;( I)
.def	_loopvar	= r5		;( I)
.def	_LED_timer	= r6		;(M )
.def	_break_timer	= r7		;( I)
.def	_1st_char	= r8		;( I)
.def	_last_direction	= r9		;( I)

.def	temp		= r16		;(MI) Temporary
.def	temp1		= r17		;(MI) Temporary
.def	temp_r		= r18		;(MI) Temporary
.def	temp1_r		= r19		;(MI) Temporary
.def	temp_irq	= r20		;( I) Temporary during timer interrupt
.def	timer_1ms	= r21		;(MI) 1 ms timer
.def	kbchar		= r22		;(MI)
.def	XTOT		= r26		;(M )
.def	buf_read	= r26		;(M )
.def	YTOT		= r28		;(MI)
.def	buf_write	= r28		;(MI)
.def	ZTOT		= r30		;(M )

;==========================================================================
;				Internal RAM
;==========================================================================
.DSEG
.ORG		0x60

r_txbuf:	.BYTE	MAX_BUF_SIZE	; Transmit buffer


;==========================================================================
;				Internal EEPROM
;==========================================================================
.ESEG
.ORG		0x00

;==========================================================================
;				Program memory (Flash)
;==========================================================================
.CSEG
.ORG		0x0000
	rjmp	RESET		; Reset Handler
	rjmp	EXT_INT0	; IRQ0 Handler
	rjmp	EXT_INT1	; IRQ1 Handler
	reti			;rjmp TIM_CAPT1	; Timer1 Capture Handler
	reti			;rjmp TIM_COMP1	; Timer1 Compare Handler
	reti			;rjmp TIM_OVF1	; Timer1 Overflow Handler
	rjmp	TIM_OVF0	; Timer0 Overflow Handler
	reti			;rjmp UART_RXC	; UART RX Complete Handler
	reti			;rjmp UART_DRE	; UDR Empty Handler
	reti			;rjmp UART_TXC	; UART TX Complete Handler
	reti			;rjmp ANA_COMP	; Analog Comparator Handler

start_msg:
	.DB	10,13,'*','*','*',' ','K','B','S','n','o','o','p',' '
	.DB	' ','v','1','.','0','0',' ','*','*','*',10,13,10,13,0

;**************************************************************************
;**************************************************************************
;************* Routines called from within Interrupt routines *************
;**************************************************************************
;**************************************************************************

;==========================================================================
; TIM_OVF0	Interrupt routine for Timer 0 Overflow
;==========================================================================
TIM_OVF0:
	in	_status_save,SREG	; Save status flags

	inc	timer_1ms		; Increment ms timer

	tst	_break_timer
	breq	end_tim0_int
	dec	_break_timer

end_tim0_int:
	ldi	temp_irq,-ONE_MS_TIMEOUT
	out	TCNT0,temp_irq		; Reload timer counter

	out	SREG,_status_save	; Restore flags
	reti

;==========================================================================
; EXT_INT0 (&1)	Interrupt routine for External Interrupt 0 & 1
;		(Data line on KB interface has gone low)
;		Since only INT0 or INT1 is used, both points to the same
;		routine. That way it is easy to change where the KB data
;		line is connected.
;==========================================================================
EXT_INT0:
EXT_INT1:
	in	_status_save,SREG	; Save status flags
	push	temp			; Save temp variable
	push	temp1			; Save temp1 variable

	rcall	kbif_get		; Get byte from KB interface
	brcs	end_ext_int0		; If timeout occured, quit

	tst	_1st_char		; If first character ever to be displayed
	breq	ei_1			;  Don't start with ", "

	brts	ei_lastkb
	tst	_last_direction		; Data from PC
	brne	ei_clrtimer		; If last data wasn't also from PC, print ","
	rjmp	ei
ei_lastkb:
	tst	_last_direction		; Data from KB
	breq	ei_clrtimer		; If last data wasn't also from KB, print ","
	rjmp	ei
ei_clrtimer:
	ldi	temp,0
	mov	_break_timer,temp

ei:
	tst	_break_timer		; If it was a long time since the last byte
	brne	ei_0			; 
	ldi	temp,','		;  Add a "," after the last data
	rcall	char_to_buf
ei_0:
	ldi	temp,' '
	rcall	char_to_buf		; Add a space

ei_1:
	brts	ei_kb			; If received data was from the PC
	ldi	temp,'*'
	rcall	char_to_buf		;  add a "*" before data
	ldi	temp,0
	rjmp	ei_kb1
ei_kb:
	ldi	temp,1
ei_kb1:
	mov	_last_direction,temp	; Indicate direction
	mov	temp,_kbreceived
	rcall	byte_to_buf		; Save data in send buffer

	ldi	temp,c_break_time
	mov	_break_timer,temp

	ser	temp
	mov	_1st_char,temp

end_ext_int0:
	ldi	temp,(1<<INTF0)
	out	GIFR,temp		; Clear Int0 flag
	pop	temp1			; Restore temp variable
	pop	temp			; Restore temp variable
	out	SREG,_status_save	; Restore flags
	reti

;==========================================================================
; byte_to_buf
;==========================================================================
byte_to_buf:
	push	temp
	swap	temp
	andi	temp,0x0F
	rcall	conv_to_char
	rcall	char_to_buf		; Save high character
	pop	temp
	andi	temp,0x0F
	rcall	conv_to_char
	rcall	char_to_buf		; Save low character
	ret


conv_to_char:
	ldi	temp1,'A'-10		; Assume > 9
	cpi	temp,10
	brge	btb_1
	ldi	temp1,'0'		; It was < 10
btb_1:
	add	temp,temp1
	ret


char_to_buf:
	st	Y+,temp			; Store character in buffer
	cpi	buf_write,MAX_BUF_SIZE+0x60+1
	brne	ctb_ret			; If end of buffer reached
	ldi	buf_write,0x60		;  begin from start again
ctb_ret:
	ret

;==========================================================================
; kbif_get	Read byte on KB interface.
;		This device does not need to clock the data on the
;		serial line.
;		This routine can only be called when the 'Data line'
;		interrupt is disabled (no interrupts from KB interface
;		is allowed).
;
; In:		nothing
; Out:		Carry flag	1=Timeout occured
;		T flag		0=Data was from PC, 1=Data was from KB
;		kbreceived	Data from KB interface
;==========================================================================
kbif_get:
	ldi	temp,c_kbtimeout
	ldi	temp1,c_kbclkmask
	clt				; Wait for KB clock to go low
	rcall	kbwaitbit
	brcs	kbif_timeout

	ldi	temp,c_kbtimeout*2	; Here we wait for clock strobe for start bit
	ldi	temp1,c_kbclkmask
	set				; Wait for KB clock to go high
	rcall	kbwaitbit
	brcs	kbif_timeout

	ldi	temp,9			; Loop for 8 data bits + 1 parity bit
	mov	_kbinloop,temp

	ldi	temp,15000/c_singletime	; First clock can be looong (up to 15ms), wait max 15 ms
	ldi	temp1,c_kbclkmask
	clt				; Wait for KB clock to go low
	rcall	kbwaitbit
	brcs	kbif_timeout
	rjmp	kbif_clklow

kbif_recloop:
	ldi	temp,c_kbtimeout
	ldi	temp1,c_kbclkmask
	clt				; Wait for KB clock to go low
	rcall	kbwaitbit
	brcs	kbif_timeout
kbif_clklow:
	ldi	temp,c_kbtimeout	; Here we wait for clock strobe for data bits
	ldi	temp1,c_kbclkmask
	set				; Wait for KB clock to go high
	rcall	kbwaitbit
	brcs	kbif_timeout

	clc				; Assume bit is 0
	sbic	c_kbinport,c_kbdatabit
	sec				; It was actually 1
	ror	_kbreceived

	dec	_kbinloop
	brne	kbif_recloop		; Make another loop if not done

	rol	_kbreceived		; Get rid of parity and get back bit 0

	ldi	temp,c_kbtimeout
	ldi	temp1,c_kbclkmask
	clt				; Wait for KB clock to go low
	rcall	kbwaitbit
	brcs	kbif_timeout

	rcall	wait20
	rcall	wait20			; Wait out stop bit (50 µs)

	ldi	temp,c_kbtimeout
	ldi	temp1,c_kbdatamask
	clt				; Wait for KB data to go low or timeout
	rcall	kbwaitbit
	set				; Assume data is from KB
	brcs	kbif_notimeout		; If timeout, data was from KB; End

kbif_frompc:
	ldi	temp,c_kbtimeout
	ldi	temp1,c_kbdatamask
	set				; Wait for KB data to go high
	rcall	kbwaitbit
	brcs	kbif_timeout

	clt				; Data was from PC
	rjmp	kbif_notimeout		; Return

kbif_timeout:
	sec				; Set timeout flag
	rjmp	kbif_return

kbif_notimeout:
	clc				; No timeout occured
kbif_return:
	ret

;==========================================================================
; kbwaitbit	Wait for specified bit on kb interface to go high or low
;		This routine is hardcoded for a 3.6864MHz crystal.
;
; In:		temp		Time to wait (in 75 µs ticks)
;		temp1		Which bit to wait for (mask with one bit set)
;		T flag		What to wait for, 0 or 1
;
; Out:		Carry flag	1=Timeout occured
;==========================================================================
kbwaitbit:
	mov	_loopvar,temp
kbwaithighloop:
	ldi	temp,29			; Make inner loop 30 times,
	mov	_kbtimeoutvar,temp	;  every inner loop takes 10 cycles
kbwaitbegin:
	in	temp,c_kbinport		; Get data from port (1 cycle)
	brts	kbwait1
kbwait0:				; Wait for 0 here (5 clockcycles)
	and	temp,temp1		; Mask apropriate bit
	breq	kbwait3			; If bit=0, stop the wait and leave
	rjmp	kbwait2
kbwait1:				; Wait for 1 here (5 clockcycles)
	and	temp,temp1		; Mask apropriate bit
	brne	kbwait3			; If bit=1, stop the wait and leave
	nop
kbwait2:
	dec	_kbtimeoutvar		; (1 cycle)
	brne	kbwaitbegin		; If no timeout, keep waiting (2 cycles)

	rjmp	PC+1			; (2 cycles)
	rjmp	PC+1			; (2 cycles)
	dec	_loopvar		; (1 cycle)
	brne	kbwaithighloop		; (2 cycles)

	sec				; Indicate timeout
	rjmp	kbwaitend
kbwait3:
	clc				; Clear timeout indication
kbwaitend:
	ret

;**************************************************************************
;**************************************************************************
;* Routines called both from Interrupt routines and from the main program *
;**************************************************************************
;**************************************************************************
;==========================================================================
; wait_ms	Wait x ms (Hard-coded for 3.6864MHz)
;
; In:		temp		Number of ms to wait
; Out:		Nothing
;==========================================================================
wait_ms:
	mov	temp_r,temp
	ldi	temp1_r,249		; 245 * (15 cycles * 0.2713) ~ 1 ms
waitmsh:
	mov	temp,temp1_r
waitmsl:
	dec	temp			; 1 cycle
	rcall	wait7clk		; 7 cycles
	rjmp	PC+1			; 2 cycles
	rjmp	PC+1			; 2 cycles
	nop				; 1 cycle
	brne	waitmsl			; 2 cycles

	rcall	wait7clk		; 7 cycles
	dec	temp_r			; 1 cycle
	brne	waitmsh			; Do X times (2 cycles)
wait7clk:
	ret

;==========================================================================
; waitxx	Wait 4-40 µs, time includes call and return
;		Hard-coded for 3.6864MHz
;
; In:		Nothing
; Out:		Nothing
;==========================================================================
;	rcall	waitxx			; 3 cycles
wait40:	rjmp	PC+1			; 40.4 µs
	rjmp	PC+1
wait39:	rjmp	PC+1			; 39.3 µs
	rjmp	PC+1
wait38:	rcall	wait18			; 38.2 µs
wait20:	rjmp	PC+1			; 20.1 µs
	rjmp	PC+1
wait19:	rjmp	PC+1			; 19.0 µs
	nop
wait18:	rcall	wait2			; 18.2 µs
wait16:	rcall	wait4			; 16.3 µs
wait12:	rcall	wait4			; 12.2 µs
wait8:	rcall	wait4			; 8.1 µs
wait4:	rcall	wait2			; 4.06 µs
	nop				; 1 cycle (7*2+1=15, 4.06µs)
wait2:	ret				; 4 cycles (+rcall, 3 cycles, 1.9µs)

;**************************************************************************
;**************************************************************************
;***************** Routines called from the main program ******************
;**************************************************************************
;**************************************************************************

RESET:
	cli				; Global Interrupt Disable
	ldi	temp,RAMEND
	out	SPL,temp

	ldi	temp,0
	out	GIMSK,temp		; Disable ExtInt 0

	ldi	temp,(1<<CS01)+(1<<CS00)
	out	TCCR0,temp		; Timer0 prescaler/64 = 17.36µs @ 3.6864MHz
	ldi	temp,-ONE_MS_TIMEOUT
	out	TCNT0,temp

	ldi	temp,0x00
	out	TIMSK,temp		; Disable all Timer/Counter Interrupts

	ldi	temp,c_mcucr_init	; Setup KB data line interrupt
	out	MCUCR,temp

	ldi	temp,(1<<INTF0)
	out	GIFR,temp		; Clear General Interrupt flag

	ldi	temp,(1<<TOV0)
	out	TIFR,temp		; Clear Timer Interrupt flag

	ldi	temp,(1<<WDTOE)+(1<<WDE)
	out	WDTCR,temp
	ldi	temp,(1<<WDTOE)
	out	WDTCR,temp		; Disable WatchDog

	ldi	temp,0
	out	DDRB,temp		; Port B as input
	out	PORTB,temp		;  and Tri-state

	out	DDRD,temp		; Port D as input
	out	PORTD,temp		;  and Tri-state

	cbi	c_kbddrport,c_kbclkbit	; Set up KB Data and Clk signals for usage
	cbi	c_kbddrport,c_kbdatabit	; Define ports as inputs
	cbi	c_kboutport,c_kbclkbit	; Set port low => tri-state
	cbi	c_kboutport,c_kbdatabit	; Set port low => tri-state

	sbi	c_ledoutport,c_ledbit	; Set up LED port for usage, turn LED off
	sbi	c_ledddrport,c_ledbit	; Set port as output

	ldi	ZL,0x60			; Clear RAM memory (all variables)
	ldi	ZH,0x00
	ldi	temp,128
	clr	temp1
init_vars:
	st	Z+,temp1
	dec	temp
	brne	init_vars

	ldi	temp,5
	out	UBRR,temp		; Set 38400 BPS at 3.6864MHz
	sbi	USR,TXC			; Clear UART Tx Interrupt flag
	sbi	UCR,TXEN		; Enable UART Tx

	ldi	temp,c_gimsk_int	; Enable proper External INT
	out	GIMSK,temp		; for KB data line
	ldi	temp,(1<<TOIE0)
	out	TIMSK,temp		; Enable Timer0 Interrupts

	clr	timer_1ms
	clr	_LED_timer		

	clr	temp
	mov	_1st_char,temp

	ldi	temp,0x60
	mov	buf_read,temp
	mov	buf_write,temp

	ldi	ZL,low(start_msg*2)
	ldi	ZH,high(start_msg*2)
start_msg_out:
	lpm				; Get character to R0
	tst	r0
	breq	end_msg
	adiw	ZTOT,1
	mov	temp,r0
	rcall	char_to_buf
	rjmp	start_msg_out
end_msg:
	sei				; Enable Global Interrupt again

main_loop:
	rcall	exec_led
	cp	buf_read,buf_write
	breq	main_loop
wait_tx:
	sbis	USR,UDRE		; Wait until transmit (to shift register)
	rjmp	wait_tx			;  is ready

	ld	temp,X+			; Get character to send
	out	UDR,temp		; Send it

	cpi	buf_read,MAX_BUF_SIZE+0x60+1
	brne	main_loop		; If end of buffer reached

	ldi	buf_read,0x60		;  begin from start again
	rjmp	main_loop

;==========================================================================
; exec_led	Blink LED
;==========================================================================
exec_led:
	cp	_LED_timer,timer_1ms	; If time left
	brpl	exec_led_ret		;  do nothing more

	in	temp,c_ledoutport
	andi	temp,c_ledmask
	brne	exec_turn_on

	sbi	c_ledoutport,c_ledbit	; Turn LED off
	rjmp	exec_led_end

exec_turn_on:
	cbi	c_ledoutport,c_ledbit	; Turn LED on

exec_led_end:
	ldi	temp,c_LED_time
	add	temp,timer_1ms
	mov	_LED_timer,temp		; Save new timeout time

exec_led_ret:
	ret

;==========================================================================
.ORG	FLASHEND
	nop
