Skip to content
Techniques & Technology

ZX Spectrum ROM disassembly

The 16 KB ROM that boots the machine, runs BASIC, and quietly underpins every Spectrum game

Annotated reference for the ZX Spectrum's 16 KB ROM at $0000-$3FFF — RST vectors, character set, system variables, and the most commonly-called ROM routines. The disassembly Logan and O'Hara published in 1983 (in the Melbourne House book of the same name) became the standard reference; this entry covers the routines most useful to game programmers.

sinclair-zx-spectrum rombasicsystem-callsreference 1982–present

Overview

The 48K Spectrum's 16 KB ROM lives at addresses $0000-$3FFF. It contains the BASIC interpreter, the character-set bitmap, the system-variable layout, the interrupt vector for IM 1, and a long list of utility routines that BASIC depends on but that any machine-code program is welcome to call directly.

The canonical published reference is Ian Logan and Frank O'Hara's The Complete Spectrum ROM Disassembly (Melbourne House, 1983) — a line-by-line annotated disassembly of the entire 16K ROM. For Spectrum machine-code work it is, alongside Programming the Z80 and the Hints & Tips books, on every serious developer's shelf.

This entry catalogues the ROM routines and system variables that matter most for game programming. The full disassembly runs to 200+ pages; what follows is a useful subset.

The ROM contains:

  • The BASIC interpreter and tokeniser ($0000-$3CFF, broadly).
  • The 768-byte ASCII character bitmap ($3D00-$3FFF).
  • The interrupt service routine entry point ($0038, called every 1/50th of a second via IM 1).
  • RST vectors at $0000, $0008, $0010, $0018, $0020, $0028, $0030, $0038.
  • Routines for screen handling, keyboard scanning, BEEP generation, channel I/O, error handling, expression evaluation, and the floating-point calculator.

System variables live in the RAM space immediately above the ROM, at $5C00-$5CB5. The ROM reads and writes them constantly; well-behaved machine code can do likewise.


Quick ROM Routine Reference

Essential System Calls

AddressNamePurposeEntry ParametersExit Values
$0D6BCLSClear screenNoneNone
$09F4PRINT_A_1Print character in AA = characterScreen updated
$0010PRINT_APrint characterA = characterScreen updated
$1601CHAN_OPENOpen channelA = channel ('K','S','R','P')Stream opened
$15EFBEEPMake beep soundDE = duration, HL = pitchNone
$02BBKEY_SCANScan keyboardNoneCarry set if key pressed
$0C55PO_STOREStore attributeATTR_P system variableScreen attributes updated
$2294BORDERChange border colourA = colour (0-7)Border changed

BORDER Command ($2294)

The ROM routine that implements BASIC's BORDER command.

Address: $2294

Purpose: Change the border colour and update the BORDCR system variable.

Entry:

  • A register contains colour value (0-7)

Operation:

  1. Validates colour is in range 0-7 (ANDs with %00000111)
  2. Shifts colour left 3 bits to position for BORDCR
  3. Stores in BORDCR system variable ($5C48)
  4. Outputs to port $FE to change physical border

Code Overview:

BORDER:
        AND $07            ; Keep colour in range 0-7
        RLCA              ; Shift left 3 times (colour * 8)
        RLCA
        RLCA
        LD HL,$5C48       ; BORDCR system variable
        AND $38           ; Mask to border bits
        LD (HL),A         ; Store in BORDCR
        OUT ($FE),A       ; Output to hardware port
        RET

Why it's slower than direct assembly:

  • Multiple shifts (could use single OUT)
  • System variable update (not needed for immediate effect)
  • ROM routine call overhead
  • BASIC interpreter overhead to get here

Your assembly version:

LD A,2
OUT ($FE),A

Speed comparison: ROM routine ~50 T-states vs your code ~18 T-states

When called from BASIC: Add ~20,000 T-states for interpreter overhead!


Character Set ($3D00-$3FFF)

The ROM character set contains bitmap data for all 96 characters (ASCII 32-127).

Location: $3D00-$3FFF (768 bytes)

Format: Each character is 8 bytes (8×8 pixel bitmap)

Character Address Formula:

Address = $3D00 + ((character - 32) × 8)

Example - Character 'A' (ASCII 65):

Address = $3D00 + ((65 - 32) × 8) = $3D00 + (33 × 8) = $3D00 + $108 = $3E08

Bitmap data for 'A' at $3E08:

Byte  Hex   Binary      Display
+0    $00   00000000    ........
+1    $10   00010000    ...█....
+2    $28   00101000    ..█.█...
+3    $44   01000100    .█...█..
+4    $7C   01111100    .█████..
+5    $44   01000100    .█...█..
+6    $44   01000100    .█...█..
+7    $00   00000000    ........

