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.

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

STICK 1: NOTHING
STICK 2: NOTHING

OK

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.

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:

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

Next: files on the memory card.

Written for BIOS v1.5. Released under the MIT License.