Kickstart 1.3 Meets the Cycle Counter
Rusty Nail is a fantasy console living on a Nucleo-H753ZI that keeps swallowing real machines: PICO-8, Game Boy and Game Boy Color, Game Gear, and most recently a Mega Drive at native speed. The next target is a different animal entirely. The Amiga 500 is not a cartridge console with a CPU and a video chip; it is a computer with a custom chipset that steals bus cycles from the processor as a way of life. Before writing a single line of Agnus or Copper, I wanted an answer to a narrower question: what does a 68000 cost on this hardware, measured, not estimated?
Today that question has numbers, and one of them came from a real Kickstart 1.3 ROM executing its reset path on the bench.

The rig is the same standing bench from the console tour: the Nucleo is the computer, and for this milestone the HDMI side stayed dark - all the evidence in this post came over the debug probe as defmt logs and DWT cycle counts.
The budget, before anything runs
A PAL Amiga frame is 312 lines of 227 colour clocks at 3.546895 MHz, which works out to 50.08 frames per second. At 480 MHz that gives the H753 exactly 9,584,586 cycles to be an entire Amiga, every frame, forever. The programme rules I set for this port are strict about what "runs" means: no slowing the emulated clock, no skipped emulated frames, and no success claims without a measured p99 on the hardware.
So the plan has gates. The CPU alone must fit in 35% of the frame (3,354,605 cycles) before the chipset work is allowed to start; CPU plus Agnus, Copper and bitplanes must fit in 65%; the full machine in 80%. Miss a gate and the honest answer is to stop and report, not to quietly ship a slower Amiga.
Two Musashis walk into a linker script
The 68000 core was never going to be written from scratch: the Mega Drive runtime already ships Karl Stenerud's Musashi in a heavily trimmed fork, and it is the only 68000 with measured numbers on this exact silicon. But upstream Musashi went MIT in 2013 and the fork carries an older non-commercial notice, so I vendored both into the new Amiga core crate and let the object files argue.
| Cortex-M7 build | CPU text | tables (RAM) |
|---|---|---|
| Musashi 3.32 fork | 139,181 B | 333,184 B (256K jump + 64K cycle) |
| Musashi 4.x upstream | 170,824 B | 590,720 B (256K jump + five 64K cycle rows) |
The surprise in that table: upstream's per-CPU-type compile switches do
not trim its cycle table. Turning off the 68010 through 68040 still leaves
all five 64K rows in RAM, and this machine has exactly one spare 64K bank
for exactly one row. The fork won on numbers, so the fork it is, with the
memory map rebuilt around Amiga windows: 512K of chip RAM at the bottom,
Kickstart at $F80000, and the reset overlay that mirrors ROM over address
zero until the CIA releases it.
317,500 opinions about my CPU
Before trusting the core with a ROM I pointed it at the SingleStepTests m68000 corpus: 317,500 single-instruction test cases generated from a microcoded 68000 model, each with full register, memory and bus-cycle expectations. The first run scored 70.2% on clean cases, which is a polite way of saying something structural was wrong.
It was. The fork's memory fast path had dropped the address-error checks entirely - reasonable for a Mega Drive, where games do not depend on odd address faults, and exactly the kind of thing a 68000 running arbitrary Amiga software cannot live without. The checks existed in the code but were compiled around, and the runtime flag that arms them was never set by anyone. Wiring them back in lifted clean cases to 98.5%.
The second find was nastier. The full corpus run hung the harness, and the trail led to the double-bus-fault branch: when an address error fires while the CPU is already stacking an address-error frame (an odd supervisor stack pointer will do it), the 68000 halts. The fork's halt path pre-compensated its cycle counter for loop bookkeeping that never happens on the longjmp path it actually takes, leaving the timeslice short of its target with a permanently faulting PC. The run loop re-faulted forever:
if(CPU_RUN_MODE == RUN_MODE_BERR_AERR_RESET)
{
CPU_STOPPED = STOP_LEVEL_HALT;
/* was: SET_CYCLES(cycle_end - CYC_INSTRUCTION[REG_IR]);
which undershoots the timeslice on the longjmp path -
with an odd PC the loop re-faults with no progress, forever */
SET_CYCLES(m68ki_cpu.cycle_end);
return;
}
Latent upstream, unreachable in the Mega Drive because that image never arms address errors, and found only because a quarter of the conformance corpus deliberately pokes the CPU with misaligned pointers. The final scoreboard: 98.58% of the 261,894 clean cases pass; the 55,606 cases that end in an address error currently all fail byte-compare because the 3.32 fork stacks a shorter group-0 exception frame than the real chip. That is a known, bounded piece of work, not a mystery, and the harness prints it per instruction file so it cannot hide.
Three boot bugs before first light
The benchmark image is a standalone firmware binary whose linker script
IS the memory plan: stack and locals in DTCM, the 512K chip RAM array
filling AXI SRAM exactly, the 256K opcode jump table in D2 SRAM, the
64K cycle table filling SRAM4 to the byte. First flash: HardFault before
main.
All three of the bugs that followed are the classic bare-metal trio.
The linker zeroed its way off a cliff. My machine sections were
declared INSERT AFTER .bss, which quietly places them inside the range
the reset handler zero-initialises. The zeroing loop finished DTCM at
0x20020000 and kept marching through four megabytes of unmapped address
space towards the chip RAM it was never meant to touch:
MEMORY { RAM : ORIGIN = 0x20000000, LENGTH = 128K /* DTCM */
AXI : ORIGIN = 0x24000000, LENGTH = 512K /* chip RAM */ }
/* wrong: joins the .bss zero-init range, reset walks into the gap */
} INSERT AFTER .bss;
/* right: NOLOAD machine memory stays out of __sbss..__ebss */
} INSERT AFTER .uninit;
The RAM that was not clocked yet. On the H7, the D2-domain SRAM banks power up with their clocks gated off. The Musashi table builder writes 256K of function pointers into D2 during init, and a buffered write into an unclocked SRAM is an imprecise BusFault with a stack trace pointing somewhere unhelpful. One RCC register write before machine init fixed it.
The output that never arrived. With the fault fixed, the bench ran
perfectly and printed nothing: the firmware parked in wfi after
reporting, and the RTT stream stayed block-buffered in the debug host
forever. Benchmarks now end at a breakpoint, which convinces probe-rs to
drain the log and exit like a good citizen.
The numbers
Six hundred PAL frames per run, DWT cycle counter, caches on, percentiles computed on-device.
| Workload | p50 | p95 | p99 | max | p99 % of frame |
|---|---|---|---|---|---|
| Synthetic memory-heavy loop | 1,613,870 | 1,616,003 | 1,617,227 | 1,618,010 | 16.9% |
| Kickstart 1.3 boot path | 50,303 | 2,502,743 | 2,507,671 | 2,508,897 | 26.2% |
The synthetic loop - longword reads, arithmetic, longword writes streamed through chip RAM - works out to about 11.4 host cycles per emulated 68000 clock, with prefetch emulation and address-error checks both enabled. I had budgeted 12.5 to 20 from the Mega Drive's measured numbers, so the Amiga configuration came in under its own estimate.
Then the real thing. The bench has a build mode that byte-swaps a user-supplied Kickstart at build time, bakes it into flash, and lets the 68000 execute it in place, exactly as the eventual runtime will. The defmt log for the run I will be framing:
[INFO ] machine up (kickstart boot path): rom fnv=f1693ddc, overlay=true
[INFO ] P2 cpu-only: 600 frames, p50=50303 p95=2502743 p99=2507671 max=2508897
[INFO ] vs budgets: frame=9584586, p99 permille of frame=261, P2 gate=3354605 -> P2 PASS
The Kickstart run is honestly bimodal and the p50 tells you why: with the chipset still made of stubs, the ROM spends most frames busy-waiting on hardware that does not exist yet, and the expensive frames are its RAM sizing and checksum sweeps. The p99 is therefore the cost of the heaviest real ROM code, not of a running game - and it still clears the gate with 25% headroom.
The most useful test boots twice
The host-side twin of the bench boots the same Kickstart image two times for a hundred frames each and demands the per-frame execution traces come out identical. They do, on the host and on the H753, and host and device agree on the ROM fingerprint. That determinism is the foundation the whole chipset effort will stand on: when Agnus lands and a trace hash changes, I will know it changed because of Agnus.
The same test prints a scoreboard of what the ROM touched while it ran:
bus stubs touched: custom r/w 0/32, cia r/w 1/3, open bus r/w 524292/6
Half a million open-bus reads is Kickstart sizing memory across an expansion space nobody answered. Three CIA writes and an overlay that never released is the ROM talking to an 8520 that is not there yet. None of that is failure; it is the chipset work, itemised by the machine itself.
What this proves, and what it does not
Proven, with hardware evidence: the 68000 substrate fits with real headroom, the Amiga memory map decodes correctly under real ROM code, flash execute-in-place behaves, and the whole machine is deterministic end to end. The question "can the H753 afford a 68000 at all" is closed.
Not proven, and deliberately so: everything that makes an Amiga an Amiga. The chipset gate is 6,229,981 cycles - 65% of the frame for CPU plus Agnus, Copper and bitplane DMA together - and cycle-stolen bus contention is the single riskiest line in the whole plan. Phase 5 is where the stubs start becoming silicon behaviour, one deterministic trace hash at a time.
The Mega Drive story started with a red square and ended with Sonic at native speed. The Amiga story starts with a ROM from 1987 running under budget on a microcontroller, patiently waiting for someone to build it a chipset.