The wrapper and the bus adapter¶
M68kMachine (src/cpu.rs) wraps the published pure-Rust
m68k 0.13 core. The core sees
the machine through an adapter implementing its AddressBus trait, so
every CPU-visible access -- RAM, ROM, custom registers, CIA, RTC,
autoconfig, Gayle, Akiko -- routes into the shared Bus and is billed in
colour clocks:
Chip and slow RAM go through chip-bus arbitration (
grant_cpu_bus_access) and genuinely wait for free slots.Fast RAM, ROM, and other external-bus targets are billed at the CPU clock (
cpu_external_access), scaled bycpu_clocks_per_cckwith sub-CCK carry so accelerated clocks bill fractional costs exactly.Unmapped space bills one ordinary external cycle per 16-bit bus cycle the access performs (an aligned word is one cycle; a misaligned word on 020+ is two), not one per byte of the sized access. On a real CD32 (
tools/cd32-proberows URD/ROMRD/ULRD/UWR), a word-read loop over the empty $A80000 expansion window runs at exactly the Kickstart-ROM pace (12.0 CPU clocks per CMP.W (A4)+/DBF iteration), so unmapped reads take the fastcpu_external_accessclass. Dropped unmapped writes are posted and earn a one-clock overlap credit so the measured 8.0-clock write+DBF cadence lands. exec’s ROMTAG scan word-reads that window for seconds at CD32 boot, which is why the earlier per-byte slow-class billing started the boot show visibly late.Addresses are masked to the model’s bus width: 24-bit for 68000/68010/68EC020, 32-bit for 68020/030/040/060.
Selectable models: 68000, 68010, 68EC020, 68020, 68030, 68040, 68060.
[cpu] fpu fits a 68881/68882 to any 020 or 030; it is on by default
for the 68040 and 68060, whose FPU is on-die. The m68k core executes
the 6888x instruction set in true 80-bit extended precision via a
pure-Rust software floating-point engine
(softfloat.rs).
It models arithmetic (add, sub, mul, div, sqrt, and the single-accuracy
FSGLMUL/FSGLDIV, which round the mantissa to 24 bits but keep the
extended exponent range -- gcc -m68040 emits them for float arithmetic,
and the Linux/m68k kernel FPSP has no emulation entry for them, so a
Line-F there is a userspace SIGILL), ordered compare, round-to-integer,
scale, getexp/getman, the format conversions, FMOVE/FMOVEM in every
operand format (including packed decimal), the constant ROM,
FBcc/FScc/FDBcc/FTRAPcc, the control registers, the FPCR rounding
mode/precision, the FPSR exception/accrued bytes, and the FSAVE/FRESTORE
state frames. The state frame is NULL after reset and, once the FPU has
been touched, the CPU’s own IDLE frame: the 68881-style 41, $28-byte frame on the 040.
Guests validate the size byte, and Linux/m68k’s sigreturn kills a process
whose saved FPU frame carries a foreign size. FMOD/FREM compute the exact
remainder and the FPSR quotient byte. This covers Kickstart’s detection
and per-task FPU context switching.
The transcendentals (FSIN/FCOS/FTAN, FASIN/FACOS/FATAN, the hyperbolics,
FETOX/FETOXM1/FTWOTOX/FTENTOX, FLOGN/FLOGNP1/FLOG2/FLOG10) and FSINCOS run
in extended precision too: a double-FloatX80 (“double-double”,
~128-bit) layer
(dd.rs)
evaluates Taylor/atanh series over reduced ranges and rounds the result
to extended under the FPCR mode, setting INEX and the domain flags
(OPERR/DZ). Accuracy is validated against an arbitrary-precision oracle
(the pure-Rust astro-float, a dev-only dependency;
fpu_accuracy.rs):
every function is within 1 ULP across a wide sweep and all four rounding
modes, and round-to-nearest is correctly rounded in practice. The results
are not chip-bit-exact -- the real 6888x uses its own CORDIC/polynomial
microcode, and on a bare 68040 these instructions trap to a software FPSP.
The 68000’s per-instruction cycle counts in the m68k core have been
corrected to exact totals across the SingleStepTests 68000 cycle corpus
(validation details),
which is what makes cycle-budgeted pacing trustworthy. Instruction
semantics are also checked against Toni Wilen’s WinUAE cputest suites
(every opcode of a CPU model, hundreds of register and memory states each,
including exception stack frames): crates/cputest-runner drives the
generated test sets against the published m68k core, and the nightly
cputest.yml workflow runs the full 68000 suite
(crates/cputest-runner/tools/cputest-gen.sh generates the data for any
model locally).
The batch/JIT execution mode¶
[cpu] jit (experimental, 68020+) swaps the per-instruction precise loop
for the core’s instruction-budgeted batch path
(CpuCore::run_batch): hot straight-line runs and self-loops execute as
compiled Cranelift traces (the cpu-jit cargo feature; without it, wasm
builds included, the same trace machinery runs interpreted), and the
largest fast-RAM bank is offered to the core as a zero-cost direct-memory
window (AddressBus::fast_mem). The timing contract is deliberately
approximate (M68kMachine::step_slice_jit):
every retired instruction is billed a flat single CPU clock, converted to colour clocks at the configured clock ratio, and fast-RAM/ROM accesses bill nothing (an ideal zero-wait external bus) -- the machine behaves like a perfect pipelined accelerator whose throughput tracks
[cpu] clock_mhzdirectly, e.g. ~50 MIPS for a 50 MHz 68040 in fast RAM. This is the deterministic equivalent of a “fastest possible” mode: a truly host-speed CPU would make emulated results depend on the host, which Copperline never allows;chip and slow RAM accesses still arbitrate into DMA slots through the normal
CpuBuspaths and advance the beam as they land (that bus is shared silicon), and CIA/RTC accesses keep their E-clock costs, keeping chipset side effects ordered against display DMA;interrupts are recognized at batch boundaries (every 64 instructions), from the live INTENA/INTREQ state; a level the boundary cannot deliver is never left in the core, since the batch would take it as soon as the mask drops without recomputing INTREQ (a spurious handler re-entry);
a slice that runs and then executes STOP is billed only its executed time -- the idle period is the next slice’s event-bounded fast-forward, exactly like the precise path’s STOP handling;
traps surfaced by the batch (A-line/F-line/TRAP/BKPT/illegal) are taken as their real hardware exceptions, mirroring the precise loop’s no-op HLE policy.
The on-chip cache models stay active under JIT: on a real accelerator it is exactly the caches that let chip-RAM-resident code run at CPU speed instead of paying chip-bus arbitration on every fetch, so bypassing them made a fast-RAM-less Workbench measure 3x slower under JIT than precise. A cache hit serves the access with no bus cycle; a miss pays the real chip-bus (or zero-wait external) cost. CACR writes inside a batch are synced to the models at the batch boundary.
The fastmem window is only offered when nothing intercepts plain fast-RAM
accesses: cache models, memory-write debug hooks, the heat map, SMC
detection, and injected bus faults all force every access back onto the
bus (so on default 020+ configs, whose caches are modelled, the window
engages only when the user opts the caches off -- and the core also
declines it whenever the guest enables the MMU, since fastmem addresses
are physical). Arming any per-instruction debug or diagnostic hook
(breakpoints, watches, traces, COPPERLINE_DBG_*) drops the whole slice
back to the precise loop, so the debugger always sees every instruction.
The host-side instruction profiler (profile::samples::InstructionSampler)
retains at most 262,144 pending instruction/IRQ records, about 42 MiB on
64-bit hosts. Both producers check the cap before constructing a record or
unwinding a stack. Overflow drops new records without altering CPU execution,
increments a saturating counter, and warns once per sampler lifetime. Draining
or clearing the buffer allows sampling to resume but preserves the counter.
The profile status and summary retain the loss count after the sampler stops.
This bound is independent of frame polling, so a debugger execution path that
does not drain samples cannot grow the buffer indefinitely.
The 68000 and 68010 never take the batch path. On the small-box machines
every CPU cycle drives the one bus shared with Agnus, and the floating-bus
model is prefetch-order dependent: Kickstart’s diagnostic-ROM probe
(CMPI.W #$1111,(A1) against unmapped $F00000) relies on the prefetch
queue having fetched the following words so the undriven bus does NOT
float to the immediate just read. The batch contract runs without
prefetch, which lands the float on the immediate and false-detects a
diagnostic ROM. [cpu] jit on these models logs a note and stays
precise.
Prefetch¶
The 68000’s two-word instruction prefetch queue (IRD/IRC) is modelled in
the m68k core
(prefetch_queue):
the next opcode is fetched before the current instruction finishes, so
self-modifying code that overwrites the next instruction executes the
stale pre-write word (real MC68000 Class 1 SMC behaviour), while a taken
branch flushes and refills the stream from the target. The queue lives in
the backend core rather than a Copperline-side bus cache, because correct
flushing depends on the CPU’s own control flow, exceptions, and
interrupts. It is gated to the 68000 and 68010 (prefetch_enabled);
68020+ fetch directly at PC through the bus adapter (their real pipelines
hide behind the instruction cache model instead). Chip-RAM probes pin both
cases: cpu_prefetch_probe_documents_self_modified_next_opcode_behavior
(stale fall-through) and
cpu_prefetch_probe_branch_refetches_self_modified_chip_ram_target
(branch refetch).
Instruction handlers can move the final prefetch earlier than the generic
end-of-instruction top-up when 68000 microcode does so. That matters for
register-only forms too: immediate ALU and CMPI to Dn perform the final
prefetch before the data-register write or compare flags, and long register
forms spend their trailing internal clocks after that prefetch. Plain
CMP.L <ea>,Dn computes its flags before the final prefetch, then spends its
2-clock long-compare tail after that prefetch. Long ADD/SUB/AND/OR <ea>,Dn
forms write Dn before the final prefetch, then spend their 2- or 4-clock
long-ALU tail after it depending on whether the source operand came from memory.
ADDA/SUBA and CMPA likewise place their address-arithmetic tail clocks after
the final prefetch. EOR Dn,Dm follows the memory-destination EOR ordering:
flags are computed first, then the final prefetch/tail runs before the Dm
writeback. Register ADDX/SUBX Dm,Dn also poll IPL on the final prefetch before
writing Dn, with long forms flushing their 4-clock tail first. MOVE SR,Dn
also waits until after the final prefetch and 2-clock tail before storing the SR
word into Dn. MOVEA <ea>,An has no flags or tail clocks, but still delays
the An update until after the final prefetch and IPL sample. The privileged
MOVE An,USP and MOVE USP,An forms use the same
prefetch-before-register-update point. Status-register writes also have
instruction-specific side-effect timing: MOVE <ea>,CCR/SR and
ORI/ANDI/EORI #imm,SR spend their internal status-write delay before the
architectural CCR/SR mutation and post-write refill, while immediate-to-CCR
mutates the CCR before that delay on the 68000.
MOVEM.L <regs>,-(An) also exposes the 68000’s word-step predecrement
microcode on the bus: each long transfer writes the low word at An-2
before the high word at An-4, leaving memory big-endian while matching
the observed access order.
For Bcc/BRA/BSR, the branch-long $FF displacement-byte sentinel is
gated to 68020 and later; on the 68000/010 the same byte remains the signed
8-bit displacement -1, so no extension word is consumed.
CHK.W on the 68000 tests the upper bound before the lower bound; upper-bound
traps take the shorter pre-frame comparison path. A negative Dn that reaches
the lower-bound test also takes that shorter path when the preceding signed
upper-bound subtraction overflowed; ordinary lower-bound traps spend two more
clocks before stacking the group-2 exception frame.
68010¶
The 68010 shares the 68000’s bus interface and two-word prefetch queue but
adds the vector base register, the format-stacking exception model
(format 0 four-word frames, format 8 bus/address-error frames, RTD), and
DBcc loop mode: a DBcc that branches -4 back to a loopable one-word
instruction holds the body/DBcc pair in the prefetch queue and re-executes
it with no instruction fetches until the condition turns true, the counter
expires, or an exception intervenes (loop_mode in
core/cpu.rs;
the loopable set and the DBcc entry/exit arms live in
core/decode.rs).
A looping DBcc iteration costs 6 internal clocks and touches the bus only
for the body’s operands, which is what makes tight copy/clear loops
measurably faster on a real 68010.
loop_mode_timing_tests.rs
pins engagement, the 68000’s non-engagement, and the no-fetch iteration
cost.
The 68010’s own cycle costs where they differ from the 68000 are
calibrated against the vAmigaTS CPU/Timing/CPU/Timing2 measurements
(cross-checked with Moira’s cycle-exact 68010 path, which matches
A500+68010 photos): MOVES spends per-EA-mode internal clocks between the
address calculation and the SFC/DFC data cycle, MOVE from CCR and
privileged MOVE from SR both cost 4 clocks to a register and perform their
final prefetch before updating Dn, MOVE from CCR to memory prefetches
before its write, long register shifts/rotates use the same base-8 cycle
total as the 68000 rather than the later barrel-shifter timing, a
format-0 RTE is 24 clocks (the format word is read once, not re-read),
and an interrupt dispatch is 46 clocks (12 internal before the four-word
format-0 frame) against the 68000’s 44. STOP semantics are shared with
the 68000: the SR operand is loaded verbatim (a single-stepped STOP
observes S and T exactly as written; the SingleStepTests 68000 fixtures
pin this), and an S-clear SR stops only momentarily -- at the next
instruction boundary the stopped state’s supervisor check raises a
privilege violation stacking the STOP itself, so the handler’s RTE
re-executes it; a pending trace (T set in the SR the instruction started
with) has priority and recovers from the stop, while a T bit loaded by
STOP does not fire while stopped.
stop_and_68010_timing_tests.rs
pins all of these.
Caches¶
The on-chip caches are silicon, so they are modelled by default on the parts
that have them: the instruction cache on the 68EC020/68020/68030/68040/68060
and the data cache on the 68030/68040/68060
(CpuModel::has_instruction_cache/has_data_cache). CACR (and CAAR on the
020/030) are always stored; software
(AmigaOS at boot) enables and clears the cache exactly as on hardware. A cache
hit costs no bus cycle, so a cached instruction fetch does not contend with
chip-bus DMA. That is the point of modelling it: 020+ code looping out of
chip RAM otherwise pays a bitplane-DMA arbitration stall on every fetch and
runs at roughly half speed, drifting an AGA demo’s interrupt-driven music or
animation to half its intended rate. The data cache only covers expansion
RAM/ROM because chip and slow RAM are DMA-visible and cache-inhibited, as on
real machines. A 68000/68010 models no cache. [cpu] icache = false/dcache = false opt a cached CPU back out; with no cache modelled, the cache-control
instructions are no-ops and self-modifying code always executes fresh bytes
(the safe direction).
Each cache is a power-of-two array of direct-mapped longword entries
(src/cache.rs): 64 entries (256 bytes) on the 020/030 -- the exact 68020
instruction-cache geometry -- 1024 (4 KB) on the 68040, and 2048 (8 KB) on
the 68060. The larger capacity is the part that matters here: a chip-RAM
loop bigger than 256 bytes stays resident on a 040 where it would thrash a
020. The 040’s 4-way set-associative, 16-byte-line, copyback organization
(and the 060’s set associativity) is deliberately not modelled, and need
not be -- the data cache only covers expansion RAM, which chipset DMA
cannot reach, so write-back versus write-through cannot be told apart.
Virtual boards, however, can host-DMA into expansion RAM behind the
model (the services board answering a DosPacket, copperhf draining a
completed disk read, the A3000 SDMAC pumping SCSI data), so both paths that
let a device touch guest memory report it and the data cache is dropped: the
synchronous device-access path clears it inside the access, and device
tick-path DMA latches Bus::devices_wrote_memory -- keyed on actual DMA
writes (DeviceHost::wrote_memory and the SDMAC/A2091 write latches), not
on mere memory access, so a board that only borrows memory on its tick does
not disable the cache -- consumed at the next instruction-boundary flush
(flush_timed_devices_dcache_coherent) and, because the JIT batch path can
flush timed devices mid-batch, before any data-cache read hit. Without
the tick half, PFS3 on a 68040 read corrupted file data through copperhf’s
asynchronous completions.
The 040 and 060 also redefine CACR (the cache models take only its instruction and data cache enable bits; there are no freeze bits or cache-clear strobes) and move invalidation to the CINV/CPUSH instructions; the model maps a CINV/CPUSH to a whole-cache clear of the indicated cache(s) -- over-clearing line/page scopes, which is always coherent (a push has no dirty data to write back in a write-through model). The data cache is write-through, and a write that hits invalidates its entry.
The instruction cache does not snoop writes (authentic 68020 behaviour): a line stays valid when the CPU or DMA rewrites its backing memory, until software clears it (via CACR on the 020/030, via CINV/CPUSH on the 040/060) or another fill replaces it. Only debugger memory writes invalidate it directly.
Restoring a save state that predates cache modelling (or was captured with the cache disabled) re-establishes the model’s cache cold and re-derives its enable bits from the restored CACR, so the machine keeps the cache its CPU has rather than silently running cacheless after a load.
The 68060¶
The 68060 is modelled as the full part: on-die FPU and MMU, the 8 KB caches (in the same pragmatic direct-mapped host model as the other parts), and its own timing engine.
Instruction set. The 060 dropped instructions from silicon; on real
boards the OS-side 68060.library (Motorola’s 68060SP) emulates them
via the chip’s trap interface, and Copperline models exactly that
contract. The unimplemented integer set (MOVEP, CHK2/CMP2, CAS2,
misaligned CAS, 64-bit MUL/DIV) raises vector 61 with a format 2
frame the 68060SP dispatches on -- next-instruction PC stacked, the
calculated operand EA in the frame, FPIAR pointing at the instruction.
Packed decimal is the unsupported-data-type exception (vector 55),
dynamic-list FMOVEM and immediate packed operands the unimplemented-EA
exception (vector 60). [cpu] unimplemented = "native" executes the
whole set directly instead, for systems without the library. LPSTOP is
implemented as STOP semantics.
Registers and frames. PCR (MOVEC 008 with the 040’s DACR0, dispatched by model. The 060 CACR persists the cache/branch-cache/store-buffer enables with CABC/CUBC as write-only clear strobes. The part has a single supervisor stack: the SR M bit is storable (and interrupts clear it) but never banks A7, and MSP/ISP are gone from the MOVEC set. Access errors push the eight-word format 4 frame.
Timing.
timing_060.rs
replaces the 020+ scaling formula for the 060: every opcode word
classifies (a build-once 64K table over a pure function) into the
MC68060UM Chapter 10 dispatch classes with a pOEP cycle cost -- most
ALU/move instructions one clock, data-dependent costs derived from the
68000-reference count as (raw/4).max(1). Costs are pOEP occupancy at
zero-wait: memory latency stays billed by the host bus per access, so
bus-bound code shows no pairing benefit, which matches silicon.
Superscalar dual issue is modelled retrospectively: an instruction that
satisfies the UM Table 10-1 dispatch test against the previous
instruction’s open sOEP slot (pOEP|sOEP class, simple EA, no RAW/WAW
against the head’s defs, CCR rule, one memory operand per pair, both
fetches icache hits, PCR.ESS enabled) refunds to zero additional cycles.
The 256-entry branch cache (CACR.EBC; CABC/CUBC clears) folds correctly
predicted taken branches onto the preceding instruction’s cycle -- a lone
predicted branch still issues for one clock, which both matches the
canonical one-clock subq/bne loop and guarantees a bare bra self idle
loop advances emulated time. Both structures are serialized: prediction state changes
cycle counts, and cycle counts change chipset interleaving. timing-test
rows 28-30 measure the pair/RAW/loop cases end to end (a 50 MHz 060
runs row 30 at exactly one clock per iteration).
Residuals, all deliberately pessimistic (they under-pair, never over-pair): the sOEP EA subset and dual-memory rule are coarser than UM Table 10-1; the 96-byte IFU FIFO and AGU change/use stalls are not modelled; the store buffer is stored (CACR.ESB) but writes bill at bus rate; pairing across a folded branch is not modelled, and a branch folds only when a pairing window is open; the FSAVE EXCP frame is not generated (the softfloat FPU retires every operation completely); FPSR quotient-bit and denormal traps behave as on the other parts rather than through the 060’s unsupported-data path.
68020 timing¶
The 68000/68010 cycle counts are validated against the SingleStepTests (TomHarte) corpus, but no equivalent vectors exist for the 68020. The later part has an instruction cache, a three-stage pipeline, execution overlap, dynamic bus sizing, and alignment-dependent transfers, so an opcode does not have one context-free cycle count.
timing_020.rs
transcribes the integer timing tables from section 8.2 of the
MC68020 User’s Manual.
The manual publishes Best Case (cached with maximum execution overlap),
Cache Case (cached without overlap), and Worst Case (uncached without
overlap). Copperline does not yet model execution-stage overlap, so an
instruction selects Cache Case only when all of its opcode, extension, and
immediate fetches hit the instruction cache; any instruction-stream miss
selects Worst Case. This covers the standard effective-address tables, the
complete MOVE matrix, arithmetic and logical instructions, multiply/divide,
shifts, bit/bitfield operations, branches, control flow, and save/restore
paths. The 68000 and 68010 keep their own timing paths.
The 68030 runs the same MC68020UM tables (m68k 0.12): its integer core is
the 020’s, and the real-hardware columns in timing-test/accelprobe.asm
(a 25 MHz 68030 CPU-slot board in an A4000) measure the same anchor
figures the real A1200’s 020 did -- taken dbra 6 clocks, mulu.w ~29.
Re-running the probe on timing-test/tt-a4000-030.toml with the 020
tables lands the move.w, mulu.w, and fast-stack trap rows within 1%
of the board (the scaled-68000 approximation used before had the move.w
loop 1.26x over and the fast-stack trap 1.13x under; mulu.w was
already exact) and pulls the bare taken dbra from 1.35x to 1.18x over.
The call, movem, and chip-stack trap rows are bus-bound and unchanged
at 1.2-1.7x over.
The manual’s totals assume aligned operands and stack, a 32-bit bus, and three-clock no-wait reads and writes. They include those ideal transfers. Copperline bills every actual operand and instruction access while the instruction executes, then advances only the unconsumed part of the table total. An A1200 chip-RAM access can therefore extend an instruction through Alice arbitration; selecting a smaller CPU total cannot erase a bus wait that already happened.
Because the billing follows the accesses, an operand has to reach the bus at
the width the processor actually transfers it. The 68020/030 size an operand
as a byte, word, three-byte or long (MC68020UM 5.3.1), so a memory bit field
is one transfer of whatever it spans rather than a composed sequence. The
three-byte case has no equivalent in the 68000 the AddressBus grew up
around, so the m68k core carries a dedicated three-byte transfer (0.7.0) and
M68kMachine overrides it.
What that transfer costs is then decided by the addressed port, which is
dynamic bus sizing: the processor requests the whole operand and the port
answers with its own width. A 32-bit port takes all three bytes in one cycle;
a 16-bit port makes the processor run a word cycle and then a byte cycle for
the remainder. Copperline splits on that boundary. Plain memory is a byte
array whichever width it answers at, so it takes the sized access and
grant_cpu_bus_access_at bills it by port width -- one Alice slot on AGA, two
on a 16-bit Agnus. Every device port is 16 bits and keeps the split cycles,
which is both what the silicon runs and what keeps each register’s own
read/write side effects: a device decodes a register per access, so a single
three-byte read of $DFFxxx would name no register and would drop the third
byte rather than fetching it from the next one.
Billing the composed pair everywhere instead over-charged the 32-bit case,
which timing-test/bfprobe.asm row 10 (bfextu (a0){4:16}) measured as a
0.5% overcharge against a real A1200 -- visible only on the 32-bit chip bus,
where a span within four bytes is one cycle but a word and a byte are two. The
other bit-field spans, and the BFSET pair driving SANITY Roots II AGA’s
“DIE” dissolve (issue #371), were already right; that row was the outlier.
Sizing the access to the span also keeps it off the byte beyond the field,
which on a memory-mapped register or at the end of a mapped region would be
observable rather than merely mistimed.
A taken branch pays a pipeline-refill charge on top of its table entry. The
section-8.2 entries are written for an instruction whose successor is already
in the three-stage pipeline; a taken branch invalidates the decode and execute
stages, so the target cannot begin until the pipe refills. The manual charges
that to the following instruction’s head, which a per-instruction model with
no overlap stage has nowhere to put, so the m68k core charges it where the
flush happens (TAKEN_BRANCH_REFILL). Without it a dbra loop runs at the
isolated cache-case 6 clocks per iteration, which makes every tight loop --
depackers, MFM decoders, chase-the-beam poll loops -- too fast; it showed up
across seven independent timing-test rows (4, 5, 7, 14, 28, 29, 30). The
charge was first calibrated to two clocks against the FS-UAE A1200 reference;
a real A1200 (stock 68EC020 at 14.19 MHz, 2026-08) measures a cached taken
dbra at 7 clocks -- cache-case 6 plus one -- in three independent loop rows,
with the move, shift, mulu, paired-op and DIV-under-display rows agreeing
once that one clock is accounted for. FS-UAE itself over-bills every taken
branch by one; see timing-test/README.md.
A second probe disk then showed the charge is conditional: every one of
those rows happens to place its dbra at a longword-aligned address, and a
dbra at pc % 4 == 2 costs the bare cache-case 6 instead. The refill is
therefore one clock when the branch opcode is longword-aligned and none
otherwise (see “Real A1200 column” in timing-test/README.md).
This is intentionally a datasheet model, not a claim of cycle exactness.
The opcode word does not retain a consumed extension word, so full-format
indexed modes use the brief-index table row, MOVES uses the slower of its
two direction bases, and long divide uses the conservative signed base.
Coprocessor operations and unclassified encodings retain the old scaled
fallback. Misalignment, detailed cache-refill sequencing, and instruction
overlap remain to be added. Motorola explicitly demonstrates in section 8.1
that the timing of an instruction stream cannot be obtained by simply
summing isolated table entries; hardware measurements remain the final
reference. timing-test/ remains the end-to-end regression
for checking the combined CPU, cache, and chip-bus model rather than the
source of the per-opcode constants.
The chip-access phase¶
On AGA machines, the 020+ CPU uses Alice’s 32-bit chip-RAM path. One granted
slot transfers a longword, and the two-entry fetch latch serves sequential
opcode words from an already fetched longword without another bus access.
OCS/ECS retain a 16-bit chip bus. The 68000/010 use their four-clock bus
cycle; the 020+ path is selected by Bus::cpu_short_bus_cycle.
For precise 020+ execution, the CPU’s position is
emulated_cck * cpu_clocks_per_cck + Bus::cpu_chip_clock_phase.
Execution clocks accumulate in this shared phase and advance the beam when
a whole colour clock is due (Bus::charge_cpu_clocks_to_cck). A chip-RAM
read first waits out any partial colour clock
(Bus::sync_cpu_to_chip_clock), then waits for a free bus slot. After the
grant it pays one colour clock for data return and two further CPU clocks
(CPU_020_CHIP_READ_RETURN_CLOCKS). Access time is credited against the
instruction’s own charge so it is not billed twice.
The two-clock return cost comes from timing-test/rdprobe.asm on a stock
A1200. The same chip-read loop measures 16 CPU clocks per iteration with a
six-clock branch and 20 with a seven-clock branch. With four CPU clocks per
colour clock, a read occupies ten clocks from the boundary it starts on;
the next read’s synchronization accounts for the difference between the two
loop alignments. The probe and recorded hardware results live in
timing-test/.
Chip writes are posted. The bus unit accepts a write at the end of its
three-clock cycle, credits those clocks against the instruction charge
(take_cpu_bus_overlap_clocks), and retires the transfer into a later free
slot (cpu_posted_write_debt). Only one transfer can be pending; the next
chip access waits for it to retire. The port accepts transfers at a
two-colour-clock cadence. Memory is currently updated before the deferred
slot retires, so DMA can observe a posted write early; this is a modelling
limitation.
Custom-register reads use a separate return path: one colour clock for data return, another for crossing the chipset’s 16-bit bus, and one CPU synchronizer clock. They are deliberately not aligned to the chip-RAM clock boundary. Hardware probes have not yet isolated this path as fully as chip RAM; the existing Copper-poll beam-position test agrees with the current model, while chip-write-plus-poll composite rows remain about 9% low in the recorded comparison.
The 68000/010 keep their existing instruction/bus reconciliation because their four-clock cycle spans two colour clocks at the stock rate. JIT batches also use the older accounting: a batch does not refresh the shared phase at every instruction, and is not the precise timing path.
The earlier result-forwarding interpretation of timing-test rows 28/29
was removed from m68k. fwdprobe.asm reversed the apparent dependency
advantage by moving the loop branches to the opposite alignments. The
measured distinction is the taken DBcc opcode’s alignment: seven clocks
at pc % 4 == 0, six at pc % 4 == 2. Twenty-four of the 26 probe rows
match this model exactly; the two rows containing NOP are about 0.5%
low. These are measurements of particular loops on one stock A1200, not a
claim that the full 020 pipeline is cycle-exact.
68040 timing¶
timing_040.rs
(m68k 0.12) gives the 68040 a single-issue pipeline model in place of the
scaled approximation, which the same accelprobe columns (a 25 MHz A3640,
two byte-identical serial captures) measured as ~2-2.5x pessimistic on
plain execution: a taken-dbra loop runs 4.06 clocks per iteration where
the approximation billed ~8, a move.w+dbra loop 4.08 (the one-clock
body hides entirely in the branch resolution), and mulu.w #imm+dbra
14.0 against ~35. The model reuses the 68060 opcode classifier: most
ALU/move instructions cost their one-clock occupancy, data-dependent and
unclassified costs derive from the corrected 68000 reference count as
(raw/4).max(1), an indexed EA adds a clock, and branches pay small static
costs (taken Bcc/BRA 3, not-taken 2, DBcc 3 either way, computed flow
changes a 4-clock refill floor) -- the 040 has no branch cache, so there is
nothing to predict or fold. CINV/CPUSH cost a dozen clocks even on a clean
line (the A3640 measures ~12 per CPUSHL DC,(An), accelprobe row 15).
Memory latency stays billed by the host bus per access, as on the 020 and
060 paths, and exception entries keep the legacy scaled costs so the
exception timing calibration is unchanged.
DBcc-taken at 3 clocks lands the ubiquitous one-clock-body loop on the
measured 4 per iteration; the empty dbra loop then reads 3 against the
measured 4.06, the deliberate compromise of a model with no
overlap/absorption stage. Residuals are not uniformly conservative: there
is no execute-stage overlap (pessimistic), shifts and bit operations take
the 060 class costs (optimistic where real 040 silicon runs those forms a
clock or two slower), and the write-back stage and store buffer are not
modelled (writes bill at bus rate).
Re-running the probe on timing-test/tt-a4000-040.toml against the A3640
captures: the move.w+dbra row is tick-exact, mulu.w and cpushl are
within 7%, and the bare dbra reads the documented 3.0 against 4.06
clocks. The trap rows stay 1.4-1.7x under (exception entries keep the
legacy scaled costs) and movem 1.4x over; everything else that still
differs is the bridge and fast-RAM region billing below, not the core.
The same accelprobe columns quantify two effects no CPU timing table covers, both still open on the Copperline side:
The A3640/BFG9060 CPU-slot bridge increases chip-RAM
move.lread latency to 2.5-3.4 us (the 68030 board’s chip path matches emulated timing directly).Fast RAM regions exhibit distinct access speeds on hardware (CPU-card ~66 ns, motherboard ~320 ns, Zorro III ~530 ns per sequential longword read) whereas the current emulator bills all fast RAM at native CPU clock rates.
MMU¶
The 68030, 68040, and 68060 model the on-chip MMU (has_pmmu), so software
that sets up and enables address translation runs rather than having its MMU
writes ignored. The 030 and 040 differ enough to need separate walkers: the
68030 uses its programmable table walk (CRP/SRP selection, the optional
function-code lookup level (TC FCL), the TC index fields TIA-TID, 4-/8-byte
descriptor modes, early-termination descriptors, indirect descriptors when
the configured levels are exhausted, and the long-format supervisor-only
bit), while the 68040 (and the 68060, which shares its walker) has a
fixed-format three-level walk (root -> pointer -> page, 4 KB or 8 KB pages,
URP/SRP split by supervisor) keyed off its own TC layout. Both walkers
share the transparent translation registers (TTRs: the 030’s TT0/1, the
040’s ITT0/1 + DTT0/1), which short-circuit the walk for a matching address
range. Accesses carry their function code into the walk: MOVES translates
in the SFC/DFC space (mmu.library’s 030 MMU-detection probe remaps the
user-data space and reads it back with MOVES from supervisor mode), and
PTEST/PLPA probe the DFC space.
One register set (mmu_* in CpuCore) is canonical for both paths, so the 030
PMOVE writes, the 040 MOVEC writes, and the walker can never desync (the 040
root pointers overload the CRP/SRP address slots, dispatched by cpu_type). The
enable bit differs by part -- TC[31] on the 030, TC[15] on the 040 -- via
tc_enable(). PMOVE/MOVEC load the registers (including the 030 MMUSR
via PMOVE PSR); PLOAD/PFLUSH are accepted as no-ops on the 030 walk
(which has no ATC to flush).
A 68040 table walk costs three descriptor fetches, far too much to pay on every
access, so the 040 walker is fronted by an address-translation cache
(mmu::atc in the m68k crate, 64 entries like the real ATC): a
direct-mapped cache of (logical page -> physical page) keyed by page frame
and supervisor flag. It is a pure cache -- never serialized (restored
empty) -- and is flushed when the mapping could change: a write to TC / a
root pointer / a TTR, or a PFLUSH. As on real hardware, a plain write to a
descriptor does not auto-flush; software must PFLUSH, which is where the
ATC is cleared.
The 68040 enforces page protection: a write to a write-protected page (the W
bit, accumulated across the table and page descriptors) or a user access to a
supervisor-only page (the S bit) raises an access fault, delivered as a 68040
format-7 access-error stack frame at the bus-error vector. The faulting
instruction is rolled back and its PC stacked, so after the handler fixes the
mapping an RTE restarts it (the demand-paging / memory-protection model). The
ATC carries the protection bits, so a cached translation cannot bypass the
check. The frame’s SSW reports the read/write direction, the access size, the
transfer modifier (function code), and -- for faults that came out of the
table walk rather than the physical bus -- the ATC bit; that bit is how an
OS-level page-fault handler (mmu.library, VMM, Enforcer) tells a translation
fault it must service from a real bus error it must pass on, and mmu.library’s
lazily-materialized user tables guru without it (issue #90). A faulted write
is reported in writeback slot 3 (WB3S valid bit, size and transfer modifier,
address and data in WB3A/WB3D), matching real 68040 silicon -- WB2 is
reserved for MOVE16 cacheline writes and stays clear. A handler that clears
WB3S.V has absorbed the store -- how Enforcer/MuForce discard writes into
protected pages -- and RTE honours that by discarding the restarted
instruction’s matching write; MuGuardianAngel completes an allowed write by
storing WB3D to WB3A itself. A handler that completes the writeback manually
but leaves V set gets the restart’s store instead (a double store to plain
memory, the restart-model gap). The other writeback slots and continuation
fields stay clear; mid-instruction continuation is not modelled.
Any access through an invalid/unconfigured descriptor raises an access
fault, instruction fetches included -- data faults are how Enforcer/MuForce
catch low-memory and freed-memory hits, and fetch faults are how a
demand-paged OS (Linux/m68k) pages code in; software that must keep
executing across an MMU enable covers itself with the transparent
translation registers. The fault delivery itself translates like any other
supervisor access: the frame pushes and the vector fetch go through the
MMU (supervisor stacks and VBR hold logical addresses -- Linux runs its
kernel at virtual 0 with RAM at a physical offset and splits URP from
SRP, so an untranslated push or vector read lands in unrelated physical
memory), a MOVES fault’s SFC/DFC override is consumed into the frame’s
SSW rather than leaking into the dispatch, and a fault raised while
delivering a fault is a clean double-fault halt. Because the rollback
model re-executes the faulting instruction, its post-fault side effects
are also contained: bus accesses are suppressed, and the handler-entry
PC/SR/register state is re-asserted at the instruction boundary so an
aborted flow instruction (a JSR whose stack push faulted) cannot divert
the dispatch into its branch target, nor a predecrement MOVEM walk the
handler’s stack pointer away.
Access granularity follows the 32-bit bus: an aligned long transfer is one
bus cycle, so a faulted long RMW writeback is reported as one long-sized
WB3 entry (not the 68000’s low-word-first word pair -- a writeback-
completing handler like Linux do_040writebacks would otherwise complete
half a store). A misaligned access that straddles an MMU page boundary
runs as separate cycles with each side translated on its own page, data
and instruction-stream fetches alike; translating only the base address
would touch the physically adjacent page instead of the mapped one (under
Linux/m68k that mis-fetched the low half of a page-straddling BSR.L
displacement and sent execution mid-instruction).
PTEST (68040) walks the addressed page and reports the physical address and
resident bit in MMUSR (the cache-mode/used/modified attribute bits are not yet
filled in). The 68030 PTEST performs a real level-limited walk in the
extension word’s function-code space and composes the 16-bit MMUSR (B, S, W
-- reported for read tests too -- I, T, and the level count N); with the A
bit it hands back the physical address of the last descriptor examined,
which is how mmu.library’s fault handler locates the shared descriptor slot
to materialize.
68030 faults push the long bus-cycle fault frame (format $B, 46 words):
SSW (DF/RW/SIZ/FC for data faults, FB|RB with the stage B address for
instruction faults), the data-cycle fault address, and -- for write faults
-- the write’s value in the data output buffer. RTE supports both
continuation protocols on top of the rollback/restart core: a handler that
fixed the mapping RTEs with DF set and the instruction restarts; a handler
that completed the data cycle itself clears DF, supplying a faulted
read’s result in the data input buffer (mmu.library emulates lazily-zeroed
pages this way) or absorbing a faulted write, honoured as a one-shot
substitution on the re-executed instruction’s matching access. Remaining:
the used/modified/limit MMUSR bits and descriptor write-back, CRP limit
checks, and the DT=1 page-descriptor root; the 030 walk has no ATC, so
every access re-walks the tables.
Interrupts and STOP¶
Paula’s INTENA/INTREQ levels are delivered as M68K autovectors through the
modelled IPL pipe and boundary sampling described in The timing model. When the CPU
executes STOP, the frame loop fast-forwards device time to the next
event that can raise an interrupt instead of spinning -- behaviour the
debugger’s Step control inherits. One event source is invisible to that
horizon: a serial sink with a live host input side (TCP, pty, the browser
channel) can start a reception at any wall-clock moment. While such a sink
is attached, the blind fast-forward span is capped to a fraction of a
serial character time, so a byte landing mid-nap raises RBF and wakes the
CPU before a second byte can complete and overrun Paula’s one-word receive
buffer -- matching a real machine, which wakes from STOP within
microseconds of the interrupt.
Exceptions¶
Guest exceptions -- bus and address errors, illegal and unimplemented instructions, traps, privilege violations -- are taken as the real hardware exceptions through the guest’s vector table: the precise loop hands every trap to the core’s no-op HLE handler, which declines it. Two conditions stop the machine instead:
A step result the wrapper does not expect ends the frame with an error (
unexpected m68k step result ... at PC=...). The window marks the CPU halted and stops stepping, but stays alive showing the last framebuffer, and the debugger can inspect the halted state. A headless capture run fails instead, since its captures can never fire.A double fault (a bus or address error during exception processing) halts the CPU, as on real silicon (
M68kMachine::cpu_double_faulted). The window pauses and reportsCPU halted: double fault at pc ...once, on the Console and to the control protocol as adouble_faultstop, and leaves the state for inspection.