Collapse OS Documentation Browser

doc/hw/z80/code/ps2ctl.asm

../ ps2ctl.asm

.include "tn45def.inc"

; Receives keystrokes from PS/2 keyboard and send them to the 595. As long as
; that number is not collected, we buffer the scan code received from ps/2. As
; soon as that number is collected we put the next number in the buffer. If the
; buffer is empty, we do nothing (the 595 already had its SRCLR pin triggered
; and shows 0).
;
; PS/2 is a bidirectional protocol, but in this program, we only care about
; receiving keystrokes. We don't send anything to the keyboard.
;
; The PS/2 keyboard has two data wires: Clock and Data. It is the keyboard that
; drives the clock with about 30-50 us between each clock.
;
; We wire the Clock to INT0 (PB2) and make it trigger an interrupt on the
; falling edge (the edge, in the PS/2 protocol, when data is set).
;
; Data is sent by the keyboard in 11-bit frames. 1 start bit (0), 8 data bits,
; one parity bit, one stop bit (1).
;
; Parity bit is set if number of bits in data bits is even. Unset otherwise.
;
; *** Receiving a data frame ***
;
; In idle mode, R18 is zero. When INT0 is triggered, it is increased and R17 is
; loaded with 0x80. We do this because we're going to right shift our data in
; (byte is sent LSB first). When the carry flag is set, we'll know we're
; finished. When that happens, we increase R18 again. We're waiting for parity
; bit. When we get it, we check parity and increase R18 again. We're waiting
; for stop bit. After we receive stop bit, we reset R18 to 0.
;
; On error, we ignore and reset our counters.

; *** Buffering scan codes ***
;
; The buffer starts at SRAM and stops at 0x100. It leaves space for the stack
; and makes overflow check easy. Also, we don't need a very big buffer. In this
; address space, Z chasing Y. When Y == Z, the buffer is empty. When 0x100 is
; reached, we go back to SRAM_START.
;
; Whenever a new scan code is received, we place it in Y and increase it.
; Whenever we send a scan code to the 595 (which can't be done when Z == Y
; because Z points to an invalid value), we send the value of Z and increase.

; *** Sending to the 595 ***
;
; Whenever a scan code is read from the 595, CE goes low and triggers a PCINT
; on PB4. When we get it, we clear the GPIOR0/1 flag to indicate that we're
; ready to send a new scan code to the 595.
;
; Because that CE flip/flop is real fast (375ns), it requires us to run at 8MHz.
;
; During the PCINT, we also trigger RCLK once because CE is also wired to SRCLR
; and we want the z80 to be able to know that the device has nothing to give
; (has a value of zero) rather than having to second guess (is this value, which
; is the same as the one that was read before, a new value or not?). With that
; "quick zero-in" scheme, there's no ambiguity: no scan code can be ready twice
; because it's replaced by a 0 as soon as it's read, until it can be filled with
; the next char in the buffer.

; *** Register Usage ***
;
; GPIOR0 flags:
;	0 - when set, indicates that the DATA pin was high when we received a
;           bit through INT0. When we receive a bit, we set flag T to indicate
;           it.
;	1 - When set, indicate that the 595 holds a value that hasn't been read
;           by the z80 yet.
;
; R16: tmp stuff
; R17: recv buffer. Whenever we receive a bit, we push it in there.
; R18: recv step:
;      - 0: idle
;      - 1: receiving data
;      - 2: awaiting parity bit
;      - 3: awaiting stop bit
; R19: Register used for parity computations and tmp value in some other places
; R20: data being sent to the 595
; Y: pointer to the memory location where the next scan code from ps/2 will be
;    written.
; Z: pointer to the next scan code to push to the 595
;
; *** Constants ***
;
.equ	CLK = PINB2
.equ	DATA = PINB1
.equ	SRCLK = PINB3
.equ	CE = PINB4
.equ	RCLK = PINB0

; init value for TCNT0 so that overflow occurs in 100us
.equ	TIMER_INITVAL = 0x100-100

	rjmp	main
	rjmp	hdlINT0
	rjmp	hdlPCINT

; Read DATA and set GPIOR0/0 if high. Then, set flag T.
; no SREG fiddling because no SREG-modifying instruction
hdlINT0:
	sbic	PINB, DATA	; DATA clear? skip next
	sbi	GPIOR0, 0
	set
	reti

; Only PB4 is hooked to PCINT and we don't bother checking the value of the PB4
; pin: things go too fast for this.
; no SREG fiddling because no SREG-modifying instruction
hdlPCINT:
	; SRCLR has been triggered. Let's trigger RCLK too.
	sbi	PORTB, RCLK
	cbi	PORTB, RCLK
	cbi	GPIOR0, 1	; 595 is now free
	reti

