UVM Objections: raise_objection, drop & drain time

A UVM objection is the mechanism a testbench uses to say “do not end this phase yet.” UVM has no way of knowing when your stimulus is finished, so every component that still has work to do raises an objection, and the phase is only allowed to end once every one of them has been dropped. Objections are what turn a pile of independently-running drivers, monitors and sequences into a simulation that stops at exactly the right moment instead of too early or never at all.

The UVM snippets on this page are UVM-library code: they need a UVM-capable simulator such as industry-standard simulation tools, VCS or industry-standard simulators, or a free EDA Playground session, to elaborate and run. They are shown here as syntax references. The one plain-SystemVerilog illustration near the end models the same idea and runs on any simulator with no UVM library at all.

raise_objection and drop_objection

Objections live on a phase object. Inside a time-consuming phase you call phase.raise_objection(this) before you begin work and phase.drop_objection(this) when that work is complete. The usual place is the test’s run_phase, wrapped around the top-level sequence:

task test_base::run_phase(uvm_phase phase);
  traffic_seq seq;
  seq = traffic_seq::type_id::create("seq");

  phase.raise_objection(this, "test: stimulus starting");
  seq.start(env.agent.sequencer);   // blocks until the sequence ends
  phase.drop_objection(this, "test: stimulus complete");
endtask

The second argument is a description string, and it is the single most useful debugging aid you have because UVM prints it in the objection trace. The third optional argument is a count, so raise_objection(this, "burst", 4) raises four at once and you must later drop four. Each raise must be matched by a drop; a mismatch is the root of almost every objection bug.

Who should raise an objection?

A good rule is that whoever controls the duration of the stimulus owns the objection. In most testbenches that is the test or the sequence, not the driver. A driver sitting in a forever loop should not raise an objection, because it never stops on its own and would hold the phase open forever. Let the sequence raise around its own body instead:

task traffic_seq::body();
  starting_phase.raise_objection(this, "seq: driving burst");
  repeat (10) begin
    req = packet::type_id::create("req");
    start_item(req);
    assert(req.randomize());
    finish_item(req);
  end
  starting_phase.drop_objection(this, "seq: burst done");
endtask

How objections end the run phase

UVM keeps a single running total of objections for each phase, summed across the whole component hierarchy. When a component raises, the total goes up; when it drops, the total goes down. The phase is permitted to end only when that total returns to zero. This is the entire contract, and it leads to one rule worth memorising: the run phase does not end when one component finishes, it ends when the last one does.

That is why two overlapping drivers do not cut each other short. If driver A finishes at 30 ns and drops, but driver B is still running, the total is still non-zero and the phase stays alive until B drops too. Only then does the count hit zero and the phase is cleared to end.

CallEffect on the phase
raise_objectionIncrements the total; guarantees the phase cannot end
drop_objectionDecrements the total; the phase may end if it reaches zero
total > 0Phase keeps running (sequences and drivers active)
total == 0Phase is eligible to end, subject to drain time

phase_ready_to_end and drain time

Reaching a count of zero does not always mean the test is truly done. A response may still be in flight, or a scoreboard may be holding items it has not yet checked. UVM gives you two hooks to handle that trailing activity.

The first is drain time: a fixed grace period after the last objection drops, during which the phase waits before ending. If no new objection is raised in that window, the phase ends; if one is raised, the clock resets. Set it once, usually in the test:

task test_base::run_phase(uvm_phase phase);
  // give trailing responses 100 ns after the final drop
  phase.phase_done.set_drain_time(this, 100ns);

  phase.raise_objection(this, "stimulus");
  seq.start(env.agent.sequencer);
  phase.drop_objection(this, "stimulus done");
  // run_phase now ends 100 ns later, not immediately
endtask

The second hook is phase_ready_to_end, a callback UVM invokes when the objection count first hits zero, before it actually ends the phase. If you are not ready, you raise a fresh objection inside the callback and the phase is kept alive. This is the clean way to drain a scoreboard without guessing a drain-time number:

function void env::phase_ready_to_end(uvm_phase phase);
  if (phase.get_name() == "run" && scoreboard.outstanding() > 0) begin
    phase.raise_objection(this, "scoreboard still draining");
    fork begin
      wait (scoreboard.outstanding() == 0);
      phase.drop_objection(this, "scoreboard drained");
    end join_none
  end
endfunction

Use drain time for a short, predictable tail, and phase_ready_to_end when the end condition is data-dependent. They can be combined, but reach for the callback first when the wait has a real condition to test.

The objection idea, in plain SystemVerilog

