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.
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 |
PRINT-A: RST $10
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
- ZX Spectrum
- Z80 - the CPU these routines are written for
- ULA - what
OUT ($FE),Atalks to - ZX Spectrum hardware ports
- Beeper music - replacing BEEPER
- User-defined graphics - the UDG system variable in use
- Sinclair BASIC - the interpreter this ROM hosts
- Machine code for beginners - where the Spectrum machine-coder learns to read this disassembly