Skip to content
Techniques & Technology

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 the 128K models; the +3's disk and printer ports. Bit assignments as the Sinclair and Amstrad manuals and Chris Smith's ULA book give them.

sinclair-zx-spectrumhardwareportsi/oulakeyboardreference

The Z80 has a separate I/O address space. IN A,(n) and OUT (n),A put the immediate byte on A7-A0 and the accumulator on A15-A8; IN A,(C) and OUT (C),A put the whole of BC on the address bus. The Spectrum uses that distinction. The ULA’s one port lives at the low byte $FE, and the high byte is free to carry data: which keyboard half-row to read, and on the 128K models which memory bank to page. Everything else - joystick, sound chip, disk - is an add-on or a later model, on a port of its own.

The ULA decodes one address line

Chris Smith’s silicon-level study of the ULA states the decoding rule flatly:

Port address 0xFE is represented by 1 1 1 1 1 1 1 0 appearing on address bus lines A7-0, and the ULA takes the partial decoding of this address to the extreme by considering only address line A0.

So any even port number selects the ULA, and a peripheral on an even address would fight it. The rule for software follows: address the ULA with A7-A1 all high - $FE, not $00 or $FC - so that a peripheral decoding those lines sees a value it can tell apart from the ULA’s. Smith traces the scheme to the ZX80, where every gate saved was a chip not bought.

Port $FE is two different things depending on direction. A write latches border, tape output and speaker; a read returns keyboard and tape input. The written value cannot be read back.

Port $FE, written

Bit 7 6 5 4 3 2 1 0
Purpose - - - Speaker MIC Border Border Border

The 48K manual gives the same map in a sentence: port 254 “drives the loudspeaker (D4) and the MIC socket (D3), and also sets the border colour (D2, D1 and D0)”. Bits 5-7 go nowhere; there is no latch behind them.

The border bits are a three-bit latch feeding the display generator directly, not a memory location - Smith’s reason being that the video circuit needs the value on every scanline and fetching it from RAM would cost too much. That is why a border write takes effect instantly, and why border-colour raster effects are possible on a machine with no raster interrupt: the display shows whatever the latch held when it drew each line.

        ld      a,2            ; red border, MIC low, speaker low
        out     ($FE),a

Because nothing can be read back, a program that changes both border and speaker keeps a copy of the last byte it wrote. The ROM does this for BASIC in the system variable BORDCR ($5C48), which the BORDER command updates; a game that has taken over the machine keeps its own. The beeper routines in beeper music toggle bit 4 while ORing in the border bits from that copy, so the border does not flicker black on every click.

Port $FE, read

Bit 7 6 5 4 3 2 1 0
Purpose - EAR - Key Key Key Key Key
Value not driven tape input not driven 0 = pressed 0 = pressed 0 = pressed 0 = pressed 0 = pressed

The manual: “bits D0 to D4 stand for the five keys in the given half row - D0 for the outside key, D4 for the one nearest the middle. The bit is 0 if the key is pressed, 1 if it is not. D6 is the value at the EAR socket.” Bits 5 and 7 are not driven by the ULA at all - Smith notes D5 was “the logical choice” for the tape input but its pin was needed by the analogue cassette circuit, so D6 got the job. Mask with AND $1F before testing keys, and expect bit 6 to be noise when no tape is playing: the +3 manual calls it “effectively random if no tape data is present”.

Which half-row

The 40 keys are wired as eight half-rows of five. The high byte of the port address selects a half-row by pulling one of A8-A15 low; the manual gives the decimal form, 254+256*(255-2^n) for n = 0 to 7, and lists the rows from “IN 65278 reads the half row CAPS SHIFT to V” to “IN 32766 reads the half row SPACE to B”.

High byte Address line low Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
$FE A8 V C X Z CAPS SHIFT
$FD A9 G F D S A
$FB A10 T R E W Q
$F7 A11 5 4 3 2 1
$EF A12 6 7 8 9 0
$DF A13 Y U I O P
$BF A14 H J K L ENTER
$7F A15 B N M SYMBOL SHIFT SPACE

Bit 0 is the key at the outer end of the row - CAPS SHIFT, A, Q, 1 on the left; SPACE, ENTER, P, 0 on the right - and bit 4 the key nearest the middle. The manual’s own demonstration prints the raw byte for each row:

10 FOR n=0 TO 7: REM half-row number
20 LET a=254+256*(255-2^n)
30 PRINT AT 0,0; IN a: GO TO 30

In machine code, load BC with the half-row in B and $FE in C, then IN A,(C). This is exactly how the ROM’s KEY-SCAN routine at $028E works: it starts with LD BC,+FEFE, reads the row, inverts and masks (CPL / AND +1F) so that a pressed key becomes a set bit, then rotates B (RLC B) to walk the low bit through all eight rows.

; Is P pressed?  Row $DF, bit 0.
        ld      bc,$DFFE
        in      a,(c)
        bit     0,a
        jr      z,p_pressed    ; bit clear = key down

More than one high-byte bit may be low at once; the result is the AND of the selected rows, which is why IN A,($FE) with A = 0 (all rows selected) answers “is any key pressed?” in one instruction. The ROM’s KEY-SCAN gives up when it finds more than two keys down, but a game reading the matrix directly can track as many keys as it likes - subject to the wiring below.

Phantom keys

The matrix has no diodes between keys, so three keys pressed at the right positions read as four. Smith’s example is Q, P and O: Q connects A10 to K0; P connects A13 to K0 as well, so when A10 is pulled low to read the Q-T row, K0 pulls A13 low with it; and O, connecting A13 to K1, then pulls K1 low too. A read of the Q-T row reports Q and W. The rule: two keys in the same column (same bit) plus a third in the row of either one produces a phantom at the fourth corner.

