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.
- First install CMake, a C++ build tool. I’ll use Homebrew:
brew install cmake
- Download Pasmo:
git clone https://github.com/jounikor/pasmo.git
cd pasmo/pasmo
(steps 3 to 7 are taken from the PasmaREADME.md
)mkdir build
cd build
cmake ../
make
- Copy Pasmo to
/usr/local/bin/
so that you can run it from anywhere:sudo cp pasmo /usr/local/bin/
- Relaunch the shell:
exec zsh -l
- Verify that you can run Pasmo by viewing the manual page:
pasmo man
Assemble our program and create the .tap
file permalink
- Before we run the assembly process, add the Pasmo directive
END $8000
at the end of yourhelloworld.asm
file. This will prompt Pasmo to include aRANDOMIZE USR
statement in the BASIC loader program. This will ensure our machine code runs when we launch the.tap
file - 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:
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 ourhelloworld.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
- Assembly on the ZX Spectrum - Part 1, Benjamin Blundel
- Original source of this ‘Hello, world’ code may have been DIDIx13/hello-world
- Spectrum Machine Language for the Absolute Beginner, ed. William Tang. 1982 (2020 reprint)
- Jonathan Cauldwell, How to Write ZX Spectrum Games, v1.1 (?2020): ‘Chapter Eighteen - Making Games Load and Run Automatically’
- Spectrum memory map, Dean Belfield
- Pasmo docs
- ChatGPT (GPT-4)