Skip to content

uvm_root & the Singleton Pattern

Why exactly one instance, get() pattern, find() topology search, timeout management.

UVM Fundamentals · Module 20

§1 — The Hidden Root Node Every Testbench Has

You've been calling run_test("my_test") since your first UVM testbench. It just works. But if you think about it for a moment — your test is a uvm_component, and every component needs a parent. You're passing null as the parent in the test's constructor. So what exactly is the parent of uvm_test_top?

The answer is uvm_root — an invisible, automatically created component that sits above your entire testbench hierarchy. It's the reason your test's get_full_name() returns "uvm_test_top" instead of just "my_test". It's the reason phases execute in the right order across every component. It's the reason you can call uvm_root::get().find("env.agent.drv") from a sequence and get a handle back.

Most engineers get through their entire career without ever explicitly thinking about uvm_root — which is exactly how the framework designers intended it. But when you need to set a simulation-wide timeout, search for a component by path, print the entire hierarchy for debug, or implement your own global singleton, you need to understand both the object and the pattern it exemplifies.

§2 — The Singleton Pattern — One Instance, Everywhere Accessible

The singleton pattern solves one specific problem: ensuring that a class has exactly one instance and providing a global access point to it. In a language without a convenient module system, this is how you create "global" state that's type-safe, initialized on demand, and accessible without passing handles around.

In UVM, the pattern works like this: the constructor is effectively private (not directly callable by users), and a static get() method is the only way to obtain a handle. The first call to get() creates the instance. Every subsequent call returns the same instance. The calling code never knows or cares whether it triggered creation or is receiving an existing handle.

UVM SingletonWhat It RepresentsHow You Access It
uvm_rootRoot of all component hierarchy. Manages phases, timeout, component searchuvm_root::get()
uvm_factoryClass registry and override table — all type substitution logicuvm_factory::get()
uvm_report_serverMessage counting, severity actions, end-of-simulation summaryuvm_report_server::get_server()
uvm_resource_poolThe backing store for all resource_db and config_db entriesuvm_resource_pool::get()

Notice the pattern: every one of these has a static get() method that returns the single global instance. The implementation differs slightly between them, but the contract is the same: one instance, always the same one, accessible from anywhere.

§3 — The uvm_root API — What You Can Do With It

SystemVerilog — complete uvm_root API reference
// ── Get the singleton ─────────────────────────────────────────────────
uvm_root root = uvm_root::get();   // always returns the same instance
 
// ── Find a component anywhere in the hierarchy ────────────────────────
uvm_component found;
found = root.find("uvm_test_top.env.apb_agent.drv");
if (found != null) begin
`uvm_info("ROOT",$sformatf("Found: %s type=%s",
found.get_full_name(), found.get_type_name()),UVM_LOW)
end
 
// ── Find all components matching a wildcard path ──────────────────────
uvm_component q[$];
root.find_all("uvm_test_top.*.drv", q);   // returns all drivers in any agent
foreach(q[i])
`uvm_info("ROOT",q[i].get_full_name(),UVM_LOW)
 
// ── Set / get simulation timeout ──────────────────────────────────────
// Timeout fires if no objections are raised/dropped after this time
root.set_timeout(100ms, 1);   // 100ms timeout, fatal=1 (uvm_fatal on expiry)
root.set_timeout(500ns, 0);   // 500ns timeout, fatal=0 (uvm_error, simulation continues)
 
// ── Print the complete component hierarchy ────────────────────────────
root.print_topology();               // default: tree format to transcript
root.print_topology(uvm_default_printer);  // explicit printer
 
