ZX Spectrum Assembly, Pong – 0x00 Hello World
Before we start developing our video games, we are going to do what you do when you learn a programming language, we are going to develop a «Hello World».
Translation by Felipe Monge Corbalán
Tabla de contenidos
- Hello World
- What is the Z80?
- Z80 registers
- ZX Spectrum Memory
- Decimal, binary, hexadecimal
- Labels, variables and constants
- ORG and END
- Loading instructions
- RST Instructions
- Increases and decreases
- Logical operations
- Programme flow changes
- Subroutines
- Ports of entry and exit
- ZX Spectrum Assembly, Pong – Hello World
- Useful links
Hello World
The creation of our Hello World will help us to acquire the minimum knowledge we will need to develop our programmes.
With «Hello World» we will discover:
- The main features of the Zilog Z80 microprocessor and its registers.
- The memory layout of the ZX Spectrum.
- Numbers in different notations.
- Labels, variables and constants in assembly.
- ORG and END directives.
- Loading instructions.
- RST instructions.
- Increments and decrements.
- Logical operations.
- Programme flow changes.
- Subroutines.
- Entry and exit ports.
You can download this spreadsheet with the Z80 instructions, cycles, bytes, flag assignments, etc.
What is the Z80?
The Z80 is a microprocessor released by Zilog in 1976. It is the microprocessor used in all models of the ZX Spectrum.
The Z80 is a CPU of the «Little Endian» type. A CPU of this type, when storing 16-bit values in memory, stores the least significant byte in the first position and the most significant byte in the next; if the value $CCFF is loaded into memory position $8000, it stores the value $FF in position $8000 and the value $CC in position $8001.
Another feature of the Z80 that we will sometimes not like is that it is not orthogonal, which means that not all operations between registers are allowed.
Z80 registers
The registers are high-speed, low-capacity memory and are integrated into the microprocessor.
The Z80 has 8-bit and 16-bit registers.
8-bit registers
- A: accumulator. It is the destination of 8-bit arithmetic, logic and comparison operations. A is the high byte of the 16-bit AF register.
- F: flags (indicators). A set of flags that provide information on the operations being performed. F is the low byte of the 16-bit AF register.
- B: general purpose register often used in loops; it is used by DJNZ as a counter. B is the high byte of the 16-bit BC register.
- C: general purpose register. C is the low byte of the 16-bit BC register.
- D: general purpose register. D is the high byte of the 16-bit DE register.
- E: general purpose register. E is the low byte of the 16-bit DE register.
- H: general purpose register. H is the high byte of the 16-bit HL register.
- L: general purpose register. L is the low byte of the 16-bit HL register.
- I: Interrupt register. Allows 128 different interrupts to be handled.
- R: Memory refresh register. Handled by the Z80, only bits 0 to 6 change. Can be used to generate semi-random numbers between 0 and 127.
Alternative registers
The alternate registers are used to make a temporary copy of the 8-bit registers:
- A’: alternative register of A.
- F’: alternative register of F.
- B’: alternative register of B.
- C’: alternative register of C.
- D’: alternative register of D.
- E’: alternative register of E.
- H’: alternative registration of H.
- L’: alternative registration of L.
16-bit registers
- AF: consisting of register A as the high byte and F as the low byte.
- BC: is formed by register B as the high byte and C as the low byte. It is generally used as a counter in operations such as LDIR, LDDR, etc.
- DE: consisting of register D as the high byte and E as the low byte. It is generally used for reading and writing in a single operation, and as a target in LDIR, LDDR, etc. operations.
- HL: formed by the H register as the high byte and L as the low byte. It is generally used for reading and writing in a single operation, as well as for source operations such as LDIR, LDDR, etc. The HL register acts as an accumulator in 16-bit operations.
- IX: indexed memory access, LD (IX + N), where N can be a value between -128 and 127.
- IY: indexed memory access, LD (IY + N), where N can be a value between -128 and 127.
- SP: Stack pointer. Points to the current position of the stack header.
- PC: Programme counter. Address of the current instruction to be executed.
Record operation codes (opcodes)
- 0: B
- 1: C
- 2: D
- 3: E
- 4: H
- 5: L
- 6: (HL)
- 7: A
These operation codes are used to calculate the operation code for instructions where the parameter is a register.
LD A, r: 0x78 + rb
LD C, r: 0x48 + rb
Where rb is the operation code of the records to be loaded.
Registration F
Each bit of the F register, flags, has its own meaning which changes automatically according to the result of the operations performed:
- Bit 0: C flag (carry). A one if the result of the previous operation needs an extra bit to represent it (I take one). The carry flag is the extra bit needed.
- Bit 1: Flag N (subtraction). Set to one if the last operation was a subtraction.
- Bit 2: P/V flag (parity/overflow). In operations that change the parity bit, it is set to one when the number of bits to one of the result is even. In operations that change the overflow bit, it is set to one when the result of the operation requires more than eight bits to represent.
- Bit 3: not used.
- Bit 4: H Flag (BCD carry). A one if there is a carry from bit 3 to bit 4 in BCD operations.
- Bit 5: not used.
- Bit 6: Z Flag (zero). A one if the previous operation returns zero. Very useful in loops.
- Bit 7: S flag (sign). A one if the above two’s complement is a negative number.
The F register is not directly accessible, and not all operations affect it.
Record F – indicators/flags | ||||||||
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Flag | S | Z | F5 | H | F3 | P/V | N | C |
ZX Spectrum Memory
The memory is divided into two blocks of 16 KB (16384 bytes) on 16K models and four blocks of 16 KB (16384 bytes) on 48K models:
- First block: from position $0000 to $3FFF (0 to 16383). This block corresponds to the ROM and is read-only.
- Second block: from position $4000 to $7FFF (16384 to 32767). This block contains the screen area, printer buffer, system variables, etc., leaving about 9KB for programs on 16K models.
The following memory blocks are only found in 48K models:
- Third block: from position $8000 to $BFFF (32768 to 49151). This is general purpose RAM.
- Fourth block: from position $C000 to $FFFF (49152 to 65535). This is general purpose RAM.
The distribution of the second block is superficially as follows:
- $4000 – $57FF (16384 to 22527): Screen pixel area. The ZX Spectrum’s screen has a resolution of 256 * 192 pixels. Each byte represents eight pixels (256 * 192 / 8 = 6144 bytes).
- $5800 – $5AFF (22528 to 23295): Colour attribute area of the display. In this case a resolution of 32 * 24 characters is available. Each byte specifies the colour of an 8 * 8 pixel area, where bits 0 to 2 define the ink colour (0 to 7), bits 3 to 5 the background colour (0 to 7), bit 6 the brightness (0 to 1) and bit 7 the flicker (0 to 1). This area occupies a total of 768 bytes (32 * 24).
- $5B00 to $5BFF (23296 to 23551): Printer buffer. 256 bytes that can be used if you don’t have a printer or if it is not used by the program.
- $5C00 – $5CB5 (23552 to 23733): System variables.
- $5CB0 – $5CB1 (23728 to 23729): Unused memory that can be used for data exchange.
- $7FFF: Stack pointer. Normally points to this address and decrements as data is fed into it.
Decimal, binary, hexadecimal
Decimal notation is the way we are used to seeing numbers, as a series of digits with values between 0 and 9. This is also known as base 10 notation.
In computing, it is different because computers work with two values: 0 and 1. These numbers are known as binary, in base 2.
In assembly, the most common way of representing numbers is in base 16 (hexadecimal notation), where each digit represents values from 0 to 15:
- A = 10
- B = 11
- C = 12
- D = 13
- E = 14
- F = 15
A hexadecimal digit represents 4 bits, so we know at a glance how many bits it is made up of. It is common to speak of multiples of 8 (8, 16, 32, 64…).
Without a calculator at hand, converting numbers between the different bases can be very tedious. It is helpful to know the value of each bit; in the case of the Z80, 8-bit and 16-bit numbers.
We will use the following table. It shows the values of each bit to help us with the conversions:
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 |
32728 | 16384 | 8192 | 4096 | 2048 | 1024 | 512 | 256 |
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
As you can see, we only need to add to convert numbers from one base to another, as shown in the example below:
5FA0h 0101 1111 1010 0000 32 + 128 + 256 + 512 + 1024 + 2048 + 4096 + 16384 = 24480
Hexadecimal to binary conversion is straightforward, with four bits making up each digit.
F0h = 1111 0000 3Ah = 0011 1010 CCh = 1100 1100 78h = 0111 1000 0001 0000 = 10h 0100 0101 = 45h 1010 1010 = AAh 0010 0011 = 23h
Labels, variables and constants
Tags allow us to refer to memory locations without having to calculate them. The assembler program is responsible for replacing the labels with the correct memory addresses; this process is done when the object code is created.
If we could not use labels, each time we modified any part of the code, the memory addresses would have to be recalculated for use in each JR, JP or CALL. The assembler replaces the labels with the memory addresses of the following instructions.
Labels are used to define routines and data; in the case of data, it can be numeric or text, constants or variables.
The data is defined by the following instructions:
- EQU: define constants name EQU value
- DB/DEFB: define bytes name DB 1, $FF, %10101010
- DM/DEFM: define message name DEFM «Hello World»
- DW/DEFW: define word name DW $0040
- DS/DEFS: define space name DEFS $08
DB, DEFB, DM, DEFM, DW, DEFW, DS and DEFS are not assembled, so it is recommended to put them at the end of the code, otherwise they will be executed as if they were Z80 instructions. If we start the code with:
DB $CD, $00, $00
ORG and END
ORG and END are two of the most important directives we will use. With ORG we specify the memory address where the code will be loaded. Multiple ORGs can be set so that code can be loaded into different memory addresses.
END indicates where the programme ends, also an autostart address for PASMO.
With what we have seen so far, we can now develop the first programme; open the text editor to write these lines:
org $8000
ret
end $8000
We save the file as «helloworld.asm» and compile with PASMO:
pasmo --name HelloWorld --tapbas helloworld.asm helloworld.tap --log
This command (pasmo…) will always be used to compile our programs.
Now we can open the holamundo.tap file with a ZX Spectrum emulator and see that it runs, even though it just exits and we haven’t broken anything.
Loading instructions
We use these instructions to load literal values into a register, the value of a register into another register, a value into memory, the value of a register into memory and a memory value into a register.
The syntax of the load instructions is as follows:
LD destination, origin
The destination can be a register or a memory location, the source can be a register, a memory location or an 8-bit or 16-bit value.
These instructions do not affect the F register, except for LD A, I and LD A, R.
This is the moment to return to our first programme, where we will add the following lines just below ORG:
ld hl, $4000
ld (hl), $ff
With these lines we activate the 8 bits of the first memory address of the screen, hereafter called VideoRAM. Compile with PASMO and load into the emulator:
pasmo --name HelloWorld --tapbas helloworld.asm helloworld.tap --log
RST Instructions
These instructions are used to jump to a specific address with a single opcode instruction.
There are several RST instructions, but we will only use RST $10 (RST 16), which prints the ASCII corresponding to the value in register A.
We get the file helloworld.asm, remove the two lines we added and write the following:
ld a, 'H'
rst $10
We compile and load it into the emulator. The letter H appears on the screen.
Increases and decreases
They are used to increment (INC) or decrement (DEC) the contents of certain registers or memory locations (pointed to by the HL, IX or IY registers) by one unit. The permitted operations are:
INC r DEC r
INC rr DEC rr
INC (HL) DEC (HL)
INC (IX + n) DEC (IX + n)
INC (IY + n) DEC (IY + n)
These operations, when performed on 16-bit registers, do not affect the F register, but when performed on 8-bit registers, they affect it in different ways:
Flags | ||||||
Instruction | S | Z | H | P/V | N | C |
INC r | * | * | * | V | 0 | – |
INC (HL) | * | * | * | V | 0 | – |
INC (ri + n) | * | * | * | V | 0 | – |
INC rr | – | – | – | – | – | – |
DEC r | * | * | * | V | 1 | – |
DEC (HL) | * | * | * | V | 1 | – |
DEC (ri + n) | * | * | * | V | 1 | – |
DEC rr | – | – | – | – | – | – |
ZX Spectrum Assembly, Pong
We edit the helloworld.asm file and leave it as it is:
org $8000 ; Address where you load the programme
ld hl, msg ; HL = message memory address
ld a, (hl) ; A = first character
rst $10 ; Prints the character
inc hl ; HL = address next character
ld a, (hl) ; A = next character
rst $10 ; Prints the character
ret
msg: defm 'Hello ZX Spectrum Assembly'
end $8000
Compile and load into the emulator. Now we see «He» on the screen.
Logical operations
Logical operations are performed at bit level by comparing two bits. There are three kinds of logical operations:
- AND: Logical multiplication. The result is only one if both bits are set to one.
- OR: Logical addition. If either bit is one, the result is one, otherwise the result is zero.
- XOR: Exclusive OR. If both bits are equal, the result is zero, otherwise the result is one.
The following table shows the possible results of the logic operations:
Bit 1 | Bit 2 | AND | OR | XOR |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
0 | 1 | 0 | 1 | 1 |
0 | 0 | 0 | 0 | 0 |
The format of the logical operations is as follows:
AND origin
OR origin
XOR origin
For logical operations, the source can be any of the 8-bit registers (except F), a value, a memory location pointed to by (HL) or by the index registers, (IX + n) or (IY + n). The target is always register A; logical operations are performed on the value contained in register A and the result is left in the same register.
Logical operations affect register F in this way:
Flags | ||||||
Instruction | S | Z | H | P/V | N | C |
AND s | * | * | * | P | 0 | 0 |
OR s | * | * | * | P | 0 | 0 |
XOR s | * | * | * | P | 0 | 0 |
ZX Spectrum Assembly, Pong
Programme flow changes
They modify the programme flow (jumps), with or without conditions, absolute (JP) or relative (JR). These instructions do not affect the F register.
Absolute jumps are possible:
- JP nn: Jumps to memory address nn, which can be a label (in the following cases as well).
- JP (HL): Jumps to the memory address of the value held by HL; to the value of HL (16 bits), not to the value of the address pointed to by HL (8 bits).
- JP (index register): jumps to the memory address of the value that has IX or IY.
- JP NZ, nn: Jumps to address nn, if the Z flag is zero; the result of the last operation is not zero.
- JP Z, nn: Jumps to memory address nn if the Z flag is set to one; the result of the last operation is zero.
- JP NC, nn: Jumps to memory address nn if the C flag is set to zero; no carry.
- JP C, nn: Jumps to memory address nn if the C flag is set to one; there is carry.
- JP PO, nn: Jumps to memory address nn if the P/V flag is zero; no parity/overflow.
- JP PE, nn: Jumps to memory address nn if the P/V flag is set to one; there is parity/overflow.
- JP P, nn: Jumps to memory address nn if the S flag is zero; the result of the last operation is positive.
- JP M, nn: Jumps to memory address nn if the S flag is set to one; the result of the last operation is negative.
Relative jumps are relative to the current instruction and jump a number of bytes from -128 to 127. Routines with relative jumps are relocatable as they do not affect the memory location in which they are loaded.
Relative jumps can be:
- JR n: Jumps to the memory address that is n bytes away; n can be a label (in the following cases also).
- JR NZ, n: Jumps to the memory address that is n bytes away if the Z flag is zero; the result of the last operation is non-zero.
- JR Z, n: Jumps to the memory address that is n bytes away if the Z flag is set to one; the result of the last operation is zero.
- JR NC, n: Jumps to the memory address that is n bytes away if the C flag is set to zero; no carry.
- JR C, n: Jumps to the memory address that is n bytes away if the C flag is set to one; there is a carry.
We open the file helloworld.asm; we use logical operations and flow changes to print the whole message:
org $8000 ; Address where the programme is loaded
ld hl, msg ; HL = message memory address
Loop:
ld a, (hl) ; Loads a character from the string
or a ; ¿A = 0? A or A = 0 only if A = 0
jr z, Exit ; If A = 0, jump to the Exit label.
rst $10 ; Paints the character
inc hl ; HL = next character
jr Loop ; Returns to the beginning of the loop
Exit:
ret ; Exits the programme
msg: defm 'Hello ZX Spectrum Assembly', $00
; String ending in 0 = null
end $8000
We compile with PASMO, load in the emulator and see the results:
Subroutines
Subroutines are blocks of code that perform a specific action and can be called multiple times; CALL is used to jump to a subroutine and RET is used to exit and return to where it was called from.
CALL is similar to JP, but before jumping it does a PC PUSH to store where the programme is going. When RET is finished, PC POP is finished and the program returns to where it was.
PUSH and POP put and remove values on the stack. The values are always 16-bit registers.
Conditional CALLs and RETs are possible, as seen with JP and JR:
CALL nn RET
CALL NZ, nn RET NZ
CALL Z, nn RET Z
CALL NC, nn RET NC
CALL C, nn RET C
CALL PO, nn RET PO
CALL PE, nn RET PE
CALL P, nn RET P
CALL M, nn RET M
We edit helloworld.asm and call ROM routines with CALL; we want the results to be more attractive:
org $8000 ; Address where the programme is loaded
; System variable: permanent display attributes.
; Format: FLASH, BRIGHT, PAPER, INK (FBPPPIII).
ATTR_S: equ $5c8d
; System variable: current attribute (FBPPPIII).
ATTR_T: equ $5c8f
;--------------------------------------------------------------------
; ROM Routine similar to Basic AT
; Position the cursor at the specified coordinates.
; Input: B -> Y-coordinate.
; C -> X-coordinate.
; In this routine, the top left-hand corner of the screen
; it is (24, 33).
; Alters the value of the A, DE and HL registers.
; -------------------------------------------------------------------
LOCATE: equ $0dd9
; -------------------------------------------------------------------
; ROM routine similar to Basic's CLS.
; Clears the display using the attributes loaded in the
; system variable ATTR_S.
; Alters the value of the AF, BC, DE and HL registers.
; -------------------------------------------------------------------
CLS: equ $0daf
Main:
ld a, $0e ; A = colour attributes
ld hl, ATTR_T ; HL = address current attributes
ld (hl), a ; Load into memory
ld hl, ATTR_S ; HL = address permanent attributes
ld (hl), a ; Load into memory
call CLS ; Clear screen: use ATTR_S
ld b, $18-$0a ; B = Y coordinate
ld c, $21-$03 ; C = X-coordinate
call LOCATE ; Position cursor
ld hl, msg ; HL = message address
Loop:
ld a, (hl) ; A = string character
or a ; ¿A = 0?
jr z , Exit ; If A = 0, skip
rst $10 ; Prints character
inc hl ; HL = address next character
jr Loop ; Loop until A = 0
Exit:
jr Exit ; Infinite loop
msg: defm 'Hello ZX Spectrum Assemby', $00
; String ending in 0 = null
end $8000
We compile with PASMO, load in the emulator and see the results:
Ports of entry and exit
The input and output ports are used for reading the keyboard, joystick, etc.
In our case, we will only use it to change the colour of the screen border, using the OUT command and the $FE port.
Write a small program to see how to change the border:
org $8000 ; Address where the programme is loaded
ld a, $01 ; A = border color
out ($fe), a ; Border = blue
ret
end $8000
We compile with PASMO, load in the emulator and see the result:
With this we can finish our first ZX Spectrum program in assembly. We restore the helloworld.asm file and add the lines to change the border colour before the CLS call:
ld a, $01 ; Load the border colour in A
out ($fe), a ; Change border colour
We compile with PASMO, load in the emulator and see the results:
ZX Spectrum Assembly, Pong – Hello World
We had already developed our first assembly program for the ZX Spectrum; we started the development of ZX-Pong.
Download the source code from here.
Useful links
ZX Spectrum Assembly, Pong by Juan Antonio Rubio García.
Translation by Felipe Monge Corbalán.
This work is licensed to Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
Any comments are always welcome.