A chip that fails its sanity test fails everything after it with the same symptom — nothing answers — and the debug starts in the wrong place. NanoSoC has three aliveness facts to prove before the first register access: the resets released in order with the CPU last, every clock toggling with its gate open, and the isolation cells present and clamping before power moves. Its one job is that aliveness proof: three checks that gate the whole suite, the longest of them the 53.948 us hardware release sequence. A pad reset adds a fourth: a RESET_N pulse must last more than 16 clk_per cycles (666.7 ns) to count, and a longer one must set RST_CAUSE bit 1.
Reset sanity replays the release table with a stopwatch: rst_per_n after the oscillator counter, rst_sys_n after the 16-cycle hold plus two synchroniser cycles, rst_cpu_n 8 system cycles plus 2 CPU cycles after that. The assertion that matters is an ordering one — rst_cpu_n must rise strictly after rst_sys_n — which is planted bug 2 with its fix stated as a check. In the faulty chip rst_cpu_n is released in the same step as rst_sys_n and passes only its own two-flop synchroniser (2 clk_cpu, 10.4 ns), so it rises about 177 ns before rst_sys_n; the failure is "CPU earlier", not "CPU equal", and the strict comparison also rejects the tied case. Software-reset sanity repeats the tail only: SWRST asserts the system and CPU nets for 16 system cycles while the peripheral net stays released, proving the documented scope. Clock sanity opens the CLK_EN gates and requires a toggle on each clock within 100 cycles of its domain, then closes them and proves the failure is bounded rather than a hang: with CLK_EN cleared, a read of DMA STATUS must complete with an error response inside the 256-cycle watchdog, which is planted bug 10 stated as a check, and the hardware must refuse to gate a block that is BUSY. Power sanity counts the isolation cells in the power intent and netlist — 114 expected across the APB bridge outputs, the SRAM0 and ROM ports and the two interrupts — and requires iso_sw_en asserted before any power transition, which is planted bug 6 with its two missing clamps named.
# Aliveness checks as functions over traces and counts, each run on a good
# case and on a planted-bug case. NanoSoC teaching values (reset release and isolation tables).
T_SYS_NS, T_CPU_NS = 1e9 / 96e6, 1e9 / 192e6
def release_times(mode="staged"):
r5 = 18 * T_SYS_NS # rst_sys_n after the 16-cycle hold and 2-flop sync
if mode == "staged": # correct: 8 clk_sys gap plus the clk_cpu synchroniser
r6 = r5 + 8 * T_SYS_NS + 2 * T_CPU_NS
elif mode == "bug2": # released in the common step: only its own 2-flop sync
r6 = 2 * T_CPU_NS
else: # "tied": same instant as rst_sys_n
r6 = r5
return {"rst_sys_n": r5, "rst_cpu_n": r6}
def order_ok(t):
return t["rst_cpu_n"] > t["rst_sys_n"]
good = release_times()
assert abs(good["rst_sys_n"] - 187.5) < 0.01
assert abs(good["rst_cpu_n"] - good["rst_sys_n"] - 93.75) < 0.01
assert order_ok(good)
bug2 = release_times("bug2")
assert not order_ok(bug2) # bug 2: the CPU comes out first
assert abs(bug2["rst_sys_n"] - bug2["rst_cpu_n"] - 177.08) < 0.01 # 177 ns early
assert not order_ok(release_times("tied")) # strict: equal is also a failure
# pad reset: a pulse is a reset only if it lasts more than 16 clk_per cycles (666.7 ns)
T_PER_NS = 1e9 / 24e6
def pad_reset_seen(width_ns):
return width_ns > 16 * T_PER_NS
assert pad_reset_seen(700) and not pad_reset_seen(600)
EXPECT_ISO = {"apb_cdc": 47, "sram0": 52, "rom": 13, "irq": 2}
def iso_gaps(present):
return {g: EXPECT_ISO[g] - present.get(g, 0) for g in EXPECT_ISO if present.get(g, 0) != EXPECT_ISO[g]}
assert sum(EXPECT_ISO.values()) == 114
assert iso_gaps(dict(EXPECT_ISO)) == {}
assert iso_gaps({"apb_cdc": 47, "sram0": 52, "rom": 13}) == {"irq": 2} # bug 6: 112 of 114
SW_RESET_SCOPE = {"rst_sys_n", "rst_cpu_n"} # SWRST never touches rst_per_n
assert "rst_per_n" not in SW_RESET_SCOPE
print("PASS: order holds, bug 2 (177 ns early) and bug 6 are caught, the SW reset scope is two nets")The ordering assertion on the two CPU and system resets is bound in every test, not only in the reset test.
Clock sanity opens each gate and proves the toggle, and proves that a closed gate ends in an error response, not a hang; no access test assumes a clock it has not observed.
The isolation count is checked against the per-group table, so a missing pair shows its group.
If sanity fails, stop: no later failure is meaningful until aliveness passes. A missing clock toggle points at CLK_EN programming, or at a gate whose enable comes from the domain it gates, which can never wake itself. A release-order violation points at the sequencer or a test that releases resets together for speed. An isolation shortfall points at the power-intent description before the RTL, because the cells are generated from it.
Whether software wrote CLK_EN with the DMA bit cleared: with the DMA clock stopped the slave cannot drive PREADY and the bridge waits forever. The fix is a 256-cycle watchdog plus hardware that refuses to gate a BUSY block.