Skip to content

UVM

Phase Completion

How objections actually end a phase — the full completion sequence (count to zero, drain time, phase_ready_to_end, phase_ended), the work-aware extension hook for state-dependent completion, and how UVM advances from one phase to the next.

Objections · Module 17 · Page 17.4

The Engineering Problem

Raising and dropping objections moves the count up and down (Modules 17.2–17.3), and the phase ends when the count returns to zero. But "ends when the count is zero" is not the whole story — there's a completion sequence between count-zero and the next phase actually starting, and it exists to handle a real gap. Drain time (Module 17.3) handles fixed trailing latency — but what about completion that depends on state, not time? A scoreboard might have outstanding, unmatched transactions when the count hits zero — work that isn't done but that a fixed drain time can't reliably cover (the latency varies run to run). The phase needs a work-aware way to say "wait — I'm not actually done" at the moment it's about to end. And once it does end, UVM must advance to the next phase cleanly. The problem this chapter solves is the full completion process: what happens between count-zero and the next phase, and how a component can extend a phase that's about to end when it has more work.

Phase completion is the sequence UVM runs when the objection count reaches zero, deciding whether the phase really ends — and then advancing to the next phase. The steps: the count reaches zero; any drain time elapses (a new raise during the drain cancels the end); then UVM calls phase_ready_to_end(phase) on the components — a last-chance hook where a component can check its state and, if it has more work, raise an objection again to extend the phase (UVM re-runs the check, up to a max-iterations limit). Only when the count stays zero through phase_ready_to_end does the phase end — UVM calls phase_ended(phase) for per-phase cleanup — and then advances to the next phase in the schedule. The key is phase_ready_to_end: it is the work-aware extension hook for state-dependent completion (outstanding transactions resolved), where a fixed drain time would only guess. This chapter is phase completion: the full sequence, the ready-to-end extension hook, the phase_ended callback, and advancing phases.

What is the full phase-completion sequence — count to zero, drain time, phase_ready_to_end, phase_ended, then advance — and how does phase_ready_to_end let a component extend a phase that's about to end when its state shows work remaining?

Motivation — why completion is more than count-zero

The phase needs more than "count-zero ends it" because completion can depend on state and because the transition to the next phase must be clean. The reasons:

  • Drain time guesses; some completion depends on state. A fixed drain time (Module 17.3) covers fixed latency. But completion that depends on state — a scoreboard's outstanding transactions resolvedvaries run to run, so a fixed delay is too short (ends dirty) or too long (wastes time). A work-aware mechanism is needed.
  • A component may discover work at the last moment. When the count first reaches zero, a component might realize it has more to do (unmatched responses, a pending check). It needs a hookat the moment of about-to-end — to say so and extend the phase. That hook is phase_ready_to_end.
  • Extension must be bounded. If a component could re-raise forever, the phase would never end. So UVM re-runs phase_ready_to_end up to a max-iterations limit and errors if it never settles — extension must make progress.
  • Per-phase cleanup belongs at the end. Some actions must happen exactly when a phase ends — flushing, reporting, finalizing. The phase_ended callback gives a defined place for them.
  • The transition must be clean. Once a phase truly ends, UVM must advance to the next phase in the schedule deterministically — so the test progresses through reset, configure, main, shutdown, and so on.

The motivation, in one line: completion is more than count-zero because some completion depends on state (not a fixed time) — so UVM provides phase_ready_to_end as a work-aware extension hook (re-raise if state shows work, bounded by max-iterations), phase_ended for per-phase cleanup, and a deterministic advance to the next phase — turning "count hit zero" into "the phase is really done, cleaned up, and we've moved on."

Mental Model

Hold phase completion as the adjournment process — "any final business?" before the meeting closes and moves to the next item:

