Spectrum Machine Code
Notes and examples of machine code on the ZX Spectrum
Table of Contents
This covers machine code for the ZX Spectrum including examples.
1 - Tape Loaders
Notes on various program loaders
1.1 - TAP
Example code to load a machine code program in TAP format
A .TAP
file is a simple image format used to represent data saved to tape.
It's a simple image format which can be generated by some assemblers like
zasm
.
Now we have a simple BASIC loader which generates a .TAP
file.
This is effectively a simple BASIC program which when run loads the next file on the tape into memory at address
24000 0x5DC0 and then executes the machine code at that same address.
Hello World Example
For this example we will write a simple machine code program which writes Hello World to the screen.
1wget https://area51.dev/sinclair/asm/loaders/tap/loader.tap
2wget https://area51.dev/sinclair/asm/loaders/tap/helloworld.z80
3zasm helloworld.z80
4cat loader.tap helloworld.tap > tape.tap
The commands do the following:
-
Download loader.tap which is the precompiled loader.
The source is viewable here along with how to compile it yourself.
-
Download
helloworld.z80
which is the source shown below.
-
Compiles the source generating
helloworld.tap
.
-
Concatenates both tap files to generate our final
tape.tap
file.
Links to the required files are available in the Resources panel at the top right of this page including
a precompiled helloworld.tap
file.
Running the example
If no errors occurred you can run it with the fuse
emulator:
You should then see something like this screenshot.
The first line visible is from the boot loader as it loaded the file in helloworld.tap
.
The second line is the output of the source below.
helloworld.z80 source
; ***************************************************************************
; Hello world example showing how to use the TAP format and our simple
; BASIC boot loader that has been recompiled into TAP format.
;
; Author: Peter Mount, Area51.dev & Contributors
; URL: https://area51.dev/sinclair/asm/loaders/tap/
; ***************************************************************************
; for zasm we need to tell it to generate a tap file
#target tap
; code_start will be where our code will be compiled to
code_start equ 24000
; ***************************************************************************
; Header block containing the block name and the size of the machine code
; ***************************************************************************
#code CODE_HEADER,0,17,0
defb 3 ; Indicates binary data
defb "helloworld" ; the block name, 10 bytes long
defw code_end-code_start ; length of data block which follows
defw code_start ; default location for the data
defw 0 ; unused
; ***************************************************************************
; Data block containing a actual machine code program:
;
; Here we simply print the "Hello World!" message to the screen.
; ***************************************************************************
#code CODE_DATA, code_start,*,0xff
; This is the code_start address 24000 0x5DC0
ld a,2 ; set print channel to Screen:
call 0x1601
ld hl,msg ; message start address
loop: ld a,(hl) ; get next byte
and a ; check for null
ret z ; stop when we get a null
inc hl ; move to next character
rst 2 ; print the character
jr loop ; jump back to the loop
msg: dm 13, "Hello World!", 13, 0
; End of code marker needed for the CODE_HEADER
code_end:
; Anything after this point will not be included in the .tap file
1.1.1 - BASIC Loader
BASIC .TAP loader
This shows the source for the BASIC .TAP loader for use with the zasm
assembler.
To compile run the following commands in a Linux shell to download the sources and compile the loader:
1wget https://area51.dev/sinclair/spectrum/reference/include/zasm/headers.z80
2wget https://area51.dev/sinclair/asm/loaders/tap/loader.z80
3zasm loader.z80
The commands do the following:
-
Download headers.z80 which contains the
definitions for the Spectrum BASIC tokens we require.
-
Download
loader.z80
which is the source shown below.
-
Compiles our loader generating
loader.tap
.
The generated loader.tap
can now be used with your own .TAP
file as long as it loads at
address 24000 and its entry point is also at that same address.
All you need to do is concatenate both .TAP
files with loader.tap
first.
The Hello World example shows how this is done.
Links to the required files are shown in the resources panel at the top right of this page.
; ***************************************************************************
; Load a machine code program using the TAP format
;
; Author: Peter Mount, Area51.dev & Contributors
; URL: https://area51.dev/sinclair/asm/loaders/tap/
; ***************************************************************************
; fill byte is 0x00
; #code has an additional argument: the sync byte for the block.
; The assembler calculates and appends checksum byte to each segment.
;
; Note: If a segment is appended without an explicit address,
; then the sync byte and the checksum byte of the preceding segment are not
; counted when calculating the start address of this segment.
#target tap
; Include Spectrum headers from http://localhost:1313/sinclair/spectrum/reference/include/zasm/headers/
#include "headers.z80"
; ***************************************************************************
; Header block containing the block name and the size of the BASIC program
; ***************************************************************************
#code PROG_HEADER,0,17,0
defb 0 ; Indicates a Basic program
defb "mloader " ; the block name, 10 bytes long
defw variables_end-0 ; length of block = length of basic program plus variables
defw 10 ; line number for auto-start, 0x8000 if none
defw program_end-0 ; length of the basic program without variables
; ***************************************************************************
; Data block containing a simple BASIC program:
;
; 10 CLEAR 23999 ; Set end of Basic memory
; 20 LOAD "" CODE 24000 ; Load next file to the free memory
; 30 RANDOMIZE USR 24000 ; Execute the loaded code
;
; This will when run mark memory from 24000 (0x5DC0) to be unavailable to
; BASIC and everything above that point is then usable by the machine code
; program who's entry point is address 24000 (0x5DC0).
; ***************************************************************************
#code PROG_DATA,0,*,0xff
; 10 CLEAR 23999
defb 0,10 ; line number
defb end10-($+1) ; line length
defb 0 ; statement number
defb BAS_CLEAR ; token CLEAR
defm "23999",$0e0000bf5d00 ; number 23999, ascii & internal format
end10: defb $0d ; line end marker
; 20 LOAD "" CODE 24000
defb 0,20 ; line number
defb end20-($+1) ; line length
defb 0 ; statement number
defb BAS_LOAD,'"','"',BAS_CODE ; token LOAD, 2 quotes, token CODE
defm "24000",$0e0000c05d00 ; number 24000, ascii & internal format
end20: defb $0d ; line end marker
; 30 RANDOMIZE USR 24000
defb 0,30 ; line number
defb end30-($+1) ; line length
defb 0 ; statement number
defb BAS_RANDOMIZE, BAS_USR ; token RANDOMIZE, token USR
defm "24000",$0e0000c05d00 ; number 24000, ascii & internal format
end30: defb $0d ; line end marker
program_end:
; ZX Spectrum Basic variables
variables_end:
2 - Managing the Screen
How to manipulate the Spectrum’s screen
2.1 - Clear the Screen
Example code to clear the Spectrum’s screen
This code snippet will clear the standard screen on all versions of the Spectrum.
It's broken down into three sections:
- Clear's the pixel data
- Reset the screen's colour data, in this instance to White text on Black Background
- Sets the screen border to black
; ***************************************************************************
; Clear the Spectrum Screen
;
; Example 1
;
; Author: Peter Mount, Area51.dev & Contributors
; URL: https://area51.dev/sinclair/asm/screen/getcharaddr/
; ***************************************************************************
;
; Clear the Spectrum screen, setting the border to black and white text on
; a black background
;
; On Exit:
; A Undefined
; BC Undefined
; DE Undefined
; HL Undefined
;
clearScreen:
; Clear the pixel data
ld hl,DISPLAYFILE ; Start of screen memory
ld (hl),&00 ; Set first byte to 0
ld de,DISPLAYFILE+1 ; Destination as next byte
ld bc, 6144 ; Number of bytes to copy
ldir ; Copy from hl to de bc times
; Clear the colour attributes
ld (hl),&07 ; White text black Background
ld bc, 767 ; Number of bytes to copy, attr size -1
ldir ; Clear the attributes
ld a, 0 ; Set border to black
ld (BORDCR),a ; set OS copy of colour
out (&fe),a ; set hardware colour
ret
Important note here:
We copy 6144 bytes in the first LDIR
not 6143 which we would normally do if we want to erase just the
DISPLAYFILE
.
Using 6144 bytes will cause the first byte of DISPLAYATTR
to be set to 0 which is fine here as we want
HL
and DE
to be pointing to DISPLAYATTR
ready for the second
LDIR
operation.
This could, in theory cause the top left character to flicker briefly as it's been set to 0
(BLACK foreground and background).
If that's an issue then set the BC
to 6143 and increment HL
and DE
after the
LDIR
as in the next example:
; ***************************************************************************
; Clear the Spectrum Screen
;
; Example 2 - does not corrupt the top left characters colour attributes
;
; Author: Peter Mount, Area51.dev & Contributors
; URL: https://area51.dev/sinclair/asm/screen/getcharaddr/
; ***************************************************************************
;
; Clear the Spectrum screen, setting the border to black and white text on
; a black background
;
; On Exit:
; A Undefined
; BC Undefined
; DE Undefined
; HL Undefined
;
clearScreen:
; Clear the pixel data
ld hl,DISPLAYFILE ; Start of screen memory
ld (hl),&00 ; Set first byte to 0
ld de,DISPLAYFILE+1 ; Destination as next byte
ld bc, 6143 ; Number of bytes to copy
ldir ; Copy from hl to de bc times
inc hl ; Move HL and DE forward 1 byte
inc de ; so they point to DISPLAYATTR
; Clear the colour attributes
ld (hl),&07 ; White text black Background
ld bc, 767 ; Number of bytes to copy, attr size -1
ldir ; Clear the attributes
ld a, 0 ; Set border to black
ld (BORDCR),a ; set OS copy of colour
out (&fe),a ; set hardware colour
ret
There are faster but longer methods of clearing the screen which I won't go into here.
For most purposes the first example above will work for most purposes other than time critical games.
2.2 - Get address of character on screen
Calculate the true address on screen of a character
Calculating the start address for a character might sound complicated due to its weird memory layout, but it is
actually pretty simple.
The way the address is calculated is described with the Display File,
however the following code allows you to calculate the address in Z80 machine code.
A link to download this routine is available under Resources at the top right of the page:
; ***************************************************************************
; Calculates the actual screen address for a character in BC (row,col)
; returning that address in HL.
;
; Author: Peter Mount, Area51.dev & Contributors
; URL: https://area51.dev/sinclair/asm/screen/getcharaddr/
; ***************************************************************************
;
; Although the memory layout for the spectrum screen seems weird,
; it's actually pretty logical. You can tell this weirdness is down to how
; the ULA works internally with the way the addressing is mapped, as shown
; at https://area51.dev/sinclair/spectrum/screen/file/
;
; Address format:
; H L
; 010rrnnn rrrccccc where r=row,
; c=column,
; n=row in the character definition.
;
; To calculate the address of a character:
; High byte = &40 or (row and &18)
; Low byte = (row<<5) or column
;
; That would then be the top byte of the character.
;
; To get the next byte in the character just increment the high byte
; (e.g. n=1) & so on for the next 7 bytes.
;
; To get the next character's address then simply increment the low byte.
; Be careful however when wrapping around the end of the line as, after
; the first 8 lines the address will fail.
;
; Ideally you should calculate the address again at the start of each line.
;
; This might explain why the original spectrum editor was 8 lines long, as you
; only needed to set the high byte to the 8-line block in memory and set the
; low byte to the offset in the line being edited to get that characters
; screen address.
;
; On Entry:
; BC b=row, c=col in Spectrum 32x24 characters
;
; On Exit:
; HL address of top row if character definition in screen
; BC unchanged
; A undefined
;
getSpectrumCharPos: ; Get Spectrum char pos (bc) into HL for physical screen address
ld a, b ; first calculate high byte - get row
and &18 ; Mask bits 4 & 5 from row
or &40 ; set bit 6, this gives us the upper memory address
ld h, a ; Set H
ld a, b ; get row
add a ; Shift left 5 to form low address
add a ; use add not srl as this saves 1 byte and half the t-states
add a ; per shift, especially as we don't need to worry about carry flag
add a
add a ; HA is now the address of the start of line
or c ; Add column value
ld l, a ; HL now address of top row of character
ret
Note: This code is actually from my Teletext Mode 7 emulator for the Spectrum
2.3 - Set the border colour
Example code to set the border colour on the Spectrum’s screen
The border colour is managed by the ULA.
Changing the border colour is simply writing the required colour to the ULA whilst
storing a copy in the BORDCR
system variable at 0x5C48 so that the OS also knows what the colour is.
In this example we set the Border to black
Set the Spectrum Screen Border:
setBorder
LDA,0x00
LD(BORDCR),A
OUT(&FE),A
RET
3 - Determine Machine Type
Detect which model of Spectrum the code is running on
There is no official method of determining which model of Spectrum code is running on, however one method is to
look for differences in the ROM on the machine.
There were six official models of Spectrum released:
- 48K with a single rom (16K had the same ROM)
- 128K with 2 roms
- Spanish 128K released 1985
- +2 with a built-in Cassette Deck
-
+2A and +3 - These have the same roms just differ with the +3 having a built-in 3" floppy drive
instead of the cassette deck.
Below is a routine which will return a value representing the machine type the code is running on in the Accumulator.
It works by first paging out the 48K rom - something that will do nothing on a real 48K machine,
before checking for the Copyright string present in the 48K rom.
If that is not present it then attempts to page out the rom on a +2A/+3 machine - as they have 4 ROM's,
then checking for strings present on the +2A/+3, then the +2 and finally the Spanish 128K machine.
If it got this far without a match then it's running on an original UK 128K machine.
Rom selection
There's two additional routines in here, romSel1
and romSel2
,
which handle the ROM selection and could be reused.
They both take the new value of the port which manages the rom selection in the Accumulator and updates both the
appropriate port and system variable.
For romSel1 load the Accumulator from 0x5BFC and make your changes before calling this routine.
For romSel2 load the Accumulator from 0x5B67 and make your changes before calling this routine.
machinetype.z80 source
; ***************************************************************************
; Determine which model of ZX Spectrum the code is running on.
;
; Author: Peter Mount, Area51.dev & Contributors
; URL: https://area51.dev/sinclair/asm/machinetype/
; ***************************************************************************
; The model's to detect
machine_48 = 0 ; Spectrum 48k
machine_128 = 1 ; Spectrum 128K UK
machine_128es = 2 ; Spectrum 128K Spanish
machine_plus2 = 3 ; Spectrum +2
machine_plus3 = 4 ; Spectrum +2A/+3
;
; detectMachine Detect the type of machine running this code
;
; Exit:
; A Model type
; Z Set for a 16K/48K machine, Reset for any 128K machine
;
detectMachine:
PUSH BC ; Save registers as BC is needed for ROM selection
PUSH DE ; and D for passing the result back
CALL detectMachine1 ; Call detection routine
LD A, D ; Copy result into A
POP DE ; Restore registers
POP BC
AND A ; CP 0 so Z is true for a 48K and NZ for any 128K machine
RET
detectMachine1:
LD A, (0x5B5C) ; Switch rom
PUSH AF ; Save current rom
AND &EF ; to ensure non-48k rom is selected, bit 4 is 0
CALL romSel1 ; change rom1, will do nothing on a 48K
CALL detectMachine2 ; run tests
POP AF ; Restore rom
; romSel1 Update rom selection port on 128K and later machines
;
; Entry:
; A New value of port, based on current value in 0x5BFC
;
; Exit:
; A undefined
; BC undefined
;
romSel1: LD BC, 0x7FFD ; Select rom on 128K and later
DI ; Switch the port between DI/EI
LD (0x5B5C), A
OUT (C), A
EI
RET
detectMachine2:
LD D, machine_48 ; Test for a 48K
LD A, (0x153B) ; Address of 1 in (C)1982 in spectrum 48k rom
CP '1'
RET Z
LD A, (0x5B67) ; Switch rom on +3
PUSH AF ; Save current rom
AND &FB ; This time bit 2 is 0 on second port, does nothing on 128/+2
CALL romSel2
CALL detectMachine3 ; run tests
POP AF ; restoring old rom
; romSel2 Update rom selection port on +2A and +3 machines
;
; Entry:
; A New value of port, based on current value in 0x5B67
;
; Exit:
; A undefined
; BC undefined
;
romSel2: LD BC, 0x1FFD ; Select rom on +2A/+3
DI ; Switch the port between DI/EI
LD (0x5B67), A
OUT (C), A
EI
RET
detectMachine3:
LD D, machine_plus3 ; Test for +3
LD A, (0x168e) ; Address of "SPECTRUM" in +3 rom 0
CP 'S'
RET Z
LD D, machine_plus2 ; Test for +2
LD A, (0x0562) ; See if we are a +2
CP '1' ; This is 1 in "(C)1982 Amstrad"
RET Z
LD D, machine_128es ; See if Spanish 128K
LD A, (0x0508)
CP '1' ; This is 1 in "(C) 1985 Sinclair"
RET Z
LD D, machine_128 ; We must be a 128K if we got here
RET