Z80 assembly
The Zilog Z80 is the CPU behind the ZX Spectrum, MSX, Amstrad CPC, Game Boy (a close relative), Sega Master System, and countless arcade boards. It was one of the most popular chips of the 8-bit era and remains a joy to program — more registers than the 6502, a richer instruction set, and a satisfying block-copy instruction that makes screen-filling trivially fast.
The IDE supports Z80 assembly for several platforms through its tool modules:
| Platform | Assembler / toolchain | Notes |
|---|---|---|
| ZX Spectrum | z88dk (zasm / sccz80) | Tape images, direct BASIC loader integration |
| MSX | z88dk / standalone Z80 asm | ROM and RAM targets |
| Amstrad CPC | Standalone Z80 asm | CPC-specific firmware calls |
| Arcade boards | Platform-specific | Preset-dependent; check the template |
Start with one platform
Z80 systems diverge quickly at the hardware level. Learn Z80 assembly on one machine first, then port the knowledge. The ZX Spectrum is a great starting point — simple memory map, no hardware sprites, visible results immediately.
The Z80 in a nutshell
The Z80 has a generous set of registers compared to the 6502:
| Register(s) | Size | Purpose |
|---|---|---|
| A (Accumulator) | 8-bit | Arithmetic and logic |
| F (Flags) | 8-bit | Status flags (Zero, Carry, Sign, Overflow, …) |
| BC | 16-bit | General purpose; B often used as loop counter |
| DE | 16-bit | General purpose; destination pointer in block ops |
| HL | 16-bit | General purpose; the memory pointer register |
| IX, IY | 16-bit | Index registers (slower; use sparingly) |
| SP | 16-bit | Stack pointer |
| PC | 16-bit | Program counter |
| A', F', B', C', D', E', H', L' | 8-bit each | Alternate register set (swap with EXX / EX AF,AF') |
HL is your best friend. Most memory operations use it as a pointer. Think of it as the Z80's equivalent of a C pointer variable.
Your first program
Here's the smallest useful ZX Spectrum program — print a message to the screen:
; ZX Spectrum — "Hello" using ROM routine
; Assemble with z88dk / nasm / pasmo
ORG $8000 ; Load address (above BASIC)
start:
LD HL, message ; Point HL at our message
print_loop:
LD A, (HL) ; Load next character
CP 0 ; Check for null terminator
RET Z ; Return if done
RST $10 ; Call ROM print-character routine
INC HL ; Advance pointer
JR print_loop ; Loop
message:
DEFM "HELLO, SPECTRUM", 13, 0 ; 13 = carriage return, 0 = end
END start
From BASIC: LOAD ""CODE then RANDOMIZE USR 32768 (32768 = $8000).
The instructions you'll use most
Loading data
LD A, 42 ; Load immediate value 42 into A
LD A, (HL) ; Load byte from address in HL into A
LD (HL), A ; Store A into address in HL
LD HL, $4000 ; Load 16-bit address into HL
LD B, 10 ; Load 10 into B (common loop counter)
LD DE, HL ; Copy HL into DE (use EX DE,HL or LD D,H / LD E,L)
LD is everywhere
LD does almost everything that would require LDA, STA, LDX, STX etc. on the 6502. The Z80's LD instruction has many variants — it's the most important instruction to know well.
Arithmetic
ADD A, B ; A = A + B
ADD A, 5 ; A = A + 5
ADC A, B ; A = A + B + Carry
SUB B ; A = A - B
SBC A, B ; A = A - B - Carry
INC A ; A = A + 1
INC HL ; HL = HL + 1 (16-bit increment)
DEC B ; B = B - 1
ADD HL, BC ; HL = HL + BC (16-bit addition)
Logic
AND B ; A = A AND B (also AND n for immediate)
OR C ; A = A OR C
XOR A ; A = 0 (XOR with itself — fast clear)
CP 32 ; Compare A with 32 (sets flags, no result stored)
Control flow
JP $8000 ; Unconditional jump to $8000
JP Z, found ; Jump if Zero flag set
JP NZ, loop ; Jump if Zero flag NOT set
JP C, overflow ; Jump if Carry set
JR loop ; Relative jump (±127 bytes — shorter encoding)
JR NZ, loop ; Relative jump if not zero
CALL $8000 ; Call subroutine
RET ; Return
RET Z ; Return if Zero flag set
Loops
The DJNZ instruction is the Z80's built-in loop counter — decrement B and jump if non-zero:
Block operations
The Z80's block instructions are extraordinarily powerful for screen fills and memory copies:
; Copy 6912 bytes from source to $4000 (ZX Spectrum screen)
LD HL, source ; Source address
LD DE, $4000 ; Destination (Spectrum screen)
LD BC, 6912 ; Byte count
LDIR ; Block copy, incrementing HL, DE; decrements BC until zero
; Fill screen with spaces
LD HL, $4000 ; Start of screen
LD DE, $4001 ; Next byte
LD BC, 6911 ; 6911 more bytes
LD (HL), $00 ; Set first byte
LDIR ; Propagate it
LDIR (Load, Increment, Repeat) is one of the most useful instructions in the entire Z80 set.
The stack
The Z80 stack grows downward. PUSH and POP work on register pairs:
PUSH AF ; Push A and F onto stack
PUSH BC ; Push BC
PUSH DE ; Push DE
PUSH HL ; Push HL
POP HL ; Pop into HL
POP DE
POP BC
POP AF
Always POP in the reverse order you PUSHed. CALL and RET use the stack automatically.
Platform memory maps
| Range | Contents |
|---|---|
| $0000–$3FFF | ROM (BASIC interpreter, routines) |
| $4000–$57FF | Screen pixel data (6144 bytes) |
| $5800–$5AFF | Screen attribute data (768 bytes) |
| $5B00–$5BFF | System variables |
| $5C00–$5CBF | More system variables |
| $5CB6–$FFFF | Free RAM (BASIC program, then your code) |
The screen layout is unusual — pixel rows are stored in a non-linear order. Attributes (colour cells) are stored separately after the pixel data.
| Range | Contents |
|---|---|
| $0000–$3FFF | ROM (paged — two ROMs) |
| $4000–$7FFF | Page 5 RAM (screen + system) |
| $8000–$BFFF | Page 2 RAM (always present) |
| $C000–$FFFF | Paged RAM (pages 0–7 selectable) |
Memory paging is controlled via port $7FFD. Page carefully — wrong page = instant crash.
| Range | Contents |
|---|---|
| $0000–$7FFF | ROMs (BIOS, BASIC) in slot 0 |
| $8000–$FFFF | RAM in slot 3 |
| I/O port $98 | VDP (TMS9918 video) data |
| I/O port $99 | VDP control |
| I/O port $A8 | Slot select register |
MSX uses a slot system for memory — different hardware lives in different slots. The BIOS handles most of this, but understanding slots matters for cartridge development.
| Range | Contents |
|---|---|
| $0000–$3FFF | Lower ROM (BASIC) or RAM |
| $4000–$7FFF | RAM |
| $8000–$BFFF | RAM |
| $C000–$FFFF | Upper ROM or RAM |
| I/O port $7F00 | Gate Array (video, memory paging) |
| I/O port $F4xx | PPI (keyboard, tape, sound) |
The CPC uses port I/O extensively. The Gate Array controls screen mode, palette, and ROM visibility.
I/O ports
Unlike the 6502 (which maps all hardware to memory addresses), the Z80 has separate I/O instructions:
IN A, ($FE) ; Read from port $FE into A (e.g. ZX Spectrum keyboard)
OUT ($FE), A ; Write A to port $FE (e.g. ZX Spectrum border colour)
On the ZX Spectrum, OUT ($FE), A sets the border colour from bits 0–2 of A and controls the EAR/MIC lines. One instruction, instant visible effect.
ZX Spectrum — practical hardware
Setting the border colour
Spectrum colours: 0=Black, 1=Blue, 2=Red, 3=Magenta, 4=Green, 5=Cyan, 6=Yellow, 7=White.
Setting screen attributes
The 32×24 attribute grid at $5800 controls ink/paper colours for each character cell:
; Attribute byte: FBPPPIII
; F = Flash, B = Bright, PPP = Paper colour, III = Ink colour
LD HL, $5800 ; Top-left attribute cell
LD A, %00111001 ; Bright off, Paper=7 (white), Ink=1 (blue)
LD (HL), A ; Set top-left cell
To fill the whole attribute area:
LD HL, $5800
LD DE, $5801
LD BC, 767
LD (HL), A ; Set first byte to desired attribute
LDIR ; Copy it to the rest
Reading the keyboard
; Read row 0 of ZX Spectrum keyboard (port $FEFE)
; Bits 0–4: CAPS V C X Z (bit low = key pressed)
LD A, $FE
IN A, ($FE) ; Read keyboard row
BIT 0, A ; Test CAPS SHIFT key
JR Z, caps_pressed ; Jump if bit was 0 (pressed)
Full minimal example — ZX Spectrum colour bars
; ZX Spectrum — rainbow attribute bars
ORG $8000
start:
LD HL, $5800 ; Start of attribute memory
LD B, 24 ; 24 character rows
row_loop:
PUSH BC
LD A, B ; Use row counter as colour
AND 7 ; Mask to 3 bits (colour 0-7)
LD C, A
RLCA ; Shift to paper bits (bits 3-5)
RLCA
RLCA
OR C ; Combine paper and ink colours
LD B, 32 ; 32 columns
col_loop:
LD (HL), A
INC HL
DJNZ col_loop
POP BC
DJNZ row_loop
RET
END start
Common mistakes
Using IX/IY unnecessarily — these index registers are useful but slower than HL. Use HL for your main pointer and only reach for IX/IY when you genuinely need two pointers at once.
Forgetting that LD variants differ — LD A, (HL) (load from address) is very different from LD A, H (copy register) or LD A, 42 (immediate). A missing or extra () changes everything.
Non-linear Spectrum screen layout — pixel rows on the 48K Spectrum aren't stored sequentially. The address of row Y, column X is:
This trips up everyone the first time. Many people just use a lookup table.
Forgetting to preserve IX — the ZX Spectrum ROM uses IX internally. If you call ROM routines, save and restore IX.
Stack imbalance — every PUSH needs a matching POP. An imbalanced stack causes the RET instruction to jump to garbage. Use a debugger or add assertions during development.
See also
- 6502 assembly — the other classic 8-bit assembly language
- Z88DK / C for Z80 — write C for Z80 platforms
- ZX Spectrum platform guide
- IDE getting started