Phase completion is the adjournment process: when all hands are down (count zero) and the grace period (drain) has passed, the chair asks 'any final business?' — and anyone who realizes they still have something raises a hand again to extend the meeting. Only when no one does does the chair adjourn, record the minutes, and move to the next agenda item. Picture the meeting (the phase) nearing its end. All hands are down (the objection count is zero) and the grace period (the drain time) has elapsed. But the chair doesn't adjourn immediately — the chair asks the room: "any final business before we adjourn?" (UVM calls phase_ready_to_end on every component). This is everyone's last chance: a participant who checks their notes and realizes they still have an unresolved item (a scoreboard with outstanding transactions) raises a hand again (raises an objection) — and the meeting continues. The chair asks again after the new business is done (phase_ready_to_end is re-run), and keeps asking until no one raises a hand — but only up to a limit (max-iterations): if someone keeps raising a hand forever with no progress, the chair calls it out of order (an error). When finally no one has final business, the chair adjourns: the minutes are recorded (the phase_ended callback — per-phase cleanup), and the meeting moves to the next agenda item (UVM advances to the next phase). The crucial part is the "any final business?" question — it's what catches the participant who's almost done but not quite, whose completion depends on their state (do they still have unresolved items?), which a fixed grace period alone couldn't reliably catch.

So phase completion is the adjournment process: hands down (count zero) plus grace period (drain) → "any final business?" (phase_ready_to_endlast-chance, work-aware extension; re-raise if your state shows work, bounded by max-iterations) → if none, adjourn (phase_ended cleanup) → next agenda item (next phase). The phase_ready_to_end "final business" question is what makes completion state-aware — it catches the component that's about to be cut off but isn't really done, extending the phase until its state is clean, where a fixed drain could only guess.

Visual Explanation — the conditions for a phase to end

The defining picture is the gate: a phase ends only when all three conditions hold — count zero, drain elapsed, and no re-raise in phase_ready_to_end.

A phase ends only when the count is zero, the drain has elapsed, and phase_ready_to_end re-raises nothingcount == 0drain elapsedready_to_end:no re-raiseall three → phaseendsphase_ended →advanceCount zeroall objections droppedDrain elapsedno raise during drainready_to_end clearno component re-raisedPhase-end gateneeds ALL threePhase endsphase_ended cleanupNext phaseUVM advances12
Figure 1 — a phase ends only when all three conditions hold. First, the objection count must be zero (all objections dropped). Second, any drain time must have elapsed (a raise during the drain cancels the end and restarts it). Third, phase_ready_to_end must complete with no component re-raising (a component checking its state and finding work re-raises, extending the phase). Only when all three hold does the phase end — then phase_ended runs for cleanup, and UVM advances to the next phase. Any single condition unmet keeps the phase open.

The figure shows the three conditions that all must hold for a phase to end. Count zero: all objections dropped — the primary condition (Modules 17.1–17.3). Drain elapsed: any drain time has passed with no raise during it (a raise during the drain cancels the end and restarts the drain, Module 17.3). ready_to_end clear: phase_ready_to_end completed with no component re-raising — a component that checks its state and finds work re-raises, extending the phase. Only when all three (the warning-colored conditions) hold does the phase-end gate open and the phase end — then phase_ended runs for cleanup, and UVM advances to the next phase. The crucial reading is that count-zero alone is necessary but not sufficient: the phase also needs the drain to elapse and phase_ready_to_end to re-raise nothing. Any single condition unmet keeps the phase open — a nonzero count (someone's still working), an unelapsed drain (trailing latency), or a phase_ready_to_end re-raise (a component's state shows more work). The brand-colored gate requires all three; the success-colored end only happens past it. This is the completeness of phase completion: the count gates the bulk of the work, the drain covers fixed trailing latency, and phase_ready_to_end covers state-dependent completion — together, they ensure the phase ends only when the work is really, completely done. The diagram is the full end condition: not just count-zero, but count-zero AND drain-elapsed AND ready-to-end-clearthree gates, all of which must open before the phase ends and UVM advances.

RTL / Simulation Perspective — the phase_ready_to_end extension hook

In code, phase_ready_to_end is a callback a component overrides to check its state and re-raise if it has more work — the work-aware extension. The code shows the hook.