main:
        ldi     r16, low(RAMEND)
        out     SPL, r16
        ldi     r16, high(RAMEND)
        out     SPH, r16

	; Set clock prescaler to 1 (8MHz)
	ldi	r16, (1<<CLKPCE)
	out	CLKPR, r16
	clr	r16
	out	CLKPR, r16


	; init variables
	clr	r18
	out	GPIOR0, r18


	; Setup int0/PCINT
	; INT0, falling edge
	ldi	r16, (1<<ISC01)
	out	MCUCR, r16
	; Enable both INT0 and PCINT
	ldi	r16, (1<<INT0)|(1<<PCIE)
	out	GIMSK, r16
	; For PCINT, enable only PB4
	ldi	r16, (1<<PCINT4)
	out	PCMSK, r16

	; Setup buffer
	clr	YH
	ldi	YL, low(SRAM_START)
	clr	ZH
	ldi	ZL, low(SRAM_START)

	; Setup timer. We use the timer to clear up "processbit" registers after
	; 100us without a clock. This allows us to start the next frame in a
	; fresh state. at 8MHZ, setting the counter's prescaler to 8 gives us
	; a nice 1us for each TCNT0.
	ldi	r16, (1<<CS01)	; clk/8 prescaler
	out	TCCR0B, r16

	; init DDRB
	sbi	DDRB, SRCLK
	cbi	PORTB, RCLK	; RCLK is generally kept low
	sbi	DDRB, RCLK

	sei

loop:
	brts	processbit	; flag T set? we have a bit to process
	cp	YL, ZL		; if YL == ZL, buffer is empty
	brne	sendTo595	; YL != ZL? our buffer has data

	; nothing to do. Before looping, let's check if our communication timer
	; overflowed.
	in	r16, TIFR
	sbrc	r16, TOV0
	rjmp	processbitReset	; Timer0 overflow? reset processbit

	; Nothing to do for real.
	rjmp	loop

; Process the data bit received in INT0 handler.
processbit:
	in	r19, GPIOR0	; backup GPIOR0 before we reset T
	andi	r19, 0x1	; only keep the first flag
	cbi	GPIOR0, 0
	clt			; ready to receive another bit

	; We've received a bit. reset timer
	rcall	resetTimer

	; Which step are we at?
	tst	r18
	breq	processbits0
	cpi	r18, 1
	breq	processbits1
	cpi	r18, 2
	breq	processbits2
	; step 3: stop bit
	clr	r18		; happens in all cases
	; DATA has to be set
	tst	r19		; Was DATA set?
	breq	loop		; not set? error, don't push to buffer
	; push r17 to the buffer
	st	Y+, r17
	rcall	checkBoundsY
	rjmp	loop

processbits0:
	; step 0 - start bit
	; DATA has to be cleared
	tst	r19		; Was DATA set?
	brne	loop		; Set? error. no need to do anything. keep r18
				; as-is.
	; DATA is cleared. prepare r17 and r18 for step 1
	inc	r18
	ldi	r17, 0x80
	rjmp	loop

processbits1:
	; step 1 - receive bit
	; We're about to rotate the carry flag into r17. Let's set it first
	; depending on whether DATA is set.
	clc
	sbrc	r19, 0		; skip if DATA cleared.
	sec
	; Carry flag is set
	ror	r17
	; Good. now, are we finished rotating? If carry flag is set, it means
	; that we've rotated in 8 bits.
	brcc	loop		; we haven't finished yet
	; We're finished, go to step 2
	inc	r18
	rjmp	loop
processbits2:
	; step 2 - parity bit
	mov	r1, r19
	mov	r19, r17
	rcall	checkParity	; --> r16
	cp	r1, r16
	brne	processbitError	; r1 != r16? wrong parity
	inc	r18
	rjmp	loop

processbitError:
	clr	r18
	ldi	r19, 0xfe
	rcall	sendToPS2
	rjmp	loop

processbitReset:
	clr	r18
	rcall	resetTimer
	rjmp	loop

; send next scan code in buffer to 595, MSB.
sendTo595:
	sbic	GPIOR0, 1
	rjmp	loop		; flag 1 set? 595 is "busy". Don't send.
	; We disable any interrupt handling during this routine. Whatever it
	; is, it has no meaning to us at this point in time and processing it
	; might mess things up.
	cli
	sbi	DDRB, DATA

	ld	r20, Z+
	rcall	checkBoundsZ
	ldi	r16, 8

