Skip to content

SystemVerilog · Module 7

Hierarchical References & $root

Symbolic pointers vs wire connections, $root scope, force/release semantics, testbench backdoor patterns, why upward references break encapsulation, and 5 debugging labs.

Module 7 · Page 7.7

Every signal in your design has a unique address in the hierarchy — like a file path in a filesystem. Hierarchical references let you reach any signal from anywhere. $root anchors that path to the top. Used correctly, this is a powerful testbench tool. Used incorrectly, it destroys encapsulation and creates unmaintainable code.

What is a Hierarchical Reference?

Every module instance in SystemVerilog lives at a specific location in the design hierarchy — like a directory in a folder tree. A hierarchical reference is a dotted path that uniquely identifies any signal, parameter, or instance anywhere in that tree.

The syntax mirrors a file path: level1.level2.level3.signal_name where each segment is either an instance name or a generate-block name.

$rootchip_top$root.chip_top OR chip_topcpu_coreu_cpumem_ctrlu_memuart_txu_uartalu_unitu_alureg_fileu_rffifou_fifoadder_nu_addExample hierarchical paths:chip_top.u_cpu.u_alu.sumchip_top.u_cpu.u_alu.u_add.a$root.chip_top.u_mem.u_fifo.countchip_top.u_uart.clk_div
Figure 1 — Every signal has a unique hierarchical address. Start from chip_top (or $root.chip_top) and navigate down with dots. Each segment is an instance name.

$root — The Implicit Top-Level Scope

$root is the implicit, unnamed scope that sits above all module instances in the simulation. It is the absolute starting point of the entire design hierarchy. Any hierarchical reference that begins with $root is an absolute path — it works the same regardless of where in the hierarchy it is written.

SystemVerilog — $root: absolute vs relative paths
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Absolute path — starts from $root ────────────────────────────
// Works identically no matter where this line is written
logic sig_abs = $root.chip_top.u_cpu.u_alu.sum;
 
// ── Absolute path — top module name alone is also absolute ───────
// (chip_top is at the top level — same as $root.chip_top)
logic sig_abs2 = chip_top.u_cpu.u_alu.sum;
 
// ── Relative path — resolved from the current scope ──────────────
// Written inside module cpu_core: 'u_alu' is a local instance
logic sig_rel = u_alu.sum;   // valid inside cpu_core only
 
// ── When are they different? ──────────────────────────────────────
// If the same module is instantiated twice:
//   chip_top.u_cpu_a.u_alu.sum  ← ALU inside first CPU
//   chip_top.u_cpu_b.u_alu.sum  ← ALU inside second CPU
// $root.chip_top.u_cpu_a.u_alu.sum  ← unambiguous absolute path
 
// ── $root in a package or program block ──────────────────────────
// $root is especially useful in program blocks and packages
// where the current scope has no module hierarchy context
program tb_program;
    initial begin
        // Must use absolute path — no relative context in a program
        $display("ALU sum = %h", $root.chip_top.u_cpu.u_alu.sum);
    end
endprogram
$rootImplicit — not a module, not elaboratedchip_top$root.chip_toptb_top$root.tb_topchecker$root.checker$root is the parent of all top-level modules — DUT, testbench, and assertions all share the same $root
Figure 2 — $root is the invisible parent of every top-level instance. The DUT (chip_top), the testbench (tb_top), and any checkers all live directly under $root.

Absolute vs Relative Hierarchical Paths