phase_ready_to_end: check state, re-raise to extend if work remains; phase_ended for cleanup
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// === SCOREBOARD: extend the phase if it has outstanding (unmatched) transactions ===
class my_scoreboard extends uvm_scoreboard;
  int outstanding;   // transactions sent but not yet matched/checked
 
  // called when the objection count first reaches zero — a LAST chance to extend
  function void phase_ready_to_end(uvm_phase phase);
    if (outstanding > 0) begin                       // ✓ state shows work remains
      phase.raise_objection(this, "outstanding txns"); // re-raise → phase EXTENDS, not ends
      fork begin
        wait (outstanding == 0);                     // wait until they resolve...
        phase.drop_objection(this, "outstanding cleared"); //  ...then drop → re-check
      end join_none
    end
    // if outstanding == 0, do nothing → the phase proceeds to end
  endfunction
 
  // called after the phase has actually ended — per-phase cleanup
  function void phase_ended(uvm_phase phase);
    report_summary();   // finalize/report for this phase
  endfunction
endclass
// UVM re-runs phase_ready_to_end after each extension, up to max_ready_to_end_iterations (else error).

The code shows the completion hooks. phase_ready_to_end(phase) is called when the objection count first reaches zero — a last chance to extend. The scoreboard checks its state (outstanding > 0 — transactions sent but not yet matched/checked) and, if work remains, re-raises an objection (phase.raise_objection(this, "outstanding txns")) — which extends the phase instead of ending it — then forks a process that waits for the outstanding to resolve (wait (outstanding == 0)) and drops the objection, triggering UVM to re-check (re-run phase_ready_to_end). If outstanding == 0, the hook does nothing and the phase proceeds to end. phase_ended(phase) is called after the phase has actually ended — for per-phase cleanup (report_summary()). The closing comment notes the bound: UVM re-runs phase_ready_to_end after each extension, up to max_ready_to_end_iterations, else error — so the extension must make progress. The shape to carry: phase_ready_to_end is the work-aware extension hookoverride it to check your state and re-raise (with a process that drops when the work resolves) if you have more to do, so the phase extends until your state is clean; phase_ended is the post-end cleanup hook. This is distinct from drain time: drain time is a fixed delay (a guess); phase_ready_to_end is a condition (the actual state) — for state-dependent completion, the hook is precise where the drain would guess.

Verification Perspective — work-aware extension vs a fixed drain

The phase_ready_to_end hook is the right tool for state-dependent completion — where a fixed drain time can only guess. Seeing the contrast is seeing when to use which.

Fixed drain time guesses variable completion; phase_ready_to_end checks the actual state and self-adjustspreset delaytoo short / too longchecks actualstatere-raise until cleanDrain timefixed delayPreset durationfor fixed latencyGuesses variablecompletiondirty or wastefulphase_ready_to_endwork-aware hookChecks the stateoutstanding work?Self-adjusts tocompletionextends until clean12
Figure 2 — fixed drain time versus work-aware extension. A fixed drain time holds the phase open a preset delay — fine for fixed, known latency, but a guess for variable completion: too short ends dirty, too long wastes time. The phase_ready_to_end extension checks the actual state (outstanding transactions) and re-raises until it's clean — self-adjusting to the real completion condition. Use a drain time for fixed trailing latency; use phase_ready_to_end when completion depends on state that varies run to run.

The figure contrasts the two completion-extension mechanisms. A drain time is a fixed preset durationfine for fixed, known latency (a pipeline of N cycles), but for variable completion it guesses: too short and the phase ends dirty (work unresolved), too long and it wastes time idling. The phase_ready_to_end hook is work-aware: it checks the actual state (are there outstanding transactions?) and re-raises until it's cleanself-adjusting to the real completion condition. The verification insight is that these are complementary tools for different situations. Use a drain time for fixed trailing latency — when the gap between last stimulus and last check is known and constant (Module 17.3). Use phase_ready_to_end when completion depends on state that varies run to run — when the gap is not a fixed time but a condition (all outstanding transactions resolved, all expected responses received). The warning-colored drain guesses a variable condition; the success-colored hook measures it. This matters because real completion is often state-dependent: a scoreboard with outstanding transactions whose resolution time varies (random latency, variable burst lengths) can't be reliably covered by a fixed drain — a drain long enough for the worst case wastes time on the common case, and a drain tuned for the common case fails the worst case. The phase_ready_to_end hook eliminates the guess: it holds the phase open exactly until the state is clean, no longer and no shorter. The figure is the decision: fixed latency → drain time; state-dependent completion → phase_ready_to_end — and the temptation to use a drain time for everything is exactly the bug (the DebugLab), because a fixed delay cannot reliably cover a variable completion condition. For state-dependent completion, check the state — don't guess a delay.

