Skip to content

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:

    LD B, 10        ; Loop 10 times
loop:
    ; ... your code here ...
    DJNZ loop       ; B = B - 1; jump back if B != 0

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

    LD A, 2         ; Red (colour index 2)
    OUT ($FE), A    ; Set 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 differLD 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:

screen address = $4000 + ((Y & 7) << 8) + ((Y >> 3) << 5) + X

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