hg80 1.0.0

Z80 and Z80N CPU core, stepped one clock edge at a time
Documentation
  • Coverage
  • 100%
    117 out of 117 items documented1 out of 1 items with examples
  • Size
  • Source code size: 1.14 MB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 1.62 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 7s Average build duration of successful builds.
  • all releases: 7s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Repository
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • CatpainBlack

hg80 — Mercury

A Z80 and Z80N CPU core, stepped one clock edge at a time.

hg80 is a hardware model, not an interpreter. It follows the shape of the T80/T80N FPGA core it is derived from: a combinational microcode decode and ALU feeding a machine-cycle and T-state sequencer, with register writes committed on the clock edge.

tick() advances one clock edge — half a T-state. That's what makes the bus edges visible at the granularity real hardware presents them. An address becomes valid, MREQ and RD assert, the data bus is latched on the falling edge, each on its own tick.

step() runs to the next instruction boundary and returns the T-states taken, for callers that don't need that detail.

Drive with step() unless you need the bus edges. The two paths cost very different amounts: tick() works at half-T-state granularity and pays for it, while step() runs an instruction at a time and is the path that has been optimised. Across the benchmark workloads — the same programs, the same T-state counts — step() costs roughly ten to thirty times less, the ratio depending on which instructions the workload runs. Absolute figures vary with the machine; the gap does not close. tick() is what a debugger or a contended-memory host needs; step() is what a machine runs on.

Using it

[dependencies]
hg80 = "1"

The core holds processor state and nothing else. Memory, ports and the signals reaching the CPU come through the Host trait, which you implement:

use hg80::{Cpu, Host};

struct Machine {
    memory: [u8; 0x10000],
}

impl Host for Machine {
    fn read(&mut self, address: u16, _at: u32) -> u8 {
        self.memory[address as usize]
    }

    fn write(&mut self, address: u16, value: u8, _at: u32) {
        self.memory[address as usize] = value;
    }

    fn input(&mut self, _port: u16, _at: u32) -> u8 {
        0xFF
    }

    fn output(&mut self, _port: u16, _value: u8, _at: u32) {}
}

let mut cpu = Cpu::new();
let mut machine = Machine { memory: [0; 0x10000] };

cpu.reset();
let t_states = cpu.step(&mut machine);

at is the transfer's position within its machine cycle, in T-states. That is what lets a host apply contention or wait states at the moment the hardware would. fetch() takes opcode fetches and defaults to read(), so override it only if you need to tell them apart.

serde is an optional feature, adding Serialize and Deserialize to the state types.

What this crate does not do

No memory, no ports, no machine — those are yours, behind Host. Nothing here knows about a Spectrum, a Next, or any other hardware, and the Z80N instructions with effects outside the CPU surface as tokens for the host rather than being acted on.

The core is no_std and #![forbid(unsafe_code)], so it brings no allocator and no platform assumptions.

Why edge granularity

Anything that watches the bus mid-instruction needs the CPU to show the same intermediate states the hardware does.

  • Contention and wait states. A host that stalls the CPU on particular addresses at particular moments can only do it if it's asked at the exact T-state of the access.
  • Cycle-exact comparison. A core that advances an edge at a time can be diffed against an HDL simulation signal for signal, instead of only at instruction boundaries.
  • Debuggers. Stepping a bus cycle, not just an instruction.

Z80 and Z80N

set_z80n_enabled() switches between a plain Z80 and the Z80N superset at run time. With Z80N off, the extended ED-prefixed opcodes decode as the no-operation forms a real Z80 gives them, so one instance can model either part.

The Z80N instructions with effects outside the CPU don't reach outside the core themselves. They surface as command tokens carrying their operand, and the host does the work. That keeps the core free of any particular machine's memory-management or register-file details.

See docs/Z80N.md for what the extended set does, measured rather than quoted.

Status

Production ready. The decode, ALU, register file and sequencer are all in place, along with the Z80N instructions and their tokens, and the core runs real software rather than test programs: it is the CPU of the Bizmuth ZX Spectrum Next emulator, written for it and maintained alongside it — a sibling project rather than a dependency picked off the shelf.

Four oracles check it:

  • Published test vectors. 1350 of 1356 pass. The other six are cases where the vectors' expected event log omits a memory read the processor really performs, or where it disagrees with the hardware about the program counter during a halt. Each is named in the harness with its reason.
  • Cycle-by-cycle against an HDL simulation. 18 programs at four wait-state depths, comparing the machine cycle, T-state, address, cycle kind, the byte written, the halt output and the interrupt enable.
  • Final register state. 185 seeded probes, each running one instruction and pushing every register pair so both sides read the result off the bus.
  • Save and restore. A core paused at any half-T-state round-trips and resumes identically.

What isn't distributed

The test vectors are GPL-2 licensed, so they aren't distributed here. That test reports itself skipped when they are absent. See CONTRIBUTING.md for how to supply them.

The HDL comparison needs the reference VHDL, which also isn't distributed here. Its results are committed, so the comparison runs either way.

Acknowledgements

hg80 rests on work other people did first.

  • Daniel Wallner, for the T80 Z80-compatible core (2001–2002, OpenCores) — the design this one is derived from, and the reason a hardware model was a reasonable thing to attempt at all.
  • The ZX Spectrum Next Project — Fabio Belavenuto, Victor Trucco, Charlie Ingley, Garry Lancaster and ACX — for the T80N modifications (2020), which are where the Z80N instructions and the Next's timing come from.
  • The Fuse emulator project, whose published Z80 test vectors are one of the four oracles this core is checked against. Nothing here is derived from Fuse: the vectors are used as expected-output data, and are not redistributed with this crate.

The T80 and T80N work is redistributed under a three-clause BSD licence — see NOTICE, which is the licence statement this section does not replace.

Licence

Licensed under either of

at your option.

This crate contains work derived from the T80 core and its T80N modifications, redistributed under a three-clause BSD licence — see NOTICE.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 licence, shall be dual licensed as above, without any additional terms or conditions.

CONTRIBUTING.md describes how to submit one, and what happens to it afterwards — which is not quite what most projects do, so it is worth reading before you spend effort.