Merge dma_check_test, dma_zero_len_test and the closure test from the closure lesson, and both coverpoints of job_cg are complete: every LEN class has been run, and so have both IRQ_EN values. The cross len_x_irq sits at 6 of 12. A manager reads that as "half the combinations untested". A designer says half of them do not matter.
Both could be right, and the only way to settle it is the plan. This lesson is about crosses, about pruning them to the cells that carry risk, and about the bookkeeping that ties every bin to a plan row.
A cross records which combinations of coverpoint bins were sampled together. Think of a café menu: knowing that every drink and every pastry was sold at some point does not tell you that anyone bought the breakfast deal. Bugs live in combinations because designers write the special cases separately: the zero-length path, the interrupt path. Whether they work together is a separate question.
A cross of two coverpoints has one cross bin (a cell) per pair of their bins: cp_len has 6 bins and cp_irq has 2, so len_x_irq has 12 cells. Values removed by ignore or illegal bins on a coverpoint never reach the cross. Inside a cross, binsof(cp) selects cells by the bins of one coverpoint, intersect {...} narrows that to bins whose values overlap a set, and ignore_bins or named bins use those selections to prune or name cells.
cp_len bin | irq_en = 0 (auto[0]) | irq_en = 1 (auto[1]) |
|---|---|---|
zero | hole | dma_zero_len_test |
one | hole | dma_check_test (len 1) |
few (2-4) | hole | dma_check_test (len 3) |
many (5-15) | hole | dma_check_test (len 10), closure (len 8) |
max_pkt (16) | closure (len 16, polled) | hole |
over (17+) | hole | closure (len 20) |
Six of twelve. The only IRQ_EN = 0 job ran with LEN = 16. The tally says irq_en=0 is covered; the cross says that one polled job never met the lengths where the interrupt logic behaves differently. That is exactly the information a cross exists to give.
The interrupt is irq = IRQ_EN && (DONE || ERR). LEN matters to it in only two ways: a zero-length job sets DONE in the GO cycle, so the interrupt rises at once, and a one-word job finishes on its first beat. Every longer job ends the same way. So the plan row "interrupt behaviour versus job length" needs zero, one and a normal length, each with IRQ_EN on and off: 6 cells, not 12.
Two equivalent styles. The first keeps automatic cells and removes the ones the plan does not need; the second names every wanted cell and drops the rest. Both are variants of job_cg, not bench code:
// snippet: plan-pruned crosses for job_cg (variants, not in the bench)
cp_irq: coverpoint irq { bins off = {0}; bins on = {1}; }
// Style 1: remove cells the plan does not need
len_x_irq: cross cp_len, cp_irq {
ignore_bins no_irq_effect = binsof(cp_len) intersect {[2:4], 16, [17:$]};
}
// Style 2: name the cells, ignore everything else (portable)
len_x_irq_named: cross cp_len, cp_irq {
bins zero_off = binsof(cp_len.zero) && binsof(cp_irq.off);
bins zero_on = binsof(cp_len.zero) && binsof(cp_irq.on);
bins one_off = binsof(cp_len.one) && binsof(cp_irq.off);
bins one_on = binsof(cp_len.one) && binsof(cp_irq.on);
bins many_off = binsof(cp_len.many) && binsof(cp_irq.off);
bins many_on = binsof(cp_len.many) && binsof(cp_irq.on);
ignore_bins rest = !binsof(cp_len.zero) && !binsof(cp_len.one) && !binsof(cp_len.many);
}In style 1, binsof(cp_len) intersect {[2:4], 16, [17:$]} selects the few, max_pkt and over bins, and every cell built from them is ignored. In style 2, a cross bin defined with bins groups everything its expression selects into one bin, which is why each named cell spells out both coverpoints. The final ignore_bins rest removes any automatic cell left over. Some teams set option.cross_auto_bin_max = 0 for the same effect, but tool support for that setting varies, and the negated binsof works everywhere.
Traceability is a table that a reviewer can check line by line. Rows come from the spec page's corner-case list; the middle column is the model element; the last column is what the bench's tally prints.
Plan row | Model element | Tally items |
|---|---|---|
Data is word ^ KEY across LEN classes |
| len=0 … len>16 |
Interrupt only when enabled, versus job length |
| irq_en=0, irq_en=1 (no cross items) |
Early TLAST ends the job with ERR |
| tlast=early, outcome=err |
DONE and ERR are W1C |
| clear=done, clear=err (no item for clear_both) |
Input gaps of every size |
| gap=0, gap=1-3, gap=4+ |
ERR survives GO |
| none |
Backpressure loses nothing | none in | none |
KEY and LEN ignored while busy | none | none |
The tally exists because this course's simulator ignores coverpoints, but it earns its place on any simulator: one grep-able line per test that names its holes. It is a projection, though. It has no cross cells and no clear_both, so "9 of 17" is a count of named items, never a covergroup percentage.
// snippet: from dma_coverage in the course DMA bench
function void hit(string item);
plan_hits[item]++;
endfunction
function string len_bin(bit [15:0] len);
if (len == 0) return "len=0";
if (len == 1) return "len=1";
if (len <= 4) return "len=2-4";
if (len <= 15) return "len=5-15";
if (len == 16) return "len=16";
return "len>16";
endfunction
function void report_phase(uvm_phase phase);
string holes = "";
`uvm_info("COV", $sformatf("covergroups: job %0.1f%%, status %0.1f%%, clear %0.1f%%, stream %0.1f%%",
job_cg.get_inst_coverage(), status_cg.get_inst_coverage(),
clear_cg.get_inst_coverage(), stream_cg.get_inst_coverage()), UVM_LOW)
foreach (plan[i]) if (!plan_hits.exists(plan[i])) holes = {holes, " ", plan[i]};
`uvm_info("COV", $sformatf("plan items hit: %0d of %0d", plan_hits.num(), plan.size()), UVM_LOW)
if (holes != "") `uvm_info("COV", {"plan holes:", holes}, UVM_LOW)
endfunction+UVM_TESTNAME=dma_check_test
UVM_INFO dma_tb.sv(1530) @ 1305000: uvm_test_top.env.cov [COV] plan items hit: 9 of 17
UVM_INFO dma_tb.sv(1531) @ 1305000: uvm_test_top.env.cov [COV] plan holes: len=0 len=16 len>16 irq_en=0 outcome=err clear=err gap=0 tlast=early
+UVM_TESTNAME=dma_cmp_trap_test
UVM_INFO dma_tb.sv(1530) @ 655000: uvm_test_top.env.cov [COV] plan items hit: 7 of 17
UVM_INFO dma_tb.sv(1531) @ 655000: uvm_test_top.env.cov [COV] plan holes: len=0 len=1 len=2-4 len=16 len>16 irq_en=0 outcome=err clear=err gap=0 tlast=early
+UVM_TESTNAME=dma_zero_len_test
UVM_INFO dma_tb.sv(1530) @ 325000: uvm_test_top.env.cov [COV] plan items hit: 4 of 17
UVM_INFO dma_tb.sv(1531) @ 325000: uvm_test_top.env.cov [COV] plan holes: len=1 len=2-4 len=5-15 len=16 len>16 irq_en=0 outcome=err clear=err gap=0 gap=1-3 gap=4+ tlast=on-time tlast=earlydma_cmp_trap_test runs one 8-word job, so it misses two more LEN classes than the random test.
dma_zero_len_test hits exactly four items (len=0, irq_en=1, outcome=done, clear=done) and no stream items at all, because no beat moves.
Each log is per test. The regression's answer is the union, which the merge lesson builds.
Look again at the last two rows of the traceability table. "KEY and LEN ignored while busy" is in the spec's corner-case list, and dma_coverage has no item for it. That gap is not hypothetical. The bench's predictor and scoreboard both take a new KEY at once, while the DMA ignores KEY writes during a job; no test writes KEY mid-job, so nothing ever showed it. A scratch run that does gives mismatches in both checkers. Coverage could have pointed straight at the missing test, but only if the row had a bin. As it stands, the tally will one day print 17 of 17 with that row never exercised.
The rule that prevents it: every plan row gets a model element before the regression is trusted, even an empty one, so it can show up as a hole. The opposite failure is the cross explosion: crossing cp_len (6), cp_gap (3), cp_tlast (2) and cp_irq (2) gives 72 cells, many impossible (a zero-length job has no beats, so it has no gap). An unprunable cross is never closed, and an unclosable number teaches the team to stop reading the report.
zero_on (zero-length test), one_on (len 1), many_on (len 10 and len 8). The polled IRQ_EN = 0 job used LEN = 16, which this cross ignores. So 3 of 6, with all three IRQ_EN = 0 cells open, even though the tally reports irq_en=0 as covered.
cp_len gains ignore_bins huge = {[1024:$]};. What happens to len_x_irq?Values in the ignore range never reach the cross, so cells in the over row count only lengths 17 to 1023. A cross is built from the coverpoints' bins after their ignore and illegal bins are applied.
len_x_irq?The tally names only single-coverpoint items (LEN classes, IRQ_EN values and so on). It has no items for cross cells, so cross holes are visible only in a covergroup report from a simulator that collects coverpoints.
Next: the same hits can still produce different percentages. Coverage Options That Change the Numbers shows which settings move the number and which move the hits.