Part 7 · Advanced & Integration · Intermediate
Hierarchical References & Force/Release
XMR uses and dangers, force/release for error injection and bring-up, deposit vs force, and keeping cross-boundary access centralized and auditable.
Hierarchical references (XMR): uses and dangers
A cross-module reference like tb.dut.u_core.u_fsm.state lets the testbench observe or drive any signal in the design without ports. It is legitimately powerful — whitebox checkers on internal FSMs, backdoor memory access, gray-box coverage — and legitimately dangerous: every XMR is a hard-coded dependency on someone else's hierarchy that breaks silently in spirit (and loudly at elaboration) when the design refactors.
// Legitimate XMR uses — observation, not control
always @(posedge clk) // whitebox FSM checker
if (tb.dut.u_core.u_fsm.state == ST_ERR)
$error("core FSM entered ST_ERR at %0t", $time);
// Backdoor memory load — seconds instead of minutes of bus traffic
function automatic void backdoor_load(string hexfile);
$readmemh(hexfile, tb.dut.u_mem.u_array.mem);
endfunction
// The danger: design refactor renames u_core -> u_cpu0
// → every scattered XMR fails at elaboration,
// → or worse: a generate/config change makes it bind to the
// WRONG instance and the checker silently checks nothing real.Why scattered XMRs rot
Hierarchy paths are not part of any interface contract — designers rename and restructure without knowing you depend on them.
XMRs typed in twenty files mean twenty edits per refactor; one missed edit is a checker that lies.
XMRs into generate blocks and parameterized instances are fragile against configuration changes.
Synthesis and emulation flows do not honor TB-side XMRs — anything functional hidden in them disappears in those flows.
force/release and deposit
Sometimes observation is not enough — bring-up and error injection need to override design behavior. SystemVerilog gives two distinct tools, and confusing them causes long debug sessions.
// FORCE — continuous override; the signal CANNOT change
// until released (drivers keep evaluating but lose)
task automatic inject_parity_error();
force tb.dut.u_link.rx_parity = 1'b0; // pin it wrong
repeat (4) @(posedge clk);
release tb.dut.u_link.rx_parity; // drivers win again
endtask
// Typical bring-up hack: clock-gate cell not ready yet
initial begin
force tb.dut.u_cgc.clk_en = 1'b1; // document + ticket it!
end
// DEPOSIT — set the value ONCE; normal drivers overwrite it
// on their next evaluation (no lock)
initial begin
tb.dut.u_core.u_fsm.state = ST_IDLE; // procedural deposit on a
// variable: next always_ff
// edge overwrites it
end
// Tools also expose deposit explicitly (vendor: 'deposit', force -deposit).FORCE vs DEPOSIT vs DRIVE
force deposit normal drive
────────────── ─────────────── ─────────────── ─────────────────
duration until release until next continuous /
driver update procedural
drivers fight? drivers LOSE drivers WIN normal resolution
use for error inject, initial state, everything else
bring-up hacks memory preload
danger forgotten force value silently —
= dead signal, reverts sooner
X-prop blocked, than expected
masked real bugKeeping it centralized, auditable, and synthesis-safe
The professional pattern is a single hierarchy access package : every XMR and every force lives in one file, named, documented, and greppable. The rest of the testbench calls functions — it never types a hierarchy path.
// hier_access_pkg.sv — the ONLY file allowed to contain XMRs
package hier_access_pkg;
// Every entry: path, owner, reason, removal condition
endpackage
// In practice (paths need module scope), a bind-style module works:
module hier_access;
// [XMR-001] whitebox FSM state, owner: verif, until: formal covers FSM
function automatic logic [2:0] core_fsm_state();
return tb.dut.u_core.u_fsm.state;
endfunction
// [XMR-002] error injection, owner: verif, test: link_err_test only
task automatic force_rx_parity(logic v);
force tb.dut.u_link.rx_parity = v;
endtask
task automatic release_rx_parity();
release tb.dut.u_link.rx_parity;
endtask
endmodule
// Audit at any moment: grep -rn "tb.dut" --include=*.sv | grep -v hier_access
// → must return NOTHING.Boundary rules
All XMRs and forces in one access module/package, each with an ID, owner, reason, and removal condition.
Every force has a matching release on every exit path — wrap force/release pairs in tasks so they cannot be separated.
Forces are test-scoped, never left in base-test or env code where they silently mask bugs for everyone.
Nothing functional crosses the boundary via XMR — stimulus and checking that must survive synthesis/emulation flows go through real ports and interfaces.
Key takeaways
XMRs are for observation and backdoor speed-ups — powerful, but a dependency on unstable hierarchy.
force locks a signal until release; deposit sets it once and lets drivers reclaim it.
Centralize every XMR and force in one audited file; the TB at large never types hierarchy paths.
Anything that must work in synthesis or emulation cannot live in an XMR.
Common pitfalls
A forgotten force — the signal is frozen for the whole sim, masking the exact bug you were hunting.
Using force where deposit was meant — drivers mysteriously 'stop working' on that net.
XMRs scattered across the TB — one design rename breaks twenty files, or worse, binds to the wrong instance.
Bring-up forces (clock gates, resets) that quietly survive into the regression suite for months.