UVM-20: End-of-Test Mechanisms — VLSI Trainers
VLSI Trainers UVM Series · UVM-20
UVM Series · UVM-20

End-of-Test Mechanisms

Everything that controls when a UVM simulation ends — the objection counter model in depth, raise/drop rules, drain time, phase_ready_to_end for scoreboards and monitors, timeout watchdog implementation, objection debug tools, and the common patterns that cause hung simulations.

📋 How a UVM Test Ends

A UVM simulation ends when all time-consuming phases (run_phase and the sub-phases) have completed. Each phase completes when its objection count has been non-zero at some point and then reaches zero. Once all run-time phases are done, the cleanup phases (extract, check, report, final) execute in zero simulation time and the simulator exits.

This means simulation time is entirely controlled by who raises and drops objections and when. If no component ever raises an objection, the run_phase exits immediately — no simulation time passes and no stimulus is driven. If a component raises an objection and never drops it, the simulation hangs forever.

End-of-Test Flow — From run_phase Start to $finish run_phase starts raise_obj (test) objection count > 0 → run_phase stays alive sequences execute, driver drives DUT, monitor observes drop_obj (test) drain run_phase ends extract → check → report → final $finish ←─────────────────── run_phase active ───────────────────→ ← 0 sim time →Simulation time flows only while run_phase (or sub-phases) are active. Extract / check / report / final execute in zero simulation time after all run-time phases end. vlsitrainers.com
Figure 1 — End-of-test flow. run_phase begins; the test raises an objection; sequences run; the test drops its objection; after drain time, run_phase ends; cleanup phases execute in zero time; simulator calls $finish. If drain time is set and the count stays at zero after the drain period, the phase truly ends.

📋 The Objection Counter Model

The phase objection is a uvm_objection object associated with each run-time phase. It maintains a hierarchical counter — each component can raise or drop its own objections, and the counts propagate up the component hierarchy. The phase ends only when the total count at the top level reaches zero after having been non-zero.

Two counter values are maintained per component:

When a component raises an objection, the raise propagates up the hierarchy — every ancestor’s total count is incremented. When it drops, the drop propagates up. This means a leaf-level sequence raising an objection keeps the entire phase alive, even if no other component has an objection.

// ── Standard pattern — test raises and drops ─────────────
task run_phase(uvm_phase phase);
  phase.raise_objection(this);            // keep phase alive
  phase.raise_objection(this, "msg");    // with debug message
  phase.raise_objection(this, "msg", 5); // raise by count=5

  // ... run stimulus ...

  phase.drop_objection(this);            // allow phase to end
endtask

// ── Query objection state ─────────────────────────────────
uvm_objection obj = phase.get_objection();

int local_count = obj.get_objection_count(this);
int total_count = obj.get_objection_total(this);
time drain_t    = obj.get_drain_time(this);

// ── Display all pending objections (debug) ────────────────
obj.display_objections();

📋 Raise and Drop Rules

Objection Counter — Raise, Drop, and Phase Termination raise (test) count=1 raise (seq) count=2 drop (seq) count=1 drop (test) count=0 drain time phase ends Rule 1: Every raise must have exactly one matching drop. Rule 2: Phase ends after count reaches 0 + drain time elapses. vlsitrainers.com
Figure 2 — Objection counter lifecycle. Test raises (count 0→1), sequence raises (count 1→2), sequence drops (count 2→1), test drops (count 1→0). After drain time, the phase ends. Every raise must be paired with exactly one drop — extra drops cause a fatal error; missing drops cause the simulation to hang.
RuleConsequence of violation
Every raise_objection() must be paired with exactly one drop_objection()Missing drop: simulation hangs. Extra drop: UVM fatal — “objection count is already zero”
Raise before starting work, drop after finishingRaising after work has started may miss the phase end window — other components may have already dropped
Only raise from the beginning of run_phaseRaising from a non-task phase (build_phase, connect_phase) causes a compile error — only tasks can consume time
Always raise before starting sequencesIf the run_phase drops to zero before the sequence starts, the phase may end prematurely
Provide a description string for debugWithout descriptions, display_objections() shows useless “unnamed” entries — impossible to debug hangs
The most common cause of hung simulations: raise without drop. This always happens in one of three ways: (1) a conditional branch takes a path where drop_objection() is never reached, (2) a sequence raises an objection but the test structure exits before the sequence completes, or (3) an ISR sequence grabs the sequencer and never ungrabs. Use the description argument and +UVM_OBJECTION_TRACE to identify which component holds the last objection.

📋 Drain Time

Drain time is a configurable delay between the objection count reaching zero and the phase actually ending. It is used to allow in-flight transactions — items that are currently being driven or observed — to complete before the simulation ends. Without drain time, the scoreboard may miss the last few transactions that were in the pipeline when the test dropped its objection.

// ── Set drain time via the objection object ───────────────
task run_phase(uvm_phase phase);
  // Option 1: set drain time before raising
  phase.get_objection().set_drain_time(this, 200ns);
  phase.raise_objection(this);
  seq.start(m_seqr);
  phase.drop_objection(this);
  // 200ns elapses after drop before phase ends
