The last week has its own failure distribution: not deep bugs, which the flow caught months ago, but configuration slips, a clock disabled to save power, a reset released in the wrong order after a hasty ECO. Each hangs the chip instead of erroring, because the final-week chip is misconfigured rather than misdesigned. This lesson names the three classic surprises, shows the watchdog guard in RTL, and states the pre-review sweep that catches the rest. Its one job is surviving the week; pricing the fixes belongs to the ECO topic.
First surprise: the gated-clock hang. Software clears the DMA clock enable to save power, then touches a DMA register; with the clock stopped the block cannot drive PREADY and the bridge waits forever, hanging the CPU with no error and no timeout. The guard has two parts. A 256-cycle watchdog in the AXI-to-APB bridge ends the AXI side of the access with SLVERR (PSLVERR on the APB side) after 256 cycles, so the CPU gets an error instead of a hang. Because the slave clock is known to be stopped, the bridge then forces the APB side to idle; an APB master may not abandon a transfer while the slave is live, so the slave's state is undefined until its clock restarts, and after a timeout software must reset the DMA before reusing it. The preferred guard is the second one: hardware that refuses to gate a clock while the block is busy or while an access to it is outstanding. Second surprise: the reset-order slip, where the CPU leaves reset with the fabric and its first fetch enters a bridge still in reset and is lost. The guard is the release-order assertion that the CPU reset must rise strictly after the system reset. Third: the missing-interrupt miswire, where the DMA interrupt lands on spare input 6 while software waits on input 1. The guard is the connectivity test that forces each source and checks its matching status bit. The sweep before the review runs all three guards plus the map checker; it takes minutes and has veto power over the agenda.
// NanoSoC teaching values: bridge watchdog. After 256 clk_sys cycles
// of one outstanding access with no pready, timeout_err rises and STAYS
// high until access_active falls, so the bridge cannot re-arm or lose
// the event. The bridge uses it to end the AXI side with SLVERR.
module nano_apb_watchdog (
input logic clk,
input logic rst_n,
input logic access_active, // access issued, awaiting pready
input logic pready, // slave ready, ends the access
output logic timeout_err // level, held until access_active falls
);
logic [7:0] count;
logic fired;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 8'd0;
fired <= 1'b0;
end else if (!access_active) begin
count <= 8'd0; // idle: re-arm only when the access ends
fired <= 1'b0;
end else if (pready) begin
count <= 8'd0;
end else if (!fired) begin
if (count == 8'd255)
fired <= 1'b1;
else
count <= count + 8'd1;
end
end
assign timeout_err = fired;
endmodule# The last-week sweep: map, reset order, interrupt wiring, watchdog
# limit. Every check is computed from a table and has a bad input that
# it must reject.
def overlaps(windows):
spans = sorted(windows.values())
return [(a, b) for a, b in zip(spans, spans[1:]) if b[0] <= a[1]]
MAP = {"DMA": (0x4000_0000, 0x4000_0FFF), "SBR": (0x4000_1000, 0x4000_1FFF),
"UART": (0x4000_2000, 0x4000_2FFF)}
assert overlaps(MAP) == []
bad_map = dict(MAP, DMA=(0x4000_0000, 0x4000_1FFF)) # an 8 KB DMA window
assert overlaps(bad_map) != []
def cpu_last(release_ns):
t = release_ns
return t["rst_cpu_n"] > t["rst_sys_n"] > t["rst_per_n"]
good = {"rst_per_n": 42750.0, "rst_sys_n": 53854.0, "rst_cpu_n": 53948.0}
assert cpu_last(good)
assert not cpu_last(dict(good, rst_cpu_n=53854.0)) # same step: the slip
WIRING = {0: "irq_pwr", 1: "irq_dma", 2: "irq_sbr", 3: "irq_uart",
4: "irq_spi", 5: "irq_gpio", 6: None, 7: None}
def wiring_errors(netlist):
return [(i, netlist.get(i), src) for i, src in WIRING.items()
if netlist.get(i) != src]
assert wiring_errors(dict(WIRING)) == []
miswired = {**WIRING, 1: None, 6: "irq_dma"}
assert wiring_errors(miswired) == [(1, None, "irq_dma"), (6, "irq_dma", None)]
def limit_ok(limit, slowest_legal_cycles):
return limit >= 4 * slowest_legal_cycles # margin rule
SLOWEST = 20 # 208.33 ns of peripheral access is about 20 clk_sys
assert limit_ok(256, SLOWEST)
assert not limit_ok(64, SLOWEST)
print("PASS: map, reset order, IRQ wiring, watchdog limit all checked and rejectable")The sweep output attached to the review pack, green, dated within the final week.
The watchdog present in the bridge with its 256-cycle limit documented as a hang guard, not a performance feature, and the busy-guard on clock gating beside it.
No clock-gating change in the final week without the busy-guard proof beside it.
A hung CPU cannot read anything, so the usual first move, reading the clock-enable register, is not available: the CPU is blocked on the bridge, and RSTPWR is a secure-only block anyway. NanoSoC has no debug access path, which is exactly why the watchdog matters. Use what survives outside the CPU. A second bus master or a debug access path, where the chip has one, can read the registers. On NanoSoC, look at the externally visible pins: is the UART banner printed, is RESET_N toggled. Then reset and replay with a trace of register writes before the hang, and read CLK_EN and RST_CAUSE after the reset. Compare the last write before the hang with the surprise list: a write that cleared a clock enable points at the gated-clock hang, a software reset cause mid-transaction points at the partial-reset hang, and the interrupt raw bits against the wiring table cover the miswire. The hang triage in the hardware-debug course generalises this order in The Chip Hangs: What Do You Do?.
The limit is wrong for the clock ratio, or the peripheral legitimately needs more than 256 fast cycles: the peripheral-side access costs 208.33 ns, about 20 system clocks, so 256 is generous on NanoSoC. On a slower fabric, size the limit from the slowest legal access plus margin, and document the arithmetic.