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
                +-- coverage

Every 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
endclass

Note: 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)"
endfunction

get_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")
endfunction

get_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
endfunction

Hierarchical 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)
endfunction

find() 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)
endfunction

Use Cases for Dynamic Discovery

  1. Testbench introspection -- Debug tools that print the full topology
  2. Late-binding connections -- Connecting components whose paths are computed at runtime
  3. 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_monitor

Without 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
endclass

Type 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 overrides

Walking 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
endclass

Output 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());
endfunction

Finding 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
endfunction

Component 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_agent

4. 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 -> mon

5. 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.     vsqr

Hierarchy 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()))
endfunction

Enable 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_TRACE

Print 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
endfunction

Complete 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
endclass

Expected 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 / ConceptPurposeExample
get_full_name()Full hierarchical path"uvm_test_top.env.agent.mon"
get_name()Local component name"mon"
get_parent()Navigate up the treeReturns agent handle
get_child(name)Navigate down by nameReturns monitor handle
get_first/next_childIterate all childrenTree walking
lookup(path)Find by relative pathlookup("agent.mon")
uvm_top.find(path)Find by absolute pathfind("uvm_test_top.env.sb")
uvm_top.find_all(pat)Pattern-based searchfind_all("*.mon", results)
set_inst_overrideFactory override by pathReplace one instance's type
print_topology()Dump entire treeDebug 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

  1. What problem does UVM Component Hierarchy solve, and what assumptions does it rely on?
  2. Which boundary or failure case is easiest to miss, and how would you expose it?
  3. What alternative design would you consider, and what trade-off would change the decision?
  4. 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