On the first NanoSoC power-down test, the chip entered SLEEP and woke at once in an endless loop, and the interrupt controller showed a pending DMA interrupt nobody had raised. Two interrupt wires from the dead domain were floating, the always-on controller read them as asserted, and the wake logic obeyed. The fix was two missing clamps out of 114. This lesson enumerates all four groups so that no wire is left to float.
Isolation sits on every signal driven from PD_SW into PD_AON and forces it to a known value while the switch is open. On NanoSoC every clamp drives 0: address and data buses idle at zero, control strobes idle inactive, and both interrupt lines idle deasserted, since the interrupts are active-high levels. Clamp-to-1 would assert an interrupt all through SLEEP; clamp-to-latch would hold whatever the last transaction left, including a pending interrupt. The polarity rule is absolute for this chip: every clamp is 0, driven by iso_sw_en, and the script below proves the count group by group.
Group | Signals | Cells |
|---|---|---|
APB bridge clk_per outputs | paddr 12, pwdata 32, psel, penable, pwrite 3 | 47 |
SRAM0 macro port | addr 14, wdata 32, byte-enable 4, chip-enable and write-enable 2 | 52 |
ROM port | addr 12, chip-enable 1 | 13 |
Interrupts | irq_dma, irq_sbr | 2 |
Total | 114 |
Two groups surprise first-time reviewers. The SRAM0 group is the largest because a 14-bit address plus 32-bit data plus enables crosses the array-versus-wrapper split on every access; the wrapper sleeps while the arrays stay awake. The interrupt group is the smallest and the most dangerous: two wires, and leaving them out produces the sleep-wake loop above. Signals in the other direction, read data back to PD_SW and irq_cpu into the CPU, need no isolation and are not counted.
# Isolation census, NanoSoC teaching values (NanoSoC isolation table).
GROUPS = {
"apb_cdc": {"paddr": 12, "pwdata": 32, "ctrl": 3},
"sram0": {"addr": 14, "wdata": 32, "be": 4, "en": 2},
"rom": {"addr": 12, "ce": 1},
"irq": {"irq_dma": 1, "irq_sbr": 1},
}
EXPECT = {"apb_cdc": 47, "sram0": 52, "rom": 13, "irq": 2}
for g, sigs in GROUPS.items():
assert sum(sigs.values()) == EXPECT[g], (g, sum(sigs.values()))
total = sum(EXPECT.values())
assert total == 114, total
# Crossing list (every signal PD_SW drives into PD_AON) against the clamp list.
crossings = {g + "." + n + str(i) for g, sigs in GROUPS.items()
for n, w in sigs.items() for i in range(w)}
clamped = set(crossings)
assert len(crossings) == 114 and crossings == clamped
# Injected bug: the two interrupt clamps are missing (112 of 114 present).
missing = crossings - {c for c in clamped if not c.startswith("irq.")}
assert missing == {"irq.irq_dma0", "irq.irq_sbr0"}, missing
assert len(clamped) - len(missing) == 112
print("PASS: 47 + 52 + 13 + 2 = 114 clamps; dropping the irq pair leaves 112 and names both nets")The clamp list is generated from the crossing list, and both total 114 with the four groups named.
Every clamp is specified clamp-to-0 with the enable iso_sw_en; any clamp-to-1 or latch needs a written reason, and on NanoSoC there is none.
The two interrupt clamps are present and separately signed off, because their absence is the loop bug.
Run the low-power static check over the intent: every net crossing from PD_SW supply to PD_AON supply must pass through an isolation cell, with no waivers on the interrupt pair. Then confirm in power-aware simulation that entering SLEEP shows no X on any PD_AON input and that the interrupt controller's RAW register stays zero through the whole sleep. X on a clamped net means the enable is late or missing; X with the enable asserted means the clamp value is wrong.
Reject. A latch cell is larger than an AND clamp, needs its own control, and holds a stale address; since ce is clamped to 0, the address value is irrelevant. Clamp-to-0 keeps every post-wake access explicit.