recipes/sms/kbd: PS/2 keyboard adapter for the SMS!

This commit is contained in:
Virgil Dupras 2019-07-19 15:45:13 -04:00
parent 23354eba94
commit 57e7b3ca05
11 changed files with 682 additions and 36 deletions

View File

@ -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

View File

@ -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

102
kernel/sms/kbd.asm Normal file
View File

@ -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

View File

@ -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

View File

@ -35,3 +35,8 @@ init:
call stdioInit
call shellInit
jp shellLoop
KBD_FETCHKC:
in a, (KBD_PORT)
ret

View File

@ -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.

View File

@ -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

25
recipes/sms/kbd/Makefile Normal file
View File

@ -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

117
recipes/sms/kbd/README.md Normal file
View File

@ -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

57
recipes/sms/kbd/glue.asm Normal file
View File

@ -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

345
recipes/sms/kbd/ps2ctl.asm Normal file
View File

@ -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<<ISC01)
out MCUCR, r16
; Enable INT0
ldi r16, (1<<INT0)
out GIMSK, 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 1MHZ, no prescaling is necessary. Each TCNT0 tick is
; already 1us long.
ldi r16, (1<<CS00) ; no prescaler
out TCCR0B, r16
; init DDRB
sbi DDRB, CP
cbi PORTB, LR
sbi DDRB, LR
sei
loop:
brts processbit ; flag T set? we have a bit to process
cp YL, ZL ; if YL == ZL, buffer is empty
brne sendTo164 ; 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 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<<TOV0)
out TIFR, r16
ret
; Send the value of r19 to the PS/2 keyboard
sendToPS2:
cli
; 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.
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