Z80 emulator in Rust
After having impelemented CHIP-8 in Rust, I was interested in emulation with a more realistic system. I chose the Z80 as a next target, picking the virtual machine's commercial counterpart from the late 70s 8-bit CPUs. z80-rs is a Rust emulator for a bare-minimum Z80 computer.
Z80 is only a (micro)processor, requiring some additional hardware to compose a full computer. For required external parts, I assumed:
- A full 64kB memory map split in half with ROM below RAM
- 8-bit values read from one port are ASCII input
- 8-bit values written to another port are ASCII output
with no other external hardware.
In the case of commercial products, what emulation means becomes unclear. This emulator operates at a layer mostly abstracted, executing an interpreter loop.
- Z80 machine code is deserialized into instruction types (documented below)
- The instruction computes the subsequent machine state with output, one emulator tick per instruction.
This might be better thought of as a binary translation layer. This does miss details, of varying practicality:
- Not real-time: in reality some instruction cycles take more time than others.
- Interrupts are not considered.
- Hardware communication protocols like UART are skipped in favor of direct port reads and writes.
- Clock is unbounded, running the interpreter loop infinitely faster than real hardware clock cycles.
- Abstract execution of, say addition, misses hardware implementation details of the Z80's arithmetic circuit.
Testing
I'm not sure the name of this general testing approach, but it's an effective one I learned in a compilers course. An input directory contains integration tests expected to pass or fail, with corresponding output for the result or error.
Interactivity was tested in my following project, a Z80 Forth interpreter.
Types
Below are the types that make up the Z80 machine state and its instructions.
8-bit registers
pub enum R {
A,
F,
B,
C,
D,
E,
H,
L,
I,
R,
Ixl,
Ixh,
Iyl,
Iyh,
}
"sandwich registers", read/write 2x8-bit registers as 1x16-bit register
pub enum Sr {
Af,
Bc,
De,
Hl,
Sp,
Pc,
Ix,
Iy,
}
program counter states
pub enum Pc {
I, // increment by Pc instruction length
Im8, // 8-bit immediate
Im16, // 16-bit immediate
J(u16), // absolute
Jr(i8), // relative jump
}
indexing memory
pub enum MemAddr {
Imm, // immediate value dereference, (**)
Reg(Sr), // register value dereference, (HL), (BC), etc.
}
opcode argument with 8-bit length
//
// AND R
// |
// +------ 8-bit register argument
//
// LD R, *
// | |
// | +---- u8 immediate argument
// +------- 8-bit register argument
//
// LD (**), A
// | |
// | +- 8-bit register argument
// +------- u16 memory index
pub enum Arg8 {
U8,
Reg(R),
Mem(MemAddr),
MemOffset(Sr),
}
opcode argument with 16-bit length
//
// DEC HL
// |
// +------ 16-bit register argument
//
// LD HL, **
// | |
// | +--- u16 immediate argument
// +------- 16-bit register argument
pub enum Arg16 {
U16, /* 16 bit immediate argument */
Reg(Sr),
Mem(MemAddr),
}
opcode argument for flags
pub enum ArgF {
True, // always (jr *)
F(Flag), // conditional (jr z, *)
Nf(Flag), // opposite conditonal (jr nz, *)
}