npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dcorp80/z80cpu

v0.1.3

Published

The hardware-accurate Z80 CPU emulator

Readme

@dcorp80/z80cpu

For the 50th anniversary of the Z80 CPU

A hardware-accurate half-cycle Z80 emulator for JavaScript and TypeScript. Models the original NMOS Z80 (Zilog Z8400 family).

What this library is

@dcorp80/z80cpu is a low-level, hardware-oriented Z80 core for:

  • emulators
  • debuggers
  • reverse-engineering tools
  • timing-sensitive simulation

It models half-cycle edges, bus-level behavior, interrupts, WAIT/BUSREQ timing, and undocumented behavior while exposing enough state for external tooling.

Companion toolkit

If you want higher-level tooling on top of this core — an interactive debugger, a disassembler, and a REPL — head over to the z80cpu-lab sibling repo. This package is the bare CPU; z80cpu-lab is where the developer-facing experience lives.

Features

  • Half-cycle timing accuracy — every rising and falling edge is a distinct simulation step.
  • Full instruction set, including undocumented opcodes.
  • Documented and undocumented flag behavior.
  • Hardware-accurate RESET, interrupt, WAIT, and BUSREQ processing.
  • Models the Special Reset feature plus all NMOS-Z80 RESET quirks beyond the datasheet — see RESET behavior.
  • Fast: ~20 MHz effective Z80 throughput in Node.js on Apple M1.
  • JIT-friendly — no allocations on the hot path.
  • Pure ESM — works in Node and the browser, no CommonJS build.
  • TypeScript types included.
  • Introspectable — exposes internal state, upcoming sequencer step, and architectural snapshots for debugger tooling.

Validation

  • 7000+ tests, with behavior validated against the Visual6502 Z80 netlist.
  • Extensive coverage of undocumented and timing-sensitive behavior.

Install

npm install @dcorp80/z80cpu

Usage

import { Z80Cpu } from "@dcorp80/z80cpu";

const cpu = new Z80Cpu();
const mem = new Uint8Array(0x10000);
mem[0] = 0x3E; mem[1] = 0x42; // LD A,42h

const resolve = () => {
    const { nMREQ, nRD, nWR, addr, data } = cpu.bus;
    if (nMREQ === 0) {
        if (nRD === 0) cpu.bus.data = mem[addr];
        if (nWR === 0) mem[addr] = data;
    }
};

for (let i = 0; i < 16; i++) {
    resolve();
    cpu.clockEdge();
}

Each clockEdge() advances the CPU by one half-cycle (rising or falling). Run your bus resolver before each edge: read the CPU's outputs, and on a read cycle set cpu.bus.data to the value the CPU should sample.

Reading internal state

"Hardware-accurate" here refers to externally observable behavior: bus pins, instruction effects, M-cycle and T-state timing visible to the host. Internal state organization — the step pipeline, the bank-swap encoding, sub-edge sequencing details — is the emulator's own design and may differ from the real chip internals, even where external behavior matches at every edge.

That distinction matters as soon as you want to sample the registers. An instruction's writebacks don't all land at the end of the instruction itself — many register commits, plus various internal flip-flops, are written inside the M1 cycle of the next instruction, spread across multiple edges (M1_T2_1, M1_T3_1, …). The F register is the last writeback in that series, committed on the T3 falling edge of the following M1. Only once that edge has fired does cpu.regs match what the real chip's internal latches would hold.

For most instructions, the right place to read the CPU's internal state is therefore StepId.M1_T3_1 of the next instruction's M1 cycle:

  ┌────────── instruction N ──────────┐  ┌────── instruction N+1 ──────┐

  M1_T1_0 → M1_T3_0 → … → lastOpTState → M1_T1_0 → M1_T3_0 → M1_T3_1
                                                                 ▲
                                                                 │
                                                       read internal state here

    M1_T1_0       official start of the instruction; PC driven onto the address bus
    M1_T1_1       PC incremented
    M1_T3_0       opcode just latched — only now does the CPU "know"
                  which instruction it is about to execute
    …             additional M-cycles / T-states as the instruction runs
    lastOpTState  last T-state of instr N (2 half-cycles). On the bus the
                  instruction looks done, but several register writebacks
                  are still pending — they land in the next M1
    M1_T1_0       next instruction's M1 cycle begins
    M1_T3_0       next instruction's opcode latched
    M1_T3_1       T3 falling edge of the next M1. The final deferred
                  writeback for instr N — the F register — lands here;
                  cpu.regs now matches the real chip's internal latches
                  100%

