Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
    • The Phase System
    • UVM Phases: Build, Connect, Elaborate and Run
    • Objections Deep Dive: Drain Time and Propagation
      • Objection Hierarchy Propagation: Up the Component Tree
      • Objection Counting Rules: Raises, Drops, and Edge Cases
      • set_drain_time Deep Dive: Grace Period After Objections Clear
      • phase_ready_to_end: Last-Chance Phase Extension
      • End-of-Test Strategy: Choosing a Mechanism
      • Objections at Scale
    • Phase Debugging and End-of-Test Signoff
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Objections Deep Dive: Drain Time and Propagation
  1. Topics
  2. UVM Verification
  3. UVM Phases & Simulation Lifecycle
  4. Objections Deep Dive: Drain Time and Propagation
  5. phase_ready_to_end: Last-Chance Phase Extension

Part 2/Phases & LifecycleIntermediate

phase_ready_to_end: Last-Chance Phase Extension

The callback that lets a component say 'not yet' when the count reaches zero, built from the DMA bench's end-of-test scoreboard and a real false-pass bug.

On this page

    While the course bench was being built, its first end-of-test scoreboard passed a test that had lost a word. The test dropped its objection as soon as the last input word was accepted. The scoreboard was supposed to hold the phase while anything was still pending. It looked, saw nothing pending, let the phase end, and reported 5 of 6 words matched with no error. Green.

    This lesson builds the version that cannot do that, dma_scoreboard_eot, and explains the callback it relies on: phase_ready_to_end.

    The idea: a last call before the doors close

    Before a flight's doors close, the gate agent does one last check: is anyone checked in who has not boarded? If so, the doors stay open until they arrive. The agent does not guess from how quiet the gate looks; they read the passenger list.

    phase_ready_to_end(uvm_phase phase) is that check. UVM calls it on every component when the objection count for a phase reaches zero (after the drain time). A component that still owes work raises an objection inside it, which keeps the phase alive; when the count reaches zero again, UVM calls the callbacks again. The loop is capped: by default 20 iterations, changeable with set_max_ready_to_end_iterations. The callback is a function, so it cannot wait. It may raise, and it may start a thread with fork ... join_none; the waiting and the drop happen in that thread.

    The picture

    Sequence diagram: the test drops at 365 ns; phase_ready_to_end on the scoreboard finds one word owed and raises; the last word arrives at 375 ns; the scoreboard drops; the second callback finds nothing owed and the phase ends.
    The bench's drain test: the test lets go with a word still inside the DMA; the scoreboard holds the phase for exactly that word.

    Build it

    The test drops early on purpose

    dma_drain_test swaps in the end-of-test scoreboard with a factory override, then drops its objection the moment the input sequence finishes, with no settle delay at all. It does not even enable the interrupt. Everything after the last input word is the scoreboard's job.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // snippet: from the course DMA bench
    // The test drops its objection as soon as the last input word is accepted, with
    // no settling delay. The end-of-test scoreboard keeps the run phase open until
    // the last output word arrives.
    class dma_drain_test extends dma_base_test;
      `uvm_component_utils(dma_drain_test)
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        dma_scoreboard::type_id::set_type_override(dma_scoreboard_eot::get_type());
        super.build_phase(phase);
      endfunction
    
      task run_phase(uvm_phase phase);
        axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
        phase.raise_objection(this);
        reg_write(KEY, 32'h1234_5678);
        reg_write(LEN, 6);
        reg_write(CTRL, 32'h1);                  // GO, interrupt not needed
        if (!pkt.randomize() with { n == 6; }) `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
        pkt.start(env.in_agt.sqr);
        phase.drop_objection(this);              // the last word is still inside the DMA
      endtask
    endclass

    The scoreboard counts what the DUT was asked to do

    dma_scoreboard_eot extends the base scoreboard. It learns the job size from the observed register writes: a LEN write sets len_reg, and a CTRL write with GO set adds len_reg to owed. Each output word decrements owed. When the phase is ready to end, it raises only if words are still owed, and a forked thread drops when the drained event fires.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // snippet: from the course DMA bench
    // A scoreboard that owns end of test. It learns how many words each job owes
    // from the observed LEN and GO register writes, and when every objection drops
    // while words are still owed, it raises its own objection until they arrive.
    // (Counting on the expected queue alone is not enough: the last input word may
    // not have been published yet when the test drops its objection.)
    class dma_scoreboard_eot extends dma_scoreboard;
      `uvm_component_utils(dma_scoreboard_eot)
      int unsigned len_reg, owed;
      event        drained;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      virtual function void write_apb(apb_item t);
        super.write_apb(t);
        if (!t.write || t.slverr) return;
        if (t.addr == LEN)                 len_reg = t.data[15:0];
        if (t.addr == CTRL && t.data[0])   owed += len_reg;   // GO: this job owes len_reg words
      endfunction
    
      virtual function void write_out(axis_item t);
        super.write_out(t);
        if (owed > 0) owed--;
        if (owed == 0) -> drained;
      endfunction
    
      function void phase_ready_to_end(uvm_phase phase);
        if (phase.get_imp() != uvm_run_phase::get() || owed == 0) return;
        phase.raise_objection(this, $sformatf("%0d word(s) still owed by the DMA", owed));
        fork begin
          @drained;
          phase.drop_objection(this, "all words out");
        end join_none
      endfunction
    endclass
    • The phase check compares phase.get_imp() with uvm_run_phase::get(). The phase argument is a node in the schedule, and get() returns the shared implementation, so comparing phase itself against uvm_run_phase::get() would never match. Comparing phase.get_name() == "run" also works.

    • *The `write_ overrides work** only because the base scoreboard declares them virtual`. The analysis imp calls them through a base-class handle; see war story 2 in Ordering bugs and silent overrides.

    • The thread does the waiting. fork begin ... end join_none is legal inside a function, and it is the only way to wait from here.

    Watch it run

    The real trace of dma_drain_test, with objection tracing on (a few lines for intermediate levels trimmed):

    Diagram
    UVM_INFO @ 365000: run_objection [OBJTN_TRC] Object uvm_test_top dropped 1 run_objection objection(s): count=0  total=0
    UVM_INFO @ 365000: run_objection [OBJTN_TRC] Object uvm_top subtracted 1 run_objection objection(s) from its total (dropped from source object uvm_test_top): count=0  total=0
    UVM_INFO @ 365000: run_objection [OBJTN_TRC] Object uvm_test_top.env.sb raised 1 run_objection objection(s) (1 word(s) still owed by the DMA): count=1  total=1
    UVM_INFO @ 365000: run_objection [OBJTN_TRC] Object uvm_test_top added 1 run_objection objection(s) to its total (raised from source object uvm_test_top.env.sb, 1 word(s) still owed by the DMA): count=0  total=1
    UVM_INFO @ 375000: run_objection [OBJTN_TRC] Object uvm_test_top.env.sb dropped 1 run_objection objection(s) (all words out): count=0  total=0
    UVM_INFO dma_tb.sv(411) @ 375000: uvm_test_top.env.sb [SB] 6 words matched, 0 mismatched
    UVM_ERROR :    0
    • At 365 ns the test drops and uvm_top's total reaches zero. In the same time step, phase_ready_to_end runs, and the scoreboard raises with the reason 1 word(s) still owed by the DMA.

    • At 375 ns the last output word arrives, drained fires and the scoreboard drops. The second callback finds owed == 0 and does nothing, so the phase ends.

    • 6 words matched: every word was predicted and compared, although the test let go one word early.

    What breaks: the check that passed with a word missing

    The first version of this scoreboard did not count register writes. It held the phase while its expected-word queue was non-empty:

    design.sv
    1
    2
    3
    4
    5
    6
    // bad-example
    function void phase_ready_to_end(uvm_phase phase);
      if (phase.get_imp() != uvm_run_phase::get() || expected.size() == 0) return;
      phase.raise_objection(this, "expected words pending");
      // ... drop when the queue empties ...
    endfunction
    • Symptom: the test passed, the scoreboard reported 5 words matched instead of 6, and there was no error.

    • Root cause: the test dropped in the same time step that the last input word was accepted, before the input monitor had published that word. The prediction for word 6 did not exist yet, so the queue looked empty, nobody raised, and the phase ended. check_phase saw an empty queue too, so SB_LEFTOVER stayed quiet.

    • Fix: derive "nothing pending" from what the DUT was asked to do (LEN words per GO, from the APB monitor), not from what the testbench happens to have seen so far.

    The general rule: an end-of-test hold must be keyed on an independent count of outstanding work. A queue of predictions is downstream of a monitor, and monitors are always a little behind the pins.

    Making it production-ready

    The bench version is deliberately small. Two additions make it safe in a real regression:

    • Bound the wait. If the DUT loses a word for real, @drained never fires and the test hangs until the global timeout. Fork a timeout branch beside it that reports uvm_error("SB_EOT", ...) with the owed count, then drops.

    • Guard against double raises. If another component raises and drops while this one is waiting, UVM calls the callback again. A waiting flag set at the raise and cleared at the drop stops a second raise for the same work.

    A raise and a drop in the same call to phase_ready_to_end do nothing: the count is back to zero before UVM looks again. If the work needs no time, you do not need an objection; if it does, the drop belongs in a forked thread.

    "Your test ends before the last transaction is checked. How do you fix it without adding delays?"

    A weak answer: "Increase the drain time" or "add a #1us before the drop." Both are guesses that slow every test and still fail on the unlucky seed.

    A strong answer: "Let the scoreboard own the tail. It counts outstanding work from what the DUT was asked to do, for example LEN words per GO from the register monitor. In phase_ready_to_end, which is a function, it checks phase.get_imp() against uvm_run_phase::get(), raises if work is owed, and forks a thread that drops when the count reaches zero, with a timeout that reports an error. UVM re-calls the callbacks after each drop, up to 20 times by default."

    Follow-ups: Why not key it on the expected queue? (the last input may not be published yet when the test drops; that is a real false pass). Why compare get_imp()? (the argument is a schedule node; get() returns the imp, so comparing the node never matches).

    Check yourself A scoreboard's phase_ready_to_end begins with if (phase != uvm_run_phase::get()) return;. What does it do in the run phase?

    Nothing, ever. The phase argument is the schedule node, and uvm_run_phase::get() returns the implementation singleton, so they are never equal and the function always returns. Compare phase.get_imp() instead, or the phase name.

    Check yourself Two components each raise in phase_ready_to_end on every call and drop 10 ns later in a thread. What happens?

    Each drop brings the count to zero, UVM calls the callbacks again, and both raise again. After 20 iterations (the default cap) UVM stops calling the callbacks, and the phase ends when the last objection drops. The test runs about 200 ns longer than needed, and if the reason is real outstanding work it may end before that work is done.

    Check yourself Why does dma_scoreboard_eot learn LEN from the APB monitor rather than reading it from the test?

    The test's value is the intent; the monitor's value is what reached the DUT. If the LEN write were lost or rejected with SLVERR, a scoreboard trusting the test would wait for words the DUT was never asked to send. Learning from the monitor keeps the end-of-test decision tied to what really happened on the bus.

    Next: you have now seen four ways to end a test. End-of-Test Strategy compares them side by side and shows how to choose.

    Related topics

    • End-of-test strategy

    • set_drain_time deep dive

    • check_phase debug

    Previous

    36 / 280

    Next: End-of-Test Strategy: Choosing a Mechanism

    Up next

    Up nextEnd-of-Test Strategy: Choosing a Mechanism

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Objection Hierarchy Propagation: Up the Component Tree
    • Objection Counting Rules: Raises, Drops, and Edge Cases
    • Objections at Scale