Runtime / Execution Flow — the completion sequence and advancing phases

At run time, completion is a defined sequence: count zero, drain, phase_ready_to_end (possibly extending), phase_ended, advance. The flow shows it end to end.

Completion sequence: count zero, drain, phase_ready_to_end with possible extension, phase_ended, advancecount reaches zero → drain elapses → phase_ready_to_end (re-raise extends, else proceed) → phase_ended cleanup → advance to next phasecount reaches zero → drain elapses → phase_ready_to_end (re-raise extends, else proceed) → phase_ended cleanup → advance to next phase1Count reaches zero, drain elapsesall objections dropped; any drain time passes (a raise during itrestarts the drain).2phase_ready_to_end: extend or proceedcomponents check their state; a re-raise extends the phase(re-checked when it drops, up to max iterations).3phase_ended: per-phase cleanupwith no re-raise, the phase ends; UVM calls phase_ended forfinalizing and reporting.4Advance to the next phaseUVM moves to the next phase in the schedule — reset, configure,main, shutdown, and so on.
Figure 3 — the completion sequence and advancing to the next phase. When the objection count reaches zero, the drain time elapses. Then UVM calls phase_ready_to_end on the components; if any re-raises (its state shows work), the phase extends and the check repeats when the new objection drops. Once phase_ready_to_end completes with no re-raise, the phase ends: UVM calls phase_ended for cleanup, then advances to the next phase in the schedule. The loop at phase_ready_to_end is the extension; the exit is the true end.

The flow shows the completion sequence end to end. Count zero, drain (step 1): all objections dropped; any drain time passes (a raise during it restarts the drain, Module 17.3). phase_ready_to_end (step 2): UVM calls the hook on the components; they check their state, and a re-raise extends the phase (re-checked when the new objection drops, up to max_ready_to_end_iterations). phase_ended (step 3): with no re-raise, the phase ends — UVM calls phase_ended for per-phase cleanup (finalizing, reporting). Advance (step 4): UVM moves to the next phase in the schedulereset, configure, main, shutdown, and so on. The runtime insight is that step 2 is a loop and steps 3–4 are the exit: phase_ready_to_end repeats (extend, re-check) until it settles (no re-raise), at which point the phase truly ends and UVM advances. So the completion is not a single instant but a negotiated end: count-zero triggers it, the drain delays it, phase_ready_to_end can extend it (as many times as components need, bounded by max-iterations), and only when all components are truly done does it exit to phase_ended and the next phase. This is how UVM advances: each run-time phase negotiates its own completion (its own objections, own drain, own phase_ready_to_end), and when one completes, UVM advances to the next — so the test progresses through the schedule, each phase ending only when its work is truly done. The flow is the machinery that turns count-zero into clean phase advancement: trigger (count zero), delay (drain), negotiate (phase_ready_to_end loop), finalize (phase_ended), advance (next phase) — the full lifecycle from one phase's end to the next phase's start.

Waveform Perspective — completion with a ready-to-end extension

The completion sequence is visible on a timeline: the count hits zero, but phase_ready_to_end re-raises (state shows work), extending the phase until the state clears, then it ends. The waveform shows the extension.

The count hits zero, but phase_ready_to_end re-raises to extend until the state is clean

