UVM phasing is the backbone of every testbench. It controls when each component builds, connects, runs, and cleans up. Master objections, top-down vs bottom-up ordering, task vs function phases, and you will stop fighting mysterious test terminations forever.
The UVM phase mechanism imposes a structured lifecycle on every component in a testbench hierarchy. Instead of an ad-hoc soup of initial blocks and fork-join, all testbench components go through the same well-defined sequence of phases. This guarantees that a driver is never asked to drive before the interface is connected, a scoreboard never checks before the DUT has been reset, and a report is never printed before all checking is complete.
UVM defines 12 standard phases executed in this order:
| # | Phase Name | Type | Order | Purpose |
|---|---|---|---|---|
| 1 | build_phase | function | top-down | Construct child components, get config |
| 2 | connect_phase | function | bottom-up | Connect TLM ports between components |
| 3 | end_of_elaboration_phase | function | bottom-up | Final hierarchy adjustments, display topology |
| 4 | start_of_simulation_phase | function | bottom-up | Print banner, set simulator state |
| 5 | run_phase | task | parallel | Apply stimulus and check — consumes time |
| 6 | extract_phase | function | bottom-up | Extract results from DUT for analysis |
| 7 | check_phase | function | bottom-up | Compare results, report errors |
| 8 | report_phase | function | bottom-up | Print coverage, stats, pass/fail |
| 9 | final_phase | function | top-down | Clean up files, close databases |
The four phases before run_phase are all function phases — they execute in zero simulation time. The key behavioural difference is execution order:
The environment's build_phase runs first. It calls super.build_phase(phase) and then creates its child components using type::type_id::create("name", this). Only after the parent creates the child does the child's build_phase run. This top-down ordering is intentional: the parent must exist before it can create children, and parents often push config into uvm_config_db for children to retrieve during their own build_phase.
Once all components exist, connect_phase wires them together via TLM ports: monitor.ap.connect(scoreboard.analysis_export). It runs bottom-up — leaf components connect first, then their parents. This matters because the parent may aggregate connections from multiple children.
These are rarely overridden in practice. end_of_elaboration_phase is useful for printing the complete component hierarchy with uvm_top.print_topology(). start_of_simulation_phase is a last chance to print banners or configure the simulator before time zero.
Every other phase is a zero-time function. run_phase is a task — it is the only place where simulation time can advance. All stimulus generation, DUT driving, monitoring, and checking that requires timing belongs here.
UVM forks run_phase across all components simultaneously. Every component that overrides run_phase gets its own independent thread. The env's run_phase, the agent's run_phase, the driver's run_phase, the monitor's run_phase — they all run in parallel. This parallelism is a feature: the driver can apply stimulus while the monitor watches outputs and the scoreboard checks results, all concurrently.
The UVM phase controller watches an internal objection counter. A phase ends only when all objections have been dropped. If the counter reaches zero (no objections), the phase ends and UVM moves to the next phase.
In run_phase, call phase.raise_objection(this) at the very start — before any delays — and phase.drop_objection(this) after all stimulus is done. The test is typically the only component that manages the top-level objection:
class my_test extends uvm_test; `uvm_component_utils(my_test) my_env env; function void build_phase(uvm_phase phase); super.build_phase(phase); env = my_env::type_id::create("env", this); endfunction task run_phase(uvm_phase phase); my_sequence seq; // MUST raise before any delays phase.raise_objection(this, "test started"); seq = my_sequence::type_id::create("seq"); seq.start(env.agent.sequencer); // Optional: let the design settle #100; // MUST drop after all work is done phase.drop_objection(this, "test done"); endtask endclass
Any component can raise and drop objections. The phase controller adds all outstanding objection counts. The phase ends when the total drops to zero. If two sequences are running in parallel, each should raise its own objection and drop it when complete — the phase will not end until both have dropped.
After all objections in run_phase are dropped, UVM proceeds through the post-run function phases — all bottom-up:
Pull final state from the DUT — read register values, drain queues. This phase exists to separate data collection from checking, allowing clean code organisation. Most teams skip this and do extraction inline in run_phase.
Compare expected vs actual results. Scoreboards do their final checks here. Any unreceived expected transactions should trigger errors. This is also where coverage thresholds are checked.
Print summaries, coverage numbers, pass/fail totals. Call uvm_report_info and uvm_report_error here to produce the final test log. UVM's built-in report server prints the global summary automatically.
Top-down (like build_phase). Used for closing file handles, flushing databases, and cleanup that must happen in hierarchy order. Most teams do not override this phase.
Sometimes one component must wait for another before proceeding within run_phase. UVM provides phase synchronisation APIs:
// Wait until ALL objections on run_phase have been dropped phase.get_objection().wait_for(UVM_ALL_DROPPED, null, 0); // Wait for a specific phase to start (e.g., from a monitor) uvm_phase run_ph = uvm_root::get().find( "uvm_test_top").get_current_phase(); // Use a semaphore or event for lightweight sync between two components event mem_init_done; // Memory model run_phase task run_phase(uvm_phase phase); initialise_memory(); ->mem_init_done; // signal ready forever @(posedge clk) check_access(); endtask // Traffic sequence — wait for memory ready task body(); @(mem_init_done); // now safe to send transactions repeat (100) `uvm_do(item) endtask
Teams working on complex multi-IP SoCs sometimes define domain-specific phases — for example, a power_on_phase before the standard reset sequence. UVM supports this via uvm_topdown_phase or uvm_bottomup_phase:
// Define a custom function phase class power_on_phase extends uvm_topdown_phase; function new(string name = "power_on_phase"); super.new(name); endfunction function void exec_func(uvm_component comp, uvm_phase phase); comp.m_current_phase = phase; endfunction static function power_on_phase get(); if (m_inst == null) m_inst = new(); return m_inst; endfunction static local power_on_phase m_inst; endclass // Register with UVM schedule (call from test's build_phase) uvm_domain domain = uvm_domain::get_common_domain(); uvm_phase build_ph = domain.find_by_name("build"); domain.add(power_on_phase::get(), .after_phase(build_ph));
| Pitfall | Symptom | Fix |
|---|---|---|
| Missing raise_objection | Simulation ends at time 0 | Call phase.raise_objection(this) at start of run_phase |
| Missing drop_objection | Simulation never ends / timeout | Always drop after all stimulus is done |
| Creating children in connect_phase | Child has no build_phase; unconfigured | Only create components in build_phase |
| Connecting ports in build_phase | Target component may not exist yet | All TLM connections go in connect_phase |
| Using uvm_config_db in run_phase | Config not found; component uses defaults | Always set config before build_phase; get in build_phase |
| Race on drop_objection in parallel forks | Premature phase end | Use a single test-level objection; let sequences signal completion via event |
The diagram below shows how phases map to simulation time, and how components execute in parallel during run_phase:
This example shows an environment with a driver and monitor, each with their own phase methods, and a test that properly manages the objection:
// ─── my_driver.sv ──────────────────────────────────────────── class my_driver extends uvm_driver#(my_seq_item); `uvm_component_utils(my_driver) virtual my_if vif; function void build_phase(uvm_phase phase); super.build_phase(phase); if (!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif)) `uvm_fatal("CFG", "No virtual interface") endfunction task run_phase(uvm_phase phase); forever begin seq_item_port.get_next_item(req); drive_item(req); // drive signals, consumes time seq_item_port.item_done(); end endtask task drive_item(my_seq_item item); @(posedge vif.clk); vif.data <= item.data; vif.valid <= 1; @(posedge vif.clk); vif.valid <= 0; endtask endclass // ─── my_scoreboard.sv ──────────────────────────────────────── class my_scoreboard extends uvm_scoreboard; `uvm_component_utils(my_scoreboard) uvm_analysis_imp#(my_seq_item, my_scoreboard) analysis_export; my_seq_item expected_q[$]; function void build_phase(uvm_phase phase); super.build_phase(phase); analysis_export = new("analysis_export", this); endfunction function void write(my_seq_item item); my_seq_item exp; if (expected_q.size() == 0) begin `uvm_error("SB", "Unexpected transaction received") return; end exp = expected_q.pop_front(); if (item.data !== exp.data) `uvm_error("SB", $sformatf("MISMATCH exp=%0h got=%0h", exp.data, item.data)) else `uvm_info("SB", "PASS", UVM_HIGH) endfunction function void check_phase(uvm_phase phase); if (expected_q.size() > 0) `uvm_error("SB", $sformatf("%0d expected items never received", expected_q.size())) endfunction function void report_phase(uvm_phase phase); `uvm_info("SB", "=== Scoreboard Report ===", UVM_NONE) endfunction endclass // ─── my_test.sv ────────────────────────────────────────────── class my_test extends uvm_test; `uvm_component_utils(my_test) my_env env; function void build_phase(uvm_phase phase); super.build_phase(phase); env = my_env::type_id::create("env", this); endfunction task run_phase(uvm_phase phase); my_sequence seq = my_sequence::type_id::create("seq"); phase.raise_objection(this, "starting test"); seq.start(env.agent.sequencer); #200; // drain pipe phase.drop_objection(this, "test complete"); endtask function void report_phase(uvm_phase phase); super.report_phase(phase); uvm_report_server svr = uvm_report_server::get_server(); if (svr.get_severity_count(UVM_ERROR) == 0) `uvm_info("TEST", "*** TEST PASSED ***", UVM_NONE) else `uvm_error("TEST", "*** TEST FAILED ***") endfunction endclass
#delay, @event, and other timing constructs. The simulator only advances time during task phases.phase.raise_objection(this) at the start of run_phase. The UVM phase controller drains all objections before ending a phase. If no component raises an objection, the objection count is already zero and the phase ends immediately — often before any stimulus is driven.wait_for_state) is rarely needed in practice.