diff --git a/recipes/rc2014/ps2/Makefile b/recipes/rc2014/ps2/Makefile new file mode 100644 index 0000000..e9513f9 --- /dev/null +++ b/recipes/rc2014/ps2/Makefile @@ -0,0 +1,22 @@ +PROGNAME = ps2ctl +AVRDUDEMCU ?= t45 +AVRDUDEARGS ?= -c usbtiny -P usb +TARGETS = $(PROGNAME).hex + +# Rules + +.PHONY: send all clean + +all: $(TARGETS) + @echo Done! + +send: $(PROGNAME).hex + avrdude $(AVRDUDEARGS) -p $(AVRDUDEMCU) -U flash:w:$< + +$(PROGNAME).hex: $(PROGNAME).asm +$(TARGETS): + avra -o $@ $< + +clean: + rm -f $(TARGETS) *.eep.hex *.obj + diff --git a/recipes/rc2014/ps2/README.md b/recipes/rc2014/ps2/README.md new file mode 100644 index 0000000..e64ba2d --- /dev/null +++ b/recipes/rc2014/ps2/README.md @@ -0,0 +1,51 @@ +# Interfacing a PS/2 keyboard + +Serial connection through ACIA is nice, but you are probably plugging a modern +computer on the other side of that ACIA, right? Let's go a step further away +from those machines and drive a PS/2 keyboard directly! + +## Goal + +Have a PS/2 keyboard drive the stdio input of the Collapse OS shell instead of +the ACIA. + +**Status: work in progress** + +## Gathering parts + +* A RC2014 Classic that could install the base recipe +* A PS/2 keyboard. A USB keyboard + PS/2 adapter should work, but I haven't + tried it yet. +* A PS/2 female connector. Not so readily available, at least not on digikey. I + de-soldered mine from an old motherboard I had laying around. +* ATtiny85/45/25 (main MCU for the device) +* 74xx595 (shift register) +* 40106 inverter gates +* Diodes for `A*`, `IORQ`, `RO`. +* Proto board, RC2014 header pins, wires, IC sockets, etc. +* [AVRA][avra] + +## Building the PS/2 interface + +TODO. I have yet to draw presentable schematics. By reading `ps2ctl.asm`, you +might be able to guess how things are wired up. + +It's rather straigtforward: the attiny reads serial data from PS/2 and then +sends it to the 595. The 595 is wired straight to D7:0 with its `OE` wired to +address selection + `IORQ` + `RO` + +## Using the PS/2 interface + +As of now, the interface is incomplete and can only be queried through the +shell's `iord`. I've set my device up for addr `8` (that is, I wired `A3` +through the inverter, the rest through diodes, and hooked this pudding to `OE`). + +When doing `iord 8` in the shell, I get the scan code of the last key I pressed, +unless the 595 was "busy" with another code. For example, if I press `A`, my +next `iord 8` will yield `1C` (the "make" code for "A" in the PS/2 protocol). + +Doing a second `iord 8` right after a first will yield `0`, indicating that the +device properly detect the first reading attempt and properly flushes the value +from the 595. + +[avra]: https://github.com/hsoft/avra diff --git a/recipes/rc2014/ps2/ps2ctl.asm b/recipes/rc2014/ps2/ps2ctl.asm new file mode 100644 index 0000000..c7205c8 --- /dev/null +++ b/recipes/rc2014/ps2/ps2ctl.asm @@ -0,0 +1,241 @@ +.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 whole SRAM (from SRAM_START to RAMEND) is used as a scan code buffer, with +; Z chasing Y. When Y == Z, the buffer is empty. When RAMEND 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 R2 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 *** +; +; R1: when set, indicates that value in R17 is valid +; R2: 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 +; it reaches 11, we know we're finished with the frame. +; R19: 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. +; 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 last scan code pushed to the 595 +; +; *** Constants *** +; +.equ CLK = PINB2 +.equ DATA = PINB1 +.equ SRCLK = PINB3 +.equ CE = PINB4 +.equ RCLK = PINB0 + + rjmp main + rjmp hdlINT0 + rjmp hdlPCINT + +; Read DATA and set R19 if high. Then, set flag T. +; no SREG fiddling because no SREG-modifying instruction +hdlINT0: + sbic PINB, DATA ; DATA clear? skip next + ser r19 + 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. +hdlPCINT: + ; SRCLR has been triggered. Let's trigger RCLK too. + sbi PORTB, RCLK + cbi PORTB, RCLK + clr r2 ; 595 is now free + +main: + ldi r16, low(RAMEND) + out SPL, r16 + ldi r16, high(RAMEND) + out SPH, r16 + + ; Set clock prescaler to 1 (8MHz) + ldi r16, (1<