Day 10: Functional Coverage — Covergroups, Coverpoints, Crosses, and Verification Plans
A 30-day SystemVerilog/UVM verification series. The scoreboard tells you what passed. Coverage tells you what you never tried.
A scoreboard answers one question: on the transactions the DUT saw, did it behave correctly? It does not answer the question that matters just as much: did the testbench actually try the conditions that matter? A testbench that writes 0x00 to a FIFO 10,000 times will produce a perfectly green scoreboard and verify almost nothing. That gap is what functional coverage exists to close.
Coverage is a declaration of intent. You write down, in code, the conditions you meant to exercise. The simulator records which ones you hit. Coverage closure is the process of finding the zeros — the conditions you meant to hit and never did — and adding sequences to reach them.
1. Covergroups and Coverpoints
A covergroup is a class-like construct that samples values and records which bins they landed in. A coverpoint is one variable whose values you want to track. Bins can be auto-generated or explicitly declared.
class fifo_coverage extends uvm_subscriber #(fifo_tx);
`uvm_component_utils(fifo_coverage)
fifo_tx tr;
covergroup cg_fifo;
option.per_instance = 1;
cp_wr_en: coverpoint tr.wr_en;
cp_rd_en: coverpoint tr.rd_en;
cp_wdata: coverpoint tr.wdata {
bins zero = {8'h00};
bins max = {8'hFF};
bins low = {[8'h01:8'h7F]};
bins high = {[8'h80:8'hFE]};
}
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
cg_fifo = new();
endfunction
function void write(fifo_tx t);
tr = t;
cg_fifo.sample();
endfunction
endclass
Three things to notice:
uvm_subscriberis the UVM class designed for exactly this. It has ananalysis_exportout of the box, so you connect it like a scoreboard.- Explicit bins (
zero,max,low,high) map value ranges to named conditions. Auto bins would give you one bin per value, which is useless for an 8-bit field. sample()is called explicitly, once per transaction. Sampling insidewrite()keeps the coverage in sync with the scoreboard.
2. Cross Coverage
Coverpoints verify individual conditions. Crosses verify combinations. A FIFO that has been written-with-zero and read-from-max might have hit both coverpoints independently while never having been full at the moment a read was attempted. That specific bug — read-from-empty — only shows up when you cross wr_en, rd_en, and the DUT's empty flag.
cross_wr_rd: cross cp_wr_en, cp_rd_en {
// The interesting combos
bins wr_only = binsof(cp_wr_en) intersect {1} &&
binsof(cp_rd_en) intersect {0};
bins rd_only = binsof(cp_wr_en) intersect {0} &&
binsof(cp_rd_en) intersect {1};
bins idle = binsof(cp_wr_en) intersect {0} &&
binsof(cp_rd_en) intersect {0};
// Simultaneous write+read: usually illegal, check the spec
bins both = binsof(cp_wr_en) intersect {1} &&
binsof(cp_rd_en) intersect {1};
}
3. Sampling at the Right Time
Coverage is only as good as when it samples. Sample too early and you record stimulus that never reached the DUT. Sample too late and you record post-correction values that hide the bug.
In a UVM subscriber, the monitor publishes a transaction after the DUT has responded. Sampling in write() therefore samples post-response values — exactly right for a checker but potentially wrong for a coverage model that wants to record the stimulus.
| Sample where… | What you record | Good for |
|---|---|---|
| Driver's analysis port | What the testbench intended to drive | Stimulus coverage ("did we try this?") |
| Monitor's analysis port | What the DUT actually saw | Behavioural coverage ("did it reach the pins?") |
| Inside a sequence | Randomization-time values | Early sanity — rarely useful as the primary source |
run_phase with @(posedge clk) and no guard, which produces a coverage sample every clock — usually dominated by idle cycles that bury the interesting bins under noise. Always sample once per transaction, not once per clock.
4. Tying Coverage to a Verification Plan
Coverage without a plan is just a number that goes up. A verification plan lists the features and corner cases the DUT is required to handle, and each line of the plan maps to one or more coverpoints or crosses. Coverage closure is then "every plan line hit," not "overall coverage is 90%."
| Plan line | Covergroup bin | Status |
|---|---|---|
| Write at full | cp_full / binsof(cp_full) intersect {1} when wr_en | Closes |
| Read at empty | cross_wr_rd / rd_only when empty | Closes |
| Wrap-around | cp_wr_ptr from max to zero | Open |
| Simultaneous write+read | cross_wr_rd / both | Closes |
When the run ends, run uvm_report_phase or use $coverage_get / $coverage_save to dump the coverage database, and cross-reference each plan line against the report. A plan line that is "closed by inspection" without a corresponding bin is a lie you are telling yourself.
5. Coverage-Driven Stimulus
Once coverage is measured, the test can be steered. Two standard techniques:
- Constrained-random with coverage feedback — run a random regression, look at the coverage report, and add targeted sequences (or tighten constraints) to the holes.
- Explicit directed sequences — for rare conditions (wrap-around, simultaneous write-read, near-full burst), write a sequence that hits it directly rather than hoping randomization lands there.
The most efficient teams do both: random regression for breadth, targeted sequences for corners, and coverage is the scoreboard that tells them when to stop.
6. Common Coverage Bugs
| Symptom | Cause |
|---|---|
| Overall coverage high but a known corner never hit | No cross for the corner — individual coverpoints hit it separately |
| Coverage says 100%, bug found in regression | Coverage model doesn't describe the bug's condition (missing coverpoint) |
| Sample count enormous, bins sparse | Sampling per clock instead of per transaction |
| Bins never hit even with obvious traffic | sample() never called, or called with tr still null |
| Coverage passes in simulation but not in the report | Coverage database not saved ($coverage_save) or merged across tests |
| Adding coverage slows simulation 10× | Per-bin callbacks in a tight loop — use option.per_instance = 0 where possible and sample at transaction boundaries |
7. What Day 11 Will Add
Tomorrow: Callbacks and Synchronization. uvm_callback lets you extend a component's behaviour without subclassing it, uvm_event lets components signal each other without knowing their paths, and uvm_barrier synchronizes multiple processes on a shared event. Together they are the escape hatch for the cases where the layered architecture is not enough.
8. Quiz
Ten questions on covergroups, coverpoints, crosses, sampling, and closure.
Coming Up Next: Day 11 — Callbacks and Synchronization
- uvm_callback — extending component behavior without subclassing, and where callbacks live in the component tree.
- uvm_event — signaling between components without them knowing each other's paths.
- uvm_barrier — synchronizing N processes on a single event, useful for multi-agent coordination.
- When not to use them — callbacks and events are escape hatches; using them everywhere makes the architecture opaque.
Series: 30 days of SystemVerilog/UVM verification. Day 1 built the skeleton. Day 2 wired the lifecycle. Day 3 put it in motion. Day 4 wired the plumbing. Day 5 installed the knobs. Day 6 made it modular. Day 7 made it yours. Day 8 made it coordinated. Day 9 made it rigorous. Day 10 made it measurable.
No comments:
Post a Comment