endtask

// ── Option 2: set from start_of_simulation_phase ──────────
// Applies globally to all run_phase participants
function void start_of_simulation_phase(uvm_phase phase);
  uvm_phase   run_ph = uvm_domain::get_uvm_schedule().find_by_name("run");
  run_ph.get_objection().set_drain_time(uvm_root::get(), 500ns);
endfunction

// ── Option 3: set on the phase directly ──────────────────
// In build_phase — applies to the upcoming run_phase
function void build_phase(uvm_phase phase);
  super.build_phase(phase);
  // ... build env ...
  uvm_config_db #(int)::set(this, "", "drain_time", 1000); // in ns
endfunction
Prefer phase_ready_to_end over a fixed drain time. A fixed drain time is a guess — too short misses transactions, too long wastes regression time. phase_ready_to_end lets the scoreboard signal exactly when it is ready, based on the actual state of its queues. Use drain time only when a simple fixed buffer (e.g., 100ns for a pipeline flush) is the right model.

📋 phase_ready_to_end

phase_ready_to_end() is called on every component when the phase objection count first reaches zero. A component that is not ready for the phase to end can raise a new objection from inside this callback, extending the phase. The callback is a function — any blocking wait must be forked.

// ── Standard pattern — scoreboard extends phase ───────────
function void phase_ready_to_end(uvm_phase phase);
  // Only act on run_phase — ignore other phases
  if (phase.get_name() != "run") return;

  if (m_expected_fifo.size() > 0 || m_actual_fifo.size() > 0) begin
    `uvm_info("SB", $sformatf(
      "Extending phase: %0d expected, %0d actual pending",
      m_expected_fifo.size(), m_actual_fifo.size()), UVM_LOW)

    // Raise new objection to extend the phase
    phase.raise_objection(this, "Scoreboard draining");

    // Fork a thread to wait for completion and drop
    // (function cannot block — must use fork)
    fork begin
      wait(m_expected_fifo.size() == 0 &&
           m_actual_fifo.size() == 0);
      #100ns;  // small buffer for in-flight comparison
      phase.drop_objection(this, "Scoreboard drained");
    end join_none
  end
endfunction
phase_ready_to_end can be called multiple times. Every time the count drops to zero, the callback fires again. If a subscriber raises a new objection from inside the callback, and that objection later drops (count goes to zero again), the callback fires again. This can loop — always check that your wait condition will eventually become true.

📋 Scoreboard End-of-Test Pattern — Full Example

The complete, correct end-of-test handling for a scoreboard that uses FIFOs — combining phase_ready_to_end with a check_phase validation:

class gpio_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(gpio_scoreboard)

  tlm_analysis_fifo #(apb_seq_item) m_expected_fifo;
  tlm_analysis_fifo #(apb_seq_item) m_actual_fifo;
  int m_matches = 0, m_mismatches = 0;

  // ... build_phase, write_expected, write_actual, compare ...

  // ── End-of-test: extend phase if FIFOs not empty ─────────
  function void phase_ready_to_end(uvm_phase phase);
    if (phase.get_name() != "run") return;
    if (m_expected_fifo.size() > 0 || m_actual_fifo.size() > 0) begin
      phase.raise_objection(this, "SB: pending transactions");
      fork begin
        wait(m_expected_fifo.size() == 0 && m_actual_fifo.size() == 0);
        phase.drop_objection(this, "SB: all drained");
      end join_none
    end
  endfunction

  // ── check_phase: validate FIFO is empty ───────────────────
  function void check_phase(uvm_phase phase);
    if (m_expected_fifo.size() > 0)
      `uvm_error("SB", $sformatf(
        "%0d expected transactions never matched",
        m_expected_fifo.size()))
    if (m_actual_fifo.size() > 0)
      `uvm_error("SB", $sformatf(
        "%0d actual transactions have no expected pair",
        m_actual_fifo.size()))
  endfunction

  // ── report_phase: final summary ───────────────────────────
  function void report_phase(uvm_phase phase);
    `uvm_info("SB", $sformatf(
      "Result: %0d matches, %0d mismatches — %s",
      m_matches, m_mismatches,
      m_mismatches == 0 ? "PASSED" : "FAILED"), UVM_LOW)
  endfunction
endclass

📋 Timeout Watchdog

A timeout watchdog detects hung simulations — cases where the run_phase never ends because an objection is never dropped. Without a watchdog, a hung simulation runs until the simulator’s wall-clock timeout (often many hours), wasting compute resources.

UVM provides a built-in simulation timeout via uvm_root. It ends the simulation with a fatal message if the specified time elapses during a run-time phase:

// ── Built-in UVM timeout ──────────────────────────────────
// Call from start_of_simulation_phase or build_phase
function void start_of_simulation_phase(uvm_phase phase);
  // End simulation with fatal if run_phase takes > 10ms
  uvm_root::get().set_timeout(10ms, 1);
  // Second arg: 1 = override command-line timeout, 0 = don't override
endfunction

// ── Command-line timeout (no recompile) ──────────────────
// +UVM_TIMEOUT=<time>,YES  (YES = override set_timeout)
// vsim tb_top +UVM_TIMEOUT=5ms,YES

