Skip to content

Repository files navigation

Zilog Z80

A Z80 you can drive from a clock, held to Zilog's own manual for the shape of every machine cycle and to a per-opcode suite for every T state of every opcode.

CI

3 parts, 1,604,000 conformance cases and 22,005,372 T states compared, 0 failures, 1,305 tests, 100% statement and branch coverage, no dependencies

from z80 import Cpu, Memory

cpu = Cpu("z80", Memory(image=bytes([0x3E, 0x42, 0x47])))
cpu.reset()
cpu.registers.pc = 0x0000

cpu.step()
cpu.step()

print(f"{cpu.registers.b:02X}")
42

Install

pip install git+https://github.com/gufranco/zilog-z80-python.git

Python 3.12 or newer. Nothing else.

The interface

Everything a caller touches. Nothing else is public.

Call Does Returns
Cpu(model, memory=None, **options) Builds a part, powered and not yet reset. The model is required and there is no default; naming none raises and lists every model. Memory of its own if none is given a Cpu
cpu.reset() Drives RESET. Costs five T states: the three Zilog names as the minimum the pin must be held, and the two the part spends after it releases the Cpu
cpu.step() Runs one instruction T states it cost
cpu.run_for(cycles) Runs whole instructions until at least that many T states have passed T states actually spent, usually a little over
cpu.run_until(check, limit=None) Steps while check(cpu) is false. limit bounds the instructions and raises RunLimit the Cpu
cpu.held() Whether the part has stopped advancing the program bool
cpu.irq(vector=0xFF) / cpu.nmi() Offers a line and acts on it now. vector may be a callable, which is how a device supplies an instruction longer than one byte in mode zero True if taken / nothing
disassemble(data, address) Reads bytes with no machine to run them in Instruction objects with .text
MODELS Every model this package covers, by the name it goes by a mapping
Pin or attribute Is
cpu.irq_line The request line as a level. Read at the final T state of an instruction, where the manual says the part reads it, so a request withdrawn before then is not taken
cpu.nmi_line The non-maskable line. Edge sensitive: the transition interrupts, and holding it afterwards does not interrupt again
cpu.wait_line Memory asking for more time. Read after T2 of each machine cycle, and every state it adds repeats T2
cpu.cycles / cpu.steps T states since construction, across resets; instructions since the last reset
cpu.halted Whether a HALT is being executed, which still costs four T states a time
cpu.registers a, f, b, c, d, e, h, l and the pairs af, bc, de, hl; sp, pc, ix, iy with their halves; the shadow set as af_, bc_, de_, hl_; i, r, iff1, iff2, im; and wz and q, the two nobody documents
cpu.bus The T states of the last instruction, and the recorded cycles when recording=True
cpu.on_cycle Called once per T state, after that state's bus activity

Options: seed= fixes the undefined state, recording=True keeps a bus log, shape= picks which edge a pin is read on, ports= takes an I/O bus.

A part arrives powered, not reset, because no board hands over one that has reset itself. Every register holds rubbish derived from the seed, the program counter included, so stepping it executes rubbish from a rubbish address. Call reset() to get a machine that runs a program.

Running it at a real speed

A part runs at whatever its crystal says. step() reports what an instruction cost, so a host can hold the part to a real clock.

import time

from z80 import Cpu

HERTZ = 3_546_895
SLICE = 0.02

cpu = Cpu("z80")
cpu.reset()
per_slice = round(HERTZ * SLICE)
owed = 0

for _ in range(5):
    began = time.perf_counter()
    owed += per_slice
    owed -= cpu.run_for(owed)
    time.sleep(max(0.0, SLICE - (time.perf_counter() - began)))

An instruction cannot be cut in half, so run_for() overshoots and returns what it really spent. Carrying the overshoot into the next slice is what stops a long run drifting.

Driving it one T state at a time

Clock stops the part between any two T states, which is where a board changes what a read will answer.

from z80 import Clock, Cpu, Memory

space = Memory(image=bytes([0x3E, 0x42, 0x00, 0x00, 0x00, 0x00]))
cpu = Cpu("z80", space, recording=True)
cpu.reset()
cpu.registers.pc = 0x0000

with Clock(cpu) as clock:
    clock.tick()
    space.write8(0x0001, 0x99)
    clock.run_for(6)

print(0x99 in [value for _, value, _ in cpu.bus.log])
True

The instruction picked up a byte written after it had already begun. That is real suspension rather than a replay, and it is what makes the three pins above mean anything.

It is not free. An instruction is an ordinary call stack and Python cannot suspend one, so the clock runs the part on a thread and lets it block where the T state is spent, which is what ares and bsnes do. Expect roughly fifty times slower than step(). Use step() for speed and Clock when the question is where a T state falls.

Models

The instruction set never changed. Three things did, all undocumented or defective, which is why software that depended on any of them had to know which board it was on.

