Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
    • DPI-C: Calling C from SystemVerilog
    • bind: Attaching Verification to RTL
    • Macros, Defines & Compilation
    • Plusargs, Files & Runtime Configuration
    • Simulation Performance & TB Memory
    • RTL/TB Boundaries, Races & Timing
      • Anatomy of a TB/RTL Race
      • Clocking Blocks as the Cure
      • Scheduling Regions: the Full Map
      • Hierarchical References & Force/Release
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹RTL/TB Boundaries, Races & Timing
  1. Topics
  2. SystemVerilog
  3. Advanced Language & Integration
  4. RTL/TB Boundaries, Races & Timing
  5. Hierarchical References & Force/Release

Part 7 · Advanced & Integration · Intermediate

Hierarchical References & Force/Release

Reach across the boundary with XMR, force and release without losing auditability.

On this page

    Observe without touching

    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.

    Force, one-shot writes and normal drives

    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.

    Common pitfall

    A forgotten force freezes the signal for the rest of the run and masks the exact bug under hunt. Using force where a one-shot write was meant makes the drivers look mysteriously dead.

    One audited access layer

    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.

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

    Interview

    Same question, two answers. “Why not just force the FSM into the error state?” The junior answer forces inline with no release. The senior answer adds scoped inject and restore tasks to the access layer, releases on every exit path, and first asks whether real stimulus could reach that state instead.

    Check yourself A one-shot write sets a state variable, then the next clock edge arrives. What does the variable read?

    Whatever its drivers compute. The write held only until the next driver update, which is exactly the one-shot semantic.

    Check yourself A forced signal is released. Which value does it take now?

    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.

    Testbench

    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.

    link_blk.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    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
    

    Related topics

    • Bind checkers

    Previous

    210 / 259

    Next: Senior & Interview Preparation

    Up next

    Up nextSenior & Interview Preparation

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Anatomy of a TB/RTL Race
    • Clocking Blocks as the Cure