Zeus Z80 assembler extras

This page collates files I have made in the course of customising Simon Brattel and Neil Mottershead's Zeus assembler for the ZX Spectrum Next and experimenting with it. The examples listed later on this page can be typed into any Z80 assembler, not just Zeus, though the code downloads are in Zeus's tokenised compressed binary .god format.

Simon N Goodwin

Quick index
Improved Zeus for Next
A minimal dot command
INKEY$ from Z80, via the FPC

Improved Zeus for Next

I've been delving again into Zeus, the Spectrum assembler formerly sold by Sinclair Research in 1983 and now free in tools/dev on your Next SD. I've fixed the bug which stopped it working in 128 BASIC (previously requiring extra memory and a code patch on Next) and made a minimalist NextBASIC 'IDE' to save remembering addresses and typing LOAD and SAVE commands in BASIC to transfer assembler source or assembled code to SD, ZX Net or wherever.

Here is the Patched version of Zeus, with better 128 compatibility and stability on Next (patches by Simon N Goodwin, original courtesy of Simon Brattel). My 'mini IDE', ZEUSx.bas gets around the regression of 128 BASIC wiping the screen of T command output. It also allows us to load source and save source and binary without any risk of mistyping CODE arguments. It just fits in the same space as the original patch loader! It uses ZEUSx.bin from the patcher, as linked above.

After loading Zeus and adapting input to the CPU speed it prompts for a project name, loads the source file e.g. project.god and enters the assembler. After assembling when you return to BASIC line 80 reads the start address of the source, while other DPEEKs find the end, and binary if any was assembled, saves that as project.bin, and re-saves the updated .god tokenised source file. Eat your heart out Xcode, VS, CodeWarrior etc.