Build it with Bare OUT (C) sends Carry flag bits Interrupt clears parity These three columns measured
Cpu("z80") nothing accumulator and latch yes, and Zilog documents it yes, by the corpus
Cpu("z84c00") every bit accumulator and latch no, Zilog fixed it no, Zilog's own sentence
Cpu("upd780c") nothing accumulator alone not stated no, independent research

The last column is narrow on purpose. One corpus exists, for the NMOS part, and everything the other two share with it is measured through it. What is not measured is the handful of behaviours that make them different parts, which is the three columns before it.

Each answers to the numbers its manufacturer sold it under. Case and separators do not matter.

Build it with Also answers to
Cpu("z80") z8400, nmosz80, z0840004psc, z0840006psc, z0840008psc, mostekmk3880, mk3880, mk3880n, sharplh0080, lh0080, lh0080a, u880, ud880d, kr1858vm1, t34vm1, mme, goldstargms z80, thesysz80
Cpu("upd780c") necupd780c, d780c, d780c1, d780c2, upd780, upd780c1, upd780c2
Cpu("z84c00") cmosz80, z80c, z8400c, z84c0006, z84c0008, z84c0010, z84c0020, toshibatmpz84c00, tmpz84c00, t84c00, kr1858vm3

A part number nothing here implements is refused rather than resolved to something close, so Cpu("z180") raises UnknownModelError instead of handing back a Z80 missing instructions the caller asked for.

Reading without running

A survey of a ROM has nothing but the file, so reading and running are separate halves.

from z80 import disassemble

for found in disassemble(bytes([0x3E, 0x42, 0x47, 0xC9]), 0x8000):
    print(f"{found.address:04X}  {found.text}")
8000  ld a,$42
8002  ld b,a
8003  ret

A run of bytes too short to complete its instruction raises Truncated rather than returning a guess.

Nothing starts clean

Memory and registers hold a reproducible scrambled pattern. There is no parameter that clears them and there will not be one: a read of a byte nothing wrote is a defect on real silicon, and memory that answers zero turns that defect into a passing test.

from z80 import Cpu, SparseMemory

print(hex(SparseMemory().read8(0x1234)))
print(SparseMemory().read8(0x1234) == SparseMemory().read8(0x1234))

powered = Cpu("z80", SparseMemory())
print(hex(powered.registers.pc), powered.cycles)
0x84
True
0x8926 0

A byte derived from the address, the same every time, and not zero. The part has spent nothing because nothing has driven RESET yet.

Is it right

Every instruction is checked against a published per-opcode suite that states each register and each byte of memory before and after: 1,604,000 cases, no failures. The comparison then goes further and checks what the part put on the bus, T state by T state, address by address, pin by pin: 22,005,372 T states, no failures.

python3 -m conformance.fetch ~/.cache/conformance-suites
python3 -m conformance.singlestep ~/.cache/conformance-suites/z80/v1
python3 -m conformance.cycles ~/.cache/conformance-suites/z80/v1

The suite commit is pinned so a build is reproducible, and a weekly job runs against whatever upstream holds now and opens a pull request or an issue. A runner reports what it checked rather than a bare pass, because a run that parsed nothing and found no failures exits zero and looks identical to one that checked everything.

Where the manual and the recordings disagree, both are kept. conformance/hardware.json holds every fact taken from a document with the sentence it came from and the page. conformance/divergences.json holds every place two sources part, with what would settle it. That reading found four places where the manual contradicts itself, three of them in its own timing tables.

Asking the die

A document says what the part does and a recording says what one part did. Neither says what is inside the package, and most of the questions left over are about what is inside.

conformance/netlist.py runs the part as a net of transistors, from a netlist extracted from die photographs. Driven from reset it fetches from 0x0000, executes, and comes to rest on every clock edge.

python3 -m conformance.netlist

The resolver follows chipsim.js and wires.js from the Visual 6502 project, which are MIT licensed; the notice they ask for is in THIRD-PARTY-NOTICES.md. The three data files are not carried here. conformance/netlist.manifest.json names them, says where they come from and records what each one hashes to; put them in docs/independent/visual6502 and a load refuses anything that is not what was read.

Two things had to be established here rather than taken from anyone. Thirty-two entries in the transistor file are pull-ups rather than transistors, and loading them is the difference between a netlist that comes to rest and one that never does. And the doubled net names hold the register set an instruction writes, while the single-letter ones hold the shadow set, which is the opposite of what the names suggest; that was measured by executing LD r,n across every register and every bit position and watching where the value landed.

Nothing in this package is held to what the netlist says. It sits below the manufacturer's documents and below a recording taken off a real part, because a netlist is an extraction and an extraction can be wrong.

