Most programming languages define a scope: a region of code where a name is visible. Scopes keep names in one part of a program from clashing with the same names elsewhere. In Verilog, a new scope is created by every module, function, task, named block and generate block, as described in Verilog Namespace. This article shows how code in one scope can still reach items in another scope, using hierarchical references.
11 min read | Beginner Level
What You'll Learn
- How scopes allow the same name to be reused, and where names must be unique
- How to reach a variable, task or block in another scope with a hierarchical path
- How upward name referencing finds items in modules above the current one
- Where hierarchical references are useful, and why they do not belong in synthesizable RTL
Within one scope, a name can identify only one item. Two variables cannot have the same name, and neither can a variable and a task, or a net and a gate instance. The same name can, however, be used again in a different scope:
module tb;
reg signal;
// Another variable cannot be declared with
// an already existing name in the same scope
reg signal;
// However, the name 'signal' can be reused inside
// a task because it belongs to a different scope.
task display;
reg signal;
begin
signal = 1;
$display("signal = %0b", signal);
end
endtask
endmodule
The second reg signal; in the module is a compile error, because tb already has a signal. The one inside the task is fine: it belongs to the scope of display, and inside the task it hides the module-level signal.
Every identifier in a design therefore has a unique hierarchical path: the names of the scopes that contain it, from the top-level module down, separated by dots. Each module instance, task, function and named block adds one level. The task variable above, for example, is tb.display.signal, while the module-level variable is tb.signal.
Hierarchical Reference Example
A hierarchical reference uses such a path to read or write an item in another scope. The testbench below instantiates two modules, A and B, and accesses items inside them from its own initial block.
module tb;
// Create two instances of different modules
A uA();
B uB();
// Create a named block that declares a signal and
// prints the value at 10ns from simulation start
initial begin : TB_INITIAL
reg signal;
#10 $display("TB signal=%0d", signal);
end
// We'll try to access other scopes using hierarchical
// references from this initial block
initial begin
TB_INITIAL.signal = 0;
uA.display;
uB.B_INITIAL.B_INITIAL_BLOCK1.b_signal_1 = 1;
uB.B_INITIAL.B_INITIAL_BLOCK2.b_signal_2 = 0;
end
endmodule
module A;
task display;
$display("Hello, this is A");
endtask
endmodule
module B;
initial begin : B_INITIAL
#50;
begin : B_INITIAL_BLOCK1
reg b_signal_1;
#10 $display("signal_1=%0d", b_signal_1);
end
#50;
begin : B_INITIAL_BLOCK2
reg b_signal_2;
#10 $display("signal_2=%0d", b_signal_2);
end
end
endmodule
xcelium> run Hello, this is A TB signal=0 signal_1=1 signal_2=0 xmsim: *W,RNQUIE: Simulation is complete.
The second initial block uses three kinds of references, all at time 0:
TB_INITIAL.signalreaches a variable declared in a named block of the same module.uA.displaycalls a task inside the instanceuA, which prints the first line at once.uB.B_INITIAL.B_INITIAL_BLOCK1.b_signal_1goes down through an instance and two levels of named blocks.
The values written at time 0 are still there when the blocks print them, at 10, 60 and 120. Variables declared in named blocks are static in Verilog: they exist for the whole simulation, not only while the block is executing, so they can be written before the block even starts.
A reference that goes from a module into its instances, like the ones above, is called a downward reference. Every path starts with a name that is visible in the current scope, such as an instance or named block, or with the name of a top-level module, as in tb.uB.B_INITIAL.B_INITIAL_BLOCK1.b_signal_1, which works from anywhere in the design.
Synthesis result: Most synthesis tools do not support hierarchical references, and those that do accept only limited forms. Hardware connections between modules must go through ports. Hierarchical references are a simulation tool, used in testbenches, assertions and debug code.
Quick Check
Why does the testbench need the block names B_INITIAL, B_INITIAL_BLOCK1 and B_INITIAL_BLOCK2?
Show Answer
A hierarchical path is made of scope names. An unnamed begin ... end block has no name to put in the path, and in Verilog it cannot declare variables either. Naming the blocks gives b_signal_1 and b_signal_2 a scope and a path, such as uB.B_INITIAL.B_INITIAL_BLOCK1.b_signal_1.
Probing Internal Signals in a Testbench
A testbench normally only sees a design's ports, but sometimes it needs to check internal state, such as the current state of a controller FSM or the fill level of a FIFO. A hierarchical reference like tb.u_dut.u_ctrl.state lets a checker read that state directly, without adding debug ports to the design. Testbenches also use hierarchical references with force and release to put an internal signal into an error condition that is hard to reach through the inputs.
Upwards Name Referencing
A module lower in the hierarchy can also reach items in the modules above it. In the version of module A below, the task writes to the named block TB_INITIAL, which is in the testbench that instantiates A:
module A;
task display;
begin
$display("Hello, this is A");
// Upward referencing, TB_INITIAL is visible in this module
#5 TB_INITIAL.signal = 1;
end
endtask
endmodule
xcelium> run Hello, this is A TB signal=1 signal_1=1 signal_2=0 xmsim: *W,RNQUIE: Simulation is complete.
The testbench sets TB_INITIAL.signal to 0 at time 0 and then calls uA.display. The task prints its message, waits 5 time units, and sets the same variable to 1, so at 10 the testbench now prints TB signal=1. Module A has no item called TB_INITIAL, so the simulator looks for it in the module that instantiated uA, which is tb, and finds it there.
This upward search continues through any number of levels. In the example below, tb contains A, which contains B, which contains C, which contains D, and D calls tasks in all of its parents:
module tb;
A a();
task display;
$display("Hello, this is TB");
endtask
endmodule
module A;
B b();
task display;
$display("Hello, this is A");
endtask
endmodule
module B;
C c();
task display;
$display("Hello, this is B");
endtask
endmodule
module C;
D d();
task display;
$display("Hello, this is C");
endtask
endmodule
module D;
initial begin
a.display; // or A.display
b.display; // or B.display
c.display; // or C.display
a.b.c.display;
end
endmodule
xcelium> run Hello, this is A Hello, this is B Hello, this is C Hello, this is C xmsim: *W,RNQUIE: Simulation is complete.
For a name such as b.display, the simulator first looks for b in the current scope, the initial block of D, and then in module D itself. When it is not found there, the search moves up the instance hierarchy: to the parent instance c, then to b, then to a, and finally to tb. At each level, the first part of the name can match either the instance name of an ancestor, such as b, or its module name, such as B, which is why both forms in the comments work. Once the first part is found, the rest of the path, here display, is followed downward from there. The last line goes down again from a through b and c, and reaches the same task as c.display.
Quick Check
Which line would D print for tb.display;, and for a.b.display;?
Show Answer
tb.display; prints "Hello, this is TB", because tb is the top-level module, so the path starts there. a.b.display; prints "Hello, this is B": the search finds the ancestor a, then follows the path down to its instance b.
Behavioral Models That Report to the Testbench
A bus functional model or memory model deep inside a testbench hierarchy sometimes needs to update a global error counter or call a reporting task in the top level. With upward referencing, the model can call tb.report_error; without passing extra ports up through every level of the hierarchy. Many teams still prefer explicit connections, because the model then also works in a testbench whose top level is named differently.
Common Beginner Mistakes
Mistake #1: Using hierarchical references to connect design modules
Wrong: Reading u_alu.result from inside another design module instead of connecting it through ports
Right: Connect modules through ports, and keep hierarchical references in the testbench
Why? Synthesis tools usually reject hierarchical references, and when they are allowed, they hide connections that do not appear in any port list. The RTL then simulates, but cannot be built or reviewed properly.
Mistake #2: Hard-coding paths that change
Wrong: Writing tb.u_top.u_core0.u_alu.result in many places in the testbench
Right: Define the path once, for example with a macro `define ALU tb.u_top.u_core0.u_alu, and use `ALU.result
Why? When an instance is renamed or the hierarchy changes, every hard-coded path breaks. A single definition means one place to update.
Mistake #3: Expecting to reach a variable in an unnamed block
Wrong: Trying to access a variable declared in initial begin ... end without a block name
Right: Name the block, as in initial begin : TB_INITIAL, and declare the variable there
Why? A hierarchical path is built from scope names. Without a name, there is no path, and in Verilog the unnamed block cannot even declare the variable.
Mistake #4: Calling functions without arguments as statements
Wrong: function display(); $display("Hi"); endfunction called as a.display();
Right: Use a task: task display; $display("Hi"); endtask, called as a.display;
Why? In Verilog, a function must have at least one input, must return a value, and can only be called inside an expression. Empty argument lists and functions called as statements are SystemVerilog features. A task is the Verilog way to call a routine that only prints or changes state.
Practice Exercise
Challenge: Write the Paths
A testbench tb instantiates soc u_soc();. Module soc contains cpu u_cpu(); and uart u_uart();. Module cpu has a reg [31:0] pc;, and module uart has a task send_byte with one 8-bit input. Write:
- The full path to read
pcfrom the testbench - A statement in the testbench that calls
send_bytewith the value8'h41 - A statement inside module
uartthat displayspc, using an upward reference
Hint
Downward paths start at a scope visible from where you are. For the upward reference, find the closest ancestor of u_uart that also contains u_cpu.
Solution
// 1. From the testbench
$display("pc = %h", u_soc.u_cpu.pc); // or tb.u_soc.u_cpu.pc
// 2. From the testbench
u_soc.u_uart.send_byte(8'h41);
// 3. Inside module uart
$display("pc = %h", u_soc.u_cpu.pc); // upward: u_soc is the parent instance
In part 3, the search for u_soc starts in uart, does not find it, moves up to the parent instance u_soc, and matches its instance name. From there, the path goes down to u_cpu.pc. Writing soc.u_cpu.pc, with the module name, would also work.
Summary
In this tutorial, you learned:
- Modules, tasks, functions, named blocks and generate blocks each create a scope, and a name must be unique within its scope
- Every item has a unique hierarchical path, made of the scope names from the top-level module down, separated by dots
- Downward references reach items in instances and named blocks below the current scope
- Upward references search the parent instances, matching either their instance name or their module name
- Variables in named blocks are static, so they can be accessed even when the block is not running
- Hierarchical references are for testbenches and debugging; synthesizable RTL connects modules through ports