A signal that enters a partition only to leave it again is the quietest budget thief in the book. It is called a feedthrough: a test mode threaded through the fabric, the SBR interrupt (irq_sbr, controller input 2) routed via an unrelated wrapper, a clock enable daisy-chained through three blocks. Each one couples three things that should be independent — the source, the carrier and the sink — so a late change at either end re-opens the middle. Nothing crosses a NanoSoC boundary without a consumer, and feedthroughs are counted at review with the goal of zero.
Four things may cross a boundary. Clocks and resets cross through dedicated trees into synchroniser or release cells, never as ordinary logic inputs. Power intent crosses as named control: isolation enables, switch controls and power-good returns. Protocol channels cross whole, with every signal of the interface in one place, never split across two boundaries. Configuration crosses as quasi-static values set before operation and never toggled during traffic. Everything else, and above all a signal that merely passes through, is refused a crossing until it is rerouted around the partition or given a genuine consumer inside.
Crossing | Allowed how | NanoSoC example |
|---|---|---|
Clock and reset | Clock through the clock tree; reset through a release cell per domain | clk_sys and rst_sys_n into every system-side block |
Power control | Named intent signals with an owner | iso_sw_en into the APB bridge and SRAM0 wrapper |
Protocol channel | Whole interface, one boundary, one owner | Full APB bundle across the system-to-peripheral bridge |
Quasi-static config | Set before traffic, synchronised once | Clock-enable bits from the reset controller |
Feedthrough | Not allowed in RTL; reroute or consume | irq_sbr must not transit an unrelated wrapper |
Suppose irq_sbr were wired through the SBR's neighbour wrapper on its way from the stream bridge to the interrupt controller: the wrapper would carry a port it never uses, and any change to interrupt routing would re-verify it. The bad version below does exactly that — the through port is marked so the checker can flag it — and the fixed version routes the signal around the wrapper, straight from the top level to the controller. A wrapper exposes only ports its core consumes; the audit after the modules proves the through port is gone and the signal count is conserved.
// bad-example: irq_sbr transits a wrapper that never uses it (a feedthrough).
module nano_sbr_neighbour_bad (
input logic clk,
input logic rst_n,
input logic [7:0] paddr,
input logic psel,
input logic irq_sbr_in,
output logic [31:0] prdata,
output logic irq_sbr_out
);
logic [31:0] core_reg;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) core_reg <= 32'h0;
else if (psel) core_reg <= core_reg + 1'b1;
end
assign prdata = (paddr[7:0] < 8'h14) ? core_reg : 32'h0;
assign irq_sbr_out = irq_sbr_in; // through port: no logic consumes it
endmodule
// Fixed: the wrapper exposes only ports its core consumes; irq_sbr stays outside.
module nano_sbr_neighbour (
input logic clk,
input logic rst_n,
input logic [7:0] paddr,
input logic psel,
output logic [31:0] prdata
);
logic [31:0] core_reg;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) core_reg <= 32'h0;
else if (psel) core_reg <= core_reg + 1'b1;
end
assign prdata = (paddr[7:0] < 8'h14) ? core_reg : 32'h0;
endmodule
module nanosoc_irq_top (
input logic clk,
input logic rst_n,
input logic irq_sbr_src,
output logic [31:0] sbr_rdata,
output logic irq_to_intc
);
// irq_sbr routes AROUND the wrapper, never through it.
assign irq_to_intc = irq_sbr_src;
nano_sbr_neighbour u_sbr (
.clk(clk), .rst_n(rst_n), .paddr(8'h04), .psel(1'b1), .prdata(sbr_rdata)
);
endmodule# Connectivity audit: a port is a feedthrough if its only fanout is an output.
bad = {"clk": ["core"], "rst_n": ["core"], "paddr": ["core"],
"psel": ["core"], "prdata": ["core"],
"irq_sbr_in": ["irq_sbr_out"]} # in -> out, never touches the core
fixed = {"clk": ["core"], "rst_n": ["core"], "paddr": ["core"],
"psel": ["core"], "prdata": ["core"]} # irq_sbr wired at the top level
def feedthroughs(netlist):
return sorted(p for p, outs in netlist.items()
if outs and all(o.endswith("_out") for o in outs)
and "core" not in outs)
assert feedthroughs(bad) == ["irq_sbr_in"], feedthroughs(bad)
assert feedthroughs(fixed) == [], feedthroughs(fixed)
# Signal conservation: the rerouted signal still exists, at the top level.
top_level_nets = set(fixed) | {"irq_sbr_src", "irq_to_intc"}
assert {"irq_sbr_src", "irq_to_intc"} <= top_level_nets
print("PASS: bad form has 1 feedthrough (irq_sbr_in); fixed form has 0, nets conserved")The boundary signal list fits on one page per partition, with each signal tagged clock, reset, power, protocol or config.
Feedthrough count is reported and the target is zero; each remaining one names its removal date.
No protocol channel is split: all its signals cross the same boundary in the same place.
Run the port-use audit on every wrapper: elaborate the block, trace each port inward, and flag any port that reaches a top-level output without touching logic. For protocol channels, check bundle completeness at each boundary: an APB crossing with address but no ready signal is a split channel and a hang waiting to happen. When a feedthrough is found, move the wire to the top level and re-run the audit; the count must drop by exactly one with no new ports added.
Quasi-static configuration deserves care because it looks like a feedthrough but is not: clock-enable bits and power-intent signals are set before traffic and never toggle during it, so they cross boundaries legally provided each is synchronised once at the destination. The test is toggle rate — if the signal can change mid-operation it is a protocol channel and needs the full handshake or FIFO, not a bare synchroniser. Document the toggle assumption beside each config crossing; a later feature that toggles it dynamically reopens the boundary. A logical feedthrough (an RTL port passing through untouched) is a design smell; physical feedthroughs that the floorplan inserts for routing are normal, and should be counted and budgeted rather than banned — see Budgeting Partitions and Their Interfaces. For choosing and documenting quasi-static crossings see Quasi-Static Signals and Configuration.
It is a feedthrough: three partitions re-verify whenever test logic changes, with no logic inside consuming it. Route it at the top level to each consumer directly, or give each partition its own registered test entry.