The first fetch from 0x0000_0000, which completes 54.073 us after the supply is good, is the first moment the whole chip does anything together: the PLL path, the release order, the fabric, the ROM and the CPU all have to agree. No sweep reaches it, because no sweep fetches. Its one job is the setup: what code runs against what RTL, under what model, proving what the sweeps cannot.
The run loads the real ROM image at 0x0000_0000, puts the second-stage image in a flash model behind the SPI pins, and releases reset exactly as silicon does: oscillator counter, peripheral release, lock, gates, system release, CPU release, 53.948 us of hardware before the CPU leaves reset. (The oscillator-ready counter is 1,024 clk_per cycles; a bench may shorten it for speed, but the full-length run must exist and must be the one that signs off.) The firmware model is blocking — one access in flight at a time — which matches the ROM code and keeps the run deterministic: same image, same latencies, the same 12.581 ms total for a fixed clock phase, and a spread of at most the 47 ns interrupt window (135.4 to 182.3 ns) across phases. Determinism is a feature, not a limitation; a boot that varies by more than that from run to run is hiding a synchroniser or arbitration race.
What this run proves that nothing else does: the reset vector is readable and executable with a cold instruction cache, the stack top at 0x2001_0000 (one past the end of SRAM0, and the address of the secure-only SRAM1) is used correctly, so the first push lands at 0x2000_FFFC, RST_CAUSE reads through the slow APB segment in 281.25 ns, the UART banner prints at 115,384.6 baud, the DMA self-test moves 64 words with the checksum compared, the SPI loader pulls the 8,192-byte image (8,196 bytes on the wire with the 4-byte command) out of the flash model at 6 Mb/s, and the jump lands on the loaded image. That last claim holds only if the image really arrives through the loader: a run that preloads SRAM0 skips the 10,928 us stage and proves nothing about the loader. A preload variant is useful for iterating on the second-stage image, but it must be labelled as bypassing the loader and never be the run that signs off. The boot table is the pass criterion, stage by stage; any stage that overruns names its wait.
# not-run: what the firmware run needs from the bench, NanoSoC teaching values
# ROM image at 0x0000_0000; a flash model behind the SPI pins that holds the
# 8,192-byte image and answers the 4-byte read command at 6 Mb/s, so the image
# reaches SRAM0 through the real loader; UART output captured to a log; pads
# modelled with their reset pulls (the RTL has none).
# Bench monitors (zero cost to the ROM): timestamp the first fetch, the first
# UART DATA write, the first DMA register access, SPI chip-select falling and
# rising, the first fetch at 0x2000_0000, and irq_dma rising to irq_cpu at the
# core input (135.4 to 182.3 ns; the DMA STATUS write is the clear, not the start).
# Blocking model: one access in flight, latencies from the latency table.# The bench monitor as a checker: expected stage events (boot flow) against the
# events a run reports, with a tolerance. NanoSoC teaching values; the 10 us
# tolerance is ILLUSTRATIVE (the ROM polls TXE between characters: at most
# 16 characters x two clk_per accesses of 281.25 ns = 9 us, plus the three SPI
# register writes, 0.84 us, before chip-select falls).
EXPECT_US = {
"cpu out of reset, first fetch 0x0": 53.948,
"first UART DATA write (start bit)": 65.865,
"banner complete": 1452.531,
"first DMA register access": 1452.531,
"SPI chip-select falls": 1460.917,
"SPI chip-select rises": 12388.917,
"first fetch at 0x2000_0000": 12580.917,
}
TOL_US = 10.0
def check(run):
return [name for name, want in EXPECT_US.items()
if name not in run or abs(run[name] - want) > TOL_US]
good = {k: v + (3.0 if "banner" in k or "SPI" in k or "0x2000" in k else 0.0)
for k, v in EXPECT_US.items()} # real polling makes later events a few us late
assert check(good) == []
# the check can fail: 64 extra ROM-printed characters at 86.667 us each push every later event out by 5.55 ms
CHAR_US = 10 / 115384.6 * 1e6
shift = 64 * CHAR_US
late = {k: v + (shift if v >= 1452.5 else 0.0) for k, v in EXPECT_US.items()}
assert abs(shift - 5546.7) < 0.1
assert "SPI chip-select falls" in check(late) and "first fetch at 0x2000_0000" in check(late)
# a preloaded image skips the loader: chip-select never moves, and the check names it
bypass = dict(good)
del bypass["SPI chip-select falls"], bypass["SPI chip-select rises"]
assert check(bypass) == ["SPI chip-select falls", "SPI chip-select rises"]
print(f"PASS: 7 events within {TOL_US:.0f} us; extra prints ({shift / 1000:.2f} ms) and a bypassed loader are caught")The image under test is the real ROM build, not a test-only fork; test hooks are compiled out by a flag the review can see.
The bench models only what silicon has not yet: the flash behind the SPI pins, the UART terminal, the pad pulls — never the chip internals — and the image reaches SRAM0 through the SPI loader, or the run says it bypasses it.
Pass means every bench event matches the stage table within the stated tolerance (a few microseconds, from the TXE polling and the three SPI register writes), not merely that the prompt appears.
A boot that hangs points at the last bench event that fired, which the next lesson places; a boot that is slow points at the slowest stage, usually the SPI load at 86.9 percent. If the run is nondeterministic across seeds, suspect the interrupt timing or the arbitration: the blocking model should make everything else repeatable. Never fix a nondeterministic boot by pinning the seed; fix the race it found.
A simplified program skips the driver order, the self-test and the loader — exactly the sequence bugs the run exists to find. Testing other code proves other code.