This lesson has one job: show what the integrator signs when the interrupt wiring is declared done. That artefact is a table with one row per controller input: source, wire name, domain, synchroniser, clear mechanism and priority reason. Against it, two real bugs become mechanical to catch: a DMA irq landed on a spare input, and two interrupt clamps missing from the power intent. The lesson ends with the controller itself as a compiling module, so the table and the RTL can be read side by side.
The wiring table is the single source of truth for every interrupt net. Each row starts at the source register bit that raises the level and ends at the controller input number, naming the wire, the source clock domain and the synchroniser it passes through. Isolation belongs in the same table: irq_dma and irq_sbr cross from the switchable domain into the always-on controller, so each needs a clamp cell driven by the isolation enable, and the power-intent check counts exactly 114 clamp cells across the whole chip including these two. A row is finished only when it also names the software clear: the exact status bit whose clearing drops the level, because a source without a documented clear is a future storm.
INTC input | Wire | From (domain) | Clear | Review check |
|---|---|---|---|---|
0 | irq_pwr | RSTPWR (clk_per) | W1C PWR_IRQ bits | Wakes the chip: never masked in sleep. |
1 | irq_dma | DMA (clk_sys) | W1C STATUS DONE or ERR | Sync + clamp + correct input number. |
2 | irq_sbr | SBR (clk_sys) | W1C SBR STATUS | Sync + clamp, same as DMA. |
3 | irq_uart | UART (clk_per) | DATA touch | Same-domain: synchroniser still present. |
4 | irq_spi | SPI (clk_per) | DATA read | Same-domain: synchroniser still present. |
5 | irq_gpio | GPIO (clk_per) | Pin condition removed | Level follows pins: test both edges. |
6-7 | Tied to 0 | n/a | n/a | Connectivity test flags any activity. |
// NanoSoC teaching values: interrupt aggregator sketch (controller core).
// 8 active-high level inputs in, one active-high level out. PENDING is a
// wire (RAW & ENABLE); TOP is the lowest pending index, 0x1F when idle.
// snippet: enables and clear paths are register-backed in the full block.
module nano_intc_agg (
input logic [7:0] raw,
input logic [7:0] enable,
output logic [7:0] pending,
output logic [4:0] top,
output logic irq_cpu
);
assign pending = raw & enable;
assign irq_cpu = |pending;
always_comb begin
top = 5'h1F;
for (int i = 7; i >= 0; i--)
if (pending[i]) top = 5'(i);
end
endmoduleA table in a document is a wish; a table checked by a script is a contract. The checker below encodes the three wiring rules that catch those bugs: every used input has a named non-empty clear, the spares are tied off, and the isolation count includes both cross-domain interrupt wires.
WIRES = [
{"irq": 0, "wire": "irq_pwr", "clear": "W1C PWR_IRQ", "tie": False},
{"irq": 1, "wire": "irq_dma", "clear": "W1C STATUS DONE/ERR", "tie": False},
{"irq": 2, "wire": "irq_sbr", "clear": "W1C SBR STATUS", "tie": False},
{"irq": 3, "wire": "irq_uart", "clear": "DATA touch", "tie": False},
{"irq": 4, "wire": "irq_spi", "clear": "DATA read", "tie": False},
{"irq": 5, "wire": "irq_gpio", "clear": "pin condition", "tie": False},
{"irq": 6, "wire": "tie0", "clear": "", "tie": True},
{"irq": 7, "wire": "tie0", "clear": "", "tie": True},
]
used = [w for w in WIRES if not w["tie"]]
assert len(used) == 6
for w in used:
assert w["clear"], f"input {w['irq']} has no documented clear"
assert w["wire"].startswith("irq_"), f"input {w['irq']} misnamed"
spares = [w for w in WIRES if w["tie"]]
assert [w["irq"] for w in spares] == [6, 7]
assert all(w["wire"] == "tie0" for w in spares)
# Isolation budget from the signal lists: every PD_SW -> PD_AON signal is clamped.
CLAMPED = {"apb_cdc outputs": 12 + 32 + 3, "sram0 port": 14 + 32 + 4 + 2,
"rom port": 12 + 1, "interrupts": ["irq_dma", "irq_sbr"]}
def clamp_count(c):
return sum(v if isinstance(v, int) else len(v) for v in c.values())
assert clamp_count(CLAMPED) == 114
missed = dict(CLAMPED, interrupts=[]) # the interrupts forgotten
assert clamp_count(missed) == 112 != 114 # the audit would flag this
wrong = [dict(WIRES[1], clear="")] + WIRES[2:] # a wire with no documented clear
assert not all(w["clear"] for w in wrong if not w["tie"])
print("PASS: 6 wired inputs cleared and named; spares tied; 114 clamps, 112 flagged")One row per input with source bit, wire name, domain, synchroniser, clear mechanism and priority reason; no row may say to be confirmed.
A connectivity test forces each source and checks the matching RAW bit, so a wire landed on the wrong input fails the same day it is miswired.
The isolation list counts the two interrupt wires explicitly; the power-intent check fails if either clamp is missing.
The per-IRQ test is the whole method: force the source at its register, read RAW, clear the source, read RAW again. When DMA STATUS reads DONE with the DMA pin high but RAW bit 1 reads 0 while bit 6 reads 1, the wire is on the wrong input, which is the miswired-interrupt bug: reconnect to input 1 and add the forcing test so it can never regress. When RAW flickers for one cycle every few thousand jobs yet every simulation passed, suspect the missing synchroniser: add the two flops and sweep the clock ratio in simulation until the flicker is gone.
The irq_dma and irq_sbr clamps: the two interrupt wires from the switchable domain were missed. Entering sleep floats them, RAW shows unknown values, and a spurious irq_cpu wakes the chip in a sleep-wake loop.