Run the bench's dma_timeout_test and look at the end of the log. There is a PH_TIMEOUT fatal and a report summary, and nothing from the scoreboard: no [SB] ... words matched line at all. Now imagine your regression script decides PASS by looking for a line the test prints in report_phase. What does it do with a log where that phase never ran?
This lesson walks through the four phases that run after run time ends, what each one is for, and how to turn their output into one verdict that a fatal, a demoted error or a silent override cannot fake.
At closing time, a shop does four things in order. It counts the cash in each drawer (extract: collect the final numbers). It reconciles the totals against the receipts (check: decide whether anything is wrong). It writes the day's summary for the owner (report: print the results). Then it locks the doors and turns off the lights (final: release files and handles). Mixing the steps, such as writing the summary before reconciling, is how a shop reports a good day with money missing.
All four are function phases, so they take no simulated time: they run at the time the run phase ended. Extract, check and report run bottom-up, so a parent sees its children's results; final runs top-down.
The bench's scoreboard compares every word live, during run time, and keeps two counts. Its check_phase asks the two aggregate questions that matter at the end: did any prediction go unmatched, and was anything compared at all? Its report_phase prints the counts.
// snippet: from the course DMA bench
// Predicts every output word from the input word and the KEY register the
// DUT was programmed with, then compares in order. The write_* methods are
// virtual so a derived scoreboard (a factory override) can extend them.
class dma_scoreboard extends uvm_scoreboard;
`uvm_component_utils(dma_scoreboard)
uvm_analysis_imp_apb #(apb_item, dma_scoreboard) apb_export;
uvm_analysis_imp_in #(axis_item, dma_scoreboard) in_export;
uvm_analysis_imp_out #(axis_item, dma_scoreboard) out_export;
bit [31:0] key;
bit [31:0] expected[$];
int unsigned n_match, n_mismatch;
function new(string name, uvm_component parent);
super.new(name, parent);
apb_export = new("apb_export", this);
in_export = new("in_export", this);
out_export = new("out_export", this);
endfunction
virtual function void write_apb(apb_item t);
if (t.write && !t.slverr && t.addr == KEY) key = t.data;
endfunction
virtual function void write_in(axis_item t);
expected.push_back(t.data ^ key);
endfunction
virtual function void write_out(axis_item t);
bit [31:0] exp;
if (expected.size() == 0) begin
`uvm_error("SB_UNEXP", $sformatf("output word %08h with nothing expected", t.data))
n_mismatch++;
return;
end
exp = expected.pop_front();
if (t.data !== exp) begin
`uvm_error("SB_MISMATCH", $sformatf("expected %08h, got %08h", exp, t.data))
n_mismatch++;
end else begin
n_match++;
end
endfunction
function void check_phase(uvm_phase phase);
if (expected.size() != 0)
`uvm_error("SB_LEFTOVER", $sformatf("%0d expected word(s) never came out", expected.size()))
if (n_match == 0)
`uvm_error("SB_EMPTY", "no words were compared: the test checked nothing")
endfunction
function void report_phase(uvm_phase phase);
`uvm_info("SB", $sformatf("%0d words matched, %0d mismatched", n_match, n_mismatch), UVM_LOW)
endfunction
endclassCheck does not re-compare anything. Per-item comparison belongs in run time, where the scoreboard has the context (time, ordering) to give a useful message. Check answers questions about the whole run.
When an env or test needs totals from several components, it copies them in extract_phase, reading the components directly through their handles. config_db is for configuration at build time, not for moving results around afterwards.
// snippet
class dma_env_signoff extends dma_env;
`uvm_component_utils(dma_env_signoff)
int unsigned words_checked, words_bad;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void extract_phase(uvm_phase phase);
words_checked = sb.n_match + sb.n_mismatch; // the scoreboard extracted first: bottom-up
words_bad = sb.n_mismatch;
endfunction
endclassUVM already prints a verdict you can trust: the report summary at the end of every run, with a count per severity. The report server counts every severity, warnings included. If a test prints its own PASS or FAIL banner, derive it from those counts, never from a local flag:
// snippet
function void report_phase(uvm_phase phase);
uvm_report_server svr = uvm_report_server::get_server();
int errs = svr.get_severity_count(UVM_ERROR) + svr.get_severity_count(UVM_FATAL);
if (errs == 0) `uvm_info("RESULT", "TEST PASSED", UVM_NONE)
else `uvm_info("RESULT", $sformatf("TEST FAILED (%0d errors)", errs), UVM_NONE)
endfunctionRemember the opening log: a UVM_FATAL ends the simulation at once, so report_phase does not run and this banner never appears. Regression scripts should therefore decide from the summary lines (UVM_ERROR : 0 and UVM_FATAL : 0), and treat a log with no summary as a failure, because it means the simulator crashed or something called $finish early.
The passing smoke test, end of log:
UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
--- UVM Report Summary ---
** Report counts by severity
UVM_ERROR : 0
UVM_FATAL : 0The timeout test, end of log. The scoreboard's report line is missing because report_phase never ran; the summary is still printed:
UVM_FATAL uvm_phase_hopper.svh(598) @ 20000000: reporter [PH_TIMEOUT] Explicit timeout of 20000000 hit, indicating a probable testbench issue
--- UVM Report Summary ---
** Report counts by severity
UVM_ERROR : 0
UVM_FATAL : 1final_phase is the place to close files, flush databases and release handles. It runs top-down, so a parent's final_phase runs before its children's. A test that closes a log file in final_phase while a child still writes to it in its own final_phase will lose those writes. Either the owner of a resource closes it, and children finish writing by report_phase, or the shared file is closed by whatever finalizes last.
A team had one known-benign error from a vendor VIP that fired on every reset. Someone added a report catcher to demote it to a warning, matching on an ID pattern that looked specific enough. Weeks later, a DUT change broke the XOR path for one key value. Tests that hit it stayed green. The catcher's pattern also matched SB_MISMATCH, so every mismatch became a warning, the error count stayed at 0, and the summary said pass.
They found it only because a designer asked why the scoreboard's report line said 3 mismatched in a passing test. The fixes became their standing rules: catchers match an exact ID and message text, every catcher counts what it demotes and reports that count in report_phase, and the regression script cross-checks the scoreboard's mismatch count against the summary.
False-pass source | How it hides an error | Guard |
|---|---|---|
Report catcher demotes or catches an error | The error is counted as a warning or not at all | Exact matches; report the number demoted |
| Messages with no action are not displayed and not counted | Grep the code base for UVM_NO_ACTION |
PASS banner from a local flag | Prints PASS while the server holds errors | Derive the verdict from server counts |
| Phases after run never execute; no summary | Treat a missing summary as a failure |
Scoreboard compared nothing | No mismatches because no compares | An SB_EMPTY-style check in every scoreboard |
TEST PASSED from a bit failed that the scoreboard sets on mismatch. The scoreboard calls uvm_error too. What can go wrong?Two sources of truth. If any other component reports an error (a protocol checker, a timeout in a sequence), the banner still says PASSED because only the scoreboard sets the flag. And after a fatal the banner never prints at all. Derive the banner from the report server's error and fatal counts.
dma_scoreboard fail on zero compares in check_phase rather than in run_phase?Only at the end of run time is it certain that no more words will arrive. In run_phase, zero compares is the normal state at the start of every test. check_phase runs once, after run time, which is exactly when the aggregate question has a final answer.
The agent's write goes to a closed handle and is lost, because final_phase runs top-down: the env finalizes first. Have the agent write by report_phase, or let the component that finalizes last own the close.
Next: everything so far assumed one schedule. Phase domains and sync shows what changes when parts of the bench run their own.