12 cycles
The count hits zero, but phase_ready_to_end re-raises to extend until the state is cleancount hits 0 → phase_ready_to_end (rte) runscount hits 0 → phase_r…scoreboard has outstanding work → re-raises → obj_count back to 1 (extended)scoreboard has outstan…outstanding cleared → drop → rte runs again → clean → phase ends at 8outstanding cleared → …clkobj_count110111100000rteoutstanding222211000000phase_endt0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — completion with an extension. The objection count drops to zero at cycle 2. UVM calls phase_ready_to_end (rte): the scoreboard's outstanding count is still nonzero, so it re-raises — obj_count goes back to 1 at cycle 3, extending the phase. The outstanding work resolves at cycle 6 (outstanding goes to 0), the scoreboard drops, and obj_count returns to zero at cycle 7. phase_ready_to_end runs again, finds the state clean, and the phase ends (phase_end) at cycle 8. The extension held the phase open until the outstanding work was truly resolved — a fixed drain time could not have known how long that took.

The waveform shows completion with an extension. The objection count drops to zero at cycle 2. UVM calls phase_ready_to_end (rte pulses): the scoreboard's outstanding count is still nonzero (2), so it re-raisesobj_count goes back to 1 at cycle 3, extending the phase. The outstanding work resolves (outstanding counts down to 0 at cycle 6), the scoreboard drops, and obj_count returns to zero at cycle 7. phase_ready_to_end runs again (rte pulses at cycle 7), finds the state clean (outstanding == 0), and the phase ends (phase_end at cycle 8). The crucial reading is the extension: the count reached zero at cycle 2, but the phase did not endphase_ready_to_end caught the outstanding work and re-raised, holding the phase open (cycles 3–6) until the state cleared. Only when phase_ready_to_end runs again (cycle 7) and finds outstanding == 0 does the phase end (cycle 8). The picture to carry is that the phase's end is negotiated, not automatic at count-zero: the first count-zero (cycle 2) triggers phase_ready_to_end, which extends based on state, and the phase ends only when phase_ready_to_end finds the state clean. Reading this waveform — does the count return to zero, does ready_to_end extend while state shows work, does the phase end only when the state clears? — confirms the completion is state-aware. The count hitting zero, ready_to_end re-raising while outstanding work remains, and the phase ending only after it clears is the signature of phase_ready_to_end: it holds the phase open until the actual completion condition is meta fixed drain time could not have known how long the outstanding work would take, but the hook measured it exactly.

DebugLab — the phase that ended with outstanding transactions

A phase ending with unresolved outstanding transactions because a fixed drain time guessed the latency

Symptom

A test used a fixed drain time to let the DUT drain after the last stimulus. It usually worked — but intermittently, on runs with higher latency, the phase ended with the scoreboard still holding outstanding, unmatched transactions: those transactions were never checked, and a bug in them escaped. The drain time was tuned for the typical case, but the DUT's latency varied (random response delays, variable burst lengths), so on slow runs the fixed drain was too short — the phase ended before the outstanding work resolved.

Root cause

The completion condition was state-dependent (all outstanding transactions resolved) but was handled with a fixed drain time, which guesses — and on high-latency runs, the guess was too short, so the phase ended dirty:

why a fixed drain time can't cover a variable completion condition
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
✗ FIXED drain time for a VARIABLE completion condition (guesses the latency):
  rp.phase_done.set_drain_time(this, 200ns);   // ✗ tuned for typical latency
  // on a high-latency run, outstanding txns take 350ns to resolve → drain ends at 200ns →
  //   phase ends with outstanding > 0 → last transactions UNCHECKED → intermittent false pass
 
✓ WORK-AWARE extension via phase_ready_to_end (checks the actual state, self-adjusts):
  function void phase_ready_to_end(uvm_phase phase);
    if (outstanding > 0) begin
      phase.raise_objection(this, "outstanding txns");
      fork begin wait (outstanding == 0); phase.drop_objection(this); end join_none
    end
  endfunction
  // the phase extends EXACTLY until outstanding == 0 — however long that takes — then ends clean

