Skip to content

TIL: ‘Hello, world’ in Z80 assembly language on the ZX Spectrum

I finally had a go at creating a machine code program and running it on a Speccy.

I recently received my ZX Spectrum Next after having backed a Kickstarter in 2020. I began my computing life on a Spectrum +2A in 1989, and it’s also the machine I first learned to program on, in BASIC. I knew you could write things ‘in machine code’ (like most games produced for the platform were) but figuring it out was way over my head at the time. (Programs written in machine code are much faster and have access to more memory, but are low level and more difficult to write.)

Now that I have ZX Spectrum hardware for the first time in a couple of decades, I thought it would be a good time to see whether I could create a ‘Hello, world’ program in Z80 assembly language, convert it to machine code, and run it.

Reader caution: I don’t really know what I’m doing here. This blog post is more or less personal documentation. I’ve tried to avoid inaccuracies, but feel free to correct me in the comments.

I found the take on ‘Hello, world’ described on Benjamin Blundell’s website to be nice and succinct, so I used that. Here it is:

org $8000
ld bc, MY_STRING

MY_LOOP:
ld a, (bc)
cp 0
jr z, END_PROGRAM
rst $10
inc bc
jr MY_LOOP

END_PROGRAM:
ret

MY_STRING:
defb "Hello, world!"
defb 13, 0

I’ll use the Pasmo assembler to translate the instructions into binary machine code so that the Spectrum can understand it.

Some notes, based on Benjamin’s blog post, on what each line does:

org $8000 permalink

This is an assembler directive to set the starting (‘origin’) memory location (address) of the proceeding program ($8000 is 0x8000, or 8000 in hex, or decimal 32768). This location is safely within a 48K Spectrum’s non-system memory. (See Dean Belfield’s website for details on how memory is mapped in the both 48 and 128K models.)

ld bc, MY_STRING permalink

Load register pair BC with the starting address of MY_STRING. Registers are small storage areas on the processor where data can be manipulated. We need to move a given value stored in memory into a register before anything can be done with it (eg print it on screen, do a calculation with it). We don’t need to think about this when working in a higher-level language like BASIC or JavaScript: memory management is handled for us. However when writing assembly language we need to move values explicitly between memory and the registers.

Regarding the address of our string, you may be wondering, as I did: ‘Hello, world!’ isn’t actually written to memory until later in the code, so how does the assembler know at this stage what the address is? The answer is that most assemblers, including Pasmo, run two passes, the first of which will read through the assembly code to determine the address of each label. Only on the second pass will the machine code be generated. So when the instruction ld bc, MY_STRING is encountered on pass two, the assembler knows the starting address of MY_STRING. This concept and process is referred to as ‘forward referencing’.

MY_LOOP: permalink

Add label MY_LOOP to mark a loop that will cycle through each byte of whatever is stored starting at location MY_STRING.

ld a, (bc) permalink

Load register A (the accumulator) with the value of the first byte at address MY_STRING (which will be the ‘H’ from ‘Hello, world!’ on the first go-around of this loop).

cp 0 permalink

Compare the contents of register A with 0. 0 is the last byte, after Hello, world! and a carriage return, set by the instruction defb 13, 0 later.

jr z, END_PROGRAM permalink

Jump to END_PROGRAM if the above comparison is true (ie the value in A is 0).

rst $10 permalink

Call the ROM routine at address 0x10, which prints whatever is in register A to the screen. RST (or ‘restart’) is the same as CALL, except it uses only one byte, and is faster. However, you can only use it with eight specific ROM addresses, one of which is 0x10, which we call here. (Source: ‘Spectrum Machine Language for the Absolute Beginner’, page 128.) You could think of these routines as built-in helper functions which perform a specific task.

inc bc permalink

Increment register pair BC so it moves to the next byte in memory (ie the next character in our string).

jr MY_LOOP permalink

Jump back to the top of the loop.

END_PROGRAM: permalink

