Skip to content

UVM

UVM Component Hierarchy

Building the named component tree with name and parent, hierarchical paths, navigating with get_parent/get_children/lookup, and how phasing walks every component.

UVM Testbench Architecture · Module 3 · Page 3.3

The Engineering Problem

You know the shape of the component tree (Module 2) and what flows through it (transactions). But a tree is only useful if every node has an address and the framework can walk it. UVM gives each component two things at construction — a name and a parent — and from those derives a unique hierarchical path like uvm_test_top.env.agent.drv. That path is the component's address: it is how the config database scopes settings to it, how messages identify it, and how you look it up from elsewhere.

The mechanics matter because they are where real bugs live. Pass the wrong parent and a component lands at the wrong path, so a configuration scoped to its expected address silently misses it. Pass no parent and the component is an orphan — not in the tree at all — so phasing never visits it and it sits inert, created but never built, connected, or run. The hierarchy is not a diagram; it is a live, named, walkable structure, and how you construct and navigate it determines whether the framework can find and run each piece.

How is the named component tree actually built from names and parents, how do you navigate it, and how does phasing walk it — so every component has a correct address and is actually visited by the framework?

Motivation — why the named tree is more than a picture

The hierarchical path and the walkable tree are working machinery you rely on constantly:

  • The path is the component's address for configuration. uvm_config_db::set is scoped by hierarchical path ("uvm_test_top.env.agent.*"). If a component's path isn't what you expect — because it got the wrong parent — your configuration silently doesn't reach it. The path is the addressing system.
  • Phasing only reaches tree members. The phaser walks the component tree and runs each phase on every node in it. A component not in the tree (no parent) is never built, connected, or run — it exists as an object but does nothing. Being in the tree is what makes a component alive.
  • Navigation enables coordination. get_parent(), get_children(), and lookup() let components and tests find each other — a virtual sequence locating sequencers, a scoreboard reaching a sibling, debug code walking the topology. The tree is queryable, and that queryability is how cross-component coordination happens.
  • Messages and debugging are path-based. Every UVM report is tagged with the reporting component's full name, and print_topology() dumps the tree. When something is wrong, the path tells you which of the dozens of identical-looking components is the culprit.

The motivation, in one line: the name-and-parent that place a component in the tree determine its address, its visibility to phasing, and its findability — so constructing the hierarchy correctly is what makes configuration, execution, and debugging work at all.

Mental Model

Hold the hierarchy as a filesystem:

The component tree is a filesystem, and each component's full name is its absolute path. A component's name is like a filename, and its parent is the directory it lives in; together they give a unique path from the root, uvm_test_top.env.agent.drv, exactly like /home/env/agent/drv. You place a file by saying which directory it goes in — in UVM, you place a component by passing its parent to create(name, parent). You navigate the filesystem with "go to parent" (get_parent()), "list contents" (get_children()), and "open this path" (lookup("env.agent.drv")). And a process that "runs every file in the tree" (the phaser) only reaches files that are in the filesystem — a component created without a parent is like a file written to nowhere: it exists in memory but is in no directory, so the tree-walker never finds it.

So whenever you construct or address a component, think in paths: what is this component's full path, and is it where configuration and phasing expect it to be? The parent handle you pass to create is the single thing that determines the answer.

Visual Explanation — the named tree

Every component sits at a definite path determined by the chain of parents from the root. The same tree you've seen as a shape is, more precisely, a set of addresses.

UVM component tree with hierarchical paths: test, env, agent, sequencer, driver, monitor, scoreboard, coverage, each with a full name from the rootsqrsqruvm_sequenceruvm_sequencerdrvdrvuvm_driveruvm_drivermonmonuvm_monitoruvm_monitoragentagentuvm_agentuvm_agentsbsbuvm_scoreboarduvm_scoreboardcovcovuvm_subscriberuvm_subscriberenvenvuvm_envuvm_envtest_basetest_baseuvm_testuvm_test
Figure 1 — the component tree as a set of hierarchical paths. From the implicit root (uvm_test_top), each component's full name is the chain of names down to it: env is uvm_test_top.env, the agent is uvm_test_top.env.agent, the driver is uvm_test_top.env.agent.drv. These paths are the component's address — used by the config database for scoping, by messages for identification, and by lookup() for navigation. The parent passed at construction is what fixes each node's place in this address space.

