Home Download

z80gfx Homebrew Computer

Programming Reference Manual — Z80 Assembly & Machine Code

Z80 CPU • 64 KB RAM 256×192 Mono Framebuffer 80×25 ANSI Terminal Square-Wave Beeper 16-Colour CGA Palette 6850 ACIA Serial

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

1.

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.

2.

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.

3.

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
Machine Code Breakdown

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
Key Concepts Demonstrated
  • Stack init — always LD SP, $FFFF before using CALL or PUSH
  • Terminal outputOUT ($81), A prints to the ANSI terminal panel
  • VRAM access — write directly to $C000-$F7FF for 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).

CPU
Zilog Z80 (full)
Clock Speed
~4–6 MHz eff.
Memory
64 KB flat
ROM Area
8 KB ($0000)
Video
256×192 mono
Terminal
80×25 ANSI
Sound
Square wave
Input
Keyboard (ASCII)
Serial
6850 ACIA
Palette
16 CGA colours
Frame Rate
60 FPS (vsync)
Inst/Frame
25,000

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
Source Files

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:

$0000–$1FFF
ROM — Loaded from file or built-in default8 KB
$2000–$BFFF
RAM — General purpose program & data40 KB
$C000–$F7FF
VRAM — 256×192 mono framebuffer (1bpp)6 KB
$F800–$FFFF
System / Stack — grows downward from $FFFF2 KB
Important Notes
  • 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
Why tiles are fast

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

Filter:
PortDirBuildFunction
$00RBothKeyboard status: $FF if key waiting, $00 if empty
$01RBothKeyboard character input (console: blocking; GUI: non-blocking)
$01WBothCharacter output (stdout in console, terminal panel in GUI)
$40WGUIBeeper frequency low byte (bits 0–7)
$41WGUIBeeper frequency high (bits 0–6) + gate on/off (bit 7)
$42WGUIBorder colour (palette index 0–15, low 4 bits)
$43WGUIVideo mode select (reserved — only mode 0 in v1)
$80RBothACIA status: bit 0 = RDRF (key ready), bit 1 = TDRE (always 1)
$80WConsoleACIA control register (stores value; $03 = master reset)
$81RBothACIA data input (reads keyboard queue, same as port $01)
$81WBothACIA 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

BitNameMeaning
0RDRFReceive Data Register Full (1 = character waiting)
1TDRETransmit Data Register Empty (always 1 = always ready)
2–7Reserved / 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

BitsNameMeaning
0–6Freq HiBits 8–14 of frequency (0–127)
7Gate1 = 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

NoteHzPort $40Port $41 (ON)NoteHzPort $40Port $41 (ON)
C4262$06$81C5523$0B$82
D4294$26$81D5587$4B$82
E4330$4A$81E5659$93$82
F4349$5D$81F5698$BA$82
G4392$88$81G5784$10$83
A4440$B8$81A5880$70$83
B4494$EE$81B5988$DC$83
Frequency Encoding Formula
freq_lo = Hz & $FF → port $40
freq_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:

0
Black
1
Dark Red
2
Dark Green
3
Brown
4
Dark Blue
5
Dark Magenta
6
Dark Cyan
7
Light Grey
8
Dark Grey
9
Bright Red
10
Bright Green
11
Yellow
12
Bright Blue
13
Bright Magenta
14
Bright Cyan
15
White

4 Video System (VRAM) GUI

Resolution
256 × 192
Colour Depth
1 bpp (mono)
Base Address
$C000
Size
6,144 bytes
Bytes/Row
32
Bit Order
MSB = left
Display Scale
2× (512×384)
Refresh
60 Hz

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

CodeNameAction
$07BELBell (reserved, no-op currently)
$08BSBackspace — move cursor left 1 position
$09TABTab — advance to next multiple of 8 columns
$0ALFLine feed — move down 1 row (scrolls at bottom)
$0CFFForm feed — clear entire screen
$0DCRCarriage return — move cursor to column 0
$1BESCStart escape sequence

ANSI Escape Sequences

