From 57e7b3ca05d1805169390882a903002564989081 Mon Sep 17 00:00:00 2001 From: Virgil Dupras Date: Fri, 19 Jul 2019 15:45:13 -0400 Subject: [PATCH] recipes/sms/kbd: PS/2 keyboard adapter for the SMS! --- README.md | 38 +--- kernel/kbd.asm | 12 +- kernel/sms/kbd.asm | 102 ++++++++++ recipes/rc2014/README.md | 1 + recipes/rc2014/ps2/glue.asm | 5 + recipes/rc2014/ps2/ps2ctl.asm | 6 +- recipes/sms/README.md | 10 +- recipes/sms/kbd/Makefile | 25 +++ recipes/sms/kbd/README.md | 117 ++++++++++++ recipes/sms/kbd/glue.asm | 57 ++++++ recipes/sms/kbd/ps2ctl.asm | 345 ++++++++++++++++++++++++++++++++++ 11 files changed, 682 insertions(+), 36 deletions(-) create mode 100644 kernel/sms/kbd.asm create mode 100644 recipes/sms/kbd/Makefile create mode 100644 recipes/sms/kbd/README.md create mode 100644 recipes/sms/kbd/glue.asm create mode 100644 recipes/sms/kbd/ps2ctl.asm diff --git a/README.md b/README.md index 0131ec4..080fd05 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,12 @@ Collapse OS is a z80 kernel and a collection of programs, tools and documentation that allows you to assemble an OS that, when completed, will be able to: -1. Run on an extremely minimal and improvised architecture. -2. Communicate through a improvised serial interface linked to some kind of - improvised terminal. +1. Run on minimal and improvised machines. +2. Interface through improvised means (serial, keyboard, display). 3. Edit text files. 4. Compile assembler source files for a wide range of MCUs and CPUs. -5. Write files to a wide range of flash ICs and MCUs. -6. Access data storage from improvised systems. -7. Replicate itself. +5. Read and write from a wide range of storage devices. +6. Replicate itself. Additionally, the goal of this project is to be as self-contained as possible. With a copy of this project, a capable and creative person should be able to @@ -21,27 +19,6 @@ manage to build and install Collapse OS without external resources (i.e. internet) on a machine of her design, built from scavenged parts with low-tech tools. -## Status - -The project unfinished but is progressing well! Highlights: - -* Self replicates: Can assemble itself from within itself, given enough RAM and - storage. -* Has a shell that can poke memory, I/O, call arbitrary code from memory. -* Can "upload" code from serial link into memory and execute it. -* Can manage multiple "block devices". -* Can read and write to SD cards. -* A z80 assembler, written in z80 that is self-assembling and can assemble the - whole project. -* Compact: - * Kernel: 3K binary, 1800 SLOC. - * ZASM: 4K binary, 2300 SLOC, 16K RAM usage to assemble kernel or itself. -* Extremely flexible: Kernel parts are written as loosely knit modules that - are bound through glue code. This makes the kernel adaptable to many unforseen - situations. -* From a GNU environment, can be built with minimal tooling: only - [libz80][libz80], make and a C compiler are needed. - ## Organisation of this repository * `kernel`: Pieces of code to be assembled by the user into a kernel. @@ -55,9 +32,10 @@ The project unfinished but is progressing well! Highlights: Each folder has a README with more details. -## More information +## Status -Go to [Collapse OS' website](https://collapseos.org) for more information on the -project. +The project unfinished but is progressing well! See [Collapse OS' website][web] +for more information. [libz80]: https://github.com/ggambetta/libz80 +[web]: https://collapseos.org diff --git a/kernel/kbd.asm b/kernel/kbd.asm index 7018046..c96d632 100644 --- a/kernel/kbd.asm +++ b/kernel/kbd.asm @@ -1,13 +1,17 @@ ; kbd - implement GetC for PS/2 keyboard ; -; Status: Work in progress. See recipes/rc2014/ps2 +; It reads raw key codes from a FetchKC routine and returns, if appropriate, +; a proper ASCII char to type. See recipes rc2014/ps2 and sms/kbd. ; ; *** Defines *** -; The port of the device where we read scan codes. See recipe rc2014/ps2. -; KBD_PORT +; Pointer to a routine that fetches the last typed keyword in A. Should return +; 0 when nothing was typed. +; KBD_FETCHKC ; *** Variables *** .equ KBD_SKIP_NEXT KBD_RAMSTART +; Pointer to a routine that fetches the last typed keyword in A. Should return +; 0 when nothing was typed. .equ KBD_RAMEND KBD_SKIP_NEXT+1 kbdInit: @@ -16,7 +20,7 @@ kbdInit: ret kbdGetC: - in a, (KBD_PORT) + call KBD_FETCHKC or a jr z, .nothing diff --git a/kernel/sms/kbd.asm b/kernel/sms/kbd.asm new file mode 100644 index 0000000..8a3a178 --- /dev/null +++ b/kernel/sms/kbd.asm @@ -0,0 +1,102 @@ +; kbd - implement FetchKC for SMS PS/2 adapter +; +; Implements KBD_FETCHKC for the adapter described in recipe sms/kbd. It does +; so for both Port A and Port B (you hook whichever you prefer). + +; FetchKC on Port A +smskbdFetchKCA: + ; Before reading a character, we must first verify that there is + ; something to read. When the adapter is finished filling its '164 up, + ; it resets the latch, which output's is connected to TL. When the '164 + ; is full, TL is low. + ; Port A TL is bit 4 + in a, (0xdc) + and 0b00010000 + jr nz, .nothing + + push bc + in a, (0x3f) + ; Port A TH output, low + ld a, 0b11011101 + out (0x3f), a + nop + nop + in a, (0xdc) + ; bit 3:0 are our dest bits 3:0. handy... + and 0b00001111 + ld b, a + ; Port A TH output, high + ld a, 0b11111101 + out (0x3f), a + nop + nop + in a, (0xdc) + ; bit 3:0 are our dest bits 7:4 + rlca \ rlca \ rlca \ rlca + and 0b11110000 + or b + ex af, af' + ; Port A/B reset + ld a, 0xff + out (0x3f), a + ex af, af' + pop bc + ret + +.nothing: + xor a + ret + +; FetchKC on Port B +smskbdFetchKCB: + ; Port B TL is bit 2 + in a, (0xdd) + and 0b00000100 + jr nz, .nothing + + push bc + in a, (0x3f) + ; Port B TH output, low + ld a, 0b01110111 + out (0x3f), a + nop + nop + in a, (0xdc) + ; bit 7:6 are our dest bits 1:0 + rlca \ rlca + and 0b00000011 + ld b, a + in a, (0xdd) + ; bit 1:0 are our dest bits 3:2 + rlca \ rlca + and 0b00001100 + or b + ld b, a + ; Port B TH output, high + ld a, 0b11110111 + out (0x3f), a + nop + nop + in a, (0xdc) + ; bit 7:6 are our dest bits 5:4 + rrca \ rrca + and 0b00110000 + or b + ld b, a + in a, (0xdd) + ; bit 1:0 are our dest bits 7:6 + rrca \ rrca + and 0b11000000 + or b + ex af, af' + ; Port A/B reset + ld a, 0xff + out (0x3f), a + ex af, af' + pop bc + ret + +.nothing: + xor a + ret + diff --git a/recipes/rc2014/README.md b/recipes/rc2014/README.md index dc90fc9..d63f315 100644 --- a/recipes/rc2014/README.md +++ b/recipes/rc2014/README.md @@ -26,6 +26,7 @@ are other recipes related to the RC2014: * [Writing to a AT28 from Collapse OS](eeprom/README.md) * [Accessing a MicroSD card](sdcard/README.md) * [Assembling binaries](zasm/README.md) +* [Interfacing a PS/2 keyboard](ps2/README.md) ## Goal diff --git a/recipes/rc2014/ps2/glue.asm b/recipes/rc2014/ps2/glue.asm index 2dc74cd..b36db91 100644 --- a/recipes/rc2014/ps2/glue.asm +++ b/recipes/rc2014/ps2/glue.asm @@ -35,3 +35,8 @@ init: call stdioInit call shellInit jp shellLoop + +KBD_FETCHKC: + in a, (KBD_PORT) + ret + diff --git a/recipes/rc2014/ps2/ps2ctl.asm b/recipes/rc2014/ps2/ps2ctl.asm index 7728f0d..e328120 100644 --- a/recipes/rc2014/ps2/ps2ctl.asm +++ b/recipes/rc2014/ps2/ps2ctl.asm @@ -74,7 +74,6 @@ ; - 1: receiving data ; - 2: awaiting parity bit ; - 3: awaiting stop bit -; it reaches 11, we know we're finished with the frame. ; 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 @@ -167,9 +166,14 @@ 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. diff --git a/recipes/sms/README.md b/recipes/sms/README.md index 7d4d772..a37a4fe 100644 --- a/recipes/sms/README.md +++ b/recipes/sms/README.md @@ -12,6 +12,13 @@ platform and this is where most of my information comes from. This platform is tight on RAM. It has 8k of it. However, if you have extra RAM, you can put it on your cartridge. +## Related recipes + +This recipe is for installing a minimal Collapse OS system on the SMS. There +are other recipes related to the SMS: + +* [Interfacing a PS/2 keyboard](kbd/README.md) + ## Gathering parts * [zasm][zasm] @@ -47,7 +54,8 @@ D-Pad is used as follow: * Start button is like pressing Return. Of course, that's not a fun way to enter text, but using the D-Pad is the -easiest way to get started. I'm working on a PS/2 keyboard adapter for the SMS. +easiest way to get started which doesn't require soldering. Your next step after +that would be to [build a PS/2 keyboard adapter!](kbd/README.md) [smspower]: http://www.smspower.org [everdrive]: https://krikzz.com diff --git a/recipes/sms/kbd/Makefile b/recipes/sms/kbd/Makefile new file mode 100644 index 0000000..831e37f --- /dev/null +++ b/recipes/sms/kbd/Makefile @@ -0,0 +1,25 @@ +PROGNAME = ps2ctl +AVRDUDEMCU ?= t45 +AVRDUDEARGS ?= -c usbtiny -P usb +TARGETS = $(PROGNAME).hex os.sms +ZASM = ../../../tools/zasm.sh +KERNEL = ../../../kernel + +# Rules + +.PHONY: send all clean + +all: $(TARGETS) + @echo Done! + +send: $(PROGNAME).hex + avrdude $(AVRDUDEARGS) -p $(AVRDUDEMCU) -U flash:w:$< + +$(PROGNAME).hex: $(PROGNAME).asm + avra -o $@ $< + +os.sms: glue.asm + $(ZASM) $(KERNEL) < $< > $@ + +clean: + rm -f $(TARGETS) *.eep.hex *.obj os.bin diff --git a/recipes/sms/kbd/README.md b/recipes/sms/kbd/README.md new file mode 100644 index 0000000..8adfcf4 --- /dev/null +++ b/recipes/sms/kbd/README.md @@ -0,0 +1,117 @@ +# PS/2 keyboard on the SMS + +Using the shell with a D-pad on the SMS is doable, but not fun at all! We're +going to build an adapter for a PS/2 keyboard to plug as a SMS controller. + +The PS/2 logic will be the same as the [RC2014's PS/2 adapter][rc2014-ps2] but +instead of interfacing directly with the bus, we interface with the SMS' +controller subsystem (that is, what we poke on ports `0x3f` and `0xdc`). + +How will we achieve that? A naive approach would be "let's limit ourselves to +7bit ASCII and put `TH`, `TR` and `TL` as inputs". That could work, except that +the SMS will have no way reliable way (except timers) of knowing whether polling +two identical values is the result of a repeat character or because there is no +new value yet. + +On the AVR side, there's not way to know whether the value has been read, so we +can't to like on the RC2014 and reset the value to zero when a `RO` request is +made. + +We need communication between the SMS and the PS/2 adapter to be bi-directional. +That bring the number of usable pins down to 6, a bit low for a proper character +range. So we'll fetch each character in two 4bit nibbles. `TH` is used to select +which nibble we want. + +`TH` going up also tells the AVR MCU that we're done reading the character and +that the next one can come up. + +As always, the main problem is that the AVR MCU is too slow to keep up with the +rapid z80 polling pace. In the RC2014 adapter, I hooked `CE` directly on the +AVR, but that was a bit tight because the MCU is barely fast enough to handle +this signal properly. I did that because I had no proper IC on hand to build a +SR latch. + +In this recipe, I do have a SR latch on hand, so I'll use it. `TH` triggering +will also trigger that latch, indicating to the MCU that it can load the next +character in the '164. When it's done, we signal the SMS that the next char is +ready by reseting the latch. That means that we have to hook the latch's output +to `TR`. + +Nibble selection on `TH` doesn't involve the AVR at all. All 8 bits are +pre-loaded on the '164. We use a 4-channel multiplexer to make `TH` select +either the low or high bits. + +## Gathering parts + +* A SMS that can run Collapse OS +* 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. +* A SMS controller you can cannibalize for the DB-9 connection. A stock DB-9 + connector isn't deep enough. +* ATtiny85/45/25 (main MCU for the device) +* 74xx164 (shift register) +* 74xx157 (multiplexer) +* A NOR SR-latch. I used a 4043. +* Proto board, wires, IC sockets, etc. +* [AVRA][avra] + +## Historical note + +As I was building this prototype, I was wondering how I would debug it. I could +obviously not hope for it to work as a keyboard adapter on the first time, right +on port A, driving the shell. I braced myself mentally for a logic analyzer +session and some kind of arduino-based probe to test bit banging results. + +And then I thought "why not use the genesis?". Sure, driving the shell with the +D-pad isn't fun at all, but it's possible. So I hacked myself a temporary debug +kernel with a "a" command doing a probe on port B. It worked really well! + +It was a bit less precise than logic analyzers and a bit of poking-around and +crossing-fingers was involved, but overall, I think it was much less effort +than creating a full test setup. + +There's a certain satisfaction to debug a device entirely on your target +machine... + +## Building the PS/2 interface + +(schematic incoming, I have yet to scan it.) + +The PS/2-to-AVR part is indentical to the rc2014/ps2 recipe. Refer to this +recipe. + +We control the '164 from the AVR in a similar way to what we did in rc2014/ps2, +that is, sharing the DATA line with PS/2 (PB1). We clock the '164 with PB3. +Because the '164, unlike the '595, is unbuffered, no need for special RCLK +provisions. + +Most of the wiring is between the '164 and the '157. Place them close. The 4 +outputs on the '157 are hooked to the first 4 lines on the DB-9 (Up, Down, Left, +Right). + +In my prototype, I placed a 1uf decoupling cap next to the AVR. I used a 10K +resistor as a pull-down for the TH line (it's not always driven). + +If you use a 4043, don't forget to wire EN. On the '157, don't forget to wire +~G. + +The code expects a SR-latch that works like a 4043, that is, S and R are +triggered high, S makes Q high, R makes Q low. R is hooked to PB4. S is hooked +to TH (and also the A/B on the '157). Q is hooked to PB0 and TL. + +## Usage + +The code in this recipe is set up to listen to the keyboard on port B, leaving +port A to drive, for example, an Everdrive with a D-pad. Unlike the generic +SMS recipe, this kernel has no character selection mechanism. It acts like a +regular shell, taking input from the keyboard. + +`kernel/sms/kbd.asm` also has a FetchKC implementation for port A if you prefer. +Just hook it on. I've tried it, it works. + +Did you get there? Feels pretty cool huh? + +[rc2014-ps2]: ../../rc2014/ps2 +[avra]: https://github.com/hsoft/avra diff --git a/recipes/sms/kbd/glue.asm b/recipes/sms/kbd/glue.asm new file mode 100644 index 0000000..8599268 --- /dev/null +++ b/recipes/sms/kbd/glue.asm @@ -0,0 +1,57 @@ +; 8K of onboard RAM +.equ RAMSTART 0xc000 +; Memory register at the end of RAM. Must not overwrite +.equ RAMEND 0xfdd0 + + jp init + +.fill 0x66-$ + retn + +#include "err.h" +#include "core.asm" +#include "parse.asm" + +#include "sms/kbd.asm" +.equ KBD_RAMSTART RAMSTART +.equ KBD_FETCHKC smskbdFetchKCA +#include "kbd.asm" + +.equ VDP_RAMSTART KBD_RAMEND +#include "sms/vdp.asm" + +.equ STDIO_RAMSTART VDP_RAMEND +#include "stdio.asm" + +.equ SHELL_RAMSTART STDIO_RAMEND +.equ SHELL_EXTRA_CMD_COUNT 0 +#include "shell.asm" + +init: + di + im 1 + + ld sp, RAMEND + + ; Initialize the keyboard latch by "dummy reading" once. This ensures + ; that the adapter knows it can fill its '164. + ; Port B TH output, high + ld a, 0b11110111 + out (0x3f), a + nop + ; Port A/B reset + ld a, 0xff + out (0x3f), a + + call kbdInit + call vdpInit + + ld hl, kbdGetC + ld de, vdpPutC + call stdioInit + call shellInit + jp shellLoop + +.fill 0x7ff0-$ +.db "TMR SEGA", 0x00, 0x00, 0xfb, 0x68, 0x00, 0x00, 0x00, 0x4c + diff --git a/recipes/sms/kbd/ps2ctl.asm b/recipes/sms/kbd/ps2ctl.asm new file mode 100644 index 0000000..9ba62b2 --- /dev/null +++ b/recipes/sms/kbd/ps2ctl.asm @@ -0,0 +1,345 @@ +.include "tn45def.inc" + +; Receives keystrokes from PS/2 keyboard and send them to the '164. On the PS/2 +; side, it works the same way as the controller in the rc2014/ps2 recipe. +; However, in this case, what we have on the other side isn't a z80 bus, it's +; the one of the two controller ports of the SMS through a DB9 connector. + +; The PS/2 related code is copied from rc2014/ps2 without much change. The only +; differences are that it pushes its data to a '164 instead of a '595 and that +; it synchronizes with the SMS with a SR latch, so we don't need PCINT. We can +; also afford to run at 1MHz instead of 8. + +; *** 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. +; +; 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 '164 +; 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 CP = PINB3 +; SR-Latch's Q pin +.equ LQ = PINB0 +; SR-Latch's R pin +.equ LR = PINB4 + +; init value for TCNT0 so that overflow occurs in 100us +.equ TIMER_INITVAL = 0x100-100 + +; *** Code *** + + rjmp main + rjmp hdlINT0 + +; 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 + +main: + ldi r16, low(RAMEND) + out SPL, r16 + ldi r16, high(RAMEND) + out SPH, r16 + + ; init variables + clr r18 + out GPIOR0, r18 + + ; Setup int0 + ; INT0, falling edge + ldi r16, (1< 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 the value of r20 to the '164 +sendTo164: + sbis PINB, LQ ; LQ is set? we can send the next byte + rjmp loop ; Even if we have something in the buffer, we + ; can't: the SMS hasn't read our previous + ; buffer yet. + ; 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 + +sendTo164Loop: + cbi PORTB, DATA + sbrc r20, 7 ; if leftmost bit isn't cleared, set DATA high + sbi PORTB, DATA + ; toggle CP + cbi PORTB, CP + lsl r20 + sbi PORTB, CP + dec r16 + brne sendTo164Loop ; not zero yet? loop + + ; release PS/2 + cbi DDRB, DATA + sei + + ; Reset the latch to indicate that the next number is ready + sbi PORTB, LR + cbi PORTB, LR + rjmp loop + +resetTimer: + ldi r16, TIMER_INITVAL + out TCNT0, r16 + ldi r16, (1< 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. + sei + 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 +