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 Hierarchy Propagation: Up the Component Tree

Part 2/Phases & LifecycleIntermediate

Objection Hierarchy Propagation: Up the Component Tree

Where objection counts live, how one raise reaches uvm_top, what source and total mean, and a runnable model of the counter.

On this page

    A colleague adds a background traffic sequence to your DMA regression. Overnight, every test runs into the timeout. You add a print to the test: get_objection_count(this) says 0. The test has dropped its objection, the bench looks idle, and yet the run phase will not end. Something is holding it open, and the number you printed cannot tell you what.

    This lesson shows where UVM really keeps objection counts, how a single raise travels up the tree, and which query names the holder. By the end you will read a display_objections() table the way you read a stack trace.

    The idea: every level keeps its own tally

    Think of an office building at closing time. Every room keeps a tally of who is inside. Every floor's tally includes all of its rooms, and the front desk's tally includes every floor. The guard locks up only when the front-desk tally reads zero. When it reads 1, the guard does not search every room: they walk down through the floors whose tallies are not zero until they reach the room with someone in it.

    UVM works the same way. Each task phase (run_phase and the twelve runtime phases) owns one objection: a uvm_objection object you reach with phase.get_objection(). It keeps two numbers for every object that has taken part:

    • the source count, the raises that object made itself, returned by get_objection_count(obj);

    • the total count, its own raises plus those of all its descendants, returned by get_objection_total(obj).

    A raise adds to the raiser's source and total counts, then to the total of its parent, its grandparent and so on up to uvm_top (the uvm_root singleton). At every level it passes, UVM calls that component's raised() callback. A drop walks the same path down. The phase can end only when uvm_top's total reaches zero. A sequence is not a component, so its parent for this purpose is the sequencer it runs on: a sequence's raise climbs through its sequencer, agent and env.

    The picture

    Tree from uvm_top down through the test, env, agent and sequencer to a sequence. The sequence and the test each have source 1; uvm_top has total 2.
    Two raisers: the test and a background sequence on the input agent's sequencer. A raise climbs these arrows in reverse, from the raiser to uvm_top; every node it passes counts it in its total, and only the raiser counts it as source.

    Now the opening mystery is easy. get_objection_count(this) on the test returns its source count, which is 0 after its drop. The test's total is still 1, because the background sequence below it never dropped. The right question was get_objection_total(), and the right tool for finding the holder is the table below.

    Build it: the smoke test's one objection

    The bench's dma_smoke_test is the simplest correct owner: it raises before its first blocking statement, does the whole job, and drops once at the end. The #100ns before the drop is a hand-sized settle delay; the drain-time lesson replaces it with something better.

    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
    // snippet: from the course DMA bench
    // Program KEY and LEN, start a job with the interrupt enabled, stream one
    // packet of LEN words, wait for the interrupt, then check and clear STATUS.
    class dma_smoke_test extends dma_base_test;
      `uvm_component_utils(dma_smoke_test)
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      task run_phase(uvm_phase phase);
        bit [31:0] status, key = 32'hA5A5_0F0F;
        int unsigned n_words = 8;
        axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
    
        phase.raise_objection(this, "smoke test running");
        reg_write(KEY, key);
        reg_write(LEN, n_words);
        reg_write(CTRL, 32'h3);                  // IRQ_EN | GO
    
        if (!pkt.randomize() with { n == n_words; })
          `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
        pkt.start(env.in_agt.sqr);
    
        wait (irq_vif.irq === 1'b1);
        reg_read(STATUS, status);
        if (status[1] !== 1'b1) `uvm_error("STATUS", $sformatf("DONE not set: STATUS=%08h", status))
        reg_write(STATUS, 32'h2);                // write 1 to clear DONE
        reg_read(STATUS, status);
        if (status[2:0] !== 3'b000) `uvm_error("STATUS", $sformatf("STATUS not clear: %08h", status))
        #100ns;                                  // let the last output word drain
        phase.drop_objection(this, "smoke test done");
      endtask
    endclass

    When you need to know who holds a phase, ask the objection object. These three calls are the whole debugging API:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    // snippet
    uvm_objection obj = phase.get_objection();
    // Source: raises this component made itself. Total: itself plus every descendant.
    `uvm_info("OBJ", $sformatf("test source=%0d total=%0d root total=%0d",
      obj.get_objection_count(this), obj.get_objection_total(this),
      obj.get_objection_total()), UVM_LOW)   // no argument means uvm_top
    obj.display_objections();                // table of every object with a non-zero total

    Watch it run

    Here is the smoke test with objection tracing turned on (+UVM_OBJECTION_TRACE on the command line, or phase.get_objection().trace_mode(1)), from the real bench. Times are in ps:

    Diagram
    UVM_INFO @ 0: run_objection [OBJTN_TRC] Object uvm_test_top raised 1 run_objection objection(s) (smoke test running): count=1  total=1
    UVM_INFO @ 0: run_objection [OBJTN_TRC] Object uvm_top added 1 run_objection objection(s) to its total (raised from source object uvm_test_top, smoke test running): count=0  total=1
    UVM_INFO @ 655000: run_objection [OBJTN_TRC] Object uvm_test_top dropped 1 run_objection objection(s) (smoke test done): count=0  total=0
    UVM_INFO @ 655000: run_objection [OBJTN_TRC] Object uvm_test_top all_dropped 1 run_objection objection(s) (smoke test done): count=0  total=0
    UVM_INFO @ 655000: run_objection [OBJTN_TRC] Object uvm_top subtracted 1 run_objection objection(s) from its total (dropped from source object uvm_test_top, smoke test done): count=0  total=0
    UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
    • count is the source count of the object named on that line; total is its total. The test has count=1 total=1; uvm_top has count=0 total=1, because it holds nothing itself.

    • The drop at 655 ns walks the same path down, and all_dropped marks the moment a node's total reached zero (after its drain time, which is 0 here).

    • The scoreboard's report line carries the same time stamp: the run phase ended at the instant uvm_top's total hit zero, and extract, check and report ran at 655 ns.

    For a hang, the snapshot is more useful than the trace. This is display_objections() from the bench's dma_timeout_test, taken 1 µs before its timeout:

    Diagram
    UVM_INFO uvm_objection.svh(1319) @ 19000000: run_objection [UVM/OBJ/DISPLAY] The total objection count is 1
    Source  Total
    Count   Count   Object
    0       1       uvm_top
    1       1         uvm_test_top

    Read it top-down like the guard: uvm_top's total is 1, so follow the indentation to the first row with a non-zero source count. That row is the holder, here the test itself, still waiting for an interrupt that was never enabled.

    Sequences show up too. In a variant of the bench where the test also starts the input packet sequence with set_automatic_phase_objection(1), the same call taken mid-packet shows the sequence's raise climbing through its sequencer:

    Diagram
    UVM_INFO uvm_objection.svh(1319) @ 170000: run_objection [UVM/OBJ/DISPLAY] The total objection count is 2
    Source  Total
    Count   Count   Object
    0       2       uvm_top
    1       2         uvm_test_top
    0       1           env
    0       1             in_agt
    0       1               sqr
    1       1                 pkt

    A model you can run

    The model below is plain SystemVerilog, not the UVM library. It keeps the same two counts per node as uvm_objection, propagates raises and drops to the root, reports a drop below zero as the library's OBJTN_ZERO fatal, prints a display_objections-style table, and shows what set_propagate_mode(0) changes. It is the one model of the counter in this course; other lessons link here.

    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
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    // A plain-SV model of uvm_objection bookkeeping (not the UVM library).
    class obj_node;
      string   name;
      obj_node parent;          // a sequence's parent is its sequencer
      int      source = 0;      // get_objection_count(this node)
      int      total  = 0;      // get_objection_total(this node)
      function new(string name, obj_node parent = null);
        this.name = name;
        this.parent = parent;
      endfunction
    endclass
    
    class objection_model;
      obj_node top;
      obj_node nodes[$];
      bit      prop_mode = 1;   // 1: every ancestor (UVM default); 0: source and top only
      int      fatals = 0;      // drops below zero (OBJTN_ZERO is a fatal in UVM)
      int      visits = 0;      // raised()/dropped() calls made, a proxy for cost
    
      function new();
        top = new("uvm_top");
        nodes.push_back(top);
      endfunction
    
      function obj_node add(string name, obj_node parent);
        obj_node n = new(name, parent);
        nodes.push_back(n);
        return n;
      endfunction
    
      function void walk(obj_node src, int delta);
        obj_node n = src;
        forever begin
          n.total += delta;
          visits++;
          if (n == top) break;
          if (prop_mode && n.parent != null) n = n.parent;
          else n = top;
        end
      endfunction
    
      function void raise(obj_node src, int count = 1);
        src.source += count;
        walk(src, count);
      endfunction
    
      function void drop(obj_node src, int count = 1);
        if (count > src.source) begin
          fatals++;
          $display("[OBJTN_ZERO] (a UVM_FATAL in the library) %s attempted to drop the count below zero", src.name);
          return;
        end
        src.source -= count;
        walk(src, -count);
      endfunction
    
      function void display();
        $display("The total objection count is %0d", top.total);
        foreach (nodes[i])
          if (nodes[i].total > 0) $display("  %-6d %-6d %s", nodes[i].source, nodes[i].total, nodes[i].name);
      endfunction
    endclass
    

    What breaks: the watchdog that could never bark

    A team had a hang watchdog in their base test. Every 100 µs it read phase.get_objection().get_objection_count() and, if the number had not changed for ten samples, printed the holders and failed the test. It never fired. Tests that hung still burned the farm's full time limit.

    The call has no argument, so it asks about uvm_top, and it returns the source count: the raises uvm_top made itself. That is zero in every bench, all the time. The watchdog was watching a number that could not move. A second version walked the component tree calling get_objection_count(comp) on each one. It found raisers that were components, but it could never see a sequence's raise: the sequence is not in a component walk, and its sequencer's own count is 0.

    The fix was two lines: watch get_objection_total() (uvm_top's total, the number that decides the phase), and when it stalls, call display_objections(), which lists every object with a non-zero total, sequences included. The rule to remember: count is what I raised; total is what my subtree holds.

    "How does UVM know the run phase is finished?"

    A weak answer: "When the objection count goes to zero." Which count? Whose? The interviewer will keep pushing until it is clear the candidate thinks there is one global counter.

    A strong answer: "Each task phase has a uvm_objection. A raise adds to the raiser's source and total counts and to the total of every ancestor up to uvm_top, calling raised() on each. A sequence propagates through its sequencer. When uvm_top's total reaches zero, the drain time runs, then phase_ready_to_end is called on every component; if nobody re-raises, the phase ends. To find a holder I use display_objections(), not get_objection_count(), because count is only the object's own raises."

    Follow-ups to expect: What does set_propagate_mode(0) change? (only the source and uvm_top are updated; the middle levels see no totals or callbacks; covered in Objections at Scale). A sequence raises: which component's total shows it? (its sequencer's, then every ancestor's).

    Check yourself Two agents each raise once, then one drops. What do get_objection_count(env) and get_objection_total(env) return, and does the phase end?

    Count is 0, because the env raised nothing itself. Total is 1, because one agent below it still holds. uvm_top's total is also 1, so the phase stays alive until the second agent drops.

    Check yourself Your sequence calls get_starting_phase().raise_objection(this). In display_objections(), under which component does it appear?

    Under the sequencer it was started on. UVM's objection uses the sequencer as a sequence's parent, so the sequence shows as a source of 1 and the sequencer, agent, env, test and uvm_top each show it in their totals.

    Check yourself A hang dump shows uvm_top with total 1, and every component, uvm_test_top included, with total 0. Who is holding the phase?

    An object that is neither a component nor a sequence running on a sequencer, for example a sequence started with a null sequencer, or a raise made on some other uvm_object. UVM gives such objects uvm_top as their parent, so the raise shows only in uvm_top's total and in no component's. display_objections() lists the object by name; a component walk never finds it.

    Next: a count that only goes up and down sounds simple, but most hangs and false passes come from a handful of counting mistakes. Objection Counting Rules lists them.

    Related topics

    • Objection counting rules

    • Objections at scale

    • Objection triage and hang forensics

    Previous

    33 / 280

    Next: Objection Counting Rules: Raises, Drops, and Edge Cases

    Up next

    Up nextObjection Counting Rules: Raises, Drops, and Edge Cases

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • 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