A boot that hangs with no output is a twelve-millisecond haystack. Checkpoints turn it into a numbered stage: the last event the bench saw is the last thing that worked, and the next stage in the table is the suspect. Its one job is the checkpoint plan: where each event is observed, what the self-test covers, and how to read a silent hang.
The shipped ROM prints exactly one thing: the 16-character banner NanoSoC boot ok with its newline. Every other checkpoint is a bench-side monitor event, a timestamp on a bus or pin observation that costs the ROM nothing: the RST_CAUSE read (the first access to the clk_per segment, which decides whether the cold-boot stack clear runs at all), the first UART DATA write, the first DMA register access, SPI chip-select falling, and the first fetch at 0x2000_0000. The banner is the one stage that is also visible on a pin, 1,386.667 us at 115,384.6 baud, 11.0 percent of the whole boot and the second largest share. The self-test and loader events that follow are bus and pin observations too. Then the DMA self-test of boot step 13: five reads proving reset values are zero, seven writes loading KEY, LEN of 64, CTRL 0x3 and the SBR addresses, the 72-cycle job, the interrupt latency at 135.4 ns, and the read-back of all 64 words against the checksum. Then the SPI load of 8,192 bytes plus the 4-byte command (8,196 bytes at 6 Mb/s), the checksum over 2,048 words, and the jump.
The self-test earns its place by touching every driver contract the later software depends on: the CTRL mirror that demands 0x3 not 0x1, the W1C clear of both DONE and ERR, the two LEN registers written from one variable, and the NS bit cleared in the same CTRL write so the SBR may write the secure-only SRAM1. A self-test that passes with the wrong KEY or a mismatched LEN is worse than none, because it certifies the driver order it should be checking; the checksum comparison is what makes it a test rather than a printout.
# Boot-stage audit from NanoSoC teaching values: recompute the 12.581 ms total
# and each stage share, so a slow boot names its stage.
T_PER_NS, T_SYS_NS, T_CPU_NS = 1e9 / 24e6, 1e9 / 96e6, 1e9 / 192e6
stages = {
"oscillator 1024 clk_per": 1024 * T_PER_NS,
"pll lock 260 clk_per": 260 * T_PER_NS,
"releases + first fetch": (2 + 2) * T_PER_NS + (18 + 8) * T_SYS_NS
+ (2 + 24) * T_CPU_NS,
"stack clear 1024 writes": 1024 * T_SYS_NS,
"rst_cause read": 281.25,
"uart init 3x281.25ns": 3 * 281.25,
"banner 160 bits/115384.6": 160 / 115384.6 * 1e9,
"dma self-test": 625 + 875 + 72 * T_SYS_NS + 135.4 + 6000,
"spi load (8192+4) B at 6Mb/s": (8192 + 4) * 8 / 6e6 * 1e9,
"checksum 2048 reads": 2048 * 18 * T_CPU_NS,
}
total_us = sum(stages.values()) / 1000
for name, ns in stages.items():
print(f"{name}: {ns / 1000:.3f} us ({ns / sum(stages.values()) * 100:.1f}%)")
assert abs(total_us - 12580.917) < 1.0, total_us
assert stages["spi load (8192+4) B at 6Mb/s"] / sum(stages.values()) > 0.86
# the check can fail: checkpoints printed by the ROM would add 64 characters of 86.67 us each
extra_us = 64 * 10 / 115384.6 * 1e6
assert abs(extra_us - 5546.7) < 0.1 and abs(total_us + extra_us - 12580.917) > 5000
print("PASS: boot totals 12.581 ms, SPI load dominates at 86.9%; ROM prints would add 5.55 ms")Every boot stage has a bench monitor event with an expected time, so the last event seen always names the failing stage; the ROM is not asked to print anything beyond the banner.
The self-test compares data, not just status bits: the 64-word read-back against the XOR checksum.
The banner text and length are asserted byte for byte — 16 characters including the newline — as the first UART proof.
Read the monitor log backwards from the hang: the missing event is the diagnosis. No UART DATA write means reset, clocks or the early ROM stages; a write but no banner on the pin means UART init or clocks; banner without self-test done means the DMA contract — check LEN agreement and the NS bit first; self-test done without prompt means the loader or the checksum. A self-test that fails only with certain keys reruns with the all-ones and walking-one keys before anyone suspects the fabric.
The SBR LEN is shorter than the DMA LEN (planted bug 8, case A): the SBR's last beat arrives before the DMA's LEN-th word, so the DMA ends the job early and sets ERR, with COUNT equal to the SBR LEN. If instead the DMA LEN were the shorter one, the DMA would report DONE and the SBR would stay BUSY with no ERR. The driver rule is one n_words variable written to both, and COUNT compared with the SBR LEN after DONE.