The complete UVM phase system — all 12 standard phases, execution order and direction, function phases vs task phases, the run-time sub-phases, phase objections, drain time, phase_ready_to_end, and common phase mistakes that break simulations.
The UVM phase system controls the execution order of every component in the testbench. Without phases, there would be no guarantee that a component’s configuration is complete before it tries to use it, or that the testbench is fully connected before the stimulus starts. Phases enforce a consistent lifecycle across all components, regardless of how complex the hierarchy is.
Every uvm_component participates in phases by overriding the corresponding phase methods. You only override the phases relevant to your component — unused phases are simply not overridden. The UVM infrastructure calls each phase method at the right time and in the right order automatically.
The 12 standard UVM phases are organised into three groups that execute in strict sequence:
The four build phases execute in zero simulation time. They run before the simulator advances time, establishing the complete testbench structure and connectivity.
The first and most important phase. Runs top-down so that each parent can configure its children before creating them. This is where every component creates its sub-components using the factory, retrieves configuration from config_db, and applies factory overrides.
Critical rule: Always call super.build_phase(phase) at the start of your override. The base class build_phase applies any config_db values that were set using field automation (`uvm_field_* macros). Forgetting super.build_phase(phase) is one of the most common UVM bugs.
function void build_phase(uvm_phase phase); super.build_phase(phase); // ALWAYS call first m_agent = my_agent::type_id::create("agent", this); endfunction
Runs after all build_phases complete. Connects TLM ports (analysis ports, seq_item ports) between components. Retrieves virtual interface handles from config_db. Bottom-up order ensures child ports are ready before parents connect to them.
The testbench hierarchy and connectivity are now final. Use this phase to print the testbench topology (uvm_top.print_topology()), verify that expected components exist, or perform final configuration checks. No new construction or connections should be made here.
Last function phase before time starts. Use it for displaying simulation banners, printing the random seed, or logging initial configuration state. Simulation time is still zero here.
Run-time phases are tasks — they consume real simulation time. The run_phase and all sub-phases execute in parallel with each other. This means a component’s run_phase and its reset_phase run at the same time in different threads.
The original and most widely used run-time phase. Drivers, monitors, and scoreboards nearly always use run_phase. It runs concurrently in all components that implement it and ends when all objections to it are dropped.
For most testbenches, using only run_phase is the right choice. The sub-phases (reset, configure, main, shutdown) exist for structured multi-phase stimulus organisation but add complexity for limited benefit.
The sub-phases provide finer-grained control of when stimulus occurs. They run in sequence with each other (reset → post_reset → pre_configure → configure → post_configure → pre_main → main → post_main → shutdown) but in parallel with run_phase.
The four cleanup phases run after all run-time phases have ended. They execute in zero simulation time, bottom-up.
| Phase | Direction | Typical use |
|---|---|---|
extract_phase | Bottom-up | Read final DUT register values via backdoor, collect statistics from monitors, extract coverage data |
check_phase | Bottom-up | Check for unmatched transactions in scoreboards, verify expected events occurred, assert final coverage requirements |
report_phase | Bottom-up | Print pass/fail summary, coverage report, error counts, performance statistics |
final_phase | Top-down | Last cleanup before simulator exits — close files, flush output, print final banner |
This distinction is critically important and a frequent source of bugs for UVM beginners:
| Property | Function phases | Task phases |
|---|---|---|
| Which phases | build, connect, end_of_elab, start_of_sim, extract, check, report, final | run_phase and all sub-phases (reset, configure, main, shutdown, and their pre/post variants) |
| Simulation time | Zero — execute instantaneously | Can consume any amount of simulation time |
| Allowed constructs | Functions only — no #delay, no @(posedge clk), no wait | Full task semantics — #delay, @event, wait, blocking calls all allowed |
| Concurrency | Sequential — one component at a time per direction | Concurrent — all components run simultaneously |
| Phase argument | function void build_phase(uvm_phase phase) | task run_phase(uvm_phase phase) |
#10ns or @(posedge clk) in build_phase, connect_phase, or check_phase will cause a simulator error or hang. These phases are functions — they cannot consume time. If you need time to pass before checking something, use run_phase or extract_phase with a separate check mechanism.
A phase objection is a counter maintained by the UVM infrastructure. While the count is non-zero, the phase continues. When the count reaches zero (after having been non-zero), the phase ends. Any component or sequence can raise and drop objections at any time during the phase.
task run_phase(uvm_phase phase); // Raise before doing any work — keeps phase alive phase.raise_objection(this); phase.raise_objection(this, "Optional message for debug"); // Do your simulation work here seq.start(m_seqr); // Drop when done — allow phase to end if no others pending phase.drop_objection(this); endtask // Set optional drain time — wait N ns after count=0 before ending function void start_of_simulation_phase(uvm_phase phase); uvm_objection obj = phase.get_objection(); obj.set_drain_time(this, 100ns); endfunction
raise_objection must be matched by exactly one drop_objection. If you raise more than you drop, the simulation never ends (hangs). If you drop without raising (or drop too many times), UVM reports a fatal error. Always structure your run_phase with raise at the top and drop at the end, or use a try/finally equivalent.
When the last objection is dropped (count reaches zero), the phase does not end immediately. The UVM infrastructure waits for a configurable drain time before actually ending the phase. If any component raises a new objection during the drain time, the timer resets.
Drain time is essential for pipelines and scoreboards. When a test drops its objection after the last sequence completes, there may still be transactions in flight — packets in a pipeline, DMA transfers in progress, responses being written to memory. Drain time gives these in-flight operations time to complete and reach the scoreboard before the simulation ends.
// Set drain time in start_of_simulation_phase function void start_of_simulation_phase(uvm_phase phase); uvm_phase run_ph = uvm_domain::get_uvm_schedule().find_by_name("run"); run_ph.get_objection().set_drain_time(this, 1us); endfunction // Or simpler — set on the current phase inside run_phase task run_phase(uvm_phase phase); phase.raise_objection(this); phase.get_objection().set_drain_time(this, 200ns); seq.start(m_seqr); phase.drop_objection(this); endtask
phase_ready_to_end is a special callback that is called on every component when the objection count first reaches zero. A component that is not yet ready for the phase to end — for example, a scoreboard with unmatched transactions still being processed — can raise a new objection from inside this callback, extending the phase.
class my_scoreboard extends uvm_scoreboard; `uvm_component_utils(my_scoreboard) int pending_count = 0; // tracks in-flight transactions // Called when objection count first hits zero function void phase_ready_to_end(uvm_phase phase); if (pending_count > 0) begin // Not ready — raise a new objection to extend the phase phase.raise_objection(this, "Scoreboard still has pending txns"); fork begin wait (pending_count == 0); // wait until all matched phase.drop_objection(this); end join_none end endfunction endclass
fork/join_none pattern shown above is the standard way to do this.
| Mistake | Symptom | Fix |
|---|---|---|
Forgetting super.build_phase(phase) | config_db get() always returns 0. Fields set via `uvm_field_* macros are not applied. | Always call super.build_phase(phase) as the first line of your build_phase override. |
| Raising objection but never dropping it | Simulation hangs forever. UVM prints “objection not dropped” after timeout. | Ensure every raise_objection has a matching drop_objection in all code paths, including error exits. |
| Not raising any objection | Simulation ends immediately — run_phase exits before any stimulus runs. | Raise an objection at the start of run_phase in the test (or whichever component drives the test scenario). |
Using #delay in build_phase | Simulator error: “cannot call time-consuming code in function”. | Only use time-consuming constructs in task phases (run_phase). Build, connect, and cleanup phases are functions. |
| Creating components in connect_phase | Components created in connect_phase may not participate in phases correctly. Path name may be wrong. | Create all components in build_phase only. connect_phase is for connections, not construction. |
| Connecting ports in build_phase | Port connections fail — the target component may not exist yet (build is top-down). | All TLM port connections go in connect_phase, never build_phase. |
| Forgetting drain time for pipelines | Scoreboard misses transactions. Test ends while packets are still in the DUT pipeline. | Set drain time via set_drain_time() or use phase_ready_to_end in the scoreboard. |
| Item | Key fact |
|---|---|
| Phase groups | Build (4 phases) → Run-time (1 + 9 sub-phases) → Cleanup (4 phases) |
| build_phase direction | Top-down — parent builds before children |
| connect_phase direction | Bottom-up — children connect before parents |
| cleanup phases direction | Bottom-up (extract, check, report) except final_phase (top-down) |
| Function phases | All phases except run_phase and sub-phases — zero simulation time, no blocking |
| Task phases | run_phase + sub-phases — can consume time, blocking constructs allowed |
| run_phase concurrency | Runs simultaneously in ALL components that implement it |
| super.build_phase(phase) | Must be first line — applies `uvm_field macros and config_db auto-get |
| Objection count = 0 after raise | Phase ends (after drain time) |
| Objection raise/drop rule | Every raise must have exactly one matching drop |
| No objection raised | run_phase exits immediately — simulation ends with no stimulus |
| Drain time purpose | Wait N simulation time after count=0 for in-flight transactions to complete |
| set_drain_time() | phase.get_objection().set_drain_time(this, Nns) |
| phase_ready_to_end use | Raise objection from it to extend phase; must fork drop_objection |
| Recommended sub-phases | reset, configure, main, shutdown — pre/post variants rarely needed |
| Best practice | Use run_phase only unless you specifically need ordered multi-phase stimulus |