Read the tree as addresses, not just boxes. The driver isn't merely "inside the agent"; its address is uvm_test_top.env.agent.drv, and that string is what uvm_config_db::set(this, "env.agent.drv", "vif", vif) targets, what its log messages are tagged with, and what lookup("env.agent.drv") resolves. Change any parent in the chain and every descendant's address changes. This is why the parent you pass at construction is consequential: it doesn't just nest the component visually, it assigns its place in the address space that configuration, messaging, and navigation all use.

RTL / Simulation Perspective — building and naming the tree

The tree is constructed in build_phase by each component creating its children with create(name, this) — where this is the parent. The name plus that parent chain produces the full path, which you can read back with get_full_name().

constructing the named tree — name + parent give the full path
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class my_env extends uvm_env;
  `uvm_component_utils(my_env)
  my_agent agent;
 
  function void build_phase(uvm_phase phase);
    agent = my_agent::type_id::create("agent", this);   // name "agent", parent = this (env)
    // agent.get_full_name() => "uvm_test_top.env.agent"
  endfunction
endclass
 
class my_agent extends uvm_agent;
  `uvm_component_utils(my_agent)
  my_driver drv;  my_monitor mon;
 
  function void build_phase(uvm_phase phase);
    drv = my_driver ::type_id::create("drv", this);     // parent = this (agent)
    mon = my_monitor::type_id::create("mon", this);
    // drv.get_full_name() => "uvm_test_top.env.agent.drv"
  endfunction
 
  function void connect_phase(uvm_phase phase);
    // navigate the tree:
    uvm_component par = drv.get_parent();               // => this agent
    int n            = get_num_children();              // => 2 (drv, mon)
    uvm_component c  = lookup("drv");                   // find a child by name
  endfunction
endclass

Three mechanics are on display. Construction: create("drv", this) makes a driver whose parent is the agent — the second argument is the entire reason it lands at ...agent.drv. Naming: get_full_name() returns the path built from the parent chain plus the local name, with no manual bookkeeping. Navigation: get_parent() walks up, get_num_children()/get_children() enumerate down, and lookup(path) resolves a relative or absolute path to a component handle. Together these are how you build the tree top-down and then query it — and the this you pass to every create is what makes the resulting addresses correct.

Verification Perspective — only tree members are phased

The hierarchy is not just an addressing scheme; it is what the phaser walks. UVM executes each phase by traversing the component tree and calling the phase method on every node — so a component's membership in the tree is exactly what subjects it to build_phase, connect_phase, run_phase, and the rest.

Phasing traverses the component tree top-down for build, then connect, then run, reaching only components that are in the treeThe phaser visits every component in the treeThe phaser visits every component in the tree1Phaser starts at the rootuvm_test_top — the phaser will walk the whole tree below it foreach phase.2build_phase, top-downtest builds env, env builds agent, agent builds drv/mon — parentbefore child, because the parent creates the child.3Each phase visits every memberconnect, run, report … the phaser calls the phase method on everycomponent IN the tree.4Orphans are skippeda component created with no parent is not in the tree — the phasernever reaches it, so it is never phased.
Figure 2 — phasing walks the tree. The phaser traverses the component hierarchy and runs each phase on every component in it: build_phase top-down (a parent must build before its children, since it creates them), then connect_phase, then run_phase, and so on. A component in the tree (correct parent) is visited and phased; a component created without a parent is not in the tree, so the phaser never reaches it — it is never built, connected, or run. Being in the tree is what makes a component execute.

The decisive fact is in step 4: the phaser only reaches what is in the tree. Because phasing is a traversal of the component hierarchy, a component's parent link is what puts it on the phaser's path. Build a component correctly (with a parent) and it is visited for every phase — built, connected, run, reported. Build it with no parent and it is an orphan: a live object that the framework never phases, so its build_phase and run_phase never execute and it does nothing. This couples two mechanics you might think are separate — construction (the parent handle) and execution (phasing) — into one: the parent you pass is what makes the component both correctly addressed and actually alive.

Runtime / Execution Flow — wrong parent, wrong address

Because the path is derived from the parent chain, passing the wrong parent silently relocates a component in the address space — and everything keyed to its expected address (configuration above all) misses it.

