The DMA works perfectly in its own testbench and fails on NanoSoC in a way that takes a week to localise: reads of STATUS, LEN and KEY return an error, a read of COUNT quietly returns STATUS with no error at all, and only CTRL at offset 0x00 reads correctly. The cause is one wire in the wrapper. The DMA decodes byte offsets on an 8-bit paddr port, but the integrator connected word addresses, apb_paddr bits 9 down to 2, as is customary for other APB blocks. Offset 4 arrives as 1, which is unmapped, and offset 0x10 arrives as 0x04, which is STATUS, so only CTRL is right. The RTL was right, the bus was right, and the handoff was wrong: it never said, in a checkable form, which address bits the block consumes. The checklist below makes that sentence checkable for every block.
A handoff is a versioned bundle of eight items, and the integrator accepts or rejects the bundle, never individual files. The RTL with its parameters fixed. The register map with offsets, access types and reset values. The clock, reset and power intent: which domains, which resets, which power domain, which enables. The constraint file for synthesis and timing. The CDC and RDC collateral: which crossings the block contains and which it assumes the parent provides. The software header or field list generated from the same map. The verification report: what was proven and what was waived. And the known-limitations note: the errata the block already carries. A bundle missing any one of these is not a handoff; it is a promise that someone else will fill the gap later, which is how word-address wiring meets byte-offset decode.
Handoff item | NanoSoC example | What breaks without it |
|---|---|---|
RTL, parameters fixed | DMA wrapped with WAIT_STATES 1 | Three-clock versus two-clock APB timing assumed wrong |
Register map with reset values | CTRL reads bit 0 as 0, LEN resets to 0 | Driver writes 0x1 and silently disables the interrupt |
Clock, reset, power intent | DMA on clk_sys, rst_sys_n, PD_SW | Gated clock hangs the bus; missing isolation wakes the chip |
Constraints | APB input delays at the slot | Signoff passes, silicon fails at speed |
Crossing inventory | irq_dma needs a 2-flop sync into clk_per | Missed interrupt once in a few thousand jobs |
Generated software view | Header from the same map table | Header from an old map hits the wrong block |
Verification report | Early-s_tlast sets ERR, proven in bench | System test mistakes specified behaviour for a bug |
Known limitations | LEN and KEY ignored while BUSY, no error | Second job silently uses the stale KEY |
Acceptance is a gate with three positions, not a handshake. Complete means every item present, versioned and stamped with the same bundle revision: wire it in. Incomplete means items missing: reject it back to the owner with the missing list, and do not wire around the gap. Waived means an item is knowingly deferred with a named owner and a date: wire it in, but the waiver ages on the integration board and turns red if the date passes. The third position exists because real projects ship handoffs with gaps; the discipline is that a gap without a name and a date is a rejection, not a waiver.
// snippet: the DMA wrapper contract that the handoff must state.
// The DMA decodes BYTE offsets on an 8-bit port, so the wrapper passes
// apb_paddr[7:0] straight through and drops apb_paddr[11:8] (the alias).
// Connecting apb_paddr[9:2] (word addresses) is the classic wiring slip:
// STATUS, LEN and KEY then answer PSLVERR and COUNT reads STATUS with no error.
// dma_paddr <-- apb_paddr[7:0] (byte offsets 0x00..0x10 mapped)
// unused <-- apb_paddr[11:8] (nothing downstream may depend on these)The script below is the gate in executable form: it lists the eight required items, passes a complete NanoSoC DMA bundle, rejects a bundle missing constraints and the crossing inventory, and accepts a waived bundle only when every waiver names an owner and a date. Keep this list beside the integration database and run it on every delivery; it takes milliseconds and it never gets tired of saying no.
REQUIRED = ["rtl", "regmap", "clock_reset_power", "constraints",
"crossings", "software_view", "verification_report", "limitations"]
def accept(bundle, waivers):
provided = set(bundle)
missing = [item for item in REQUIRED if item not in provided]
excused = []
for item in missing:
w = waivers.get(item)
if w and w.get("owner") and w.get("date"):
excused.append(item)
rejected = [item for item in missing if item not in excused]
status = "accept" if not missing else ("waived" if not rejected else "reject")
return status, rejected, excused
good = {item: "rev C" for item in REQUIRED}
assert accept(good, {})[0] == "accept"
thin = {item: "rev C" for item in REQUIRED
if item not in ("constraints", "crossings")}
status, rejected, _ = accept(thin, {})
assert status == "reject"
assert rejected == ["constraints", "crossings"]
status, rejected, excused = accept(
thin, {"constraints": {"owner": "pd", "date": "M3"},
"crossings": {"owner": "cdc", "date": "M3"}})
assert (status, rejected, excused) == ("waived", [], ["constraints", "crossings"])
status, rejected, _ = accept(thin, {"constraints": {"owner": "pd"}})
assert status == "reject" and rejected == ["constraints", "crossings"]
print("PASS: complete accepted, thin rejected, dated waivers excused")Every block in the chip directory carries the same bundle revision across all eight items.
Every waiver names an owner and a milestone date, and no date is past.
The wrapper states, in a comment the checker can grep, which address bits each port consumes.
When a block misbehaves only at the top level, read the bundle before the RTL: diff the register map against the driver's header field by field, confirm the parameter values against the verification report, and check the wrapper's address-bit comment against the block's port widths. The DMA word-address slip is found at once by that reading, because the handoff either states byte offsets, in which case the wrapper is wrong, or states nothing, in which case the handoff was never complete.
Waive only with a named owner and a milestone date, and only if the wrapper can be constrained without it; otherwise reject. Encrypted RTL makes the inventory more important, not less, because you cannot reverse-engineer the crossings.