ZX Spectrum Assembler Cheats

ZX Spectrum Assembler Cheats

I'm one of the oldies who remembers the ZX Spectrum in its first guise back in 1982. Back then my knees worked but Google didn't. So trying to get information on the rubber keyed black box of joy was difficult. Particularly when it came to Assembler language.

Now Google and its errant child You Tube can solve answers the 10 year old me could only fathom at. And while it might only be Grandad programmers like me with an interest I thought it wise to share some stuff I have discovered. A shortcut to ZX80 Assembler on the ZX Spectrum from someone who really didn't get it first time around, is still a bit unsure and quickly gets distracted while watching You Tube videos if the curtain rustles in the room.

So let's start with a chunk of code I've quickly started copying and pasting into new Z80 Assembler files. This is from a You Tube channel called Spriteworx and it's no judgement on them that my curtains rustle a lot and distract me. They do a great job. The code provides some very quick and easy shortcuts to coding Spectrum Assembler. It is, as the title suggest, some cheats.

Here is the code and then I'll do a breakdown.

;START OF PROGRAM
org $8000             ;$8000 is 32768 in Decimal
MAIN

;MAIN LOOP

call INIT

ld a,2
call OUTCHAN

ld de, str1
ld bc, ENDstr1-str1            
call PRINTSTR

ret

;END OF MAIN LOOP

;SUBROUTINES

INIT
;Lets set the paper to Blue
;Then clear the screen
ld  hl, ATTR_P
ld (hl),blu*8
call CLS

;Set the border
ld a, blk
call BORDER

ret                    ;Return from INIT


;************ Strings *************

str1
    defb _at,10,4,ink,yel
    defb "This is a",cr
    defb ink,grn,"string of text"
ENDstr1    equ $

;************ ROM ROUTINES ************

PRINTSTR EQU 8252                   ;ROM routine to print string.
OUTCHAN EQU 5633                    ;ROM routine to activate output channel.
CLS EQU 3503                        ;ROM routine to clear entire screen using attributes in ATTR-P (23693)
BORDER EQU 8859                     ;ROM routine to set border color.

;********** SYSTEM VARIABLES *********

ATTR_P EQU 23693                    ;ATTR-P Sysvar (Attributes)

;******** CONTROL CODES ************

ink equ 16                          ;INK control code.
paper equ 17                        ;PAPER control code.
flash equ 18                        ;FLASH attribute.
bright equ 19                       ;BRIGHT attribute.
cr equ 13                           ;Carriage return.
_at equ 22                          ;AT control code.
tab equ 23                          ;TAB
blk equ 0                           ;Blk.
blu equ 1                           ;Blue.
red equ 2                           ;Red.
mag equ 3                           ;Magenta.
grn equ 4                           ;Green.
cyn equ 5                           ;Cyan.
yel equ 6                           ;Yellow.
wht equ 7                           ;White.

;******** End ***********

The start is a catch all really for whatever assembler program you are using. ORG $8000 is a standard but some assemblers might look for the word MAIN to know where to start your program.

ORG is the origin and the dollar sign indicates this is a hex number. Therefore it's saying start writing the code to memory location Hex 8000 which is 32768 in binary.

;START OF PROGRAM
org $8000             ;$8000 is 32768 in Decimal
MAIN

I remember as a little kid seeing 32768 a lot. It turns out this is start fast access memory for the Spectrum 48K. Hence why lots of programs start there. Also Hex 8000 is quite easy to remember.

If you're new to ZX Spectrum Assembler then you would call this from a BASIC program which looks something like this.

10 RANDOMISE USR 32768
20 PAUSE 0

The RANDOMISE USR 32768 says which memory location it should start running and the PAUSE 0 simply tells it to hold there and wait once the Assembler Program has returned to BASIC. So basically "Run this memory location and when you've finished wait."

We will now, for clarity, jump to the bottom of the code.

;************ ROM ROUTINES ************

PRINTSTR EQU 8252                   ;ROM routine to print string.
OUTCHAN EQU 5633                    ;ROM routine to activate output channel.
CLS EQU 3503                        ;ROM routine to clear entire screen using       attributes in ATTR-P (23693)
BORDER EQU 8859                     ;ROM routine to set border color.

;********** SYSTEM VARIABLES *********

ATTR_P EQU 23693                    ;ATTR-P Sysvar (Attributes)

;******** CONTROL CODES ************

ink equ 16                          ;INK control code.
paper equ 17                        ;PAPER control code.
flash equ 18                        ;FLASH attribute.
bright equ 19                       ;BRIGHT attribute.
cr equ 13                           ;Carriage return.
_at equ 22                          ;AT control code.
tab equ 23                          ;TAB
blk equ 0                           ;Blk.
blu equ 1                           ;Blue.
red equ 2                           ;Red.
mag equ 3                           ;Magenta.
grn equ 4                           ;Green.
cyn equ 5                           ;Cyan.
yel equ 6                           ;Yellow.
wht equ 7                           ;White.

