Every component you build - drivers, monitors, environments, tests - needs a parent somewhere up the chain, all the way to a single root. uvm_root is that root. You never write uvm_root my_root = new(); yourself, but every testbench you have ever run has one, automatically created the moment run_test() is called, and every orphaned component in your hierarchy quietly becomes its child.

This article covers what uvm_root actually is, where it sits in the base class hierarchy, and the three things its global handle uvm_top is most commonly used for: implicit parenting, hierarchical component search, and global report configuration.

7 min read | Intermediate Level

What You'll Learn

  • What uvm_root is and why you should never instantiate it yourself
  • How orphaned components (no parent specified) end up under uvm_top
  • How to search the component hierarchy with find() and find_all()
  • How to set verbosity and access the config database globally through uvm_top

What is uvm_root ?

uvm_root is a singleton class that serves as the top-level container for all UVM components in a verification environment. Its single instance is called uvm_top. UVM creates this instance automatically the moment run_test() is called - you never construct it, and you should never try to create a second instance.

If you only remember one thing about uvm_root, make it this: it is always there, it is always exactly one object, and every component without an explicit parent ends up attached to it.

How the Source Actually Creates It

It's worth seeing the mechanism in the UVM library itself rather than taking "it's a singleton" on faith. Inside uvm_root.svh, the class keeps a private static handle and only ever lets one instance exist:


class uvm_root extends uvm_component;

  // singleton handle
  static local uvm_root m_inst;

  // Function: get()
  // Get the singleton uvm_root instance
  extern static function uvm_root get();

  ...

endclass

m_inst is declared static local, so no code outside uvm_root itself can touch the handle directly. The only door in is the static get() function, which goes through a layer called uvm_coreservice_t - UVM's central lookup point for the factory, the report server, and the root, all in one place:


// uvm_coreservice_t::get_root() ultimately calls back into uvm_root
// to allocate the one instance on first use, and returns the same
// handle on every call after that.

uvm_coreservice_t cs;
uvm_root r;

cs = uvm_coreservice_t::get();
r = cs.get_root();

// uvm_root::get() is a convenience wrapper around the two lines above
r = uvm_root::get();

This is also exactly what the global uvm_top variable resolves to - uvm_top and uvm_root::get() always hand back the same object, because there is only ever one to hand back.

The other detail worth seeing in source is how a brand-new component decides whether it belongs to a real parent or to uvm_top. That logic lives in uvm_component::new() inside uvm_component.svh:


function uvm_component::new (string name, uvm_component parent);
  uvm_root top;
  uvm_coreservice_t cs;

  super.new(name);

  // If this construction IS uvm_top itself, blank its name so it
  // never shows up in any child's full hierarchical path, then return
  if (parent==null && name == "__top__") begin
    set_name("");
    return;
  end

  ...
  // Any other component with parent==null is attached to uvm_top here
endfunction

That name == "__top__" check is the special case reserved for uvm_root's own construction. Every other component you create with parent=null falls through past that check and is parented to uvm_top instead - which is the source-level reason behind the "orphaned components attach to uvm_top" behavior described above.

Finally, this is what actually happens when your testbench calls run_test("my_test"). The global run_test task (defined in uvm_globals.svh) does almost nothing itself - it just forwards to the one root instance:


task run_test (string test_name="");
  uvm_root top;
  uvm_coreservice_t cs;

  cs = uvm_coreservice_t::get();
  top = cs.get_root();

  top.run_test(test_name);
endtask

Inside uvm_root::run_test(), the test class you named is pulled from the factory and constructed with uvm_top as its parent and the fixed instance name "uvm_test_top":


task uvm_root::run_test(string test_name="");
  uvm_factory factory = uvm_factory::get();
  uvm_component uvm_test_top;
  ...

  $cast(uvm_test_top, factory.create_component_by_name(test_name,
        "", "uvm_test_top", null));

  ...
endtask

That single line is the entire source-level explanation for why every full path in your log starts with uvm_test_top: the factory builds your test, names the instance literally "uvm_test_top", and the constructor logic shown above attaches it under uvm_top because no other parent was given.