sendTo595Loop:
	cbi	PORTB, DATA
	sbrc	r20, 7		; if leftmost bit isn't cleared, set DATA high
	sbi	PORTB, DATA
	; toggle SRCLK
	cbi	PORTB, SRCLK
	lsl	r20
	sbi	PORTB, SRCLK
	dec	r16
	brne	sendTo595Loop	; not zero yet? loop

	; We're finished sending our data to the 595 and we're ready to go back
	; to business as usual. However, timing is important here. The z80 is
	; very fast and constantly hammers our 595 with polls. While this
	; routine was running, it was getting zeroes, which is fine, but as soon
	; as we trigger RCLK, the z80 is going to fetch that value. What we want
	; to do is to enable back the interrupts as soon as RCLK is triggered
	; so that the z80 doesn't have enough time to poll twice. If it did, we
	; would return a double character. This is why RCLK triggering is the
	; last operation.

	; release PS/2
	cbi	DDRB, DATA

	; Set GPIOR0/1 to "595 is busy"
	sbi	GPIOR0, 1

	; toggle RCLK
	sbi	PORTB, RCLK
	cbi	PORTB, RCLK
	sei

	rjmp	loop

resetTimer:
	ldi	r16, TIMER_INITVAL
	out	TCNT0, r16
	ldi	r16, (1<<TOV0)
	out	TIFR, r16
	ret

; Send the value of r19 to the PS/2 keyboard
sendToPS2:
	; We don't use the general INT0 mechanism here. However, we still want
	; to listen to PCINT, so we don't disable interrupts entirely, just
	; INT0.
	ldi	r16, (1<<PCIE)
	out	GIMSK, r16

	; First, indicate our request to send by holding both Clock low for
	; 100us, then pull Data low
	; lines low for 100us.
	cbi	PORTB, CLK
	sbi	DDRB, CLK
	rcall	resetTimer

	; Wait until the timer overflows
	in	r16, TIFR
	sbrs	r16, TOV0
	rjmp	PC-2
	; Good, 100us passed.

	; Pull Data low, that's our start bit.
	cbi	PORTB, DATA
	sbi	DDRB, DATA

	; Now, let's release the clock. At the next raising edge, we'll be
	; expected to have set up our first bit (LSB). We set up when CLK is
	; low.
	cbi	DDRB, CLK	; Should be starting high now.

	; We will do the next loop 8 times
	ldi	r16, 8
	; Let's remember initial r19 for parity
	mov	r1, r19

sendToPS2Loop:
	; Wait for CLK to go low
	sbic	PINB, CLK
	rjmp	PC-1

	; set up DATA
	cbi	PORTB, DATA
	sbrc	r19, 0		; skip if LSB is clear
	sbi	PORTB, DATA
	lsr	r19

	; Wait for CLK to go high
	sbis	PINB, CLK
	rjmp	PC-1

	dec	r16
	brne	sendToPS2Loop	; not zero? loop

	; Data was sent, CLK is high. Let's send parity
	mov	r19, r1		; recall saved value
	rcall	checkParity	; --> r16

	; Wait for CLK to go low
	sbic	PINB, CLK
	rjmp	PC-1

	; set parity bit
	cbi	PORTB, DATA
	sbrc	r16, 0		; parity bit in r16
	sbi	PORTB, DATA

	; Wait for CLK to go high
	sbis	PINB, CLK
	rjmp	PC-1

	; Wait for CLK to go low
	sbic	PINB, CLK
	rjmp	PC-1

	; We can now release the DATA line
	cbi	DDRB, DATA

	; Wait for DATA to go low. That's our ACK
	sbic	PINB, DATA
	rjmp	PC-1

	; Wait for CLK to go low
	sbic	PINB, CLK
	rjmp	PC-1

	; We're finished! Enable INT0, reset timer, everything back to normal!
	rcall	resetTimer
	clt			; also, make sure T isn't mistakely set.
	ldi	r16, (1<<INT0)|(1<<PCIE)
	out	GIMSK, r16
	ret

; Check that Y is within bounds, reset to SRAM_START if not.
checkBoundsY:
	tst	YL
	breq	PC+2
	ret			; not zero, nothing to do
	; YL is zero. Reset Y
	clr	YH
	ldi	YL, low(SRAM_START)
	ret

; Check that Z is within bounds, reset to SRAM_START if not.
checkBoundsZ:
	tst	ZL
	breq	PC+2
	ret			; not zero, nothing to do
	; ZL is zero. Reset Z
	clr	ZH
	ldi	ZL, low(SRAM_START)
	ret

; Counts the number of 1s in r19 and set r16 to 1 if there's an even number of
; 1s, 0 if they're odd.
checkParity:
	ldi	r16, 1
	lsr	r19
	brcc	PC+2		; Carry unset? skip next
	inc	r16		; Carry set? We had a 1
	tst	r19		; is r19 zero yet?
	brne	checkParity+1	; no? loop and skip first LDI
	andi	r16, 0x1	; Sets Z accordingly
	ret


Collapse OS and its documentation are created by Virgil Dupras and licensed under the GNU GPL v3.

This documentation browser by James Stanley. Please report bugs on github or to james@incoherency.co.uk.

This page generated at 2024-12-25 21:05:04 from documentation in CollapseOS snapshot 20230427.