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 Singleton | What It Represents | How You Access It |
|---|---|---|
uvm_root | Root of all component hierarchy. Manages phases, timeout, component search | uvm_root::get() |
uvm_factory | Class registry and override table — all type substitution logic | uvm_factory::get() |
uvm_report_server | Message counting, severity actions, end-of-simulation summary | uvm_report_server::get_server() |
uvm_resource_pool | The backing store for all resource_db and config_db entries | uvm_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
// ── 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| Method | Return | Purpose |
|---|---|---|
uvm_root::get() | uvm_root | Get the singleton handle. Creates instance on first call. |
root.find(path) | uvm_component | Find 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) | void | Set simulation timeout. fatal=1 triggers uvm_fatal, fatal=0 triggers uvm_error. |
root.print_topology() | void | Print complete component hierarchy in tree format. |
root.run_test("name") | void | Start 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 |
|---|---|
| Immediately | Creates the test component via factory (uvm_test_top) as its own child |
| Phase: build_phase | Calls build_phase top-down on every component in the hierarchy (depth-first) |
| Phase: connect_phase | Calls connect_phase bottom-up — children before parents |
| Phase: run_phase | Spawns 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 dropped | Proceeds through extract → check → report → final phases |
| final_phase done | Calls $finish — simulation ends |
§5 — Code Examples — Practical uvm_root Usage
Example 1 — Topology Inspection and Component Search
// ── 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)
endfunctionExample 2 — Implementing Your Own UVM-Style Singleton
// ── 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 neededExample 3 — Using find() From a Sequence (No Component Context)
// 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 Event | uvm_root State | What You Can Safely Do |
|---|---|---|
Before run_test() | Instance exists (created lazily on first get() call), but test component not yet created | Call uvm_root::get() safely. Do NOT call find() — hierarchy doesn't exist yet. |
During build_phase | Hierarchy 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_phase | Complete hierarchy exists. All components built and connected. | Safe to call find(), find_all(), print_topology(). All queries return accurate results. |
During run_phase | Full hierarchy, phases running, timeout active if set | All uvm_root operations safe. find() returns accurate handles. |
After report_phase | Hierarchy intact, phases done. About to call $finish. | Last chance for topology queries and summary reports. |
// 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
| Usage | Code | Why Not Another Approach |
|---|---|---|
| Regression timeout | uvm_root::get().set_timeout(5ms, 1) in build_phase | Config_db timeout requires a component; uvm_root::get() works from any context |
| Topology debug | uvm_root::get().print_topology() in start_of_simulation | Only uvm_root has the full view of the entire hierarchy |
| Finding components from sequences | uvm_root::get().find("path") | Sequences have no component parent — find() is the only path-based lookup |
| Verifying architecture in CI | find_all("*.drv") + assertion on count | Automated check that the expected number of agents were built — catches configuration bugs |
| Custom singleton infrastructure | Static get() + null-initialized static member | Shared 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.
// ── 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
// 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_rootis 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, anduvm_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()inuvm_componentbuilds 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_componenthandle. 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
| Practice | Reasoning |
|---|---|
| Set timeout in build_phase or start_of_simulation_phase | Timeout 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 later | The 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 value | A 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 development | if ($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 possible | Direct 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.