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.
Keys are deaf while the ports are released
Between KBDisable and KBEnable the keyboards cannot report anything. The window is short — a few hundred microseconds — but do not put anything slow inside it, and do not leave the encoders disabled while you draw a frame.
Reading the keyboard as a keyboard
There is no key-down/key-up interface: the encoders hand over finished ASCII characters, not scan codes. That means you cannot ask "is the space bar held right now", which is occasionally what a game wants.
The usual answer is a joystick — that is what they are for. The other 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 heldNext: files on the memory card.

