dma_check_test ends with eight plan holes, among them gap=0 (back-to-back beats), len=16, len>16 and outcome=err. On this machine the covergroups print 0.0%, because Verilator ignores coverpoints, so the plan-item tally is the only coverage evidence in the log. The checker you bound in lesson 1 also has covers. Do they agree with the tally, how do their hits reach the same report, and what does a cover tell you about the assertions sitting next to it?
A cover property records that a sequence of signal values happened. It is evidence that the stimulus reached a situation, nothing more: a cover hit is not a pass. When to choose a cover property over a covergroup bin is its own decision, made in SVA cover property vs Covergroups. This lesson is about what happens after the cover is written: getting its hits to wherever the plan is judged, using it to show that each assertion actually had something to check, and making a directed test prove its own scenario happened.
A witness who saw the crime but never testifies does not help the jury. Cover hits that sit in a simulator database nobody merges, or in a log section nobody reads, are that witness.
Use both. The database route is what sign-off reads: commercial simulators record every cover property by hierarchical name next to covergroups, and the vendor's plan tool maps each cover to a plan row and merges hits across the whole regression. The action-block route is what one test can act on during the run: a UVM component can print the hits, add them to the plan-item tally, or fail a directed test whose scenario never happened.
A concurrent cover cannot live in a class, and a class cannot name the checker's hierarchy. A small package shared by both solves it: the cover's pass action calls a package function, and any component can read the package's counters.
// snippet: covers in dma_rules_chk, and the glue that brings their hits into UVM
package sva_glue_pkg; // compiled before the checker and the tests
int unsigned cover_hits[string];
function automatic void cover_hit(string name);
cover_hits[name]++;
endfunction
endpackage
// inside dma_rules_chk:
c_b2b_in: cover property (@(posedge clk) disable iff (!rst_n) take ##1 take)
sva_glue_pkg::cover_hit("b2b_in");
c_len_ge16: cover property (@(posedge clk) disable iff (!rst_n) go && len >= 16)
sva_glue_pkg::cover_hit("len>=16");
c_err: cover property (@(posedge clk) disable iff (!rst_n) $rose(err))
sva_glue_pkg::cover_hit("err");// snippet: from the scratch bench
class sva_cover_report extends uvm_component;
`uvm_component_utils(sva_cover_report)
string required[$] = '{"b2b_in", "len>=16", "err"};
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void report_phase(uvm_phase phase);
string holes = "";
foreach (required[i]) begin
int unsigned n = sva_glue_pkg::cover_hits.exists(required[i]) ? sva_glue_pkg::cover_hits[required[i]] : 0;
`uvm_info("SVA_COV", $sformatf("cover %-8s %0d hit(s)", required[i], n), UVM_LOW)
if (n == 0) holes = {holes, " ", required[i]};
end
if (holes != "") `uvm_info("SVA_COV", {"cover holes:", holes}, UVM_LOW)
endfunction
endclassOne counter per cover, keyed by plan-item name, so the names match the plan rows rather than the hierarchy.
`disable iff (!rst_n)` on every cover, so nothing during reset counts.
The report is per test. Merging across tests is still the database's job; this component answers "what did this run reach?"
+UVM_TESTNAME=sva_rand_test
UVM_INFO sva_tb.sv(98) @ 1305000: uvm_test_top.cov [SVA_COV] cover b2b_in 0 hit(s)
UVM_INFO sva_tb.sv(98) @ 1305000: uvm_test_top.cov [SVA_COV] cover len>=16 0 hit(s)
UVM_INFO sva_tb.sv(98) @ 1305000: uvm_test_top.cov [SVA_COV] cover err 0 hit(s)
UVM_INFO sva_tb.sv(101) @ 1305000: uvm_test_top.cov [SVA_COV] cover holes: b2b_in len>=16 err
UVM_INFO dma_tb.sv(411) @ 1305000: uvm_test_top.env.sb [SB] 14 words matched, 0 mismatched
+UVM_TESTNAME=sva_truncate_knob_test
UVM_INFO sva_tb.sv(98) @ 485000: uvm_test_top.cov [SVA_COV] cover b2b_in 0 hit(s)
UVM_INFO sva_tb.sv(98) @ 485000: uvm_test_top.cov [SVA_COV] cover len>=16 0 hit(s)
UVM_INFO sva_tb.sv(98) @ 485000: uvm_test_top.cov [SVA_COV] cover err 1 hit(s)
UVM_INFO sva_tb.sv(101) @ 485000: uvm_test_top.cov [SVA_COV] cover holes: b2b_in len>=16`b2b_in` is 0 in every run, and so is the tally's gap=0. Two independent measurements, one a cover on the RTL's take and one a covergroup on monitor timing, report the same hole: the stream driver always drops TVALID between beats. When two instruments agree, the hole is in the stimulus.
`len>=16` is 0, matching the tally's len=16 and len>16 holes. It is also why the fault campaign missed LEN bit 4: the cover names the reason for the miss.
`err` is hit only by the truncate test, as expected. In the random test, any assertion about ERR behaviour would have passed without ever being exercised.
An implication A |-> B that never sees A succeeds vacuously on every clock: it reports no failure because it was never asked anything. That is the assertion version of the scoreboard that compared zero words. The defence is the same as the scoreboard's SB_EMPTY check: pair each important assertion with a cover of its antecedent, and treat a zero as a gap. For a_full_packet, the antecedent take && s_tlast happens once per packet, so the random test exercised it three times; for a rule such as "after ERR, BUSY stays low until GO", the random test would have exercised it zero times. Commercial tools report vacuous and non-vacuous success counts per assertion, and the plan should read the non-vacuous count.
Two counting details come up in interviews. A cover property counts at most one success per attempt, while a cover sequence counts every match of every attempt, so a sequence with a range (##[1:4]) can count several times from one start. And a cover with a long range written as [*3:$] matches from many starting clocks, which inflates its count without meaning more.
Look at the truncate knob test again. c_err was hit, and the plan row "a short packet sets ERR" looks covered. In that same run a_full_packet was switched off, which was correct for that test. Now imagine the plan row were "the bench flags packets shorter than LEN": it would be marked covered by a run in which the rule that flags them was disabled. Coverage from a run says what happened; it does not say what was checked during it. Two rules keep this honest. Count coverage only from passing runs, as the plan-ranking script in the industry section does, and for any plan row that names a check, require both a cover hit and the related assertion enabled in that run, which is easy when every checker prints its knob state.
c_err reports 0 hits. What do you conclude, and what would you change in the test?The test did not do what its name says: maybe the truncating driver override did not apply, or the packet was full length. The pass is meaningless for ERR. Make the test check its own scenario: in report_phase (or check_phase), read the cover counter and raise uvm_error if err is 0. A directed test that cannot fail when its scenario is missing is a false pass waiting to happen.
$rose(irq) |-> irq_en has run in 2,000 regressions without a failure. What do you check before quoting that as evidence?The non-vacuous count, or a cover of $rose(irq). If no test ever raised the interrupt (for example because irq_en was always 0), the assertion never had an antecedent to check. Also check it was enabled in those runs, and not killed by a test helper.
c_len_ge16 written on go && len >= 16 rather than on the APB write of LEN?Because the plan item is "a job of 16 or more words ran", not "someone wrote a large LEN". A LEN write while busy is ignored by the DMA, and a LEN written but never followed by GO starts nothing. Covering the moment the job starts, with the value the DUT really holds, measures what the design did, not what the bench sent.
Next: the covers and assertions in this checker are ordinary SVA. Reusing Properties in Formal runs the same file under a formal tool, and brings a counterexample back as a UVM test.