Phases & Objections
The FIFO testbench built up over the previous section — driver, monitor, sequencer, scoreboard, agent, environment, test — relied on build_phase, connect_phase, and run_phase throughout, without ever stepping back to explain the full mechanism behind them. This page fills that gap: the complete phase list, what order everything actually runs in, and how a simulation built entirely out of concurrent forever loops and task run_phase methods ever knows when to stop.
Why phases exist at all
A hand-written testbench typically builds everything in a series of initial blocks, and getting the order right — instantiate the driver before connecting it, connect the monitor's port before the scoreboard tries to use it — is easy to get subtly wrong, especially as a class-based tree grows deeper than a few levels. UVM replaces manual ordering with phases: named callback methods that every uvm_component in the tree runs through automatically, in a fixed, framework-guaranteed order. Nothing about fifo_agent, fifo_env, or fifo_base_test from the previous section ever had to coordinate ordering by hand — every build_phase/connect_phase override just had to trust the framework to call it at the right moment.
The full phase list
| Phase | Kind | Direction |
|---|---|---|
build_phase | function | top-down |
connect_phase | function | bottom-up |
end_of_elaboration_phase | function | bottom-up |
start_of_simulation_phase | function | bottom-up |
run_phase | task | bottom-up, time-consuming |
extract_phase | function | bottom-up |
check_phase | function | bottom-up |
report_phase | function | bottom-up |
final_phase | function | top-down |
A few things worth noticing in this table beyond the two phases already familiar from every component in the previous section:
build_phaseruns top-down (parent before children) — by design, since a parent has to exist before it can construct its children withcreate(). This is exactly whyfifo_env::build_phasecould safely callfifo_agent::type_id::create(...)— the environment's own construction is guaranteed complete by the time it starts building its children.- Every other phase runs bottom-up (children before parents) — except
final_phase, which flips back to top-down. Bottom-up is whyfifo_env::connect_phasecould safely wireagent.monitor.ap.connect(scoreboard.ap_imp): by the time a parent'sconnect_phaseruns, every child has already had its own chance to run first. run_phaseis the onlytaskin the list — every other phase is afunction, meaning it executes instantaneously, consuming zero simulation time.run_phaseis where actual clock-cycle-consuming activity happens, which is exactly why the driver's and monitor'sforeverloops from the previous section live there and nowhere else.end_of_elaboration_phaseis the last function-phase checkpoint before simulation time starts moving — a natural place to sanity-check the built tree (uvm_top.print_topology(), first introduced in Getting Started, is typically called here or instart_of_simulation_phase).extract_phase/check_phase/report_phaserun afterrun_phasecompletes, in that order — for pulling final data out of components, running any end-of-test checks that don't fit naturally insiderun_phaseitself, and summarizing results.
Every phase method, at every level, should call super.<phase_name>(phase) first — the same discipline followed throughout the previous section. Skipping it silently drops whatever the base class was contributing to that phase, which is a difficult bug to trace back to a missing one-line call.
The twelve run-time sub-phases
run_phase itself is actually made up of twelve finer-grained sub-phases, running in this exact order, that exist specifically to give components a shared vocabulary for "where we are" during the part of the test that actually consumes time:
The FIFO testbench never needed these — fifo_base_test::run_phase just raised an objection, started a sequence, and dropped it, all within the single unified run_phase. The sub-phases exist for testbenches where different components genuinely need to synchronize around named stages of a test — for instance, everything agreeing that DUT reset has finished before anything starts driving configuration, and that configuration has finished before anything starts driving real traffic. Overriding sub-phases like main_phase instead of run_phase directly is how a component opts into that shared vocabulary; a simple environment like this section's FIFO testbench is free to ignore them entirely and just use run_phase.
Objections, in full
Getting Started and every test in the previous section used the same two-line pattern — raise_objection/drop_objection bracketing a sequence — without exploring what else the mechanism offers.
task run_phase(uvm_phase phase);
fifo_write_read_seq seq = fifo_write_read_seq::type_id::create("seq");
phase.raise_objection(this, "Starting FIFO stimulus");
seq.start(env.agent.sequencer);
phase.drop_objection(this, "FIFO stimulus complete");
endtask
raise_objection/drop_objectionboth take optionaldescriptionandcountarguments beyond justthis— the description shows up in objection-tracing output (below), andcountlets one call represent raising or dropping more than one objection at once, though1(the default) is by far the common case.- Objections are a distributed counter, not a single flag. Any number of components can each raise their own objection independently — a scoreboard waiting on a final response, a coverage collector waiting to sample one last transaction — and
run_phaseonly ends once every outstanding objection, from every component that raised one, has been dropped. set_drain_time(this, time)adds a delay after the objection count reaches zero before the phase actually ends — useful when a transaction might still be propagating through the DUT's pipeline for a few cycles after the last objection drops, and ending immediately would cut it off mid-flight.- Objections only mean something in
run_phase(and other task-based run-time phases). Sinceraise_objection/drop_objectionexist to control when a phase ends, and every non-run_phasephase in the table above is a zero-timefunctionthat's already over before the next line of testbench code executes, callingraise_objectionfrom insidebuild_phaseorconnect_phasehas no meaningful effect — UVM issues a warning, and the objection is effectively ignored, because a zero-time function phase has already ended by the time anything could act on it. - Convention, not enforcement: keep objections at high-level components. UVM doesn't stop a driver or monitor from raising its own objections, but doing so tends to scatter responsibility for "when does this test end" across components that shouldn't need to know. The pattern used throughout this section — the test (or, in a larger environment, a top-level virtual sequence) owns the objection — keeps that responsibility in one predictable place.
Automatic objections for sequences
Sequences started with a plain .start() call, as every sequence in the previous section was, need their test to own the objection, exactly as shown above. UVM also offers an automatic mode, set in a sequence's own constructor, for cases where a sequence should manage its own objection lifetime instead:
function new(string name = "fifo_write_read_seq");
super.new(name);
set_automatic_phase_objection(1);
endfunction
With this set, the sequence raises its own objection in pre_start() and drops it in post_start() automatically — useful for a sequence that might be started from several different places and shouldn't rely on each caller remembering to wrap it in raise_objection/drop_objection correctly.
Debugging objections and phases
Two command-line switches, needing no code changes, are worth knowing before ever hand-rolling debug `uvm_info calls for this: +UVM_OBJECTION_TRACE logs every raise/drop as it happens, and +UVM_PHASE_TRACE logs phase transitions across the whole tree. A test that seems to hang, or one that ends earlier than expected, is usually fastest to diagnose by rerunning with one of these plusargs rather than adding print statements. display_objections(), callable on any uvm_phase handle, dumps the current outstanding objections on demand.
Forgetting to raise an objection at all means run_phase sees no reason to stay open, and the simulation can exit almost immediately — often before the driver has driven a single transaction. If a test "does nothing" the moment it runs, check the objection first, before suspecting the driver or sequence.
The opposite mistake has the opposite symptom. Raise an objection and never drop it, and run_phase never sees its count return to zero — the simulation hangs indefinitely instead of ending, never reaching extract_phase/check_phase/report_phase at all. +UVM_OBJECTION_TRACE's raise/drop log is exactly the tool for this case too: scan it for a raise with no matching drop from the same source object.
What's next
Every phase in this testbench runs in a framework-guaranteed order — but that order is fixed by UVM itself. The next page covers what's involved in inserting an entirely new, custom phase into that schedule, for the rarer cases where the built-in phases genuinely aren't enough.