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. Objection Counting Rules: Raises, Drops, and Edge Cases

Part 2/Phases & LifecycleIntermediate

Objection Counting Rules: Raises, Drops, and Edge Cases

Pairing every raise with a drop, OBJTN_ZERO, NULL_OBJECTION, the test that ends at time 0, and sequences as raisers.

On this page

    A new test passes in 0 ns of simulated time. The log says UVM_ERROR : 0. A second test, written by the same engineer the same afternoon, dies with a UVM_FATAL about an objection count below zero. A third sails through but leaves the last two DMA words uncompared. Three different symptoms, one root cause: the objection count did something the author did not expect.

    This lesson collects the rules of the count. Each one is short, each one is asked in interviews, and each one has burned a real regression.

    The idea: a ledger, not a flag

    An objection is a ledger, not an on/off flag. Every raise is a deposit and every drop is a withdrawal, per object. UVM checks only two things: you may not withdraw more than you deposited, and the phase may end only when the whole bank, uvm_top's total, is empty. It never checks who should have deposited. That is your job, and it is why the rules below are about ownership as much as arithmetic. The mechanics of the counts themselves, and a runnable model, are in Objection Hierarchy Propagation.

    The rules in one table

    What you do

    What UVM does

    Severity

    Raise and drop once each, on every path

    Count returns to 0; the phase may end

    None

    Drop an objection that was never raised

    Reports OBJTN_ZERO: the drop would take the count below zero

    UVM_FATAL

    Raise in build_phase or another function phase

    Reports UVM/PH/NULL_OBJECTION: a function phase has no objection

    UVM_ERROR

    Never raise in run_phase

    Nothing holds the phase, so it ends at time 0

    None (a silent false pass)

    Raise, then fork ... join_none, then drop

    The drop runs at once; the phase ends while the forked work runs

    None (an early end)

    raise_objection(this, "burst", 3)

    Adds 3 to the count; drops must total 3

    None

    Two of these deserve a closer look because they are the opposite of what people expect. A drop without a raise is fatal, not a warning: the simulation stops there. And a test with no raise does not hang; it ends immediately, with every forked thread still pending. If all objections drop, the phase ends after the drain time even if threads started with fork ... join_none are still running. That is an early end, not a hang.

    Raise before the first blocking statement

    UVM decides whether anyone objects to the run phase right at the start, in the same time step it is entered. If your run_phase waits for anything before raising, even one clock edge, the phase may already have ended.

    design.sv
    1
    2
    3
    4
    5
    6
    // bad-example
    task run_phase(uvm_phase phase);
      @(posedge vif.clk);                       // blocks first ...
      phase.raise_objection(this, "too late");  // ... the phase ended at time 0
      // the job below never runs; check_phase sees nothing
    endtask

    The bench's tests all raise on their first line. The same rule holds for runtime phases such as main_phase: raise first, then wait.

    One exit, one drop

    Every raise needs a drop on every path out of the task, including error returns and timeouts. The safest shape has exactly one drop, after the work, which every path reaches. Bounded waits help: wrap a wait that might never finish so it gives up with an error instead of hanging.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // snippet
    task run_phase(uvm_phase phase);
      phase.raise_objection(this, "TEST: job");
      fork begin                                // isolates disable fork (see below)
        fork
          run_job_and_wait_irq();
          begin #50us; `uvm_error("TIMEOUT", "no interrupt within 50 us") end
        join_any
        disable fork;
      end join
      phase.drop_objection(this, "TEST: job done");  // reached on every path
    endtask

    disable fork kills every child of the current process, not only the fork above it. Wrapping it in its own fork begin ... end join limits it to the two threads you meant. Without the wrapper, it would also kill any thread the task forked earlier.

    Sequences as raisers

    A sequence can own its own duration, which is useful for a long background sequence started from a virtual sequence. There are three ways, and only two are safe.

    Pattern

    Works?

    Why

    set_automatic_phase_objection(1) before start()

    Yes (UVM 1.2 and 1800.2)

    UVM raises before pre_start and drops after post_start, on the starting phase.

    Raise in pre_start, drop in post_start, with get_starting_phase() and a null check

    Yes

    pre_start and post_start run on every start of the sequence.

    Raise in pre_body, drop in post_body

    Not always

    pre_body and post_body are skipped when a parent starts the sequence with call_pre_post = 0, which the uvm_do macros do.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // snippet
    task pre_start();
      uvm_phase p = get_starting_phase();
      if (p != null) p.raise_objection(this, "SEQ: bg traffic");
    endtask
    
    task post_start();
      uvm_phase p = get_starting_phase();
      if (p != null) p.drop_objection(this, "SEQ: bg traffic done");
    endtask

    get_starting_phase() returns null when the sequence was started from code that never set a phase, such as a sub-sequence started inside a parent's body. The null check makes the same sequence usable in both places.

    In IEEE 1800.2: the starting_phase member is removed. It was already deprecated in UVM 1.2. Use get_starting_phase() and set_starting_phase(), or set_automatic_phase_objection(1); all three work in both versions.

    Watch it run: a test that never raised

    Delete the raise_objection line from dma_smoke_test and run it. The run phase ends at time 0, before a single APB write. The scoreboard's checks are the only thing that notices:

    Diagram
    UVM_ERROR dma_tb.sv(407) @ 0: uvm_test_top.env.sb [SB_EMPTY] no words were compared: the test checked nothing
    UVM_INFO dma_tb.sv(411) @ 0: uvm_test_top.env.sb [SB] 0 words matched, 0 mismatched
    UVM_ERROR :    1

    Without SB_EMPTY, this log would read UVM_ERROR : 0, and the test would be green. Every scoreboard that can compare zero items should fail when it does.

    What breaks: the sub-sequence that let go too early

    A team's long-running background sequence raised in pre_body and dropped in post_body. Started directly from a test, it worked. Then someone moved it into a virtual sequence that launched it with a uvm_do-style macro, and regressions started passing in a fraction of their usual time, with coverage collapsing. The trace showed no SEQ raise at all.

    The macro starts the child with call_pre_post set to 0, so pre_body and post_body never ran. With no raise from the sequence and the virtual sequence finishing quickly, the test dropped and the phase ended under the background traffic. Moving the pair to pre_start and post_start, and later to set_automatic_phase_objection(1), fixed it. The lesson generalises: put an objection where the code is guaranteed to run, not where it usually runs.

    "What happens if you drop an objection you never raised? And if you raise one in build_phase?"

    A weak answer: "Both are ignored with a warning." Interviewers ask this precisely because many people believe it.

    A strong answer: "The drop is a UVM_FATAL, OBJTN_ZERO, because the count would go below zero, so the simulation stops. The raise in build_phase is a UVM_ERROR, UVM/PH/NULL_OBJECTION, because function phases have no objection. And the related trap: a test that never raises in run_phase does not hang; it ends at time 0, which is why scoreboards fail on zero compares."

    Follow-ups: Where should a sequence raise? (pre_start/post_start or set_automatic_phase_objection, never pre_body). Two raises, one drop? (the count stays at 1 and the phase hangs until the timeout).

    Check yourself A test raises once, starts a sequence, and on an error path calls return before its drop. What does the log show?

    The phase never ends on that path. uvm_top's total stays at 1 until the global timeout fires a PH_TIMEOUT fatal. On the success path the test passes, so the bug appears only when the error path is taken, which is exactly when you most need a clean log. One drop after the work, reached on every path, fixes it.

    Check yourself A colleague 'fixes' an OBJTN_ZERO fatal by deleting the second drop. The fatal goes away. Is the bug fixed?

    Not necessarily. OBJTN_ZERO means one path drops twice, or drops without raising. Deleting a drop can leave another path with a raise and no drop, which turns a fatal into a hang. Find the path that double-drops, usually an error branch that drops and then falls through to the normal drop.

    Check yourself Your run_phase raises, forks three stream sequences with join_none, and drops. How long does the test run?

    About zero time. join_none returns at once, so the drop follows the raise in the same time step, uvm_top's total reaches zero and the phase ends while the three sequences are still starting. Use join, or wait fork before the drop.

    Next: even a perfectly paired objection can drop a moment too early, while the last word is still inside the DUT. set_drain_time Deep Dive covers the grace period after the last drop.

    Related topics

    • Objection hierarchy propagation

    • set_drain_time deep dive

    • Runtime sub-phases

    Previous

    34 / 280

    Next: set_drain_time Deep Dive: Grace Period After Objections Clear

    Up next

    Up nextset_drain_time Deep Dive: Grace Period After Objections Clear

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • phase_ready_to_end: Last-Chance Phase Extension
    • End-of-Test Strategy: Choosing a Mechanism
    • Objections at Scale