The full product almost always contains combinations the design or the environment makes impossible. Left in the cross, those cells sit at zero hits forever and cap the achievable percentage below 100%.
ignore_bins removes such cells from the denominator. illegal_bins goes further: it removes them from the goal and fires a runtime error if a sample ever lands there, turning the cross into a checker.
The listing below is not run in this course's checks: the open simulators do not count covergroups, so the lesson's bench counts it with the bins model.
// not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; the bins model counts it
covergroup err_cg @(posedge clk iff txn_done);
cp_dir : coverpoint is_write { bins rd = {0}; bins wr = {1}; }
cp_err : coverpoint err_mode { bins none = {2'b00};
bins parity = {2'b01};
bins crc = {2'b10}; }
x_dir_err : cross cp_dir, cp_err {
// Environment fact: error injection only exists on the write path.
// read x parity and read x crc are IMPOSSIBLE -> remove from goal.
ignore_bins rd_no_inject =
binsof(cp_dir.rd) && (binsof(cp_err.parity) || binsof(cp_err.crc));
}
endgroupnone | parity | crc | |
|---|---|---|---|
rd | Reachable | Unreachable: ignored | Unreachable: ignored |
wr | Reachable | Reachable | Reachable |
Before pruning the goal is 6 cells with 4 reachable, so the best possible score is 4 of 6, 66.7%. After ignore_bins the goal is the 4 reachable cells, and 100% is possible.
Cross coverage percentage is hit cells divided by goal cells. Pruning changes the denominator, so identical simulation results read very differently before and after.
Same regression, same hits | Hit | Goal | Coverage |
|---|---|---|---|
Without pruning | 4 | 6 | 66.7%: looks like a hole problem |
With | 4 | 4 | 100%: honest closure |
The 33.3% gap was never a stimulus gap. It was a modeling bug: the coverage model demanded combinations the design cannot produce.
Hole triage therefore starts by asking whether a cell is reachable, before asking which test hits it. An unreachable cell belongs in ignore_bins with a comment citing the spec line that forbids it, not in a directed-test backlog.
No. The denominator shrank from 6 to 4 when two unreachable cells moved to ignore_bins. The 33.3% gap was a modeling artifact, never a stimulus gap.
Use illegal_bins when a combination is forbidden by the spec rather than merely outside the plan. A hit means something is broken, in the DUT or in the testbench, and the simulator reports the failing combination at the offending sample.
The listing below is not run in this course's checks, for the same reason.
// not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; the bins model counts it
x_mode_size : cross cp_mode, cp_size {
// Spec 4.3.1: WRAP bursts must be 2, 4, 8 or 16 beats.
// WRAP x size-1 is a protocol violation, not a coverage hole.
illegal_bins wrap1 = binsof(cp_mode.wrap) && binsof(cp_size.b1);
// Environment limitation (not a spec violation): exclude silently.
ignore_bins no_fixed_long =
binsof(cp_mode.fixed) && binsof(cp_size.b16);
}Aspect | ignore_bins | illegal_bins |
|---|---|---|
Goal effect | Cell leaves the denominator | Cell leaves the denominator |
Hit at runtime | No error, by the standard | Simulator raises a runtime error |
Use when | The combination cannot or need not happen | The combination must never happen |
Review needs | Comment with the spec or environment reason | Comment with the spec section number |
The decision rule fits one line: if a hit means a bug gets filed, it is illegal_bins. If the response would be a shrug, it is ignore_bins. Either way, uncommented pruning is the first thing a coverage review flags.
The bench counts both crosses from this lesson with the bins model: the direction-by-error cross with and without the ignore_bins, then the mode-by-size cross with its illegal WRAP × 1 cell and a last sample that lands there.
4 of 6 without pruning and 4 of 4 with it, from the same hits. The WRAP × 1 sample is reported at once and adds nothing to the mode-by-size score, which reads 7 of 7.
// uses: covmodel
// Pruning a cross: the same hits before and after ignore_bins, then an illegal cell as a checker.
module tb_cross_prune;
cm_point cp_dir = new("cp_dir"), cp_err = new("cp_err"), cp_mode = new("cp_mode"), cp_size = new("cp_size");
cm_cross x_raw, x_pruned, x_mode_size;
initial begin
cp_dir.add("rd", 0, 0); cp_dir.add("wr", 1, 1);
cp_err.add("none", 0, 0); cp_err.add("parity", 1, 1); cp_err.add("crc", 2, 2);
x_raw = new("x_dir_err (no pruning)", cp_dir, cp_err);
x_pruned = new("x_dir_err (pruned)", cp_dir, cp_err);
// ignore_bins rd_no_inject = binsof(cp_dir.rd) && (binsof(cp_err.parity) || binsof(cp_err.crc));
x_pruned.ignore(x_pruned.sel_and(x_pruned.sel("rd", ""), x_pruned.sel_or(x_pruned.sel("", "parity"), x_pruned.sel("", "crc"))));
// A regression that hits every reachable combination: error injection exists on writes only.
begin
int dirs[6] = '{0, 0, 1, 1, 1, 1};
int errs[6] = '{0, 0, 0, 1, 2, 1};
foreach (dirs[i]) begin cp_dir.sample(dirs[i]); cp_err.sample(errs[i]); x_raw.sample(); x_pruned.sample(); end
end
x_raw.report(); x_pruned.report();
// Spec 4.3.1: WRAP bursts are 2, 4, 8 or 16 beats, so WRAP x size-1 must never happen.
cp_mode.add("incr", 0, 0); cp_mode.add("wrap", 1, 1); cp_mode.add("fixed", 2, 2);
cp_size.add("b1", 1, 1); cp_size.add("b4", 4, 4); cp_size.add("b16", 16, 16);
x_mode_size = new("x_mode_size", cp_mode, cp_size);
x_mode_size.illegal(x_mode_size.sel("wrap", "b1")); // illegal_bins wrap1
x_mode_size.ignore(x_mode_size.sel("fixed", "b16")); // ignore_bins no_fixed_long: the bench never sends it
begin
int modes[8] = '{0, 0, 0, 1, 1, 2, 2, 1};
int sizes[8] = '{1, 4, 16, 4, 16, 1, 4, 1}; // the last sample is WRAP x 1
foreach (modes[i]) begin cp_mode.sample(modes[i]); cp_size.sample(sizes[i]); x_mode_size.sample(); end
end
x_mode_size.report();
if (x_raw.n_bins() != 6 || x_raw.n_covered() != 4 || x_pruned.get_coverage() != 100.0 || x_mode_size.n_bins() != 7 ||
x_mode_size.get_coverage() != 100.0 || x_mode_size.illegal_hits != 1) $fatal(1, "FAIL: pruning math");
$display("PASS: the same hits read 4 of 6 unpruned and 4 of 4 pruned; the illegal WRAP x 1 was reported");
$finish;
end
endmodule cross x_dir_err (no pruning): 66.67% (4 of 6 bins)
hole: <rd, parity>
hole: <rd, crc>
cross x_dir_err (pruned): 100.00% (4 of 4 bins)
[cov] x_mode_size: illegal cross bin <wrap, b1> hit
cross x_mode_size: 100.00% (7 of 7 bins)
PASS: the same hits read 4 of 6 unpruned and 4 of 4 pruned; the illegal WRAP x 1 was reportedignore_bins with an environment-limitation comment, or nothing at all. illegal_bins would turn legal DUT behavior into a simulation failure.
Counted with the course's bins model (rich/coverage/cov_model.sv, listed on the Coverage hub) on Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown. Verilator 5.048 parses covergroups with a COVERIGN warning and ignores their bins, and Icarus 13 has none, so each covergroup listing is marked not run and its bench counts the same bins with the model.
The same six samples read 4 of 6 unpruned and 4 of 4 pruned, and the WRAP × 1 sample was reported as an illegal cross bin.