The DMA's functional coverage is closed: all 17 plan items are hit in the merged regression. Then someone turns on code coverage for the first time, and the report shows that pslverr, the DMA's error response for a bad register address, never toggled once. No test ever accessed an unmapped address, and no plan item asked for it, so functional coverage could not notice. The plan was complete by its own measure and still missed a feature. How do code coverage and functional coverage check each other, and how do you close both?
Code coverage is collected automatically by the simulator from the structure of the RTL: which lines ran, which branches were taken, which signals toggled. Nobody writes it, so it has no opinion about what matters, and it cannot see anything that is not in the code. Functional coverage is written by you from the spec and the plan (the covergroups of the coverage model lesson); it measures the scenarios you thought of, and cannot see anything you did not think of.
Picture a delivery driver's week. The GPS log (code coverage) shows every street driven, with no idea why. The errand list (functional coverage) shows every delivery made, with no idea which streets were used. A street never driven is either a road nobody needs or a customer the list forgot. An errand ticked off without a street that should have been on the way means the list counted something that did not really happen as planned. You need both records to know the week was done right.
Metric | Counts | Finds | Blind to |
|---|---|---|---|
Line or statement | Each executable statement ran at least once | Dead code and untested features: an error path no test reaches | Values: a line runs the same for LEN=0 and LEN=8 |
Branch (block) | Each arm of every | The missing | Which combination of conditions took the branch |
Condition or expression | Each term of a Boolean expression decided the result |
| Timing and sequences |
Toggle | Each bit went 0 to 1 and 1 to 0 | Stuck bits, unused upper bits of a counter, untested ports | Meaning: a toggle of LEN bit 4 says nothing about the job |
FSM state and transition | Each state reached and each arc taken, extracted from a state register | Unreached states and arcs: error recovery, rare handshakes | Data and the cycles spent in a state |
Tools extract FSM coverage from code that looks like a state machine (a state register with a case on it). The DMA has no explicit state register (its state is the busy, done and err flags), so its state space shows up in branch and toggle coverage instead.
Verilator collects code coverage when the bench is built with --coverage. Each run writes a data file, verilator_coverage merges files and writes an annotated copy of each source file with a hit count in front of every line. The commands for this lesson's scratch runs:
# build once with coverage instrumentation (the course's run.sh accepts EXTRA_ARGS)
EXTRA_ARGS="--coverage" scripts/tools/uvm-bench/run.sh
# one coverage file per run
./Vtop +UVM_TESTNAME=dma_smoke_test +verilator+coverage+file+dma_smoke_test.dat
# merge, then annotate (lines with a count below 1 are marked %)
verilator_coverage --write merged.dat dma_smoke_test.dat dma_zero_len_test.dat ...
verilator_coverage --annotate-min 1 --annotate annotated/ merged.datCommercial simulators do the same with their own switches and databases. The sketch below names the usual options; check your tool's manual for the exact spelling.
CODE COVERAGE SWITCHES (sketch: verify against your tool's manual)
VCS: compile and run with -cm line+cond+fsm+tgl+branch, report with urg
Questa: vlog +cover=bcesft (branch, condition, expression, statement, fsm, toggle),
vsim -coverage, merge and report with vcover
Xcelium: xrun -coverage all (block, expression, toggle, fsm), report with imcHere is part of the annotated dma_dut.sv after dma_smoke_test alone: one 8-word job, with a STATUS read and a DONE clear. The number is how many times the line ran; %000000 marks a line never executed and ~ marks a line where some of its coverage points (a branch arm or an expression term) were never hit.
Verilator annotation, dma_dut.sv, dma_smoke_test only (excerpt)
000079 always_comb begin
000079 unique case (paddr)
000041 8'h00: prdata = {30'b0, irq_en, 1'b0};
000024 8'h04: prdata = {29'b0, err, done, busy};
000007 8'h08: prdata = {16'b0, len};
000007 8'h0C: prdata = key;
%000000 8'h10: prdata = {16'b0, count};
%000000 default: prdata = 32'h0;
endcase
...
~000004 8'h08: if (!busy) len <= pwdata[15:0];
~000004 8'h0C: if (!busy) key <= pwdata;
%000000 default: ;
...
000062 if (go) begin
000001 busy <= (len != 16'd0);
000001 done <= (len == 16'd0); // a zero-length job is done at once
000001 count <= 16'd0;
end
...
000001 if (last_word) begin
000001 busy <= 1'b0;
000001 done <= 1'b1;
~000007 end else if (s_tlast) begin // input packet ended early
%000000 busy <= 1'b0;
%000000 err <= 1'b1;
endThe ERR path never ran. No input packet ended early, so the two lines that set ERR show zero. This is the same hole as the functional plan's outcome=err, seen from the code side.
COUNT was never read (8'h10 arm at zero), and no unmapped address was read (default at zero).
The `if (!busy)` lines are partial. Their else arm (a LEN or KEY write while a job is running) was never taken.
The LEN=0 path looks covered, and is not. busy <= (len != 16'd0) ran once, with LEN=8. Line coverage counts the line, not the value, so a zero-length job (DONE at once, no data) is invisible to it. Only the functional bin len=0 sees that hole.
Merging dma_smoke_test, dma_zero_len_test, dma_truncate_test, dma_check_test, dma_backpressure_test and dma_reset_test (all passing) closes the easy holes: dma_truncate_test runs the ERR path, dma_zero_len_test reads COUNT, and dma_backpressure_test drives m_tready low, closing the (m_tready==0) expression term. Counts of coverage points in dma_dut.sv, as Verilator reports them:
Point type (Verilator) | dma_smoke_test | Six tests merged |
|---|---|---|
Line | 14 of 17 | 15 of 17 |
Branch | 36 of 43 | 40 of 43 |
Expression | 15 of 18 | 16 of 18 |
Toggle | 258 of 452 | 384 of 452 |
What remains after the merge, classified the way the closure meeting would:
Remaining hole | Cause | Decision |
|---|---|---|
| No test accesses an unmapped or unaligned address | Missing plan item. Add "unmapped access returns PSLVERR" to the plan and a directed test |
| No test in | Missing plan item. This is war story 9's rule; the busy-LEN register test in |
| Random jobs stop at LEN=16; the register is 16 bits | Reachable: one long job (LEN above 32, ideally near 65535) or a reviewed decision on the maximum tested length |
| Every STATUS write in these tests clears DONE | Small test: clear ERR alone |
Some | KEY is loaded from random values and reset only at the start | More seeds close it; low value |
Notice that not one hole is unreachable. The instinct when a coverage report arrives is to exclude whatever looks odd; on this DUT every hole is either a feature nobody tested or a value range nobody chose.
An exclusion removes a coverage point from the denominator because it cannot or need not be reached: code for a configuration that is not built, a defensive default that the logic makes impossible, a constant tie-off, a debug port that is never used in this product. Exclusions are written down with the point, the reason, the reviewer and a condition to revisit, and they are reviewed with the designer, who knows what is reachable. The merge and closure lesson shows one for the DMA: the zero-wait-state APB path, which only exists when WAIT_STATES = 0. The rule of thumb: if the spec says the hardware can do it, it is a test, not an exclusion.
100% code coverage, functional holes. Code coverage sees structure, not values or sequences. The LEN=0 job runs no line of its own; a back-to-back beat (the plan's gap=0, which the bench's stream driver never produces) uses exactly the same lines as a beat with gaps; a len x irq_en cross is a combination of two values, not a line of code. All three can be missing at 100% line and branch coverage.
100% functional coverage, code holes. The plan is written by people, so it misses what people forget. On the DMA, all 17 plan items were hit while PSLVERR and the write-while-busy rule had never run. Code coverage is the automatic check on the plan's completeness.
Both at 100% and a bug escapes. Coverage counts what happened, not what was checked. A line that ran while the scoreboard was demoted, waived or comparing nothing (the false-pass playbook) is still covered. Merge only passing runs, and keep the checker counts next to the coverage numbers.
Code coverage is merged exactly like functional coverage: per-run databases from passing runs on one build, combined as a union, so a point is covered if any run covered it. A change to the RTL invalidates old databases, because line numbers and points move. In the closure meeting, the verification engineer and the designer walk the merged code holes and the functional holes side by side. Each code hole ends as a test, a new plan item, or a reviewed exclusion; each functional hole as stimulus, a constraint change, or a coverage-model fix. The most valuable minute of the meeting is the designer saying "that is reachable" about a hole the verification engineer was about to exclude.
War story 9 in this course is a model bug: the DMA ignores LEN and KEY writes while a job is running, but the bench's predictor and scoreboard took a new KEY at once. No test wrote KEY mid-job, so it went unnoticed until a scratch run produced 5 mismatches. The code-coverage report above would have pointed straight at it: the else arm of if (!busy) key <= pwdata was never taken in any test of the block bench. A hole in code coverage is a question ("what happens when this condition is false?") and on the DMA the honest answer was "nobody knows, and the model is wrong about it". Code coverage does not find bugs by itself; it finds the places nobody looked.
go block is 100% after the smoke test. Has the zero-length job been tested?No. busy <= (len != 16'd0) and done <= (len == 16'd0) run on every GO, whatever LEN is. The LEN=0 behaviour is a value, not a line, so line and branch coverage cannot see it; the functional bin len=0 (closed by dma_zero_len_test) can.
pslverr never toggled, and a colleague proposes excluding it because "software never uses bad addresses". What do you say?The spec defines the PSLVERR response, so the hardware can produce it and software bugs can trigger it. It is a missing plan item and a missing test, not an exclusion. Add the plan item, write a directed test that reads and writes an unmapped and an unaligned address, and check PSLVERR and that no register changed.
Either the change added code the tests do not reach (a new feature or path), or old databases from the previous RTL were mixed in and points moved. Re-run the regression on the new build only, merge only those passing runs, and look at the new holes: they are usually the new feature's missing tests.
Next: the checking and coverage part is complete. The register layer replaces hand-written register traffic with a model that knows every field. The numbers from this lesson go on the sign-off sheet in Metrics and Sign-off.