User-Defined Phases
The nine phases from the previous page, plus the twelve run-time sub-phases underneath run_phase, cover the overwhelming majority of what any UVM testbench needs. Occasionally — most often in large, multi-team environments — none of the built-in phases quite fit a step that genuinely needs its own name and its own guaranteed position in the schedule. UVM allows inserting an entirely new phase for that case, though it's worth treating as a last resort rather than a first instinct.
Three base classes, chosen by how the phase behaves
A custom phase extends one of three base classes, matching the same function-vs-task and top-down-vs-bottom-up distinctions the built-in phases already follow:
uvm_task_phase— for a phase that consumes simulation time, the same rolerun_phaseplays among the built-in phases. Requires overridingexec_task().uvm_topdown_phase— a zero-time phase running parent-before-children, likebuild_phase. Requires overridingexec_func().uvm_bottomup_phase— a zero-time phase running children-before-parent, likeconnect_phaseorreport_phase. Also overridesexec_func().
Choosing among them is the same question asked implicitly of every built-in phase on the previous page: does this step need real simulation time, and if not, which direction does dependency flow in?
Defining a phase: the singleton pattern
class fifo_calibration_phase extends uvm_task_phase;
virtual task exec_task(uvm_component comp, uvm_phase phase);
fifo_env env;
if ($cast(env, comp)) begin
phase.raise_objection(this);
// ... e.g. drive a fixed calibration sequence before real stimulus starts ...
phase.drop_objection(this);
end
endtask
local static fifo_calibration_phase m_inst;
static function fifo_calibration_phase get();
if (m_inst == null) m_inst = new();
return m_inst;
endfunction
protected function new(string name = "fifo_calibration");
super.new(name);
endfunction
endclass
Custom phase classes follow the singleton pattern — exactly one instance ever exists, accessed through a static get() method rather than new() — so that every component in the tree shares the identical phase object when the schedule references it. exec_task() receives the specific uvm_component currently being phased (comp), which is why it needs its own $cast to whatever component type actually cares about this phase — fifo_env, in this sketch — since the same phase runs across every component in the tree, most of which have nothing to do here.
Choosing a domain
UVM organizes phases into domains — the built-in "common" domain (build_phase through final_phase) and the "uvm" domain (the twelve run_phase sub-phases from the previous page):
uvm_domain::get_common_domain(); // build/connect/.../report/final
uvm_domain::get_uvm_domain(); // pre_reset/reset/.../post_shutdown
A new phase joins whichever domain makes it a peer of the phases it's conceptually closest to — a one-time calibration step run once at test start belongs in the common domain, alongside build_phase/connect_phase; something that needs to interleave with reset/configure/main belongs in the uvm domain instead.
Domains aren't limited to just these two, either: a large environment can create additional domains of its own and move a subset of components into one via set_domain(), letting that subset's run-time sub-phases advance on their own schedule, independent of the rest of the tree — the usual motivation being two genuinely decoupled pieces of a DUT (a multi-clock chip's independently-resettable subsystems, say) that shouldn't have to march through reset/configure/main in lockstep. Two domains can optionally be re-coupled with a sync() call if some (but not all) of their phases still need to line up.
Inserting the phase
uvm_phase common_dm = uvm_domain::get_common_domain();
uvm_phase run_ph = common_dm.find(uvm_run_phase::get());
common_dm.add(fifo_calibration_phase::get(), .after_phase(run_ph));
find() locates an existing built-in phase by reference (uvm_run_phase::get() — the same singleton-accessor pattern used for the custom phase itself), and add() inserts the new phase relative to it — .after_phase(run_ph) here, with .before_phase(...) the equivalent for the other direction. This insertion has to run before run_test() starts phasing — typically from a static initializer or a dedicated setup routine invoked from testbench-top, alongside the other one-time setup covered in The Test & Testbench-Top.
A related hazard: phase jumping
Once a custom phase is wired into a schedule, it becomes a valid target for uvm_phase::jump() — forcing execution to skip forward or backward to a named phase, e.g. phase.jump(fifo_calibration_phase::get()). A few restrictions matter enough to know before ever reaching for it:
- A jump affects every component sharing that schedule simultaneously — it is not scoped to one component, which is exactly why jumping is easy to reason about wrong in a large environment.
- Jumping backward into any of the build-time phases (
build_phase,connect_phase, etc.) is illegal — those phases are meant to run exactly once per simulation, and re-entering them triggers a fatal error. - Because of both restrictions,
jump()is normally reserved for a small, deliberate class of uses — most commonly re-running the run-time phases for a reset scenario — not as a general-purpose way to restructure test flow around a custom phase.
When this is worth reaching for
A custom phase gives every component in the tree a new named synchronization point — powerful, but it also means anything reusing this environment elsewhere now depends on a phase schedule that isn't the UVM standard everyone else assumes, and it raises the chance that some component ends up out of sync with the rest of the tree (chasing a null handle for a phase object that was never registered in its own hierarchy). The twelve built-in run-time sub-phases from the previous page already cover the overwhelming majority of "I need components to agree on a named stage of the test" cases without this cost. Reach for a custom phase only when a genuinely new, cross-cutting synchronization point is needed that doesn't map onto any of the existing nine phases or twelve sub-phases — not as a routine way to organize test logic.
What's next
With the full phasing picture in view — the nine phases, the twelve run-time sub-phases, objections, and now custom phases — the next section turns to the mechanism that decides which class actually gets built at each of these phases: the factory.