Code coverage is free: switch it on in the simulator and every regression reports which lines of RTL ran. Teams love it for that reason, and some treat 100% line coverage as the finish line. Then a bug turns up in a line that was executed thousands of times, with a value no test ever drove, or in a feature whose code was never written at all.
What does code coverage measure, and what can it not see?
The obvious belief is that executing every line means testing every behaviour. A line can execute with only one value of its inputs; a condition with three terms can be true for only one reason; a bug that is a missing line has no line to cover. Code coverage measures the code that exists, against the stimulus that ran, and nothing about intent.
Metric | Measures | Misses |
|---|---|---|
Line (statement) | Each statement executed | Values; which branch of a ternary; missing code |
Branch | Each if/case branch taken, including implicit else | Combinations of conditions; values inside the branch |
Condition (expression) | Each term of a condition shown to affect the outcome | Combinations across separate conditions; timing |
Toggle | Each signal bit seen rising and falling | What the toggles meant; sequences |
FSM | Each state and transition visited | Paths through several transitions; data in each state |
The CTRL write in DCRouter's register block executes on every write to address 0x00:
// snippet: from dcrouter.sv, the CTRL write
if (paddr == 8'h00) begin port_en <= pwdata[3:0]; prio_en <= pwdata[4]; lp_en <= pwdata[5]; endLesson 4.10's random hunt writes CTRL at the start of every round, so this line has full line and branch coverage. Yet every write sets port_en to 4'hF: no port is ever disabled, and BUG_23 (a disabled port still accepts traffic) escapes. Toggle coverage would notice: the port_en bits never fall from 1 to 0. Functional coverage notices directly: the bins "CTRL port k disabled" stay empty (lesson 6.9).
The second line is the error counter's saturation:
// snippet: from dcrouter.sv, ERR_CNT increment
err_cnt[k] <= (err_cnt[k] == 8'hFF) ? 8'hFF : err_cnt[k] + 8'd1;Line coverage marks it covered after the first error. Whether the == 8'hFF side of the ternary was ever taken is invisible to line coverage and, written as a ternary, often to branch coverage too; expression coverage or a functional bin "ERR_CNT saturated" is needed. That is exactly the case where BUG_15 (wrap instead of saturate) hides.
Finding dead or unreached code: logic no test executed is untested for certain.
Finding gaps in the plan: an unexecuted branch is often a feature nobody listed.
Cheap, automatic, objective: no model to write.
Necessary, not sufficient: unexecuted code is a definite gap; executed code is not a definite pass.
Practice | Purpose |
|---|---|
Code coverage collected on every regression | Unexecuted code found early |
Exclusions reviewed and documented | Unreachable or debug-only code waived with a reason (lesson 6.6) |
Code and functional coverage required together at sign-off | Each catches what the other misses (Part 14) |
A team (illustrative) reached 100% line and branch coverage on a DMA engine. A customer found that descriptors crossing a 4 KB boundary were split incorrectly. The splitting code had been written for the common case only; the boundary-crossing case had no code at all, so there was nothing for code coverage to report. A functional bin "descriptor crosses 4 KB" written from the specification would have stayed empty.
Toggle coverage: the port_en bits would be seen at 1 but never falling to 0.
It measures only code that exists; a feature that was never written has no lines, branches or toggles to leave unexecuted.
Code coverage measures execution of existing RTL: lines, branches, conditions, toggles, FSM states.
It misses values, combinations and missing code.
Unexecuted code is a definite gap; executed code is not proof of anything.