A covergroup has three counting layers. The covergroup is the container and owns the sampling trigger. Each coverpoint inside it watches one value or expression. Each coverpoint holds bins, the named buckets that samples fall into.
Options, crosses and guards shape the count but add no new layer. Coverage rolls up bottom-up: hit bins cover a point, and covered points cover the group.
Layer | Role | Coverage counts |
|---|---|---|
Covergroup | Container with trigger and options | Covered when its points are covered |
Coverpoint | One watched value or expression | Covered when its bins are hit |
Bins | Named buckets for sampled values | Hit counts collected at each sample |
// snippet
covergroup cg_alu @(posedge clk); // the container, with its sampling event
option.per_instance = 1; // options configure it
cp_op : coverpoint opcode { // a coverpoint watches one value
bins add = {OP_ADD}; // a bin is a named bucket
bins sub = {OP_SUB};
bins shifts[] = {OP_SHL, OP_SHR}; // a bin array: one bin per value
}
cp_zero : coverpoint (result == 0); // an expression coverpoint
x_op_zero : cross cp_op, cp_zero; // a cross counts combinations
endgroupThe label in cp_op : coverpoint opcode becomes the coverpoint name in reports and cross references. An unlabeled coverpoint gets an auto-generated name from its expression, which turns brittle when the expression changes. Label every coverpoint.
The expression point cp_zero declares no bins. It still counts: a 1-bit expression gets the two automatic bins for 0 and 1.
A covergroup can be declared in a module, interface, program, package or class. The two working homes are a class collector and a module next to the signals.
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.
// not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; the bins model counts it
// --- In a class: declare AND construct in new() ---
class alu_coverage;
logic [3:0] opcode;
logic zero;
covergroup cg_alu; // embedded covergroup
cp_op : coverpoint opcode;
cp_zero : coverpoint zero;
endgroup
function new();
cg_alu = new(); // MANDATORY: see below
endfunction
function void collect(logic [3:0] op, logic z);
opcode = op;
zero = z;
cg_alu.sample();
endfunction
endclass
// --- In a module: type + explicit instance ---
module alu_cov_mod (input logic clk, input logic [3:0] opcode);
covergroup cg @(posedge clk); // type with clocked sampling
cp_op : coverpoint opcode;
endgroup
cg cg_inst = new(); // explicit instantiation
endmoduleThe class form declares the type and constructs it in new(). The module form declares a clocked type and instantiates it once beside the signals it watches.
Declaring a covergroup creates a handle, not an instance. Without new(), the covergroup does not exist and nothing samples.
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.
// not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; the bins model counts it
class broken_coverage;
logic [3:0] opcode;
covergroup cg;
cp_op : coverpoint opcode;
endgroup
function new();
// BUG: cg = new(); is missing.
// Compiles clean. Nothing here calls cg.sample(), so the
// handle stays null and the group silently measures nothing.
// An explicit cg.sample() on the null handle would usually
// stop the run with a null-object error instead.
endfunction
endclass
class fixed_coverage;
logic [3:0] opcode;
covergroup cg;
option.name = "alu_op_cov"; // name it for the report
cp_op : coverpoint opcode;
endgroup
function new();
cg = new(); // the one-line fix
endfunction
endclassLabel every coverpoint: labels are the names in reports and cross expressions.
Give each instance its own option.name. A constant name is shared by every instance, so pass it in as a covergroup argument with option.per_instance = 1, as the first-model lesson does.
Prefix consistently: cp_ for coverpoints, x_ for crosses.
Name bins after plan scenarios (burst_max), not after raw values (b16).
Reports show an auto-generated name derived from the expression, and cross references turn brittle. A label keeps both stable when the expression changes.
The constructor never ran cg = new(). The handle stayed null and no instance ever sampled. When nothing calls sample() on it, the flow stays quiet; an explicit cg.sample() on the null handle usually fails at runtime instead.
The bench builds cg_alu from the diagram with the bins model, twice: once in a collector whose constructor forgot new(), once in a fixed one. Both receive the same six transactions.
The broken one reports nothing at all: its handle is null, so no sample ever lands. The fixed one shows every opcode bin hit and the holes left in the cross.
// uses: covmodel
// cg_alu from the anatomy diagram, counted with the bins model, and the forgotten new().
typedef enum int {OP_ADD, OP_SUB, OP_SHL, OP_SHR} opcode_e;
class alu_coverage;
cm_group cg; // the covergroup handle
cm_point cp_op, cp_zero;
cm_cross x_op_zero;
function new(bit construct);
if (!construct) return; // the bug: no cg = new()
cg = new("cg_alu");
cp_op = new("cp_op");
cp_op.add("add", OP_ADD, OP_ADD);
cp_op.add("sub", OP_SUB, OP_SUB);
cp_op.add_each("shifts", OP_SHL, OP_SHR); // bins shifts[] = {OP_SHL, OP_SHR};
cp_zero = new("cp_zero"); // coverpoint (result == 0): auto bins 0 and 1
cp_zero.add("auto[0]", 0, 0); cp_zero.add("auto[1]", 1, 1);
x_op_zero = new("x_op_zero", cp_op, cp_zero);
cg.points.push_back(cp_op); cg.points.push_back(cp_zero); cg.crosses.push_back(x_op_zero);
endfunction
function void collect(opcode_e op, bit zero);
if (cg == null) return; // nothing was built, so nothing counts
cp_op.sample(op); cp_zero.sample(zero); x_op_zero.sample();
endfunction
endclass
module tb_cov_anatomy;
alu_coverage broken = new(0);
alu_coverage fixed = new(1);
initial begin
opcode_e ops[6] = '{OP_ADD, OP_SUB, OP_SHL, OP_SHR, OP_ADD, OP_SUB};
bit zs[6] = '{0, 1, 0, 0, 1, 0};
foreach (ops[i]) begin broken.collect(ops[i], zs[i]); fixed.collect(ops[i], zs[i]); end
if (broken.cg == null) $display("broken collector: cg is null, so it measured nothing");
fixed.cg.report();
if (broken.cg != null || fixed.cp_op.get_coverage() != 100.0 || fixed.x_op_zero.n_covered() != 6) $fatal(1, "FAIL: anatomy counts");
$display("PASS: the built covergroup counted 4 opcode bins, 2 zero bins and 6 of 8 crosses; the unbuilt one counted nothing");
$finish;
end
endmodulebroken collector: cg is null, so it measured nothing
covergroup cg_alu: 91.67% (goal 100)
coverpoint cp_op: 100.00% (4 of 4 bins)
add 2
sub 2
shifts[2] 1
shifts[3] 1
coverpoint cp_zero: 100.00% (2 of 2 bins)
auto[0] 4
auto[1] 2
cross x_op_zero: 75.00% (6 of 8 bins)
hole: <shifts[2], auto[1]>
hole: <shifts[3], auto[1]>
PASS: the built covergroup counted 4 opcode bins, 2 zero bins and 6 of 8 crosses; the unbuilt one counted nothingCounted 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 bench samples the same traffic into a built and an unbuilt group: the built one counted 4 opcode bins, 2 zero bins and 6 of 8 cross cells; the unbuilt one counted nothing.