Why Learn UVM Phases? A testbench with hundreds of components still needs every one of them built before it is connected, and connected before stimulus starts. UVM phases enforce that order for you, and many null-handle and nothing-happened bugs in a new testbench trace back to code placed in the wrong phase.

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
UVM phase flow diagram showing build, run, and cleanup phases in order

The Main UVM Phases

CategoryPhase NameTypePrimary Purpose
Buildbuild_phasefunctionConstruct and configure testbench components
Buildconnect_phasefunctionConnect TLM ports and exports between components
Buildend_of_elaboration_phasefunctionFinal adjustments after all connections are made
Buildstart_of_simulation_phasefunctionSet initial runtime configuration, display topology
Runrun_phasetaskDrive stimulus; runs in parallel with run-time sub-phases
Cleanupextract_phasefunctionCollect final data from scoreboard and DUT
Cleanupcheck_phasefunctionCompare expected vs. actual results, flag errors
Cleanupreport_phasefunctionPrint test results and summary
Cleanupfinal_phasefunctionClose 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:

UVM run-time sub-phases: pre_reset through post_shutdown
Sub-PhaseTypical Use
pre_reset_phaseWait for power-good signals, initial checks
reset_phaseAssert and release DUT reset
post_reset_phaseWait for DUT to come out of reset cleanly
pre_configure_phasePrepare for configuration writes
configure_phaseProgram registers and configure DUT for test
post_configure_phaseVerify configuration was applied correctly
pre_main_phaseFinal setup before main stimulus
main_phasePrimary test stimulus and functional traffic
post_main_phaseFlush pipelines, drain pending transactions
pre_shutdown_phaseBegin shutdown sequence
shutdown_phaseWait for DUT to finish all in-flight operations
post_shutdown_phaseFinal 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.

PhaseUpon EntryTypical UsesExit Criteria
build_phaseTop-level components instantiated under uvm_rootInstantiate sub-components; get/set config values; build register modelAll uvm_component objects have been created
connect_phaseAll components have been instantiatedConnect TLM ports/exports and sockets; connect register model adapter; set up phase domainsAll inter-component connections established
end_of_elaboration_phaseEnvironment fully assembled and connectedPrint topology; open log files; apply final configuration overridesNone
start_of_simulation_phaseSimulator ready; environment fully configuredDisplay topology; set debugger breakpoints; set initial runtime configNone
run_phaseSimulation time begins (power applied to DUT)Drive sequences; monitor DUT responses; raise/drop objections to control end-of-testAll objections dropped; DUT no longer needs simulation
extract_phaseSimulation time has stoppedCollect final scoreboard data; probe DUT via backdoor; compute statisticsAll data collected and summarized
check_phaseAll data collectedCompare expected vs. actual; flag any unmatched transactionsTest known to pass or fail
report_phaseTest result determinedPrint pass/fail summary; write results to fileEnd of test
final_phaseAll test activity completeClose files; terminate co-simulation enginesSimulator 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):

  1. test
  2. env
  3. agent
  4. driver
  5. monitor

connect_phase order (bottom-up):

  1. driver
  2. monitor
  3. agent
  4. env
  5. 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_phase and final_phase run top-down; all other phases (connect, run, cleanup) run bottom-up through the component hierarchy
  • run_phase runs 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 in connect_phase
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5