Our ultimate goal with the ZX81 here is to work out how to get our own machine code into it so we can watch it go, and maybe also learn about the oddities of the platform. Unlike its successor the ZX Spectrum, the ZX81 was not particularly beloved—I’ve only seen a few instances of high-quality spec-smashing work with it and the most dramatic of them are extremely recent. I also don’t actually fully understand how they work yet, so for now we’re just going to try to work out program and save file organization.
Unlike the Commodore, Apple, and DOS BASICs, Sinclair BASIC has no genetic relationship with Microsoft BASIC. Many aspects of the system are different, and the data that’s saved and loaded is wildly different.
Some things are the same, of course. In this era of BASIC, all lines of a program have a line number associated with them, and the program is organized in line number order. In the Microsoft-descended BASICs, program lines are silently added to the program. They aren’t even syntax-checked until the program itself is run.
Sinclair BASIC is a little more interesting. You only can enter text at the bottom of the screen, as we saw in the previous article, but as you enter a program it appears at the top:
The traditional Hello World program is only one line of BASIC that is usually typed in outside of a program. I’m not sure why, but it seems like the usual First BASIC Program is one that converts temperatures.
Notice also the inverse-video greater-than sign by the final line of the code. That’s our cursor into the code itself. Unlike the other BASICs, Sinclair BASIC does syntax-check your lines as you type them and will refuse to add them if they’re unparseable, but if you made an erroneous but legal statement, it will add it happily. You can move the cursor back and forth through the program, or set it directly with LIST, and then call up any line for editing later. It’s less convenient than the C64 or PC BASIC’s full-screen editors, but it’s easier to fix mistypes than on an Apple.
Running out of memory
Now, suppose we get frisky and decide to add absolute temperature. Something interesting happens if you do that on an unexpanded ZX81:
(This doesn’t happen in the sz81 emulator, which always includes a RAM expansion in its emulated system, but EightyOne can be configured to have only the stock hardware.)
What’s going on here? Well, the stock ZX81 only had 1KB of RAM. While that’s eight times as much as the Atari 2600, the Atari didn’t use RAM for its program or its display, while the ZX81 needs to use it for both. A 32×24 display is 768 bytes, and this final line of code starts bumping up against memory limits. However, to their credit, Sinclair Research expected this to happen. We are seeing the system make more room for our program by compressing the screen memory.
It’s not actually reducing the number of lines on-screen, though. The screen memory (or “display file”, as they call it) is actually a list of 24 lines, each of which is individually terminated with a newline. IF you have 4KB or more of RAM installed in the system, each line will be the full 32 characters long, which, with terminators (and an additional newline at the start that signals the beginning of the screen), takes up 793 bytes (33 per line, 25 per line, one initial newline). Under more constrained conditions, an empty screen is just 26 newlines.
Saving the result
As far as the ZX81 is concerned, saving a file means emitting a very precisely modulated angry buzz out its speaker and into a tape recorder for a few minutes. That doesn’t do us much good, so we turn to emulation to get a more pliable format. EightyOne includes a sophisticated tape emulation system designed to handle the entire line of Sinclair products, built around the TZX format, and while it’s possible to work with this, it’s not easy to work with. The sz81 emulator works with a simpler format called the .P format. The audio produced by a SAVE is some synchronization signals, followed by an encoding of a bunch of bytes. A .P file is just those bytes.
But what are those bytes? Well, it’s a memory dump. The start point for the memory dump is fixed, and it’s in the middle of the system’s critical control variables. The end point is the end of BASIC’s variable space.
This has some super-neat side effects, and some super-dumb ones. The neat bits:
- If you save out the variables, this means that you can actually independently initialize important data as part of the loading process and skip initialization.
- The system variables that are stored out actually include enough state for the BASIC interpreter that you can cause loading the program to automatically run at any point.
The dumb bits:
- If you save the variables out and rely on this, then the end user typing RUN instead of, say, GOTO 1 will wipe out all those variables with no real way to get them back.
- If you don’t rely on saving the variables, your program is larger if you don’t execute a CLEAR command just before you save.
- The display file is actually located between the BASIC program and its variable area, so you end up saving out 793 bytes of whitespace with every file you save.
- Loading a file that’s too large for your system—say, because it was saved on a 16KB ZX81 and you’re loading it on a 1KB ZX81 or a 2KB T/S 1000—you’ll set some of BASIC’s core pointers to invalid RAM locations and lock the system so hard that you’ll have to unplug the computer to fix it.
Good and dumb bits aside, though, this means that if we know what the bytes of a BASIC program are, packaging it in a .P file isn’t hard. It’s more work than a C64 BASIC program, but fundamentally you’re just computing a few pointers based on the size of the BASIC program, and then writing those values out alongside a large number of values that are constant in every saved program. It’d be nice to also get control of the autorun behavior enough to let us autorun from arbitrary lines without doing anything to the program being run, but that’s a secondary goal for now.
BASIC lines themselves are pretty straightforward and in fact have their binary format documented in the manual. Each line is a two-byte bigendian line number, a two-byte little-endian line length, and then a tokenization of the line. Entertainingly, you can get the token for any command by simply taking the character code of the command—those commands all being single keystrokes wasn’t just for show. You were typing the tokenized version of the program in manually.
Well, almost. It turns out that every time a numerical constant appears in your source code, the tokenized version of it follows up the number you typed (replicated verbatim) with byte $7E and the five-byte binary floating point representation of that number. That’ll let us have some fun.
Having some fun
The ZX81 manual included this as an exercise:
If you enjoy humiliating computers, type PRINT "2+2 = ";2+1
I am unimpressed. The computer is clearly being instructed to give the wrong answer. Instead, let’s make that a program line, and a correct one: 10 PRINT "2+2 = ";2+2. The BASIC program always begins in memory at memory location 16509; a little exploration with PEEK shows that the first instance of $7E in the program source is at memory location 16524. That means that memory location 16525 is the binary exponent of the floating point number representation of the first 2 outside of quotation marks. If we change that 2 to a 1 by dropping the exponent by 1, with a command like POKE 16525,PEEK 16525-1, the listing will remain unchanged but RUNning the program will happily inform us that 2+2=3 with no visible change in the source.
It’s petty, but I’ll take my victories where I can get them.
- Vickers, Steven. ZX81 BASIC Programming. Sinclair Research, 1981.