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.

systemverilog
// 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.

systemverilog
// 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).
diagram
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 bug

Keeping 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.

systemverilog
// 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

  1. All XMRs and forces in one access module/package, each with an ID, owner, reason, and removal condition.

  2. Every force has a matching release on every exit path — wrap force/release pairs in tasks so they cannot be separated.

  3. Forces are test-scoped, never left in base-test or env code where they silently mask bugs for everyone.

  4. 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.