Format: ESC [ params command where params are semicolon-separated numbers.

SequenceActionExample
ESC[row;colHMove cursor (1-indexed)ESC[1;1H = top-left
ESC[row;colfMove cursor (same as H)ESC[12;40f = centre
ESC[2JClear entire screenESC[2J
ESC[KClear cursor to end of lineESC[K
ESC[0mReset attributes to defaultESC[0m
ESC[30mESC[37mSet foreground colour 0–7ESC[31m = red
ESC[40mESC[47mSet background colour 0–7ESC[44m = blue bg
ESC[90mESC[97mSet bright foreground 8–15ESC[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

KeyCodeKeyCodeKeyCode
A–Z$41–$5Aa–z$61–$7A0–9$30–$39
Space$20Enter$0DBackspace$08
Tab$09Ctrl+C$03ESCNot 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

ModeSyntaxExampleDescription
Immediaten / nnLD A, $42Value is part of the instruction
Registerr / rrLD A, BValue 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
RelativeeJR NZ, labelPC + signed 8-bit offset (-128..+127)
Bitb, rBIT 3, ATests/sets/clears bit b in register r
I/O(n) / (C)IN A, ($80)Port address (8-bit or register C)
RestartpRST $38Call 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

InstructionOpcodeOperationFlags
LD r, r'01dddssr = r'
LD r, nxx nnr = immediate byte
LD r, (HL)01ddd110r = mem[HL]
LD (HL), r01110sssmem[HL] = r
LD (HL), n36 nnmem[HL] = n
LD A, (BC)0AA = mem[BC]
LD A, (DE)1AA = mem[DE]
LD A, (nn)3A lo hiA = mem[nn]
LD (nn), A32 lo himem[nn] = A
LD rr, nnx1 lo hirr = immediate word
LD HL, (nn)2A lo hiHL = word at nn
LD (nn), HL22 lo hiword at nn = HL
LD SP, HLF9SP = HL
PUSH rrx5SP-=2, (SP)=rr
POP rrx1rr=(SP), SP+=2
EX AF, AF'08Swap AF ↔ AF'
EXXD9Swap BC/DE/HL ↔ shadows
EX DE, HLEBSwap DE ↔ HL
EX (SP), HLE3Swap HL ↔ (SP)

Arithmetic

InstructionOperationFlags
ADD A, r/n/(HL)A = A + operandS Z H PV C
ADC A, r/n/(HL)A = A + operand + CarryS Z H PV C
SUB r/n/(HL)A = A - operandS Z H PV N C
SBC A, r/n/(HL)A = A - operand - CarryS 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 + 1S Z H PV (C unchanged)
DEC r/(HL)r = r - 1S Z H PV N (C unchanged)
NEGA = 0 - A (negate)S Z H PV N C
DAABCD decimal adjust AS Z H PV C
ADD HL, rrHL = HL + rrH C (S Z PV unchanged)
ADC HL, rrHL = HL + rr + CarryS Z H PV C
SBC HL, rrHL = HL - rr - CarryS Z H PV N C
INC rrrr = rr + 1— (no flags)
DEC rrrr = rr - 1— (no flags)

Logic & Bit Manipulation

InstructionOperationFlags
AND r/n/(HL)A = A AND operandS Z P, H=1, N=0, C=0
OR r/n/(HL)A = A OR operandS Z P
XOR r/n/(HL)A = A XOR operandS Z P
CPLA = NOT A (complement)H=1, N=1
SCFSet Carry FlagC=1, H=0, N=0
CCFComplement Carry FlagC toggled
BIT b, r/(HL)Test bit bZ (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

InstructionOperationFlags
RLCARotate A left circularC = old bit 7
RRCARotate A right circularC = old bit 0
RLARotate A left through CarryC = old bit 7
RRARotate A right through CarryC = old bit 0
RLC r/(HL)Rotate left circularS Z P C
RRC r/(HL)Rotate right circularS Z P C
RL r/(HL)Rotate left through CarryS Z P C
RR r/(HL)Rotate right through CarryS 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
RLDRotate BCD left nibbles (HL) ↔ AS Z P
RRDRotate BCD right nibbles (HL) ↔ AS Z P

Jump, Call & Return

InstructionOperationNotes
JP nnPC = nnUnconditional
JP cc, nnIf cc: PC = nnAll 8 conditions
JR ePC += signed offsetUnconditional, ±127
JR cc, eIf cc: PC += offsetNZ/Z/NC/C only
JP (HL)PC = HLJump to address in HL
DJNZ eB--; if B≠0: PC += offsetLoop counter in B
CALL nnPUSH PC; PC = nnSubroutine call
CALL cc, nnIf cc: CALL nnAll 8 conditions
RETPC = POPReturn from subroutine
RET ccIf cc: RETAll 8 conditions
RETIReturn from interruptIFF1 = IFF2
RETNReturn from NMIIFF1 = IFF2
RST pCALL pp = $00,$08,...,$38

Condition Codes

CodeMnemonicConditionCodeMnemonicCondition
0NZNot Zero4POParity Odd
1ZZero5PEParity Even
2NCNo Carry6PPositive (Sign=0)
3CCarry7MMinus (Sign=1)
JR Limitation
JR only supports conditions NZ, Z, NC, C (the first four). For PO, PE, P, M conditions, you must use JP or CALL.

I/O & Block Operations

InstructionOperation
IN A, (n)A = port[n | (A<<8)]
OUT (n), Aport[n | (A<<8)] = A
IN r, (C)r = port[BC]; sets S/Z/P flags
OUT (C), rport[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/CPIRCompare A-(HL); HL++; BC-- [repeat till match or BC=0]
CPD/CPDRCompare 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/OTIRout(BC,(HL)); HL++; B-- [repeat till B=0]
OUTD/OTDRout(BC,(HL)); HL--; B-- [repeat till B=0]
NOPNo operation (4 cycles)
HALTStop CPU until interrupt or reset
DIDisable interrupts (IFF1=IFF2=0)
EIEnable interrupts (IFF1=IFF2=1)
IM 0/1/2Set 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

InstructionsSZHP/VNC
ADD/ADC/SUB/SBC/CP/NEG***V**
AND**1P00
OR / XOR**0P00
INC***V0
DEC***V1
RLCA/RRCA/RLA/RRA00*
RLC/RRC/RL/RR/SLA/SRA/SRL**0P0*
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 IFF1 is set (interrupts enabled via EI)
  • 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 use EI before RETI in your ISR.

12 Debugger Reference Console

Launch with: z80emu -d [rom.bin [addr]]

CommandDescription
s [n]Step n instructions (default 1). Shows captured output if n ≤ 20
c / contContinue until breakpoint, HALT, or 100M-instruction limit
r / regsDisplay 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 addrSet breakpoint at hex address
bd addrDelete breakpoint
blList all breakpoints
bcClear all breakpoints
w addr valWrite byte to memory
ww addr valWrite word to memory (little-endian)
g [addr]Go: set PC (optional) and continue
pc addrSet program counter
sp addrSet stack pointer
ioToggle I/O trace (shows every OUT with port and value)
resetReset CPU (clears registers, preserves memory)
testRun 21-test CPU self-test suite
qQuit debugger
EnterRepeat 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

FieldBytesMeaning
:1Start marker
LL2Byte count
AAAA416-bit load address
TT2Type: 00=data, 01=EOF
DD...2*LLData bytes
CC2Checksum

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.py and tools/make_pong.py for 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

AddressNameDescription
$2000dir_varCurrent direction (0=up, 1=right, 2=down, 3=left)
$2001next_dirBuffered next direction from input
$2002head_idxHead index into circular buffer
$2003tail_idxTail index into circular buffer
$2004snake_lenCurrent snake length (max 120)
$2005food_xFood tile X
$2006food_yFood tile Y
$2007rand_seedPRNG state
$2008grow_flag1 = food eaten this frame
$2300+snake_bufCircular 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

z80gfx Homebrew Computer — Programming Reference Manual