z80gfx Homebrew Computer
Programming Reference Manual — Z80 Assembly & Machine Code
START Quick Start Guide
Get up and running in under 3 minutes. This section walks you through running your first program, and understanding the basic workflow for writing software.
Step 1: Run Something
# Built-in Hello World
z80emu
# Built-in demo with graphics + sound
z80emu_gui
# Run the Snake game
z80emu_gui snake.bin
# Run Nascom BASIC (real-world ROM)
z80emu_gui ROM_32K.HEX
# Self-test suite (21 CPU tests)
z80emu --test
# Interactive debugger
z80emu -d snake.bin
Step 2: Understand the Workflow
Write Z80 Assembly
Use any Z80 cross-assembler (z80asm, zmac, Pasmo, TASM) or hand-assemble with a Python script. Output a raw .bin or Intel .hex file.
Load & Run
Pass your binary to the emulator: z80emu_gui myrom.bin. It loads at address $0000 by default, or specify a load address: z80emu_gui myrom.bin 100.
Debug & Iterate
Use the console debugger (z80emu -d myrom.bin) to step through code, set breakpoints, inspect registers and memory, and trace I/O. The GUI version supports F1 soft-reset.
START Hello World Tutorial
Here's the simplest possible program that prints text. This works on both the console and GUI builds:
; hello.asm - Prints "Hello!" via the ACIA data port
; Assemble to hello.bin, run with: z80emu_gui hello.bin
ORG $0000
LD SP, $FFFF ; Initialize stack pointer
LD HL, message ; Point HL to our string
print_loop:
LD A, (HL) ; Load next character
OR A ; Test for null terminator
JR Z, done ; If zero, we're done
OUT ($81), A ; Output character via ACIA
INC HL ; Advance to next character
JR print_loop ; Loop
done:
HALT ; Stop the CPU
message:
DB "Hello from z80gfx!", 13, 10, 0
If you're hand-assembling, here are the raw bytes:
31 FF FF ; LD SP, $FFFF
21 0D 00 ; LD HL, $000D (address of message)
7E ; LD A, (HL)
B7 ; OR A
28 05 ; JR Z, +5
D3 81 ; OUT ($81), A
23 ; INC HL
18 F7 ; JR -9 (back to LD A,(HL))
76 ; HALT
48 65 6C ... ; "Hello from z80gfx!\r\n\0"
Hello World with Graphics (GUI only)
This version clears the screen, draws a pattern to VRAM, beeps, and sets the border colour:
; graphical_hello.asm - Graphics + Sound + Terminal demo
ORG $0000
LD SP, $FFFF
; Print to terminal
LD HL, message
CALL print_str
; Clear VRAM
LD HL, $C000 ; VRAM base
LD DE, $1800 ; 6144 bytes
clr:
LD (HL), 0
INC HL
DEC DE
LD A, D
OR E
JR NZ, clr
; Draw a horizontal line at row 96 (center of screen)
LD HL, $C000 + (96 * 32) ; = $CC00
LD B, 32 ; 32 bytes = full row width
line:
LD (HL), $FF
INC HL
DJNZ line
; Play a 440 Hz beep
LD A, $B8
OUT ($40), A ; Freq low byte (440 & $FF)
LD A, $81
OUT ($41), A ; Freq high ($01) + gate ON ($80)
; Short delay
LD BC, $FFFF
delay:
DEC BC
LD A, B
OR C
JR NZ, delay
; Silence beeper
LD A, $01
OUT ($41), A ; Gate OFF
; Set border to dark blue
LD A, 4
OUT ($42), A
HALT
print_str:
LD A, (HL)
OR A
RET Z
OUT ($81), A
INC HL
JR print_str
message:
DB 27, "[2J" ; ANSI: clear screen
DB 27, "[H" ; ANSI: cursor home
DB "z80gfx says hello!", 13, 10, 0
- Stack init — always
LD SP, $FFFFbefore using CALL or PUSH - Terminal output —
OUT ($81), Aprints to the ANSI terminal panel - VRAM access — write directly to
$C000-$F7FFfor pixel graphics - Beeper — frequency on ports $40/$41, gate bit controls on/off
- Border colour — palette index to port $42
- ANSI escape codes — ESC[2J clears terminal, ESC[H moves cursor home
1 System Overview
The z80gfx is a homebrew 8-bit computer built around the Zilog Z80 CPU, implemented as a software emulator. It comes in two flavours that share the same CPU core (z80.h) and disassembler (z80disasm.h).
Console Build: z80emu Console
The original single-file build with no external dependencies. Compiles from main.cpp alone. Provides:
- Text-only I/O through stdin/stdout
- Full interactive debugger (breakpoints, stepping, disassembly, memory/register inspection)
- 21-test CPU self-test suite (
--test) - Built-in Hello World and uppercase echo ROMs
- Intel HEX and raw binary file loading
- Cross-platform: Windows (MSVC/MinGW), Linux, macOS
GUI Build: z80emu_gui GUI
The raylib-based graphical build. Compiled from main_gui.cpp, links raylib (auto-fetched by CMake). Adds:
- 256×192 monochrome framebuffer panel (VRAM at
$C000) - 80×25 ANSI terminal panel with escape sequence support
- Square-wave beeper with 15-bit frequency control
- Border colour control (16-colour palette)
- F1 soft-reset
- Window size: 1312 × 932 pixels
The entire codebase is four files: z80.h (CPU core), z80disasm.h (disassembler), main.cpp (console host), main_gui.cpp (GUI host). The CPU core has zero host dependencies and builds cleanly without raylib.
2 Memory Map
The z80gfx has a flat 64 KB address space with no bank switching or MMU. All memory is freely readable and writable. The following layout is used by convention in the GUI build:
- There is no ROM write-protection. Programs can overwrite their own code.
- VRAM is ordinary memory. Writing to it has immediate visual effect on the next frame.
- Stack pointer should be initialized to
$FFFF(grows downward). - The console build uses the full 64 KB as flat RAM with no VRAM semantics.
VRAM Layout Detail
The framebuffer occupies $C000-$F7FF (6,144 bytes). It represents a 256×192 monochrome bitmap in row-major order. Each byte represents 8 horizontal pixels, with the MSB being the leftmost pixel.
Address of pixel (x, y):
byte_addr = $C000 + (y * 32) + (x / 8)
bit_mask = $80 >> (x % 8)
To set pixel: mem[byte_addr] |= bit_mask ; turn ON
To clear pixel: mem[byte_addr] &= ~bit_mask ; turn OFF
To flip pixel: mem[byte_addr] ^= bit_mask ; toggle
Byte layout:
Bit: 7 6 5 4 3 2 1 0
Pixel: [0] [1] [2] [3] [4] [5] [6] [7]
MSB (leftmost) LSB (rightmost)
Row stride: 32 bytes
Total: 192 rows × 32 bytes = 6,144 bytes
The renderer uses palette[15] (white) for set bits and palette[0] (black) for clear bits. There is no attribute memory or colour-per-tile system in the current video mode.
Tile Coordinates
For tile-based games using 8×8 pixel tiles, the framebuffer forms a 32×24 tile grid. The address calculation is very efficient:
; Convert tile (tx, ty) to VRAM address
; H = ty + $C0 (high byte = VRAM page + tile row)
; L = tx (low byte = tile column)
; This gives the first byte of the 8-row tile.
; Each subsequent row is +32 bytes.
; Example: draw solid tile at (10, 5)
LD A, 5
ADD A, $C0 ; H = $C5
LD H, A
LD L, 10 ; L = $0A -> HL = $C50A
LD B, 8 ; 8 rows
LD DE, 32 ; row stride
LD A, $FF ; solid fill pattern
.loop:
LD (HL), A
ADD HL, DE
DJNZ .loop
The tile-to-address formula H = ty + $C0, L = tx requires just one ADD and two LD instructions — no multiplication needed. This is because VRAM starts at $C000 and each tile row is exactly 256 bytes (8 pixel rows × 32 bytes/row). This alignment is by design.
3 I/O Port Reference
I/O ports are accessed with the Z80 IN and OUT instructions. The emulator matches on the low byte of the port address only (port & $FF). Both builds share keyboard and ACIA ports; the GUI build adds video and sound.
Complete Port Map
| Port | Dir | Build | Function |
|---|---|---|---|
$00 | R | Both | Keyboard status: $FF if key waiting, $00 if empty |
$01 | R | Both | Keyboard character input (console: blocking; GUI: non-blocking) |
$01 | W | Both | Character output (stdout in console, terminal panel in GUI) |
$40 | W | GUI | Beeper frequency low byte (bits 0–7) |
$41 | W | GUI | Beeper frequency high (bits 0–6) + gate on/off (bit 7) |
$42 | W | GUI | Border colour (palette index 0–15, low 4 bits) |
$43 | W | GUI | Video mode select (reserved — only mode 0 in v1) |
$80 | R | Both | ACIA status: bit 0 = RDRF (key ready), bit 1 = TDRE (always 1) |
$80 | W | Console | ACIA control register (stores value; $03 = master reset) |
$81 | R | Both | ACIA data input (reads keyboard queue, same as port $01) |
$81 | W | Both | ACIA data output (prints character, same as port $01) |
Simple I/O Ports ($00–$01)
Port $00 (Read) — Keyboard Status
Returns $FF if at least one keypress is waiting, $00 if empty. Non-blocking.
; Poll-wait for a keypress
wait_key:
IN A, ($00) ; read keyboard status
OR A ; test if zero
JR Z, wait_key ; loop until key available
IN A, ($01) ; read the key
; A now holds the ASCII code
Port $01 (Read) — Character Input
Returns the next character from the keyboard queue. Console build: blocks until a key is available. GUI build: returns 0 if empty (non-blocking).
Port $01 (Write) — Character Output
Sends a character to the display. Console: stdout. GUI: the on-screen ANSI terminal panel.
LD A, 'H' ; ASCII $48
OUT ($01), A ; print 'H'
LD A, 'i' ; ASCII $69
OUT ($01), A ; print 'i'
6850 ACIA Ports ($80–$81)
The ACIA provides a serial-port-style interface compatible with the Motorola 6850, as used in Grant Searle's designs. Functionally equivalent to the simple ports, but adds a status register for polled I/O — making it compatible with existing Z80 software like Nascom BASIC.
Port $80 (Read) — ACIA Status Register
| Bit | Name | Meaning |
|---|---|---|
| 0 | RDRF | Receive Data Register Full (1 = character waiting) |
| 1 | TDRE | Transmit Data Register Empty (always 1 = always ready) |
| 2–7 | — | Reserved / unused (read as 0) |
; Standard ACIA polling pattern (used by BASIC ROMs)
acia_rx_wait:
IN A, ($80) ; read ACIA status
AND $01 ; test RDRF bit
JR Z, acia_rx_wait ; loop until character ready
IN A, ($81) ; read the character
RET
acia_tx: ; Output character in A
PUSH AF
.wait:
IN A, ($80) ; check TDRE (always ready here)
AND $02
JR Z, .wait
POP AF
OUT ($81), A ; transmit
RET
Beeper Ports ($40–$41) GUI
The beeper generates a square wave. Frequency is a 15-bit unsigned integer in Hz (direct 1:1 mapping). Bit 7 of port $41 is the on/off gate.
Port $40 (Write) — Frequency Low Byte
Sets bits 0–7 of the frequency register. High byte is preserved from the last $41 write.
Port $41 (Write) — Frequency High Byte + Gate
| Bits | Name | Meaning |
|---|---|---|
| 0–6 | Freq Hi | Bits 8–14 of frequency (0–127) |
| 7 | Gate | 1 = sound on, 0 = sound off |
; Play 440 Hz (concert A) then silence
LD A, $B8 ; 440 & $FF = $B8
OUT ($40), A ; freq low byte
LD A, $81 ; (440 >> 8) | $80 = $01 | $80 = $81
OUT ($41), A ; freq high + gate ON
; ... delay ...
LD A, $01 ; freq hi $01, gate OFF (bit 7 clear)
OUT ($41), A ; silence
Musical Note Frequency Table
| Note | Hz | Port $40 | Port $41 (ON) | Note | Hz | Port $40 | Port $41 (ON) |
|---|---|---|---|---|---|---|---|
| C4 | 262 | $06 | $81 | C5 | 523 | $0B | $82 |
| D4 | 294 | $26 | $81 | D5 | 587 | $4B | $82 |
| E4 | 330 | $4A | $81 | E5 | 659 | $93 | $82 |
| F4 | 349 | $5D | $81 | F5 | 698 | $BA | $82 |
| G4 | 392 | $88 | $81 | G5 | 784 | $10 | $83 |
| A4 | 440 | $B8 | $81 | A5 | 880 | $70 | $83 |
| B4 | 494 | $EE | $81 | B5 | 988 | $DC | $83 |
freq_lo = Hz & $FF → port $40freq_hi = ((Hz >> 8) & $7F) | $80 → port $41 (with gate ON)To silence: write
(Hz >> 8) & $7F to port $41 (bit 7 clear)
Video Ports ($42–$43) GUI
Port $42 (Write) — Border Colour
Sets the border colour surrounding the framebuffer. Accepts palette index 0–15.
LD A, 4 ; dark blue
OUT ($42), A
Port $43 (Write) — Video Mode (Reserved)
Reserved for future colour modes. Currently a no-op. Only mode 0 (256×192 mono) exists.
Colour Palette
16 CGA-style colours used for border and terminal text:
4 Video System (VRAM) GUI
Common VRAM Operations
Clear Screen
clear_screen:
LD HL, $C000 ; VRAM start
LD DE, $1800 ; 6144 bytes to clear
.loop:
LD (HL), 0 ; zero = all pixels off
INC HL
DEC DE
LD A, D
OR E ; test DE == 0
JR NZ, .loop
RET
Draw 8×8 Solid Tile
; Input: B = tile_x (0-31), C = tile_y (0-23), A = pattern byte
draw_tile:
PUSH AF
LD A, C
ADD A, $C0 ; H = tile_y + VRAM page base
LD H, A
LD L, B ; L = tile_x
LD DE, 32 ; row stride
POP AF
LD B, 8 ; 8 rows per tile
.loop:
LD (HL), A ; write pattern to row
ADD HL, DE ; advance to next pixel row
DJNZ .loop
RET
Draw Custom 8×8 Sprite from Data
; Input: B = tile_x, C = tile_y, IX = pattern data (8 bytes)
draw_sprite:
LD A, C
ADD A, $C0
LD H, A
LD L, B
LD DE, 32
LD B, 8
.loop:
LD A, (IX+0) ; load pattern row
LD (HL), A ; write to VRAM
INC IX
ADD HL, DE ; next pixel row
DJNZ .loop
RET
; Sprite data examples:
spr_heart: DB $00,$66,$FF,$FF,$FF,$7E,$3C,$18
spr_arrow: DB $10,$30,$7F,$FF,$7F,$30,$10,$00
spr_ball: DB $18,$3C,$7E,$7E,$7E,$7E,$3C,$18
Set Individual Pixel
; Input: B = x (0-255), C = y (0-191)
set_pixel:
LD H, 0
LD L, C ; HL = y
ADD HL, HL ; *2
ADD HL, HL ; *4
ADD HL, HL ; *8
ADD HL, HL ; *16
ADD HL, HL ; *32 (y * 32)
LD A, B
SRL A
SRL A
SRL A ; A = x / 8
LD E, A
LD D, 0
ADD HL, DE ; + byte column
LD DE, $C000
ADD HL, DE ; + VRAM base
; Build bit mask
LD A, B
AND $07 ; x mod 8
LD B, A
LD A, $80 ; leftmost bit
JR Z, .apply
.shift:
SRL A ; shift mask right
DJNZ .shift
.apply:
OR (HL) ; merge with existing
LD (HL), A
RET
Scroll Screen Up by 1 Tile Row
scroll_up_tile:
LD HL, $C100 ; source = tile row 1
LD DE, $C000 ; dest = tile row 0
LD BC, $1700 ; 23 tile rows * 256 bytes
LDIR ; block copy
; Clear bottom tile row
LD HL, $F700
LD B, 0 ; 256 bytes
.clr:
LD (HL), 0
INC HL
DJNZ .clr
RET
Draw Horizontal Line
; Draw a full-width horizontal line at pixel row Y
; Input: C = y (0-191)
draw_hline:
LD H, 0
LD L, C
ADD HL, HL
ADD HL, HL
ADD HL, HL
ADD HL, HL
ADD HL, HL ; HL = y * 32
LD DE, $C000
ADD HL, DE ; HL = VRAM addr of row start
LD B, 32
LD A, $FF
.loop:
LD (HL), A
INC HL
DJNZ .loop
RET
5 Terminal System (ANSI) GUI
The GUI build includes an 80×25 character terminal panel below the framebuffer. Characters written to port $01 or $81 appear on this terminal. It supports a subset of ANSI/VT100 escape sequences.
Control Characters
| Code | Name | Action |
|---|---|---|
$07 | BEL | Bell (reserved, no-op currently) |
$08 | BS | Backspace — move cursor left 1 position |
$09 | TAB | Tab — advance to next multiple of 8 columns |
$0A | LF | Line feed — move down 1 row (scrolls at bottom) |
$0C | FF | Form feed — clear entire screen |
$0D | CR | Carriage return — move cursor to column 0 |
$1B | ESC | Start escape sequence |
ANSI Escape Sequences
Format: ESC [ params command where params are semicolon-separated numbers.
| Sequence | Action | Example |
|---|---|---|
ESC[row;colH | Move cursor (1-indexed) | ESC[1;1H = top-left |
ESC[row;colf | Move cursor (same as H) | ESC[12;40f = centre |
ESC[2J | Clear entire screen | ESC[2J |
ESC[K | Clear cursor to end of line | ESC[K |
ESC[0m | Reset attributes to default | ESC[0m |
ESC[30m–ESC[37m | Set foreground colour 0–7 | ESC[31m = red |
ESC[40m–ESC[47m | Set background colour 0–7 | ESC[44m = blue bg |
ESC[90m–ESC[97m | Set bright foreground 8–15 | ESC[92m = bright green |
; Terminal control examples
esc_clear: DB $1B, "[2J", $1B, "[H", 0 ; clear screen + cursor home
esc_red: DB $1B, "[31m", 0 ; red foreground
esc_green: DB $1B, "[32m", 0 ; green foreground
esc_bright: DB $1B, "[92m", 0 ; bright green
esc_reset: DB $1B, "[0m", 0 ; reset to default
esc_pos: DB $1B, "[5;10H", 0 ; cursor to row 5, col 10
6 Sound System (Beeper) GUI
Single-channel square wave generator. 50% duty cycle, 22,050 Hz sample rate, 15-bit frequency control (0–32,767 Hz). The audio callback runs on a separate thread; frequency and gate values are atomic.
Programming Pattern
; Play a note
; 1. Write freq low byte to port $40
; 2. Write freq high | $80 to port $41 (gate ON)
; 3. Delay for desired duration
; 4. Write freq high (without $80) to port $41 (gate OFF)
; Play frequency in DE (Hz) for B duration units
play_tone:
LD A, E
OUT ($40), A ; freq low
LD A, D
AND $7F
OR $80 ; set gate bit
OUT ($41), A ; freq high + ON
.wait:
PUSH BC
LD BC, $2000 ; inner delay
.inner:
DEC BC
LD A, B
OR C
JR NZ, .inner
POP BC
DJNZ .wait
; Silence
LD A, D
AND $7F ; gate OFF
OUT ($41), A
RET
Sound Effect Examples
; Short beep (440 Hz, ~20ms)
beep:
LD A, $B8
OUT ($40), A
LD A, $81
OUT ($41), A
LD C, 16
.outer:
LD B, 0
.inner:
DJNZ .inner
DEC C
JR NZ, .outer
LD A, $01
OUT ($41), A ; silence
RET
; Rising tone (power-up sound)
rising:
LD DE, $0100 ; start at 256 Hz
LD C, 30
.step:
LD A, E
OUT ($40), A
LD A, D
OR $80
OUT ($41), A
LD B, 0
.dly:
DJNZ .dly
INC DE
INC DE
INC DE ; raise pitch
DEC C
JR NZ, .step
LD A, 0
OUT ($41), A
RET
7 Keyboard Input
Three Input Methods
; Method 1: Simple port polling (both builds)
wait_key:
IN A, ($00) ; keyboard status
OR A
JR Z, wait_key ; loop until ready
IN A, ($01) ; read key
RET
; Method 2: ACIA status polling (compatible with BASIC ROMs)
acia_read:
IN A, ($80) ; ACIA status
AND $01 ; test RDRF
JR Z, acia_read
IN A, ($81) ; read character
RET
; Method 3: Non-blocking check (good for game loops)
check_key:
IN A, ($00)
OR A
RET Z ; Z flag set = no key available
IN A, ($01) ; A = key, Z clear
RET
Key Codes
| Key | Code | Key | Code | Key | Code |
|---|---|---|---|---|---|
| A–Z | $41–$5A | a–z | $61–$7A | 0–9 | $30–$39 |
| Space | $20 | Enter | $0D | Backspace | $08 |
| Tab | $09 | Ctrl+C | $03 | ESC | Not queued* |
*ESC is reserved by raylib for window close and is not passed to Z80 programs.
Interrupt-Driven Input
The emulator triggers interrupts cooperatively when keys are pending and IFF1 is set (~every 1,024 instructions in GUI). Use IM 1 for the simplest setup:
ORG $0000
LD SP, $FFFF
IM 1 ; interrupts jump to $0038
EI ; enable interrupts
JP main
ORG $0038 ; IM 1 vector
isr:
PUSH AF
IN A, ($81) ; read the key
LD ($2000), A ; store it
LD A, 1
LD ($2001), A ; set flag
POP AF
EI
RETI
main:
LD A, ($2001) ; check flag
OR A
JR Z, main
XOR A
LD ($2001), A ; clear flag
LD A, ($2000) ; process key
OUT ($81), A ; echo it
JR main
8 Z80 CPU Reference
Registers
Main Registers: Shadow (Alternate) Set:
16-bit Hi Lo (swapped with EX AF,AF' and EXX)
------ --- --- AF' BC' DE' HL'
AF A F (flags)
BC B C Index Registers:
DE D E IX (16-bit, displacement addressing)
HL H L IY (16-bit, displacement addressing)
SP Stack Pointer Special:
PC Program Counter I Interrupt Vector (high byte for IM 2)
R Refresh Counter (7-bit + preserved bit 7)
A (Accumulator) is the primary register for arithmetic and I/O. HL is the primary memory pointer. BC is commonly used as a counter (B for DJNZ loops, BC for LDIR/LDDR). DE is commonly a destination pointer. F holds the CPU flags (see Flag Register).
Addressing Modes
| Mode | Syntax | Example | Description |
|---|---|---|---|
| Immediate | n / nn | LD A, $42 | Value is part of the instruction |
| Register | r / rr | LD A, B | Value is in a register |
| Register Indirect | (HL) | LD A, (HL) | Address is in HL (or BC, DE) |
| Indexed | (IX+d) | LD A, (IX+5) | Address = IX/IY + signed offset |
| Direct | (nn) | LD A, ($2000) | Absolute 16-bit address |
| Relative | e | JR NZ, label | PC + signed 8-bit offset (-128..+127) |
| Bit | b, r | BIT 3, A | Tests/sets/clears bit b in register r |
| I/O | (n) / (C) | IN A, ($80) | Port address (8-bit or register C) |
| Restart | p | RST $38 | Call to fixed addresses $00,$08,...,$38 |
9 Instruction Set Summary
Complete instruction set as implemented in the emulator. r = B/C/D/E/H/L/A, rr = BC/DE/HL/SP, cc = condition code.
Load & Exchange
| Instruction | Opcode | Operation | Flags |
|---|---|---|---|
LD r, r' | 01dddss | r = r' | — |
LD r, n | xx nn | r = immediate byte | — |
LD r, (HL) | 01ddd110 | r = mem[HL] | — |
LD (HL), r | 01110sss | mem[HL] = r | — |
LD (HL), n | 36 nn | mem[HL] = n | — |
LD A, (BC) | 0A | A = mem[BC] | — |
LD A, (DE) | 1A | A = mem[DE] | — |
LD A, (nn) | 3A lo hi | A = mem[nn] | — |
LD (nn), A | 32 lo hi | mem[nn] = A | — |
LD rr, nn | x1 lo hi | rr = immediate word | — |
LD HL, (nn) | 2A lo hi | HL = word at nn | — |
LD (nn), HL | 22 lo hi | word at nn = HL | — |
LD SP, HL | F9 | SP = HL | — |
PUSH rr | x5 | SP-=2, (SP)=rr | — |
POP rr | x1 | rr=(SP), SP+=2 | — |
EX AF, AF' | 08 | Swap AF ↔ AF' | — |
EXX | D9 | Swap BC/DE/HL ↔ shadows | — |
EX DE, HL | EB | Swap DE ↔ HL | — |
EX (SP), HL | E3 | Swap HL ↔ (SP) | — |
Arithmetic
| Instruction | Operation | Flags |
|---|---|---|
ADD A, r/n/(HL) | A = A + operand | S Z H PV C |
ADC A, r/n/(HL) | A = A + operand + Carry | S Z H PV C |
SUB r/n/(HL) | A = A - operand | S Z H PV N C |
SBC A, r/n/(HL) | A = A - operand - Carry | S Z H PV N C |
CP r/n/(HL) | Compare: A - operand (discard result) | S Z H PV N C |
INC r/(HL) | r = r + 1 | S Z H PV (C unchanged) |
DEC r/(HL) | r = r - 1 | S Z H PV N (C unchanged) |
NEG | A = 0 - A (negate) | S Z H PV N C |
DAA | BCD decimal adjust A | S Z H PV C |
ADD HL, rr | HL = HL + rr | H C (S Z PV unchanged) |
ADC HL, rr | HL = HL + rr + Carry | S Z H PV C |
SBC HL, rr | HL = HL - rr - Carry | S Z H PV N C |
INC rr | rr = rr + 1 | — (no flags) |
DEC rr | rr = rr - 1 | — (no flags) |
Logic & Bit Manipulation
| Instruction | Operation | Flags |
|---|---|---|
AND r/n/(HL) | A = A AND operand | S Z P, H=1, N=0, C=0 |
OR r/n/(HL) | A = A OR operand | S Z P |
XOR r/n/(HL) | A = A XOR operand | S Z P |
CPL | A = NOT A (complement) | H=1, N=1 |
SCF | Set Carry Flag | C=1, H=0, N=0 |
CCF | Complement Carry Flag | C toggled |
BIT b, r/(HL) | Test bit b | Z (set if bit=0), H=1 |
SET b, r/(HL) | Set bit b to 1 | — |
RES b, r/(HL) | Reset bit b to 0 | — |
Rotate & Shift
| Instruction | Operation | Flags |
|---|---|---|
RLCA | Rotate A left circular | C = old bit 7 |
RRCA | Rotate A right circular | C = old bit 0 |
RLA | Rotate A left through Carry | C = old bit 7 |
RRA | Rotate A right through Carry | C = old bit 0 |
RLC r/(HL) | Rotate left circular | S Z P C |
RRC r/(HL) | Rotate right circular | S Z P C |
RL r/(HL) | Rotate left through Carry | S Z P C |
RR r/(HL) | Rotate right through Carry | S Z P C |
SLA r/(HL) | Shift left arithmetic (bit 0 = 0) | S Z P C |
SRA r/(HL) | Shift right arithmetic (bit 7 preserved) | S Z P C |
SRL r/(HL) | Shift right logical (bit 7 = 0) | S Z P C |
SLL r/(HL) | Shift left, bit 0 = 1 (undocumented) | S Z P C |
RLD | Rotate BCD left nibbles (HL) ↔ A | S Z P |
RRD | Rotate BCD right nibbles (HL) ↔ A | S Z P |
Jump, Call & Return
| Instruction | Operation | Notes |
|---|---|---|
JP nn | PC = nn | Unconditional |
JP cc, nn | If cc: PC = nn | All 8 conditions |
JR e | PC += signed offset | Unconditional, ±127 |
JR cc, e | If cc: PC += offset | NZ/Z/NC/C only |
JP (HL) | PC = HL | Jump to address in HL |
DJNZ e | B--; if B≠0: PC += offset | Loop counter in B |
CALL nn | PUSH PC; PC = nn | Subroutine call |
CALL cc, nn | If cc: CALL nn | All 8 conditions |
RET | PC = POP | Return from subroutine |
RET cc | If cc: RET | All 8 conditions |
RETI | Return from interrupt | IFF1 = IFF2 |
RETN | Return from NMI | IFF1 = IFF2 |
RST p | CALL p | p = $00,$08,...,$38 |
Condition Codes
| Code | Mnemonic | Condition | Code | Mnemonic | Condition |
|---|---|---|---|---|---|
| 0 | NZ | Not Zero | 4 | PO | Parity Odd |
| 1 | Z | Zero | 5 | PE | Parity Even |
| 2 | NC | No Carry | 6 | P | Positive (Sign=0) |
| 3 | C | Carry | 7 | M | Minus (Sign=1) |
I/O & Block Operations
| Instruction | Operation |
|---|---|
IN A, (n) | A = port[n | (A<<8)] |
OUT (n), A | port[n | (A<<8)] = A |
IN r, (C) | r = port[BC]; sets S/Z/P flags |
OUT (C), r | port[BC] = r |
LDI/LDIR | (DE)=(HL); DE++; HL++; BC-- [repeat till BC=0] |
LDD/LDDR | (DE)=(HL); DE--; HL--; BC-- [repeat till BC=0] |
CPI/CPIR | Compare A-(HL); HL++; BC-- [repeat till match or BC=0] |
CPD/CPDR | Compare A-(HL); HL--; BC-- [repeat till match or BC=0] |
INI/INIR | (HL)=in(BC); HL++; B-- [repeat till B=0] |
IND/INDR | (HL)=in(BC); HL--; B-- [repeat till B=0] |
OUTI/OTIR | out(BC,(HL)); HL++; B-- [repeat till B=0] |
OUTD/OTDR | out(BC,(HL)); HL--; B-- [repeat till B=0] |
NOP | No operation (4 cycles) |
HALT | Stop CPU until interrupt or reset |
DI | Disable interrupts (IFF1=IFF2=0) |
EI | Enable interrupts (IFF1=IFF2=1) |
IM 0/1/2 | Set interrupt mode |
10 Flag Register
Bit: 7 6 5 4 3 2 1 0
Flag: S Z - H - P/V N C
Hex masks:
S = $80 Sign flag (bit 7 of result)
Z = $40 Zero flag (result == 0)
H = $10 Half-Carry (carry from bit 3 to bit 4)
P/V = $04 Parity (even 1-bits) or Overflow (signed)
N = $02 Subtract flag (set by SUB/SBC/CP/DEC/NEG)
C = $01 Carry flag (unsigned overflow / borrow)
Flag Effects by Instruction Group
| Instructions | S | Z | H | P/V | N | C |
|---|---|---|---|---|---|---|
ADD/ADC/SUB/SBC/CP/NEG | * | * | * | V | * | * |
AND | * | * | 1 | P | 0 | 0 |
OR / XOR | * | * | 0 | P | 0 | 0 |
INC | * | * | * | V | 0 | — |
DEC | * | * | * | V | 1 | — |
RLCA/RRCA/RLA/RRA | — | — | 0 | — | 0 | * |
RLC/RRC/RL/RR/SLA/SRA/SRL | * | * | 0 | P | 0 | * |
BIT b, r | * | * | 1 | * | 0 | — |
SET/RES | — | — | — | — | — | — |
ADD HL, rr | — | — | * | — | 0 | * |
ADC/SBC HL, rr | * | * | * | V | * | * |
INC/DEC rr | — | — | — | — | — | — |
* = affected by result, V = overflow, P = parity, 0 = cleared, 1 = set, — = unchanged
11 Interrupt System
Interrupt Modes
IM 0
Device places instruction on bus. Emulated as RST $38 (same as IM 1). Default mode at reset.
IM 1 (Recommended)
Interrupts always jump to $0038. Simplest to use. Just place your ISR at that address.
IM 2
Vectored: address = word at (I << 8) | vector. Supports up to 128 different handlers.
Emulator Timing
- Console: checks every ~10,000 instructions if a key is pending
- GUI: checks every ~1,024 instructions during each frame
- Only triggered when
IFF1is set (interrupts enabled viaEI) - Keyboard input is the only interrupt source (no timer)
DI / EI / HALT Behaviour
DI— Disables maskable interrupts (IFF1 = IFF2 = 0)EI— Enables interrupts. Takes effect after the next instruction.HALT— Stops CPU. Only an interrupt (if enabled) or reset resumes it.RETI/RETN— Copies IFF2 to IFF1, pops PC. Always useEIbeforeRETIin your ISR.
12 Debugger Reference Console
Launch with: z80emu -d [rom.bin [addr]]
| Command | Description |
|---|---|
s [n] | Step n instructions (default 1). Shows captured output if n ≤ 20 |
c / cont | Continue until breakpoint, HALT, or 100M-instruction limit |
r / regs | Display all registers, flags, and current instruction |
l [addr] [n] | Disassemble n lines (default 16) from addr (default PC) |
m addr [n] | Hex dump of n bytes (default 64) at address |
k [n] | Show n stack entries (default 8) relative to SP |
b addr | Set breakpoint at hex address |
bd addr | Delete breakpoint |
bl | List all breakpoints |
bc | Clear all breakpoints |
w addr val | Write byte to memory |
ww addr val | Write word to memory (little-endian) |
g [addr] | Go: set PC (optional) and continue |
pc addr | Set program counter |
sp addr | Set stack pointer |
io | Toggle I/O trace (shows every OUT with port and value) |
reset | Reset CPU (clears registers, preserves memory) |
test | Run 21-test CPU self-test suite |
q | Quit debugger |
Enter | Repeat last command |
; Example debugger session:
z80emu> r
AF=0042 BC=0000 DE=0000 HL=000D SP=FFFF PC=0003
IX=0000 IY=0000 I=00 R=03 IM=0 IFF1=0 IFF2=0
Flags: S=0 Z=1 H=0 P=0 N=0 C=0
0003: 7E LD A,(HL)
z80emu> b 0008 ; set breakpoint at $0008
z80emu> c ; continue to breakpoint
z80emu> m C000 20 ; dump first 32 bytes of VRAM
z80emu> l ; disassemble from current PC
13 File Loading
Intel HEX Format (.hex)
Files with .hex extension are auto-detected and parsed. Each line: :LLAAAATT[DD...]CC
| Field | Bytes | Meaning |
|---|---|---|
: | 1 | Start marker |
LL | 2 | Byte count |
AAAA | 4 | 16-bit load address |
TT | 2 | Type: 00=data, 01=EOF |
DD... | 2*LL | Data bytes |
CC | 2 | Checksum |
Raw Binary Format
All other files are loaded as raw binary blobs, copied directly into memory at the specified address.
z80emu rom.bin # load at $0000
z80emu rom.bin 100 # load at $0100
z80emu -d rom.bin 100 # debug with load at $0100
z80emu_gui snake.bin # GUI, load at $0000
z80emu ROM_32K.HEX # Intel HEX auto-detected
Creating ROM Files
Options for creating binary ROMs:
- Z80 cross-assembler: z80asm, zmac, Pasmo, TASM, NASM-z80
- Python scripts: see
tools/make_snake.pyandtools/make_pong.pyfor a mini assembler approach - Manual hex editing: hand-assemble opcodes and save as binary
14 Programming Techniques
Program Template
; Minimal z80gfx GUI program
ORG $0000
LD SP, $FFFF ; ALWAYS init stack first
LD A, 4
OUT ($42), A ; border = dark blue
CALL clear_screen
; === Your code here ===
HALT ; stop CPU
clear_screen:
LD HL, $C000
LD DE, $1800
.loop:
LD (HL), 0
INC HL
DEC DE
LD A, D
OR E
JR NZ, .loop
RET
Delay Loops
; Short (~8ms) ; Medium (~250ms)
short_delay: medium_delay:
LD B, 0 LD BC, $6000
.loop: .loop:
DJNZ .loop DEC BC
RET LD A, B
OR C
; Long (~1 second) JR NZ, .loop
long_delay: RET
LD D, 4
.outer:
LD BC, $FFFF
.inner:
DEC BC
LD A, B
OR C
JR NZ, .inner
DEC D
JR NZ, .outer
RET
Testing 16-bit Zero (Common Pattern)
; The Z80 does NOT set flags on DEC rr (16-bit decrement).
; To test if DE or BC reached zero:
DEC DE ; decrement 16-bit pair
LD A, D ; copy high byte to A
OR E ; OR with low byte
JR NZ, loop ; Z flag set only if both bytes are 0
; This works for BC too:
DEC BC
LD A, B
OR C
JR NZ, loop
Random Number Generator
; LCG: rand = 5*rand + 1 (mod 256)
; Full-period per Hull-Dobell theorem
; Output: A = pseudo-random byte
rand_byte:
LD A, (rand_seed)
LD B, A ; save copy
ADD A, A ; *2
ADD A, A ; *4
ADD A, B ; *5
ADD A, 1 ; +1
LD (rand_seed), A
RET
; Restrict to range 0..N-1 (N must be power of 2):
CALL rand_byte
AND N-1 ; e.g. AND $1F for 0-31
String Printing
; Print null-terminated string via ACIA
; Input: HL = string address
print_str:
LD A, (HL)
OR A ; test for null
RET Z ; return when done
OUT ($81), A ; output character
INC HL
JR print_str
; Print A register as 2-digit hex
print_hex:
PUSH AF
SRL A
SRL A
SRL A
SRL A ; high nibble
CALL print_nibble
POP AF
AND $0F ; low nibble
print_nibble:
CP 10
JR C, .digit
ADD A, 'A' - 10
JR .out
.digit:
ADD A, '0'
.out:
OUT ($81), A
RET
Game Loop Architecture
game_init:
LD SP, $FFFF
LD A, 4
OUT ($42), A ; border colour
CALL clear_screen
CALL draw_playfield
CALL init_game_state
game_loop:
CALL delay ; frame timing
CALL read_input ; poll keyboard (non-blocking)
CALL update_logic ; move objects, check collisions
CALL draw_frame ; update VRAM
JP game_loop
; Non-blocking input for game loops:
read_input:
IN A, ($80) ; ACIA status
AND $01 ; test RDRF
RET Z ; no key = return
IN A, ($81) ; read key
; CP 'w' / CP 'a' / CP 's' / CP 'd' etc.
RET
15 Game Example: Snake GUI
734 bytes. Demonstrates tile graphics, keyboard input, collision detection, sound, circular buffers, and RNG. Run: z80emu_gui snake.bin
Playfield
32×24 tile grid. Walls on boundary. 30×22 playable area. Each tile = 8×8 pixels.
Controls
W/A/S/D to steer. Reverse moves rejected via XOR trick: (dir XOR new) == 2 means opposite.
Snake Buffer
Circular buffer at $2300 (page-aligned). 256 bytes = 128 max segments. (x,y) pairs. Index wraps at L register boundary.
Food
Random placement via LCG. Retries if on wall or body. Circle sprite pattern: $18/$3C/$7E/$7E/$7E/$7E/$3C/$18.
RAM Variable Map
| Address | Name | Description |
|---|---|---|
$2000 | dir_var | Current direction (0=up, 1=right, 2=down, 3=left) |
$2001 | next_dir | Buffered next direction from input |
$2002 | head_idx | Head index into circular buffer |
$2003 | tail_idx | Tail index into circular buffer |
$2004 | snake_len | Current snake length (max 120) |
$2005 | food_x | Food tile X |
$2006 | food_y | Food tile Y |
$2007 | rand_seed | PRNG state |
$2008 | grow_flag | 1 = food eaten this frame |
$2300+ | snake_buf | Circular buffer: (x, y) byte pairs |
Key Design Decisions
; Direction encoding: UP=0, RIGHT=1, DOWN=2, LEFT=3
; Reverse detection: (current XOR new) == 2 means opposite
; UP(0) XOR DOWN(2) = 2 -> reject
; LEFT(3) XOR RIGHT(1) = 2 -> reject
; All other combos != 2 -> accept
; Page-aligned circular buffer trick:
; Buffer at $2300 means H is always $23.
; L register wraps naturally at 256.
; head_idx and tail_idx are just L values.
; Advancing by 2 bytes (one segment) = INC L; INC L.
16 Game Example: Pong GUI
512 bytes. Pixel-level rendering, physics, AI opponent, wall/paddle collision, beeper sound. Run: z80emu_gui pong.bin
Ball
8×8 pixel block. Moves 1px/frame in X and Y. Bounces off walls and paddles. Resets to centre on miss.
Paddles
1 byte wide × 24px tall. Left = player (W/S, 2px/frame). Right = AI (tracks ball centre, 1px/frame).
Rendering
Erase-move-draw cycle. No full-screen clear. Walls and net drawn once at init.
Sound
440 Hz beep on wall/paddle hits. ~7ms duration via nested DJNZ loops.
Pixel-to-VRAM Address Calculation
; Convert (byte_column, pixel_row) to VRAM address
; Used for drawing vertical sprites (ball, paddles)
LD H, 0
LD L, y ; HL = pixel row
ADD HL, HL ; *2
ADD HL, HL ; *4
ADD HL, HL ; *8
ADD HL, HL ; *16
ADD HL, HL ; *32 (y * bytes_per_row)
LD E, xb ; byte column
LD D, 0
ADD HL, DE ; + column offset
LD DE, $C000
ADD HL, DE ; + VRAM base
; Draw vertical column:
LD B, height
LD DE, 32 ; row stride
.loop:
LD (HL), A ; A = $FF (draw) or $00 (erase)
ADD HL, DE
DJNZ .loop
17 Quick Reference Card
MEMORY MAP
$0000-$1FFF ROM (8K) $C000-$F7FF VRAM (6K, 256x192 mono)
$2000-$BFFF RAM (40K) $F800-$FFFF Stack (2K, grows down)
I/O PORTS
$00 R Key status ($FF/00) $40 W Beeper freq lo
$01 RW Char I/O $41 W Beeper freq hi + gate (bit 7)
$80 R ACIA status $42 W Border colour (0-15)
$81 RW ACIA data $43 W Video mode (reserved)
VRAM ADDRESSING
Base: $C000 End: $F7FF Row stride: 32 bytes
Pixel (x,y): addr = $C000 + y*32 + x/8 mask = $80 >> (x%8)
Tile (tx,ty): H = ty + $C0, L = tx 8 rows, stride 32
BEEPER
Sound ON: OUT ($40), freq_lo OUT ($41), (freq_hi & $7F) | $80
Sound OFF: OUT ($41), freq_hi & $7F
ESSENTIAL PATTERNS
Init: LD SP, $FFFF Test DE==0: LD A,D / OR E / JR NZ
Loop N times: LD B,N / DJNZ Print char: LD A,ch / OUT ($81),A
Poll key: IN A,($80) / AND 1 Block copy: LDIR (HL->DE, BC bytes)
Read key: IN A,($81) Set pixel: OR (HL) / LD (HL),A
REGISTERS
Main: AF(acc+flags) BC DE HL SP PC Shadow: AF' BC' DE' HL'
Index: IX IY (displacement: IX+d, IY+d, d = -128..+127)
Special: I(interrupt vector) R(refresh) Flags: S Z H P/V N C
COLOURS
0 Black 4 Dk Blue 8 Dk Grey 12 Br Blue
1 Dk Red 5 Dk Magenta 9 Br Red 13 Br Magenta
2 Dk Green 6 Dk Cyan 10 Br Green 14 Br Cyan
3 Brown 7 Lt Grey 11 Yellow 15 White
COMMAND LINE
Console: z80emu [-d] [rom.bin [addr]] | --test | --echo
GUI: z80emu_gui [rom.bin [addr]] (F1 = reset)
Debugger: s=step c=continue r=regs l=list m=memory b=break q=quit