Coarser markers — cpu.seq.lastOpTState or the M1_T1_0 boundary — are useful for tracing or single-stepping "between instructions", but state read at those points is still mid-writeback: the next M1 hasn't yet finished applying it. If your debugger needs exact internal-state semantics, advance to M1_T3_1 of the next M1 before sampling.

Introspection

The CPU exposes enough of itself for external observers (debuggers, visualizers, tracers) to be built without reaching into the simulator's implementation details.

Architectural snapshot

import { Z80Cpu, type CpuState } from "@dcorp80/z80cpu";

const cpu = new Z80Cpu();
// …run for a while…

const s = cpu.snapshot();           // fresh CpuState each call
console.log(s.pc, s.main.a, s.alt.b, s.im, s.iff1);

// In tight loops, allocate once and reuse:
const buf: CpuState = cpu.snapshot();
for (let i = 0; i < N; i++) {
    cpu.clockEdge();
    cpu.snapshot(buf);              // writes into buf, no allocation
}

CpuState decodes the bank-swap encoding (exAf, exx, exDe) into named main / alt register banks, flattens the IM flip-flops to 0|1|2 and IFFs to booleans. Numeric fields are plain JS numbers. Values are explicitly constrained at CPU writeback sites, so out-of-range values indicate a bug rather than runtime state. See Plain arrays below.

interface RegBank {
    b: number; c: number; d: number; e: number;
    h: number; l: number; a: number; f: number;
}

interface CpuState {
    pc: number;
    sp: number;
    ix: number;
    iy: number;
    i: number;
    r: number;
    wz: number;         // internal "MEMPTR"

    main: RegBank;      // currently-visible bank
    alt:  RegBank;      // what EXX / EX AF,AF' would expose

    im: 0 | 1 | 2;
    /**
     * `true` when the IM flip-flops are in the undocumented
     * `imFa=0, imFb=1` state — behaves as IM 0, `im` still reads `0`;
     * this flag is the only way to tell the two IM-0 states apart.
     */
    imUndocumented: boolean;

    iff1: boolean;
    iff2: boolean;      // restored to IFF1 by RETN; exposed via P/V on LD A,I/R
    nmiPending: boolean;
}

Use decodeFlags(f) to unpack an F byte into named bits when you need the individual sign / zero / half-carry / etc. flags.

halted is intentionally omitted from CpuState — the HALT state is surfaced through cpu.ctl.haltLatch (see the Hardware-level state flags table below) and the cpu.bus.nHALT pin.

Upcoming step

import { StepId } from "@dcorp80/z80cpu";

if (cpu.nextStep === StepId.M1_T1_0) {
    // about to start new instruction — a clean boundary for breakpoints, etc.
}

cpu.nextStep returns the StepId of the step the sequencer will execute on the next clockEdge(). Stable enum — the only public API for observing the upcoming sequencer step. The sequencer's internal cpu.next function pointer stays out of the public surface.

Raw state

cpu.regs, cpu.bus, cpu.seq, and cpu.ctl are all public objects. You can read any of them at any half-cycle edge.

The register file cpu.regs.file: number[] is a hardware-like flat view — hardware slot positions rather than architectural register names. The bank-swap state lives elsewhere:

  • cpu.seq.prefix — bits 0 / 1 / 2 are the exAf / exx / exDe flip-flops; bits 3 / 4 flag the IX / IY substitution active for the current DD / FD prefix.
  • cpu.seq.exDe0 and cpu.seq.exDe1 — the per-bank DE↔HL swap flip-flops (multiplexed onto bit 2 of prefix by exx).

For most consumers, cpu.snapshot() is the right answer — it decodes all of that into named main / alt register banks. Going through regs.file directly is for code that needs to track or reproduce the bank-routing logic itself (e.g. a netlist comparator).

Hardware-level state flags

A few internal flip-flops are useful for instrumentation:

| Field | Meaning | |--------------------------------|----------------------------------------------------------------------------------------------------------------------------| | cpu.seq.lastOpTState | true during the last T-state of the current instruction — a convenient point to log state or single-step execution. | | cpu.seq.phase | Clock phase, matches CLK: true after a rising edge (CLK high), false after a falling edge (CLK low). | | cpu.ctl.haltLatch | Internal HALT state, set by the HALT instruction and cleared by RESET / INT / NMI. Distinct from the nHALT output pin. | | cpu.ctl.nres | Normal-reset latch — set when RESET was sampled, cleared once recovery completes. | | cpu.ctl.sres | Special-reset flip-flop — set when RESET is sampled during M1 T2. See RESET behavior section below | | cpu.ctl.iff1, cpu.ctl.iff2 | Interrupt-enable flip-flops (also surfaced in CpuState). | | cpu.ctl.nmiFf | NMI pending — set by cpu.nmi(), cleared at the NMI M1. Surfaced as nmiPending in CpuState. |

