ZX81: On to Machine Code

All right. It’s time to step away from BASIC and start working with machine code. By the end of this post we’ll have a technique for mixing machine code with BASIC programs, and also have a functioning Hello World program that’s 100% machine language while still being as easy to load and run as any BASIC program.

Sinclair BASIC offers us only one mechanism to get into machine code from BASIC: the USR function. This takes the address of a machine code routine and jumps to it. The ABI rules are pretty simple:

  • The I and IX registers are sacred to the display routines in the ROM, and so we are not permitted to use those if the display is active.
  • EDIT: ADDED 11 Mar 2017: The AF' shadow register is also reserved by the display routines, which means that you may not use the associated EX command. EXX is still fine, however.
  • The IY register is set to $4000, the start of system variable space. BASIC expects this to be the case but will put some reasonable effort into forcing it to that value. In practice, though, if you need IY at all $4000 is probably where you were hoping to put it anyway.
  • You return to BASIC with the RET instruction or one of its conditional variants. The value in BC is treated as an unsigned integer and this becomes the value of the USR function.

An Example From the Literature

Let’s look at the game “Surge”, by Tim Rogers. This was a short type-in program about flying a spaceship through an asteroid belt. To check for collisions, the program consults the value of the screen memory where it is about to draw the ship’s next position. This task is complicated by the fact that lines of screen memory might be different lengths, so you can’t simply compute the location of a point on the screen from the base address. The location might not even exist. To address this situation, location $400E (named DF_CC by the documentation) holds the address of the point in screen memory the cursor is pointed at. So, to check the screen memory at row R, column C, place the cursor with PRINT AT R,C;, which prints nothing at a location and keeps the cursor there, and then checking screen memory with PEEK(PEEK 16398 + 256*PEEK 16399).

That PEEK statement is pretty cumbersome, so Tim Rogers rendered it as a USR routine instead, which is seven bytes long:

2A 0E 40    LD HL, ($400E)
4E          LD C, (HL)
06 00       LD B, 0
C9          RET

(Oh, um, I seem to have not actually mentioned this yet, but the ZX81 uses a Zilog Z80A for its CPU. This is actually the first time Z80 code has appeared on Bumbershoot Software.)

An interesting property of this code is that every single byte in it except for one is either a character code for a symbol on your keyboard or the token of a BASIC keyword. That means you can almost completely type it in, in its binary form, into a program comment. This is exactly what Surge does:

surge_01

And since it’s the first line of the program, it’s at a very consistent location, so making use of it is pretty easy too:

surge_80

The untypable byte is POKEd into place by the third line of the program, but there’s nothing stopping you from entering the line, entering the POKE as an immediate command, and then leaving the now-perfect line as-is.

And in case you’re curious, the untypable byte isn’t $00. It’s $4E. $00 is space.

Generalizing the Technique

I’ve always wanted to do this trick on the Commodore 64, but you really can’t do it in Microsoft-descended BASICs. Those BASICs use null bytes as line terminators, and it’s very difficult to write a useful assembly language routine with no $00s in it. Each program line also includes a pointer to the next line, but the BASIC does not trust those pointers, rewriting them on every load and using them only to quickly search the program for a specific line number. Every other operation relies on the null terminators.

Sinclair BASIC, however, prepends each line with its length and this length value is the only source of truth. There are thus no real restrictions on this technique at all. Unprintable bytes show up as question marks in listings and do no harm. $76 (newline) and $7E (the byte that indicates the next five bytes are a parsed floating-point number) will garble the listing so that you can’t reconstruct the size of the program from inspection, but otherwise don’t hurt anything. A program large enough that it doesn’t fit on the screen might confuse the ROM enough to soft-lock it when you LIST the program, and that’s extra nasty because as we’ve seen Sinclair BASIC likes to fill the top of the screen with program listing when it’s not doing anything else, but this isn’t a problem in practice for three reasons. First, it doesn’t always happen. Second, as we noted last time it’s very easy to make the program autorun right after loading so there’s no chance to list it. And finally, the saved-out state of the BASIC interpreter includes the state that indicates where the listing is scrolled to, so a long program can simply be scrolled off the screen before it’s ever loaded.

