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::setis 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(), andlookup()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 tocreate(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.
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().
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
endclassThree 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.
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.
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 cyclesThe 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
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.
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:
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 → phasedPhasing 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.
The tell is a component that exists but whose phases never run. Diagnose tree-membership problems through topology and parentage:
- Print the topology.
uvm_top.print_topology()(orprint_topologyfrom the root) dumps the tree. A component that should exist but is absent was never inserted — it has no parent. - 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. - Confirm the
createpassed a parent. Look at the construction call:create("mon")ornew("mon")with no parent makes an orphan;create("mon", this)inserts it. The missing second argument is the bug.
A component is only alive if it is in the tree — always give it a parent:
- Always pass the parent to
create. Usecreate("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. - 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. - 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")ornew("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
lookupof that path returns null. Verifyget_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 inbuild_phase; building components elsewhere fights the phaser and the parent-before-child ordering. Create children inbuild_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
- Trace the path. Given a test that builds
envwhich buildsagentwhich buildsdrv(each withcreate(name, this)), write theget_full_name()of each component, and state whichuvm_config_db::setscope would reachdrvand onlydrv. - Diagnose the inert component. A monitor was created but its
run_phasenever runs and it's absent fromprint_topology(). State the root cause, the one-line construction fix, and the method you'd call to confirm it's now in the tree. - 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. - 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_phasebycreate(name, this);get_full_name()returns the path from the parent chain, and you navigate withget_parent(),get_children(), andlookup(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 itsget_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.