// ── Check component count in hierarchy ───────────────────────────────
`uvm_info("ROOT",$sformatf("Root has %0d direct children",
root.get_num_children()),UVM_LOW)
 
// ── Access via run_test() — the most common usage ─────────────────────
// run_test() is actually a shorthand for:
// uvm_root::get().run_test("my_test");
// Both are equivalent — use whichever reads more clearly
MethodReturnPurpose
uvm_root::get()uvm_rootGet the singleton handle. Creates instance on first call.
root.find(path)uvm_componentFind one component by exact path string. Returns null if not found.
root.find_all(pattern, q)void (fills queue)Find all components matching a glob path pattern. Queue may be empty.
root.set_timeout(t, fatal)voidSet simulation timeout. fatal=1 triggers uvm_fatal, fatal=0 triggers uvm_error.
root.print_topology()voidPrint complete component hierarchy in tree format.
root.run_test("name")voidStart phase execution — same as global run_test() shorthand.

§4 — uvm_root in the Hierarchy — The View From the Top

uvm_rootSINGLETON — get_full_name() = ""uvm_test_topyour_test class — created by run_test()uvm_test_top.env...env.apb_agent...env.scb...drv...mon...seqruvm_root Responsibilities• Phase execution engine• Root of hierarchy tree• Timeout management• find() component search• print_topology()• Objection coordinationPath Constructionuvm_root = ""test = "uvm_test_top"env = "...top.env"agent = "...env.agent"drv = "...agent.drv"get_full_name() buildseach path upward Figure 1 — uvm_root sits invisibly above uvm_test_top. Every component's get_full_name() walks up to uvm_root (which has an empty string name) to build the complete dot-separated path. uvm_root owns the phase engine, timeout, and component search — all the infrastructure that makes the testbench run.

Phase Execution — What uvm_root Drives

When run_test() is called…uvm_root does this
ImmediatelyCreates the test component via factory (uvm_test_top) as its own child
Phase: build_phaseCalls build_phase top-down on every component in the hierarchy (depth-first)
Phase: connect_phaseCalls connect_phase bottom-up — children before parents
Phase: run_phaseSpawns all run_phase tasks in parallel, starts the objection timer
Timeout fires (if set)Triggers uvm_fatal or uvm_error depending on fatal flag passed to set_timeout()
All objections droppedProceeds through extract → check → report → final phases
final_phase doneCalls $finish — simulation ends

§5 — Code Examples — Practical uvm_root Usage

SystemVerilog — using uvm_root to inspect and navigate the hierarchy
// ── In start_of_simulation_phase — hierarchy is fully built ──────────
function void start_of_simulation_phase(uvm_phase phase);
uvm_root      root = uvm_root::get();
uvm_component drv;
uvm_component all_comps[$];
super.start_of_simulation_phase(phase);
 
// Print the entire component hierarchy — great for debug
root.print_topology();
 
// Find a specific component by exact path
drv = root.find("uvm_test_top.env.apb_agent.drv");
if (drv == null)
`uvm_error("TOPO","Driver not found — check agent naming")
else
`uvm_info("TOPO",$sformatf("Found driver: %s",drv.get_type_name()),UVM_LOW)
 
// Find ALL monitors across ALL agents using wildcard
root.find_all("uvm_test_top.*.mon", all_comps);
`uvm_info("TOPO",$sformatf("Found %0d monitors total",all_comps.size()),UVM_LOW)
foreach(all_comps[i])
`uvm_info("TOPO",$sformatf("  Monitor: %s",all_comps[i].get_full_name()),UVM_LOW)
endfunction
 
// ── Timeout: kill simulation if it runs too long ─────────────────────
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Set a 1ms timeout — prevents hung simulations in regression
uvm_root::get().set_timeout(1ms, 1);   // fatal=1: uvm_fatal fires on expiry
`uvm_info("TEST","Simulation timeout set to 1ms",UVM_LOW)
endfunction

Example 2 — Implementing Your Own UVM-Style Singleton

SystemVerilog — custom singleton following the UVM pattern
// ── Custom singleton: global transaction counter ──────────────────────
// Pattern: private constructor + static get() method
class txn_counter;
 
// The single instance — static class variable
static txn_counter m_inst = null;
 
// Instance data
int total = 0;
int errors = 0;
int warnings = 0;
 
// "Private" constructor — protected by convention, not language enforcement
// In SV, we can't truly enforce privacy, but we document the intent
function new(); endfunction
 
// ── The critical get() method ─────────────────────────────────────
static function txn_counter get();
if (m_inst == null)
m_inst = new();   // lazy initialization — created on first call only
return m_inst;
endfunction
 
// ── Instance methods ──────────────────────────────────────────────
function void increment(bit is_error = 0, bit is_warning = 0);
total++;
if (is_error)   errors++;
if (is_warning) warnings++;
endfunction
 
function void report();
$display("=== Transaction Counter Report ===");
$display("  Total:    %0d", total);
$display("  Errors:   %0d", errors);
$display("  Warnings: %0d", warnings);
endfunction
endclass
 
// ── Usage from anywhere — monitor, scoreboard, sequence ───────────────
// Monitor:
txn_counter::get().increment(0, 0);   // normal transaction
 
// Scoreboard (on mismatch):
txn_counter::get().increment(1, 0);   // error
 
// Test's report_phase:
txn_counter::get().report();
 
// All three access THE SAME counter object — no passing handles needed

Example 3 — Using find() From a Sequence (No Component Context)