This is the fixed-drain-for-variable-completion bug — using a fixed drain time where the completion condition is state-dependent and variable, so the fixed guess is sometimes too short and the phase ends dirty. The real completion condition was "all outstanding transactions resolved" — a state condition whose resolution time varied run to run (random latency, variable bursts). A fixed drain time (200ns) guesses that time: tuned for the typical case, it's fine most runs, but on a high-latency run where the outstanding work takes 350ns, the drain ends at 200nsbefore the work resolves — so the phase ends with outstanding > 0, and those last transactions are never checked (an intermittent false pass, the silent failure of Module 17.3, now caused by a too-short drain under variable latency). The intermittence is the tell: the fixed drain happens to be long enough on fast runs and too short on slow ones, so the failure correlates with latency. The fix is the work-aware phase_ready_to_end hook: check the actual state (outstanding > 0) and re-raise an objection that drops only when outstanding == 0 — so the phase extends exactly until the outstanding work resolves, however long that takes, then ends clean. Now the completion is tied to the state, not a guessed time, so it's correct on every run regardless of latency. The general lesson, and the chapter's thesis: for state-dependent completion — where the phase should end when a condition holds (outstanding work resolved), not after a fixed time — use phase_ready_to_end to check the state and re-raise until it's clean, not a fixed drain time that guesses and fails under variable latency; a drain time covers fixed, known latency, but state-dependent completion needs the work-aware hook. When completion depends on state, measure the state — don't guess a delay.

Diagnosis

The tell is intermittent dirty completion correlated with latency. Diagnose fixed-drain misuse:

  1. Check whether completion depends on state. If the phase should end when outstanding work resolves (a condition), a fixed drain time is the wrong tool.
  2. Correlate failures with latency. Intermittent unchecked-final-transactions failures that track DUT latency point at a too-short fixed drain.
  3. Look for a drain time covering variable completion. A set_drain_time tuned for typical latency fails the worst case.
  4. Confirm with a high-latency run. Force the worst-case latency and check whether outstanding transactions are still resolved before the phase ends.
Prevention

Match the completion mechanism to the condition:

  1. Use phase_ready_to_end for state-dependent completion. Check the actual state (outstanding work) and re-raise until it's clean, so the phase ends exactly when the condition holds.
  2. Reserve drain time for fixed, known latency. A fixed delay is right only when the trailing latency is constant and bounded.
  3. Make the extension make progress. The re-raise must eventually drop (the state must resolve), or the phase hits max_ready_to_end_iterations; extend only on resolvable work.
  4. Test the worst-case latency. Confirm completion is clean under the highest latency, proving the mechanism adapts.

The one-sentence lesson: for state-dependent completion — where the phase should end when a condition holds (outstanding work resolved), not after a fixed time — use phase_ready_to_end to check the state and re-raise until it's clean; a fixed drain time guesses the latency and ends the phase dirty under variable latency, while the work-aware hook holds the phase open exactly until the condition is met.

Common Mistakes

  • Using a fixed drain time for state-dependent completion. A fixed delay guesses variable latency and ends dirty; use phase_ready_to_end to check the actual state.
  • Re-raising in phase_ready_to_end without progress. An extension whose condition never resolves hangs the phase or hits max_ready_to_end_iterations; extend only on resolvable work.
  • Assuming count-zero ends the phase immediately. The phase also needs the drain to elapse and phase_ready_to_end to re-raise nothing; all three conditions must hold.
  • Putting per-phase cleanup in the wrong place. Finalizing and reporting belong in phase_ended, which runs when the phase actually ends.
  • Forgetting the extension is re-checked. UVM re-runs phase_ready_to_end after each extension; the hook must reflect the current state each time.
  • Ignoring max_ready_to_end_iterations. An error here means an extension never settled; the completion condition isn't resolving.

Senior Design Review Notes

Interview Insights

A phase completes through a defined sequence that starts when the objection count reaches zero and ends with UVM advancing to the next phase. First, all objections are dropped, so the count reaches zero. Second, any drain time elapses — and if an objection is raised during the drain, that cancels the end and restarts the drain. Third, UVM calls phase_ready_to_end on the components, which is a last-chance hook: a component can check its state and, if it has more work, raise an objection again to extend the phase. If anything re-raises, the phase extends, and UVM re-runs phase_ready_to_end after the new objection drops, up to a max_ready_to_end_iterations limit, erroring if it never settles. Fourth, only when phase_ready_to_end completes with no re-raise does the phase actually end: UVM calls phase_ended for per-phase cleanup like finalizing and reporting. Fifth, UVM advances to the next phase in the schedule. So the key point is that count-zero alone doesn't end the phase — the phase ends only when three conditions all hold: the count is zero, the drain time has elapsed, and phase_ready_to_end re-raises nothing. The completion is negotiated, not automatic: count-zero triggers it, the drain delays it, phase_ready_to_end can extend it as many times as components need within the iteration limit, and only when everyone is truly done does it finalize and advance. The mental model is adjournment: hands down and grace period passed, the chair asks "any final business?" — and only when no one raises a hand does the meeting adjourn, record minutes, and move to the next agenda item. That "any final business?" is phase_ready_to_end, the work-aware last chance to extend.

