ZX Spectrum System Tour: Text Mode | Bumbershoot Software
Now that we’ve taken a look at the kind of control that the Spectrum’s BASIC gives us over the hardware, it’s time to dip down into machine language and see what is offered to us there. There’s quite a bit more to cover in this realm, and I expect it’s going to end up spread out over quite a few posts.
It is also quite a bit more fraught than on many other platforms, because Sinclair only paid minimal attention to exposing a consistent and structured firmware interface. Commodore’s 8-bits, from the original PET through to the C128, all had backwards-compatible jump tables at the end of their "KERNAL" ROMs which meant that quite a lot of machine language code could function identically even when doing things like file processing and disk access. MSX was not a single computer at all, but an industry standard that manufacturers wrote to, and a major part of that was a well-defined jump table up near the interrupt vectors. The IBM PC’s BIOS served a similar role, though less voluntarily.
The Spectrum, however, has only a handful of defined entry points and as a result developers tended to directly call deep within the system ROM itself in order to get the results they wanted. The Timex Sinclair 2068 had an incompatible ROM and as a result almost no Spectrum software ran on it—it seems like this was understood to have been a major contributor to its failure in the US Market. I’m a bit skeptical of this, because that meteoric US crash was shared by the MSX, the Commodore 16, and the Coleco Adam, all of which were plausible direct competitors to the TS2068. However, given that the 128 was released after the 2068’s failure, and that the 128 kept the relevant parts of its ROM very carefully compatible with the 48K version, I suspect that Sinclair nevertheless learned an important lesson here.
For the purposes of this series I will be taking the 48K Spectrum as the target platform, but also making sure that the techniques I describe also work on the 16K and 128K machines. There were a vast number of Spectrum clones and successors, of varying fidelity; if I encounter something interesting regarding them along the way I’ll bring it up, but it will all be purely sidebar territory.
For our first foray, I’m going to look at the facilities we have to command to get a display like the 100%-BASIC "Manic Mechanic" game I showed off last week:
In this first post, we’ll start by looking at basic system organization: how machine language programs exist on the system, how they are loaded and run, and how they coexist with BASIC and the BIOS. Then we’ll dig into the facilities for text-based displays on the system, including the "user-defined graphics" facility that Manic Mechanic itself relies on. We’ll wrap up by looking at keyboard and joystick input, which turns out to be quite close to what we had to do in BASIC.
First Principles: Running Code at All
When I first started exploring the ZX Spectrum, I was very pleasantly surprised at how easily the BASIC could coexist with machine code. Many of the standard BASIC commands have extra modes that lend themselves very well not only to pure machine code programs but to mixed BASIC/ML combinations.
Files on cassette identify themselves as BASIC code, BASIC data, or binary data. BASIC’s SAVE and LOAD commands have extensions to specify what exactly is to be loaded, similar to the distinct LOAD and BLOAD commands in other BASICs. To load machine code from tape, we put the keyword CODE after the filename.
BASIC can restrict its memory usage by passing a parameter to the CLEAR command. By lowering the top of BASIC’s memory to just below your machine code’s loading point, memory conflicts should be entirely avoidable.
BASIC calls into machine code subroutines with the USR function. This takes the address of the routine as its argument; the routine does its work, then returns to BASIC with a RET instruction. The value of the BC register pair is returned to BASIC as the value of the function as a 16-bit unsigned integer. Routines with no meaningful return value generally dispose of it by passing it to the random number generator: RANDOMIZE USR in Sinclair BASIC accomplishes much the same thing as SYS in C64 BASIC.
In addition to saving out ranges of binary data, the SAVE function can also save a BASIC program such that it starts running immediately after LOAD ing—and it allows you start execution from any line number you wish. Back in 2017 I created a somewhat more sophisticated BASIC stub for my Spectrum programs that I’ve been using ever since. The general strategy was this:
The BASIC program is saved so that it begins execution on line 30 instead of 10.
Code at line 30 restricts BASIC’s memory with a CLEAR command and then loads the next file on the tape (our actual program) as binary. Once the load finishes it then jumps up to the top of the program.
Lines 10 and 20 call the loaded machine code with RANDOMIZE USR...