Seventeen questions remain where being faithful is a claim rather than a measurement, and each names the measurement that would close it: OPEN-QUESTIONS.md. Some cannot be closed by anyone. Bits 3 and 5 of the flag register have gone undocumented through eleven revisions in forty years, and the internal register the recordings call WZ appears nowhere in 780 pages of Zilog's own paper.

Working on it

python -m coverage erase
for file in $(find z80 conformance -name '*.test.py' | sort); do
  python -m coverage run -a "$file"
done
python -m coverage report

python3 z80/doctor.py says what is actually on this machine: the parts, what makes each one different, and whether the suites this repository cannot carry are fetched and whole. It is run as a file rather than with -m so that it still runs when the package itself will not import, which is the case it exists for. Its report is what an issue asks for, because a report is only as good as what it says about the machine that produced it.

Tests sit beside the module they cover, named <module>.test.py. Coverage is 100% of statements and branches, enforced. Types are mypy at strict. Commits follow Conventional Commits, and releases are cut by semantic-release.

AGENTS.md is the document for an agent working here. FAMILY.md is the standard this repository shares with mos65xx-python, kept identical in both.

z80/
  core.py          the processor
  bus.py           machine cycles, and the pins each T state carries
  clock.py         driving it one T state at a time
  models.py        the three parts, by name and alias
  memory.py        memory that holds what it held
  opcodes.py       the opcode table and a disassembler
  registers.py     the register file, shadow set included
conformance/
  suites.json      which corpus, at which commit
  singlestep.py    running it, state by state
  cycles.py        running it, T state by T state
  hardware.json    what Zilog printed, fact by fact
  divergences.json where sources part

References

This repository carries no documents. Every claim is traced to something published elsewhere, listed here so a reader can fetch the same file and check the same page. Each row gives the page count and the first sixteen characters of the file's SHA-256, because vendor links move and a link that has rotted into a different revision is easy to follow without noticing. Compute the full digest with shasum -a 256 <file>.

Every manufacturer document below is copyrighted and not redistributable, which is why none is in this repository. Individual sentences are quoted in conformance/hardware.json with the page they came from.

Document Date Pages SHA-256 Redistributable
Zilog, Z80 CPU User Manual, UM008011-0816 2016-08 332 e3c83da5a5d8e372… No
Zilog, Z80 Family Data Book, 00-2490-01 1989-01 448 844681b63ffc45bd… No
Zilog, Z84C00 Product Specification, PS017801-0602 undated 36 06198d3c22a79a3f… No
NEC, µPD780C data sheet undated 24 2036fa845533feee… No

Independent research, used only where two lineages that never consulted each other agree, and never as a citation for a figure a manufacturer gave.

Document Author Pages SHA-256 Licence
The Undocumented Z80 Documented, v0.91 Sean Young, 2005-09-18 52 6413048f39c2e735… GFDL 1.1 or later
Z80 CCF SCF Outcome Stability Sainz de Baranda y Goñi, Brewer, Helcmanovsky 4 be87311012f9edaf… GFDL 1.3
Undocumented Z80 Flags, rev 1.0 David Banks, 2018-08-21 3 33766df5494e2fdf… None stated
MEMPTR, esoteric register of the ZiLOG Z80 CPU Boo-boo, trans. Vladimir Kladov text f9e8e87cdd205e15… None stated
redcode/Z80 wiki: Interrupts and MEMPTR Sainz de Baranda y Goñi and contributors web n/a GFDL 1.3
Source Used for
SingleStepTests/z80 The pinned corpus, 1,604,000 cases. Commit in conformance/suites.json
raddad772/jsmoo The generator that produced it, so it can be rebuilt rather than only downloaded
gdevic/Z80Explorer The netlist whose behaviour is recorded in conformance/divergences.json

Fetching them is a command rather than an exercise. conformance/documents.json carries the full digest and the byte count for each, and an address where one is recorded, and conformance/documents.py brings down what can be fetched into docs/, which git ignores, and refuses anything whose digest does not match.

python3 -m conformance.documents          # fetch what can be fetched and verify every digest
python3 -m conformance.documents --check  # verify what is already here

Four of the eight have no address recorded anywhere here, and three more were printed from a web page rather than downloaded, so a fetch would replace them with markup. The manifest marks each of those and verifies them instead of replacing them.

The MEMPTR paper is pinned above as text and the copy in that folder is a print of the same writing, so the two have different digests. Neither is the other, and checking one against the other's digest finds a mismatch that means nothing.

Citing this

CITATION.cff is kept in step with the released version by the same script that stamps the package, so the version it names is the version that shipped. GitHub renders it as a Cite this repository button.

License

MIT

About

Zilog Z80 CPU emulator in Python, cycle accurate to the T state. Held to Zilog's own manual for every machine cycle and to a per-opcode suite for every T state: 1,604,000 cases and 22,005,372 T states compared, zero failures. Three parts, NMOS and CMOS, 100% branch coverage, no dependencies

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages