ZX Spectrum hardware ports
Talking to the ULA, the AY chip, and the joystick — one port at a time
The Spectrum's I/O port map: port $FE for keyboard, border, beeper, and tape on every model; port $1F for the Kempston joystick; ports $7FFD and $1FFD for 128K and +2A/+3 memory paging; ports $FFFD and $BFFD for the AY-3-8912 sound chip on 128K+ models. The technical reference Shadowkeep Unit 4 and Unit 7 read from.
Overview
The Z80 has a separate I/O address space — 256 ports addressed by the low 8 bits of the address bus on the simplest instructions, or by the full 16 bits when using IN A,(C) / OUT (C),A. On the Spectrum, almost every I/O operation is one of a small set of well-defined port addresses, decoded by the ULA or by add-on hardware. This entry catalogues the ports that matter for game development on the platform.
The most important port, by a long margin, is $FE — the ULA's single multi-purpose port. It carries the keyboard, the border colour, the beeper, and the tape interface, all on one address. Almost every Spectrum game touches $FE on every frame.
Port $FE - The Multi-Purpose Port
Port $FE is the most important port on the Spectrum. It controls border colour, reads the keyboard, controls the speaker, and handles tape I/O.
Port $FE Output (Writing)
Usage: OUT ($FE),A
Bit Layout:
| Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| Purpose | — | — | — | Speaker | Tape | Border | Border | Border |
| Value | x | x | x | MIC | EAR | Border | Border | Border |
Bit Functions:
- Bits 0-2: Border colour (0-7)
- Bit 3: Tape output (EAR socket)
- Bit 4: Speaker control (0 = off, 1 = on)
- Bits 5-7: Not used (but should be preserved)
Example - Change Border:
LD A,2 ; Red border
OUT ($FE),A
Example - Make Sound:
; Toggle speaker bit for simple beep
LD A,$10 ; Speaker on, black border
OUT ($FE),A
; Delay
LD A,$00 ; Speaker off
OUT ($FE),A
Example - Preserve Upper Bits:
; Read current value, modify border only
IN A,($FE) ; Read current port state
AND $F8 ; Clear border bits (0-2)
OR 5 ; Set to cyan (5)
OUT ($FE),A ; Write back
Port $FE Input (Reading)
Usage: IN A,($FE)
Bit Layout:
| Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| Purpose | — | EAR | — | Key | Key | Key | Key | Key |
| Active | reads 1 | tape input | reads 1 | row bit 4 | row bit 3 | row bit 2 | row bit 1 | row bit 0 |
Bit Functions:
- Bits 0-4: Keyboard row data (active low - 0 = pressed)
- Bit 5: Not used
- Bit 6: Tape input (EAR socket)
- Bit 7: Not used
Important: Keyboard reading requires setting the upper byte of BC to select keyboard half-row.
Keyboard Reading
The Spectrum keyboard is arranged in a matrix of 8 half-rows × 5 keys each.
Keyboard Matrix
To read keyboard:
- Set BC register: High byte selects half-row, low byte = $FE
- Read with
IN A,(C) - Test bits 0-4 (active low: 0 = pressed, 1 = not pressed)
Keyboard Half-Row Map
| BC High | Half-row | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
|---|---|---|---|---|---|---|
| $FE | Row 0 | V | C | X | Z | SHIFT |
| $FD | Row 1 | G | F | D | S | A |
| $FB | Row 2 | T | R | E | W | Q |
| $F7 | Row 3 | 5 | 4 | 3 | 2 | 1 |
| $EF | Row 4 | 6 | 7 | 8 | 9 | 0 |
| $DF | Row 5 | Y | U | I | O | P |
| $BF | Row 6 | H | J | K | L | ENTER |
| $7F | Row 7 | B | N | M | SYM | SPACE |
Reading Single Key
Example - Read Q key:
LD BC,$FBFE ; Half-row $FB (Q-T row)
IN A,(C) ; Read keyboard
BIT 0,A ; Test bit 0 (Q key)
JR Z,QPressed ; Jump if Q pressed (bit = 0)
Example - Read Arrow Keys (using QAOP):
; Read Q (up) key
LD BC,$FBFE ; Q-T row
IN A,(C)
BIT 0,A
JR Z,MoveUp
; Read A (down) key
LD BC,$FDFE ; A-G row
IN A,(C)
BIT 0,A
JR Z,MoveDown
; Read O (right) key
LD BC,$DFFE ; P-Y row
IN A,(C)
BIT 1,A
JR Z,MoveRight
; Read P (left) key
LD BC,$DFFE ; Same row
IN A,(C)
BIT 0,A
JR Z,MoveLeft
Reading Multiple Keys Simultaneously
The Spectrum supports multiple simultaneous key presses (within matrix limitations).
Example - Detect multiple keys:
; Read Q-T row
LD BC,$FBFE
IN A,(C)
LD D,A ; Save for later
BIT 0,D ; Q pressed?
CALL Z,HandleQ
BIT 1,D ; W pressed?
CALL Z,HandleW
Scan All Keys
Example - Detect any key press:
ScanKeyboard:
LD BC,$FEFE ; Start with row 0
LD D,8 ; 8 rows to scan
ScanLoop:
IN A,(C) ; Read row
AND $1F ; Mask to key bits
CP $1F ; All bits set = no keys
JR NZ,KeyFound ; Jump if any key pressed
RLC B ; Next row (rotate high byte)
DEC D
JR NZ,ScanLoop
; No keys found
RET
KeyFound:
; Key detected - A contains key bits
RET
Ghost Keys and Limitations
Matrix ghosting: Pressing certain 3-key combinations can register phantom 4th key.
Example problem:
- Press: Q + A + O
- Ghost: P may appear pressed
Solution: Use diagonally opposite keys in your control schemes (e.g., QAOP works well).
Speaker/Beeper ($FE Bit 4)
The Spectrum has a simple 1-bit speaker controlled by port $FE bit 4.
Simple Beep
Beep:
LD B,200 ; Duration counter
BeepLoop:
LD A,$10 ; Speaker on
OUT ($FE),A
LD E,50 ; Frequency delay
Delay1: DEC E
JR NZ,Delay1
LD A,$00 ; Speaker off
OUT ($FE),A
LD E,50
Delay2: DEC E
JR NZ,Delay2
DJNZ BeepLoop
RET
Variable Pitch Beep
; HL = pitch (higher = higher pitch)
; B = duration
ToneBeep:
ToneLoop:
LD A,$10
OUT ($FE),A
PUSH HL
ToneDelay1:
DEC HL
LD A,H
OR L
JR NZ,ToneDelay1
POP HL
LD A,$00
OUT ($FE),A
PUSH HL
ToneDelay2:
DEC HL
LD A,H
OR L
JR NZ,ToneDelay2
POP HL
DJNZ ToneLoop
RET
Preserving Border Colour with Speaker
Problem: Writing to port $FE affects border and speaker.
Solution: Read-modify-write doesn't work — IN A,($FE) returns the keyboard rows + EAR, not the last-written border colour. The Spectrum has no readback path for OUT-set ULA state. You must keep a software variable holding the current border value:
; Globally maintained border colour (bits 0-2, MIC bit 3 if you use tape)
current_border:
DEFB 1 ; Blue border, MIC low
play_click:
LD HL,current_border
LD A,(HL)
OR $10 ; Speaker on, border preserved
OUT ($FE),A
; ... delay ...
LD A,(HL) ; Speaker off, border preserved
OUT ($FE),A
RET
To change the border colour, write to current_border; the next speaker toggle picks it up.
ULA Port ($FE) - Complete Specification
Output Bit Summary
| Bit | Function | Effect |
|---|---|---|
| 0 | Border LSB | Border colour bit 0 |
| 1 | Border | Border colour bit 1 |
| 2 | Border MSB | Border colour bit 2 |
| 3 | MIC | Tape output (cassette save) |
| 4 | Speaker | 1-bit speaker output |
| 5-7 | Unused | No effect (recommended: write 0) |
Border colours (bits 0-2):
%000 = 0 = Black %100 = 4 = Green
%001 = 1 = Blue %101 = 5 = Cyan
%010 = 2 = Red %110 = 6 = Yellow
%011 = 3 = Magenta %111 = 7 = White
Input Bit Summary
| Bit | Function | Effect |
|---|---|---|
| 0-4 | Keyboard | 5 keys per half-row (active low) |
| 5 | Unused | Always reads 1 |
| 6 | EAR | Tape input signal |
| 7 | Unused | Always reads 1 |
Other Spectrum Ports
Port $7FFD - 128K Memory Paging (128K models only)
Not available on 48K Spectrum. Used for bank switching on 128K, +2, +2A, +3 models.
Bit layout (write):
| Bit | Function |
|---|---|
| 0-2 | RAM bank at $C000 (selects one of 8 banks) |
| 3 | Screen RAM bank (0 = normal screen at $C000-$DFFF in bank 5; 1 = shadow screen in bank 7) |
| 4 | ROM select (0 = 128K editor ROM, 1 = 48K BASIC ROM) |
| 5 | Paging disable — once set, can only be cleared by reset. Preserves the current configuration; some games use this to lock memory after setup. |
| 6-7 | Reserved |
Bank 5 is always at $4000 (regular screen). Bank 2 is always at $8000. Bank 0-7 is selected by bits 0-2 at $C000.
Port $1FFD - +2A/+3 Special Paging
The +2A/+3 (Amstrad redesign) adds a second paging port. Only relevant for those models — see the +3 technical reference for bit layout.
Ports $FFFD / $BFFD - AY-3-8912 Sound Chip (128K+ models only)
3-channel programmable sound generator on 128K/+2/+3 models (not on 48K). Two ports work together:
$FFFD— register select. Write the AY register number (0-15) here first.$BFFD— register data. Write or read the value of the previously selected register.
; Write value $0F to AY register 7 (mixer)
LD BC,$FFFD
LD A,7 ; Select register 7
OUT (C),A
LD BC,$BFFD
LD A,$0F ; New value
OUT (C),A
See the AY-3-8910 entry for the register set and channel layout.
+3 Disk Drive Ports (+3 only)
The Spectrum +3 has a built-in 3-inch floppy drive controlled by an μPD765 disk controller, accessed through:
$2FFD— Disk controller status (read) / command (write)$3FFD— Disk controller data (read/write)$1FFDbits 3-4 — Drive motor control + ROM/RAM switch combinations
Most +3 software uses the +3DOS BIOS rather than poking these ports directly. See the +3 technical manual for the full interface.
Kempston Joystick - Port $1F
Not a standard Spectrum feature - third-party add-on.
Read: IN A,($1F)
| Bit | Function |
|---|---|
| 0 | Right |
| 1 | Left |
| 2 | Down |
| 3 | Up |
| 4 | Fire |
Why hardware ports matter for Code Like It's 198x
Shadowkeep Unit 4 reads $FBFE (Q-row), $FDFE (A-row), and $DFFE (P-row) to implement QAOP input — three of the eight half-row reads listed above. Unit 7 will write to $FE bit 4 in a tight loop to generate beeper music. The 128K AY ports become relevant in Phase 2+ when the Spectrum 128 is brought online. The single most-used line of platform-specific code in the entire Project is OUT ($FE), A — every other Spectrum hardware concern flows downstream of this one port.
See also
- ZX Spectrum
- ULA — The chip that decodes most of these ports.
- Z80 — The CPU side of the conversation.
- AY-3-8910 — The 128K+ sound chip.
- Beeper music — What port $FE bit 4 is really for.
- ZX Spectrum ROM Disassembly