Part 5 · Functional Coverage · Intermediate
Building Your First Coverage Model
Complete worked example: ALU spec to coverage plan to covergroup implementation to simulation to reading the report.
From spec to plan
A coverage model starts in the spec, not in code. Read the spec, extract the scenarios that must be proven to have occurred, write them down as a numbered plan, and only then implement covergroups whose bins map one-to-one onto plan items. The worked example: a small ALU.
ALU SPEC (excerpt)
──────────────────
ops: ADD, SUB, AND, OR, XOR, SHL
inputs: a[7:0], b[7:0]
outputs: y[7:0], zero (y == 0), carry (ADD/SUB only)
COVERAGE PLAN (derived from spec)
─────────────────────────────────
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 operand extremes: a and b each seen at 0 and 255
CP-5 every opcode executed with zero result (cross)
CP-6 shift by 0 and shift by 7 for SHLEach plan item is a sentence a non-programmer could check off. The covergroup's job is to make every item mechanically measurable — by name.
Plan to covergroup
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 → 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 only meaningful for ADD/SUB → iff guard
cp_carry : coverpoint carry iff (op inside {ADD, SUB}) {
bins carry_set = {1};
}
// 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 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
endclassDriving and sampling it
module tb;
alu_coverage cov = new();
op_e op;
bit [7:0] a, b, y;
bit zero, carry;
initial begin
repeat (2000) begin
assert(std::randomize(op, a, b));
alu_model(op, a, b, y, carry); // reference computation
zero = (y == 0);
cov.cg.sample(op, a, b, y, zero, carry);
end
$display("ALU coverage: %0.1f%%", cov.cg.get_inst_coverage());
end
endmoduleReading the report and closing the loop
After simulation, the coverage report lists every coverpoint and bin with hit counts. Because bins are named after plan items, reading the report IS reviewing the plan. A typical first-run result for this model:
COVERGROUP alu_cov 87.2%
├── cp_op 100.0% (CP-1 ✓)
│ add 412 sub 388 and_ 401 or_ 395 xor_ 379 shl 425
├── cp_zero 100.0% (CP-2 ✓)
├── cp_carry 100.0% (CP-3 ✓)
├── cp_a_ext 50.0% (CP-4 ✗)
│ min_v 7 max_v 0 ◄── a == 255 never randomized
├── cp_b_ext 50.0%
│ min_v 9 max_v 0
├── x_op_zero 66.7% (CP-5 ✗)
│ <shl, asserted> 0 ◄── SHL never produced y == 0
└── cp_shamt 100.0% (CP-6 ✓)
HOLES → ACTIONS
cp_a_ext.max_v → add constraint corner: a dist {255 := 5, ...}
x_op_zero <shl> → directed case: SHL with a == 0Extract scenarios from the spec into a numbered, reviewable plan.
Implement one coverpoint/cross per plan item; name bins so the report reads like the plan.
Run with random stimulus; query get_inst_coverage() and dump the database.
Read holes as missing scenarios, not missing percentage — each hole names a stimulus gap.
Fix holes with constraint tweaks or small directed cases; rerun; repeat until every plan item is hit.
Key takeaways
Coverage models are derived from the spec via a written plan — never invented at the keyboard.
Name bins and coverpoints after plan items so reports are self-auditing.
Holes are actionable stimulus gaps: each unhit bin dictates a constraint change or directed test.
iff guards and binsof-filtered crosses keep the model measuring only meaningful combinations.
Common pitfalls
Writing covergroups without a plan — you measure what was easy to type, not what the spec requires.
Covering raw operand values exhaustively instead of extremes and properties — uncloseable bins.
Crossing everything with everything — CP-5 needed one filtered cross, not a full 6×2×2 product.
Declaring victory on the covergroup percentage without checking which bins are the unhit ones.