That worked example is the classic Q-A-O-P layout with two of its keys held. Any control scheme that expects three simultaneous keys should be checked against the table above for rectangles; putting fire on a key whose bit and row are shared with neither movement key keeps the phantom away.

The speaker: bit 4

Sound on a 48K Spectrum is bit 4 of port $FE toggled by the CPU. There is no tone generator; pitch is the time between toggles and volume is fixed. The ROM’s BEEPER routine at $03B5 disables interrupts and counts T-states between OUTs, and every beeper engine since has been a more elaborate version of the same loop. Beeper music covers it.

Kempston joystick: port $1F

The Kempston interface puts the joystick on port 31 ($1F). The address is odd, so the ULA ignores it. The RAM Turbo interface manual, which emulates the protocol, gives the reading from BASIC as IN 31 with the bit values 1 Right, 2 Left, 4 Down, 8 Up, 16 Fire:

Bit Function
0 Right
1 Left
2 Down
3 Up
4 Fire

Unlike the keyboard, a Kempston bit is 1 when active.

        in      a,($1F)
        bit     4,a
        jr      nz,fire

The +2A and +3’s built-in joystick ports take a different route: they are wired into the keyboard matrix and read as the number keys. The +3 manual gives the layout - joystick 1 is fire, up, down, right, left on bits 0-4 of the 6-0 row and joystick 2 is left, right, down, up, fire on the 1-5 row - and adds “From BASIC, these read as the number keys”. A game that reads the matrix already supports them.

128K memory paging: port $7FFD

The 128K models have two 16K ROMs and eight 16K RAM banks, switched through port $7FFD (32765). The +3 manual, which documents the same port for the +2A/+3, gives the bit field:

Bit Function
0-2 RAM bank paged at $C000-$FFFF (0-7)
3 Screen select: 0 = screen 0 in bank 5 ($4000, the one BASIC uses); 1 = screen 1 in bank 7, “can only be used by machine code programs”
4 ROM at $0000: 0 = 128K editor, 1 = 48K BASIC
5 Disable paging - “once this bit has been set, no further paging operations will work” until reset or power off
6-7 Unused

Bank 5 is always at $4000 and bank 2 at $8000; only the top 16K moves. The two screens are both in banks that can also be paged in at $C000, which is how a program draws into the hidden screen: page bank 7 to $C000, draw, flip bit 3. Bit 5 is the lock the +3 manual suggests for a 48K game that misbehaves: SPECTRUM then OUT 32765,48 leaves the machine in 48K mode with paging dead until reset, sound chip still reachable.

The port is write-only. The manual is explicit about the consequence: “you cannot determine the current state of the paging by an IN instruction. This is why the BANKM system variable is always kept up to date with the last value output to this port”. Keep a shadow byte, and disable interrupts around the OUT if the interrupt handler could page.

Banks 4-7 are contended - the +3 manual gives the cost as NOPs in contended RAM running at “an effective clock frequency of 2.66MHz as opposed to the normal 3.53MHz” - and banks 0-3 are not. Time-critical code and sample data belong in 0-3. Bank switching covers the technique across machines.

+2A/+3 paging: port $1FFD

The +2A and +3 have four 16K ROMs and a second paging port, $1FFD (8189), which BASIC shadows in the variable BANK678. From the manual:

Bit Function
0 0 = normal paging; 1 = one of four all-RAM layouts, chosen by bits 1-2, “provided for authors of operating systems/games”
1 No effect when bit 0 is 0; half of the all-RAM layout number when bit 0 is 1
2 With bit 0 clear, the “vertical” ROM switch - ROM 0/2 or ROM 1/3 - combined with $7FFD bit 4, the “horizontal” switch
3 Disk motor, 1 = on
4 Printer STROBE, active low

The +3’s disk controller, a µPD765A, is read at $2FFD (main status register) and transferred through $3FFD (data register); the Centronics port is $0FFD. All three are +3-only, and, in the manual’s words on $3FFD, “Random OUTputting to this port will probably confuse the poor disk controller”.

AY-3-8912 sound: ports $FFFD and $BFFD

The 128K models’ AY-3-8912 has its registers behind two ports. Write the register number to $FFFD (65533), then the value to $BFFD (49149); reading $FFFD returns the selected register.

        ld      bc,$FFFD
        ld      a,7            ; mixer register
        out     (c),a
        ld      bc,$BFFD
        ld      a,%00111110    ; tone on channel A only
        out     (c),a

The chip’s own I/O port is what the 128K models hang the RS232/MIDI and AUX sockets off, which is why the manual notes the same two ports “also control RS232/MIDI and AUX interfaces”.

Reserved

The 48K manual reserves ports 254, 247 and 239 for Sinclair’s own peripherals and gives 251 to the ZX Printer. Software that assumes a port is free because no hardware is fitted will, on the ULA at least, be talking to the ULA.

Why hardware ports matter for Code Like It’s 198x

Every Spectrum game reads $FE for the keyboard and writes it for the border and beeper; the 128K games page through $7FFD and play through $FFFD/$BFFD. The two tables for $FE, and the one for $7FFD, are what the curriculum’s Spectrum units read from when they touch the hardware directly.

See also

Not yet fact-checked. This entry was drafted by an AI and nobody has verified it. The dates, figures and technical details may be wrong. Use it to find your bearings, then confirm anything that matters against a primary source.