Inside the Prototype ZX Spectrum’s ROM

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 like OPEN # and CLOSE # (becoming MOVE #), 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 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 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                    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 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 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 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 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 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                    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 1e 01              OPEN_K: ld e,001h 
1b22 18 06                      jr OPEN_END 
1b24 1e 06              OPEN_S: ld e,006h 
1b26 18 02                      jr OPEN_END 
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 c3 b8 0c           OPEN_N: jp OPEN_N_DO 
1b35 1e 15              OPEN_T: ld e,015h 
1b37 18 f1                      jr OPEN_END 
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 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                    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                    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 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 06 2c 23 06 00     P_MOVE: defb 06h,",#",06h,00h 
1f1b 39 1b                      defw MOVE 
1f1d 0a 00              P_ERASE:defb 0ah, 00h; 
1f1f 9e 1b                      defw CAT_ETC 
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 (,
  • 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.