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:
MOVEtoken got a trailing hash symbol like
MOVE #), which was not even reflected by the stickers on the prototype’s keyboard.
- An additional error report
S Device unformattedhas 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
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
RUN mentioned above. The instructions at
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
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
The next substantial difference with the production ROM is in the routine handling
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
MOVE # 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.
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
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.