Functional Coverage
Randomization and Constraints generates legal stimulus automatically — but that raises an immediate follow-up question a directed test never has to ask: has enough actually been tested? A directed test's coverage is exactly whatever the writer enumerated; a randomized test's coverage is whatever happened to come up, which is a much less certain answer without some way to explicitly track it. Functional coverage is that tracking mechanism — a way to declare, in code, exactly what values and combinations of values a test is supposed to exercise, then measure how much of that target was actually hit.
covergroup and coverpoint: the basic shape
covergroup transaction_cg @(posedge clk);
coverpoint opcode {
bins add = {2'b00};
bins sub = {2'b01};
bins and_op = {2'b10};
bins or_op = {2'b11};
}
endgroup
transaction_cg cg_inst = new();
A covergroup is a coverage-collection unit, sampled once per the trigger given in its declaration (@(posedge clk) here — once per clock edge). A coverpoint inside it tracks one specific signal or expression (opcode), broken into named bins — each bin corresponding to one value or range of interest. Every time the covergroup samples, whichever bin opcode's current value falls into gets its hit count incremented. Instantiating transaction_cg (via new(), exactly like any other class — a covergroup is compiled to behave like a special kind of class) starts it actively sampling. Sampling isn't limited to the declared clocking event either — calling cg_inst.sample() explicitly from testbench code triggers an extra sample immediately, useful when the moment worth sampling doesn't line up cleanly with a clock edge (e.g. right when a transaction object is captured by a monitor).
Automatic bins vs. explicit bins
covergroup addr_cg @(posedge clk);
coverpoint address {
bins low_addr = {[0:63]};
bins mid_addr = {[64:191]};
bins high_addr = {[192:255]};
}
endgroup
Without any bins declared at all, a coverpoint automatically creates one bin per distinct value the signal can take — fine for a small enumerated field like the 4-value opcode above, but impractical for an 8-bit address (256 automatic bins, mostly uninteresting individually). Explicit bins, as shown, group the address space into a small number of meaningful ranges instead — deciding which ranges matter (low/mid/high addresses, in this example) is a real design decision every coverage model has to make deliberately, not something the tool can infer on its own. Left to itself, this automatic-bin behavior is also capped: option.auto_bin_max (default 64) limits how many bins the tool will ever auto-create for one coverpoint, silently coalescing/truncating further values rather than growing without bound.
Cross coverage: combinations of coverpoints
covergroup transaction_cg @(posedge clk);
coverpoint opcode {
bins add = {2'b00};
bins sub = {2'b01};
}
coverpoint operand_is_zero {
bins yes = {1};
bins no = {0};
}
cross opcode, operand_is_zero; // every (opcode, operand_is_zero) combination
endgroup
A cross tracks every combination of two or more coverpoints' bins together, not just each independently — here, whether the test has actually exercised add with a zero operand, add with a nonzero operand, sub with a zero operand, and sub with a nonzero operand, as four separate tracked combinations. This catches a real gap plain per-signal coverage would miss entirely: opcode and operand_is_zero might individually show 100% coverage (every opcode seen, and a zero operand seen at some point) while the specific combination "sub with a zero operand" was never actually exercised together in the same cycle — exactly the kind of interaction bug a corner-case-sensitive test needs to know it hasn't checked yet.
A cross's automatically-generated combinations can also be pruned with ignore_bins, using binsof(...) intersect {...} to name which combinations to drop: cross opcode, operand_is_zero { ignore_bins skip_and_zero = binsof(opcode) intersect {2'b10} && binsof(operand_is_zero) intersect {1}; } removes just the "and with a zero operand" combination from the cross's tracked bins (say, because it's structurally impossible for this design) without touching any of the other three. intersect here plays the same role && plays between ordinary boolean expressions — it's specific to combining binsof(...) references inside a cross's bin expression.
iff: gating when a coverpoint counts at all
covergroup transaction_cg @(posedge clk);
coverpoint mode iff (!reset) {
bins idle = {0};
bins run = {1};
}
endgroup
An iff (condition) clause on a coverpoint (it can also be placed on the whole covergroup) gates whether that sample counts at all — on any sample where the condition is false, no bin's hit count is incremented, even if mode's current value would otherwise match one exactly. Above, iff (!reset) means samples taken while reset is asserted are simply skipped for this coverpoint, the coverage-model equivalent of the disable iff reset guard Concurrent Assertions uses for properties — both exist to stop reset-time values, which were never meant to be judged, from polluting a passing/measured result.
Reading a coverage report
Simulators report functional coverage as a percentage — bins hit divided by bins defined, per coverpoint, per cross, and rolled up per covergroup and overall. 100% functional coverage doesn't mean the DUT is bug-free (a passing test that hits every defined bin still only proves what the coverage model explicitly asked it to check) — it means every scenario the verification plan identified as worth checking has been exercised at least once. The coverage model is only as good as the bins someone thought to define; a coverage-driven methodology treats "what bins should exist" as a real design task in its own right, not an afterthought.
Why coverage and assertions are complementary, not overlapping
| Assertions (Immediate / Concurrent) | Functional coverage | |
|---|---|---|
| Answers | "Did the DUT behave correctly?" | "Has the test actually exercised this scenario?" |
| Fails/flags when | Behavior violates a rule | (Never "fails" — just reports what wasn't hit) |
| Useful even on a passing test? | No news is good news — nothing to report | Yes — a passing test with low coverage means the pass proves less than it looks like |
A test suite with strong assertions but no coverage tracking can be passing cleanly while quietly never exercising most of the design's actual behavior; a test suite with strong coverage but no assertions can report 100% coverage while never actually checking that any of the exercised behavior was correct. Real verification environments track both together.
Refining bins: wildcard, ignore, illegal, and transitions
covergroup transaction_cg @(posedge clk);
coverpoint opcode {
wildcard bins reserved = {4'b1??1}; // ? matches either 0 or 1
ignore_bins unused = {4'b0000}; // excluded from the coverage percentage entirely
illegal_bins bad_state = {4'b1111}; // hitting this bin is a simulation error, not just a missed bin
bins seq_add_then_sub = (2'b00 => 2'b01); // transition bin: only counts when add is immediately followed by sub
}
endgroup
Real coverage models need more than flat value bins. A wildcard bin (wildcard bins) treats ?, x, or z in its value list as "don't care" positions, so one bin can match a whole family of values instead of listing each one. An ignore_bins entry removes specific values from the coverage calculation altogether — useful for combinations that are structurally impossible or genuinely irrelevant to verify, so they don't sit in the report as permanently-missed coverage. An illegal_bins entry is different in kind: it isn't excluded, it's a simulation-time error if hit at all, since that value should never occur — it flags a real design or test bug the moment it's sampled, not just a coverage gap. A transition bin, written bins name = (value1 => value2), tracks a sequence across consecutive samples rather than a single value — the bin above only increments when opcode reads add on one sample and sub on the very next one, catching ordering that a plain value bin can't distinguish from any other pair of samples.
Coverage options: weighting and thresholds
covergroup transaction_cg @(posedge clk);
option.goal = 90;
coverpoint opcode {
option.weight = 3;
}
endgroup
Beyond bins themselves, a covergroup or coverpoint can carry options that shape how its number is computed and judged, without changing what gets sampled. option.weight controls how much a coverpoint or covergroup counts toward the rolled-up overall percentage relative to its peers — setting it to 0 removes that coverpoint/covergroup from the calculation entirely, useful for a coverpoint kept around for visibility but not meant to gate signoff. option.goal sets the percentage a coverpoint/covergroup must reach to be considered "done" for that scope — it defaults to 100 but can be lowered where 100% genuinely isn't achievable or meaningful. Code can also read a covergroup's live percentage back with cg_inst.get_coverage(), which returns the aggregated coverage (all coverpoints and crosses in that instance) as a real number from 0 to 100 — handy for scripting a regression that stops early once a target percentage is reached.
A covergroup as a class member
class monitor;
bit [1:0] opcode;
covergroup cg;
coverpoint opcode;
endgroup
function new();
cg = new();
endfunction
function void write(bit [1:0] op);
opcode = op;
cg.sample();
endfunction
endclass
A covergroup doesn't have to sit at module scope sampling off a clock — declaring one as a member of a class is the common pattern real testbenches (and UVM monitors specifically) actually use: the covergroup is constructed with its own new() call inside the class's constructor (cg = new(); above, separate from the class's own new()), and sampled explicitly (cg.sample()) at whatever moment the class's own code decides is meaningful — here, every time write() is called with a freshly observed transaction, rather than on a fixed clock edge.
What's next
Section E closes out assertions and coverage. Section F covers the last practical language pieces — clocking blocks and program blocks, which manage the subtle race conditions that can occur between testbench code and the DUT — before a capstone example ties classes, randomization, an interface, and an assertion together into one small testbench.