Every 48K Spectrum carries a 16 KB program it cannot forget, in ROM at $0000-$3FFF. It is the BASIC you type into, but it is also a library. Fifty times a second on a British machine it stops whatever else is running to update a clock and read the keyboard, and its routines for printing, clearing the screen and sounding the speaker sit at fixed addresses that any machine-code program can call. The catch is that the routines were written to serve BASIC. Each expects its input in particular registers or system variables, most leave side effects behind, and some can only be reached from BASIC at all.
Spectrum owners mapped it themselves. In December 1982 a reader wrote to Sinclair User with a warning about RST $10, the ROM’s print routine: it “is also used to output characters to the printer and microdrives, so to be sure of screen output place at the beginning of any routine the instructions” LD A,02h and CALL 1601h. A month later Ian Logan and Frank O’Hara signed the preface to The Complete Spectrum ROM Disassembly (Melbourne House, 1983), which prints the whole ROM as commented Z80 assembly language and names every routine. The book’s story is on its own entry. The names and addresses below are the book’s, checked against the ROM’s bytes.
The authors describe the ROM as “developed directly from the 4K program of the ZX 80”, and say its programmers “have always however been short of ‘room’ and hence the program is written for ‘compactness’ rather than ‘speed’”. That trade-off decides which routines a game borrows and which it rewrites.
What is where
- $0000-$0038: the eight restart vectors, the targets of the one-byte
RSTinstructions. - $0038: the maskable-interrupt routine, reached every 20 ms in interrupt mode 1, which the ROM selects when the machine starts.
- Up to $386D: the BASIC interpreter and everything it uses: the keyboard and tape routines, screen and printer output, channels and streams, error handling, expression evaluation and the floating-point calculator.
- $386E-$3CFF: unused. The book notes that these locations are “spare” and “all hold +FF”.
- $3D00-$3FFF: the 768-byte character set.
The ROM keeps its working state in RAM, in the system variables at $5C00-$5CB5 (23552-23733), just above the printer buffer. It reads and writes them constantly, and a machine-code program can do the same.
The restarts
RST n is a one-byte call to one of eight fixed addresses, where CALL takes three bytes, and the ROM uses the restarts for the routines it calls most often: printing, reading the next character of a BASIC line, the calculator. The book’s names:
| Instruction | Address | Name | What it does |
|---|---|---|---|
RST $00 |
$0000 | START | Disables interrupts and jumps to START/NEW, which sets the machine up from scratch |
RST $08 |
$0008 | ERROR-1 | Reports the error whose code is in the byte after the instruction |
RST $10 |
$0010 | PRINT-A-1 | Prints the character in A on the current channel (jumps to PRINT-A-2 at $15F2) |
RST $18 |
$0018 | GET-CHAR | Fetches the character at CH-ADD, stepping past spaces and control codes |
RST $20 |
$0020 | NEXT-CHAR | Advances CH-ADD, then does the same: the interpreter’s way of stepping along a line |
RST $28 |
$0028 | FP-CALC | Enters the floating-point calculator at $335B |
RST $30 |
$0030 | BC-SPACES | Opens BC bytes of free space in the workspace |
RST $38 |
$0038 | MASK-INT | The interrupt routine |
A game usually meets two of these: RST $10 for printing and, as long as it leaves interrupt mode 1 on, RST $38. RST $08 is worth knowing because the ROM’s own routines use it when something is wrong. CHAN-OPEN, for example, ends in report O, “Invalid stream”, if it is given a stream that is not open, and the error handler abandons the machine-code program and goes back to BASIC.
The interrupt: $0038
The book sums it up in one sentence: “The real time clock is incremented and the keyboard scanned whenever a maskable interrupt occurs.” MASK-INT adds one to the low two bytes of FRAMES, adds one to the third byte when they wrap round to zero, and calls KEYBOARD at $02BF. KEYBOARD scans the keys and, once the repeat delay allows it, puts the new key’s code in LAST-K. All of this happens between two instructions of your program every 20 ms, unless you disable interrupts with DI or install your own interrupt-mode-2 handler.
Routines a game calls
| Address | Name | Purpose | Entry | Exit |
|---|---|---|---|---|
| $028E | KEY-SCAN | Scan the keyboard | Nothing | E = key value $00-$27, or $FF for none; D = a shift key; DE = $FFFF if no key; zero flag reset if three or more keys are down, or two with neither a shift |
| $03B5 | BEEPER | Sound a note | DE = frequency × duration in seconds; HL = timing-loop length in T-states ÷ 4 | Interrupts re-enabled |
| $09F4 | PRINT-OUT | The output routine for the screen and printer channels | A = control code, printable character or token | - |
| $0D6B | CLS | Clear the whole display | Nothing | Upper screen in ATTR-P’s colours, lower screen in BORDCR’s; channel K current |
| $15F2 | PRINT-A-2 | Send A to the current channel | A = character | - |
| $1601 | CHAN-OPEN | Make a stream’s channel current | A = stream number, normally $FD-$03; 2 is the upper screen | Later output goes to that channel |
| $2294 | BORDER | The BORDER command | The colour on the calculator stack, not in a register | Border changed, BORDCR updated |
Printing: RST $10
The character in A goes to whichever channel is current, and that is not always the one you expect. A program started with RANDOMIZE USR often finds channel K, the lower screen, current, unless the BASIC before it has just printed something. Its text then goes to the bottom two lines, and the “0 OK” report wipes it out as soon as the program returns. The fix is the one Sinclair User’s reader gave in 1982: open stream 2, the upper screen, first.
ld a,2
call $1601 ; CHAN-OPEN: stream 2 = upper screen
ld hl,text
loop: ld a,(hl)
or a
ret z
rst $10 ; PRINT-A-1 -> PRINT-A-2
inc hl
jr loop
text: defm "HELLO"
defb 0The ROM does the rest. PRINT-OUT understands the same control codes as BASIC’s PRINT: 22 (AT) followed by a row and a column, 16 to 21 for INK, PAPER, FLASH, BRIGHT, INVERSE and OVER, each followed by a value, and 13 for a new line. A game that prints its score through the ROM gets positioning and colour for a few bytes of control codes. What it pays is speed: the book calls PRINT-OUT “rather slow”, because it checks every option for every byte it puts on the screen.
CLS: $0D6B
In the book’s words, “the whole of the display is ‘cleared’ - the ‘pixels’ are all reset and the attribute bytes are set to equal the value in ATTR-P - then the lower part of the display is reformed.” Set ATTR-P ($5C8D) first and the ROM clears to your colours. The lower two lines take their colours from BORDCR instead. CLS finishes by opening channel K, so a program that calls it and then prints needs LD A,2 / CALL $1601 again.
The direct alternative is two LDIRs, one for the 6,144-byte bitmap and one for the 768 attribute bytes:
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 ; black ink on white paper
ldir ; attributesIt takes 26 bytes, keeps no records, and uses whatever colours you give it. The attribute byte holds the paper colour in bits 3-5 and the ink in bits 0-2, so $38 is white paper (7) with black ink (0), the Spectrum’s colours at switch-on.
BORDER: $2294
The ROM’s BORDER is the BASIC command itself, not a subroutine with a register interface. It fetches its parameter from the calculator stack with FIND-INT1, compares it with 8, and gives report K, “Invalid colour”, if it is too big. What it does next is the instructive part. It writes the colour to port $FE, multiplies it by eight to move it into the paper bits, and stores the result in BORDCR, the attribute byte for the lower screen. On a dark border it first XORs in 7, making the lower screen’s ink white; on a light one (green, cyan, yellow or white) it leaves the ink black. After BORDER 2, BORDCR holds 23: red paper, white ink. After BORDER 5 it holds 40: cyan paper, black ink.
A game does the first step itself:
ld a,2
out ($FE),a ; red borderIf it will return to BASIC it should update BORDCR the same way, so that the editor does not come back with unreadable text on the lower screen. The other bits of port $FE drive the speaker and the MIC socket; see hardware ports.
BEEPER: $03B5
DE holds the frequency multiplied by the duration in seconds, and HL the length of the timing loop in T-states divided by 4. The book works through middle C. At 261.63 Hz on a 3.5 MHz clock the speaker has to be switched “as close as possible to every 6,689 T states”, so for one second DE is $0105 and HL is $066A, “derived from 6,689/4 - 30.125”. BEEPER’s first instruction is DI, “Disable the interrupt for the duration of a ‘beep’”, so FRAMES stops and the keyboard goes unread until the note ends. Its last is EI, which switches interrupts back on whether or not they were on before. Beeper music tells what programmers did when they wanted more than one note at a time.
KEY-SCAN: $028E
KEY-SCAN returns a key value, not a character. E is a number from $00 to $27, a different one for each of the forty keys; D holds the value of a shift key if one is down; DE is $FFFF when nothing is pressed. It scans the eight half-rows of the keyboard one at a time, and gives up as soon as it finds a third key. A game that needs several keys at once, such as fire while moving diagonally, reads port $FE itself, as described under hardware ports. A game that only wants “press any key” can call KEY-SCAN and test for $FFFF.
The character set: $3D00-$3FFF
The ROM holds 96 characters, codes $20 (space) to $7F (©), each eight bytes deep, top row first:
Address = $3D00 + ((code - 32) × 8)The book’s example is the letter 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, 256 less than the start of the table, so that the ROM can find a character from its code without first subtracting 32. Point CHARS at a table of your own in RAM and every ROM print uses your font. To draw with the ROM’s letters 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 is most likely to read or write, with the descriptions from the Spectrum manuals:
| Address | Name | Size | Purpose |
|---|---|---|---|
| $5C00 | KSTATE | 8 | “Used in reading the keyboard”: two sets of four bytes, so that a new key can be caught while the last one is still repeating |
| $5C08 | LAST-K | 1 | “Stores newly pressed key” |
| $5C09 | REPDEL | 1 | How long, in 50ths of a second, a key must be held before it repeats; the ROM sets it to 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” (see user-defined graphics) |
| $5C8D | ATTR-P | 1 | “Permanent current colours”, set by INK and PAPER as commands; CLS and PRINT start from these |
| $5C8F | ATTR-T | 1 | “Temporary current colours”, set by colour items inside a PRINT |
FRAMES
FRAMES is the machine’s clock. The Spectrum’s manual introduces it to BASIC programmers: “The one that increases most often is PEEK 23672. Every 1/50 second it increases by 1.” It also warns that the clock “stops temporarily whenever you do BEEP, or a cassette tape operation, or use the printer”, because those routines turn interrupts off.
For a game, FRAMES is a free frame counter. This loop waits until the next interrupt has been and gone:
ld hl,$5C78
ld a,(hl)
wait: cp (hl)
jr z,waitHALT does the same in one instruction. Both depend on the interrupt: with interrupts off, or with an interrupt-mode-2 handler that does not call the ROM’s, FRAMES never changes and the loop never ends. What the counter offers that HALT does not is a record. A game can save FRAMES, do a variable amount of work, and then see how many frames went by.
Sources
- Ian Logan and Frank O’Hara, The Complete Spectrum ROM Disassembly (Melbourne House, 1983): preface (the ZX80 origin, “compactness” rather than “speed”); introduction (PRINT-OUT “rather slow”); the restart routines at
0000-0038; the maskable interrupt at0038; KEY-SCAN at028Eand KEYBOARD at02BF; BEEPER at03B5and BE-END at03F6; the CLS command at0D6B, CLS-LOWER at0D6E, CL-CHAN at0D94and CL-ALL at0DAF; START/NEW at11CB(CHARS, REPDEL andIM 1); the initial channel and stream data at15AFand15C6; CHAN-OPEN at1601and report O; the BORDER command at2294; the spare locations386E-3CFFand the character set at3D00-3FFF. - Sinclair User issue 9, December 1982, letters page: a reader’s letter on the Spectrum ROM, with
LD A,02h/CALL 1601h“to be sure of screen output”. - Steven Vickers, Sinclair ZX Spectrum BASIC Programming, edited by Robin Bradbeer (Sinclair Research, 1982; second edition 1983): chapter 18 (the frame counter and when it stops); chapter 24 (the memory map, with the system variables from 23552 and CHANS at 23734).
- ZX Spectrum +3 Manual (Amstrad, 1987), chapter 8, part 25: the system variables table, which keeps the 48K addresses and descriptions.
- The 48K Spectrum ROM itself: the bytes at every address given above, the $FF fill from
386Eto3CFF, and the letter A at3E08.
See also
- ZX Spectrum
- The Complete Spectrum ROM Disassembly, the book these names come from
- Ian Logan
- Steven Vickers, who wrote most of the ROM
- Z80, the processor these routines are written for
- ULA, the chip that
OUT ($FE),Atalks to - ZX Spectrum hardware ports
- Beeper music
- User-defined graphics
- Sinclair BASIC, the interpreter this ROM holds
- Machine code for beginners