Abstract duties fix nothing until they have names. The DMA interrupt fires at its own block, the controller never sees it, both blocks pass their own tests, and the fix turns out to be one wire plus one new test. Walking that triage once, against the named owners below, gives a pattern for every misconnection in the course.
The cast is small enough to hold in one table. The CPU, its cache and the async AXI bridge belong to the CPU owner. The interconnect, the APB bridge and slot decoder, and the APB clock-domain bridge belong to the integrator directly: the fabric is integration, not a block. The DMA and the stream bridge belong to their block owners, with the integrator owning the stream contract between them. The slow peripherals, UART, SPI and GPIO, belong to one peripheral owner. The interrupt controller belongs to the integrator, because its inputs are everyone else's outputs. The reset, clock and power controller belongs to the integrator for the same reason. The SRAM and ROM wrappers belong to the memory owner, with retention and repair shared with power and test. Software owns the drivers, the linker script, the boot image and the headers, all generated from integrator-owned tables.
Owner | Owns on NanoSoC | Signs |
|---|---|---|
CPU owner | nano_cpu, I-cache, nano_axi_cdc_m CPU side | Core timing, cache behaviour |
Integrator | nano_xbar, bridges, decoders, nano_intc, nano_rstpwr, maps, sequences | Top wiring, tables, release order |
DMA owner | nano_dma internals and bench | Stream and register behaviour per map |
SBR owner | nano_sbr read and write engines | Memory-to-stream contract, error reporting |
Peripheral owner | UART, SPI, GPIO | Register behaviour, interrupt sources |
Memory owner | SRAM and ROM wrappers, macros | Latency, BIST and repair hooks, retention |
Software | Drivers, linker script, boot image, headers | Builds against the frozen tables |
Verification | Chip-level proof | Connectivity, boot, interrupt, DMA end-to-end |
PD | Geometry, clock tree, power grid | Closure against integrator-owned constraints |
The symptom: DMA STATUS reads DONE equals 1, the irq pin at the DMA is high, controller RAW bit 1 is 0, and RAW bit 6 is 1. The block owner confirms irq is irq_en anded with done-or-err, held as a level. The controller owner confirms PENDING equals RAW anded with ENABLE and the output follows. Both true, chip broken. The integrator forces each source in turn and watches RAW: forcing irq_dma lights bit 6, not bit 1. Diagnosis in one step: the wire lands on input 6, a reserved spare, instead of input 1. Fix the wire, tie unused inputs 6 and 7 to zero, and add the standing test: force each of the six real sources and expect exactly its bit, then force the spares and expect nothing.
The script below is that standing test in miniature: the NanoSoC interrupt table as data, a check that every real source maps to a distinct input with spares 6 and 7 tied off, and a demonstration that it catches the exact miswire from the story. One table generates the RTL tie-offs, the software header and this test, which is the single-source rule applied to interrupts.
TABLE = { # source: (input, priority); NanoSoC specification values
"irq_pwr": (0, 0), "irq_dma": (1, 1), "irq_sbr": (2, 2),
"irq_uart": (3, 3), "irq_spi": (4, 4), "irq_gpio": (5, 5)}
SPARES = (6, 7)
def check(table, wired):
problems = []
inputs = [wired[src] for src in table]
if sorted(inputs) != sorted(table[src][0] for src in table):
problems.append("wiring differs from the table")
if len(set(inputs)) != len(inputs):
problems.append("two sources share one input")
if any(s in inputs for s in SPARES):
problems.append("a real source lands on a spare input")
return problems
wired_ok = {src: inp for src, (inp, _) in TABLE.items()}
assert check(TABLE, wired_ok) == []
wired_bug = dict(wired_ok, irq_dma=6) # the story: DMA on spare 6
hits = check(TABLE, wired_bug)
assert hits == ["wiring differs from the table",
"a real source lands on a spare input"], hits
prio = sorted(TABLE.items(), key=lambda kv: kv[1][1])
assert [s for s, _ in prio][0] == "irq_pwr" # power first, lower number wins
print("PASS: irq table distinct, spares free; DMA-on-6 miswire caught")Every top-level wire appears in exactly one generated table, and the table version stamps the RTL, the header and the test.
Spare inputs are tied to zero in the RTL and flagged if ever used.
Triage starts by forcing the source and observing the sink, never by opening a passing block.
Run the per-source force test on every build: six forces, six single-bit expectations, plus the spare check. When a new peripheral arrives, it takes the next free input, the table version bumps, and all three consumers regenerate. If RAW flickers for one cycle instead of holding, suspect the crossing rather than the wire: interrupt inputs must be levels held at least two peripheral-clock cycles, and a pulse shorter than that is missed no matter how correct the wiring is.
Whether a real source is miswired onto 6, as in this lesson's story; then whether the spare is actually tied to zero and whether isolation clamps it during power transitions.