Ending a UVM test cleanly means keeping the run phase open until everything in flight has finished, and noticing when something has stopped making progress. Two mechanisms help beyond plain objections: phase_ready_to_end(), a callback that lets a component hold the phase open at the very last moment, and uvm_heartbeat, which checks that a group of components keeps showing signs of life and reports when they don't.

Objections and drain time, covered in UVM Objection, handle the common cases. They fall short when the right time to end depends on the testbench's own state, such as "every expected packet has been checked", or when a hung component should be caught long before the global timeout.

9 min read | Intermediate

What You'll Learn

  • Use phase_ready_to_end() to delay the end of a phase until a condition is met
  • Explain the order of callbacks when a phase ends
  • Set up a uvm_heartbeat to detect a stalled component
  • Choose between a drain time, a callback and a heartbeat

What Happens When the Last Objection Drops

End of test heartbeat

When every objection to the run phase has been dropped, the phase doesn't end at once. UVM first calls phase_ready_to_end() on every component, and gives each one a chance to raise the objection again if it isn't ready:

  • If nobody raises an objection, the phase ends after a delta cycle, and phase_ended() is called.
  • If somebody raises one, UVM waits until all objections are dropped again, then calls phase_ready_to_end() once more.
  • This repeats at most 20 times. After the twentieth iteration, phase_ended() is called whether or not anyone still objects. The limit protects against endless loops caused by a coding error.

Compared with a drain time, which waits a fixed amount of time, this callback waits for a condition. It costs no simulation time when the testbench is already idle, and it still waits as long as needed when it isn't.

Syntax


// In any uvm_component
virtual function void phase_ready_to_end(uvm_phase phase);
virtual function void phase_ended(uvm_phase phase);

// To hold the phase open from inside the callback
phase.raise_objection(this, "reason");
phase.drop_objection (this, "reason");

Example: Wait Until the Scoreboard Is Empty

The scoreboard keeps a count of expected transactions that the DUT hasn't produced yet. When the test's sequences finish, a few outputs may still be on their way. The scoreboard holds the phase open until the count reaches zero:


class my_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(my_scoreboard)
  int unsigned pending;                        // expected but not yet seen
  // Rest of the code (constructor, analysis imps that update pending)

  virtual function void phase_ready_to_end(uvm_phase phase);
    if (phase.get_name() != "run") return;     // only care about the run phase
    if (pending > 0) begin
      phase.raise_objection(this, "outputs still pending");
      fork
        begin
          wait (pending == 0);
          phase.drop_objection(this, "all outputs checked");
        end
      join_none
    end
  endfunction
endclass

Without any change to the test or its sequences, the run phase now lasts exactly as long as it takes the last output to arrive. The test doesn't need a guessed drain time, and a faster DUT doesn't make the test slower.

Two details matter. The check on the phase name keeps the callback from reacting to other phases such as main_phase or post_main_phase in the same simulation, which also call it. And the objection is dropped from a separate process, because phase_ready_to_end() is a function and cannot wait.

Always give the wait an upper limit

If an output is lost, pending never reaches zero and this test hangs until the global timeout. Add a bounded wait so the failure is reported clearly:


