Skip to content

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

asm
  jsr ReadJoystick1             ; A = the state of stick 1
  jsr ReadJoystick2             ; A = the state of stick 2

One 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. The include doesn't name the bits, so the names below are the ones the program further down gives them itself.

BitMask
7JOY_RRight
6JOY_LLeft
5JOY_DDown
4JOY_UUp
3JOY_YButton Y
2JOY_XButton X
1JOY_BButton B
0JOY_AButton A

So the test for "up is held" is that the bit came back zero:

asm
  jsr ReadJoystick1
  and #JOY_U
  beq MovingUp                  ; zero means held

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

The joystick byte: eight bits, and a held control reads zero One byte from JOY(1) bit 7 Right $80 bit 6 Left $40 bit 5 Down $20 bit 4 Up $10 bit 3 Button Y $08 bit 2 Button X $04 bit 1 Button B $02 bit 0 Button A $01 nothing held11111111$FF pushed up11101111$EF A held direction or button reads 0, not 1. That is why the test is IF (JOY(1) AND 16) = 0
Held reads 0. So AND with the mask followed by BEQ is the player pushing up, and BNE is the bug.

A program that reads both

asm
; 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-VDP.inc"

.segment "CODE"

BasicStartup:
  .byte $0A, $08, $0A, $00, $A5, $32, $30, $36, $30, $00, $00, $00

; One bit per direction and button. The include names the routines that read a
; stick but not the bits, so they are named here.
JOY_R = %10000000
JOY_L = %01000000
JOY_D = %00100000
JOY_U = %00010000
JOY_Y = %00001000
JOY_X = %00000100
JOY_B = %00000010
JOY_A = %00000001

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 ", $00
RUN
HOLD A STICK, THEN PRESS ENTER

STICK 1: NOTHING
STICK 2: NOTHING

OK
With no stick plugged in, both ports read NOTHING — which is the answer, not a failure. Open the full emulator

Hold 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:

KBDisableTell both encoders to release the ports, then wait for them to
KBEnableGive 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:

asm
  jsr KBDisable                 ; one settle, not two
  lda GPIO_PORTB                ; stick 1, raw
  sta Stick1
  lda GPIO_PORTA                ; stick 2, raw
  sta Stick2
  jsr KBEnable

That 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:

asm
  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 held

The 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:

asm
  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 joystick

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

Written for BIOS v2.0. Released under the MIT License.