Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
    • Scoreboard Architecture: Expected vs Actual
    • Reference Models: Predictors, DPI-C and Built-in Comparators
    • Coverage & Closure
      • Designing a Coverage Model from the Spec
      • Coverage Components and Sampling
      • Bins, Illegal Bins and Ignore Bins
      • Crosses and Plan Traceability
      • Coverage Options That Change the Numbers
      • SVA cover property vs Covergroups
      • Regression Merge and the Closure Loop
      • Closing Coverage: The Holes Random Never Hits
      • Reporting and Verbosity Control
      • Code Coverage and How It Closes with Functional Coverage
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Coverage & Closure
  1. Topics
  2. UVM Verification
  3. Checking & Coverage in UVM
  4. Coverage & Closure
  5. Code Coverage and How It Closes with Functional Coverage

Part 8 · Checking & Coverage · Intermediate

Code Coverage and How It Closes with Functional Coverage

Line, branch, expression, toggle and FSM coverage: what each finds, real Verilator results on the DMA, exclusions, and why code and functional coverage each have holes the other sees.

On this page

    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?

    The idea: one measures the code, the other measures the plan

    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.

    The metrics, and what each one finds

    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 if, case and ?: was taken

    The missing else: "what if the condition is false?"

    Which combination of conditions took the branch

    Condition or expression

    Each term of a Boolean expression decided the result

    a && b always true because of a; b never mattered

    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.

    The picture: what a hole means depends on which kind it is

    Flowchart: merged results; a code-only hole leads to either a new plan item and test or a reviewed exclusion; a functional-only hole leads to new stimulus; both at 100% leads to checking whether results were compared.
    A code hole asks "did the plan forget this?"; a functional hole asks "why did the stimulus not get there?". Full marks on both still say nothing about whether the scoreboard compared the results.

    Build it: code coverage on the DMA, with Verilator

    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:

    Diagram
    # 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.dat

    Commercial 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.

    Diagram
    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 imc

    What the smoke test misses (Verilator line and branch counts)

    Here 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.

    Diagram
    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;
                    end
    • The 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 six tests: which holes close and which remain

    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

    pslverr never toggled; default arms of both case statements; mapped never false; paddr bits 0, 1, 5 to 7 never toggled

    No test accesses an unmapped or unaligned address

    Missing plan item. Add "unmapped access returns PSLVERR" to the plan and a directed test

    else of if (!busy) for LEN and KEY; expression (busy==1) never decided it

    No test in dma_tb.sv writes LEN or KEY during a job

    Missing plan item. This is war story 9's rule; the busy-LEN register test in dma_ral_tb.sv exercises it, so merge that bench's runs too

    len and count bits 5 to 15 never toggled

    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

    else of if (pwdata[1]) in the STATUS write

    Every STATUS write in these tests clears DONE

    Small test: clear ERR alone

    Some key bits never went 1 to 0

    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.

    Exclusions and their review

    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.

    Common pitfall

    Exclusion files outlive the RTL they were written for. A line excluded as unreachable last month may be reachable after an RTL change. Tools match exclusions by file and line or by a checksum of the code; review the exclusion list at every RTL drop, and treat "exclusion no longer matches" warnings as failures.

    Why 100% on one side and holes on the other both happen

    • 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.

    Merging and the closure meeting

    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.

    What breaks: the rule nobody tested and the model got wrong

    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.

    Interview

    "You have 100% functional coverage. Why do you still need code coverage?"

    A weak answer: "Management wants both numbers for sign-off."

    A strong answer: "Functional coverage measures my plan, so it is blind to whatever the plan forgot. Code coverage is generated from the RTL, so it is an independent check on the plan's completeness: an untoggled error output or an untaken else arm usually means a feature with no plan item. The reverse also holds: code coverage cannot see values, combinations or sequences, like a zero-length job or back-to-back beats, so I need both. Each code hole becomes a test, a new plan item or a reviewed exclusion, and I merge only passing runs on one build."

    Follow-ups: What does toggle coverage tell you that line coverage does not? (stuck or unused bits, untested ports and upper counter bits). When is an exclusion acceptable? (unreachable or unbuilt code, with a written reason the designer has reviewed).

    Check yourself Line coverage on the DMA's 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.

    Check yourself Code coverage shows 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.

    Check yourself After an RTL change, last month's merged code coverage is 97% and this week's is 91%, with the same tests. What happened, and what do you do?

    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.

    Related topics

    • Up: Coverage & Closure

    • Regression Merge and the Closure Loop

    • Closing Coverage: The Holes Random Never Hits

    Previous

    146 / 280

    Next: UVM Register Abstraction Layer (RAL)

    Up next

    Up nextUVM Register Abstraction Layer (RAL)

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Designing a Coverage Model from the Spec
    • Coverage Components and Sampling
    • Bins, Illegal Bins and Ignore Bins