ZEUSx.bas makes assumptions, but it is written in BASIC so easy to edit if those are invalid. Specifically it assumes the source is at the default address 32768 (but if that's changed it spots and honours the change later) and that the first line of assembled code or data to be saved has its address labelled with ENT.

Both are good defaults but not always the right choice when working with big programs. We should have room for an extra 50 lines or so now the printer buffer no longer needs saving. But actually the patch may be more significant than that, because I found the PAGE over-write was also affecting return to BASIC and even the resolution of some forward jumps.

Here is the new Zeus loader with automatic project LOAD and SAVE for source and binary in standard ZX CODE files. It replaces the original loader and patcher Zeus.bas.

The patcher ZEUSy.bas loads ZEUS.BIN from your SD, sets a safe keyboard rate, flags a cold start for the source default address, fixes the off by 1 error in CLS that broke Spectrum 128 paging, and sets some other defaults which make automatic saving easier later... ZEUSx.bin is the result, same size as the 1983 version but 128-friendly and with room for even more lines of yummy Z80 assembler.

image of Zeus patching BASIC code

The POKEs ensure a known state so a BASIC framework can load and autosave source and binary. It infers the object code addresses from ENT and asmPC, the next codegen address (cheers Simon Brattel for the disassembly!) so I init those to 0 - in the distro image asmPC is 30017 IIRC so someone did the manual example before it was saved. Also the magic startup flag is 42 (indicating already initialised) so the distro copy never coldstarts, though that seems benign. And then I set the source pointer to 32767 so the T command reports 0 bytes not 53000-odd if nothing's loaded.

Zeus patcher - this loads Zeus.bin from Tools/Dev in the Spectrum Next distro and saves an improved version Zeusx.bin, which you can download from the link above. This file is not needed unless you're interested in how the patched version differs from the 1983 release or want to add further patches.


A minimal DOT command

Interested in writing dot commands but not sure where to start? This tiny but complete example shows how to read the command arguments and write messages to any channel from a dot command assembled to the required offset by Zeus. Put whatever you like in the middle, but read the Next system documentation before you make any assumptions about memory paging!

Zeus source for a minimal but useful dot command, ECHO - this file is in tokenised .god format which loads directly into Zeus. It shows how a dot command can be implemented in just 336 bytes of source - less than a page: ECHO source code in Zeus

The assembled ECHO command, just 17 bytes of assembled Z80 code. Compare that with the equivalent directly-executable native code command file size in 'modern' C-based systems... /bin/echo is 18,128 bytes on my macbook, but does have the extra -n option - you should be able to add that on Next in fewer than 18,111 bytes, if you need it!

Notice that A is 0 at the end and the Z flag is always set on return to signal 'no error' - see the docs for how to return an error code or message.

Here's the Dot-command converter to take the CODE saved after assembly and make a raw file which can be placed in C:/DOT directory and run by typing its name, e.g.

.ECHO

The key part of this is the loop at the end that writes a binary file with no header. ZX BASIC adds PLUS3DOS headers to files it saves, but those stop dot commands loading and working as expected. You'll need to do this for any dot command written on Next itself and saved as CODE from BASIC.

The first few lines sets up and run Zeus.bin from the March 2020 distro. Tweak this to suit yourself, e.g. to use the patched Zeus start at 57344 rather than 57062, and replace ECHO.ASM with ECHO.GOD to use the source version downloadable from the link above. The Next system browser was confused by .ASM used here, so the Zeus IDE now uses the .god extension for Zeus tokenised source files.


Reading CODE INKEY$ with the Floating Point Calculator

The BASIC command INKEY$ is a useful way to detect when a single key is currently pressed, and know which key it is. CODE INKEY$ returns the ASCII code of the character, e.g. 65 for "A", 49 for "1", etc. But it's difficult to call the corresponding Z80 routine in the ROM from assembly language, because the subroutine returns not a character code in a register but a five-byte entry on the BASIC calculator stack.

As well as BASIC and Z80 machine language, every Spectrum (and ZX-81) supports an internal language called Floating Point Calculator code, which is embedded in Z80 programs between RST 40 instructions and 'end-calc' FPC instructions that switch from the stack-based Forth-like FPC language to the hardware-level Z80 instruction stream.

FPC code is slower than Z80 assemble language but it can manipulate strings and five-byte decimal values accurate to nine decimal places, as well as the simple bytes and 16-bit integers natively supported by the Z80n. It is much more concise than Z80 code for this purpose, and extensively used in the ZX ROM to plot curves and implement complex mathematical functions like ATAN, LN and the ^ operator. ZX BASIC would be a lot less capable without it.

This example was written to test out the FPC disassembler in my Nextramon utility (shipped in the Next distro, alongside Zeus in Tools/Dev) and draws on examples from books by Ian Logan. It demonstrates a very general way of calling ROM routines even when they do not have a convenient entry point or ideal input and output expectations for Z80 programmers. It also enabled me to fix a bug - RST 40 was being incorrectly disassembled as RST 56. This is fixed in the version here and will be corrected in the next distro release.

Here is the code-inkey binary ready to load with LOAD "code-inkey.bin" CODE - it is fully relocatable and can run from whatever address you load it to.

Here is the code-inkey source code, in Zeus assembler format ready to load and assemble with ZEUSx.bas as documented above. Zeus does not know about the FPC calculator tokens so most of those instructions are byte values prefixed with DEFB. Unlike other dissassemblers, Nextramon detects and decodes FPC instructions automatically as it finds them in a Z80n program. Here is the relatively easy-to-follow dissassembly, with comments noting the token and call labels.

65500 3EA6     LD   A,166      ; INKEY$ 
65502 CD282D   CALL 11560      ; Stack-A 
65505 EF       RST   40        ; FPCALC 
65506 2F       CHR$ 
65507 18       VAL$ 
65508 1C       CODE 
65509 38       End-calc 
65510 C3A22D   JP   11682      ; Fp-to-BC

Two ROM routines are used to get integer values into and out of the floating point calculator. The routine at address 11560 is known as 'stack-A' and puts the value in the a register onto the 'calculator stack' where it does arithmetic. In this case the value of A is 166, the token of the INKEY$ function in ZX BASIC. The second ROM routine serves the opposite purpose. The routine at address 11682 reads the last value from the calculator stack and stores it as 16 bit integer in the BC register pair. This is uncoincidentally where USR expects the value it is to return to BASIC, so a USR call to address 65500 returns to BASIC the result of the preceding sequence of FPC instructions.

The ingenious bit is the four-byte sequence after the RST 40 instruction switches the Spectrum into FPC language. The CHR$ function, FPC token 47 ($2F) works as in BASIC, converting a byte value 0..255 into a single-character string. Now we have a string "INKEY$" on the stack, Logan showed how the much-maligned and misunderstood VAL$ function can be called to evaluate this 'string expression'. Token 24 ($18) calls the ROM string expression evaluator, which is not otherwise easily accessible, to find the string value of VAL$ CHR$ 166.

As in BASIC, VAL$ CHR$ "INKEY$" (where the string contains the INKEY$ function token byte 166, rather than the six-character name) returns the same thing as INKEY$ but by a more devious route. In this case the deviation is necessary to call the INKEY$ ROM routine in the context it expects. The next FPC instruction, byte value 28 ($1C), calls the ROM CODE function. This takes a string and converts it into an integer in the same place on the FPC stack. Token 56 ($38) stands for 'End-calc' and returns from FPC sub-language to Z80 code. The final JP to 11682 (Fp-to-BC) gets the stacked value into the BC register so it can be returned by USR. As we want to return after this, we use JP rather than CALL, saving a byte of code and five avoidable memory transfers.

The FPC instruction set is listed in the NextGuide-format documentation for Nextramon, downloadable from here. This includes links to the books by Ian Logan which explain the FPC works and show how it can be used. Use the .GUIDE command on the Next itself to read this hypertext file.

The following BASIC example program POKEs the code-inkey routine into any specified address and calls it repeatedly in a loop until the space key (ASCII code 32) is pressed. The PAUSE and BORDER statements in the REPEAT loop change the border from blue to red while the USR call is being interpreted, showing how long it takes to run. It's much slower than using IN to read any single group of five keys, but does a lot more work, scalling all the keys and converting the result to ASCII as well as doing the necessary string and numeric conversions required by the ROM routines.

490 POKE 65500,62,166,205,40,45,239,47,24,28,56,195,162,45
495 PAPER 6: CLS
500 REPEAT
510   PAUSE 1: BORDER 2:%k=% USR 65500: BORDER 1
520 REPEAT UNTIL %k=32
530 STOP

Just pressing the SHIFT key is enough to alter the timing of the routine, moving the border stripes down. Switch the CPU speed with RUN AT or the NMI menu to see how the border stripes shrink at higher speeds. If the border tumbles and flickers this means the loop is taking more than one PAUSE frame - around a 50th of a second - to complete. At speeds above 3.5 MHz this should not be a problem until you put a lot more code into the REPEAT loop.

This little program can be downloaded from here. Add

515   PRINT AT 0,0;%k;" "; : BORDER 0

or similar to see the values being read back - and how slow BASIC PRINT commands are. Now the blue area of the border shows time spent on line 515 and any spare time in the frame appears as a black area.


Enjoy! Simon N Goodwin, August 2020

Link to Simon's NextBASIC demos and download page, for Nextramon and other goodies.

Link to Simon N Goodwin's personal home-page