Module 1: Scalable Environment Architecture
UVM Component Hierarchy
Learning objectives
- Explain the core mental model behind UVM Component Hierarchy
- Apply UVM Component Hierarchy within Scalable Environment Architecture
- Identify important boundaries, trade-offs, and failure modes
- Produce concrete evidence from the practice exercise
Related: UVM Component | UVM Factory | UVM Config DB | UVM Env | UVM Agent | UVM Test
Why Component Hierarchy Matters
Every UVM component exists in a tree. The root is uvm_top (an implicit singleton), and every component you create becomes a node in this tree. The hierarchy is not just organizational -- it is the addressing system that config_db, the factory, and reporting use to target specific components.
If you don't understand the hierarchy, you cannot:
- Set a configuration that reaches the right component
- Override a specific instance via the factory
- Debug which component produced a log message
- Navigate a complex testbench programmatically
uvm_top
|
+-- uvm_test_top (your test)
|
+-- env
|
+-- agent
| |
| +-- sequencer
| +-- driver
| +-- monitor
|
+-- scoreboard
+-- coverageEvery component has a full hierarchical name like uvm_test_top.env.agent.monitor. This string is how config_db lookups are resolved, how factory instance overrides are targeted, and how log messages identify their source.
Core Hierarchy Methods
get_full_name()
Returns the dot-separated path from uvm_test_top down to this component.
class my_monitor extends uvm_monitor;
`uvm_component_utils(my_monitor)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Prints: "uvm_test_top.env.agent.monitor"
`uvm_info("HIER", $sformatf("My full name: %s", get_full_name()), UVM_LOW)
endfunction
endclassNote: get_full_name() does NOT include uvm_top in the path. It starts from the test.
get_name()
Returns just the component's local name (the string passed to new()):
$display("Local name: %s", get_name()); // "monitor"
$display("Full name: %s", get_full_name()); // "uvm_test_top.env.agent.monitor"get_parent()
Returns a handle to the parent component:
function void connect_phase(uvm_phase phase);
uvm_component parent = get_parent();
`uvm_info("HIER", $sformatf("My parent: %s (type: %s)",
parent.get_full_name(), parent.get_type_name()), UVM_LOW)
// Output: "My parent: uvm_test_top.env.agent (type: bus_agent)"
endfunctionget_num_children() and get_child()
Navigate downward in the tree:
function void end_of_elaboration_phase(uvm_phase phase);
int num = get_num_children();
`uvm_info("HIER", $sformatf("I have %0d children", num), UVM_LOW)
// Get a specific child by name
uvm_component child = get_child("monitor");
if (child != null)
`uvm_info("HIER", $sformatf("Found child: %s", child.get_full_name()), UVM_LOW)
else
`uvm_warning("HIER", "Child 'monitor' not found")
endfunctionget_children() -- Iterate All Children
function void print_children();
uvm_component children[$];
string child_name;
uvm_component child;
// Method 1: Using get_first_child / get_next_child
if (get_first_child(child_name)) begin
do begin
child = get_child(child_name);
`uvm_info("HIER", $sformatf(" Child: %-20s Type: %s",
child_name, child.get_type_name()), UVM_LOW)
end while (get_next_child(child_name));
end
endfunctionHierarchical Path Strings
Path Format
uvm_test_top.env.agent.monitor
^ ^ ^ ^
| | | +-- component name (from new("monitor", parent))
| | +-------- parent name
| +------------ grandparent name
+------------------------- root test instance (always this name)How Paths Are Constructed
The path is determined entirely by the name argument in the constructor and the parent-child relationship:
// In the agent's build_phase:
monitor = bus_monitor::type_id::create("mon", this);
// ^^^ ^^^^
// name parent = this agent
// If this agent's full name is "uvm_test_top.env.agent"
// Then monitor's full name is "uvm_test_top.env.agent.mon"Why Naming Matters for config_db
uvm_config_db uses hierarchical paths to scope configurations:
// In the test: set config for a specific monitor
uvm_config_db#(virtual bus_if)::set(
this, // context: uvm_test_top
"env.agent.mon", // path relative to context
"vif", // field name
bus_vif // value
);
// In the monitor's build_phase: retrieve it
uvm_config_db#(virtual bus_if)::get(
this, // context: uvm_test_top.env.agent.mon
"", // empty = look at my path
"vif", // field name
vif // destination
);The set path "env.agent.mon" relative to the test context resolves to uvm_test_top.env.agent.mon. The monitor's get with context this and empty instance string also resolves to uvm_test_top.env.agent.mon. They match, so the lookup succeeds.
If you named the monitor "monitor" instead of "mon", the set path would need to change to "env.agent.monitor". Getting the name wrong means config_db silently fails (no match), and you get a runtime error when the component tries to use the null value.
Wildcards in config_db Paths
// Apply to ALL monitors in ALL agents:
uvm_config_db#(int)::set(this, "env.*.mon", "enable_checks", 1);
// Apply to everything under env:
uvm_config_db#(int)::set(this, "env.*", "verbosity", UVM_HIGH);The * wildcard matches any single level in the hierarchy. This is how you configure multiple instances at once without knowing exact paths.
find() and lookup() -- Dynamic Component Discovery
lookup()
Searches for a component by path. Supports absolute and relative paths:
function void connect_phase(uvm_phase phase);
uvm_component comp;
// Absolute path (starts from uvm_test_top)
comp = uvm_top.find("uvm_test_top.env.agent.mon");
// Relative path from current component (use "." prefix)
comp = lookup("agent.mon"); // Searches relative to this component
// Search upward with ".." (parent)
comp = lookup("..scoreboard"); // Sibling of my parent
if (comp != null)
`uvm_info("LOOKUP", $sformatf("Found: %s", comp.get_full_name()), UVM_LOW)
endfunctionfind() on uvm_top
uvm_top.find() searches the entire hierarchy by absolute name:
// Find a component anywhere in the tree
uvm_component target;
target = uvm_top.find("uvm_test_top.env.scoreboard");find_all() -- Search by Pattern
function void end_of_elaboration_phase(uvm_phase phase);
uvm_component comps[$];
// Find all monitors in the hierarchy
uvm_top.find_all("*.mon", comps);
`uvm_info("FIND", $sformatf("Found %0d monitors:", comps.size()), UVM_LOW)
foreach (comps[i])
`uvm_info("FIND", $sformatf(" [%0d] %s (%s)",
i, comps[i].get_full_name(), comps[i].get_type_name()), UVM_LOW)
endfunctionUse Cases for Dynamic Discovery
- Testbench introspection -- Debug tools that print the full topology
- Late-binding connections -- Connecting components whose paths are computed at runtime
- Verification IP integration -- Finding monitors from third-party VIP without hardcoding paths
Factory Integration: Instance Overrides by Path
The UVM factory allows you to replace one component type with another. Instance overrides use hierarchical paths to target specific instances.
Why Path-Based Overrides Matter
You have two AXI agents in your SoC testbench. One connects to a memory controller, the other to a peripheral. You want to use a specialized monitor only for the memory-side agent:
uvm_test_top.env.mem_agent.mon --> use axi_mem_monitor
uvm_test_top.env.periph_agent.mon --> keep default axi_monitorWithout path-based overrides, you'd have to change the agent code. With them:
class specialized_test extends base_test;
`uvm_component_utils(specialized_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
// Instance override: only replace the monitor in mem_agent
axi_monitor::type_id::set_inst_override(
axi_mem_monitor::type_id::get(), // replacement type
"env.mem_agent.mon", // target path (relative to test)
this // context
);
// Alternative syntax using the factory directly:
factory.set_inst_override_by_type(
axi_monitor::get_type(), // original type
axi_mem_monitor::get_type(), // replacement type
{get_full_name(), ".env.mem_agent.mon"} // absolute path
);
super.build_phase(phase); // Now agent creates axi_mem_monitor via factory
endfunction
endclassType Override vs Instance Override
// TYPE override: affects ALL instances of this type everywhere
axi_monitor::type_id::set_type_override(axi_debug_monitor::type_id::get());
// INSTANCE override: affects only the specified path
axi_monitor::type_id::set_inst_override(
axi_debug_monitor::type_id::get(),
"env.agent.mon",
this
); Type Override:
agent1.mon --> axi_debug_monitor (overridden)
agent2.mon --> axi_debug_monitor (overridden)
Instance Override (targeting agent1.mon only):
agent1.mon --> axi_debug_monitor (overridden)
agent2.mon --> axi_monitor (original)Factory Creates Only If You Use create()
The factory override only works if the component is created with type_id::create(), not new():
// CORRECT: Factory-aware creation
monitor = axi_monitor::type_id::create("mon", this);
// If there's an override for this path, create() returns axi_debug_monitor
// WRONG: Bypasses factory entirely
monitor = new("mon", this);
// Always creates axi_monitor, ignores all overridesWalking the Hierarchy: Printing Topology
UVM provides a built-in method to print the entire component tree:
class my_test extends uvm_test;
`uvm_component_utils(my_test)
my_env env;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env", this);
endfunction
function void end_of_elaboration_phase(uvm_phase phase);
// Print the full topology
uvm_top.print_topology();
// Or print just from this component down
print();
endfunction
endclassOutput looks like:
---------------------------------------------------------
Name Type Size
---------------------------------------------------------
uvm_test_top my_test -
env my_env -
agent bus_agent -
sequencer bus_sequencer -
driver bus_driver -
mon bus_monitor -
scoreboard bus_scoreboard -
cov bus_coverage -
---------------------------------------------------------Custom Hierarchy Walker
For more control, write your own recursive walker:
function void walk_hierarchy(uvm_component root, int depth = 0);
string child_name;
uvm_component child;
string indent = {depth * 2{" "}};
`uvm_info("WALK", $sformatf("%s%s (%s) [%0d children]",
indent,
root.get_name(),
root.get_type_name(),
root.get_num_children()), UVM_LOW)
// Iterate over children
if (root.get_first_child(child_name)) begin
do begin
child = root.get_child(child_name);
walk_hierarchy(child, depth + 1); // Recurse
end while (root.get_next_child(child_name));
end
endfunction
// Call from test:
function void end_of_elaboration_phase(uvm_phase phase);
walk_hierarchy(uvm_root::get());
endfunctionFinding Components by Type
// Find all components of a specific type
function void find_all_monitors(uvm_component root);
string child_name;
uvm_component child;
uvm_monitor mon;
// Check if root is a monitor
if ($cast(mon, root))
`uvm_info("FIND", $sformatf("Monitor found: %s", root.get_full_name()), UVM_LOW)
// Recurse into children
if (root.get_first_child(child_name)) begin
do begin
child = root.get_child(child_name);
find_all_monitors(child);
end while (root.get_next_child(child_name));
end
endfunctionComponent Naming Best Practices
1. Use Meaningful, Short Names
// GOOD: concise, descriptive
agent = bus_agent::type_id::create("agent", this);
monitor = bus_monitor::type_id::create("mon", this);
driver = bus_driver::type_id::create("drv", this);
// BAD: redundant with type name
agent = bus_agent::type_id::create("bus_agent_inst", this);
monitor = bus_monitor::type_id::create("bus_monitor_component", this);The type information is available via get_type_name(). The instance name should identify the role or instance, not repeat the type.
2. Use Indexed Names for Arrays of Components
for (int i = 0; i < NUM_AGENTS; i++) begin
agents[i] = bus_agent::type_id::create($sformatf("agent_%0d", i), this);
end
// Paths: env.agent_0, env.agent_1, env.agent_2, ...3. Name Reflects the Interface or Purpose
// Multi-interface SoC env
axi_mem_agent = axi_agent::type_id::create("mem_agent", this);
axi_dma_agent = axi_agent::type_id::create("dma_agent", this);
apb_cfg_agent = apb_agent::type_id::create("cfg_agent", this);
// Paths:
// uvm_test_top.env.mem_agent
// uvm_test_top.env.dma_agent
// uvm_test_top.env.cfg_agent4. Never Use Dots in Component Names
Dots are path separators. A component named "agent.mon" breaks path resolution:
// NEVER DO THIS
monitor = bus_monitor::type_id::create("agent.mon", this);
// get_full_name() returns "uvm_test_top.env.agent.mon"
// But the ACTUAL tree structure is: env -> "agent.mon" (single component)
// config_db and factory will misinterpret this as env.agent -> mon5. Consistent Naming Across Projects
Establish naming conventions and stick to them. Common conventions:
Component Name Convention
----------- ----------------
Agent <protocol>_agent or <role>_agent
Monitor mon
Driver drv
Sequencer sqr
Scoreboard sb or scoreboard
Coverage cov
Environment env
Virtual Seq. vsqrHierarchy and Phasing Interaction
Phases execute top-down for build/connect and bottom-up for run/cleanup. The hierarchy determines the order:
build_phase: uvm_test_top -> env -> agent -> monitor
-> driver
-> sequencer
-> scoreboard
connect_phase: uvm_test_top -> env -> agent -> monitor
-> driver
-> scoreboard
run_phase: All run in parallel (concurrent)
report_phase: monitor -> driver -> sequencer -> agent
scoreboard -> env -> uvm_test_top
(bottom-up)This means:
- build_phase: Parent builds before children. You can set config_db values in the parent's build_phase and children will find them.
- connect_phase: Top-down, but all components exist. Safe to call
get_child(). - end_of_elaboration_phase: Best place to walk hierarchy -- everything is built and connected.
Debugging Hierarchy Issues
Common Problem: config_db Miss Due to Wrong Path
// Test sets config:
uvm_config_db#(int)::set(this, "env.agent.monitor", "enable", 1);
// ^^^^^^^
// But the component was created as:
monitor = bus_monitor::type_id::create("mon", this);
// ^^^
// Path mismatch! "monitor" != "mon"Debug with:
// In the component that can't find its config:
function void build_phase(uvm_phase phase);
super.build_phase(phase);
`uvm_info("DEBUG", $sformatf("Looking for config at path: %s", get_full_name()), UVM_LOW)
if (!uvm_config_db#(int)::get(this, "", "enable", enable_val))
`uvm_warning("CFG", $sformatf("Config 'enable' not found at %s", get_full_name()))
endfunctionEnable config_db Tracing
// In your test -- turn on config_db debug for all operations
initial begin
uvm_config_db#(int)::set(null, "*", "recording_detail", UVM_FULL);
end
// Or via command line:
// +UVM_CONFIG_DB_TRACEPrint Factory Overrides
function void end_of_elaboration_phase(uvm_phase phase);
uvm_factory factory = uvm_factory::get();
factory.print(); // Shows all type and instance overrides
endfunctionComplete Example: Hierarchy Traversal and Factory Override
// ============================================================
// Custom monitor that adds extra debug logging
// ============================================================
class debug_monitor extends bus_monitor;
`uvm_component_utils(debug_monitor)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
`uvm_info("DBG_MON", $sformatf("Debug monitor created at: %s", get_full_name()), UVM_LOW)
endfunction
task collect_one_txn(bus_txn txn);
`uvm_info("DBG_MON", "Collecting transaction with extra debug...", UVM_HIGH)
super.collect_one_txn(txn);
`uvm_info("DBG_MON", $sformatf("DETAILED: %s @%0t", txn.convert2string(), $time), UVM_LOW)
endtask
endclass
// ============================================================
// Test that uses hierarchy to apply targeted overrides
// ============================================================
class hierarchy_demo_test extends uvm_test;
`uvm_component_utils(hierarchy_demo_test)
my_env env;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
// Instance override: only agent_0's monitor gets the debug version
bus_monitor::type_id::set_inst_override(
debug_monitor::type_id::get(),
"env.agent_0.mon",
this
);
super.build_phase(phase);
env = my_env::type_id::create("env", this);
endfunction
function void end_of_elaboration_phase(uvm_phase phase);
// Print full hierarchy
`uvm_info("TEST", "=== Component Hierarchy ===", UVM_LOW)
print_tree(this, 0);
// Demonstrate lookup
begin
uvm_component found;
found = lookup("env.agent_0.mon");
if (found != null)
`uvm_info("TEST", $sformatf("Lookup found: %s (type: %s)",
found.get_full_name(), found.get_type_name()), UVM_LOW)
end
// Verify the factory override took effect
begin
uvm_component comps[$];
uvm_top.find_all("*.mon", comps);
foreach (comps[i])
`uvm_info("TEST", $sformatf("Monitor: %s is type %s",
comps[i].get_full_name(), comps[i].get_type_name()), UVM_LOW)
// Expected output:
// Monitor: uvm_test_top.env.agent_0.mon is type debug_monitor
// Monitor: uvm_test_top.env.agent_1.mon is type bus_monitor
end
// Print factory state
uvm_factory::get().print();
endfunction
// Recursive tree printer
function void print_tree(uvm_component node, int depth);
string indent = {depth * 2{" "}};
string child_name;
uvm_component child;
$display("%s|- %s (%s)", indent, node.get_name(), node.get_type_name());
if (node.get_first_child(child_name)) begin
do begin
child = node.get_child(child_name);
print_tree(child, depth + 1);
end while (node.get_next_child(child_name));
end
endfunction
task run_phase(uvm_phase phase);
phase.raise_objection(this);
#1000;
phase.drop_objection(this);
endtask
endclassExpected output from the tree printer:
|- hierarchy_demo_test (hierarchy_demo_test)
|- env (my_env)
|- agent_0 (bus_agent)
|- sqr (bus_sequencer)
|- drv (bus_driver)
|- mon (debug_monitor) <-- overridden!
|- agent_1 (bus_agent)
|- sqr (bus_sequencer)
|- drv (bus_driver)
|- mon (bus_monitor) <-- original
|- sb (bus_scoreboard)
|- cov (bus_coverage)Summary
| Method / Concept | Purpose | Example |
|---|---|---|
get_full_name() | Full hierarchical path | "uvm_test_top.env.agent.mon" |
get_name() | Local component name | "mon" |
get_parent() | Navigate up the tree | Returns agent handle |
get_child(name) | Navigate down by name | Returns monitor handle |
get_first/next_child | Iterate all children | Tree walking |
lookup(path) | Find by relative path | lookup("agent.mon") |
uvm_top.find(path) | Find by absolute path | find("uvm_test_top.env.sb") |
uvm_top.find_all(pat) | Pattern-based search | find_all("*.mon", results) |
set_inst_override | Factory override by path | Replace one instance's type |
print_topology() | Dump entire tree | Debug tool |
Practice lab
Create a minimal executable SystemVerilog or UVM artifact that demonstrates UVM Component Hierarchy. Record the expected behavior, inject one deliberate defect, and capture the evidence that identifies the failure. Add an operational constraint such as concurrency, recovery, security, latency, or cost, and defend the resulting design trade-off.
Review questions
- What problem does UVM Component Hierarchy solve, and what assumptions does it rely on?
- Which boundary or failure case is easiest to miss, and how would you expose it?
- What alternative design would you consider, and what trade-off would change the decision?
- What artifact, trace, test, or metric proves that your implementation is correct?
Completion evidence
- A working artifact, annotated trace, or reproducible experiment
- At least one normal case and one deliberately failing or boundary case
- A concise explanation of the design choice and its trade-offs
- Saved output showing how correctness was evaluated