A cross-module reference such as tb.dut.u_core.state lets the testbench observe any signal without ports. Legitimate uses are whitebox checkers on internal FSMs, backdoor memory preload, and gray-box coverage.
Every such reference hard-codes a dependency on hierarchy that designers restructure freely. A rename breaks the reference at elaboration, and a generate change can rebind it to the wrong instance.
Hierarchy paths are not part of any interface contract.
References scattered across files multiply every refactor edit.
References into generate blocks are fragile against configuration changes.
Synthesis and emulation flows do not honor testbench-side references.
Observation is sometimes not enough: bring-up and error injection must override design behavior. IEEE 1800 provides force and release (and the rarely used procedural assign and deassign). There is no deposit keyword.
A bare cross-hierarchy write such as tb.dut.u_core.state = ST_IDLE is a one-shot procedural write, legal only to a variable. Normal drivers overwrite it at their next evaluation, and it cannot drive a net at all. A variable written by an always_ff may not be written by any other process (IEEE 1800 9.2.2.4), so a one-shot write into an always_ff target is illegal: use the tool's deposit command or VPI there.
Aspect | force with release | One-shot write (tool deposit) | Normal drive |
|---|---|---|---|
Duration | Until the matching release | Until the next driver update | Continuous or procedural per its process |
Drivers fight | Drivers lose while forced | Drivers win at once | Normal resolution |
Use for | Error injection windows, documented bring-up hacks | Initial state, memory preload | Everything else |
Danger | Forgotten force freezes the signal | Value reverts sooner than expected | None specific to this table |
Deposit is tool and VPI terminology for the one-shot write, exposed by some vendors as an explicit command. In portable code it is simply a blocking assignment to a variable.
The professional pattern keeps every cross-hierarchy path in one access module with an ID, an owner, and a removal condition. The rest of the testbench calls functions and tasks, never types a path.
Forces live in paired inject and restore tasks so no exit path skips the release. Forces stay scoped to one test, never in shared base code.
Audit at any moment:
grep -rn "dut.u_" --include=*.sv . | grep -v hier_access
-> must return NOTHING.Anything functional that must survive synthesis or emulation travels through real ports. The access layer is for observation and temporary overrides only, and it complements bind-based checkers.
Whatever its drivers compute. The write held only until the next driver update, which is exactly the one-shot semantic.
It depends on what drives it. A net, or a variable with a continuous driver, takes the driver value at once. A procedurally assigned variable keeps the forced value until its next procedural assignment.
An access layer over a parity link and a free-running state variable. The testbench checks normal tracking, a 4-cycle forced error window, release recovery, and the one-shot write semantic.
timeunit 1ns; timeprecision 1ps;
module link_blk (
input logic clk,
input logic [7:0] rx_data,
output logic rx_parity
);
assign rx_parity = ^rx_data;
endmodule
module core_blk (
input logic clk,
output logic [1:0] state
);
// Plain always, not always_ff: the initial and the testbench's one-shot
// write are other processes, which IEEE 1800 forbids on an always_ff variable.
initial state = 2'b00;
always @(posedge clk) state <= state + 2'd1;
endmodule
module dut (
input logic clk,
input logic [7:0] rx_data,
output logic rx_parity,
output logic [1:0] state
);
link_blk u_link (.clk(clk), .rx_data(rx_data), .rx_parity(rx_parity));
core_blk u_core (.clk(clk), .state(state));
endmodule
module hier_access;
function automatic logic read_parity();
return tb_force_xmr.dut.u_link.rx_parity;
endfunction
function automatic logic [1:0] read_state();
return tb_force_xmr.dut.u_core.state;
endfunction
task automatic hold_parity(input logic v);
force tb_force_xmr.dut.u_link.rx_parity = v;
endtask
task automatic free_parity();
release tb_force_xmr.dut.u_link.rx_parity;
endtask
task automatic init_state(input logic [1:0] v);
tb_force_xmr.dut.u_core.state = v; // one-shot write to a variable
endtask
endmodule