Class Hierarchy

uvm_root extends uvm_component, which means it inherits the full chain described in UVM Base Classes: hierarchy management, phasing, reporting, and factory access all come along for free. The one thing that makes uvm_root different from every other component you write is that UVM itself creates and manages its single instance - you interact with that instance through the global variable uvm_top, never through a class handle you declare yourself.

Implicit Top-Level

uvm_top is the implicit top-level component, and any component whose parent argument is left unspecified (or explicitly passed as null) becomes a child of uvm_top instead of floating around with no place in the hierarchy.


class my_env extends uvm_env;
	`uvm_component_utils (my_env)

	// Note that if parent is not explicitly passed during creation, it will be null
	function new(string name = "my_env", uvm_component parent = null);
		super.new(name, parent);
	endfunction

endclass

class my_test extends uvm_test;
	`uvm_component_utils (my_test)

	my_env 		m_env_1;
	my_env 		m_env_2;
	...

	virtual function void build_phase(uvm_phase phase);
		super.build_phase(phase);

		// Parent is set to my_test
		m_env_1 = my_env::type_id::create("m_env_1", this);

		// Parent is null, and hence set to uvm_top
		m_env_2 = my_env::type_id::create("m_env_2");
	endfunction
endclass

In the example above, m_env_1 is built normally, with this (the test) passed as its parent - that is the pattern you should use for almost everything in a real testbench. m_env_2 is created with no second argument at all, so it silently attaches itself under uvm_top instead of under the test. The test class itself works the same way: when you call run_test("my_test"), UVM creates an instance of my_test with uvm_top as its parent and names it uvm_test_top, which is why that name shows up in every full hierarchical path you have ever seen in a log file.

Quick Check

Q: If you create a component and pass null as its parent, where does it end up in the hierarchy?

Show Answer

It becomes a direct child of uvm_top, the same place any component with an unspecified parent ends up.

Search Functionality

uvm_top can search for components by their full hierarchical name, using find() for a single match or find_all() for every match. Both accept wildcard patterns: * matches any sequence of characters and ? matches exactly one character. The search walks the hierarchy top to bottom in alphanumeric order, and a pattern starting with a dot is treated as an absolute path rather than a wildcard search from the top.


class my_test extends uvm_test;
	`uvm_component_utils (my_test)

	my_env 		m_env;

	virtual function void build_phase(uvm_phase phase);
		super.build_phase(phase);

		m_env = my_env::type_id::create("m_env", this);
	endfunction

	virtual function void end_of_elaboration_phase(uvm_phase phase);
		uvm_component 	comp;
		uvm_component 	comp_q [$];

		super.end_of_elaboration_phase(phase);

		// Gets handle to the driver indicated by the path
		comp = uvm_top.find("uvm_test_top.m_env.m_agent.m_driver");

		// Gets the first handle to the driver that matches the pattern
		comp = uvm_top.find("*apb_driver*");

		// Gets queue of all handles that match the pattern
		uvm_top.find_all("*apb_driver*", comp_q);

		foreach (comp_q[i]) begin
			`uvm_info (get_type_name(), $sformatf("Found component %s", comp_q[i].get_full_name()), UVM_LOW)
		end

	endfunction
endclass

find_all() takes an optional third argument, a starting component, which limits the search to that component's subtree instead of the whole testbench. This is useful when two different agents happen to have identically-named children and you only want matches under one of them.

Where Is This Used?

Wildcard search through uvm_top is commonly used in debug code and in generic testbench utilities that need to act on every instance of a particular component type - for example, raising verbosity on every monitor in a multi-agent SoC environment, or collecting handles to every scoreboard to print a combined pass/fail summary at the end of a test, without hardcoding each instance name.

Report Configuration

uvm_top can set a global report verbosity that affects every component in the simulation in one call, and its reporting interface is reachable from outside uvm_component entirely - from modules, sequences, or anywhere else that has visibility into uvm_pkg.


