A coverage model starts in the spec, not in code. Scenarios move from the spec into a numbered plan, and only then into covergroups whose bins map one-to-one onto plan items. The worked example is a small ALU.
Item | Scenario |
|---|---|
CP-1 | Every opcode executed at least once |
CP-2 | Zero flag seen asserted and deasserted |
CP-3 | Carry seen asserted for ADD and for SUB |
CP-4 | Operands |
CP-5 | Every opcode executed with a zero result |
CP-6 | Shift by 0 and shift by 7 for SHL |
Each plan item is a sentence a non-programmer can check off. The covergroup makes every item mechanically measurable by name.
The next listing is not run in this course's checks: the open simulators do not count covergroups. The bench at the end of the lesson counts the same bins with the course's bins model, listed on the Bins Model page of this topic.
// not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; the bins model counts it
typedef enum logic [2:0] { ADD, SUB, AND_, OR_, XOR_, SHL } op_e;
class alu_coverage;
covergroup cg with function sample(op_e op, bit [7:0] a, b, y,
bit zero, bit carry);
option.per_instance = 1;
option.name = "alu_cov";
// CP-1: enum gives one named bin per opcode, automatically
cp_op : coverpoint op;
// CP-2
cp_zero : coverpoint zero {
bins asserted = {1};
bins deasserted = {0};
}
// CP-3: carry is meaningful only for ADD and SUB
cp_carry : coverpoint carry iff (op inside {ADD, SUB}) {
bins carry_set = {1};
bins carry_clear = {0};
}
// CP-4: operand extremes
cp_a_ext : coverpoint a { bins min_v = {0}; bins max_v = {255}; }
cp_b_ext : coverpoint b { bins min_v = {0}; bins max_v = {255}; }
// CP-5: every opcode with a zero result
x_op_zero : cross cp_op, cp_zero {
ignore_bins nz = binsof(cp_zero.deasserted);
}
// CP-6: shift amount extremes (b[2:0] is the shift count)
cp_shamt : coverpoint b[2:0] iff (op == SHL) {
bins sh0 = {0};
bins sh7 = {7};
}
endgroup
function new();
cg = new();
endfunction
endclassOne coverpoint or cross per plan item, with bins named after the item. The iff guards keep the model measuring only meaningful combinations.
The filtered cross holds 6 bins: 6 opcodes times the asserted half of cp_zero. Only lower-case and, or and xor are keywords, so AND would also be legal; the underscore in AND_ is just a readability choice.
Before IEEE 1800-2023 a covergroup could not be extended; 2023 adds covergroup extends, but tool support varies. A covergroup also travels badly on its own. The standard reuse unit is a wrapper class: it owns construction, names each instance, and exposes a stable sampling API.
The next listing is not run in this course's checks: the open simulators do not count covergroups. The bench at the end of the lesson counts the same bins with the course's bins model, listed on the Bins Model page of this topic.
// not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; the bins model counts it
class alu_cov_collector;
covergroup cg (string inst_name) with function sample(op_e op, bit zero);
option.per_instance = 1;
option.name = inst_name; // one report row per instance
cp_op : coverpoint op; // 6 named bins, one per opcode
cp_zero : coverpoint zero {
bins asserted = {1};
bins deasserted = {0};
}
endgroup
function new(string inst_name);
cg = new(inst_name); // covergroup new() takes arguments
endfunction
function void sample_txn(op_e op, bit zero);
cg.sample(op, zero);
endfunction
function real get_pct();
return cg.get_inst_coverage();
endfunction
endclass
// Merged reporting hides weak instances; per-instance reporting does not:
// cpu_cov = new("cpu_port"); -> report row "cpu_port"
// dma_cov = new("dma_port"); -> report row "dma_port"Each collector object owns one covergroup instance with its own report row. One strong port can no longer mask a weak one.
The wrapper gives construction control, reuse across testbenches, and a stable sample_txn API.
Per-instance reporting plus distinct names keeps multi-port coverage honest.
A covergroup type may also be declared at package scope. The sampling context is what limits reuse, which is why the wrapper class still wins.
Argument plumbing, value versus ref formals, and instance merging are detailed in the Options topic.
The same plan counted with the bins model, from 2000 random transactions and then a directed top-up:
// uses: covmodel
// The ALU plan counted with the bins model: random stimulus first, then the directed top-up.
typedef enum int {ADD, SUB, AND_, OR_, XOR_, SHL} op_e;
class alu_coverage;
cm_group cg = new("alu_cov");
cm_point cp_op = new("cp_op"), cp_zero = new("cp_zero"), cp_carry = new("cp_carry");
cm_point cp_a_ext = new("cp_a_ext"), cp_b_ext = new("cp_b_ext"), cp_shamt = new("cp_shamt");
cm_cross x_op_zero;
function new();
cp_op.add("add", ADD, ADD); cp_op.add("sub", SUB, SUB); cp_op.add("and_", AND_, AND_);
cp_op.add("or_", OR_, OR_); cp_op.add("xor_", XOR_, XOR_); cp_op.add("shl", SHL, SHL); // CP-1
cp_zero.add("deasserted", 0, 0); cp_zero.add("asserted", 1, 1); // CP-2
cp_carry.add("carry_clear", 0, 0); cp_carry.add("carry_set", 1, 1); // CP-3
cp_a_ext.add("min_v", 0, 0); cp_a_ext.add("max_v", 255, 255); // CP-4
cp_b_ext.add("min_v", 0, 0); cp_b_ext.add("max_v", 255, 255);
cp_shamt.add("sh0", 0, 0); cp_shamt.add("sh7", 7, 7); // CP-6
x_op_zero = new("x_op_zero", cp_op, cp_zero); // CP-5
x_op_zero.exclude("", "deasserted"); // ignore_bins nz
cg.points = '{cp_op, cp_zero, cp_carry, cp_a_ext, cp_b_ext, cp_shamt};
cg.crosses.push_back(x_op_zero);
endfunction
function void sample(op_e op, bit [7:0] a, b, bit zero, carry);
cp_op.sample(op); cp_zero.sample(zero); x_op_zero.sample();
if (op inside {ADD, SUB}) cp_carry.sample(carry); // iff (op inside {ADD, SUB})
cp_a_ext.sample(a); cp_b_ext.sample(b);
if (op == SHL) cp_shamt.sample(b[2:0]); // iff (op == SHL)
endfunction
endclass
after 2000 random transactions:
covergroup alu_cov: 97.62% (goal 100)
coverpoint cp_op: 100.00% (6 of 6 bins)
add 339
sub 316
and_ 371
or_ 343
xor_ 315
shl 316
coverpoint cp_zero: 100.00% (2 of 2 bins)
... 6 more lines ...
min_v 5
max_v 7
coverpoint cp_b_ext: 100.00% (2 of 2 bins)
min_v 8
max_v 6
coverpoint cp_shamt: 100.00% (2 of 2 bins)
sh0 39
sh7 47
cross x_op_zero: 83.33% (5 of 6 bins)
hole: <or_, asserted>
after the directed top-up: 100.00%
PASS: random stimulus left named holes, and six directed cases closed the model at 100%The report reads like the plan: every coverpoint is full, and the one hole is a named cross bin, <or_, asserted>. OR gives zero only when both operands are zero, 1 in 65,536 for uniform operands, so 2000 random transactions almost never produce it. The group sits at 97.62%, the mean of its seven members. Six directed cases close it.
Extract scenarios from the spec into a numbered, reviewable plan.
Implement one coverpoint or cross per plan item, with bins named after the item.
Run random stimulus, then query get_inst_coverage() and dump the database.
Read holes as missing scenarios, not missing percentage.
Close holes with constraint tweaks or small directed cases, then rerun.
A wrapper owns construction, names each instance and exposes one sample_txn call, so any testbench can reuse it and each port gets its own report row.
Counted with the course's bins model (rich/coverage/cov_model.sv, listed on The Bins Model page) 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.
Random stimulus left named holes, and six directed cases closed the model at 100%.