;******** End ***********

This is simply setting a series of variables we can then use in our code. For instance if you wanted to clear the screen you would use the command.

CALL 3503

This is because in memory location 3503 is a small ROM routine which clears the screen. Now we could remember the number 3503 or we could set CLS to equal (EQU) 3503. Hey presto, copy and paste the code above and now we can just say

CALL CLS

And not have to remember 3503.

When you clear the screen using that ROM code then it looks to memory address 23693 and whatever the paper and ink colours are there it clears the screen to match. Again, rather than remember 23693 this sets the variable ATTR-P to EQU 23693. We then do more settings with PRINTSTR, OUTCHAN and BORDER which we will look at in a bit.

Where this is even more useful is the Control Codes. When you print a string using the PRINTSTR (8252) call it requires certain codes but rather than writing:

str1
    defb 22,10,4,16,6
    defb "This is a",13
    defb 16,4,"string of text"
ENDstr1    equ $

We can write:

str1
    defb _at,10,4,ink,yel
    defb "This is a",cr
    defb ink,grn,"string of text"
ENDstr1    equ $

Not only are things easier to remember but much easier to debug. And none of this will have any effect on the size of the final code because these variables are only used by the assembler and us.

This is why you'll see there is never a CALL to this section of the code and we can easily stick it at the end out of the way. It's just used by the assembler, once it's compiled and put into memory it's not needed.

Now we can return to the top of the code. The first command in the main loop is CALL INIT

The INIT subroutine lies in the subroutine section of the code and looks like this:

INIT
;Lets set the paper to Blue
;Then clear the screen
ld  hl, ATTR_P
ld (hl),blu*8
call CLS

;Set the border
ld a, blk
call BORDER

ret                    ;Return from INIT

This loads the assembler register HL with ATTR_P (Or 23693) then it puts the value of blu multiplied by 8 into that memory address. It then calls CLS or 3503. What is going on here?

Remember we said above whatever was in ATTR_P would be used for the paper and ink settings when you call CLS or 3503? That is what this is doing but why is it multiplying by 8?

Let's look at what we would do if we wanted Blue paper and yellow ink:

ld  hl, ATTR_P
ld (hl),blu*8+yel
call CLS

The paper and ink are all stored in a single byte of code. Without getting into binary addition and subtraction is easy to remember if you want to change the attribute paper colour multiple by 8. If you want to change the ink, you don't have to multiply. If you don't specify the ink it will default to black.

;Set the border
ld a, blk
call BORDER

This should all start making sense now. We load the a register with the colour black and then call the ROM routine BORDER (8859) The way that routine at 8859 works is it will set the border to whatever number is in the a register.

RET then returns us from the INIT subroutine and back to the main loop.

ld a,2
call OUTCHAN

We now want to print a string. You need to know the ZX Spectrum screen is divided into two sections. The first section is at the bottom and is only two lines high. The second section is at the top and fills the rest of the screen. We have to tell the ZX Spectrum we want to use the second section.

We load the a register with 2 and then CALL OUTCHAN (5633). 5633 takes the number in the a register and makes that the output channel.

ld de, str1
ld bc, ENDstr1-str1            
call PRINTSTR

Now we're in the right part of the screen we need to print the string. CALL PRINTSTR calls a ROM routine which prints what is at memory location stored in DE and the it prints the amount of characters specified in BC

That's what the first two lines do. They set those two registers to be the correct memory locations. Set DE to be the start of the string. Then take whatever the start is away from whatever the end is to find the length. How does it know where these strings start and stop? With this bit of code:

;************ Strings *************

str1
    defb _at,10,4,ink,yel
    defb "This is a",cr
    defb ink,grn,"string of text"
ENDstr1    equ $

STR1 is a label just for the assembler. As is ENDSTR1. The EQU $ is again just a thing to tell the assembler where the end of this string is. DEFB stands for DEFINE BYTES. Essentially "make these bytes these characters"

By working like this you can just have str1 and endstr1 and put whatever you like in between. If the string grows then the numbers all move around with it. You don't have to do anything yourself.


A Quick Note on Using Multiple Strings

If your code uses multiple strings you will need to give them different names for the assembler:

;************ Strings *************

str1
    defb _at,10,4,ink,yel
    defb "This is a",cr
    defb ink,grn,"string of text"
ENDstr1    equ $

str2
    defb _at,11,4,ink,white
    defb "This is the second string."
ENDstr2 equ $

You can then call them like this:

ld de, str1
ld bc, ENDstr1-str1            
call PRINTSTR

ld de, str2
ld bc, ENDstr2-str2            
call PRINTSTR

The final command then is another RET which is Return from the main loop. As the main loop was called from a BASIC program then it will return to BASIC.

Closing

Hopefully you've found that breakdown helpful. I place it here as much for my benefit as anyone else's but if you do find helpful let me know. And again I want to thank Sparkworx for their excellent resource on this. Give them a follow.

Dave Taylor
30th March 2023