class my_test extends uvm_test;

	...

	virtual function void build_phase(uvm_phase phase);
		super.build_phase(phase);

		uvm_top.set_report_verbosity_level_hier(UVM_HIGH);
	endfunction
endclass

The name of uvm_top itself is set to an empty string "", specifically so it does not appear in the full hierarchical name of its children - this is why a path looks like uvm_test_top.m_env.m_agent rather than uvm_top.uvm_test_top.m_env.m_agent. Another very common use of the same instance is passing it as the context argument in uvm_config_db, which makes a value visible to every component in the testbench:


// Set m_cfg to the top level and let everything else beneath it have access
uvm_config_db #(my_cfg)::set(uvm_root::get(), "*", "my_cfg", m_cfg);

if (! uvm_config_db #(my_cfg)::get(uvm_root::get(), "*", "my_cfg", m_cfg))
	`uvm_fatal (get_type_name(), $sformatf("[ERROR] my_cfg not found in config_db !"))

uvm_root::get() is the static accessor for the singleton instance, and it returns the exact same object as the global uvm_top variable. The two are interchangeable; uvm_root::get() is just the form you reach for in contexts (like static functions) where referencing a global variable directly feels less natural.

One more behavior worth knowing about uvm_top: at the end of the end_of_elaboration phase, it checks whether any errors have been generated so far in the simulation. If it finds any, it raises a UVM_FATAL and the simulation stops before start_of_simulation_phase or run_phase ever begins. If your test fails before a single clock edge happens, this check - not your run_phase code - is usually the first place to look.

Common Mistakes

Mistake #1: Forgetting to pass this as the parent when creating a child component

Wrong: m_agent = my_agent::type_id::create("m_agent");
Right: m_agent = my_agent::type_id::create("m_agent", this);

Why? Omitting the parent does not cause an error - the component still builds and runs. But it silently attaches under uvm_top instead of under its intended parent, so its full hierarchical name (and anything that depends on that name, like uvm_config_db scoping) ends up wrong in a way that is easy to miss until a config lookup fails.

Mistake #2: Calling find() before the hierarchy is built

Wrong: calling uvm_top.find("*.m_driver") inside the test's build_phase
Right: calling it in end_of_elaboration_phase or later

Why? build_phase runs top-down, so while the test's build_phase is executing, the components below it have not been created yet. find() can only return components that already exist, so it returns null and prints a warning even though the path is correct.

Practice Exercise

Challenge: Find Every Driver in a Multi-Agent Environment

Suppose your environment has three agents, each named differently but each containing a driver named m_driver. Write a snippet inside end_of_elaboration_phase that collects handles to all three drivers using a single wildcard search, and prints each one's full hierarchical name.

Hint

You want every match, not just the first one, so reach for find_all() with a pattern that matches any path ending in m_driver.

Solution

virtual function void end_of_elaboration_phase(uvm_phase phase);
  uvm_component drv_q[$];
  super.end_of_elaboration_phase(phase);

  drv_q = uvm_top.find_all("*m_driver");

  foreach (drv_q[i]) begin
    `uvm_info(get_type_name(), $sformatf("Driver found: %s", drv_q[i].get_full_name()), UVM_LOW)
  end
endfunction

The leading * matches any prefix path, so this picks up m_driver regardless of which agent or environment it sits under.

Summary

In this tutorial, you learned:

  • uvm_root is a singleton, automatically created by run_test(), and accessed through the global handle uvm_top or the static accessor uvm_root::get()
  • uvm_root extends uvm_component, inheriting hierarchy, phasing, reporting, and factory access from the same base class chain every other component uses
  • Any component created with no parent (or parent=null) automatically becomes a child of uvm_top, including the top-level test itself, which is named uvm_test_top
  • find() and find_all() search the hierarchy by wildcard pattern, with an optional starting component to limit the search scope
  • uvm_top can set verbosity globally and is commonly used as the context argument in uvm_config_db so a value is visible from anywhere in the testbench
  • uvm_top raises a UVM_FATAL at the end of end_of_elaboration if any errors were already reported, stopping the simulation before run_phase begins
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5