SystemVerilog — sequence accessing scoreboard via uvm_root.find()
// Sequences don't have a parent component handle — they can't walk up the hierarchy.
// But uvm_root::get().find() gives them access to any component.
 
class corner_case_seq extends uvm_sequence#(apb_txn);
`uvm_object_utils(corner_case_seq)
function new(string n="corner_case_seq"); super.new(n); endfunction
 
task body();
uvm_component scb_comp;
apb_scoreboard scb;
 
// Find the scoreboard by path — uvm_root makes this possible from a sequence
scb_comp = uvm_root::get().find("uvm_test_top.env.scb");
if (scb_comp == null) begin
`uvm_fatal("SEQ","Cannot find scoreboard — check env topology")
return;
end
 
// Downcast to the actual scoreboard type
if (!$cast(scb, scb_comp)) begin
`uvm_fatal("SEQ","Component is not apb_scoreboard")
return;
end
 
// Now configure scoreboard mode before sending transactions
scb.set_check_mode(apb_scoreboard::CHECK_STRICT);
`uvm_info("SEQ","Scoreboard configured for strict mode",UVM_LOW)
 
// Now run the corner-case transactions
repeat(10) begin
apb_txn t = apb_txn::type_id::create("t");
start_item(t);
void'(t.randomize());
finish_item(t);
end
endtask
endclass

§6 — Simulation Thinking — When uvm_root Is Created and What Happens

Simulation Eventuvm_root StateWhat You Can Safely Do
Before run_test()Instance exists (created lazily on first get() call), but test component not yet createdCall uvm_root::get() safely. Do NOT call find() — hierarchy doesn't exist yet.
During build_phaseHierarchy is being built top-down. Some children may not exist yet.find() may fail on not-yet-built children. Print topology is incomplete.
After connect_phaseComplete hierarchy exists. All components built and connected.Safe to call find(), find_all(), print_topology(). All queries return accurate results.
During run_phaseFull hierarchy, phases running, timeout active if setAll uvm_root operations safe. find() returns accurate handles.
After report_phaseHierarchy intact, phases done. About to call $finish.Last chance for topology queries and summary reports.
SystemVerilog — uvm_root lazy initialization (simplified source)
// How UVM implements the singleton — simplified from uvm_root source
 
class uvm_root extends uvm_component;
 
// The one-and-only instance
local static uvm_root m_inst;
 
// Protected/local constructor — cannot be called from outside
// (in real UVM source it's protected; shown as local here for clarity)
local function new();
super.new("__top__", null);  // name="__top__", no parent
endfunction
 
// The ONLY way to get uvm_root — lazy initialization
static function uvm_root get();
if (m_inst == null) begin
m_inst = new();             // created ONCE — ever
end
return m_inst;
endfunction
 
// What run_test("my_test") actually does:
task run_test(string test_name = "");
// 1. Create the test class via factory
// 2. Set up objection mechanism
// 3. Start phase execution engine
// 4. Wait for phases to complete
// 5. Call $finish
endtask
endclass
 
// The global run_test() shorthand is just:
// task run_test(string test_name = "");
//   uvm_root::get().run_test(test_name);
// endtask

§7 — Real Verification Usage — Where uvm_root Matters

UsageCodeWhy Not Another Approach
Regression timeoutuvm_root::get().set_timeout(5ms, 1) in build_phaseConfig_db timeout requires a component; uvm_root::get() works from any context
Topology debuguvm_root::get().print_topology() in start_of_simulationOnly uvm_root has the full view of the entire hierarchy
Finding components from sequencesuvm_root::get().find("path")Sequences have no component parent — find() is the only path-based lookup
Verifying architecture in CIfind_all("*.drv") + assertion on countAutomated check that the expected number of agents were built — catches configuration bugs
Custom singleton infrastructureStatic get() + null-initialized static memberShared counters, loggers, policy objects — avoids config_db overhead for non-hierarchical data

§8 — Bugs and Debugging Scenarios

Bug 1 — Calling find() Before the Hierarchy Exists

Symptom: find("uvm_test_top.env.drv") returns null even though the component definitely exists.

Root Cause: Called in build_phase of a component that builds before the target component. The hierarchy is constructed top-down — at the time build_phase of the environment runs, the agent (and its driver) haven't been built yet.

Fix: Use find() in start_of_simulation_phase or later. The hierarchy is complete after all build_phases finish.

SystemVerilog — common uvm_root bugs and fixes
// ── Bug 1: find() in build_phase — too early ──────────────────────────
// ❌ WRONG — driver may not exist yet
function void build_phase(uvm_phase phase);
super.build_phase(phase);
drv = uvm_root::get().find("uvm_test_top.env.apb_agent.drv");
// drv is null — agent hasn't been built yet
endfunction
 
