Two stories from the same week. In the first, a test drops its objection as soon as the last input word is accepted; the scoreboard compares five words out of six, reports no error, and the test passes with a word still inside the DMA. In the second, a hung test hits its timeout and dies with a fatal; the regression dashboard shows no PASS/FAIL banner for it at all, because the banner was printed in report_phase, which never ran. One is a check that looked at the wrong thing, the other a verdict that lived in the wrong phase. What exactly has to be true before a test may pass, and where does each check belong?
A restaurant does not close when the last customer walks in; it closes when every order has been served, every bill is paid, nothing is left on the pass, and the till has been counted. And a night with zero customers is not a great night with no complaints; it is a reason to ask whether the doors were locked. A test's end is that audit. Run-time checks catch wrong items as they happen. End-of-test checks catch what can only be known when traffic has stopped: things still owed, things never seen, and the suspicious absence of anything at all.
Check | Why it matters | Where it lives | In the bench |
|---|---|---|---|
Nothing unexpected arrived | An extra output is as wrong as a missing one | At once, in |
|
Owed work was delivered | The DUT was asked for N words; the test may end before they come out |
|
|
Expected queues are empty | A prediction left over is output that never appeared |
|
|
Something was compared | Zero compares proves nothing |
|
|
No outstanding IDs or partial frames | A response or a last fragment never came |
| Per-ID leftovers in the AXI scoreboard |
Interrupts handled, status clean | A pending interrupt or a sticky error bit is work the test ignored | The test, before dropping its objection |
|
Monitors not mid-transfer | A transfer cut off by the end of the test was never published | The monitor's | A good addition to any monitor |
Two rules follow from the table. Waiting and judging are different jobs: phase_ready_to_end decides whether the run phase may end yet; check_phase decides whether what happened was right. And monitors never raise objections: a monitor only observes, so end of test is owned by the test and, when it must wait for outstanding items, by a scoreboard through phase_ready_to_end.
// 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 scoreboard learns the job size from the observed LEN and GO writes, adds it to owed, and counts output words down. When the run phase's objection count reaches zero while words are still owed, phase_ready_to_end raises an objection and forks a thread that drops it when the count reaches zero. The mechanism itself, including the iteration cap and the get_imp() comparison, is taught in phase_ready_to_end.
+UVM_TESTNAME=dma_drain_test
UVM_INFO dma_tb.sv(411) @ 375000: uvm_test_top.env.sb [SB] 6 words matched, 0 mismatched
UVM_ERROR : 0check_phase is a function phase: it takes no time, runs after the run phase (and every runtime phase) has finished, and visits components bottom-up. It is the right home for any check whose answer is only final when traffic has stopped. It is the wrong home for anything that could have been reported earlier: an unexpected word reported at end of test has lost its time stamp.
report_phase is where the bench prints 8 words matched, 0 mismatched. Keep it for summaries. Two facts about the report server matter for verdicts. First, it counts every severity, warnings included: every run on this machine shows UVM_WARNING : 2 from the library's own notes about running without DPI, and the scratch latency run above showed 4. The UVM_COUNT action only decides which messages count toward the quit count (set_max_quit_count), not which are counted. Second, it prints the summary of those counts even when a fatal ends the run.
A UVM_FATAL ends the simulation at once. The library calls pre_abort() on the components, prints the report summary and finishes; extract, check, report and final never run. dma_timeout_test, which waits for an interrupt that is never enabled, shows it:
+UVM_TESTNAME=dma_timeout_test
UVM_FATAL uvm_phase_hopper.svh(598) @ 20000000: reporter [PH_TIMEOUT] Explicit timeout of 20000000 hit, indicating a probable testbench issue
UVM_FATAL : 1
[PH_TIMEOUT] 1There is no [SB] ... words matched line: the scoreboard's report_phase never ran, and neither did its check_phase. Anything that must appear for every test, pass or fail, therefore cannot live there. The robust verdict is the report summary itself (regression scripts decide on UVM_ERROR : and UVM_FATAL :), and when you need a custom verdict line, put it in an override of the report server's report_summarize or in a test's pre_abort() as well as in report_phase.
Some tests legitimately compare nothing. dma_zero_len_test starts a job with LEN = 0 to close a coverage hole: the DMA must report DONE at once with COUNT = 0, and no data moves. SB_EMPTY would fail it. The test does not disable the check, does not remove it from the scoreboard, and does not raise the global error budget. It demotes one ID, on one component, in one test, with a comment saying why:
// snippet: from dma_zero_len_test
// No data moves, so the scoreboard's "nothing was compared" error is expected
// here, and only here: demote that one ID on that one component, on purpose.
function void end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
env.sb.set_report_severity_id_override(UVM_ERROR, "SB_EMPTY", UVM_INFO);
endfunction+UVM_TESTNAME=dma_zero_len_test
UVM_INFO dma_tb.sv(1718) @ 275000: uvm_test_top [ZERO] zero-length job finished at once with DONE
UVM_INFO dma_tb.sv(407) @ 325000: uvm_test_top.env.sb [SB_EMPTY] no words were compared: the test checked nothing
UVM_INFO dma_tb.sv(411) @ 325000: uvm_test_top.env.sb [SB] 0 words matched, 0 mismatched
UVM_ERROR : 0
UVM_FATAL : 0The waiver is visible: the message still prints, now as UVM_INFO, so anyone reading the log sees that the check fired and was waived. The test replaces the waived check with checks of its own (DONE set, ERR clear, COUNT = 0), so it still proves something. That is the standard for any exception: narrow, commented, visible in the log, and replaced by a check that fits the case.
This is the first opening story, and it happened while the bench was being built. The first end-of-test scoreboard decided whether to extend the run phase by looking at its expected queue: if predictions were waiting, words were still owed. dma_drain_test dropped its objection in the same timestep the last input word was accepted, before the input monitor had published that word. At that instant the queue looked empty. Nothing was extended, the run phase ended, and the last word was never predicted and never compared. Five of six words matched, no leftovers, UVM_ERROR : 0.
The fix is in the code above: the scoreboard learns how many words each job owes from the observed LEN and GO register writes, and holds the phase until they have all come out. The principle generalises to every end-of-test check: "nothing pending" must come from what the DUT was asked to do, not from what the testbench happens to have seen so far. A queue can only tell you about predictions already made.
SB_LEFTOVER: 1 on some seeds only, always on the last word of the last job. What is the likely cause, and the fix?The test drops its objection before the last word comes out of the DUT, and on some seeds the DUT is a little slower. The leftover is real in the sense that the word was never compared. The fix is not a longer fixed delay but end-of-test ownership: a scoreboard that knows how many words are owed (from LEN and GO) and holds the run phase in phase_ready_to_end until they arrive, as dma_scoreboard_eot does.
*** TEST PASSED *** in report_phase when the error count is zero, and their dashboard treats a missing banner as "not run". What goes wrong on a timeout?The timeout is a fatal, so report_phase never runs and no banner prints. The dashboard files a hung test as "not run" rather than as a failure. Decide pass and fail from the report summary's UVM_ERROR and UVM_FATAL counts, which print even after a fatal, or emit the verdict from a report-server report_summarize override.
SB_EMPTY from the scoreboard. What do you suggest instead?Keep the check and waive it narrowly in that one test: set_report_severity_id_override(UVM_ERROR, "SB_EMPTY", UVM_INFO) on that scoreboard only, with a comment saying why, as dma_zero_len_test does. Then add the checks that fit the test (register read-backs, status). Removing the check from the scoreboard would let every other test that accidentally compares nothing pass.
Next: the model inside dma_scoreboard has done its job for this topic. Reference Models takes it out into a predictor component of its own, calls a C model through DPI-C, and shows when the library's built-in comparators help and when they lie.