The DMA's 8-bit address port inside a 12-bit APB slot is the canonical wrapper problem: somebody must drop four bits, document the sixteen alias images that result, and guarantee no slave logic depends on the dropped bits. If that logic hides inside the block, the next redelivery can change it; if it hides in the top-level wiring, no single file owns it. The wrapper is the file that owns it. The questions to settle are where that boundary runs, what the wrapper contains, what stays in the block, and why the distinction survives redelivery.
The wrapper translates everything the block got wrong for this chip without changing what the block is. Address slicing and alias documentation belong here: the DMA wrapper passes apb_paddr bits 7 down to 0 and ignores bits 11 down to 8, and states the sixteen images as fact. Unused-protocol tying belongs here: the DMA has no PSTRB and no PPROT, so the wrapper ties the fabric-side expectations and documents that every write is a full 32-bit write. Clock gating and reset synchronising belong here: the DMA clock enable from CLK_EN and the synchronised rst_sys_n enter through the wrapper, because the block's reset is asynchronous-assert with no internal release synchroniser. Power isolation belongs here: the irq lines leaving the switchable domain pass wrapper cells that clamp to zero. What never belongs here is behaviour: the GO-while-busy rule, the W1C semantics, the same-cycle DONE. Behaviour stays in the block and in its tests; the wrapper only translates the boundary.
Ownership follows the boundary. The block team owns the block and may redeliver it; the integrator owns the wrapper and reviews it as project RTL with the same lint, CDC and constraint treatment as any other project file. The checker below enforces the split mechanically. It takes the port list from the delivered module (here parsed from the module header text; in a real flow from the RTL or the register and port manifest, never from a hand-copied list) and fails on any port with no wrapper rule and on any rule that names a port the block no longer has. A redelivery that adds a port is therefore a build error rather than a silent tie.
DELIVERED_V1 = """module dma_dut (
input logic clk,
input logic rst_n,
input logic [7:0] paddr,
input logic psel,
input logic penable,
input logic pwrite,
input logic [31:0] pwdata,
output logic [31:0] prdata,
output logic pready,
output logic pslverr,
input logic [31:0] s_tdata,
input logic s_tvalid,
input logic s_tlast,
output logic s_tready,
output logic [31:0] m_tdata,
output logic m_tvalid,
output logic m_tlast,
input logic m_tready,
output logic irq
);"""
WRAPPER_RULES = { # each block port mapped to its wrapper treatment
"clk": "gate with CLK_EN bit 0", "rst_n": "sync release in clk_sys",
"paddr": "slice apb_paddr[7:0], document alias A1",
"psel": "from slot-0 select", "penable": "pass through",
"pwrite": "pass through", "pwdata": "pass through",
"prdata": "pass through", "pready": "pass through",
"pslverr": "pass through",
"s_tdata": "from sbr read engine", "s_tvalid": "from sbr",
"s_tlast": "from sbr", "s_tready": "to sbr",
"m_tdata": "to sbr write engine", "m_tvalid": "to sbr",
"m_tlast": "to sbr", "m_tready": "from sbr",
"irq": "isolate clamp-0 into clk_per domain",
}
def ports_of(rtl):
names = []
for line in rtl.splitlines():
line = line.strip().rstrip(",);")
if line.startswith(("input", "output")):
names.append(line.split()[-1])
return names
def audit(rtl, rules):
ports = ports_of(rtl)
no_rule = [p for p in ports if p not in rules]
stale = [r for r in rules if r not in ports]
return no_rule, stale
assert len(ports_of(DELIVERED_V1)) == 19
assert audit(DELIVERED_V1, WRAPPER_RULES) == ([], [])
assert "alias A1" in WRAPPER_RULES["paddr"]
# redelivery that adds a port: the check must fail loudly
V2 = DELIVERED_V1.replace(" output logic irq", " output logic irq,\n input logic dbg_en")
no_rule, stale = audit(V2, WRAPPER_RULES)
assert no_rule == ["dbg_en"] and stale == [], (no_rule, stale)
# redelivery that removes a port: the stale rule is reported
V3 = DELIVERED_V1.replace(" input logic s_tlast,\n", "")
no_rule, stale = audit(V3, WRAPPER_RULES)
assert no_rule == [] and stale == ["s_tlast"], (no_rule, stale)
print("PASS: clean delivery passes; added port and removed port are each reported by name")The wrapper file list with one wrapper per instantiated block, each reviewed as project RTL.
Every block port traced to a wrapper rule; no port tied or sliced without a comment stating why.
No behavioural logic in any wrapper: translators and ties only, nothing that could change a protocol outcome.
Diff the wrapper against the block port list on every redelivery: a new or renamed port with no wrapper rule is reported by the checker above. When a block-level test passes but the integrated test fails, read the wrapper before the block; the translation is guilty until proven innocent, and the usual suspect is a sliced or tied bit whose comment describes the old revision.
A comment with the reason and a connectivity-checker entry listing the tie explicitly. An untied-looking input tied silently is how upper address bits get grounded and high memory aliases low; the tie list is reviewed tie by tie.