fork
  begin
    fork
      wait (pending == 0);
      #(50us);                                  // upper limit for the drain
    join_any
    disable fork;
    if (pending > 0)
      `uvm_error("SCBD", $sformatf("%0d outputs never arrived", pending))
    phase.drop_objection(this, "drain finished");
  end
join_none

Quick Check

Q: Two components each raise an objection in phase_ready_to_end() and each drops it later. How many times is phase_ready_to_end() called?

Show Answer

At least twice. The first call lets them raise the objections. When both have been dropped, UVM calls it again to check whether anyone still needs more time. The phase ends only after an iteration in which nobody raises an objection, or after the 20th iteration at the latest.

Heartbeats: Detecting a Stalled Testbench

Ending late is one problem. A component that silently stops working is another, and the test only notices when the global timeout expires, long after the cause. A uvm_heartbeat checks at regular intervals that components have been active, and reports a failure otherwise.

A heartbeat works with an objection that is used only for this purpose, not the phase objection. Components being watched raise or drop it to show that they are alive. A trigger event marks the end of each heartbeat window, and at that point the heartbeat checks whether enough components were active during the window.


function new(string name, uvm_component cntxt, uvm_objection objection = null);
function uvm_heartbeat_modes set_mode(uvm_heartbeat_modes mode = UVM_NO_HB_MODE);
function void set_heartbeat(uvm_event#(uvm_object) e, ref uvm_component comps[$]);
function void add(uvm_component comp);
function void remove(uvm_component comp);
function void start(uvm_event#(uvm_object) e = null);
function void stop();

The mode decides how many watched components must show activity in each window: UVM_ALL_ACTIVE for every one, UVM_ONE_ACTIVE for exactly one, or UVM_ANY_ACTIVE for at least one.

Example: Watching a Driver and a Monitor


class my_env extends uvm_env;
  `uvm_component_utils(my_env)
  uvm_objection          m_hb_obj;
  uvm_heartbeat          m_hb;
  uvm_event #(uvm_object) m_hb_event;
  // Rest of the code (constructor, agent handle)

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    m_hb_obj   = new("hb_obj");
    m_hb       = new("hb", this, m_hb_obj);
    m_hb_event = new("hb_event");
    // Components find the shared objection through the configuration database
    uvm_config_db #(uvm_objection)::set(this, "*", "hb_obj", m_hb_obj);
  endfunction

  virtual task run_phase(uvm_phase phase);
    uvm_component comps[$];
    comps.push_back(m_agent.m_driver);
    comps.push_back(m_agent.m_monitor);

    m_hb.set_mode(UVM_ANY_ACTIVE);             // either one showing life is enough
    m_hb.set_heartbeat(m_hb_event, comps);     // monitoring starts now

    forever begin
      #10us;                                   // one heartbeat window
      m_hb_event.trigger();
    end
  endtask
endclass

Each watched component signals that it is alive by raising and dropping the heartbeat objection whenever it does real work:


// In the monitor, after it captures a transaction
m_hb_obj.raise_objection(this);
m_hb_obj.drop_objection(this);

If a whole window passes with none of the watched components showing activity, the heartbeat reports a failure at the end of that window, pointing at the stall soon after it happened instead of leaving it to a generic timeout much later. What your simulator does after the report depends on the severity UVM uses, so check the log of a deliberately stalled run once. The window must be longer than the longest quiet period the DUT legitimately has. If a test deliberately pauses traffic, a heartbeat with a short window reports false alarms. Call stop() around such pauses, or choose a longer window.

The heartbeat's objection is private to the heartbeat. Raising it has no effect on whether the run phase can end, so it never keeps a test alive by accident.

Choosing the Right Tool

SituationUse
The test's sequences are the only activityAn objection in the test, nothing more
The DUT needs a roughly known time to finish after stimulus endsDrain time on the phase objection
The end depends on testbench state (queues empty, counts matched)phase_ready_to_end() with a bounded wait
A hung driver, monitor or DUT should be caught earlyuvm_heartbeat

Common Mistakes

Mistake #1: Forgetting the phase check

phase_ready_to_end() runs for every phase that ends, including the run-time phases. A callback that doesn't check phase.get_name() raises and drops objections in phases it was never meant for.

Mistake #2: Waiting forever in the callback

If the condition can never become true, the objection is never dropped and the test hangs. Always bound the wait and report an error when the limit is hit.

Mistake #3: Raising without dropping

Every objection raised in the callback needs a matching drop, even on the error path. After 20 iterations UVM ends the phase anyway, but only after wasting simulation time.

Mistake #4: A heartbeat window shorter than the quiet periods

A DUT that legitimately idles for longer than the window triggers false failures. Set the window from the longest expected gap, not from the typical one.

Summary

In this article, you learned:

  • When the last objection drops, UVM calls phase_ready_to_end() and repeats while anyone raises an objection, up to 20 times
  • A component can use the callback to keep a phase open until a testbench condition is true, such as an empty scoreboard queue
  • Always check the phase name and bound the wait
  • uvm_heartbeat checks that watched components stay active in every window and reports when they don't
  • A drain time suits a known delay, the callback suits state-based endings and a heartbeat suits catching hangs early
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5