A hierarchical reference is either absolute (starts from $root or the top-level module name) or relative (resolved from the current module's scope). Understanding the difference prevents confusing path errors.

TypeStarts withResolved fromWorks in
Absolute$root.x or top module nameThe simulation root — always the sameAnywhere: modules, programs, packages, assertions
Relative (downward)An instance name directly below current scopeThe scope where the reference appearsInside the module that owns the instance
Relative (upward)A parent's signal or instance nameMust traverse upward through hierarchyWorks but strongly discouraged — breaks encapsulation
SystemVerilog — Absolute and relative path examples
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ════ Scenario: tb_top monitors signals inside chip_top ═══════════
 
module tb_top;
 
    chip_top  u_dut (...);   // DUT is u_dut inside tb_top
 
    initial begin
 
        // ── Absolute path — most readable ─────────────────────────
        $display("sum (abs) = %h", u_dut.u_cpu.u_alu.sum);
        // 'u_dut' is in tb_top's scope → this is a downward relative path
 
        // ── $root absolute path — unambiguous everywhere ──────────
        $display("sum ($root) = %h", $root.tb_top.u_dut.u_cpu.u_alu.sum);
 
        // ── Access a localparam inside a sub-module ───────────────
        $display("WIDTH = %0d", u_dut.u_cpu.u_alu.WIDTH);
        // parameters and localparams are also accessible via hierarchy
 
        // ── Access a generate-block signal ───────────────────────
        // Inside u_alu: generate for (genvar i=0; i<N; i++) begin : gen_fa
        $display("FA[2] carry = %b", u_dut.u_cpu.u_alu.gen_fa[2].u_fa.cout);
 
    end
 
endmodule

Testbench Backdoor Access

The most common legitimate use of hierarchical references is testbench backdoor access — reading or forcing internal DUT signals that are not exposed as ports. This is valuable for:

Observing Internal State

Read a state machine's current state or a counter's value directly, without adding debug ports to the RTL.

Initialising Memory

Pre-load a RAM array directly into the DUT's internal memory before simulation starts, skipping a lengthy boot sequence.

Force / Release

Override a signal to a specific value (force) to test a corner case, then release it back to normal RTL drive.

Coverage Collection

Collect coverage on internal signals that are not exported, enabling measurement of internal toggle and state coverage.

tb_top$display(u_dut.u_cpu.u_alu.sum);forceu_dut.u_cpu.rst_n = 0READ (observe)FORCE (override)chip_top (u_dut)cpu_core (u_cpu)alu_unit (u_alu)logic [31:0] sum ← observedlogic rst_n ← forced to 0Both signals are internal — no ports exposed
Figure 3 — Testbench backdoor access reads internal signals directly (green dashed) or forces them to a value (red). No ports are added to the DUT. This is simulation-only — not synthesisable.
SystemVerilog — Backdoor Read, Force, Release, and Memory Init
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_top;
 
    chip_top  u_dut (...);
 
    initial begin
 
        // ── ① Backdoor READ — observe internal signal ────────────────
        @(posedge u_dut.u_cpu.clk); // wait on DUT's internal clock
        $display("ALU result = %0h", u_dut.u_cpu.u_alu.result);
        $display("FSM state  = %s",  u_dut.u_cpu.state.name());
 
        // ── ② Backdoor WRITE — inject value directly ─────────────────
        // Works for simulation only — synthesiser ignores hierarchical LHS
        u_dut.u_cpu.u_alu.result = 32'h1234_5678;  // inject test value
 
        // ── ③ FORCE / RELEASE — override then restore ────────────────
        force u_dut.u_cpu.rst_n = 1'b0;   // hold in reset
        #100;
        release u_dut.u_cpu.rst_n;         // let RTL drive it again
 
        // ── ④ Memory initialisation (backdoor) ───────────────────────
        // Pre-load DUT's internal RAM before simulation starts
        for (int i = 0; i < 256; i++)
            u_dut.u_mem.u_ram.mem[i] = 32'h0;    // zero the RAM
        u_dut.u_mem.u_ram.mem[0] = 32'hC001_C0DE; // load boot code
 
        // ── ⑤ Access a parameter via hierarchy ───────────────────────
        $display("ALU width = %0d", u_dut.u_cpu.u_alu.DATA_WIDTH);
 
    end
 
endmodule

Upward References — Why You Should Avoid Them

A child module can technically reference a signal in its parent using a hierarchical path that traverses upward. This is called an upward reference. It works in simulation — but it is one of the worst things you can do in RTL design.

SystemVerilog — Upward reference (wrong) vs port (correct)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ════ WRONG: Upward hierarchical reference ════════════════════════
module alu_unit;          // ← child module
    // Reaching up to read parent's signal — terrible practice
    logic clk = cpu_core.clk;  // ❌ upward reference to parent's 'clk'
    // Problems:
    //  1. alu_unit can ONLY be used inside cpu_core — not reusable
    //  2. You cannot tell the module's inputs by reading its ports
    //  3. Tool may not support this in synthesis
    //  4. Rename cpu_core → cpu_subsystem and it silently breaks
endmodule
 
 
// ════ CORRECT: Use a port ════════════════════════════════════════
module alu_unit (
    input logic clk    // ← receive clk through a proper port
);
    // ✅ Reusable. Self-documenting. Works in synthesis and simulation.
endmodule

Hierarchical References in Assertions

Assertions written in a testbench or checker module often need to observe internal DUT signals — signals that are not exposed as ports. Hierarchical references are the correct and standard way to do this. This is one of the most legitimate uses of hierarchical paths.

SystemVerilog — Assertions using hierarchical references
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_top;
 
    chip_top  u_dut (...);
 
    // ── Alias internal signals for cleaner assertion syntax ──────────
    logic        clk    = u_dut.u_cpu.clk;
    logic        rst_n  = u_dut.u_cpu.rst_n;
    logic [31:0] result = u_dut.u_cpu.u_alu.result;
    logic        valid  = u_dut.u_cpu.u_alu.result_valid;
 
    // ── Concurrent assertion using aliased internal signals ──────────
    // After reset, if valid goes high, result must not be zero
    ap_result_nonzero: assert property (
        @(posedge clk) disable iff (!rst_n)
        valid |-> (result != 32'h0)
    ) else
        $error("ASSERTION FAIL: valid high but result is zero at time %0t", $time);
 
    // ── Assertion directly on hierarchical path (no alias needed) ────
    ap_fifo_no_overflow: assert property (
        @(posedge clk) disable iff (!rst_n)
        !(u_dut.u_mem.u_fifo.full && u_dut.u_mem.u_fifo.wr_en)
    ) else
        $fatal(1, "FIFO overflow detected!");
 
    // ── Cover property on internal state ─────────────────────────────
    cp_busy_state: cover property (
        @(posedge clk)
        (u_dut.u_cpu.state == 3'b101)  // cover the BUSY state
    );
 
endmodule

Rules, Limits, and What You Can Reference

What You Can ReferenceAllowed?Example
Internal signal (logic, wire)Yes — read and observeu_dut.u_cpu.sum
Parameter / localparamYes — read onlyu_dut.u_cpu.DATA_WIDTH
Instance nameYes — as a path segmentu_dut.u_cpu.u_alu
Generate block signalYes — include block indexu_dut.gen_fa[2].u_fa.sum
Force / ReleaseYes — simulation onlyforce u_dut.rst_n = 0;
Backdoor write (=)Yes — simulation only, LHSu_dut.mem[0] = 32'h0;
Synthesisable drive via hierarchyNo — not synthesisableNever use as synthesisable driver
Upward reference in RTLTechnically yes, but always wrongNever write in production RTL
SystemVerilog — Everything you can do with hierarchical references
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Read a signal ─────────────────────────────────────────────
$display("%h", u_dut.u_cpu.result);
 
// ── Wait on an internal event ────────────────────────────────
@(posedge u_dut.u_cpu.clk);
@(u_dut.u_mem.u_fifo.full);           // wait until FIFO is full
 
// ── Read a parameter ────────────────────────────────────────
$display("ALU width: %0d", u_dut.u_cpu.u_alu.DATA_WIDTH);
 
// ── Generate-block path ─────────────────────────────────────
$display("FA[2] carry: %b", u_dut.u_cpu.u_alu.gen_fa[2].u_fa.cout);
 
// ── Backdoor write ──────────────────────────────────────────
u_dut.u_mem.mem_array[0] = 32'hC0DE;
 
// ── Force / Release ─────────────────────────────────────────
force   u_dut.u_cpu.stall = 1'b1;    // override RTL driver
#50;
release u_dut.u_cpu.stall;            // restore RTL control
 
// ── $root in a program block ─────────────────────────────────
program checker;
    initial
        $display("%b", $root.tb_top.u_dut.u_cpu.done);
endprogram

When to Use — and When to Avoid

Use hierarchical references for...

Testbench backdoor access, assertion monitors on internal signals, memory pre-loading, force/release in directed tests, coverage on internal state, and debug display tasks.

Never use them for...

Driving signals in RTL modules, replacing ports with upward references, anything that goes through synthesis. If it is in a .sv file that synthesis sees, use ports instead.

Common Mistakes

SystemVerilog — Hierarchical reference mistakes and fixes
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ════ MISTAKE 1: Upward reference instead of port ════════════════
module alu_bad;
    always_ff @(posedge cpu_core.clk)  // ❌ upward reference to parent
        result <= a + b;
endmodule
// ✅ FIX: pass clk as a port
module alu_good (input logic clk);
    always_ff @(posedge clk) result <= a + b; // ✅ clean port
endmodule
 
 
// ════ MISTAKE 2: Wrong instance name in path ══════════════════════
$display("%h", u_dut.u_core.u_alu.sum);  // ❌ 'u_core' doesn't exist
// — the instance is actually named 'u_cpu'
// ✅ FIX: double-check instance names in the RTL
$display("%h", u_dut.u_cpu.u_alu.sum);   // ✅ correct path
 
 
// ════ MISTAKE 3: Forgetting the generate block index ══════════════
$display("%b", u_dut.u_cpu.gen_fa.u_fa.cout);     // ❌ missing [i]
// gen_fa is an indexed array of blocks
// ✅ FIX: include the index
$display("%b", u_dut.u_cpu.gen_fa[2].u_fa.cout); // ✅ correct
 
 
// ════ MISTAKE 4: Using force without release ══════════════════════
force u_dut.rst_n = 0;
// ... simulation continues forever with rst_n=0 — RTL never recovers
// ✅ FIX: always release after the test
force   u_dut.rst_n = 0;
#100;
release u_dut.rst_n;  // ✅ RTL drives rst_n again

Quick Reference — Hierarchical References Cheat Sheet

SystemVerilog — Hierarchical References Quick Reference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Path syntax ──────────────────────────────────────────────
inst1.inst2.signal_name                  // relative downward path
top_module.inst1.inst2.signal_name       // absolute path
$root.top_module.inst1.inst2.signal_name // absolute from $root
inst1.gen_block[i].inst2.signal          // through generate block
 
// ── Read (observe) ───────────────────────────────────────────
$display("%h", u_dut.u_cpu.result);
@(posedge u_dut.clk);
@(u_dut.u_fifo.full);
 
// ── Alias (cleaner assertions) ───────────────────────────────
logic clk = u_dut.clk;   // alias for readability
 
// ── Force / Release ──────────────────────────────────────────
force   u_dut.signal = value;
release u_dut.signal;
 
// ── Backdoor write ───────────────────────────────────────────
u_dut.mem[0] = 32'h0;   // simulation only
 
// ── Access parameter ─────────────────────────────────────────
u_dut.u_alu.DATA_WIDTH  // read parameter value via hierarchy
 
// ── Rules ────────────────────────────────────────────────────
// • Use for: testbench monitoring, assertions, force, memory init
// • Never use for: RTL drivers, replacing ports, upward refs in RTL
// • $root = implicit top — use for absolute paths in programs/packages
// • Always release after force
// • Generate block path: gen_name[index].instance.signal

Verification Usage — Hierarchical References in the Testbench

Hierarchical references are a verification-side feature first and foremost. They let the testbench see and manipulate DUT internals that don't appear on the module's port list — a register file's storage, a FIFO's pointer, a state machine's current state. Used judiciously, this enables fast initialisation, white-box checks, and surgical fault injection. Used carelessly, it couples the testbench to the DUT's internal structure and breaks every time the DUT refactors.

SystemVerilog — Three legitimate backdoor patterns in a UVM testbench
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Pattern 1: Fast memory init (bypass slow protocol writes) ──
class mem_init_seq extends uvm_sequence;
    task body();
        // Load 64 KiB image directly into the RAM backing storage
        for (int i = 0; i < 16384; i++)
            top.dut.u_l2_cache.u_ram.mem[i] = init_image[i];
        `uvm_info("INIT", "Memory pre-loaded via backdoor", UVM_LOW)
    endtask
endclass
 
// ── Pattern 2: White-box scoreboard — read DUT state for golden compare ──
class arch_state_scoreboard extends uvm_scoreboard;
    virtual function void check_phase(uvm_phase phase);
        // Read DUT's architectural register file via hierarchical reference
        for (int r = 0; r < 32; r++) begin
            if (top.dut.u_cpu.reg_file.x[r] !== expected_x[r])
                `uvm_error("REGCHK",
                    $sformatf("x[%0d]: got %h, exp %h",
                              r, top.dut.u_cpu.reg_file.x[r], expected_x[r]))
        end
    endfunction
endclass
 
// ── Pattern 3: Surgical fault injection — flip a bit, observe recovery ──
task automatic inject_seu(input int reg_idx, input int bit_pos);
    bit [31:0] before  = top.dut.u_cpu.reg_file.x[reg_idx];
    bit [31:0] flipped = before ^ (32'h1 << bit_pos);
 
    force top.dut.u_cpu.reg_file.x[reg_idx] = flipped;
    @(posedge top.clk);
    release top.dut.u_cpu.reg_file.x[reg_idx];
 
    `uvm_info("SEU",
        $sformatf("Flipped x[%0d] bit %0d: %h → %h",
                  reg_idx, bit_pos, before, flipped), UVM_LOW)
endtask

Simulation Behavior — How Hierarchical References Resolve and Drive

A read through a hierarchical reference behaves like any other variable read — it samples the current value at the time of the read. A force through a hierarchical reference is different: it overrides the procedural and continuous drivers of the target signal and holds the forced value until a release reverses it. Understanding the timing of force/release at the simulation-region level is the difference between a successful fault-injection test and a deadlocked DUT.

SystemVerilog — force/release timing inside the SV scheduling regions
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// DUT — normal continuous driver on internal signal
module dut;
    logic [7:0] state;
    always_ff @(posedge clk)
        state <= next_state;          // regular NBA driver
endmodule
 
// Testbench — surgical override of 'state'
initial begin
    @(posedge clk);
    force top.dut.state = 8'h42;      // active from this point
    @(posedge clk);
    @(posedge clk);
    release top.dut.state;            // procedural driver wins again
end

Waveform Analysis — Identifying a force in the Viewer

Modern waveform viewers annotate forced signals visually so you can tell at a glance whether a signal's value is coming from the design or from a testbench override. In Verdi: a small "F" marker next to the signal name; the signal name itself may turn red. In DVE: the signal trace shows a yellow band. In GTKWave: forced segments render in a lighter colour.

Expected waveform — force followed by release on a state register
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                  t=10  t=20  t=30  t=40  t=50  t=60  t=70  t=80
 
clk           __│‾│___│‾│___│‾│___│‾│___│‾│___│‾│___│‾│___│‾│__
 
dut.next_state  01    02    03    04    05    06    07    08
 
dut.state       00 →  01 →  02 →  42 →  42 →  42 →  06 →  07
                                  ^^    ^^    ^^    ^^
                                  forced by TB     released — picks up next NBA
 
                                  └── F annotation in Verdi/DVE here ──┘
 
Reading the trace:
  • t=10–t=30: dut.state follows next_state via the always_ff — normal operation
  • t=30: TB issues 'force top.dut.state = 8'h42;'
  • t=30–t=60: viewer marks the signal as forced; value held at 0x42 regardless of next_state
  • t=60: TB issues 'release top.dut.state;'
  • t=70 onwards: next clock edge writes next_state into the (now unforced) flop normally

Industry Insights — Hierarchical Reference Practices in Production Teams

Debugging Academy — 5 Real Hierarchical Reference Bugs

Each lab is a real failure mode. Buggy code, symptom, root cause, fix.

1

Forgotten release — Forced Signal Outlives the Test

RESOURCE LEAK
Buggy Code
Forgotten release across test boundaries
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SEU injection test — flips a bit, expects DUT to recover
task automatic test_seu();
    @(posedge clk);
    force top.dut.u_cpu.reg_file.x[5] = 32'hDEADBEEF;
    @(posedge clk);
    @(posedge clk);
    check_recovery();
    // ← forgot: release top.dut.u_cpu.reg_file.x[5];
endtask
 
// Later test in the same regression:
task automatic test_arithmetic();
    set_reg(5, 32'h00000001);
    add(5, 5, 6);
    assert (top.dut.u_cpu.reg_file.x[6] == 32'h00000002); // FAILS
endtask
Symptom

test_seu passes. test_arithmetic fails — register 5 reads as 0xDEADBEEF instead of the value it was supposedly written. The failure looks like a register-write bug in the DUT until someone notices register 5 reads as 0xDEADBEEF in every later test, not just one.

Root Cause

force holds until release or end-of-simulation. The test that injected the SEU forgot to release; the force persisted across the test boundary into test_arithmetic. The DUT's always_ff couldn't update x[5] because the force was still active.

Fix

Wrap every force in a task that pairs it with release:

Pair force with release inside a wrapper task
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task automatic force_for_one_cycle(input string path, input bit [31:0] val);
    // Caller-provided hierarchical path via virtual handle or generate-resolved tag
    force /* resolved path */ = val;
    @(posedge clk);
    release /* resolved path */;
endtask

The wrapper guarantees release. Add a regression-end check that asserts no forces are active: enumerate known forced paths in a final block and $fatal on any leftover.

2

Upward Reference Rebinds After Module Reuse

SCOPE DRIFT
Buggy Code
Upward reference binds to closest enclosing scope
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module child;
    // Upward reference — no explicit scope
    always_ff @(posedge clk)
        if (enable)             // reaches up to parent's 'enable'
            counter <= counter + 1;
endmodule
 
module parent_a;
    logic enable;               // child binds to this
    logic clk;
    child u_c();
endmodule
 
// Six months later, child is reused under parent_b:
module parent_b;
    logic enable;               // also has 'enable' — different semantics!
    logic clk;
    child u_c();                // now binds to parent_b.enable
endmodule
Symptom

child works correctly under parent_a. When reused under parent_b, it silently binds to parent_b.enable — a different signal with different timing semantics — and the counter counts at unexpected times. No compile error, no warning that the binding changed.

Root Cause

The upward reference resolves to "closest enclosing scope with this name." Reusing child under a different parent rebinds silently. The child's behavior depends on its environment, violating encapsulation.

Fix
Pass signals through ports — no upward reference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module child (input logic clk, input logic enable);
    always_ff @(posedge clk)
        if (enable) counter <= counter + 1;
endmodule
 
module parent_a;
    logic enable;
    logic clk;
    child u_c(.clk(clk), .enable(enable));   // explicit
endmodule

Pass signals through ports. Add a lint rule rejecting upward references (Spyglass upward_reference) and fail CI on any occurrence. No exceptions.

3

Missing Generate Index — Probe Lands on Iteration 0

AMBIGUOUS PATH
Buggy Code
Path through generate block missing the iteration index
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// DUT has a 4-stage generate-for pipeline
module pipe;
    generate
        for (genvar i = 0; i < 4; i = i + 1) begin : g_stage
            stage_reg u_s();
        end
    endgenerate
endmodule
 
// Test wants to probe stage 3:
initial begin
    if (top.dut.u_pipe.g_stage.u_s.value != expected)  // ← no [3]!
        $error("Stage 3 mismatch");
end
Symptom

Some tools (VCS old versions, Verilator) silently bind to g_stage[0] and the test appears to probe stage 0 instead of stage 3. The bug surfaces as "stage 3 randomly reads the value of stage 0" — actually it always does, but only matters when stage 0 and stage 3 happen to differ. Other tools (Questa, modern Xcelium) error on the ambiguous reference.

Root Cause

Generate-for produces an array of named scopes: g_stage[0], g_stage[1], ..., g_stage[N-1]. The path must include the bracketed index to disambiguate. SystemVerilog LRM doesn't strictly require the error — implementations vary.

Fix

Always specify the index: top.dut.u_pipe.g_stage[3].u_s.value. For all-stage probes, use a generate-for in the testbench:

Generate-for in the testbench probes every stage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
generate
    for (genvar i = 0; i < 4; i = i + 1) begin : g_probe
        initial @(posedge clk)
            if (top.dut.u_pipe.g_stage[i].u_s.value != expected[i])
                $error("Stage %0d mismatch", i);
    end
endgenerate
4

Hierarchical Read Race — Sampling Wrong Side of NBA

TIMING REGION RACE
Buggy Code
Read and write hit the same posedge — Active-region race
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Scoreboard reads DUT state immediately on the clock edge
always @(posedge clk) begin
    if (top.dut.u_cpu.x[5] != expected_value)
        $error("Reg mismatch");
end
 
// DUT also writes x[5] on the same edge:
always_ff @(posedge clk)
    if (we) x[5] <= wdata;
Symptom

Sporadic false-positives on the scoreboard check. The same test passes 9 out of 10 simulator runs, fails on the 10th. Re-running with a different seed doesn't always reproduce. The error message correctly reports that the value doesn't match, but adding $display changes the outcome ("the bug disappears when I add a print statement").

Root Cause

Both the scoreboard's always @ and the DUT's always_ff trigger on the same posedge clk. Within the Active region they execute in implementation-defined order; the scoreboard might read x[5] before or after the DUT's NBA update lands. Race.

Fix

Use a clocking block to sample at the Postponed region, which is after all NBAs have settled:

Clocking block samples in the Postponed region
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clocking cb @(posedge clk);
    input #1step  x_5 = top.dut.u_cpu.x[5];  // Postponed region sample
endclocking
 
initial forever begin
    @cb;
    if (cb.x_5 != expected_value) $error("Reg mismatch");
end

Alternative: read the DUT signal one cycle later via a registered shadow. Both approaches eliminate the race.

5

Renamed DUT Internal — Hundreds of Tests Break Overnight

REFACTOR FALLOUT
Buggy Code
Hierarchical paths hardcoded across many test files
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Hundreds of tests reference internal RAM directly:
test_basic.sv:    top.dut.u_cache.u_ram.mem[i] = init_data[i];
test_seu.sv:      force top.dut.u_cpu.reg_file.x[5] = 0;
scoreboard.sv:    if (top.dut.u_alu.result_r != expected) ...
arch_check.sv:    top.dut.u_cpu.pc_r ...
 
// Architect renames u_cache → u_l2_cache.  Suddenly:
//   Error: hierarchical reference to non-existent scope 'u_cache'
// Hundreds of tests fail to compile. Day-long refactor sprint follows.
Symptom

Every test that uses the renamed path fails to elaborate. CI is red for everyone. "Why did you change the DUT?" "Because it should have been u_l2_cache from the start." Refactor blocked until every test is updated.

Root Cause

Every test file directly hardcoded the DUT's internal hierarchy. The DUT and tests became tightly coupled by name. Any internal rename ripples to every test that uses any path through the renamed scope.

Fix

Refactor the backdoors into a single probe package. Every test calls tb_probe_pkg::read_ram(i) instead of writing top.dut.u_cache.u_ram.mem[i] inline. When the DUT renames, update the package once; every test stays as-is. The cost is one round of mechanical refactoring; the payoff is permanent immunity to internal renames.

Interview Q&A — 12 Questions on Hierarchical References

Drawn from real interviews at chip-design and verification companies. Try to answer before reading each response.

A hierarchical reference is a name that addresses a signal or task by its position in the design's instance tree, using dot-separated scopes. Example: top.dut.u_cpu.reg_file.x[5] — read or write the x[5] storage inside the reg_file instance, inside u_cpu, inside dut, inside top. The reference resolves at elaboration time into a direct pointer to that storage cell.

Best Practices — Hierarchical Reference Rules to Walk Away With

  1. No hierarchical references in RTL. They are testbench-only. Lint rule fails CI on any hierarchical reference outside tb, program, or interface scopes.
  2. Isolate all backdoor access in a single probe package. Tests call tb_probe_pkg::read_x(idx); the package owns every hierarchical path. DUT renames touch one file.
  3. Always pair force with release. Wrap in a task: force_for_cycles(path, val, n) — never expose raw force to test authors.
  4. Use absolute paths in cross-module code. Start with $root. or the top module name. Avoid relative paths that depend on calling context.
  5. Name every generate block. Hierarchical paths through generate blocks need g_name[index]. Unnamed blocks produce tool-dependent paths.
  6. Use bind for read-only DUT observation. Coverage collectors, assertion checkers, monitoring probes — all cleaner via bind than scattered hierarchical reads.
  7. Read DUT signals in the Postponed region. Use a clocking block with input #1step for samples that must land after NBAs. Avoid sampling at the Active region for signals the DUT updates.
  8. No upward references in RTL. Pass everything through ports. Upward references silently rebind on module reuse — lint should reject them as errors.
  9. Add an end-of-test assertion that no forces are leaking. Enumerate known forced paths in a final block; $fatal on any leftover.
  10. Document every hierarchical reference at the call site. A comment explaining why this signal is accessed via the backdoor (e.g., "fast init — not exposed on port") makes the dependency visible to the next reader.