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
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.