Skip to main content

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​

PhaseKindDirection
build_phasefunctiontop-down
connect_phasefunctionbottom-up
end_of_elaboration_phasefunctionbottom-up
start_of_simulation_phasefunctionbottom-up
run_phasetaskbottom-up, time-consuming
extract_phasefunctionbottom-up
check_phasefunctionbottom-up
report_phasefunctionbottom-up
final_phasefunctiontop-down

A few things worth noticing in this table beyond the two phases already familiar from every component in the previous section:

  • build_phase runs top-down (parent before children) — by design, since a parent has to exist before it can construct its children with create(). This is exactly why fifo_env::build_phase could safely call fifo_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 why fifo_env::connect_phase could safely wire agent.monitor.ap.connect(scoreboard.ap_imp): by the time a parent's connect_phase runs, every child has already had its own chance to run first.
  • run_phase is the only task in the list — every other phase is a function, meaning it executes instantaneously, consuming zero simulation time. run_phase is where actual clock-cycle-consuming activity happens, which is exactly why the driver's and monitor's forever loops from the previous section live there and nowhere else.
  • end_of_elaboration_phase is 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 in start_of_simulation_phase).
  • extract_phase/check_phase/report_phase run after run_phase completes, in that order — for pulling final data out of components, running any end-of-test checks that don't fit naturally inside run_phase itself, 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 twelve run-time sub-phases in order: pre_reset, reset, post_reset, pre_configure, configure, post_configure, pre_main, main, post_main, pre_shutdown, shutdown, post_shutdown

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_objection both take optional description and count arguments beyond just this — the description shows up in objection-tracing output (below), and count lets one call represent raising or dropping more than one objection at once, though 1 (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_phase only 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). Since raise_objection/drop_objection exist to control when a phase ends, and every non-run_phase phase in the table above is a zero-time function that's already over before the next line of testbench code executes, calling raise_objection from inside build_phase or connect_phase has 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.

The most common new-testbench bug

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 mirror-image bug: forgetting to drop

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.