That does leave one problem, though: there is still no equivalent of SYS. The USR function returns a value, and in BASIC, unlike C, values may not be ignored. Functions and statements are fundamentally different things in BASIC. There seem to be two traditional solutions:

  • If your program is a meaningful mix of BASIC and machine language, machine language routines all return a status code of some kind that the BASIC framework uses to decide what to do next. That’s probably going to mean “assign the result to a variable”, but a real slick dispatch system could get a lot of mileage out of a statement like GOTO USR 16514.
  • If your whole program is machine code, take the result from USR and use it to reseed the random number generator. Variations on RAND USR 16514 seem to show up a lot.

In the end, that meant that this technique was the one that everyone actually used. There are alternatives—one of which I will explore in detail later on—but jamming your machine code into REM statements is unquestionably the community standard.

Getting In On the Act

It’s time to start writing some machine code ourselves. This is the first Z80-based system we’ve worked with here, so we’ll need a new toolchain. I ultimately went with the z88dk suite, which targets tons of systems (including the ZX81) out of the box, includes its own C compiler, and is also part of the stock repos of both Fedora and Debian. It got me off the ground pretty much immediately and I’m very happy with the result.

Let’s get started with a simple Hello World program:

        LD      BC, msg
lp:     LD      A, (BC)
        CP      A, $FF
        RET     Z
        PUSH    BC
        SUB     27
        RST     $10
        POP     BC
        INC     BC
        JR      lp

msg:    DEFB    "HELLO",27,"FROM",27,"THE",27,"ZX?8",27,"WORLD6",145,255

z88dk’s “Z80ASM” program treats all strings as ASCII, so I bridge the gap between the two encodings by normalizing on A (ASCII 65, ZX81 charset 38) and using a fixed offset of 27. Numbers, spaces, and punctuation all end up getting warped a bit as a result. The actual printing of characters is farmed out to the ROM with the RST $10 instruction.

We can assemble, link, and package this program with the following two commands:

z80asm -b -r4082 hello.asm
appmake +zx81 -b hello.bin

This gets us a file hello.P. Load that up into our emulator, type RUN, and…

zx81hello_output

Excellent. Let’s took a look at the listing:

zx81hello_rem

Two things jump out from this listing. First, notice that the whole last bit of the program—the mostly-ASCII message—is all question marks. It turns out the unprintable/untypable range in the ZX81 character set is $43$7F and $C3. That includes nearly all ASCII letters. Second, notice the command that appmake uses to start the code is not RAND USR 16514, but the more verbose RAND USR VAL "16514". It turns out that this is more economical with memory. Remember, every time you type a numeric constant into program text, six additional, invisible bytes are added to the program text to represent that number’s floating-point representation. That means that the statement RAND USR 16514 is actually thirteen bytes long: one each for RAND and USR, five for the individual digits of 16514, and then six more for the converted form. In comparison, this version uses a string constant instead of a numerical one. We spend three additional bytes on the quotation marks and the VAL keyword, but then save the six bytes of the parsed form.

Some of the type-in programs of the era went to extravagant lengths to never type in numeric constants directly. I’m not actually sure how wise that was; if you were a large enough program to require the 16KB expansion back, you had plenty of room for numbers, but processing cycles be just as desperately scarce as they always were.

The Road Ahead

At the moment, I have three goals for my Sinclair work:

  1. Replicate and improve upon the ZX81 program packager that z88dk provides. In particular, I want to have better control over autorun behavior and experiment with other mechanisms for loading and saving machine code.
  2. Produce a machine code program for the ZX81 of a reasonable size and put it through its paces.
  3. Come to grips with the various techniques developed for “pseudo hi-res graphics” and explain them. Actually using them isn’t really on the table right now, much less attempting to develop my own pseudo-hi-res routines instead of simply adapting others.

I’m most of the way there for the first already, and the second is done except for those parts of the first that need to be made to work. I believe I have located all the documentation and sample code that I need to do the third, but I haven’t made any real progress with it yet.

References

  1. Baker, Toni. Mastering Machine Code on Your ZX81. Reston, 1982.
  2. Rogers, Tim. “Surge.” 51 Game Programs for the Timex Sinclair 1000™ and 1500™, edited by Tim Hartnell, New American Library, 1983, pp. 16-17.
  3. z88dk z80 Development Kit, https://www.z88dk.org.
Advertisement