Being a professional developer and being fascinated with all things retro tech & Z80, I have flirted with the idea of writing a Z80 emulator for a long time. It has been 47 years since I was first exposed to my first Z80 computer and I know itโs already been done a thousand times over however, for me itโs about the challenge of learning and understanding Z80 architecture to a highly granular level and writing it in a language I havenโt used in โangerโ for almost 30 years, C++. Needless to say, things have changed a little and the days of Borland Turbo C++ are long gone. So anyway, Iโve started this project and I plan on documenting progress as I go; both the progress of the emulator and my โrediscoveryโ of C++!
A from-scratch Z80 CPU emulator written in modern C++20.
This project is a structured learning exercise in emulator development, focusing first on correctness, determinism, and architectural clarity before performance or hardware accuracy.
The goal is not to rush to a full machine emulator, but to build a solid, extensible CPU core with proper timing (T-state based), clean separation of concerns, and strong debugging visibility.
A slightly obsessive, thoroughly geeky, modern C++20 Z80 emulator.
This isnโt a โletโs throw something togetherโ emulator.
Itโs a learn it properly, build it cleanly, test it properly, understand every opcode kind of emulator.
And yesโฆ itโs being built because the Z80 is awesome.
This project is:
- A ground-up Z80 emulator written in modern C++20
- Built with CMake
- Test-driven using Catch2
- Structured to be clean, readable, and extendable
- A learning vehicle as much as a functional emulator
This project is not:
- A copy-paste from an existing emulator
- A โgood enoughโ hack
- A speed-optimised black box
The goal here is understanding. Every instruction. Every flag. Every tick.
The emulator is deliberately split into simple, clear layers.
The CPU class handles:
- 8-bit and 16-bit register pairs (AF, BC, DE, HL)
- Flag manipulation via helper functions
- Instruction decoding via switch dispatch
- Opcode family grouping (e.g.
LD r,r,INC r,DEC r) - Clean helper functions for arithmetic (
inc8,dec8, etc.)
The aim is clarity over cleverness.
If something looks โtoo magicโ, it probably gets rewritten.
The bus abstracts memory access.
The CPU connects to the bus and performs:
- Instruction fetch
- Memory reads
- Memory writes
This separation makes unit testing clean and predictable.
Each step:
- Fetch opcode from memory (PC)
- Decode via switch statement
- Execute instruction
- Update PC
- Update flags as required
No macro trickery. No hidden state mutation.
LD BC,nn(0x01)LD DE,nn(0x11)LD HL,nn(0x21)LD SP,nn(0x31)
LD r,n- A, B, C, D, E, H, L
LD r,r
INC r(including(HL))DEC r(including(HL))
-
INC BC(0x03) -
INC DE(0x13) -
INC HL(0x23) -
INC SP(0x33) -
DEC BC(0x0B) -
DEC DE(0x1B) -
DEC HL(0x2B) -
DEC SP(0x3B)
ADD A,r(0x80โ0x87)ADD A,n(0xC6)ADC A,r(0x88โ0x8F)SUB r(0x90โ0x97)SBC A,r(0x98โ0x9F)
AND n(0xE6)OR n(0xF6)XOR n(0xEE)CP n(0xFE)
ADD HL,BC(0x09)ADD HL,DE(0x19)ADD HL,HL(0x29)ADD HL,SP(0x39)
-
PUSH BC(0xC5) -
PUSH DE(0xD5) -
PUSH HL(0xE5) -
POP BC(0xC1) -
POP DE(0xD1) -
POP HL(0xE1)
JP nn(0xC3)JP (HL)(0xE9)
JR e(0x18)JR NZ,e(0x20)JR Z,e(0x28)JR NC,e(0x30)JR C,e(0x38)
CALL nn(0xCD)RET(0xC9)
All control-flow instructions:
- Correct little-endian handling
- Proper signed displacement behaviour
- Accurate stack interaction
- No unintended flag side-effects
SCF(0x37)
- 76+ passing tests
- ALU flag behaviour verified
- 16-bit half-carry verified
- Stack byte order verified
- Stack pointer movement verified
- Control flow and signed displacement verified
The emulator now has:
- Core 8-bit ALU functionality
- 16-bit arithmetic
- 16-bit register increment/decrement
- Functional stack primitives
- Verified flag behaviour for implemented instructions
cmake -S . -B out/build
cmake --build out/build
ctest --test-dir out/buildIf all goes well, youโll see a nice wall of passing tests.
If notโฆ thatโs why we have tests.
The foundation is now solid.
What remains is depth, completeness and hardware accuracy.
RET NZ,RET Z,RET NC,RET CCALL NZ,nn,CALL Z,nn,CALL NC,nn,CALL C,nn
Completes flag-driven subroutine branching.
DJNZ e
Classic Z80 tight-loop instruction (decrement B and branch).
JP NZ,nnJP Z,nnJP NC,nnJP C,nnJP PO,nnJP PE,nnJP P,nnJP M,nn
Extends conditional branching beyond relative forms.
CCF(Complement Carry Flag)CPL(Complement Accumulator)DAA(Decimal Adjust Accumulator โ BCD correctness)
These tighten full flag compliance.
RLCARRCARLARRA
Flag-sensitive and timing-sensitive.
RLC,RRC,RL,RRSLA,SRA,SRLBITSETRES
Large instruction family.
Major milestone once complete.
- Block transfer (
LDI,LDIR,LDD,LDDR) - Block compare (
CPI,CPIR, etc.) - Extended arithmetic
- I/O instructions (
IN,OUT) - Interrupt mode control (
IM 0/1/2) RETI,RETN
Brings the CPU closer to real-world software compatibility.
- IX / IY register support
- Indexed addressing with displacement
- CB-prefixed indexed bit operations
This significantly increases decoder complexity.
IN A,(n)OUT (n),A- Register-based port I/O
- Block I/O (ED-prefixed)
Required for real hardware emulation.
DI,EI- Interrupt modes 0 / 1 / 2
HALT- I and R registers
- Interrupt acknowledge cycle modelling
Essential for CP/M and full system behaviour.
- Per-instruction T-state modelling
- Taken vs not-taken branch timing differences
- Prefix timing penalties
- Optional memory contention modelling
- Accurate HALT cycle behaviour
Moves from logical correctness to hardware realism.
- ROM/RAM region separation
- Write-protected ROM
- Configurable memory map
- ROM image loading
- Bank switching (future)
- Integration test harness for real programs
- Execute small hand-written assembly programs
- Run test ROM suites
- Eventually boot CP/M
- Add minimal peripheral layer
- Possibly build a simple Z80-based virtual machine
- Proper T-state timing model
- Interrupt handling
- Stack operations
- Jump / Call / Return instructions
- Real program execution
- CP/M experiments
- Possibly memory-mapped peripherals
- Something mildly ridiculous
This emulator grows incrementally.
Each session:
- One instruction group
- Full flag correctness
- Tests first
- Everything green before moving on
No rushed megacomits. No โweโll fix flags laterโ.
Because writing an emulator is one of the best ways to truly understand:
- CPU architecture
- Binary execution
- Instruction decoding
- Timing behaviour
- Clean software structure
And because the Z80 deserves it.
......Itโs 50 years old.
......Itโs elegant.
......Itโs still everywhere.
......And itโs a joy to implement properly.
If you're building your own emulator โ welcome.
If you love the Z80 โ even better.