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

Reference for the ZX Spectrum's 16 KB ROM at $0000-$3FFF — the restart vectors, the interrupt routine, the character set, the system variables, and the ROM routines a game is most likely to call, each checked against Logan and O'Hara's 1983 disassembly.

sinclair-zx-spectrumrombasicsystem-callsreference

The 48K Spectrum’s 16 KB ROM lives at $0000-$3FFF. It holds the BASIC interpreter, the character-set bitmap, the system-variable layout, the IM 1 interrupt routine, and the utility routines BASIC depends on - and any machine-code program is welcome to call them at their documented addresses.

The published reference is Ian Logan and Frank O’Hara’s The Complete Spectrum ROM Disassembly (Melbourne House, 1983): the whole ROM as annotated Z80 assembly, every routine named, every entry condition stated. Its preface describes the ROM as “developed directly from the 4K program of the ZX 80”, and warns that its authors “have always however been short of ‘room’ and hence the program is written for ‘compactness’ rather than ‘speed’”. That trade-off is the reason a game calls some ROM routines and rewrites others. The book’s own history is on its entry; the names and addresses below are the book’s.

The ROM contains:

  • The BASIC interpreter and tokeniser, most of $0000-$386D. The book notes that $386E-$3CFF are “spare” and hold $FF.
  • The 768-byte character bitmap at $3D00-$3FFF.
  • The maskable-interrupt routine at $0038, reached every 20 ms through IM 1.
  • The eight restart vectors at $0000, $0008, $0010, $0018, $0020, $0028, $0030 and $0038.
  • Routines for screen and printer output, keyboard scanning, the beeper, channels and streams, error handling, expression evaluation, and the floating-point calculator.

The system variables sit immediately above the screen and printer buffer, at $5C00-$5CB5 (23552-23733). The ROM reads and writes them constantly; machine code can do the same, and for FRAMES and the attribute variables it usually should.


The restarts

RST n is a one-byte call to one of eight fixed addresses. The book’s names:

Instruction Address Name What it does
RST $00 $0000 START Disable interrupts and restart the machine - the reset entry
RST $08 $0008 ERROR-1 Report the error whose code follows the instruction in the next byte
RST $10 $0010 PRINT-A-1 Print the character in A to the current channel (jumps to PRINT-A-2 at $15F2)
RST $18 $0018 GET-CHAR Fetch the character at CH-ADD, skipping spaces and control codes
RST $20 $0020 NEXT-CHAR Advance CH-ADD and fetch the next character - the interpreter’s stepping routine
RST $28 $0028 FP-CALC Enter the floating-point calculator at $335B
RST $30 $0030 BC-SPACES Open BC bytes of free space in the workspace
RST $38 $0038 MASK-INT The IM 1 interrupt routine

Of these a game uses RST $10 (printing) and, by leaving IM 1 on, RST $38. RST $08 is how the ROM’s own routines report errors: a BEEP with an out-of-range value ends up here, and control returns to BASIC, which is a reason game code that calls ROM routines validates its arguments first.

The interrupt: $0038

The book’s summary is one sentence: “The real time clock is incremented and the keyboard scanned whenever a maskable interrupt occurs.” MASK-INT increments the low two bytes of FRAMES, bumps the third byte when they wrap to zero, then calls KEYBOARD at $02BF, which scans the matrix and, allowing for the repeat delay, drops a key code into LAST-K. Every 20 ms, on every 48K Spectrum, that is what happens between two instructions of your program - unless you DI or install your own IM 2 handler.


Routines a game calls

Address Name Purpose Entry Exit
$028E KEY-SCAN Scan the keyboard matrix None E = key value $00-$27 or $FF for no key; D = shift key; DE = $FFFF for no key; Z reset if more than two keys are down or a pair has no shift
$03B5 BEEPER Sound a note DE = frequency × duration in seconds; HL = T-states of the timing loop ÷ 4 Interrupts re-enabled
$09F4 PRINT-OUT Print to the screen or printer A = control code, printable character or token Screen updated
$0D6B CLS Clear the whole display and reset the print position None Screen cleared with ATTR-P’s colours
$15F2 PRINT-A-2 Send A to the current channel A = character -
$1601 CHAN-OPEN Make a stream’s channel current A = stream number, $FD-$03; 2 is the screen Later output goes to that channel
$2294 BORDER The BORDER command Parameter on the calculator stack (via FIND-INT1), not in a register Border changed, BORDCR updated

The character in A goes to whichever channel is current. From BASIC that is the screen; from code that has just been RANDOMIZE USR’d it usually still is, but a program that has used the printer or the lower screen selects the upper screen first with LD A,2 / CALL $1601. The initial stream table at $15C6 maps stream 2 to ‘S’.

        ld      a,2
        call    $1601           ; CHAN-OPEN: stream 2 = upper screen
        ld      hl,message
loop:   ld      a,(hl)
        or      a
        ret     z
        rst     $10             ; PRINT-A-1 -> PRINT-A-2
        inc     hl
        jr      loop

message:
        defm    "HELLO"
        defb    0

PRINT-OUT handles the control codes too: 22 (AT) followed by row and column, 16-21 for the colour items, 13 for a new line. A game that prints its scores through the ROM gets positioning and colour for the cost of a few bytes of control codes.

CLS: $0D6B

