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.
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.
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();
| Rule | Consequence 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 finishing | Raising after work has started may miss the phase end window — other components may have already dropped |
| Only raise from the beginning of run_phase | Raising from a non-task phase (build_phase, connect_phase) causes a compile error — only tasks can consume time |
| Always raise before starting sequences | If the run_phase drops to zero before the sequence starts, the phase may end prematurely |
| Provide a description string for debug | Without descriptions, display_objections() shows useless “unnamed” entries — impossible to debug hangs |
+UVM_OBJECTION_TRACE to identify which component holds the last objection.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
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
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
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
+UVM_TIMEOUT on the command line so different tests can override the base test’s default without recompilation.// ── 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 tool | What it shows | When to use |
|---|---|---|
+UVM_OBJECTION_TRACE | Every raise and drop with component path, description, and count | First tool to use when simulation hangs — identifies last outstanding objection |
+UVM_PHASE_TRACE | Every phase transition — start, ready_to_end, end | When phase_ready_to_end loops unexpectedly |
display_objections() | Current outstanding objections with hierarchy | Add to start_of_simulation_phase to snapshot initial state |
get_objection_total(null) | Total count across entire hierarchy | Periodic check in run_phase to verify count is decreasing |
| Description string in raise/drop | Meaningful name in OBJECTION_TRACE output | Always add — makes tracing readable |
When a simulation appears hung (run_phase never ends), the diagnosis procedure:
+UVM_OBJECTION_TRACE to the simulator command line and re-run. The last logged raise without a matching drop identifies the culprit.if/else branch where only one path calls drop_objection().uvm_root::get().set_timeout() so future hangs produce a fatal rather than an infinite wait.| Symptom | Likely cause | Fix |
|---|---|---|
| Simulation never ends | raise_objection() without matching drop_objection() | Add +UVM_OBJECTION_TRACE, find last unpaired raise, add drop in all exit paths |
| Simulation ends immediately | No component ever raises an objection | Add raise_objection() at the start of run_phase in the test |
| Simulation ends too early | Test drops objection before sequence finishes | Raise before starting sequence, drop only after sequence completes |
| Scoreboard misses last transactions | Phase ends while FIFO still has items | Implement phase_ready_to_end in scoreboard |
| phase_ready_to_end loops forever | Wait condition never becomes true — FIFO never drains | Add a timeout inside the fork/join_none; check that run_phase is actually processing FIFO items |
| Item | Key fact |
|---|---|
| How test ends | run_phase ends when objection count reaches zero (after having been non-zero) + drain time |
| Phase ends if count never raised | run_phase exits immediately — no simulation time passes |
| raise_objection() syntax | phase.raise_objection(this, "description", count=1) |
| drop_objection() syntax | phase.drop_objection(this, "description", count=1) |
| Pairing rule | Every raise must have exactly one matching drop. Extra drop → fatal. |
| Description argument | Always provide — essential for +UVM_OBJECTION_TRACE debugging |
| Drain time purpose | Wait N time after count=0 before phase ends — allows pipeline flush |
| set_drain_time() syntax | phase.get_objection().set_drain_time(this, 200ns) |
| phase_ready_to_end use | Scoreboard/monitor extends phase when FIFOs non-empty. Preferred over fixed drain time. |
| phase_ready_to_end rule | Function — must fork any blocking wait. Drop objection inside the forked thread. |
| phase_ready_to_end re-entry | Called again every time count drops to zero. Guard with a flag or condition check. |
| Built-in timeout | uvm_root::get().set_timeout(10ms, 1) |
| Command-line timeout | +UVM_TIMEOUT=5ms,YES |
| Debug hung simulation | Add +UVM_OBJECTION_TRACE — shows last unpaired raise with component path |
| +UVM_PHASE_TRACE | Shows every phase transition — helps debug phase_ready_to_end loops |