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.
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.
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.
// 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
endclassdma_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.
// 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
endclassThe 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.
The real trace of dma_drain_test, with objection tracing on (a few lines for intermediate levels trimmed):
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 : 0At 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.
The first version of this scoreboard did not count register writes. It held the phase while its expected-word queue was non-empty:
// 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 ...
endfunctionSymptom: 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.
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.
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.
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.
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.