“The whole of the display is ‘cleared’” - CL-ALL at $0DAF wipes the bitmap and fills the attributes from ATTR-P, then CLS-LOWER resets the lower screen and the print position. Set ATTR-P ($5C8D) first and the ROM clears to your colours.

The direct alternative is two LDIRs, one for the 6,144-byte bitmap and one for the 768 attributes:

        ld      hl,$4000
        ld      de,$4001
        ld      bc,6143
        ld      (hl),0
        ldir                    ; bitmap
        ld      hl,$5800
        ld      de,$5801
        ld      bc,767
        ld      (hl),$38        ; white ink, black paper
        ldir                    ; attributes

It costs eighteen bytes, does no bookkeeping, and takes whatever colours you give it.

BORDER: $2294

The ROM’s BORDER routine is the BASIC command, not a subroutine with a register interface: it fetches its parameter from the calculator stack with FIND-INT1, checks it against 8, and errors out if too big. What it does next is the instructive part. It writes the value straight to port $FE, multiplies it by eight, and - if the colour is a light one - XORs in 7 so that the lower screen’s ink will be black on that paper, then stores the result in BORDCR. The book’s comment: “The parameter is then saved in the system variable BORDCR.”

A game does the first step itself:

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

and, if it ever returns to BASIC, updates BORDCR the way the ROM would, so the editor does not come back with an unreadable lower screen. See hardware ports for the port’s other bits.

BEEPER: $03B5

DE holds frequency × seconds and HL the timing-loop length in T-states ÷ 4. The book works the example: middle C for one second is DE = $0105 and HL = $066A, “derived from 6,689/4 - 30.125”, 6,689 being the T-states between speaker toggles at 261.63 Hz on a 3.5 MHz clock. The routine disables interrupts for the note’s duration, so FRAMES stops and the keyboard is not scanned until it returns. Beeper music is what happens when a game replaces it.

KEY-SCAN: $028E

KEY-SCAN returns a key value, not a character - E is a number from $00 to $27 unique to each of the forty keys, D the shift key if one is held, and DE = $FFFF when nothing is pressed. It refuses to decode more than two keys at once. A game that needs several simultaneous keys reads port $FE directly, as described under hardware ports; a game that wants “any key to continue” can call KEY-SCAN and test for $FFFF.


The character set: $3D00-$3FFF

Ninety-six characters, codes $20 (space) to $7F (©), eight bytes each, top row first:

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

The book’s example is ‘A’, at $3E08: 00 3C 42 42 7E 42 42 00.

+0    $00   ........
+1    $3C   ..████..
+2    $42   .█....█.
+3    $42   .█....█.
+4    $7E   .██████.
+5    $42   .█....█.
+6    $42   .█....█.
+7    $00   ........

The system variable CHARS ($5C36) holds $3C00 - the table’s address minus 256 - so that the ROM can index it by character code without subtracting 32; point CHARS at your own table and every ROM print call uses your font. To draw with the ROM’s glyphs yourself:

        ld      a,'A'
        sub     32
        ld      l,a
        ld      h,0
        add     hl,hl
        add     hl,hl
        add     hl,hl           ; × 8
        ld      bc,$3D00
        add     hl,bc           ; HL -> eight bytes of 'A'

System variables: $5C00-$5CB5

The ones a game reads or writes, with the manual’s descriptions:

Address Name Size Purpose
$5C00 KSTATE 8 “Used in reading the keyboard” - two sets of four bytes for key repeat
$5C08 LAST-K 1 “Stores newly pressed key”
$5C09 REPDEL 1 Frames a key is held before it repeats; starts at 35
$5C36 CHARS 2 “256 less than address of character set … Normally in ROM, but you can set up your own in RAM and make CHARS point to it”
$5C3C TV-FLAG 1 “Flags associated with the TV”
$5C48 BORDCR 1 “Border colour multiplied by 8, also contains the attributes normally used for the lower half of the screen”
$5C78 FRAMES 3 “frame counter incremented every 20mS”, least significant byte first
$5C7B UDG 2 “Address of first user-defined graphic”
$5C8D ATTR-P 1 “Permanent current colours” - what CLS and PRINT use
$5C8F ATTR-T 1 “Temporary current colours” - the colour items inside one PRINT

FRAMES

FRAMES is the machine’s clock. The manual introduces it from BASIC - “PEEK 23672. Every 1/50 second it increases by 1” - and warns that it “stops temporarily whenever you do BEEP, or a cassette tape operation, or use the printer”, because those routines run with interrupts off. For a game it is the free frame counter: read the low byte, and wait for it to change.

; Wait for the next frame without HALT
        ld      hl,$5C78
        ld      a,(hl)
wait:   cp      (hl)
        jr      z,wait

HALT does the same in one instruction when interrupts are on; the FRAMES loop is for code that has disabled them or runs an interrupt handler of its own that still updates the counter.


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

The curriculum’s Spectrum games leave IM 1 on and time themselves from FRAMES, print their text through RST $10, and clear the screen with two LDIRs. Each of those is a decision about which ROM routine to call and which to replace, and the disassembly is where the entry conditions come from. For the BASIC track’s hybrids, RANDOMIZE USR addr is the seam from BASIC into machine code, and the restarts are the cheap primitives the machine-code side can lean on.

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.