Objections and premature completion
Keep the run phase open exactly as long as work is in flight, and catch the tests that end too early.
- Lesson
- U1.3
- Time
- 25 min
- Runs on
- Verilator with UVM
- Check
- 6 questions
A new test starts a 200-item sequence in its run_phase, and the regression marks it passed. Its log shows the report summary at time 0 with no errors, and no item ever reached the design. Nothing raised an objection, so UVM ended the run phase in the first time step and killed the sequence before it sent anything.
The sequence would have sent an item every 10 ns, but the run phase was already over.
An objection keeps the run phase open
The run phase has a counter of objections. phase.raise_objection(this) adds one, and phase.drop_objection(this) takes one away. While the count is above zero, someone still has work to do, so the phase stays open.
When the count returns to zero, the run phase ends, after an optional wait called the drain time. UVM then kills every run_phase task that is still running, such as the driver's and monitor's forever loops. Extract, check and report follow at that same moment.
If nobody raises an objection at all, the count is zero from the start. The run phase ends in the first time step, after the run tasks have started but before any of them waits for a clock. Every check then runs against an empty scoreboard, and an empty scoreboard has no mismatches.
Raise before the work and drop after it
No objection
task run_phase(uvm_phase phase);
three_items seq = three_items::type_id::create("seq");
seq.start(env.sqr);
endtask
The run phase ends at 0 ns and kills the sequence. Nothing is sent, nothing is checked, and the test passes.
Raise and drop around the sequence
task run_phase(uvm_phase phase);
three_items seq = three_items::type_id::create("seq");
phase.raise_objection(this, "sending 3 items");
seq.start(env.sqr);
phase.drop_objection(this, "sequence done");
endtask
The phase stays open while the sequence runs and can end once it returns.
Raise the objection before the first statement that waits, and drop it after the last one. The string argument is a description that appears when you trace objections, so make it say what the work is.
With tracing on, a raise or drop prints one line for the component that made it, with the description and two numbers. count is the objections that component raised itself, and total adds those raised below it. Then it prints one line for each parent whose total changes, ending at uvm_top. The example prints these lines, so you can watch the count go from 1 to 0.
Each raise must be matched by a drop from the same component. A drop with no matching raise stops the test with a fatal error, and a raise with no drop keeps the test alive forever.
Who should raise the objection
The test usually raises it, because the test knows what stimulus it intends to run. A top-level sequence can do the same job for its own body.
Drivers and monitors normally do not object. They run forever, so an objection raised in their loops would either never drop or flip on and off with every item. Raising and dropping per item also slows a large simulation.
A scoreboard is the one checker that may object, because it knows when results are still owed. The section after the drain-time trap shows how it does that without objecting on every item.
Objections add up the tree. When the test raises one, its own count becomes 1, and the total at uvm_top, the hidden root above the test, also becomes 1. The run phase ends when the total at the root reaches zero, wherever in the tree the objections came from.
The drain-time trap
The right-hand version above still has a hole. The sequence returns when the driver has finished the last item, but the design may still be working on it. In the example, the incrementer takes three clock cycles to produce each result.
Results arrive at 45, 65 and 85 ns. Without a drain time, the run phase ends at 60 with two results still inside the design.
Without a drain time, the run phase ends at 60 ns. Two results are still in the pipeline, the monitor is killed before it sees them, and the scoreboard has checked one item out of three. If nothing complains about the two missing results, the test passes.
A drain time makes UVM wait after the count reaches zero. phase.get_objection().set_drain_time(this, 50ns); keeps the phase open until 110 ns, so all three results are checked. If anyone raises an objection during the drain, the wait is cancelled and starts again at the next drop.
Drop and trust it
phase.raise_objection(this);
seq.start(env.sqr);
phase.drop_objection(this);
// check_phase: no check for leftovers
Ends at 60 ns with two results unchecked, and passes.
Drain, and fail on leftovers
phase.get_objection().set_drain_time(this, 50ns);
phase.raise_objection(this);
seq.start(env.sqr);
phase.drop_objection(this);
// check_phase: uvm_error if expected.size() != 0
Ends at 110 ns with all three checked; a too-short drain still fails loudly.
A drain time is a guess about the slowest response. If a later design change makes the pipeline deeper, the same guess silently becomes too short. So pair it with a check_phase that reports an error for every expected item that never arrived. Better still, have the test wait until the scoreboard has nothing outstanding before it drops its objection.
Let the scoreboard hold the end
- The last drop60 ns (illustrative)
The sequence returns and the test drops. The total reaches 0, so UVM calls
phase_ready_to_endon every component before it ends the phase.More context
The first result, y=11, was checked at 45 ns. The other two are still inside the three-stage pipeline when the sequence returns.
Follow the calls, values and waits
- call test drops: total 0: Test → run objection
- call phase_ready_to_end: run objection → Scoreboard
- Results are owed: raise60 ns (illustrative)
Two results are still expected, so the scoreboard raises the objection again and starts a thread that waits for its queue to empty.
More context
The raise happens inside the hook, in the same time step as the drop, so the total never stays at 0 long enough for the phase to end.
Follow the calls, values and waits
- call sb raises: total 1: Scoreboard → run objection
- Waiting: Scoreboard — Owes 2 results
- The second result65 ns (illustrative)
The monitor publishes y=21. The scoreboard checks it and now owes one result.
More context
Nothing happens to the objection here: the forked thread is still waiting for
expected.size() == 0.Follow the calls, values and waits
- data y=21: Pipeline → Monitor
- data y=21: Monitor → Scoreboard
- Waiting: Scoreboard — Owes 1 result
- Released: Scoreboard
- The last result, then the drop85 ns (illustrative)
y=31 arrives and the queue is empty. The thread drops at 85 ns; UVM asks again, nothing is owed, and the run phase ends at 85 ns with 3 checked.
More context
The second call to the hook finds
expectedempty and returns without raising. With the 50 ns drain instead, the same bench ended at 110 ns.Follow the calls, values and waits
- data y=31: Pipeline → Monitor
- data y=31: Monitor → Scoreboard
- call sb drops: total 0: Scoreboard → run objection
- call phase_ready_to_end: run objection → Scoreboard
- Released: Scoreboard
Read the diagram as text
- Test: Drops when its sequence returns
- run objection: Total at the root
- Scoreboard: expected queue: owed results
- Pipeline: Three cycles of latency
- Monitor: Publishes each result
- Test → run objection: drop_objection
- run objection → Scoreboard: phase_ready_to_end
- Scoreboard → run objection: raise / drop_objection
- Pipeline → Monitor: y at out_valid
- Monitor → Scoreboard: write(result)
The end of the test follows the last result at 85 ns, not a guessed drain time.
phase_ready_to_end is a function every component inherits. UVM calls it each time a phase's objection total reaches zero. If a component raises the objection inside it, the phase stays open; when the total reaches zero again, UVM calls it again. After 20 calls UVM stops asking, but an objection that is never dropped still holds the phase until the timeout.
UVM also calls the hook when nothing ever objected, and once for every phase; the name check keeps the raise to the run phase. In the opening 0 ns failure nothing is owed yet, so this hook cannot replace the test's objection.
function void phase_ready_to_end(uvm_phase phase);
if (phase.get_name() != "run" || expected.size() == 0) return;
phase.raise_objection(this, "results owed");
fork begin
wait (expected.size() == 0);
phase.drop_objection(this, "results owed");
end join_none
endfunctionCompare this with the drain time. The drain waited a fixed 50 ns and ended at 110 ns; this waits exactly as long as results are owed. If a result never comes, though, the wait never finishes, so keep a timeout and the check_phase leftover check. When the bench cannot know how many results to expect, [U7.3](U7-3-uvm-scoreboard-drain.html) adds a quiet window instead.
Read the count, or let the root sequence object
The objection is an object you can query while the test runs. It returns the same count and total that the trace lines print.
| Call | What it does | Measured on this lesson's bench |
|---|---|---|
phase.raise_objection(this, "why", n) | Adds n (default 1) to this component's count; the description appears in traces | Raised once by the test at 0 ns |
obj.get_objection_count(this) | Objections this object raised itself | 1 for the test |
obj.get_objection_total(uvm_root::get()) | Count plus everything below; the phase ends when the root total is 0 | 1 at the root |
obj.display_objections() | Prints the tree of objectors with both numbers | Shown below |
obj.set_drain_time(this, t) | Waits t after the total reaches 0 | 50 ns in the Try it example |
obj is phase.get_objection(). Every call takes the component or sequence that objects.
The test objects for the sequence
phase.raise_objection(this, "sending 3 items");
seq.start(env.sqr);
phase.drop_objection(this, "sequence done");
The test owns the objection; it must remember to drop it on every path. Measured on the same hooked bench: run ends at 85 ns.
The root sequence objects for itself
seq.set_starting_phase(phase);
seq.set_automatic_phase_objection(1);
seq.start(env.sqr);
start() raises before pre_start and drops after post_start. Measured on the bench with the scoreboard's phase_ready_to_end hook and no drain time. All three results are checked by 85 ns.
The total objection count is 1
Source Total
Count Count Object
0 1 uvm_top
0 1 uvm_test_top
0 1 env
0 1 sqr
1 1 seqUse the automatic objection on a root sequence, the one the test starts, so the run lasts exactly as long as its body. Call both setters before start. Without set_starting_phase, the sequence has no phase to object to. Measured: the run ends at 0 ns with 0 sent and 0 checked, no warning and no error.
Child sequences started inside a root sequence do not need their own objection. The root already holds the phase open until every child returns, as long as its body() waits for them (no fork ... join_none left running).
The opposite failure: a test that never ends
A raise without a drop, or a drop that sits behind a wait that never finishes, keeps the run phase open forever. UVM has a global timeout of 9,200 seconds of simulated time, after which it reports a fatal error. With a free-running clock, reaching that time can take hours of real time.
Set a timeout that fits your test, for example uvm_root::get().set_timeout(1ms); before run_test. A hang then becomes a fatal error with a clear message instead of a stuck regression slot.
Try it
The test turns on objection tracing, sets a 50 ns drain time and raises an objection around the sequence. The trace lines show the raise at 0, the drop at 60 and the end of the drain at 110, and the scoreboard reports all three results checked.
Delete the set_drain_time line and run again. The scoreboard now reports two results that never arrived, as a UVM_ERROR. Then delete the raise and drop as well, and watch the test pass at time 0 having checked nothing.
UVM_INFO @ 0: run_objection [OBJTN_TRC] Object uvm_test_top raised 1 run_objection objection(s) (sending 3 items): count=1 total=1 UVM_INFO @ 60: run_objection [OBJTN_TRC] Object uvm_test_top dropped 1 run_objection objection(s) (sequence done): count=0 total=0 UVM_INFO @ 110: run_objection [OBJTN_TRC] Object uvm_test_top all_dropped 1 run_objection objection(s) (sequence done): count=0 total=0 UVM_INFO main.sv(109) @ 110: uvm_test_top.env.sb [SB] 3 sent, 3 checked, 0 outstanding UVM_ERROR : 0Check yourself
Remember
- The run phase stays open while its objection count is above zero; with no objection it ends at time 0.
- The test raises an objection before its stimulus and drops it after; drivers and monitors normally do not object.
- Dropping as soon as the sequence returns can end the run while results are still inside the design.
- A drain time is a guess; pair it with a
check_phasethat fails on outstanding items. - Set a timeout so that a missing drop fails quickly instead of hanging.