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.
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.
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.
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.
// 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
endclassWhen you need to know who holds a phase, ask the objection object. These three calls are the whole debugging API:
// 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 totalHere 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:
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 mismatchedcount 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:
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_topRead 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:
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 pktThe 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.
// 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
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.
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.
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.
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.