The keyboard and the sticks
Both plug into the same card — a VIA at $9400 with a microcontroller on each of its two ports. Port B carries the board's own keyboard and joystick 1; port A carries a PS/2 keyboard and joystick 2.
Keys
You have already met the input side: keys arrive as interrupts, land in the ring buffer at $0200, and come out through Chrin and BufferSize. Nothing else is needed for typing.
Both keyboards work at once, and so does the serial port. All three feed the same buffer, and your program cannot tell which one a character came from — which is exactly what you want.
InitKB sets the ports up and enables the two interrupts. The Kernal has already called it by the time your program runs.
Sticks
jsr ReadJoystick1 ; A = the state of stick 1
jsr ReadJoystick2 ; A = the state of stick 2One byte each, and every bit is upside down: a bit reads 1 while nothing is happening and drops to 0 while that direction or button is held.
| Bit | Mask | |
|---|---|---|
| 7 | JOY_R | Right |
| 6 | JOY_L | Left |
| 5 | JOY_D | Down |
| 4 | JOY_U | Up |
| 3 | JOY_Y | Button Y |
| 2 | JOY_X | Button X |
| 1 | JOY_B | Button B |
| 0 | JOY_A | Button A |
So the test for "up is held" is that the bit came back zero:
jsr ReadJoystick1
and #JOY_U
beq MovingUp ; zero means heldGet this backwards and your game runs in every direction at once until someone touches the stick, which is at least an easy bug to recognize.
Diagonals are free
Two bits can be low at the same time, so testing them one at a time gets you diagonals with no extra work. Test the pairs, not a switch statement.
A program that reads both
; Reading the joysticks.
;
; A stick reports the opposite of what you would guess: a bit reads 1 while
; nothing is happening and drops to 0 while a direction or a button is held.
; So the test for "up is held" is that the up bit came back zero.
.setcpu "65C02"
.include "6502.inc"
.segment "CODE"
BasicStartup:
.byte $0A, $08, $0A, $00, $A5, $32, $30, $36, $30, $00, $00, $00
Reading := $40
Anything := $41
Start:
lda #<Prompt
ldy #>Prompt
jsr PrintStr
Waiting:
jsr Chrin
bcc Waiting
cmp #CHAR_CR
bne Waiting
jsr PrintCRLF
lda #<One
ldy #>One
jsr PrintStr
jsr ReadJoystick1
jsr Report
lda #<Two
ldy #>Two
jsr PrintStr
jsr ReadJoystick2
jsr Report
rts
; Name every direction and button that is being held. A = the stick reading.
Report:
sta Reading
stz Anything
ldx #0
@next:
lda Mask,x
and Reading
bne @skip ; bit still high — that one is not held
inc Anything
lda Name_lo,x
ldy Name_hi,x
jsr PrintStr
@skip:
inx
cpx #8
bne @next
lda Anything
bne @done
lda #<Nothing
ldy #>Nothing
jsr PrintStr
@done:
jmp PrintCRLF
Mask:
.byte JOY_U, JOY_D, JOY_L, JOY_R, JOY_A, JOY_B, JOY_X, JOY_Y
Name_lo:
.lobytes Up, Down, LeftWay, RightWay, ButtonA, ButtonB, ButtonX, ButtonY
Name_hi:
.hibytes Up, Down, LeftWay, RightWay, ButtonA, ButtonB, ButtonX, ButtonY
Prompt: .byte "HOLD A STICK, THEN PRESS ENTER", CHAR_CR, CHAR_LF, $00
One: .byte "STICK 1: ", $00
Two: .byte "STICK 2: ", $00
Nothing: .byte "NOTHING", $00
Up: .byte "UP ", $00
Down: .byte "DOWN ", $00
LeftWay: .byte "LEFT ", $00
RightWay: .byte "RIGHT ", $00
ButtonA: .byte "A ", $00
ButtonB: .byte "B ", $00
ButtonX: .byte "X ", $00
ButtonY: .byte "Y ", $00RUN
HOLD A STICK, THEN PRESS ENTER
STICK 1: NOTHING
STICK 2: NOTHING
OKHold a direction while you press Enter and it names it. Two things about the structure are worth stealing: a mask table beside a table of names turns eight ifs into a loop, and the Anything counter is how you know to print "nothing" without testing the byte twice.
What reading a stick actually does
The two ports are normally being driven by the keyboard controllers. To read a joystick they have to let go first, which is what these two do:
KBDisable | Tell both encoders to release the ports, then wait for them to |
KBEnable | Give the ports back |
ReadJoystick1 and ReadJoystick2 each do the whole dance — disable, read the port, enable — which is why calling both costs two settling delays.
If you are polling both sticks every frame, do it in one window instead:
jsr KBDisable ; one settle, not two
lda GPIO_PORTB ; stick 1, raw
sta Stick1
lda GPIO_PORTA ; stick 2, raw
sta Stick2
jsr KBEnableThat is a genuine saving in a game loop, and it is safe: while the encoders are released the ports are static, and nothing in the Kernal's interrupt handler touches them.
Nothing arrives while the ports are released
Between KBDisable and KBEnable the controller cannot hand a character over. A PS/2 keystroke is only delayed — it queues in the controller and lands when you give the port back — but the board's own keys are not being scanned at all. The window is short, a few hundred microseconds, and it should stay that way: do not put anything slow inside it, and do not leave the encoders disabled while you draw a frame.
Is that key held down?
Nothing above can tell you. The encoders hand over finished ASCII characters, not scan codes, so Chrin says a key was typed and never says it is still down — which is occasionally exactly what a game wants to know.
The cheap answer is to read the most recent key and let it decay:
jsr Chrin
bcc @NoKey ; nothing new, keep the old one
sta LastKey
lda #DECAY ; how long a key counts as "held"
sta KeyTimer
@NoKey:
lda KeyTimer
beq @Idle
dec KeyTimer ; still counts as heldThe better answer for a game is a joystick — that is what they are for.
Reading the matrix yourself
The real answer, when you want the keys themselves, is that the keyboard is an 8 × 8 grid hanging off the same sixteen lines, and KBDisable hands it to you along with the joystick ports. Drive one row low on port A, read the columns back on port B, and a 0 bit is a key held at that intersection:
jsr KBDisable ; both encoders let go of the ports
stz GPIO_PORTA ; the level first, then the driver — the other
lda #%10000000 ; way puts stale bits on the lines
sta GPIO_DDRA ; PA7 an output, the other seven left alone
ldx #10 ; let the lines follow, ~50 cycles
@Settle:
dex
bne @Settle
lda GPIO_PORTB ; the eight columns of row PA7
stz GPIO_DDRA ; rows back to inputs before letting go
pha
jsr KBEnable
pla
and #%00001000 ; PB3 — the space bar
beq @SpaceHeld ; zero means held, same as a joystickEight passes gets you all 67 keys, and the four the firmware ignores — Caps Lock, Menu, Alt, Fn — read like any other switch. The keyboard matrix says which row and column each key sits on.
There are no diodes in the grid
Three keys held in a rectangle report a phantom fourth at the free corner. A modifier or two alongside a key is fine; chords are not. Sweep the rows and give the ports back, too: a PS/2 keyboard goes on filling the controller's buffer while you hold them, and the controller's own scan of the board's keys is suspended until you do.
Next: files on the memory card.