Bus and signals

The CPU exposes its pins through cpu.bus. The host advances time by calling cpu.clockEdge() — that call is one half-cycle of CLK, so there's no nCLK pin on the bus. Bus signals are exchanged between those edges.

Active-low convention throughout: 0 = asserted, 1 = released. The transaction signals (MREQ, IORQ, RD, WR) tri-state represented as undefined, therefore typed 0 | 1 | undefined. The address bus (number | undefined, 16-bit) and data bus (number | undefined, 8-bit) tri-state alongside them. The status signals (M1, RFSH, HALT) and BUSACK stay driven throughout — typed 0 | 1.

cpu.bus.nM1     // 0 during M1 fetch
cpu.bus.nMREQ   // memory request; undefined while granted
cpu.bus.nIORQ   // I/O request; undefined while granted
cpu.bus.nRD     // read strobe; undefined while granted
cpu.bus.nWR     // write strobe; undefined while granted
cpu.bus.nRFSH   // refresh (DRAM refresh during M1 T3/T4)
cpu.bus.nHALT   // HALT acknowledged on the pin
cpu.bus.nBUSACK // bus-grant acknowledge
cpu.bus.addr    // 16-bit address bus; undefined while granted
cpu.bus.data    // 8-bit data bus; CPU drives during writes, host fills during reads

Inputs are typed 0 | 1 — the host sets them, the CPU samples them at the modeled sampling edges:

cpu.bus.nRESET  = 0;  // assert RESET
cpu.bus.nINT    = 0;  // level-triggered interrupt request
cpu.bus.nWAIT   = 0;  // stretch the current memory / IO cycle
cpu.bus.nBUSRQ  = 0;  // request a bus grant

NMI is the exception. The real Z80 NMI pin is edge-triggered — a falling edge latches a request internally. To model that faithfully through a level pin you'd need the host to toggle it and the CPU to detect the transition, which adds friction in a synchronous emulator. Instead, NMI is a method:

cpu.nmi();

One call = one edge = one latched NMI, serviced at the end of the current instruction. Calling again while a previous NMI hasn't been acknowledged is a no-op.

RESET behavior

The reset path has more nuance than typical references suggest:

Special Reset

A designed but historically obscure feature. If RESET is sampled during M1 T2, the CPU performs a partial reset: only PC is cleared to 0, while registers and most internal state are preserved — not a full reset. Documented by Tony Brewer at primrosebank.net. Surfaced as cpu.ctl.sres.

Beyond-the-datasheet quirks

The Z80 datasheet warns that if RESET is sampled during T2 or T4 of a cycle, MREQ may become indeterminate for one T-state shortly afterward. In systems with dynamic RAM this can potentially corrupt memory contents, which is why the datasheet recommends synchronizing RESET with the falling edge of M1 when RAM must be preserved.

The transistor-level netlist suggests broader behavior:

  • Sampled on the last T-state of any M-cycle, not only T2 or T4.
  • RD also becomes indeterminate, alongside MREQ, during the next M1 T1.
  • A spurious RD low pulse appears on the following reset recovery edge.

These effects may be netlist artifacts rather than real silicon behavior, but are modeled for completeness.

Plain arrays inside the CPU

The CPU's internal register state — cpu.regs.file, cpu.regs.f — is plain number[]. Uint8Array is intentionally avoided here.

Why: V8 specializes object/array shapes by element-kind. Mixing typed arrays with number[] on the same hot path tends to introduce element-kind transitions ("type churn") that defeat inline caching. Sticking to SMI-tagged 32-bit integers in plain arrays through the register file keeps everything on one fast path.

Host-side storage is the host's choice. The bus resolver runs outside the CPU — it can back memory and I/O ports with Uint8Array, Uint16Array, plain number[], or anything else that responds to addr and yields a number.

Side effect of the plain-array choice: register values aren't automatically truncated to 8/16 bits — the CPU does that explicitly at every writeback site, and cpu.snapshot() trusts that invariant. Consumers reading raw state shouldn't need to mask either, as long as the CPU stays correct.

License

Apache-2.0 — see LICENSE and NOTICE.

Acknowledgements

Inspired by the Visual6502 and VisualZ80 Remixed projects.