dma_truncate_test passes, and its scoreboard line says 4 words matched, 0 mismatched. A new team member is puzzled: the packet was cut in half, so why did the scoreboard not complain? Then a sharper question: suppose the DMA had a bug and ignored the early TLAST. What in this bench would fail? Only the test's own if (status[2] !== 1'b1) line, which lives in one test and nowhere else. Who in a testbench should know that a fault is expected, and how should they find out?
There are two ways for a checker to know a fault is coming. It can be told: the test or sequence sends a "fault planned" flag to the scoreboard by some side channel. Or it can see: it predicts the DUT's error response from the same monitored traffic it already uses to predict data. A football referee who blows the whistle because they saw the foul is trustworthy; one who waits for a coach to tell them a foul happened is not, because the coach's plan and what happened on the pitch can differ.
The told approach has exactly that weakness. If the injection silently failed (the override was registered late, a callback was never attached), the flag still says "fault planned", the DUT behaves normally, and the scoreboard dutifully reports a missed fault that the DUT never received, or worse, forgives a real error at the wrong moment. The seen approach checks the DUT against what actually reached its pins. Keep the told approach for faults no monitor can observe, such as a memory word corrupted through a backdoor.
dma_scoreboard predicts each output word from an input word seen by the input monitor. The truncating driver dropped beats 4 to 7 before the pins, so the monitor saw four beats, the scoreboard made four predictions, the DMA moved four words, and all four matched. That is correct behaviour: the data checker's question is "did the DUT transform what it received correctly?", and the answer was yes. The error response, ERR instead of DONE, is a separate question that the data checker was never asked.
The DMA's rule is simple enough to predict exactly: after GO, if an input beat with TLAST arrives before LEN beats, the job ends in ERR; otherwise it ends in DONE. The scoreboard already receives every APB transfer and every input beat, so the extension needs only a little state. It extends dma_scoreboard, which declares its write_* methods virtual precisely so that a derived scoreboard's versions are the ones the analysis imps call.
// snippet: scratch run alongside the DMA bench
class dma_err_scoreboard extends dma_scoreboard;
`uvm_component_utils(dma_err_scoreboard)
int unsigned len_reg, beats_in;
bit job_open, err_expected;
int unsigned n_err_seen, n_done_seen;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void write_apb(apb_item t);
super.write_apb(t); // KEY tracking stays in the base class
if (t.slverr) return; // a rejected access changes nothing
if (t.write && t.addr == LEN) len_reg = t.data[15:0];
if (t.write && t.addr == CTRL && t.data[0]) begin // GO: a new job starts
beats_in = 0; err_expected = 0; job_open = 1;
end
if (!t.write && t.addr == STATUS && job_open && !t.rdata[0]) begin
check_status(t.rdata); // first STATUS read after the job ended
job_open = 0;
end
endfunction
virtual function void write_in(axis_item t);
super.write_in(t); // the data prediction is unchanged
if (!job_open) return;
beats_in++;
if (t.last && beats_in < len_reg) err_expected = 1; // the spec's ERR rule, seen on the pins
endfunction
function void check_status(bit [31:0] st);
bit [1:0] exp = err_expected ? 2'b10 : 2'b01; // {ERR, DONE}
if (st[2:1] !== exp)
`uvm_error("SB_STATUS", $sformatf("STATUS=%08h, expected %s", st, err_expected ? "ERR" : "DONE"))
else if (err_expected) n_err_seen++;
else n_done_seen++;
endfunction
function void report_phase(uvm_phase phase);
super.report_phase(phase);
`uvm_info("SB_ERR", $sformatf("%0d job(s) ended in ERR as predicted, %0d in DONE as predicted",
n_err_seen, n_done_seen), UVM_LOW)
endfunction
endclassNotice what is not here: no flag from the test, no knowledge of which driver or callback did the cutting. The scoreboard would predict ERR just the same for a short packet from a sequence, a truncating override, a callback, or a DUT upstream in a subsystem that really sent a short packet. A test turns it on with one more type override, dma_scoreboard::type_id::set_type_override(dma_err_scoreboard::get_type()).
The same structure extends to the interrupt: the expected level after a job is IRQ_EN & (DONE | ERR), so with the interrupt agent connected, the scoreboard can also check that the line rose after the job and fell after the W1C write.
A scratch test overrides both the stream driver (truncating) and the scoreboard. Job 1 has LEN = 8 and is cut after beat 3; job 2 has LEN = 3, shorter than the cut, so it runs clean:
UVM_INFO dma_tb.sv(520) @ 275000: uvm_test_top.env.in_agt.drv [TRUNC] TLAST forced on beat 3
UVM_INFO dma_tb.sv(411) @ 825000: uvm_test_top.env.sb [SB] 7 words matched, 0 mismatched
UVM_INFO scratch_tb.sv(368) @ 825000: uvm_test_top.env.sb [SB_ERR] 1 job(s) ended in ERR as predicted, 1 in DONE as predicted
UVM_ERROR : 07 words matched: 4 from the cut job and 3 from the clean one. Data checking is untouched.
1 ERR and 1 DONE, both as predicted: the scoreboard knew which job should fail without being told, and it would have flagged a DUT that reported DONE for job 1 or ERR for job 2.
Every fault test sorts what happened into three buckets. Counting each one separately is what keeps the bench from forgiving real bugs.
What the DUT did | Predicted | Verdict |
|---|---|---|
Error response | Error | Tolerated: counted, not an error |
Error response | No error | Surprising: an unexpected error, test fails |
Normal response | Error | Missed fault: a DUT bug in its error detection, test fails |
Normal response | No error | Normal compare applies |
Forgive by prediction, never by quota. A scoreboard that tolerates "up to two errors per test" will forgive a real bug in a clean job as long as the planned fault happened to be missed.
On this bench, a DMA that ignored the early TLAST would not report DONE. It would wait for words 5 to 8, which the truncating driver dropped, so no interrupt would ever come and run_job would wait forever. Missed faults in a DUT frequently show up as a hang rather than a wrong status, which is why every fault test needs a bounded run: uvm_root::get().set_timeout(...) in the base test, as dma_timeout_test shows, turns the hang into a PH_TIMEOUT fatal with a time stamp.
Some injected faults make other checkers complain, correctly: a protocol assertion or a VIP monitor flags the violation you injected on purpose. Do not switch those checkers off for the whole test. Demote exactly the expected message, only while the fault is active, and count it, so the test fails if the expected error did not appear:
// snippet: demote one expected error ID and count it
class expect_err_catcher extends uvm_report_catcher;
string id_to_catch;
int unsigned caught;
function new(string name = "expect_err_catcher");
super.new(name);
endfunction
function action_e catch();
if (get_severity() == UVM_ERROR && get_id() == id_to_catch) begin
set_severity(UVM_INFO); // expected: keep the message, drop the error
caught++;
end
return THROW;
endfunction
endclass
// In the test, for a monitor whose protocol check reports AXIS_EARLY_LAST:
// c.id_to_catch = "AXIS_EARLY_LAST"; uvm_report_cb::add(env.in_agt.mon, c);
// In check_phase: if (c.caught == 0) `uvm_error("EXPECT", "the injected fault was never flagged")A team had a large fault-injection suite built on the told approach: each fault sequence set a fault_planned bit in a shared object, and the scoreboard skipped every check on a transaction that carried it, data and status alike. After a refactor, the factory override that installed the error-injecting driver was registered one line too late, after the env was built. Every fault test kept passing for a month.
Nothing was injected, so the DUT behaved normally; and since the scoreboard skipped all checks on planned transactions, a normal response to a planned fault was never examined. The tests could not fail. The redesign did three things this lesson teaches: the scoreboard predicted error responses from observed traffic, so a normal response where the pins showed a fault became an error; a missed fault became its own failure instead of a skipped check; and each fault test asserted that the fault was seen on the pins (a count from the input monitor), not merely planned.
dma_err_scoreboard in place, what does the run report?All eight beats are driven, the input monitor sees no early TLAST, so the scoreboard predicts DONE, and the DMA reports DONE: the scoreboard is satisfied. The test's own ERR check fails, and so would a check that the cut was seen on the pins. Predicting from observation never blames the DUT for a fault it never received; catching a broken injection is the job of the "fault seen on the pins" check.
dma_err_scoreboard predict, and is it right?DONE. When the TLAST beat arrives, beats_in is 4, which is not less than len_reg, so err_expected stays 0. That matches the DUT, where last_word takes priority over s_tlast and the job ends with DONE.
A read while BUSY is set reflects a job still running, so DONE and ERR are legitimately both clear; checking then would report false errors. Checking only the first read after the job ends ties the check to the job's outcome, and job_open stops later reads (after the W1C clear) from being compared against the old prediction.
Next: fault injection completes the core agent topics. Agent patterns covers the shapes senior interviews probe next: reactive slaves, reset on the fly, interrupt agents and pipelined drivers.