With thanks to Daniel A. Nagy for contributing this article.
It is well known that the ZX Spectrum entered production with a ROM content (firmware) that was not quite finished. One of my hobby projects is creating a drop-in replacement ROM in which all the code actually works as originally intended and that matches the Manual in all the subtle ways in which the production ROM deviates from it, assuming that the Manual, also written by Steven Vickers, the principal author of the ROM, was its true specification. Because of this project, I was especially keen on taking a look at the ROM of the ZX Spectrum prototype generously donated to the British Centre for Computing History by John Grant, the CEO of Nine Tiles, the company Sinclair subcontracted for writing the ROM for the ZX line of computers. John Grant also worked on the ZX Spectrum’s ROM and he is the sole author of the ROM of the ZX80, from which the ZX Spectrum inherited some code.
When the ROM dump from the prototype was made available at the Centre’s webpage, the first thing I did, like others, was to start up a ZX Spectrum emulator with it. For my own ZX82 project, I have previously written a test suite for all the bugs about which I know in the expression evaluator subsystem, so I run it against this ROM. I was somewhat disappointed to learn that all those bugs, without a single exception, are still there. Then I tried all the known bugs in the display device driver. Again, none of them are fixed. How about the NMI (warm reset) bug? Still there. When I typed in a few lines of BASIC and tried to RUN
it, I got a M RAMTOP no good error
report. Running it with a GO TO
instruction worked, and since RUN
in the ZX Spectrum is essentially a CLEAR
followed by GO TO
, I tried CLEAR
. Same error report. So, I have identified a new bug, not present in the production ROM.
Looking at the hexdump of the ROM revealed three interesting changes:
- The
MOVE
token got a trailing hash symbol likeOPEN #
andCLOSE #
(becomingMOVE #
), which was not even reflected by the stickers on the prototype’s keyboard. - An additional error report
S Device unformatted
has been added. - The amount of spare space before the display font has shrunk considerably.
At this point, it became quite clear that Nine Tiles was busy cramming in new features rather than fixing bugs (about which they did not even necessarily know at the time). So, I delved in, armed with a disassembler, hoping to learn more about what those features were. As one could expect looking at the hexdump, this ROM is, indeed, more advanced than the production ROM in one particular area, which is very obviously unfinished in the production ROM: the I/O subsystem. However, more advanced does not mean more usable: unlike the production ROM which was a proof-of-concept build for a trade show intended for demonstrations, this ROM is obviously work-in-progress, with some previously working parts broken and some new parts still very far from production quality.
The first surprise came with the very first byte of the ROM. In the production ROM, it is a DI
instruction, disabling maskable interrupts. There is a good reason to start the startup procedure with that instruction, even though maskable interrupts are disabled after RESET
by the Z80: the machine can also be reset from software by jumping to address 0, in which case enabled interrupts might cause problems. This is not a critical problem, however. So, let’s follow the startup procedure now, that we know that it is different from the production ROM:
0000 11 ff ff START: ld de,0ffffh ; top address
0003 06 01 ld b,001h ; initialize UDG's and RS232
0005 c3 45 15 jp RESET
1545 f3 RESET: di
1546 fd 21 80 5c ld iy,PTR_IY ; 0x5C80 (PR_CC) in this build
154a 3e 07 ld a,007h
154c d3 fe out (0feh),a
154e 3e 3f ld a,03fh
1550 ed 47 ld i,a
1552 62 ld h,d
1553 6b ld l,e
1554 RAM_FILL:
1554 36 02 ld (hl),002h
1556 2b dec hl
1557 bc cp h
1558 20 fa jr nz,RAM_FILL
155a RAM_READ:
155a a7 and a
155b ed 52 sbc hl,de
155d 19 add hl,de
155e 23 inc hl
155f 30 06 jr nc,RAM_DONE
1561 35 dec (hl)
1562 28 03 jr z,RAM_DONE
1564 35 dec (hl)
1565 28 f3 jr z,RAM_READ
1567 RAM_DONE:
1567 2b dec hl
1568 e5 push hl
1569 10 15 djnz RAM_SET
156b 11 af 3e ld de,l3eafh
156e 01 a8 00 ld bc,00a8h
1571 eb ex de,hl
1572 ed b8 lddr
1574 62 ld h,d
1575 6b ld l,e
1576 e3 ex (sp),hl
1577 13 inc de
1578 dd 21 0c 00 ld ix,0000ch
157c 01 40 00 ld bc,00040h
157f d9 exx
1580 d9 RAM_SET:exx
1581 22 b4 5c ld (P_RAMT),hl
1584 ed 43 38 5c ld (RASP),bc
1588 ed 53 7b 5c ld (UDG),de
158c dd 22 c0 5c ld (BAUD),ix
1590 21 00 3c ld hl,FONT - 0100h
1593 22 36 5c ld (CHARS),hl
1596 e1 pop hl
1597 22 b2 5c ld (RAMTOP),hl
159a 36 3e ld (hl),03eh
159c 2b dec hl
159d f9 ld sp,hl
159e 2b dec hl
159f 2b dec hl
15a0 22 3d 5c ld (ERR_SP),hl
15a3 ed 56 im 1
15a5 fb ei
15a6 21 c2 5c ld hl,CHINFO
15a9 22 4f 5c ld (CHANS),hl
While similar to the production ROM, it has been somewhat re-arranged, the 24-T-state warm-up delay has been removed and in addition to the UDG area, the baud rate of the RS232 interface is also reset to its default value. A very important change that breaks the compatibility of this ROM with quite a lot of software and hardware that was designed with the production ROM in mind is that the IY
register points to PR_CC
(5C80
) rather than ERR_NR
, changing all the offsets to system variables. It actually makes sense, as it increases the range of system variables accessible through IY
offsets to its theoretical maximum; the previous value would have left some new system variables (more on them later) out of reach.
It is also this part that contains the bug responsible for the incorrect working of CLEAR
and RUN
mentioned above. The instructions at 1568
and 1576
assume that the stack pointer has already been initialized, although it happens only much later at 159d
. So, what ends up happening here upon startup is that UDG initialization overwrites the pointer to the top of the physical RAM saved on the stack, resulting in a faulty initialization of the P_RAMT
system variable, breaking the CLEAR
routine.
Also note that the address of CHINFO
, the channel information area after the system variables has changed, as there are more system variables in this ROM. One of them, which I called BAUD
following the name of the analogous system variable in ZX Interface 1 and ZX Spectrum 128k, contains the initial counter value for the timing of RS232 baud rate.
The rest of the initialization procedure is pretty much the same as the one in the production ROM. However, we cannot fail to notice that the initial channel information got a bit longer, so let’s have a closer look:
192b CHINFO_START:
192b 27 0d K_CH: defw PRINT_OUT
192d 21 14 defw KEY_INPUT
192f 4b defb "K"
1930 27 0d S_CH: defw PRINT_OUT
1932 48 19 defw REPORT_J
1934 53 defb "S"
1935 af 12 R_CH: defw ADD_CHAR
1937 48 19 defw REPORT_J
1939 52 defb "R"
193a 27 0d P_CH: defw PRINT_OUT
193c 48 19 defw REPORT_J
193e 50 defb "P"
193f f9 12 T_CH: defw SERIAL_OUT
1941 ce 12 defw SERIAL_IN
1943 54 defb "T"
A new channel type “T” has been added for RS232, though the following stream table shows no default stream assigned to it, which is perfectly fine. The service routines themselves may or may not work, but the prototype hardware has no serial interface, so there is no easy way to check. The port number FD
that it uses has eventually been used by the 128k Spectrum completely differently, so no actual hardware exists with which these service routines would be compatible. ZX Interface 1, for which these routines have been apparently developed, ended up using port number F7
. However, it is very likely that the service routines are in an early stage of development, since in their present state, even if they did work, they would not allow the use of serial printers, as there is no de-tokenizing for LLIST
. My personal opinion is that unlike how ZX Interface 1 and the ZX Spectrum 128k go about it, the proper place for adding de-tokenizing would have been the LLIST
instruction itself, not the output service routine. It might well be the case that Nine Tiles would have taken this route, had they worked more in the same direction, but, unfortunately, Ian Logan had no other choice for ZX Interface 1, since the LLIST
instruction in the production ROM (just like in this ROM) ended up being no more than a shorthand for LIST #3
.
The next substantial difference with the production ROM is in the routine handling SAVE
, LOAD
, VERIFY
and MERGE
instructions, so let’s take a look at it:
0607 SAVE_ETC:
0607 f1 pop af
0608 3a 74 5c ld a,(T_ADDR)
060b d6 ec sub 0ech ; reflects difference in syntax table
060d 32 74 5c ld (T_ADDR),a
0610 df rst 18h
0611 fe 23 cp "#" ; Streams can be used!
0613 28 06 jr z,USE_STREAM
0615 cd 99 20 call CLASS_0A ; file name
0618 a7 and a
0619 18 05 jr USE_NAME
061b USE_STREAM:
061b e7 rst 20h
061c cd 8f 20 call CLASS_06 ; stream
061f 37 scf
0620 USE_NAME:
0620 cd 25 29 call SYNTAX_Z
0623 28 46 jr z,SA_DATA
0625 08 ex af,af' ; CF'=1 - stream, CF'=0 - file name
0626 01 11 00 ld bc,00011h
0629 3a 74 5c ld a,(T_ADDR)
062c a7 and a
062d 28 02 jr z,SA_SPACE
062f 0e 22 ld c,022h
0631 SA_SPACE:
0631 f7 rst 30h
0632 d5 push de
0633 dd e1 pop ix
0635 06 0b ld b,00bh
0637 3e 20 ld a,020h
0639 SA_BLANK:
0639 12 ld (de),a
063a 13 inc de
063b 10 fc djnz SA_BLANK
063d 08 ex af,af' ; restore stream/filename flag
063e 9f sbc a,a
063f 3d dec a
0640 dd 77 01 ld (ix+001h),a ; $FF - filename $FE - stream
0643 38 23 jr c,USE_STRM_DO
0645 cd e6 2f call sub_2fe6h
0648 21 f6 ff ld hl,0fff6h
064b 0b dec bc
064c 09 add hl,bc
064d 03 inc bc
064e 30 0f jr nc,SA_NAME
0650 3a 74 5c ld a,(T_ADDR)
0653 a7 and a
0654 20 02 jr nz,SA_NULL
0656 cf rst 8
0657 0e defb 0eh ; F Invalid file name
0658 78 SA_NULL:ld a,b
0659 b1 or c
065a 28 0f jr z,SA_DATA
065c 01 0a 00 ld bc,000Ah
065f dd e5 SA_NAME:push ix
0661 e1 pop hl
0662 23 inc hl
0663 eb ex de,hl
0664 ed b0 ldir
0666 18 03 jr SA_DATA
0668 USE_STRM_DO:
0668 cd 83 24 call SET_STREAM
066b df SA_DATA:rst 18h
This is very-very cool. All these instructions can now accept stream identifiers instead of file names. You can see it in action by typing SAVE #2. More generally speaking, file instructions were in the process of being abstracted from the hardware and made usable with storage devices other than cassette tape. Had this effort borne fruit before the release of the ZX Spectrum, all the incompatibilities between various mass storage interfaces, such as microdrives, other tape drives and disk interfaces could have been prevented. What a missed opportunity!
The next part where we find very substantial differences are the handlers of I/O instructions CLOSE #
, OPEN #
, MOVE #
and FORMAT
. CAT
and ERASE
are still stubbed, just like in the production ROM.
1a65 cd ba 1a CLOSE: call STR_DATA2
1a68 cd 81 1a call CLOSE_2
1a6b 01 00 00 ld bc,0000h
1a6e 11 e2 a3 ld de,0a3e2h
1a71 eb ex de,hl
1a72 19 add hl,de
1a73 38 07 jr c,CLOSE_1
1a75 01 58 19 ld bc,STRMS_END
1a78 09 add hl,bc
1a79 4e ld c,(hl)
1a7a 23 inc hl
1a7b 46 ld b,(hl)
1a7c eb CLOSE_1:ex de,hl
1a7d 71 ld (hl),c
1a7e 23 inc hl
1a7f 70 ld (hl),b
1a80 c9 ret
1a81
1a81 e5 CLOSE_2:push hl
1a82 2a 4f 5c ld hl,(CHANS)
1a85 09 add hl,bc
1a86 23 inc hl
1a87 23 inc hl
1a88 23 inc hl
1a89 4e ld c,(hl)
1a8a eb ex de,hl
1a8b 21 a2 1a ld hl,CLOSESTRM
1a8e cd 5c 1a call INDEXER
1a91 30 05 jr nc,CLOSE_3
1a93 4e ld c,(hl)
1a94 06 00 ld b,000h
1a96 09 add hl,bc
1a97 e9 jp (hl)
1a98
1a98 21 05 00 CLOSE_3:ld hl,0005h
1a9b 19 add hl,de
1a9c 7e ld a,(hl)
1a9d 23 inc hl
1a9e 66 ld h,(hl)
1a9f 6f ld l,a
1aa0 e9 jp (hl)
1aa1 e9 jp (hl)
1aa2
1aa2 CLOSESTRM:
1aa2 4b defb "K"
1aa3 0d defb CLOSE_K - $
1aa4 53 defb "S"
1aa5 0b defb CLOSE_S - $
1aa6 50 defb "P"
1aa7 09 defb CLOSE_P - $
1aa8 4e defb "N"
1aa9 04 defb CLOSE_N - $
1aaa 54 defb "T"
1aab 05 defb CLOSE_T - $
1aac 00 defb 00h
1aad
1aad cd 12 0d CLOSE_N:call CLOSE_N_DO
1ab0 CLOSE_T:
1ab0 CLOSE_P:
1ab0 CLOSE_S:
1ab0 e1 CLOSE_K:pop hl
1ab1 c9 ret
1ab2 STR_DATA:
1ab2 cd a1 22 call FIND_INT1
1ab5 fe 10 cp 010h
1ab7 d8 ret c
1ab8 REPORT_O2:
1ab8 cf rst 8
1ab9 17 defb 17h ; O Invalid stream
1aba STR_DATA2:
1aba cd b2 1a call STR_DATA
1abd STR_DATA1:
1abd 87 add a,a
1abe c6 16 add a,016h
1ac0 6f ld l,a
1ac1 26 5c ld h,05ch
1ac3 4e ld c,(hl)
1ac4 23 inc hl
1ac5 46 ld b,(hl)
1ac6 2b dec hl
1ac7 c9 ret
1ac8
1ac8 ef OPEN: rst 28h
1ac9 01 38 defb 01h, 38h
1acb cd ba 1a call STR_DATA2
1ace 78 ld a,b
1acf b1 or c
1ad0 28 16 jr z,OPEN_1
1ad2 eb ex de,hl
1ad3 2a 4f 5c ld hl,(CHANS)
1ad6 09 add hl,bc
1ad7 23 inc hl
1ad8 23 inc hl
1ad9 23 inc hl
1ada 7e ld a,(hl)
1adb eb ex de,hl
1adc fe 4b cp "K"
1ade 28 08 jr z,OPEN_1
1ae0 fe 53 cp "S"
1ae2 28 04 jr z,OPEN_1
1ae4 fe 50 cp "P"
1ae6 20 d0 jr nz,REPORT_O2
1ae8 cd ef 1a OPEN_1: call OPEN_2
1aeb 73 ld (hl),e
1aec 23 inc hl
1aed 72 ld (hl),d
1aee c9 ret
1aef
1aef e5 OPEN_2: push hl
1af0 cd e6 2f call STK_FETCH
1af3 78 ld a,b
1af4 b1 or c
1af5 20 02 jr nz,OPEN_3
1af7 REPORT_F:
1af7 cf rst 8
1af8 0e defb 0eh ; F Invalid file name
1af9
1af9 c5 OPEN_3: push bc
1afa 1a ld a,(de)
1afb e6 df and 0dfh
1afd 4f ld c,a
1afe 21 15 1b ld hl,OPENSTRM
1b01 cd 5c 1a call INDEXER
1b04 30 06 jr nc,OPEN_4
1b06 4e ld c,(hl)
1b07 06 00 ld b,000h
1b09 09 add hl,bc
1b0a c1 pop bc
1b0b e9 jp (hl)
1b0c
1b0c c1 OPEN_4: pop bc
1b0d 2a be 5c ld hl,(OPEN_X)
1b10 7c ld a,h
1b11 b5 or l
1b12 28 e3 jr z,REPORT_F
1b14 e9 jp (hl)
1b15
1b15 OPENSTRM:
1b15 4b defb "K"
1b16 0a defb OPEN_K - $
1b17 53 defb "S"
1b18 0c defb OPEN_S - $
1b19 50 defb "P"
1b1a 0e defb OPEN_P - $
1b1b 4e defb "N"
1b1c 16 defb OPEN_N - $
1b1d 54 defb "T"
1b1e 17 defb OPEN_T - $
1b1f 00 defb 0
1b20
1b20 1e 01 OPEN_K: ld e,001h
1b22 18 06 jr OPEN_END
1b24
1b24 1e 06 OPEN_S: ld e,006h
1b26 18 02 jr OPEN_END
1b28
1b28 1e 10 OPEN_P: ld e,010h
1b2a OPEN_END:
1b2a 0b dec bc
1b2b 78 ld a,b
1b2c b1 or c
1b2d 20 c8 jr nz,REPORT_F
1b2f 57 ld d,a
1b30 e1 pop hl
1b31 c9 ret
1b32
1b32 c3 b8 0c OPEN_N: jp OPEN_N_DO
1b35 1e 15 OPEN_T: ld e,015h
1b37 18 f1 jr OPEN_END
1b39
1b39 cd b2 1a MOVE: call STR_DATA
1b3c f5 push af
1b3d cd b2 1a call STR_DATA
1b40 c1 pop bc
1b41 4f ld c,a
1b42 MOVE_LOOP:
1b42 79 ld a,c
1b43 c5 push bc
1b44 cd 89 19 call CHAN_OPEN
1b47 c1 pop bc
1b48 MOVE_READ:
1b48 cd 6a 19 call INPUT_AD
1b4b 38 03 jr c,MOVE_WRITE
1b4d 28 f9 jr z,MOVE_READ
1b4f c9 ret
1b50 MOVE_WRITE:
1b50 f5 push af
1b51 78 ld a,b
1b52 c5 push bc
1b53 cd 89 19 call CHAN_OPEN
1b56 c1 pop bc
1b57 f1 pop af
1b58 d7 rst 10h
1b59 18 e7 jr MOVE_LOOP
1b5b
1b5b cd e6 2f FORMAT: call STK_FETCH
1b5e 1a ld a,(de)
1b5f e6 df and 0dfh
1b61 fe 4e cp "N"
1b63 28 06 jr z,FORMAT_N
1b65 fe 54 cp "T"
1b67 28 0c jr z,FORMAT_T
1b69 18 8c jr REPORT_F
1b6b
1b6b FORMAT_N:
1b6b cd 93 1b call VAL_REST
1b6e cd a1 22 call FIND_INT1
1b71 32 b7 5c ld (STATION),a
1b74 c9 ret
1b75
1b75 FORMAT_T:
1b75 cd 93 1b call VAL_REST
1b78 ef rst 28h
1b79 34 00 40 42 03 76 01 05 38 defb 34h, 00h, 40h, 42h, 03h, 76h, 01h, 05h, 38h
1b82 cd a6 22 call FIND_INT2
1b85 0b dec bc
1b86 0b dec bc
1b87 0b dec bc
1b88 cb 78 bit 7,b
1b8a c2 ac 22 jp nz,REPORT_B1
1b8d 03 inc bc
1b8e ed 43 c0 5c ld (BAUD),bc
1b92 c9 ret
1b93 VAL_REST:
1b93 13 inc de
1b94 0b dec bc
1b95 cd a7 2e call STK_STO
1b98 06 1d ld b,01dh
1b9a ef rst 28h
1b9b 1d 38 defb 1dh,38h ; VAL
1b9d c9 ret
1b9e
1b9e c3 b8 1a CAT_ETC:jp REPORT_O2
Now, this is a lot more mature code than the near-useless stubs found in the production ROM, but still far from completion.
CLOSE
still crashes mishandling unassigned streams, but at least the intended behavior is now entirely clear, unlike with the production ROM. The routine that I labeled CLOSE_3
shows clear signs of binary patching; this must have been a part actively worked on. The table of channels with known closing procedures is properly terminated in contrast to the production ROM. In case of unknown channels, the closing procedure’s address is read directly from the channel descriptor by CLOSE_3
, which is the obviously correct abstraction which would have made it unnecessary for ZX Interface 1 and others to trap this part of the ROM. The only remaining issue is detecting and correctly handling the corner case of unassigned channels.
STR_DATA
and related routines that help finding the stream descriptor have been a bit refined to allow more fine-grained processing. An obvious improvement over the production ROM.
OPEN
is ready. This is how it should have been released instead of how it actually was. Upon not finding the channel letter in the table of known opening procedures, it reads a system variable that I called OPEN_X
, and uses that, if set. Otherwise, it reports an F Invalid file name
error. Unfortunately, replacement ROMs, such as my ZX82 cannot adopt this solution, because it would break compatibility with too many existing software and hardware.
Also note that opening and closing procedures for channels “N” (LAN) and “T” (RS232) have been added. The opening procedure for “N” expects a remote station number after the letter N
in the string argument of OPEN #
, in contrast to ZX Interface 1, where it is a separate argument.
The MOVE
command that is stubbed in the production ROM accepting two comma-separated string arguments, has been turned into a useful instruction accepting two numerical arguments separated by a comma and a hash symbol: MOVE #from,#to
moving data from one stream to another until EOF. Entirely analogous to ZX Interface 1’s MOVE #from TO #to
except for the delimiting token. Even the omission of not checking the BREAK key is the same.
FORMAT
, accepting a single string argument was also stubbed in the production ROM, whereas here it allows for setting up the baud rate of the RS232 interface and the station number of the LAN interface, both within the string after the letter identifying the channel. In ZX Interface 1 and in ZX Spectrum 128k, these are additional numeric arguments to the FORMAT
instruction. The calculation in VM bytecode turns the baud rate into a timing counter value. It is probably not very precise, definitely lacks sanity checks, but might just work for some values. In the absence of hardware, it is difficult to check. Also note that unlike OPEN
, FORMAT
does not allow for extending the set of allowed channels, so it is not quite ready.
The corresponding entries in the syntax table have been updated, so these instructions actually work.
1f12 P_FORMAT:
1f12 0a 00 defb 0ah, 00h
1f14 5b 1b defw FORMAT
1f16
1f16 06 2c 23 06 00 P_MOVE: defb 06h,",#",06h,00h
1f1b 39 1b defw MOVE
1f1d
1f1d 0a 00 P_ERASE:defb 0ah, 00h;
1f1f 9e 1b defw CAT_ETC
1f21
1f21 00 P_CAT: defb 00
1f22 9e 1b defw CAT_ETC
The additional service routines for “T” and “N” channels are not that interesting and probably not even working correctly, given that they were never tested on real hardware, as it never existed. The rest of the code in this ROM is identical to that in the production ROM, except for the displaced addresses and system variable offsets.
All in all, we can conclude that firmware development was still weeks from being ready and already weeks late for the release. Finishing what we see started here would have certainly made the ZX Spectrum a much better machine and a much more future-proof platform than it was, but whether it would have made it a bigger and more lasting success or being more than a month late to market would have relegated it to obscurity is anyone’s guess with no way to decide.
This software archeology project would not have been possible without
- the generous donation of the prototype machine to the Centre for Computing History by John Grant,
- the publication of its ROM content by the Centre for Computing History (http://www.computinghistory.org.uk/),
- information provided in private correspondence about the historical and technical context by Steven Vickers and John Grant,
- the pioneering work in disassembling the production ROM by Ian Logan and Frank O’Hara,
- more information about the historical and technical context by Andrew Owen,
- my parents, Olga Burda and AndrĂ¡s Nagy buying me a ZX Spectrum+ in 1986,
- my wife, Valentina Nagy being patient and supportive while I was working on it.