You cannot run the UVM blocks above without a UVM-capable simulator, but the mechanism is a plain counter. The model below raises two objections before any time passes, lets two threads drop at different times, waits for the count to reach zero, and then honours a drain window. Run it on any simulator and watch the run end at the second drop, not the first, and only after the drain elapses.

module tb;
  int pending = 0;          // stands in for the UVM objection count
  time drain = 20;          // "drain time": extra wait after the last drop

  // raise_objection: a component says "the run phase may not end yet"
  task raise(string who);
    pending++;
    $display("[%0t] %s raises  -> pending=%0d", $time, who, pending);
  endtask

  // drop_objection: that component is finished
  task drop(string who);
    pending--;
    $display("[%0t] %s drops   -> pending=%0d", $time, who, pending);
  endtask

  initial begin
    $display("[%0t] run_phase begins", $time);

    // Both drivers raise BEFORE any time passes, exactly as components do
    // when the run phase starts. Now the total count is non-zero.
    raise("driver_a");
    raise("driver_b");

    // Two independent threads finish at different times and each drops once.
    fork
      begin #30 $display("[%0t] driver_a sent its last packet", $time); drop("driver_a"); end
      begin #50 $display("[%0t] driver_b sent its last packet", $time); drop("driver_b"); end
    join_none

    // The phase stays alive while ANY objection is still raised.
    wait (pending == 0);
    $display("[%0t] last objection dropped; draining for %0t", $time, drain);

    // drain time: a grace window after the final drop for late activity.
    #drain;
    $display("[%0t] drain elapsed, no new objections; ending run_phase", $time);
    $finish;
  end
endmodule

[0] run_phase begins
[0] driver_a raises  -> pending=1
[0] driver_b raises  -> pending=2
[30] driver_a sent its last packet
[30] driver_a drops   -> pending=1
[50] driver_b sent its last packet
[50] driver_b drops   -> pending=0
[50] last objection dropped; draining for 20
[70] drain elapsed, no new objections; ending run_phase
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpbg_plgn1/tb.v:38: $finish called at 70 (1s)

Swap pending++/pending-- for phase.raise_objection/drop_objection, the two #delay threads for real sequences on sequencers, and the initial block for run_phase, and you have the UVM version. The ordering is identical: the phase ends on the last drop, after the drain.

Common objection hang bugs

Almost every objection problem is one of two symptoms: the test ends too early, or it never ends. Here is how each is usually caused.

  • No raise at all. If nothing raises an objection in run_phase, the count is zero from the start and the phase ends at time zero. Your sequence may never drive a single transaction. Symptom: a test that finishes almost instantly.
  • A raise with no matching drop. The count never returns to zero, so the phase runs until the global timeout fires and the simulation is killed. Symptom: a hang that ends only at +UVM_TIMEOUT or a watchdog.
  • A drop on an exception path that is skipped. If the drop sits after code that can throw or return early, it may never execute. Keep the raise and drop at the same level so every exit path reaches the drop.
  • A driver’s forever loop holding an objection. A component that never stops must never own the objection, or the phase can never end. Move the objection to the sequence or test.
  • Dropping below zero. More drops than raises triggers a fatal objection error. This usually means two places are both managing the same objection; pick one owner.
  • A drain time that is too long. A huge set_drain_time value will not hang forever, but it inflates every test’s end time. Keep it to the real tail latency of your design.

When a hang happens, raise the objection trace verbosity so UVM prints every raise and drop with its description and source component. Whichever component still holds a count at the timeout is your culprit, and the description you wrote earlier names the exact call.

Quick check

When exactly is the run phase allowed to end?

When the total objection count for the phase, summed across every component, returns to zero, and then only after any drain time has elapsed with no new objection raised. One component finishing is not enough; the last one must drop too.

A test finishes at time zero without driving anything. What is the likely cause?

Nothing raised an objection in run_phase, so the count was zero from the start and the phase ended immediately. Add a raise_objection before the stimulus starts and a matching drop_objection after it completes.

What is the difference between drain time and phase_ready_to_end?

Drain time is a fixed grace window after the final drop, good for a short predictable tail. phase_ready_to_end is a callback fired when the count hits zero, letting you raise a fresh objection and keep the phase alive until a real data-dependent condition is met.

Why should a driver in a forever loop not raise an objection?

Because it never stops on its own, so its objection would never be dropped and the phase could never end. The objection belongs to whatever controls the duration of the stimulus, normally the sequence or the test.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Compile and simulate this in the browser — no install. Edit the code and re-run instantly.

Open in Verification Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD