The fault campaign forced eight stuck-at faults into the DMA and the bench caught five. Two of the three misses were not stimulus problems. With DONE stuck at 1, every test still passed, because nothing checks that DONE rises only after the last word. With the output TLAST stuck at 0, every test still passed, because the scoreboard compares data and never looks at TLAST. Both checks are missing. The interesting question is not whether to add them, but where each one belongs.
Every check needs two things: the signals or transactions it reads, and a statement of what "correct" means. A rule about cycles ("PSEL stays high until PREADY") reads pins clock by clock, so it lives next to the pins, in an interface or a checker module. A rule about internal state ("DONE and BUSY are never set together") reads signals that only exist inside the RTL, so it lives in a checker attached to the RTL with bind. A rule about meaning ("the LEN-th output word carries TLAST") needs to know LEN, which word this is, and what the input looked like, so it lives in a component that already keeps that state: the scoreboard.
Think of a building. A smoke detector sits in each room because smoke is local and fast. A structural inspector checks that the floors carry load, which needs drawings and calculations, not a sensor in the ceiling. Both are checks; neither can do the other's job.
Home | Sees | DMA example |
|---|---|---|
Checker bound to the ports (assertion VIP) | Pins, every clock | The APB protocol rules in Assertion VIP and bind |
Checker bound into the RTL | Pins and internal signals | DONE and BUSY never together; an input TLAST only on the LEN-th word |
Monitor | One assembled transaction | A stream beat with TVALID high has no X on TDATA |
Scoreboard or predictor | Transactions from several streams plus register state | Each output word is input XOR KEY; the LEN-th output word carries TLAST |
No monitor sees DONE or BUSY on every cycle. The APB monitor sees STATUS only when a test reads it, which is a few samples per job. A two-line property inside the RTL sees every cycle. The checker below is attached to every dma_dut with bind, so the RTL file is untouched; the port connections resolve inside dma_dut, which is why internal wires such as take and last_word are reachable.
// snippet: the rule checker from the scratch bench (sva_tb.sv)
module dma_rules_chk (
input logic clk, rst_n,
input logic busy, done, err, go, take, s_tlast, last_word,
input logic [15:0] len
);
import uvm_pkg::*;
bit short_pkt_en = 1'b1; // knob: see "Assertion Control from the Test"
// State invariant: a finished job is never still busy.
a_done_not_busy: assert property (@(posedge clk) disable iff (!rst_n)
!(done && busy))
else `uvm_error("DMA_DONE_BUSY", $sformatf("%m: DONE and BUSY both set"))
// Usage rule: the input packet is as long as LEN.
a_full_packet: assert property (@(posedge clk) disable iff (!rst_n || !short_pkt_en)
take && s_tlast |-> last_word)
else `uvm_error("DMA_SHORT_PKT", $sformatf("%m: input TLAST before word LEN=%0d", len))
endmodule
bind dma_dut dma_rules_chk u_rules (
.clk(clk), .rst_n(rst_n), .busy(busy), .done(done), .err(err), .go(go),
.take(take), .s_tlast(s_tlast), .last_word(last_word), .len(len));`a_done_not_busy` is a statement about the design's own state. It does not care which test runs; it is either true on every clock or the RTL is wrong.
`a_full_packet` is different: it is a rule about how the DMA is used. A packet that ends before LEN words is legal for the DUT (it reports ERR), but in a normal test it means the bench sent the wrong thing. Usage rules need a knob, because error-injection tests break them on purpose. The next lesson uses it.
Failures go through `uvm_error`, so they count in the verdict and can be waived and triaged by ID. Lesson 5 covers the reporting choices.
A property could check TLAST too: take && last_word |=> m_tlast. It would catch the stuck-at fault, and it would be the wrong home. It reuses the RTL's own last_word, so if the designer's last_word is off by one, the property and the RTL agree with each other and both are wrong. The scoreboard knows LEN from the observed APB write and counts input words itself, so its idea of "last" is independent of the RTL. It also already keeps one expectation per word, so adding TLAST is a small change:
// snippet: from the scratch bench; the base write_* methods are virtual
class dma_scoreboard_tlast extends dma_scoreboard;
`uvm_component_utils(dma_scoreboard_tlast)
bit [15:0] len;
int unsigned n_in;
bit exp_last[$];
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 && t.addr == LEN) len = t.data[15:0];
if (t.write && !t.slverr && t.addr == CTRL && t.data[0]) n_in = 0;
endfunction
virtual function void write_in(axis_item t);
super.write_in(t);
n_in++;
exp_last.push_back(t.last || n_in == len); // an early input TLAST passes through
endfunction
virtual function void write_out(axis_item t);
if (exp_last.size() != 0) begin
bit e = exp_last.pop_front();
if (t.last !== e)
`uvm_error("SB_TLAST", $sformatf("TLAST=%0b on output word %08h, expected %0b", t.last, t.data, e))
end
super.write_out(t);
endfunction
endclassA test switches the bench to this scoreboard with a type override; nothing else changes. The override works only because dma_scoreboard declares its write_* methods virtual (bench war story 2 is what happens otherwise).
The same eight faults, run against the random test and the truncate test with the checker bound and the TLAST scoreboard in place. Each cell is the error and fatal count and the message IDs that fired:
fault sva_rand_test sva_truncate_knob_test
1 E11 F0 [SB_MISMATCH] 11 E0 F0 (KEY bit 0: test's KEY has bit 0 = 0)
2 E8 F0 [SB_MISMATCH] 8 E2 F0 [SB_MISMATCH] 2
3 E2 F0 [DMA_SHORT_PKT] 1 [STATUS] 1 E0 F0
4 E0 F1 [PH_TIMEOUT] 1 E0 F1 [PH_TIMEOUT] 1
5 E0 F0 E0 F1 [PH_TIMEOUT] 1
6 E57 F0 [DMA_DONE_BUSY] 57 E15 F0 [DMA_DONE_BUSY] 15
7 E3 F0 [SB_TLAST] 3 E1 F0 [SB_TLAST] 1
8 E0 F0 E0 F0
faults detected by at least one test: 7 of 8 (was 5 of 8)Fault 6 is now caught on the first busy cycle of the first job, by the bound invariant. Fifty-seven errors for one stuck flag is its own problem; lesson 5 deals with it.
Fault 7 is caught once per job by SB_TLAST: three jobs, three errors, each naming the word.
Fault 3 (a COUNT bit stuck at 0) was already caught through STATUS, and now also by DMA_SHORT_PKT: the stuck bit corrupts the RTL's last_word, so the input TLAST arrives on a word the DUT does not think is last. An internal rule caught a counter bug it was not written for, which is typical of invariants.
Fault 8 (LEN bit 4) is still missed. No checker can catch a fault the stimulus never reaches: the random lengths stayed below 16. That is a coverage question, and lesson 3 makes the hole visible.
The tempting TLAST property above is a real pattern in industry checkers, and its failure is quiet. A property built from the RTL's own intermediate signals is a second copy of the designer's reasoning, so it fails only when a signal is forced or glitched, which is exactly what a fault campaign does. That makes such properties look strong in a campaign and weak against real bugs, where the designer's reasoning itself is what is wrong. The test for a property is simple: write it from the specification's sentence, using signals the specification names (ports, architectural registers), and use internal signals only to observe something the spec defines, never to decide what is correct.
The opposite mistake is just as common: a scoreboard that re-checks cycle timing ("TDATA was stable while TREADY was low") by keeping previous values in class variables. It duplicates what a two-line property does better, and the two drift apart over time. Cycles go next to the signals; meaning goes where the state is.
take |=> m_tdata == ($past(s_tdata) ^ key). Is that a good home?Partly. It is a correct cycle-level statement and it would catch a corrupted word at once. But it reads the RTL's internal key, so a bug in how KEY is captured (for example the busy-write rule) is invisible to it, and it says nothing about word order across a whole packet. The scoreboard, which predicts from the observed APB write and the observed input, stays the main data check; the property is a cheap extra that localises a failure to the cycle.
No. The random jobs use lengths 1 to 16 and none of these runs had bit 4 set, so the faulty bit never influenced anything. Detection needs stimulus that sets LEN to 16 or more, plus a check that would notice (the scoreboard's leftover check would, because fewer words would come out). The fix belongs in the stimulus and the plan, not in a checker.
a_full_packet need a knob while a_done_not_busy does not?a_done_not_busy is a design invariant: no legal stimulus can make it fail, so any failure is a DUT bug. a_full_packet describes how the bench is supposed to use the DMA. An error-injection test such as the truncate test breaks it on purpose, so that test must be able to switch that one rule off.
Next: the truncate test now fails on DMA_SHORT_PKT, which it causes on purpose. Assertion Control from the Test switches that one rule off, for that one test, without touching the others.