Skip to content

Phases

Why phasing exists

A testbench has dozens of components, often built by different engineers, that all need to: get created, find each other, connect their ports, run stimulus, check results, and report a pass/fail — in a consistent order, regardless of where in the hierarchy a component sits or who wrote it. Without an enforced ordering, "did the scoreboard finish connecting before the driver started sending transactions" becomes a race condition that depends on class-declaration order or simulator scheduling quirks. UVM phasing makes the ordering a language-enforced contract instead of a convention people have to remember.

The main phases, in order

Phase Purpose Time-consuming (task) or instant (function)?
build_phase Create child components, get config via uvm_config_db function
connect_phase Wire up TLM ports/exports (analysis ports, etc.) function
end_of_elaboration_phase Last chance to inspect/tweak the fully-built hierarchy function
start_of_simulation_phase Final setup right before time starts advancing function
run_phase The actual test — drive stimulus, monitor, check task
extract_phase Pull final data out of components (e.g. scoreboard tallies) function
check_phase Compare extracted data, raise errors function
report_phase Print pass/fail summary function

Every phase runs top-down through the hierarchy for function-phases (build_phase on the test, then env, then agent, then driver...) which is why a parent can create its children in build_phase before those children's own build_phase runs. connect_phase runs bottom-up, so children finish wiring their own ports before a parent tries to connect across them.

run_phase is different: it's a task, and phases can overlap

Unlike the function-phases (instantaneous, no time passes), run_phase is a task — it can consume simulation time, and by default every component's run_phase starts at the same simulated time, running concurrently. There's no implicit ordering between a driver's run_phase and a scoreboard's run_phase — they're just separate concurrent processes, coordinated only by whatever TLM connections (analysis ports, sequencer/driver handshakes) actually exist between them.

Objections: how the simulation knows when to stop run_phase

If run_phase just ran until every component happened to finish, unrelated components could arbitrarily extend or truncate each other's execution. The objection mechanism makes "still busy" explicit:

task run_phase(uvm_phase phase);
  phase.raise_objection(this);
  // ... do work, send sequences, wait for responses ...
  phase.drop_objection(this);
endtask

The run_phase doesn't end until every raised objection across the entire hierarchy has been dropped. A component that raises an objection and forgets to drop it (a mistake in an error-handling path is the classic cause) hangs the simulation indefinitely — the top-level run_phase never sees the objection count reach zero.

The most common phasing bug: forgetting an objection in an error path

task run_phase(uvm_phase phase);
  phase.raise_objection(this);
  if (some_error_condition) begin
    `uvm_error("BAD", "unexpected state")
    return;   // BUG: objection was never dropped, simulation hangs
  end
  // ... normal flow ...
  phase.drop_objection(this);
endtask
Use a try-equivalent structure (SystemVerilog has no exceptions, so in practice: a single exit point, or explicit drop_objection calls on every return path) rather than an early return that skips the matching drop_objection.

Phase jumping and phase.raise_objection(this, "reason")

Passing a description string as the second argument to raise_objection/drop_objection costs nothing and pays for itself the first time you have to debug a hang — +UVM_OBJECTION_TRACE (or your simulator's equivalent) prints every raise/drop with its description, turning "why won't the simulation finish" from a hierarchy-wide search into a two-minute log read.