Correct parent gives path env.agent matched by config scope; wrong parent gives path test.agent not matched, so config misses itparent = envmatchesenv.agent.*parent = test(bug)does NOT match env.agent.*does NOTmatch…create("agent", env)correct parentpath: uvm_test_top.env.agentexpected addressconfig reaches itset scoped to env.agent.*appliescreate("agent", test)wrong parentpath: uvm_test_top.agentunexpected addressconfig misses itscope no longer matches → settinglost12
Figure 3 — how the parent determines the address, and how a wrong parent breaks configuration. Created with the env as parent, the agent's path is uvm_test_top.env.agent and a config set scoped to env.agent.* reaches it. Created with the test as parent by mistake, its path becomes uvm_test_top.agent — the same config scope no longer matches, so the setting silently never reaches it. The parent handle at construction is what places the component in the address space the config database uses.

This is why "wrong parent" is a subtle, expensive bug: nothing crashes. The component is built and phased (it is in the tree, just at the wrong place), but its address is not where the rest of the environment expects it, so configuration scoped to the expected path silently fails to apply, and lookup of the expected path returns null. The fix is always the same — pass the correct parent so the path matches — and the diagnostic is always the same: print get_full_name() and compare it to the address your configuration and lookups assume. The parent handle is the one input that decides a component's place in the address space.

Waveform Perspective — the whole tree built before the leaf acts

The hierarchy is constructed and connected in zero time, top-down, before any component behaves. The waveform shows that the leaf driver — deep in the tree — only touches pins in run_phase, after the entire tree above it has been built and connected.

The tree is built top-down at time zero; the leaf driver acts only in run_phase