// ✓ CORRECT — use start_of_simulation_phase (hierarchy is complete)
function void start_of_simulation_phase(uvm_phase phase);
super.start_of_simulation_phase(phase);
drv = uvm_root::get().find("uvm_test_top.env.apb_agent.drv");
if (drv == null) `uvm_fatal("TOPO","Driver not found — check naming")
endfunction
 
// ── Bug 2: Singleton custom class with static initializer issues ───────
// ❌ WRONG — calling new() directly creates a SECOND instance
class my_manager;
static my_manager m_inst;
function new(); endfunction        // public constructor — anyone can call this
static function my_manager get();
if(m_inst==null) m_inst=new(); return m_inst;
endfunction
endclass
 
my_manager a = my_manager::get();    // ← correct — singleton
my_manager b = new();                 // ← WRONG — creates second instance!
// a and b are different objects — a == b is 0
 
// ── Bug 3: set_timeout() in run_phase — too late ──────────────────────
// ❌ WRONG — timeout mechanism starts at beginning of run_phase
task run_phase(uvm_phase phase);
#100ns;
uvm_root::get().set_timeout(50ns, 1);  // set AFTER 100ns have passed
// timeout fires immediately — 50ns already expired
endtask
 
// ✓ CORRECT — set timeout in build_phase or start_of_simulation_phase
function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_root::get().set_timeout(1ms, 1);
endfunction
 
// ── Bug 4: find() path case sensitivity ───────────────────────────────
// find() is CASE SENSITIVE
root.find("uvm_test_top.Env.apb_agent.drv");   // ← "Env" with capital E → null!
root.find("uvm_test_top.env.apb_agent.drv");   // ← correct case → returns handle

§9 — Ready-to-Run Demo

Ready to Run — Questa / VCS / Xcelium

SystemVerilog — uvm_root_demo.sv (copy and run)
// uvm_root_demo.sv — uvm_root and singleton pattern demonstration
// Questa : vlog -sv uvm_root_demo.sv && vsim -c root_demo_top -do "run -all; quit"
// VCS    : vcs -sverilog -ntb_opts uvm uvm_root_demo.sv && ./simv
// Xcelium: xrun -sv -uvm uvm_root_demo.sv -input "run; exit"
 
`include "uvm_macros.svh"
import uvm_pkg::*;
 
// ── Custom singleton following UVM pattern ─────────────────────────────
class sim_stats;
local static sim_stats m_inst = null;
int total_txns = 0;
int errors = 0;
function new(); endfunction
static function sim_stats get();
if(m_inst==null) m_inst=new(); return m_inst;
endfunction
function void record(bit err=0); total_txns++; if(err) errors++; endfunction
function void print_report();
$display("=== Simulation Stats (singleton) ===");
$display("  Total: %0d  Errors: %0d", total_txns, errors);
endfunction
endclass
 
// ── Simple child component ─────────────────────────────────────────────
class my_driver extends uvm_component;
`uvm_component_utils(my_driver)
function new(string n, uvm_component p); super.new(n,p); endfunction
task run_phase(uvm_phase phase);
sim_stats::get().record(0);
sim_stats::get().record(1);   // simulate an error
sim_stats::get().record(0);
endtask
endclass
 
class my_env extends uvm_env;
`uvm_component_utils(my_env)
my_driver drv;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase p);
super.build_phase(p);
drv = my_driver::type_id::create("drv",this);
endfunction
endclass
 
class root_demo_test extends uvm_test;
`uvm_component_utils(root_demo_test)
my_env env;
function new(string n, uvm_component p); super.new(n,p); endfunction
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env",this);
uvm_root::get().set_timeout(1ms, 1);
`uvm_info("TEST",$sformatf("uvm_root path: '%s'",
uvm_root::get().get_full_name()),UVM_NONE)
endfunction
 
function void start_of_simulation_phase(uvm_phase phase);
uvm_component found;
uvm_component all[$];
super.start_of_simulation_phase(phase);
 
`uvm_info("TEST","=== Topology ===",UVM_NONE)
uvm_root::get().print_topology();
 
found = uvm_root::get().find("uvm_test_top.env.drv");
`uvm_info("TEST",$sformatf("find(drv) = %s",
found!=null ? found.get_type_name() : "NULL"),UVM_NONE)
 
