3.1 CPU¶
The Next runs a Z80, and every program it will ever run is a stream of Z80
instructions. That makes the CPU the emulator's metronome as much as its
processor: Emulator executes one instruction, then hands every other subsystem
the T-states that instruction consumed, and everything else — video, audio,
timers, DMA — advances by that amount. Nothing in JNEXT moves without the CPU
moving first.
It is not quite an ordinary Z80, though. The FPGA implements a soft core called
T80N: a Z80 with an extra instruction set of its own (Z80N), a clock that
software can switch between 3.5, 7, 14 and 28 MHz through NextREG 0x07, and an
interrupt fabric with fourteen prioritised sources instead of the single /INT
line a 48K Spectrum has. JNEXT reaches memory through Mmu and I/O through
PortDispatch, both behind abstract interfaces, and calls back out at
instruction-fetch time so that DivMMC paging, the NMI state machine and the
Multiface window can settle before the byte they affect is read.
src/cpu/ holds the Z80Cpu wrapper (z80_cpu.h/.cpp), the Z80N extension
opcodes (z80n_ext.h/.cpp), the IM2 interrupt fabric (im2.h/.cpp) and the
small Im2Client facade peripherals use to reach it. The CTC is not here
despite driving interrupts — it lives in src/peripheral/ctc.h/.cpp.
The wrapper and the backend¶
The rest of the emulator only ever sees Z80Cpu. No FUSE type appears in
z80_cpu.h, and that is the point: swapping the backend would mean rewriting
z80_cpu.cpp and nothing else. Z80Cpu reaches the machine through two
abstract interfaces declared in that header — MemoryInterface and
IoInterface — implemented by Mmu and PortDispatch.
The backend itself is the FUSE Z80 core, vendored under third_party/fuse-z80/
from FUSE 1.6.0 (GPLv2-or-later, compatible with JNEXT's GPLv3).
fuse_z80_core.c is a single translation unit that #includes the opcode
files, while fuse_z80_shim.h stubs out every FUSE emulator dependency those
files would otherwise pull in. The core is inherently single-instance — it keeps
one global z80 struct and one global tstates counter — which happens to suit
JNEXT's one-CPU design. Z80Cpu re-points the file-static s_mem / s_io
pointers on every reset() and execute(), and copies the register file in and
out around each instruction.
Z80Registers carries the architectural registers plus MEMPTR, FUSE's Q latch
and IncDecZ. That last one is a one-bit shadow that exists only in the VHDL
(t80n.vhd:1358-1367), where it becomes observable through the Z80N LDWS
instruction. It cannot be read out of FUSE at all, so execute() reconstructs
it after the fact for DJNZ, INC BC / DEC BC and the ED block transfers,
walking any DD/FD prefix chain first because those prefixes do not change which
mcode path runs.
The Z80N instruction set¶
Z80N is the Next's own extension to the Z80. Every one of its instructions is
encoded behind the ED prefix, in slots a real Z80 leaves undefined and quietly
executes as a two-byte NOP, so Z80N code is harmless on a plain Spectrum and
does real work on a Next. The additions exist to make cheap the things a Z80 is
bad at: MUL D,E is an 8×8→16-bit multiply, the BSLA / BSRA / BSRL /
BSRF / BRLC family barrel-shifts DE by B, PIXELAD converts a (Y, X)
coordinate pair in DE into the corresponding ULA screen address in HL while
PIXELDN steps that address down one pixel row, NEXTREG nn,n writes a NextREG
directly from an opcode, and the LDIX family copies blocks while treating one
byte value as transparent. This is not an optional corner of the machine —
NextZXOS itself uses NEXTREG, MUL and the ADD rr,A forms constantly.
Interception¶
execute() reads the byte at PC itself. On 0xED it reads the extension byte
and consults kZ80NOpcodeTable, a 256-entry table built at static-init from the
Z80NOpcode enum. A hit dispatches to execute_z80n() and never reaches FUSE,
which would otherwise decode it as a NOP; a miss falls through untouched.
Two encodings are checked before that table: ED FF and DD 01, the two magic
breakpoints. A magic breakpoint is an instruction a program plants in its own
code to stop the emulator at that exact spot and hand control to the debugger —
the two encodings are the ones ZEsarUX and CSpect established, and JNEXT honours
both. They only bite when --magic-breakpoint is given; otherwise they execute
as on real silicon: ED FF as a two-byte NOP, DD 01 nn nn as LD BC,nn
(the DD prefix ignored).
Z80N instructions account for their own time rather than quoting a table
constant. The wrapper charges the two M1 fetches via contend_read(),
execute_z80n() charges every operand access through the same FUSE bus
callbacks the standard opcodes use, and the wrapper returns the elapsed
tstates delta. NEXTREG nn,n and NEXTREG nn,A route through
IoInterface::nextreg_opcode_write() instead of writing ports 0x243B/0x253B,
because the opcode must not disturb the port-0x243B select latch: a following
IN A,(0x253B) still has to read whatever that port last selected.
Bus callbacks and contention¶
On a 48K or 128K Spectrum the CPU and the ULA share the same RAM, and the ULA
wins. While it is fetching display bytes it holds the CPU off the bus, so an
instruction that touches contended memory takes longer than the data book says —
and by an amount that depends on where the raster beam happens to be. Software
can and does measure this, which is why contention has to be modelled rather
than approximated. On the Next it is a compatibility feature: present under 48K,
128K and +3 timing, absent under Pentagon timing, switchable off outright
(NR 0x08 bit 6), and simply outrun at any CPU speed above 3.5 MHz. The gate
itself lives in ContentionModel — see 3.2 Memory — and this
section is about how the CPU calls into it.
fuse_z80_readbyte, fuse_z80_writebyte, fuse_z80_readport and
fuse_z80_writeport are the extern "C" functions the FUSE macros expand to;
they dispatch to s_mem / s_io and add the base FUSE timing.
Contention is the subtle part, and it is not applied where you might expect.
There is no per-instruction contention hook on Z80Cpu — there used to be an
on_contention callback, but Emulator::init() only ever assigned it
nullptr and it has been removed — and no pre-computed table is ever
consulted either. Every bus cycle instead calls
ContentionModel::contention_tick(), a per-cycle transcription of the VHDL
gate. There are seven such call sites: the four callbacks above, plus the three
FUSE in-opcode macros contend_read, contend_read_no_mreq and
contend_write_no_mreq. Those three become extern calls only because
src/cpu/CMakeLists.txt compiles the FUSE translation unit — and only that one
— with -DCORETEST, which flips the macros in z80_macros.h from table lookups
into overridable functions.
Each site first tests contention_possible() (NR 0x08 bit 6, NR 0x07 CPU speed,
Pentagon timing), so the raster arithmetic is skipped entirely when contention
cannot fire. When it can, derive_hc_vc() turns the FUSE tstates counter into
a raw frame position and to_ula_counters() rebases that onto the ULA's own
display-relative counters. The rebase is not a nicety: the VHDL gate is written
against i_hc/i_vc, which reset at the start of the active display rather
than at the start of the frame, so feeding it raw coordinates would contend the
top border while missing the bottom 64 display lines. The runtime is installed
by z80_set_contention_runtime(); with it null, all seven sites are inert.
The two port callbacks charge the I/O cycle clock by clock — T1, the automatic
wait, T2, T3 — each clock's stretch from ContentionModel::io_clock_tick()
(zxula.vhd:587-595): the port's page can stretch any clock, the port
decode only the second, and a port-contended cycle stops stretching after it.
fuse_z80_readport calls the port handler in the last clock, after every
stretch, because the T80 latches the data bus only on the falling edge of T3;
fuse_z80_writeport calls it at the start of the second clock, because a write
strobe acts as soon as IORQ and WR go low. The order matters to any read whose
value depends on time — the floating bus, NR 0x1E/0x1F, the tape EAR bit — and
Z80Cpu::io_clock_into_instruction() tells those reads where each clock of the
cycle began, stretches included.
derive_hc_vc() only gives the right answer if the FUSE counter and the master
clock agree: counter × CPU divisor = clock − frame start, at every instruction
boundary. Emulator::rebase_fuse_tstates_() re-establishes that where the
counter's origin or unit changes — the frame start, which seeds it with the
last instruction's overshoot past the frame end, and an effective CPU-speed
change — and advance_fuse_tstates_() moves it with the clock when the clock
advances with no CPU instruction (DMA holding the bus, a parked CPU, a tape
trap's synthetic cycles).
A separate stretch is the 28 MHz SRAM read wait (zxnext.vhd:3171-3181): at CPU
speed 3, a memory read cycle costs one extra T-state when it raises the SRAM
request or schedules a bank-5 read. Mmu::sram_read_wait28() decides this in the
read's own overlay order (Multiface, DivMMC, an engaged Layer 2 mapping, then the
slot). An engaged Layer 2 mapping decides alone: it waits on an active page and
does not on an inactive one, whatever the slot holds. A boot-ROM read waits as
the access beneath it would.
The DivMMC term follows the view of the cycle that asks: the M1 view for an
opcode fetch, the after-fetch (hold) view for every other read, DMA included.
Z80Cpu decides each M1's wait when it reads that byte, under the overlay that
M1's own automap decode left, and contend_read(PC, 4) charges that recorded
value, so the first M1 of a prefix chain is not charged for the state the chain's
last M1 leaves behind.
M1 hooks and interrupts¶
Two callbacks bracket the opcode fetch, and the order matters. on_m1_prefetch
fires before the read, because DivMMC automap, the NMI FSM and the Multiface
window all decide what memory the CPU is about to see, and must be settled
before the byte is fetched through them. on_m1_cycle fires after, once per
fetched byte of a prefix chain — including the inner byte of DD ED xx —
because the IM2 opcode decoder models one event per M1. Both fire for every M1
byte of an instruction, the inner bytes of a prefix chain included
(fetch_prefix_chain_()): the DivMMC and Multiface decisions are per M1, and an
inner byte can sit on an entry point or in the unmap window.
request_interrupt(vector, first, last) gives the CPU the window of
instruction boundaries, on the FUSE T-state counter, at which /INT is seen low.
The T80 samples INT_n into INT_s on every CPU rising edge (t80n.vhd:1664) and
decides at the end of an instruction with the INT_s taken at the start of its
last T-state (t80n.vhd:1742-1772), so a pulse first sampled on CPU edge E_1 and last on
E_N is taken at a boundary in [E_1 + 1 T, E_N + 1 T] — 32 CPU cycles on 48K/+3,
36 otherwise (zxnext.vhd:2033). execute() takes it at the first boundary
inside the window, keeps it pending at one before (a request resolved at the end
of an instruction can be too late for that instruction's boundary), and drops it
once the window has passed — unconditionally rather than gating on IFF1, because
the hardware line goes high again regardless of whether anyone was listening.
The one-argument request_interrupt(vector) is the window "from now, for the
pulse width", for callers without a timeline. rebase_fuse_tstates_() moves
the window, and FUSE's EI stamp, with the counter whenever it re-bases it — at
each frame, and at a CPU-speed change, where the window keeps its distance in
CPU T-states because the pulse counts CPU clock edges (the IM2 fabric re-places
its own pulse edges, Im2Controller::set_cpu_divisor()). A snapshot
stores the window relative to the counter, which a load re-seeds, and the EI
stamp as whether its grace is pending at the saved boundary. execute() also replicates FUSE's EI-grace rejection before
calling on_int_ack, so the daisy chain is not advanced by an acknowledge cycle
that never happens. The vector comes from
on_int_ack() when one is installed, and from int_vector_ otherwise. NMI has
a path of its own, including the Next's stackless mode (NR 0xC0 bit 3), which
suppresses the stack writes and substitutes the live NR 0xC3:0xC2 pair on the
matching RETN/RETI.
The IM2 fabric¶
Im2Controller owns 14 device slots in VHDL priority order (DevIdx). Each
slot carries the im2_peripheral.vhd wrapper state — request edge detect,
enable bit, unqualified one-shot, status latch — and the im2_device.vhd
four-state machine S_0 → S_REQ → S_ACK → S_ISR with its IEI/IEO daisy chain.
The controller additionally holds the im2_control.vhd decoder that recognises
RETI/RETN and IM-mode changes out of the M1 byte stream, the legacy pulse-mode
/INT generator, and the NR 0xCC/CD/CE DMA-delay latch.
It is ticked once per instruction, at the end of the instruction's slot
(Emulator::finish_slot_interrupts()), after the devices and the interrupt
events have raised that instruction's requests, and it runs on the CLK_28
timeline: each request carries the edge it was raised on
(raise_req(dev, edge)), and the tick resolves them in edge order — the status
and im2_int_req latches set on the edge after the request
(im2_peripheral.vhd:154-178), the pulse falling on the CLK_28 falling edge
after it and lasting 32/36 CPU edges (zxnext.vhd:2017-2044), a device
entering S_REQ on the first CPU edge after its latch that has M1_n high
(im2_device.vhd:106; M1_n is low for T1-T2 of every opcode fetch, which is why
the tick is told how many fetches the instruction opened with).
latch_edges_until() does the same part-way through an instruction, for an
interrupt-status read or a write that must be ordered against the requests.
A request raised while no instruction runs — a DMA burst, a NEX boot hold —
keeps its edge for the status latch, but its pulse starts with the next
instruction: jnext's DMA holds the bus for whole bursts, and letting a pulse
expire unseen behind one would be a bus-arbitration model jnext does not have,
not the VHDL.
Peripherals do not call the controller directly in new code. They hold an
Im2Client instead — a two-field facade binding one DevIdx — so a device
needs to know its own priority slot and nothing else about the fabric.
What the FUSE suite proves¶
test/fuse/fuse_z80_test.cpp runs FUSE's own tests.in / tests.expected
corpus of 1356 cases, comparing the entire register file, MEMPTR, IFF1/IFF2, IM,
the halt flag, every touched memory byte, and the total T-state count.
test/z80n/z80n_test.cpp is the equivalent harness for the Z80N set, with 85
cases.
What those suites do not prove matters just as much. They do not check the
per-bus-cycle event trace FUSE's coretest emits, only the total. And they run
Z80Cpu against a flat 64 KB TestMemory and a TestIO that echoes the port
high byte: no MMU, no contention runtime, no IM2 acknowledge callback, no M1
hooks. A green 1356/1356 is a statement about instruction semantics and
per-instruction totals in isolation. Everything the wrapper layers on top —
contention stretch, IM2 arbitration, automap timing, the interrupt pulse window
— is proved by other suites and by the screenshot regression.