Another label. Labels can have arbitrary values, and help us and the assembler navigate the code.

ret permalink

Exit program.

defb "Hello, world!" permalink

Define bytes with the string we want to print.

defb 13, 0 permalink

Define two more bytes — a carriage return (13) and string terminator (0) — so that cp 0 earlier in the code can check whether we’ve reached the end of the string.

I saved the file as helloworld.asm.

Converting our program into machine code permalink

We’ll use the command line tool Pasmo (a Z80 cross-assembler) to assemble our machine code and create our .tap file, which we can then run on a Spectrum emulator or actual Speccy hardware. The TAP will comprise helloworld.asm in machine code form, and a BASIC loader program. The latter loads the machine code into memory and runs it. (Alternative assemblers include zasm, and Odin and Zeus if you’d like to develop on an actual Spectrum.)

Build and install Pasmo permalink

I’m using macOS here, but the steps should be similar on other platforms.

  1. First install CMake, a C++ build tool. I’ll use Homebrew: brew install cmake
  2. Download Pasmo: git clone https://github.com/jounikor/pasmo.git
  3. cd pasmo/pasmo (steps 3 to 7 are taken from the Pasma README.md)
  4. mkdir build
  5. cd build
  6. cmake ../
  7. make
  8. Copy Pasmo to /usr/local/bin/ so that you can run it from anywhere: sudo cp pasmo /usr/local/bin/
  9. Relaunch the shell: exec zsh -l
  10. Verify that you can run Pasmo by viewing the manual page: pasmo man

Assemble our program and create the .tap file permalink

  1. Before we run the assembly process, add the Pasmo directive END $8000 at the end of your helloworld.asm file. This will prompt Pasmo to include a RANDOMIZE USR statement in the BASIC loader program. This will ensure our machine code runs when we launch the .tap file
  2. Run pasmo --tapbas helloworld.asm helloworld.tap

Open helloworld.tap in Fuse or another emulator, or on an actual Spectrum Next, as I did. It should look like this:

Screenshot of the ‘Hello, world’ program running on a Sinclair ZX Spectrum Next
Screenshot of the ‘Hello, world’ program running on a Sinclair ZX Spectrum Next. The original .src file was converted to a PNG with Remy Sharp’s image and font conversion tool, then that 256×192 file was upscaled to 1920×1440 with Pixelmator Pro’s nearest-neighbour algorithm.

The BASIC loader program permalink

This is the BASIC loader program that Pasmo created:

10 CLEAR 32767
20 POKE 23610,255
30 LOAD "" CODE
40 RANDOMIZE USR 32768

Line by line:

  • 10 CLEAR 32767 — ensure that the BASIC interpreter doesn’t write to memory above address 32767 (as well as clear any variables that are already stored there). This value should be a byte before the start of where our machine code will be stored.
  • 20 POKE 23610,255 — ‘avoid a[n] error message when using +3 loader’. (Source: spectrum.cxx in the pasmo-0.5.5 codebase.)
  • 30 LOAD "" CODE — load the next binary code file the Spectrum finds (which would typically have been on a cassette tape back in the day, located just after the BASIC loader program).
  • 40 RANDOMIZE USR 32768: call (run) the machine code that’s stored starting at address 32768, which we specified in our helloworld.asm code on line 1 (org $8000), and at the end (END $8000) as Pasmo requires. $8000 is a shorthand for hexadecimal 8000 (0x8000), or decimal 32768.

What next? permalink

I just received my copy of 40 Best Machine Code Routines for the ZX Spectrum by John Hardman and Andrew Hewson (with a new chapter on the Next by Jim Bagley), and it seems like an accessible guide to using machine code. I’ll try out some of the routines.

The tutorial ‘ZX Spectrum Machine Code Game in 30 Minutes!’ by Jon Kingsman looks intruiging, and promises the ability to program in machine code in the time it takes to consume a large cup of tea. I tend to ‘learn by doing’ so I think I may tackle this next.

Sources permalink