A prototype runs a boot in 100 ms and shows 64 signals; simulation shows all of them and takes minutes. The speed buys long runs and the visibility cost decides whether a failure on them can be explained. Its one job is the trade in numbers: how much of a run a trace buffer can see, how the chip's pins connect to host-side models, and which of the planted bugs a platform can and cannot show.
A trace buffer is a window: probe width times depth is a fixed memory budget, and the window covers depth samples around a trigger. On an illustrative 64-bit by 8,192-sample buffer sampling clk_sys, one window covers 8,192 of the 1,207,768 cycles of a boot, 0.68 percent, so where the trigger sits is the whole game. A trigger on the symptom shows the silence after the bug; a trigger on an earlier consequence of the hypothesis shows the cause. The Hardware Debug lesson linked below teaches the method of aiming and replaying windows; here the arithmetic fixes how much room there is.
The chip's pins are connected to host-side models so that the prototype boots like the chip would: the UART transmit pin to a terminal, the SPI pins to a flash model holding the 8,192-byte image (the loader reads 8,196 bytes with its 4-byte command), the GPIO pins to scripted levels, the reset pin to a script that can hold it as long as a lab bench would. In emulation the same models run as transactors beside the design. The models are part of the test: a flash model that answers faster than the real part hides a loader bug.
# Trace-window sizing and the platform coverage check. The buffer size is
# ILLUSTRATIVE; cycle counts and the 72-cycle DMA job come from the boot flow.
PROBES, DEPTH = 64, 8192 # 64 bits wide, 8,192 samples: a 512 Kbit budget
BOOT_CYCLES = round(12580.917 * 96)
coverage = DEPTH / BOOT_CYCLES * 100
window_us = DEPTH / 12e6 * 1e6 # sampled at the prototype's 12 MHz clk_sys
print(f"window {DEPTH} samples = {window_us:.0f} us on the prototype; {coverage:.2f}% of one boot")
assert PROBES * DEPTH == 524_288
assert abs(coverage - 0.68) < 0.01
# a window triggered on irq_dma must hold the whole 72-cycle job plus the interrupt path
JOB, IRQ_PATH_MAX, MARGIN = 72, 35 / 2, 40 # irq: 35 clk_cpu cycles = 17.5 clk_sys
need = JOB + IRQ_PATH_MAX + MARGIN
assert need < DEPTH # fits with a large margin
# Which planted bugs each platform can show, as a small capability model (ILLUSTRATIVE
# requirement sets): a platform finds a bug if it has every capability of one of the bug's
# alternatives. The emulator keeps the staged reset only if its wrapper does.
PLATFORMS = {
"rtl sim": {"firmware", "staged-reset"},
"gate+power sim": {"firmware", "staged-reset", "timing-checks", "power-intent"},
"emulator": {"firmware", "staged-reset"},
"fpga": {"firmware", "real-flops"}, # a board reset releases everything at once
}
NEEDS = { # each bug: alternative capability sets, any one is enough
1: [{"firmware"}], # overlap: any run that writes SBR CTRL
2: [{"staged-reset"}], # reset order
3: [{"firmware"}], 4: [{"firmware"}], # wiring seen by the boot
5: [{"real-flops"}, {"timing-checks"}], # unsynchronised crossing
6: [{"power-intent"}], # missing isolation
7: [{"firmware"}], 8: [{"firmware"}], 9: [{"firmware"}], 10: [{"firmware"}],
}
def finds(platform, bug):
return any(need <= PLATFORMS[platform] for need in NEEDS[bug])
def found_by(bug, platforms=PLATFORMS):
return [p for p in platforms if any(n <= platforms[p] for n in NEEDS[bug])]
assert all(found_by(b) for b in NEEDS) # every planted bug has a platform that can show it
assert found_by(6) == ["gate+power sim"] # only the power-aware run
assert found_by(2) == ["rtl sim", "gate+power sim", "emulator"] # the board hides it
assert not finds("rtl sim", 5) and not finds("emulator", 5) # no real flops, no timing checks
assert finds("gate+power sim", 5) and finds("fpga", 5) # timing checks can flag it; real flops rarely fail
# the check can fail: take the power intent away and bug 6 has no platform left
no_power = {p: caps - {"power-intent"} for p, caps in PLATFORMS.items()}
assert found_by(6, no_power) == []
# what the SHIPPED BOOT exercises: it never writes CLK_EN = 0, mismatches a LEN, or writes
# while BUSY, and the ROM clears NS, so bugs 7 to 10 need directed tests
TRIGGER = {1: "sbr-start", 2: "reset-release", 3: "irq-wait", 4: "dma-register-read",
7: "ns-left-default", 8: "len-mismatch", 9: "write-while-busy", 10: "clk-en-zero"}
BOOT = {"sbr-start", "reset-release", "irq-wait", "dma-register-read"}
boot_finds = sorted(b for b, t in TRIGGER.items() if t in BOOT)
assert boot_finds == [1, 2, 3, 4], boot_finds
assert sorted(b for b, t in TRIGGER.items() if t in BOOT | {"ns-left-default"}) == [1, 2, 3, 4, 7]
print("PASS: window sized; bug 6 only by the power-aware run, bug 5 by timing checks or real flops, the boot finds 1 to 4")Planted bug | RTL simulation | Gate-level and power-aware simulation | Emulator | FPGA prototype |
|---|---|---|---|---|
1 Address overlap | Sweep finds it | Not needed | Any run that writes SBR CTRL (the boot does) | Same, slower to diagnose |
2 Reset release order | Assertion finds it (the boot hangs too) | Gate-level finds release skew | Found if the reset staging is kept | Hidden if the board resets everything at once |
3, 4 Interrupt input, byte-offset wiring | Sweeps find them; the boot self-test too | Not needed | The boot finds them | The boot finds them, with few probes |
5 Unsynchronised crossing | Misses it unless the bench randomises clock phase and models a violation as X; structural CDC checking is the guard | Can flag a timing violation on the path (the library check turns the flop output to X) | Passes: the model has no metastability | May fail rarely: real flops, different clocks |
6 Missing isolation | Passes | Finds it in one SLEEP entry | Passes | Passes: no power domains |
7, 8, 9, 10 NS default, LEN mismatch, BUSY writes, CLK_EN = 0 | Directed tests find them; the shipped boot does not exercise them | Not needed | Directed tests, fast | Directed tests, few probes |
Pad bugs P1 to P3 (pulls, simultaneous switching) | Not shown: no pad cell in the RTL | Not shown | Not shown | Not shown: an FPGA I/O cell does not reproduce the pad pulls or the SSO limit; board and silicon only |
Two cautions. The shipped boot exercises only bugs 1 to 4: its self-test writes one LEN to both blocks, writes 0x3, clears 0x6 and never writes CLK_EN = 0, and its ROM clears NS, so bugs 7 to 10 are found by the directed tests of the top-level list, not by the boot (bug 7 would appear in the boot only if the ROM forgot to clear NS). And the last row is why the board bring-up topic exists: the pad pulls, the two supply rails and the simultaneous-switching limit are properties of the pad ring and the board, and no pre-silicon platform here shows them.
Each trace window is sized against the event it must hold, with the trigger and the pre- and post-trigger split written next to it.
Every pin model has a stated timing: the flash model runs at the real 6 Mb/s rate (scaled with the clocks), not instantly.
The coverage table above exists for the real project (it matches what the boot exercises, not what it could), and a bug with only one platform that can show it carries an owner for that platform.
Check the setup before trusting a failure. Put a known pattern through each pin model and confirm the UART terminal reads the 16-character banner at the scaled baud rate, that the flash model returns the image the checksum expects, and that the trigger fires on the same cycle in two runs. A trigger that moves between runs means the run is not deterministic, which usually means an uncontrolled input or a free-running board clock, and replay-based debugging is impossible until that is fixed.
RTL simulation models flops as ideal, so setup and hold violations never produce metastability and every run passes (gate-level timing checks can still flag the path). Real flops on a real board can go metastable, so a long soak can expose it as a rare failure; but with scaled clocks and different cells the failure rate is not the silicon's, so a clean prototype soak does not prove the crossing safe. The structural CDC check does.