How a UVM testbench hierarchy is structured, the roles of test / env / agent, top-down build and bottom-up connect, deferred construction, the UVM phase system, naming and path conventions, and a complete block-level testbench skeleton.
A UVM testbench is a tree of uvm_component objects. Every node in the tree except the root has a parent. The tree is built top-down during the build phase — each component creates its children, which in turn create their children, until the leaf nodes (drivers, monitors) are reached.
The standard hierarchy for a single-interface block-level testbench has five levels:
The UVM hierarchy is built using has-a (composition) relationships, not is-a (inheritance) relationships. A my_env “has a” my_agent and “has a” scoreboard. The agent “has a” driver, “has a” monitor, “has a” sequencer.
Every component is declared as a member handle inside its parent, then constructed in the parent’s build_phase using the factory create() method. The parent passes itself as the parent argument, establishing the tree link:
class my_env extends uvm_env; `uvm_component_utils(my_env) my_agent m_agent; // handle — null until build_phase my_scoreboard m_sb; my_coverage m_cov; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); // ① Factory creates child — passes "this" as parent m_agent = my_agent::type_id::create("agent", this); m_sb = my_scoreboard::type_id::create("sb", this); m_cov = my_coverage::type_id::create("cov", this); endfunction endclass
type_id::create() — never call new() directly. Calling new() bypasses the factory entirely, which means factory overrides will not work. Every UVM component and object must be created through the factory so test-time substitution is possible.
Every uvm_component has a unique dot-separated path name that reflects its position in the hierarchy. The root test is always named uvm_test_top by the UVM infrastructure. Below it, the path is built from the name strings passed to create().
Path names are used everywhere in UVM. When you call uvm_config_db::set(), you specify a path pattern to determine which components receive the value. When `uvm_info prints a message, the component’s path is included automatically. When a phase error occurs, the path tells you exactly which component caused the problem.
"apb_agent", then config_db patterns like "*.apb_agent.*" will match its children. Generic names like "agent" work but can cause ambiguity in multi-agent environments. Consistent naming is critical for config_db to work correctly.
UVM controls the lifecycle of every testbench through a phase system. Phases execute in a defined order, ensuring that components are fully built before they are connected, fully connected before they run, and able to report results after running. There are 12 standard phases organised into three groups.
| Phase | Type | Direction | What happens |
|---|---|---|---|
build_phase | Function | Top-down | Components create child components. config_db values are retrieved. Factory overrides applied. |
connect_phase | Function | Bottom-up | TLM ports connected between components. Analysis ports connected to subscribers. Virtual interfaces assigned from config_db. |
end_of_elaboration_phase | Function | Bottom-up | Hierarchy is final. Print topology, check configuration. No construction or connections allowed. |
start_of_simulation_phase | Function | Bottom-up | Last setup before time starts. Print configuration, seed info. |
run_phase | Task | Concurrent | Main simulation. All components run simultaneously. Ends when all objections dropped. |
extract_phase | Function | Bottom-up | Extract results from simulation — read final register values, collect statistics. |
check_phase | Function | Bottom-up | Check for errors — unmatched transactions, protocol violations, coverage holes. |
report_phase | Function | Bottom-up | Print results — pass/fail, coverage summary, statistics. |
final_phase | Function | Top-down | Final cleanup before simulation exits. |
run_phase is a function — it executes in zero simulation time. run_phase is a task — it can consume simulation time and run concurrently in all components. This is why you can only call #delay or @(event) inside run_phase, never inside build_phase or connect_phase.
The build phase runs top-down: the test’s build_phase runs first, then env’s, then agent’s, then driver/monitor/sequencer. This order is mandatory and enforced by UVM. You cannot change it.
Top-down ordering exists for a specific reason: the parent needs to configure the child before the child builds. The test creates and configures config objects, places them in config_db, then creates the env. When the env’s build_phase runs, it retrieves the config from config_db (already set by the test), uses it to decide what to build, and creates agents. Each agent then retrieves its own config and builds its sub-components.
class base_test extends uvm_test; `uvm_component_utils(base_test) my_env m_env; my_env_config m_cfg; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); // ① Create config, assign virtual interface from SV top m_cfg = my_env_config::type_id::create("m_cfg"); if (!uvm_config_db #(virtual dut_if)::get( this, "", "dut_vif", m_cfg.vif)) `uvm_fatal("CFG", "No virtual interface!") // ② Place config so env can retrieve it uvm_config_db #(my_env_config)::set( this, "m_env", "cfg", m_cfg); // ③ Build env — triggers env's build_phase m_env = my_env::type_id::create("m_env", this); endfunction task run_phase(uvm_phase phase); my_sequence seq; phase.raise_objection(this); seq = my_sequence::type_id::create("seq"); seq.start(m_env.m_agent.m_seqr); // start on sequencer phase.drop_objection(this); endtask endclass
The connect phase runs bottom-up — the opposite direction to build. It runs after all build_phases have completed, meaning the entire component tree is fully constructed. Only then can connections between components be made safely (you cannot connect to a component that hasn’t been built yet).
Three types of connections happen in connect_phase:
analysis_port.connect(scoreboard.analysis_export)seq_item_port connects to the sequencer’s seq_item_exportclass my_env extends uvm_env; `uvm_component_utils(my_env) my_agent m_agent; my_scoreboard m_sb; my_coverage m_cov; function void connect_phase(uvm_phase phase); // Connect monitor's analysis port → scoreboard + coverage m_agent.m_monitor.ap.connect(m_sb.analysis_export); m_agent.m_monitor.ap.connect(m_cov.analysis_export); endfunction endclass
The run_phase is a task — the only phase that consumes simulation time. It executes concurrently in all components that implement it. The driver’s run_phase, monitor’s run_phase, scoreboard’s run_phase all run simultaneously using SystemVerilog’s automatic fork/join semantics.
Simulation time in run_phase ends when all phase objections are dropped. An objection is a counter — as long as the count is non-zero, the phase continues. This allows any component to extend the simulation by raising an objection, and signal completion by dropping it.
The agent is the most important structural unit in UVM. It encapsulates all the components needed to drive and observe one protocol interface. A well-designed agent can be plugged into any environment that uses the same interface.
An agent has two modes controlled by the is_active field (inherited from uvm_agent):
is_active == UVM_ACTIVE) — contains sequencer + driver + monitor. Drives stimulus and observes responses.is_active == UVM_PASSIVE) — contains only the monitor. Does not drive anything. Used when observing a third-party DUT interface that is driven by another agent or by the RTL itself.class my_agent extends uvm_agent; `uvm_component_utils(my_agent) my_driver m_drv; my_sequencer m_seqr; my_monitor m_mon; my_agent_cfg m_cfg; function void build_phase(uvm_phase phase); // Retrieve config from config_db if (!uvm_config_db #(my_agent_cfg)::get( this, "", "cfg", m_cfg)) `uvm_fatal("CFG", "No agent config!") // Monitor always built — active or passive m_mon = my_monitor::type_id::create("m_mon", this); // Sequencer + driver only in active mode if (m_cfg.is_active == UVM_ACTIVE) begin m_seqr = my_sequencer::type_id::create("m_seqr", this); m_drv = my_driver::type_id::create("m_drv", this); end endfunction function void connect_phase(uvm_phase phase); // Connect driver seq_item_port to sequencer if (m_cfg.is_active == UVM_ACTIVE) m_drv.seq_item_port.connect(m_seqr.seq_item_export); endfunction endclass
is_active.
The complete file structure for a typical block-level UVM testbench. Each file contains one class. All files are compiled as a package.
| File | Contains | Extends |
|---|---|---|
my_seq_item.sv | Transaction class with rand fields | uvm_sequence_item |
my_sequence.sv | Base sequence, body() generates items | uvm_sequence#(my_seq_item) |
my_driver.sv | run_phase drives DUT via virtual interface | uvm_driver#(my_seq_item) |
my_monitor.sv | run_phase observes DUT, publishes analysis_port | uvm_monitor |
my_sequencer.sv | Typedef — usually just a type alias | uvm_sequencer#(my_seq_item) |
my_agent_cfg.sv | Config: vif handle, is_active flag | uvm_object |
my_agent.sv | build + connect: seqr/drv/mon, conditional on is_active | uvm_agent |
my_env_cfg.sv | Top-level config: agent config, enable flags | uvm_object |
my_scoreboard.sv | analysis_imp, write(), compare logic | uvm_scoreboard |
my_env.sv | build: creates agent + sb + cov; connect: ap wiring | uvm_env |
base_test.sv | build: config + env; run: start root sequence | uvm_test |
my_pkg.sv | Package: `include all the above in order | — |
tb_top.sv | DUT + interface + clock; initial run_test() | module (static) |
| Item | Key fact |
|---|---|
| Hierarchy root | uvm_test_top — always this name, created by run_test() |
| Has-a relationship | Parent declares child handle, creates it via factory in build_phase |
| Factory creation | ChildClass::type_id::create("name", this) — never use new() |
| Path name format | uvm_test_top.env.agent.driver — built from create() name args |
| build_phase direction | Top-down — test first, leaf components last |
| connect_phase direction | Bottom-up — leaf components first, test last |
| run_phase type | Task — only phase that consumes simulation time |
| run_phase concurrency | Runs simultaneously in all components that implement it |
| run_phase end condition | All phase objections dropped (count = 0) |
| raise/drop objection | phase.raise_objection(this) / phase.drop_objection(this) |
| Active agent | Contains sequencer + driver + monitor. is_active == UVM_ACTIVE |
| Passive agent | Contains monitor only. is_active == UVM_PASSIVE |
| Monitor built when | Always — both active and passive agents |
| config_db set location | Test’s build_phase — before creating the env that will retrieve it |
| connect_phase typical work | analysis_port.connect(), driver seq_item_port.connect() |