phase_ready_to_end is a callback UVM invokes when the objection count first reaches zero, before the phase actually ends, giving a component a last chance to check its state and extend the phase if it has more work. You override it in a component, check whether work remains — for example, whether a scoreboard still has outstanding, unmatched transactions — and if so, raise an objection again to extend the phase, typically forking a process that waits for the work to resolve and then drops the objection, which makes UVM re-check. If no work remains, you do nothing and the phase proceeds to end. UVM re-runs phase_ready_to_end after each extension, up to a max_ready_to_end_iterations limit, so the extension must make progress or it errors. You use phase_ready_to_end when completion depends on state that varies run to run, rather than on a fixed time. The classic case is a scoreboard with outstanding transactions whose resolution time varies because of random latency or variable burst lengths. A fixed drain time can't cover that reliably — tuned for the typical case, it's too short on slow runs and ends the phase with work unresolved; tuned for the worst case, it wastes time on fast runs. phase_ready_to_end eliminates the guess: it holds the phase open exactly until the state is clean, however long that takes, then lets the phase end. So the distinction is that drain time is a fixed delay for fixed, known latency, while phase_ready_to_end is a condition check for state-dependent completion. When the phase should end because a condition holds — all outstanding work resolved — and that condition's timing varies, you use phase_ready_to_end to check the state and re-raise until it's clean. It's the work-aware extension hook, and it's the right tool whenever completion is about state rather than a known delay.

Use a drain time for fixed, known trailing latency, and phase_ready_to_end for completion that depends on state that varies run to run. They're complementary tools for different situations. A drain time is a fixed delay that holds the phase open a preset duration after the count reaches zero. It's right when the gap between the last stimulus and the last check is constant and bounded — a pipeline of a known number of cycles, for instance. You set it once to cover that fixed latency, and it works. But a fixed drain time guesses when the completion condition is variable. If the real condition is "all outstanding transactions resolved" and their resolution time varies — random response delays, variable burst lengths — then a fixed delay is too short on slow runs, ending the phase dirty with work unresolved, or too long on fast runs, wasting simulation time. phase_ready_to_end solves that by checking the actual state and re-raising until it's clean, self-adjusting to the real completion. So the decision rule is: if the trailing latency is fixed and known, a drain time is simpler and sufficient; if completion depends on a condition whose timing varies, use phase_ready_to_end to measure the condition rather than guess a delay. The temptation to use a drain time for everything is exactly the trap, because a fixed delay cannot reliably cover a variable completion condition, and the failure is intermittent — fine on typical runs, broken on high-latency ones, with the last transactions silently unchecked. In practice, many testbenches use both: a drain time for the basic fixed latency of the interfaces, and phase_ready_to_end in the scoreboard to ensure no outstanding transactions remain regardless of latency. The principle is to match the mechanism to the condition — fixed time for fixed latency, state check for state-dependent completion.

