Skip to content
HN On Hacker News ↗

ZX Spectrum System Tour: Text Mode

▲ 57 points 4 comments by rbanffy 3w ago HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is human-written.

0 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 1 of 1
SEGMENTS · AI 0 of 1
WORD COUNT 1,735
PEAK AI % 0% · §1
Analyzed
Aug 3
backend: pangram/v3.3
Segments scanned
1 windows
avg 1735 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,735 words · 1 segments analyzed

Human AI-generated
§1 Human · 0%

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 LOADing—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 and then jump past the loader logic to cause the program to exit normally. The end result of all of this a program that runs its machine code component when run, but which also, when loaded, carries out all necessary preparations exactly once. Turning this into something I could use as a turnkey packaging script is a bit out of scope for this tour, so I’ll just link to the original article about it and a copy of the script itself, if you want it. Machine Code’s View of the Spectrum The Z80 has a 64KB address space and on a 48K Spectrum all of it is used. $0000–$3FFF is the system ROM. $4000–$5AFF is the graphics RAM, for both the bitmap data and the color table. Everything else is RAM for use by the system. This is basically what it looks like on 16K and 128K Spectrums as well, though RAM on the 16K ends at $7FFF and both the ROM and RAM on the 128 are bankswitched. When writing our assembly language code, we can’t use facilities that are owned by the system ROMs. That includes everything to do with interrupts, the shadow registers, and the IY register. The IY register is special though, because while we are not allowed to edit it, we can and regularly will read it (it’s always $5C3A) and also index from it: it holds the base address of what Sinclair calls the system variables, a collection of data about the current system state that we will regularly find ourselves reading and writing. The locations of these variables are far more consistent across incompatible variants than the ROM; I leaned on this quite heavily when I made my first project. The ROM itself turns out to be pretty casual about this, often referring to system variables directly by address, but then also indexing IY to access them as needed. Indexed operations are more expensive but more flexible, so a use case like this one where you know exactly what your base address is but also have an index register will cause you to swap back and forth between using absolute addresses and using indexed access. I’ll be writing my code here such that all system variable access goes through IY but this wasn’t generally done. (Indeed, I myself do not do it in the Spectrum platform guide.) Printing Out Text As we saw in our BASIC tour, the Spectrum divides the screen into a 22-line upper window for its main display and a 2-line lower window for input and status messages. This is not an artifact of BASIC; this division is present at the machine-code level as well. In order to print out text to the upper window, we need to configure the system variables appropriately to tell its output routines to direct text output there; we can do this by writing a 0 to TVFLAG at IY+2. Once this has been done, we may call the ROM routine at address $10 to print the character in A. Spectrum uses ASCII so normal text will be quite familiar to us. That makes Hello World pretty straightforward: org $7000 ld (iy+2),0 ; Write to upper screen ld hl,msg lp: ld a,(hl) inc hl or a ret z rst $10 jr lp msg: db "Hello, world!",13,0 I usually use $7000 as my origin address because it’s deep enough into RAM to leave BASIC some breathing room while still having enough space left to probably let my smaller projects still fit on a 16K machine. I assemble this with a command like sjasm hello.asm hello.bin and package it with spectralink.py hello.bin 7000, then load up hello.bin.tzx into FUSE and… We are off the ground! Defining and Using Graphical Characters When working with BASIC, we found that we had two kinds of graphical characters: built-in semigraphics and then a suite of reprogrammable characters it called “user-defined graphics.” We’d type them in by shifting into “graphics input mode” and then pressing keys 1-8 for the semigraphics (using inverse-video mode as necessary to get all 16 possible combinations) and A-U for the user-defined characters. This is a place where things are slightly easier in machine language than BASIC. “Graphics Mode” is a quirk of the input system, and freed of the necessity of directly typing in the characters we want we can simply pass extended characters on to the character-printing routine at RST $10. When using the predefined semigraphics, we get another nice bonus: the whole mess with inverse video was an awkward workaround we faced due to not having enough keys on our keyboard. Under the hood, we have all 16 possible semigraphics characters immediately available in the range $80–$8F. They are also laid out in a sensible order: bits 1, 2, 4, and 8 correspond to the upper-right, upper-left, lower-right, and lower-left quadrants of the character, respectively. The user-defined graphics are similar; these occupy the range $90–$A4 and run from graphic-A through graphic-U. (The 128, with two fewer characters available, only offers up through $A2.) The rest of the character set through $FF is the abbreviated forms of BASIC keywords. (That’s why we lose a couple spots on the 128, incidentally; that’s where the SPECTRUM and PLAY commands show up.)