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.
// 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.
// 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 next listing is not run in this course's checks: the open simulators do not count covergroups. It is the complete self-checking testbench to write on a commercial simulator; the bench after it counts the same plan with the bins model.
// 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;
task automatic alu_model(input op_e op, input bit [7:0] a, b,
output bit [7:0] y, output bit carry);
carry = 1'b0;
case (op)
ADD: {carry, y} = {1'b0, a} + {1'b0, b};
SUB: {carry, y} = {1'b0, a} - {1'b0, b};
AND_: y = a & b;
OR_: y = a | b;
XOR_: y = a ^ b;
SHL: y = a << b[2:0];
endcase
endtask
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_op : coverpoint op;
cp_zero : coverpoint zero {
bins asserted = {1};
bins deasserted = {0};
}
cp_carry : coverpoint carry iff (op inside {ADD, SUB}) {
bins carry_set = {1};
bins carry_clear = {0};
}
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}; }
x_op_zero : cross cp_op, cp_zero {
ignore_bins nz = binsof(cp_zero.deasserted);
}
cp_shamt : coverpoint b[2:0] iff (op == SHL) {
bins sh0 = {0};
bins sh7 = {7};
}
endgroup
function new();
cg = new();
endfunction
endclass
module tb;
alu_coverage cov = new();
op_e op;
bit [7:0] a, b, y;
bit zero, carry;
real cov_pct;
task automatic directed(op_e o, bit [7:0] x, bit [7:0] z);
op = o; a = x; b = z;
alu_model(op, a, b, y, carry);
zero = (y == 0);
cov.cg.sample(op, a, b, y, zero, carry);
endtask
initial begin
repeat (2000) begin
assert(std::randomize(op, a, b));
alu_model(op, a, b, y, carry);
zero = (y == 0);
cov.cg.sample(op, a, b, y, zero, carry);
end
// Directed top-up for the CP-5 cells uniform random rarely hits:
directed(OR_, 8'h00, 8'h00); // OR is zero only for a == b == 0 (1 in 65536)
directed(ADD, 8'h01, 8'hFF); // ADD wraps to zero (1 in 256); also b == 255
directed(SUB, 8'hFF, 8'hFF); // SUB of equal values (1 in 256); also a == 255
directed(XOR_, 8'h5A, 8'h5A); // XOR of equal values (1 in 256)
cov_pct = cov.cg.get_inst_coverage();
if (cov_pct >= 99.9)
$display("PASS: ALU coverage closes at %0.1f%%", cov_pct);
else
$fatal(1, "FAIL: ALU coverage stalls at %0.1f%%", cov_pct);
$finish;
end
endmoduleThe 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.
Code coverage is extracted automatically from RTL structure: lines, branches, toggles. It answers whether the implementation ran. Functional coverage is written from the spec. It answers whether required scenarios happened.
Code coverage | Functional coverage | |
|---|---|---|
Source | RTL structure | The specification |
Written | Automatically, by the tool | By the verification engineer |
Measures | Implementation exercised | Scenarios that occurred |
100% means | All written code ran | All planned scenarios hit |
Blind to | Missing code, scenario meaning | Anything not in the coverage model |
Effort | A switch | A plan and covergroups |
The FIFO corner below is the classic proof. Both lines run, yet the killing combination never occurs.
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
module fifo_wr (input logic clk, input logic rst_n,
input logic push, pop, input logic full, input logic empty,
input logic [7:0] wdata, input logic [2:0] wr_ptr,
output logic [2:0] rd_ptr);
logic [7:0] mem [8];
// Line A runs on any normal push; line B on any normal pop.
// Pushes until full plus pops until empty give 100% line,
// toggle and branch coverage: yet push && pop && full NEVER OCCURS.
// BUG: simultaneous push && pop while FULL corrupts the count
// elsewhere in the design.
always_ff @(posedge clk) begin
if (push && !full)
mem[wr_ptr] <= wdata;
if (pop && !empty)
rd_ptr <= rd_ptr + 1;
end
// The functional coverage that names the gap:
covergroup cg_fifo @(posedge clk);
cp_corner : coverpoint {push, pop, full} iff (rst_n) {
bins push_pop_full = {3'b111}; // unhit means the scenario was never tried
}
endgroup
cg_fifo cg_inst = new();
endmoduleThe unhit bin points at the exact stimulus gap. Line coverage had no vocabulary to even ask the question.
Verilator measures code coverage for real, so the claim can be checked. The bench fills a small FIFO, drains it, and samples the corner bin with the bins model:
// uses: covmodel
// A 4-deep FIFO, a test that fills it and drains it, and the one corner both miss.
module fifo4 (
input logic clk, rst_n, push, pop,
input logic [7:0] wdata,
output logic [7:0] rdata,
output logic full, empty
);
logic [7:0] mem [4];
logic [1:0] wp, rp;
logic [2:0] count;
assign full = (count == 4);
assign empty = (count == 0);
assign rdata = mem[rp];
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin wp <= 0; rp <= 0; count <= 0; end
else begin
if (push && !full) begin mem[wp] <= wdata; wp <= wp + 1; end
if (pop && !empty) rp <= rp + 1;
count <= count + (push && !full) - (pop && !empty);
end
endmodule
module tb_fifo_codecov;
logic clk = 0, rst_n = 0, push = 0, pop = 0;
logic [7:0] wdata = 0, rdata;
logic full, empty;
always #5 clk = ~clk;
fifo4 dut (.*);
cm_point cp_corner = new("cp_corner {push, pop, full}");
always @(posedge clk) if (rst_n) cp_corner.sample({push, pop, full});
initial begin
cp_corner.add("push_pop_full", 3'b111, 3'b111);
repeat (2) @(negedge clk);
rst_n = 1;
repeat (5) begin @(negedge clk) push = 1; wdata = wdata + 1; end // fill, and one push while full
@(negedge clk) push = 0;
repeat (5) begin @(negedge clk) pop = 1; end // drain, and one pop while empty
@(negedge clk) pop = 0;
repeat (2) @(negedge clk);
cp_corner.report();
$display("PASS: the fill-and-drain test finished; compare the corner bin with the line coverage of fifo4");
$finish;
end
endmodule coverpoint cp_corner {push, pop, full}: 0.00% (0 of 1 bins) below goal 100
push_pop_full 0 <- hole
PASS: the fill-and-drain test finished; compare the corner bin with the line coverage of fifo4Built a second time with line coverage on (verilator --binary --coverage-line, with the FIFO in its own file fifo4.sv), the same test writes coverage.dat, and verilator_coverage --annotate-min 1 --annotate-all --annotate annot coverage.dat annotates the FIFO. Every line ran, both branches of every if included, while the corner bin above reads 0:
// // verilator_coverage annotation
module fifo4 (
input logic clk, rst_n, push, pop,
input logic [7:0] wdata,
output logic [7:0] rdata,
output logic full, empty
);
logic [7:0] mem [4];
logic [1:0] wp, rp;
logic [2:0] count;
assign full = (count == 4);
assign empty = (count == 0);
assign rdata = mem[rp];
000016 always_ff @(posedge clk or negedge rst_n)
000014 if (!rst_n) begin wp <= 0; rp <= 0; count <= 0; end
000014 else begin
000010 if (push && !full) begin mem[wp] <= wdata; wp <= wp + 1; end
000010 if (pop && !empty) rp <= rp + 1;
000014 count <= count + (push && !full) - (pop && !empty);
end
endmoduleRecorded with Verilator 5.048 on 2026-10-18. The counts at the left margin are how many times each line ran; a line that never ran would show %000000. Line coverage is complete and the FIFO's one dangerous scenario was never tried. That is the whole argument for functional coverage, measured on one design.
Functional coverage low | Functional coverage high | |
|---|---|---|
Code coverage high | Stimulus wanders; scenarios never formed | Sign-off candidate: review waivers, then close |
Code coverage low | Early bring-up: keep testing | Coverage model too thin, or dead RTL: investigate |
An assertion counter is a different construct: SystemVerilog cover property counts assertion matches, not covergroup bins. Both appear in reports, so name which one each number came from.
x_op_zero sits at 83.3 percent. What is missing?One opcode never produced a zero result: five of six asserted bins hit. Add the directed case the hole names.
Stimulus wanders without forming planned scenarios. Steer it with constraints or directed cases instead of running longer.
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.
Random stimulus left named holes, and six directed cases closed the model at 100%.
The FIFO test was also built with verilator --binary --coverage-line and annotated with verilator_coverage on 2026-10-18: every line and both sides of every branch ran while the corner bin read 0.