phase_ended is a callback UVM invokes after the phase has actually ended, for per-phase cleanup, whereas phase_ready_to_end is invoked before the phase ends, as a chance to extend it. They sit on opposite sides of the moment the phase ends. phase_ready_to_end runs when the objection count first reaches zero, before the phase is allowed to end; its purpose is to let a component check its state and, if it has more work, re-raise an objection to extend the phase. It can run multiple times, looping as extensions happen, and it's where you keep the phase open until completion is truly reached. phase_ended runs after the phase has truly ended — after phase_ready_to_end has settled with no re-raise — and its purpose is finalizing actions specific to that phase: flushing buffers, writing a summary or report, releasing resources, recording final state. So phase_ready_to_end is about deciding whether to end and extending if not, while phase_ended is about cleaning up once the decision to end is final. You use phase_ready_to_end to ensure the phase doesn't end prematurely when state shows work remaining, and you use phase_ended to do work that must happen exactly when the phase concludes. Putting cleanup in phase_ended gives it a defined place that runs once per phase at the right moment, rather than scattering it or trying to detect the end some other way. The two together complete the lifecycle: phase_ready_to_end negotiates the true end, and phase_ended finalizes it. A common mistake is to confuse them and either try to extend in phase_ended, which is too late because the phase has already ended, or do cleanup in phase_ready_to_end, which is premature because the phase might still extend. So the rule is: extend in phase_ready_to_end, clean up in phase_ended.

UVM advances by completing each phase's objection-based termination and then moving to the next phase in the schedule. Each run-time phase has its own objection mechanism: it stays alive while its objection count is positive and ends when the count reaches zero, the drain time elapses, and phase_ready_to_end re-raises nothing. When a phase satisfies all of those and truly ends — after phase_ended runs for cleanup — UVM advances to the next phase in the phase schedule. The run-time phases include reset, configure, main, shutdown, and others, and they run in a defined order, so the test progresses through them one after another, each negotiating its own completion. This means phase advancement is driven entirely by completion: a phase doesn't end on a timer or after a fixed number of cycles, but when its work is truly done as signaled by its objections clearing and any extensions settling. Once that happens, the next phase begins, raises its own objections for its own work, runs, and completes the same way. So the overall test execution is a sequence of phases, each held open by objections until its work is finished, then handing off to the next. This is why getting objections right matters at every phase: if a phase's objections never clear, the test hangs at that phase and never advances; if they clear too early, that phase's work is cut off and the test advances prematurely. The phase schedule defines the order, and the objection mechanism defines the timing within each phase. Together they give a deterministic progression where each phase runs exactly as long as its work requires, then UVM moves on. The completion process — count zero, drain, phase_ready_to_end, phase_ended — is the per-phase machinery, and advancing through the schedule applies it phase by phase until the test finishes.

Exercises

  1. List the conditions. State the three conditions that must all hold for a phase to end, and what each covers.
  2. Use the hook. Write a phase_ready_to_end that extends the phase until a scoreboard's outstanding count is zero.
  3. Choose the mechanism. For fixed pipeline latency vs variable outstanding-transaction resolution, pick drain time or phase_ready_to_end and justify.
  4. Trace the advance. Describe what happens from a phase's count reaching zero through to the next phase starting.

Summary

  • Phase completion is the sequence UVM runs when the objection count reaches zero: the count reaches zero, any drain time elapses (a raise during it restarts the drain), UVM calls phase_ready_to_end (extension hook), then — if no re-raise — phase_ended (cleanup), then advances to the next phase.
  • A phase ends only when all three conditions hold: count zero, drain elapsed, and phase_ready_to_end re-raises nothing — count-zero alone is necessary but not sufficient.
  • phase_ready_to_end is the work-aware extension hook: a component checks its state and re-raises (with a process that drops when the work resolves) to extend the phase — self-adjusting to state-dependent completion, bounded by max_ready_to_end_iterations.
  • Use a drain time for fixed, known latency; use phase_ready_to_end when completion depends on state that varies (outstanding transactions resolved) — a fixed drain guesses and ends dirty under variable latency; phase_ended is for per-phase cleanup.
  • The durable rule of thumb: a phase ends only when the count is zero, the drain has elapsed, and phase_ready_to_end re-raises nothing — so for state-dependent completion, override phase_ready_to_end to check the actual state and re-raise until it's clean (not a fixed drain time that guesses variable latency and ends dirty), put per-phase cleanup in phase_ended, and UVM then advances to the next phase.

Next — Common Objection Bugs: the failure modes catalogued — the recurring objection mistakes (never-raised, never-dropped, dropped-too-early, unbalanced count, raised-too-late, missing drain) gathered with their symptoms and fixes, so you can recognize and avoid each one before it costs a debug session.