Functional Coverage

Your regression is green: ten thousand constrained-random tests, zero failures. Time to celebrate? Not yet — a green regression answers "did anything we tried fail?" and stays silent on the question that actually matters: what did we try? Ten thousand tests that all happened to be small aligned reads prove nothing about writes, or boundaries, or the read-after-write hazard. Functional coverage is the discipline that makes "have we tested it?" a measured quantity: you declare the scenarios that matter, the simulator counts how often each actually occurred, and the holes in the count are your to-do list. It is the difference between "we ran a lot of tests" and "we know what our tests did".

Covergroups, coverpoints, bins

The declaration mirrors the assertion machinery: you write what matters, the simulator tallies it. A covergroup samples signals at an event (usually a clock edge); each coverpoint watches one expression; its bins partition the values into the cases you care about:

covergroup alu_cov @(posedge clk); cp_op: coverpoint op { bins add = {OP_ADD}; bins sub = {OP_SUB}; bins logic_ops[] = {OP_AND, OP_OR, OP_XOR}; // one bin each illegal_bins bad = {4'hF}; // must NEVER be sampled } cp_a: coverpoint a { bins zero = {0}; bins small = {[1:255]}; bins big = {[256:$]}; // $: up to the maximum } op_x_a: cross cp_op, cp_a; // 5 x 3 = 15 cross bins endgroup

Each bin is a little odometer: "how many times did a subtraction happen?", "how many times was an operand zero?". An illegal_bins is a tripwire — sampling it fails the test outright. And the cross is where the real power lives: it counts combinations — did we ever subtract from zero? AND with a big operand? Bugs overwhelmingly hide at the intersections of features ("the FIFO was full while an error arrived during a flush"), which no single coverpoint can see. Crossing an n-bin point with an m-bin point makes n \times m cross bins — the combinatorics that keeps verification teams honest and busy.

The closure loop

Coverage turns verification into a feedback loop with a name — coverage closure: run the regression; read the coverage report; find the holes (bins at zero); write or steer tests to hit them (new constraints, new directed tests, new seeds); repeat until every bin is hit or consciously waived. The curve it produces is one of the most recognisable shapes in the industry:

Random stimulus sprints through the easy scenarios — the first hundred tests buy more coverage than the next ten thousand — then stalls: some bins are simply too improbable to hit by luck (the FIFO-full-during-error cross might need a one-in-a-billion coincidence). Those last bins are hit by aiming: tightening constraints around the hole, or writing an old-fashioned directed test. The plateau is not failure; it is the report telling you where imagination must take over from dice.

Count the bins yourself

A coverage collector is honestly just a map of counters. Watch one discover a hole — and watch the hole persist until the stimulus is steered:

// ── A tiny coverage collector: bins are counters in a Map. ── const ops = ["ADD", "SUB", "AND", "OR"]; const sizes = ["zero", "small", "big"]; function collect(nTests: number, steered: boolean): void { const bins = new Map<string, number>(); for (const o of ops) for (const s of sizes) bins.set(`${o} x ${s}`, 0); let rng = 12345; const rand = () => ((rng = (rng * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff); for (let t = 0; t < nTests; t++) { const op = ops[Math.floor(rand() * 4)]; // Unsteered stimulus almost never draws a zero operand (1 value in 4096): const zeroP = steered ? 0.2 : 1 / 4096; const r = rand(); const size = r < zeroP ? "zero" : r < 0.8 ? "small" : "big"; bins.set(`${op} x ${size}`, (bins.get(`${op} x ${size}`) ?? 0) + 1); } const hit = [...bins.values()].filter((n) => n > 0).length; const holes = [...bins.entries()].filter(([, n]) => n === 0).map(([k]) => k); const pct = ((100 * hit) / bins.size).toFixed(0); console.log(`${steered ? "steered" : "random "} | ${nTests} tests: ${hit}/${bins.size} cross bins = ${pct}% covered`); if (holes.length) console.log(` holes: ${holes.join(", ")}`); } collect(50, false); // early random run: easy bins fill fast collect(5000, false); // 100x more tests... the zero-operand holes REMAIN collect(200, true); // steer stimulus at the hole: closure with 25x fewer tests

Five thousand aimless tests lose to two hundred aimed ones — the closure loop in miniature. The report told us which scenarios were missing; the fix was a constraint making zero operands likely; the holes closed.

Code coverage, functional coverage, and "done"

Tools also report code coverage for free: which RTL lines executed, which branches both ways, which signal bits toggled. It is necessary — an unexecuted line is untested by definition — but shallow: code coverage measures the design as written, not the intent. Every line of a FIFO can execute without ever testing "full and empty in the same microsecond" — a scenario, not a line. Functional coverage measures scenarios; code coverage catches the parts of the design your scenario list forgot even to mention. You need both, and neither is sufficient.

So when is verification done? Here is the uncomfortable truth this course will repeat at tape-out: it is never done — it is risk management. There is no proof at the end of simulation, only evidence: functional coverage closed, code coverage explained, regressions clean across thousands of seeds, assertions never vacuous, reviews signed. Tape-out is the day the accumulated evidence outweighs the cost of waiting — a judgement call wearing an engineering costume, made honest by the numbers this lesson taught you to collect.

Functional coverage quietly revolutionised how chip projects are run. Before it, verification status was vibes: "we feel good about the cache". After it, a project has a coverage dashboard — thousands of bins rolled up into percentages per block, trending week by week — and tape-out reviews walk the numbers: which holes remain, which are waived and why, whose signature is on each waiver. The waiver list is the fascinating part: a hole can be legitimately unhittable ("this error type cannot occur with this bus configuration"), and writing that sentence down forces someone to prove it, which has itself found bugs. The dashboard also produces verification's best-known career wisdom: the engineer who closes the last 2% of coverage learns more about the design than the person who wrote it.

Coverage measures your tests against your coverage model — the bins you chose to declare. It cannot see scenarios you never wrote down: forget a coverpoint for back-to-back writes and the report gleams at 100% while that scenario goes untested into silicon; nobody counts a bin that doesn't exist. And coverage says nothing about checking: a bench with a broken scoreboard can hit every bin — every scenario exercised, none of them actually verified — and a green 100% dashboard crowns the whole illusion. Coverage measures the completeness of your test plan's execution, never the correctness of the design. The mature reading of "100% coverage, zero failures" is precisely: "every scenario we thought of was exercised, and our checkers — as good as they are — stayed quiet". Both italicised clauses are load-bearing, and both are exactly where the escaped bugs will have lived.