A design review for a DDR controller bench. Link training must happen after the registers are configured and before any traffic, and it takes a variable time. Someone proposes a new phase, training_phase, between configure and main. It sounds clean: every component that cares implements it, and the schedule guarantees the order. Someone else asks the question that decides it: what does every other VIP in the SoC do with a phase it has never heard of?
This lesson shows how a custom phase is built and inserted, because interviewers ask and because you will meet them in vendor VIP. It also shows why most teams decide against writing one.
A UVM schedule is like a bus route: a fixed list of stops, in order, that every passenger (component) rides. A custom phase adds a stop. Passengers who know about it get off and do something there; everyone else sits through it. The route map is shared, so adding a stop affects every passenger on that route, not only the ones who asked for it.
Mechanically, every phase is a singleton implementation object (the imp) that knows how to call one method on a component, plus a node in a schedule graph that says where it runs. uvm_main_phase::get() returns the imp; a schedule holds nodes that point at imps. A custom phase is a new imp class plus a call that inserts it into a schedule.
A task phase extends uvm_task_phase (function phases extend uvm_topdown_phase or uvm_bottomup_phase). Follow the library's own pattern: a protected constructor, a static get() that returns the single instance, and exec_task, which UVM calls once for every component in the domain. uvm_component has no training_phase method, so exec_task casts to a base class that declares one and does nothing for anyone else.
// snippet
// Components that take part in link training extend this.
virtual class training_component extends uvm_component;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
pure virtual task training_phase(uvm_phase phase);
endclass
class training_phase_imp extends uvm_task_phase;
local static training_phase_imp m_inst;
protected function new(string name = "training");
super.new(name);
endfunction
static function training_phase_imp get();
if (m_inst == null) m_inst = new();
return m_inst;
endfunction
virtual function string get_type_name();
return "training_phase_imp";
endfunction
// Called for every component in the schedule's domain.
virtual task exec_task(uvm_component comp, uvm_phase phase);
training_component t;
if ($cast(t, comp)) t.training_phase(phase);
endtask
endclassA base class works only for components you write. For a component that must extend something else, such as uvm_driver, an SV interface class with the training_phase task does the same job on simulators that support casting to interface classes.
The phase is a task phase, so it ends when its objections drop, exactly like main_phase. A component that does work there must raise first.
// snippet
class phy_trainer extends training_component;
`uvm_component_utils(phy_trainer)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual task training_phase(uvm_phase phase);
phase.raise_objection(this, "PHY: link training");
`uvm_info("TRAIN", "training the link", UVM_LOW)
#200ns; // stands in for the training sequence
`uvm_info("TRAIN", "link up", UVM_LOW)
phase.drop_objection(this, "PHY: link up");
endtask
endclassInsert the node once, before the runtime phases start, naming its neighbours. Adding it to the uvm domain's schedule puts it in front of every component in that domain:
// snippet
function void build_phase(uvm_phase phase); // in the test
uvm_phase sched;
super.build_phase(phase);
sched = uvm_domain::get_uvm_schedule();
sched.add(training_phase_imp::get(),
.after_phase(uvm_post_configure_phase::get()),
.before_phase(uvm_pre_main_phase::get()));
endfunctionTo limit it to one subtree, put that subtree in its own domain and insert into that domain's schedule instead. set_domain creates the domain's runtime schedule, named uvm_sched, if it does not exist yet. It can be called in build_phase (children built later inherit the domain) or later; here the test does it in connect_phase, in the same place it edits the schedule, which must happen before the runtime phases start:
// snippet
function void connect_phase(uvm_phase phase); // in the test
uvm_phase sched;
phy_dom = new("phy_dom");
env.phy.set_domain(phy_dom);
sched = phy_dom.find_by_name("uvm_sched");
sched.add(training_phase_imp::get(),
.after_phase(uvm_post_configure_phase::get()),
.before_phase(uvm_pre_main_phase::get()));
endfunctionAdded to the DMA bench with a phy_trainer in the env that also logs its configure_phase and main_phase entries, either version gives the same order (Verilator, times in ps):
UVM_INFO cp_tb.sv(51) @ 0: uvm_test_top.env.phy [PH] configure
UVM_INFO cp_tb.sv(45) @ 0: uvm_test_top.env.phy [TRAIN] training the link
UVM_INFO cp_tb.sv(47) @ 200000: uvm_test_top.env.phy [TRAIN] link up
UVM_INFO cp_tb.sv(55) @ 200000: uvm_test_top.env.phy [PH] main startsmain_phase starts at 200 ns, exactly when the trainer drops its objection. In the first version, exec_task was called for every component in the uvm domain, ports included (they are components too), and did nothing for all but one. That is the bus route: everyone rides through the new stop.
The phase worked. Most teams would still reject it, for reasons that only show up at scale:
Nobody else knows the phase. Vendor VIP, other teams' agents and your own existing components implement only the standard phases. Most of them do their real work in run_phase anyway, so a runtime-phase barrier does not even pause them.
Reuse carries the schedule change. Every bench that reuses the env must perform the same insertion, exactly once, or the trainer's task never runs. The library does not stop a second insertion from another env either; it simply adds a second training node to the schedule.
Jumps and domains get harder. A jump back to reset re-runs the custom phase; a sync between domains must now consider it; every reader must learn it.
The ordering is easy to get without it. A test or virtual sequence that runs the training sequence after configuration and before traffic gives the same order, visibly, in one place.
The industry default is the shape from Reset phase and phase jump: components live in run_phase and react to reset and to events, and the test sequences the scenario (configure, train, traffic) explicitly. Custom phases appear mostly in large in-house methodologies that control every component in the bench, and in some VIP that ships with its own domain.
A team added a calibrate_phase for an analog front-end model and implemented it in their own agents. At SoC integration, the third-party PCIe VIP in the same domain started its link-up traffic in run_phase at time 0, straight through the calibration period, because it knew nothing about the new phase. The calibration results were corrupted intermittently, depending on how fast PCIe trained.
The fix was not a better phase. They replaced calibrate_phase with a calibration sequence started by the SoC virtual sequence, gated the PCIe link-up on an event that the calibration triggered, and deleted the custom phase. The order became visible in one file, and the next integration did not need to know about any schedule changes.
training_phase into the uvm schedule, but phy_trainer's task never runs, and no error is reported. What are the likely causes?Either exec_task's cast fails for the trainer (it does not extend the type exec_task casts to, or a method name differs), or the trainer is in a different domain whose schedule has no training node. A third possibility is that the insertion ran after the runtime phases had already started; insert it in build or connect.
training_phase and calls a 200 ns task. What happens?The phase ends at once, because nothing objects to it, and the trainer's process is killed when the phase ends. main_phase starts without waiting for training. A custom task phase follows the same objection rules as every runtime phase.
Most VIP drives from run_phase, which is in the common domain and runs in parallel with all runtime phases, standard or custom. A runtime phase only orders code that lives in runtime phases. Gate the VIP's traffic on an event or a sequence instead.
Next: this completes the phases part. Factory and configuration picks up the question every lesson here took for granted: how a test swaps components and settings without editing them.