UVM phases are the backbone of every UVM testbench. They define a strict, ordered lifecycle that every testbench component goes through - from construction all the way to cleanup. Without phases, components could try to communicate before they've been built, or a driver could start sending stimulus before the scoreboard is ready to receive data. Understanding phases is essential before writing any real UVM testbench code.
8 min read | Intermediate Level
What You'll Learn
- What UVM phases are and why they exist
- The three phase categories and what happens in each
- Which phase to use for building, connecting, running, and checking
- How run-time sub-phases extend the
run_phase
What Are UVM Phases?
All testbench components derived from uvm_component participate in a pre-defined set of phases. A component cannot move to the next phase until every component in the testbench has finished the current one. This makes phases a global synchronization mechanism - ensuring that, for example, no component tries to connect a TLM port to another component that hasn't been constructed yet.
Phases are implemented as callbacks. Each phase corresponds to either a function (zero simulation time) or a task (may consume simulation time). You override the relevant phase method in your component class to perform work during that phase.
The run_phase is the only top-level phase implemented as a task. All build, connect, and cleanup phases are functions and execute in zero simulation time.
All phases are grouped into three categories:
- Build phases - Construct components, make connections, configure the environment
- Run phase - Drive stimulus and monitor DUT behavior (consumes simulation time)
- Cleanup phases - Extract results, check for errors, and report
The Main UVM Phases
| Category | Phase Name | Type | Primary Purpose |
|---|---|---|---|
| Build | build_phase | function | Construct and configure testbench components |
| Build | connect_phase | function | Connect TLM ports and exports between components |
| Build | end_of_elaboration_phase | function | Final adjustments after all connections are made |
| Build | start_of_simulation_phase | function | Set initial runtime configuration, display topology |
| Run | run_phase | task | Drive stimulus; runs in parallel with run-time sub-phases |
| Cleanup | extract_phase | function | Collect final data from scoreboard and DUT |
| Cleanup | check_phase | function | Compare expected vs. actual results, flag errors |
| Cleanup | report_phase | function | Print test results and summary |
| Cleanup | final_phase | function | Close files, terminate co-simulation engines |
Build Phase Order: Top-Down
The build_phase executes top-down through the component hierarchy - the test runs first, then the environment, then agents, then drivers and monitors. build_phase and final_phase are the only two phases that run top-down; all other phases (including connect_phase and all cleanup phases) run bottom-up.
The reason is straightforward: a parent component must create its child components before those children can do anything. The test creates the environment, the environment creates the agent, and so on. If the agent tried to run its build_phase before the environment created it, the object simply wouldn't exist.
Here's an example of a typical build_phase in an environment:
class my_env extends uvm_env;
`uvm_component_utils(my_env)
my_agent agent;
my_scoreboard sb;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Create child components - must happen in build_phase
agent = my_agent::type_id::create("agent", this);
sb = my_scoreboard::type_id::create("sb", this);
endfunction
// Rest of the code (connect_phase, etc.)
endclass
Never create child components outside of build_phase. Creating them in connect_phase or later means they won't exist when other components try to connect to them.
The UVM topology printed by uvm_root reflects this hierarchy after build_phase completes:
// Typical build output from $cast or uvm_top.print_topology()
// uvm_test_top [my_test]
// e0 [my_env]
// a0 [my_agent]
// d0 [my_driver]
// m0 [my_monitor]
// s0 [uvm_sequencer]
// sb0 [my_scoreboard]
Quick Check
Q: A driver tries to connect its TLM port inside build_phase. What problem could occur?
Show Answer
The sequencer the driver wants to connect to may not have been created yet. TLM connections belong in connect_phase, which runs only after all components have finished build_phase.
Connect Phase: Wiring the Testbench
Once every component exists, connect_phase wires them together. This is where TLM ports are connected to exports, and where a driver's seq_item_port is connected to a sequencer's seq_item_export.
class my_agent extends uvm_agent;
`uvm_component_utils(my_agent)
my_driver drv;
my_monitor mon;
uvm_sequencer #(my_transaction) seqr;
function void connect_phase(uvm_phase phase);
// Connect driver to sequencer - both must exist before this runs
drv.seq_item_port.connect(seqr.seq_item_export);
endfunction
// Rest of the code (build_phase, etc.)
endclass
Why Verilog Testbenches Don't Need Phases
A Verilog testbench is built entirely from modules, which are static constructs. All modules are elaborated before simulation time zero - every driver, monitor, and checker exists before a single clock edge occurs. There is no dynamic object creation, so there's no risk of one component referencing another before it exists.
// Verilog: all components exist at time 0, no phases needed
module tb_top;
dut d0 (...);
driver drvr (...);
monitor mon0 (...);
endmodule
Why UVM Testbenches Require Phases
UVM testbenches are built from class objects, not modules. A class object only exists after it has been constructed with new() or type_id::create(). This means it's entirely possible to reference a component handle that is still null if the construction hasn't happened yet.
class my_env extends uvm_env;
my_driver drv; // Just a handle - drv is null until build_phase constructs it
my_monitor mon;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
drv = my_driver::type_id::create("drv", this); // Now drv is valid
mon = my_monitor::type_id::create("mon", this);
endfunction
endclass
Without a phase synchronization mechanism, a monitor could attempt to connect an analysis port to a scoreboard that hasn't been constructed yet, causing a null pointer access. Phases eliminate this entire class of bugs by enforcing a strict ordering: build everything first, then connect, then run.
Reusable verification IP (VIP) from different vendors can be plugged together reliably because they all follow the same phase contract. This is one of the key reasons UVM phases exist.
Run-Time Sub-Phases
The run_phase runs in parallel with a set of finer-grained run-time sub-phases. These sub-phases let you structure the stimulus sequence inside a component without using manual event synchronization. They run in the following order:
| Sub-Phase | Typical Use |
|---|---|
pre_reset_phase | Wait for power-good signals, initial checks |
reset_phase | Assert and release DUT reset |
post_reset_phase | Wait for DUT to come out of reset cleanly |
pre_configure_phase | Prepare for configuration writes |
configure_phase | Program registers and configure DUT for test |
post_configure_phase | Verify configuration was applied correctly |
pre_main_phase | Final setup before main stimulus |
main_phase | Primary test stimulus and functional traffic |
post_main_phase | Flush pipelines, drain pending transactions |
pre_shutdown_phase | Begin shutdown sequence |
shutdown_phase | Wait for DUT to finish all in-flight operations |
post_shutdown_phase | Final checks before cleanup phases begin |
Most testbenches only use run_phase for simplicity. Use the sub-phases when you need fine-grained control over reset sequencing, DUT configuration ordering, or when integrating VIP components from different vendors that expect specific sub-phases.
What to Do in Each Phase
The table below summarizes the entry conditions, typical tasks, and exit criteria for each phase.
| Phase | Upon Entry | Typical Uses | Exit Criteria |
|---|---|---|---|
build_phase | Top-level components instantiated under uvm_root | Instantiate sub-components; get/set config values; build register model | All uvm_component objects have been created |
connect_phase | All components have been instantiated | Connect TLM ports/exports and sockets; connect register model adapter; set up phase domains | All inter-component connections established |
end_of_elaboration_phase | Environment fully assembled and connected | Print topology; open log files; apply final configuration overrides | None |
start_of_simulation_phase | Simulator ready; environment fully configured | Display topology; set debugger breakpoints; set initial runtime config | None |
run_phase | Simulation time begins (power applied to DUT) | Drive sequences; monitor DUT responses; raise/drop objections to control end-of-test | All objections dropped; DUT no longer needs simulation |
extract_phase | Simulation time has stopped | Collect final scoreboard data; probe DUT via backdoor; compute statistics | All data collected and summarized |
check_phase | All data collected | Compare expected vs. actual; flag any unmatched transactions | Test known to pass or fail |
report_phase | Test result determined | Print pass/fail summary; write results to file | End of test |
final_phase | All test activity complete | Close files; terminate co-simulation engines | Simulator exits |
Quick Check
Q: You want to print the full UVM component hierarchy after all components are built and connected. Which phase is the best place to do this?
Show Answer
end_of_elaboration_phase - at this point the entire environment has been assembled and all connections made, but simulation hasn't started yet. It's the standard place to call uvm_top.print_topology().
Overriding a Phase: Example
To perform work in a phase, simply override the corresponding method in your component. Here's a driver using both build_phase and run_phase:
class my_driver extends uvm_driver #(my_transaction);
`uvm_component_utils(my_driver)
virtual my_interface vif;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Retrieve virtual interface from config database
if (!uvm_config_db #(virtual my_interface)::get(this, "", "vif", vif))
`uvm_fatal("NO_VIF", "Virtual interface not found in config_db")
endfunction
virtual task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
// Drive transaction onto the interface
@(posedge vif.clk);
vif.addr <= req.addr;
vif.data <= req.data;
seq_item_port.item_done();
end
endtask
// Rest of the code (connect_phase, constructor, etc.)
endclass
Where Are UVM Phases Used?
UVM phases are mandatory in any industrial verification environment. SoC verification teams often integrate VIP components from multiple vendors - each following the same phase protocol - making it possible to assemble complete testbenches without custom synchronization logic. Phases are especially critical in complex SoCs where the reset sequence, register initialization, and main stimulus windows each need to happen in a well-defined, coordinated order across dozens of components.
Common Mistakes
Mistake #1: Connecting TLM ports in build_phase
Wrong: drv.seq_item_port.connect(seqr.seq_item_export); inside build_phase
Right: Move all TLM connections to connect_phase
Why? At the time a parent's build_phase runs, child components may not have been constructed yet. connect_phase is guaranteed to run only after the entire hierarchy is built.
Mistake #2: Forgetting super.build_phase(phase)
Wrong: Overriding build_phase without calling super.build_phase(phase)
Right: Always call super.build_phase(phase) as the first line
Why? The parent class's build_phase applies uvm_config_db settings to the component. Skipping it means configuration values won't be applied before you try to read them.
Mistake #3: Raising objections in a non-run-phase
Wrong: Calling phase.raise_objection(this) from build_phase or connect_phase
Right: Objections are only meaningful in run_phase (or run-time sub-phases)
Why? The objection mechanism controls when the run-time phases end. It has no effect in zero-time function phases and will generate warnings or unexpected behavior.
Practice Exercise
Challenge: Trace the Phase Order
Given a testbench with the structure test → env → agent → driver + monitor, write out the exact order in which build_phase and connect_phase are called across all five components. Remember: build_phase is top-down, all other phases are bottom-up.
Hint
Start at uvm_test_top for build_phase. For connect_phase, start at the leaves (driver, monitor) and work upward.
Solution
build_phase order (top-down):
- test
- env
- agent
- driver
- monitor
connect_phase order (bottom-up):
- driver
- monitor
- agent
- env
- test
Bottom-up ordering for connect_phase ensures that by the time a parent component tries to wire things together, all child components have already completed their own connection setup.
Summary
In this tutorial, you learned:
- UVM phases synchronize the lifecycle of all testbench components, preventing null-handle and ordering bugs
- The three phase categories are: build (zero time), run (consumes simulation time), and cleanup (zero time)
build_phaseandfinal_phaserun top-down; all other phases (connect, run, cleanup) run bottom-up through the component hierarchyrun_phaseruns in parallel with 12 finer-grained run-time sub-phases for structured stimulus control- Always call
super.build_phase(phase)first, and keep TLM connections inconnect_phase