12 cycles
The tree is built top-down at time zero; the leaf driver acts only in run_phasebuild_phase (zero time): tree constructed top-down — test → env → agent → driverbuild_phase (zero time…connect_phase (zero time): parent/child links and ports wired across the whole treeconnect_phase (zero ti…run_phase: only now does the leaf driver, deep in the hierarchy, act on the pinsrun_phase: only now do…clkphaseBLDBLDCONCONRUNRUNRUNRUNRUNRUNRUNRUNvaliddata00000000A0A100B0B1000000t0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — build_phase and connect_phase happen at time zero (no pin activity): the phaser walks the tree top-down, constructing test → env → agent → driver and then wiring it. Only in run_phase does the driver — a leaf deep in the hierarchy — act on the pins. The entire named, connected tree exists before the first meaningful clock edge; a component must be built (in the tree) before it can run, which is why construction precedes behaviour.

The phase track tells the construction story: the entire tree is built (BLD) and connected (CON) at time zero, and only then does RUN begin and the driver's pins move. This ordering is a direct consequence of the hierarchy mechanics — a parent must build before its children because it creates them, so build is necessarily top-down, and a leaf component cannot run before it exists, so behaviour necessarily follows construction. The whole named, connected tree comes into being in an instant of zero simulation time, and then the leaves, addressed and in place, begin to act.

DebugLab — the component that was created but never ran

A monitor built with no parent — present in memory, absent from the tree, never phased

Symptom

An engineer added a second monitor to observe an internal interface. They constructed it, the code compiled, the simulation ran — and the monitor did nothing. Its build_phase never printed, its run_phase never executed, it never connected to the scoreboard, and print_topology() didn't show it anywhere in the tree. The object clearly existed (the handle was non-null), yet the framework behaved as if it weren't there.

Root cause

The monitor was created without a parent — so it was never inserted into the component tree, and the phaser, which walks the tree, never reached it:

why an orphan is never phased
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
written:   mon = my_monitor::type_id::create("mon");        // no parent argument → orphan
           // (or: mon = new("mon");  same effect — not in the tree)
result:    mon.get_parent() == null ; mon not under uvm_test_top
phaser:    walks the component TREE → only phases components IN it
           → orphan's build_phase/connect_phase/run_phase NEVER called
symptom:   the object exists, but does nothing; absent from print_topology()
fix:       create("mon", this)  // pass the parent → inserted into the tree → phased

Phasing is a tree traversal, and the monitor wasn't in the tree. Construction (the parent handle) and execution (phasing) are the same mechanic from two sides: no parent means not in the tree means never phased.

Diagnosis

The tell is a component that exists but whose phases never run. Diagnose tree-membership problems through topology and parentage:

  1. Print the topology. uvm_top.print_topology() (or print_topology from the root) dumps the tree. A component that should exist but is absent was never inserted — it has no parent.
  2. Check get_parent() / get_full_name(). An orphan returns a null parent and a degenerate full name. If a component's phases aren't running, confirm it actually has a parent and a sensible path.
  3. Confirm the create passed a parent. Look at the construction call: create("mon") or new("mon") with no parent makes an orphan; create("mon", this) inserts it. The missing second argument is the bug.
Prevention

A component is only alive if it is in the tree — always give it a parent:

  1. Always pass the parent to create. Use create("name", this) (or the appropriate parent handle) for every component, so it is inserted into the tree and subjected to phasing. Never construct a component with no parent.
  2. Build components in build_phase, via the factory, with a parent. This is the one place and way to grow the tree correctly; constructing components elsewhere or without a parent invites orphans.
  3. Verify topology during bring-up. Call print_topology() once and confirm every component you expect is present at the path you expect — it catches both orphans (missing) and wrong-parent placements (present at the wrong address) in one look.

The one-sentence lesson: phasing walks the component tree, so a component created without a parent is an orphan the phaser never visits — it exists but is never built, connected, or run, and the cure is to always pass the parent to create.

Common Mistakes

  • Creating a component with no parent (orphan). create("name") or new("name") without a parent leaves the component out of the tree, so phasing never reaches it and it does nothing. Always pass the parent: create("name", this).
  • Passing the wrong parent. The parent determines the path; a wrong parent gives a wrong address, so configuration scoped to the expected path silently misses the component and lookup of that path returns null. Verify get_full_name().
  • Hard-coding or guessing full paths. Assuming a path without checking it (especially across refactors that move components) breaks config scoping and lookups. Read the actual get_full_name() rather than assuming.
  • Constructing components outside build_phase. The tree is grown top-down in build_phase; building components elsewhere fights the phaser and the parent-before-child ordering. Create children in build_phase.
  • Confusing instance name with type. get_full_name() is the instance path (...agent.drv), not the class name. Config scoping and lookup use the instance path; mixing them up targets the wrong thing.
  • Ignoring print_topology(). Not checking the constructed tree leaves orphans and misplaced components undetected until a downstream symptom appears. One topology print at bring-up catches both.

Senior Design Review Notes

Interview Insights

A component is placed in the hierarchy by the parent you pass when you construct it, and its full name is built from the chain of parent names plus its own instance name. You create a component with create("name", parent) — typically create("drv", this) in the parent's build_phase, where this is the parent — and that parent argument is what inserts the component into the tree under the parent. Its get_full_name() then returns the path from the root, like uvm_test_top.env.agent.drv, derived automatically from the parent chain and the local name. This full path is the component's address: the config database uses it for scoping, UVM messages are tagged with it, and lookup(path) resolves it. Because the path comes from the parent chain, the parent you pass is the single thing that determines where the component lives in the address space — pass the wrong one and the address (and everything keyed to it) is wrong.

Exercises

  1. Trace the path. Given a test that builds env which builds agent which builds drv (each with create(name, this)), write the get_full_name() of each component, and state which uvm_config_db::set scope would reach drv and only drv.
  2. Diagnose the inert component. A monitor was created but its run_phase never runs and it's absent from print_topology(). State the root cause, the one-line construction fix, and the method you'd call to confirm it's now in the tree.
  3. Wrong parent. A component created with the test as parent instead of the env doesn't receive a config setting scoped to env.agent.*. Explain why, give its actual vs expected full name, and the fix.
  4. Navigate. Write (in prose) how a virtual sequence would find the driver's sequencer using hierarchy navigation, and explain why this is more robust than storing a hard-coded handle to it.

Summary

  • A UVM component is placed in the tree by its name and parent, which together give a unique hierarchical path (uvm_test_top.env.agent.drv) — the component's address, used by configuration, messaging, and lookup.
  • The tree is built top-down in build_phase by create(name, this); get_full_name() returns the path from the parent chain, and you navigate with get_parent(), get_children(), and lookup(path).
  • Phasing walks the tree, running each phase on every component in it — so construction (the parent handle) and execution (phasing) are one mechanic: only components with a parent are in the tree, and only tree members are phased.
  • Two classic bugs follow directly: a wrong parent gives a wrong address, so configuration scoped to the expected path silently misses the component; no parent makes an orphan the phaser never visits, so it exists but is never built, connected, or run.
  • The durable rule of thumb: always pass the correct parent to create — the parent fixes both the component's address and its membership in the tree — and when configuration or execution mysteriously skips a component, print its get_full_name() and the topology first.

Next — Data Flow Through UVM: you've built and addressed the static tree of components. The next chapter sets it in motion — how transactions actually move through this hierarchy at run time, the TLM connections that carry them between components, and the full path of data from sequence to scoreboard.