// ── Custom watchdog using a forked thread ─────────────────
task run_phase(uvm_phase phase);
  phase.raise_objection(this);

  fork
    // Main stimulus thread
    begin
      seq.start(m_env.m_agent.m_seqr);
    end

    // Watchdog thread — kills simulation if hung
    begin
      #1ms;  // timeout value
      `uvm_fatal("TIMEOUT", "Test timed out after 1ms")
    end
  join_any
  disable fork;   // kill the timeout thread if stimulus finished

  phase.drop_objection(this);
endtask
Set the timeout to something sensible — not too short, not too long. A timeout shorter than the longest expected test will cause false failures. A timeout of 24 hours wastes a full day on a hung simulation. A good rule: set it to 2–5× the longest expected test duration. Use +UVM_TIMEOUT on the command line so different tests can override the base test’s default without recompilation.

📋 Objection Debug Tools

// ── Print all pending objections ─────────────────────────
phase.get_objection().display_objections();

// ── Command-line trace — every raise/drop logged ─────────
// +UVM_OBJECTION_TRACE  — prints every raise and drop with context
// +UVM_PHASE_TRACE     — prints every phase transition

// ── Who is holding the last objection? ───────────────────
// After simulation appears hung, check the transcript for:
// "UVM_INFO @ T: uvm_test_top [OBJECTION] ..."
// The component path tells you exactly who raised the last objection

// ── Programmatic check in end_of_elaboration_phase ───────
function void end_of_elaboration_phase(uvm_phase phase);
  uvm_objection obj = uvm_run_phase::get().get_objection();
  `uvm_info("EOT", $sformatf(
    "Current run_phase objection count: %0d",
    obj.get_objection_total(null)), UVM_LOW)
endfunction
Debug toolWhat it showsWhen to use
+UVM_OBJECTION_TRACEEvery raise and drop with component path, description, and countFirst tool to use when simulation hangs — identifies last outstanding objection
+UVM_PHASE_TRACEEvery phase transition — start, ready_to_end, endWhen phase_ready_to_end loops unexpectedly
display_objections()Current outstanding objections with hierarchyAdd to start_of_simulation_phase to snapshot initial state
get_objection_total(null)Total count across entire hierarchyPeriodic check in run_phase to verify count is decreasing
Description string in raise/dropMeaningful name in OBJECTION_TRACE outputAlways add — makes tracing readable

📋 Diagnosing Hung Simulations

When a simulation appears hung (run_phase never ends), the diagnosis procedure:

  1. Add +UVM_OBJECTION_TRACE to the simulator command line and re-run. The last logged raise without a matching drop identifies the culprit.
  2. Check the component path in the trace. The component name tells you which class and which sequence is holding the objection.
  3. Look for missing drops in conditional code paths — an if/else branch where only one path calls drop_objection().
  4. Check for deadlocks — a sequencer left grabbed (grab() without ungrab()), or a FIFO blocking get() that is never written to.
  5. Add a timeout using uvm_root::get().set_timeout() so future hangs produce a fatal rather than an infinite wait.
SymptomLikely causeFix
Simulation never endsraise_objection() without matching drop_objection()Add +UVM_OBJECTION_TRACE, find last unpaired raise, add drop in all exit paths
Simulation ends immediatelyNo component ever raises an objectionAdd raise_objection() at the start of run_phase in the test
Simulation ends too earlyTest drops objection before sequence finishesRaise before starting sequence, drop only after sequence completes
Scoreboard misses last transactionsPhase ends while FIFO still has itemsImplement phase_ready_to_end in scoreboard
phase_ready_to_end loops foreverWait condition never becomes true — FIFO never drainsAdd a timeout inside the fork/join_none; check that run_phase is actually processing FIFO items

📋 Quick Reference

ItemKey fact
How test endsrun_phase ends when objection count reaches zero (after having been non-zero) + drain time
Phase ends if count never raisedrun_phase exits immediately — no simulation time passes
raise_objection() syntaxphase.raise_objection(this, "description", count=1)
drop_objection() syntaxphase.drop_objection(this, "description", count=1)
Pairing ruleEvery raise must have exactly one matching drop. Extra drop → fatal.
Description argumentAlways provide — essential for +UVM_OBJECTION_TRACE debugging
Drain time purposeWait N time after count=0 before phase ends — allows pipeline flush
set_drain_time() syntaxphase.get_objection().set_drain_time(this, 200ns)
phase_ready_to_end useScoreboard/monitor extends phase when FIFOs non-empty. Preferred over fixed drain time.
phase_ready_to_end ruleFunction — must fork any blocking wait. Drop objection inside the forked thread.
phase_ready_to_end re-entryCalled again every time count drops to zero. Guard with a flag or condition check.
Built-in timeoutuvm_root::get().set_timeout(10ms, 1)
Command-line timeout+UVM_TIMEOUT=5ms,YES
Debug hung simulationAdd +UVM_OBJECTION_TRACE — shows last unpaired raise with component path
+UVM_PHASE_TRACEShows every phase transition — helps debug phase_ready_to_end loops
Scroll to Top