Using ROM characters in your programs:

; Copy ROM character bitmap to screen
        LD A,'A'          ; Character to copy
        SUB 32            ; Convert to ROM offset
        LD L,A
        LD H,0
        ADD HL,HL         ; × 2
        ADD HL,HL         ; × 4
        ADD HL,HL         ; × 8 (each char is 8 bytes)
        LD BC,$3D00
        ADD HL,BC         ; HL = character bitmap address
        ; Now copy 8 bytes from HL to screen

Purpose: Print character in A register to current print position.

Entry:

  • A = character code (0-255)

Exit:

  • Character printed at current cursor position
  • Cursor advanced
  • Special characters handled (newline, etc.)

Example use:

        LD A,'H'
        RST $10           ; Print 'H' (RST $10 = CALL $0010)
        LD A,'I'
        RST $10           ; Print 'I'

Note: RST instructions are 1-byte calls to ROM routines. RST $10 = CALL $0010.

PrintString:
        LD HL,Message
PrintLoop:
        LD A,(HL)
        OR A              ; Check for 0 (string terminator)
        RET Z
        RST $10           ; Print character
        INC HL
        JR PrintLoop

Message:
        DEFM "HELLO",0    ; 0-terminated string

CLS - Clear Screen ($0D6B)

Purpose: Clear the entire screen to current PAPER colour.

Entry: None

Exit:

  • Screen cleared (bitmap + attributes)
  • Cursor reset to top-left

Called by BASIC: CLS command

Your assembly:

        CALL $0D6B        ; Clear screen

Manual screen clear (faster):

; Clear screen bitmap (6144 bytes)
        LD HL,$4000       ; Screen start
        LD DE,$4001
        LD BC,6143
        LD (HL),0
        LDIR              ; Copy 0 across entire screen

; Clear attributes (768 bytes)
        LD HL,$5800
        LD DE,$5801
        LD BC,767
        LD (HL),$38       ; White INK, black PAPER
        LDIR

Manual version is ~3× faster but uses more code space.


System Variables ($5C00-$5CB5)

Key system variables you might reference:

AddressNameSizePurpose
$5C00KSTATE8Keyboard state
$5C08LAST_K1Last key pressed
$5C3CTV_FLAG1TV control flags
$5C48BORDCR1Border colour (bits 3-5)
$5C78FRAMES3Frame counter (incremented 50× per second by the IM 1 interrupt)
$5C8DATTR_P1Permanent attributes
$5C8FATTR_T1Temporary attributes

Reading the frame counter:

; Get frame count (increments 50 times per second on PAL)
        LD HL,($5C78)     ; Read low 16 bits
        LD A,($5C7A)      ; Read high 8 bits (if needed)

The frame counter is the standard way to time short waits without using HALT — useful when interrupts can't be relied upon (e.g., under some emulators, or in code paths that briefly disable interrupts):

; Wait one frame
        ld      hl, $5C78
        ld      a, (hl)
        ld      b, a
.wait:  ld      a, (hl)
        cp      b
        jr      z, .wait

RST Instructions - Quick ROM Calls

RST instructions are 1-byte CALL instructions to fixed ROM addresses:

InstructionAddressT-statesCommon Use
RST $00$000011Reset machine
RST $08$000811Error handling
RST $10$001011Print character (PRINT_A)
RST $18$001811Get next character
RST $20$002011Get next floating point
RST $28$002811Calculator
RST $30$003011Make BC spaces
RST $38$003811Interrupt routine (IM 1)

Example - Using RST for printing:

        LD A,'X'
        RST $10           ; 1 byte vs CALL $0010 (3 bytes)

Why the ROM disassembly matters for Code Like It's 198x

Shadowkeep code is small enough — and the FRAMES-wait pattern important enough — that it sits adjacent to the ROM rather than calling it. Most Shadowkeep code does not invoke ROM routines (CLS, BORDER, BEEP). But the system-variable area at $5C00-$5CB5 is mandatory reading — FRAMES at $5C78 is the rate-limiting heartbeat for every unit from U4 onwards, and ATTR_P at $5C8D controls the default ink/paper used in any ROM print calls the title screen makes. Unit 7's beeper code may call the BEEP routine at $15EF; if it does, the framework here is the reference.

For programs that are hybrid (BASIC wrapper, machine-code inner loop — common in the BASIC track), RANDOMIZE USR addr is the seam from BASIC into machine code, and ROM routines via RST are the small primitives the inner loop can lean on cheaply.

See also