Overview
Read keyboard input by polling the I/O ports directly. The Spectrum's keyboard is arranged in a matrix of 8 rows, each accessible via a specific port address. Keys are active low (0 = pressed). Use when you need responsive input without relying on the ROM.
Code
; =============================================================================
; KEYBOARD READING - ZX SPECTRUM
; Read keyboard matrix via I/O ports
; Taught: Game 1 (Ink War), Unit 1
; CPU: ~50 cycles | Memory: ~40 bytes
; =============================================================================
KEY_PORT equ $fe
; Keyboard row addresses (active low)
ROW_QAOP equ $fb ; Q W E R T (bits: T R E W Q)
ROW_ASDF equ $fd ; A S D F G (bits: G F D S A)
ROW_YUIOP equ $df ; Y U I O P (bits: P O I U Y)
ROW_12345 equ $f7 ; 1 2 3 4 5 (bits: 5 4 3 2 1)
ROW_09876 equ $ef ; 0 9 8 7 6 (bits: 6 7 8 9 0)
ROW_SPACE equ $7f ; Space, Sym, M, N, B
; Read keyboard and return direction code
; Returns: A = 0 (none), 1 (up), 2 (down), 3 (left), 4 (right)
read_keyboard:
xor a
ld (key_pressed), a ; Clear previous
; Check Q (up)
ld a, ROW_QAOP
in a, (KEY_PORT)
bit 0, a ; Q is bit 0
jr nz, .not_q
ld a, 1
ld (key_pressed), a
ret
.not_q:
; Check A (down)
ld a, ROW_ASDF
in a, (KEY_PORT)
bit 0, a ; A is bit 0
jr nz, .not_a
ld a, 2
ld (key_pressed), a
ret
.not_a:
; Check O (left)
ld a, ROW_YUIOP
in a, (KEY_PORT)
bit 1, a ; O is bit 1
jr nz, .not_o
ld a, 3
ld (key_pressed), a
ret
.not_o:
; Check P (right)
ld a, ROW_YUIOP
in a, (KEY_PORT)
bit 0, a ; P is bit 0
jr nz, .not_p
ld a, 4
ld (key_pressed), a
.not_p:
ret
key_pressed:
defb 0
Usage:
main_loop:
halt ; Wait for frame
call read_keyboard
ld a, (key_pressed)
or a
jr z, main_loop ; No key pressed
cp 1
jr z, handle_up
cp 2
jr z, handle_down
; ... etc
jr main_loop
Trade-offs
| Aspect | Cost |
|---|---|
| CPU | ~50 cycles per read |
| Memory | ~40 bytes |
| Limitation | No debouncing (keys may auto-repeat) |
When to use: Any game needing keyboard input. ROM-free approach works on all Spectrums.
When to avoid: When you need debounced input for menus or single-press detection.
Keyboard Matrix Reference
| Port | Bit 0 | Bit 1 | Bit 2 | Bit 3 | Bit 4 |
|---|---|---|---|---|---|
| $F7 | 1 | 2 | 3 | 4 | 5 |
| $EF | 0 | 9 | 8 | 7 | 6 |
| $FB | Q | W | E | R | T |
| $FD | A | S | D | F | G |
| $DF | P | O | I | U | Y |
| $BF | Enter | L | K | J | H |
| $7F | Space | Sym | M | N | B |
| $FE | Shift | Z | X | C | V |
Keyboard ghosting
The Spectrum's keyboard is a passive matrix — keys connect rows to columns. When three or more keys in the same row are held, the matrix returns ambiguous data. Worse, holding two keys in different rows can produce a "phantom" third key reading at the intersection point.
For games that need multi-key combos (e.g. "diagonal-up + fire"), pick keys that span different rows. The classic Spectrum control scheme — Q/A (up/down on different rows) + O/P (left/right on the same row) + Space (separate row) — distributes inputs across the matrix to minimise ghosting.
Reading multiple keys at once
The pattern above returns the first key it finds. To check all directions plus fire on the same frame, store each result rather than returning early:
read_all_keys:
ld a, 0
ld (key_state), a
; Check Q (up): row $FB, bit 0
ld a, $fb
in a, ($fe)
bit 0, a
jr nz, .not_q
ld hl, key_state
set 0, (hl) ; Bit 0 of state = up
.not_q:
; ... continue for each key, setting different bits ...
ret
key_state: defb 0 ; Bit 0=up, 1=down, 2=left, 3=right, 4=fire
Related
Patterns: Game Loop (HALT)
Vault: ULA — port $FE keyboard matrix | ZX Spectrum