uvm_root::get().find_all("uvm_test_top.*", all);
`uvm_info("TEST",$sformatf("find_all(uvm_test_top.*) found %0d",all.size()),UVM_NONE)
 
// Verify singleton — both calls return same object
`uvm_info("TEST",$sformatf("sim_stats singleton: %s",
(sim_stats::get() === sim_stats::get()) ?
"same instance (correct)" : "different instance (BUG)"),UVM_NONE)
endfunction
 
task run_phase(uvm_phase phase);
phase.raise_objection(this);
#10ns;
phase.drop_objection(this);
endtask
 
function void report_phase(uvm_phase phase);
super.report_phase(phase);
sim_stats::get().print_report();
endfunction
endclass
 
module root_demo_top;
initial run_test("root_demo_test");
endmodule
 
// EXPECTED OUTPUT (abridged):
// TEST: uvm_root path: ''  (empty string — root has no name)
// TEST: === Topology ===
// UVM_INFO: uvm_test_top [root_demo_test]
//   env [my_env]
//     drv [my_driver]
// TEST: find(drv) = my_driver
// TEST: find_all(uvm_test_top.*) found 2  (env and drv)
// TEST: sim_stats singleton: same instance (correct)
// === Simulation Stats (singleton) ===
//   Total: 3  Errors: 1

§10 — Interview Questions

  • What is uvm_root and why can there only be one instance? uvm_root is the implicit parent of all UVM components — it sits at the root of every testbench hierarchy. There can only be one because it owns the global phase execution engine, the component hierarchy tree, and the simulation timeout. If there were multiple instances, phases would execute independently for each tree, components could not be found across trees, and the framework would lose coherence. The singleton pattern enforces this: the constructor is protected, and uvm_root::get() is the only way to obtain the handle — always returning the same object.
  • What is the full path (get_full_name()) of uvm_root itself, and why? An empty string — "". get_full_name() in uvm_component builds the path by concatenating parent names with dots. uvm_root has no parent, so its base name is an empty string. The test component (named "uvm_test_top" by default) gets its path as: empty string + "." + "uvm_test_top" = "uvm_test_top". This is why every component's full path starts with "uvm_test_top" — it's the first real name in the chain.
  • Why should find() be called in start_of_simulation_phase rather than build_phase? build_phase executes top-down. When a parent component's build_phase runs, its children's build_phases have not executed yet — they don't exist in the hierarchy yet. find() searching for a child during the parent's build_phase will return null because the child was not created yet. start_of_simulation_phase is called after all build_phases and connect_phases have completed, guaranteeing the full hierarchy is in place and every component is findable by path.
  • Tricky: If a sequence calls uvm_root::get().find("uvm_test_top.env.scb") during run_phase, can it safely call methods on the returned component? Yes, with one caveat: the downcast. find() returns a uvm_component handle. You must $cast() it to your actual scoreboard type before calling custom methods. The component exists, is fully built, and its run_phase is executing in parallel — so method calls are safe if the scoreboard's methods are non-time-consuming (functions). Calling a task on the scoreboard from a sequence introduces potential race conditions if both the sequence and the scoreboard's own run_phase modify shared state — in that case, use analysis ports, not direct method calls.

§11 — Best Practices and Engineering Summary

PracticeReasoning
Set timeout in build_phase or start_of_simulation_phaseTimeout must be set before run_phase begins. Setting it in run_phase after a delay may trigger immediately if the simulation has already exceeded the timeout.
Call find() only in start_of_simulation_phase or laterThe hierarchy is only guaranteed complete after all build_phases finish. Earlier calls may return null for components not yet constructed.
Always null-check find() return valueA wrong path string, case mismatch, or incorrect component name silently returns null. Crashing on null gives a useless simulation error — a null check with uvm_fatal gives a meaningful message.
Use print_topology() gated by a plusarg during developmentif ($test$plusargs("PRINT_TOPO")) keeps regression logs clean but gives you a one-command way to verify architecture during debug.
For custom singletons: private constructor + static member + static get()This is the only correct implementation. A public constructor breaks the singleton contract — any code can create additional instances, defeating the entire purpose.
Prefer find() over direct handles where possibleDirect handles create compile-time dependencies. find() with a path string keeps packages decoupled — a scoreboard found by path doesn't require importing the scoreboard's package.

uvm_root is one of those things you use every day without knowing it. Every run_test() call goes through it. Every phase executes under its orchestration. Every component's dot-separated path derives from its empty-string name at the top.

The singleton pattern it exemplifies is worth internalizing as a design tool. When you need something that is genuinely global — one instance, accessible from any context, no handle passing, no hierarchy dependency — the static get() pattern is the right answer. UVM uses it in four places. On a sufficiently large project, you'll need it in at least one of your own.