Day 18 of 25
← Day 17 Day 19 →
HomeVerification SeriesDay 18 — Code Coverage
▶ Verification Series — Day 18

Code Coverage

Master all structural coverage metrics — line, statement, branch, condition, expression, toggle, and FSM — and learn how to enable them in industry simulators, merge multi-run databases, apply exclusion pragmas, and interpret results to close coverage without false confidence.

🕑 20 min read 💻 SV + Tcl examples By EcrioniX · Updated 22 Jun 2026
On this page
  1. Why Code Coverage?
  2. Coverage Metric Types
  3. Enabling Coverage in Simulators
  4. Pragma Exclusions
  5. Merging Coverage Databases
  6. Tradeoffs and Limitations
  7. Coverage Closure Strategy
  8. FAQ

1. Why Code Coverage?

Functional coverage tells you whether you exercised the specification. Code coverage tells you whether you exercised the implementation. The two are complementary: you can have 100% functional coverage but still leave large portions of RTL unexecuted because those code paths were never mapped to a cover point. Conversely, you can hit every RTL line without ever checking that the outputs were correct.

Code coverage is a negative indicator — low code coverage is a strong signal that testing is incomplete. High code coverage is a necessary but not sufficient condition for sign-off. Industry practice targets 90–100% line/branch coverage and 80–95% toggle coverage at tape-out, with documented exclusions for the remainder.

Key insight: Code coverage is automatically collected by the simulator from your existing test suite — no additional testbench code is required. You simply enable it at compile/simulation time and analyze the resulting database.

2. Coverage Metric Types

Modern EDA simulators collect six main structural coverage metrics. Understanding each is essential for interpreting coverage reports and crafting targeted tests to fill gaps.

MetricWhat It TracksGranularityTypical Target
LineEach source line executed at least oncePer line100%
StatementEach statement (finer than line if multiple per line)Per statement100%
BranchBoth true and false outcomes of every if/case/ternaryPer branch arm100%
ConditionEach sub-expression in a Boolean condition evaluated true and falsePer condition90%+
ExpressionAll unique combinations of condition values (MC/DC or full)Per expression90%+
ToggleEach net/register bit transitions 0→1 and 1→0Per bit85–95%
FSMEach state visited, each arc (transition) takenPer state/arc100% states, 90%+ arcs

Line and Statement Coverage

Line coverage is the coarsest metric. A line is "covered" if the simulator executed the code on that line at least once. Statement coverage goes one step deeper: if you write a = 1; b = 2; on a single line, statement coverage tracks both assignments independently.

In practice, the vast majority of verification teams use statement coverage because it is more precise. A line can appear covered while a statement on that same line is dead. Always look at the statement number, not just the highlighted line in the HTML report.

Branch Coverage

Branch coverage requires both the true and false paths of every conditional to be taken. For an if-else, this means both the if-body and the else-body must execute. For a case statement, every case item including default must be reached. For a ternary operator (sel ? a : b), both sel=1 and sel=0 paths must occur.

if (cond) TRUE branch cond=1 (must hit) FALSE branch cond=0 (must hit) merge / continue

Condition and Expression Coverage

Condition coverage requires each individual Boolean sub-expression to evaluate both true and false independently, regardless of the overall expression outcome. Expression coverage (also called MC/DC — Modified Condition/Decision Coverage in safety-critical contexts) requires each condition to independently affect the decision outcome. These metrics expose bugs that branch coverage misses in complex multi-condition guards.

// Branch coverage: only needs (a&&b&&c) = true AND false
// Condition coverage: each of a, b, c must be true and false independently
// Expression/MC-DC: each of a, b, c must independently affect the outcome
if (a && b && !c) begin
  // branch body
end

// Ternary — branch coverage needs sel=1 and sel=0
assign out = sel ? data_a : data_b;

Toggle Coverage

Toggle coverage operates at the bit level. For each net and register bit, the tool checks whether it saw a low-to-high transition (0→1) and a high-to-low transition (1→0) during the simulation run. Bits that never toggle are highlighted as "stuck." Common causes include undriven outputs, constants tied to ports, and inputs exercised only in one polarity.

Toggle coverage is particularly useful for catching analog/mixed-signal interface bits and scan-chain signals that are architecturally valid but never stimulated by the test suite. It also catches multi-bit buses where certain bit positions are never exercised.

FSM Coverage

When the simulator identifies a finite state machine (by detecting a register used in a case on its own value), it automatically instruments both state coverage (every state reached) and arc coverage (every transition taken). FSM coverage is extremely valuable for controllers with many states — it directly maps to the state-transition diagram in the specification.

// Tool auto-detects this as an FSM and instruments states + arcs
typedef enum logic [1:0] {
  IDLE  = 2'b00,
  FETCH = 2'b01,
  EXEC  = 2'b10,
  STALL = 2'b11
} state_t;

state_t state, next_state;

always_ff @(posedge clk or posedge rst) begin
  if (rst) state <= IDLE;
  else     state <= next_state;
end

always_comb begin
  case (state)
    IDLE:  next_state = req    ? FETCH : IDLE;
    FETCH: next_state = ready  ? EXEC  : STALL;
    EXEC:  next_state = done   ? IDLE  : EXEC;
    STALL: next_state = resume ? FETCH : STALL;
  endcase
end

3. Enabling Coverage in Simulators

Coverage collection is disabled by default in all commercial simulators because it adds overhead to compile time and simulation speed (typically 10–30% slowdown). You enable it explicitly at compile and simulation time.

Synopsys VCS

# Compile: enable all coverage types
vcs -full64 -sverilog \
    -cm line+cond+fsm+branch+tgl \
    -cm_dir ./coverage.vdb \
    top.sv tb.sv

# Simulate: collect to the same VDB
./simv -cm line+cond+fsm+branch+tgl \
       -cm_dir ./coverage.vdb \
       -cm_name run1

# Generate HTML report
urg -dir coverage.vdb -report urgReport -format html

Mentor/Siemens Questa / ModelSim

# Compile with coverage instrumentation vlog -sv +cover=bcefst top.sv tb.sv # b=branch c=condition e=expression f=fsm s=statement t=toggle # Simulate vsim -coverage -do "run -all; coverage save -onexit run1.ucdb; quit" # Generate report vcover report -html -output covhtmlreport run1.ucdb

Cadence Xcelium

# Compile + elaborate
xmvlog -sv -coverage all top.sv tb.sv
xmelab -coverage all worklib.top:sv

# Simulate
xmsim -coverage -covworkdir ./cov_work worklib.top:sv

# Report via IMC (Integrated Metrics Center)
imc -load cov_work/scope/runs/run1 -exec gen_report.tcl
Performance tip: Enable toggle coverage only for the top-level signals and one or two levels of hierarchy. Full-chip toggle coverage can slow simulation by 3–5x. Use -cm_hier cover.cfg (VCS) or -covfile cover.tcl (Questa) to restrict which modules are instrumented.

4. Pragma Exclusions

Not every uncovered line is a verification gap. Reset states, debug-only paths, synthesis-excluded blocks, and constant tie-offs are valid reasons to exclude code from coverage accounting. Simulators provide inline pragma comments for this purpose.

VCS / Questa Shared Pragmas

// Exclude an entire block from all coverage types // coverage off always_ff @(posedge clk) begin debug_reg <= debug_data; // simulation-only debug register end // coverage on // Exclude a single line from toggle coverage only assign tie_off = 1'b0; // tgl off // Questa: exclude a specific branch if (PARAM_MODE == 0) begin // coverage off -item b // this branch never taken in mode 1 configs end // coverage on -item b

Exclusion File Syntax (Questa)

For large blocks or entire modules, maintain a separate exclusion file rather than polluting RTL with pragmas. The exclusion file is applied at report generation time, keeping the RTL clean.

# exclusions.do — Questa exclusion file # Exclude entire module from toggle coverage coverage exclude -scope /tb/dut/debug_monitor -togglenode * # Exclude a specific line range from statement coverage coverage exclude -srcfile top_ctrl.sv -line 145 -to 162 -item s # Exclude an FSM state never reachable in this config coverage exclude -scope /tb/dut/ctrl -fsmstate SLEEP # Apply when generating report # vcover report -html -exclude exclusions.do merged.ucdb
Best practice: Never exclude code silently. Each exclusion must have a written justification in the coverage closure document: design intent, bug number, or configuration restriction. Reviewers check exclusions at sign-off as carefully as uncovered holes.

5. Merging Coverage Databases

In real projects, you run hundreds or thousands of regression tests in parallel on a compute farm. Each simulation produces its own coverage database. The merge step combines all individual run databases into one, giving you the union of all coverage hits — the cumulative coverage of your entire test suite.

Questa Merge Flow

# Merge all UCDB files produced by regression vcover merge merged.ucdb \ run_smoke.ucdb \ run_directed.ucdb \ run_random_0.ucdb \ run_random_1.ucdb \ run_random_2.ucdb # Generate HTML report from merged database vcover report -html -output cov_report merged.ucdb # Quick summary to stdout vcover report -summary merged.ucdb

VCS Merge Flow

# VCS stores databases as directories (.vdb) # urg merges multiple VDBs and generates reports urg -dir run1.vdb \ -dir run2.vdb \ -dir run3.vdb \ -report urgReport \ -format both # Merge into a single combined VDB for incremental additions urg -dir run1.vdb -dir run2.vdb \ -dbname merged.vdb \ -nomap

Understanding Merge Semantics

Coverage databases record each coverage point as a hit/miss bit (for line, branch, FSM) or a hit count (for toggle transitions). Merging takes the logical OR — if any run hit a coverage point, it is marked covered in the merged result. There is no subtraction: once covered, always covered in the merge. This means you can add new test runs incrementally to an existing merged database without reprocessing prior runs.

Incremental regression: Many teams keep a rolling merged.ucdb and append each new nightly regression run. This gives a cumulative picture of coverage across all tests ever run, making it easy to track closure progress over the project's verification lifecycle.

6. Tradeoffs and Limitations

100% Line Coverage vs. 100% Toggle Coverage

Line coverage is relatively easy to achieve — a single well-structured directed test can often push line coverage above 95% quickly. The remaining 5% typically represents error-handling paths, reset sequences, or rarely-triggered arbitration logic that requires careful directed testing.

Toggle coverage is harder at the bit level. A 64-bit data bus where 20 bits are only ever zero will show 60 uncovered toggle bits. Closing toggle coverage requires understanding the data ranges your design processes. Some bits may be architecturally impossible to toggle (e.g., reserved fields always tied to zero), and these should be excluded with justification rather than forcing artificial stimulus.

MetricEasy to Close?Value AddedCommon Exclusions
Line/StatementYes (95%+)Dead code detectionDebug blocks, synthesis pragmas
BranchModerateUntested code pathsImpossible parameter branches
Condition/ExpressionHardComplex logic bugsPhysically impossible combos
ToggleHard (bit-level)Stuck-at, unused signalsTie-offs, reserved bits
FSMModerateUnreachable states/arcsError recovery sequences

Why 100% Code Coverage Does Not Mean Bug-Free

Code coverage is a structural metric — it measures execution, not correctness. Consider a simple adder: you can achieve 100% branch coverage by driving a few input combinations, but if your test never checks the output sum, incorrect behavior goes undetected. Code coverage says "we ran this code" but not "this code did the right thing."

This is why code coverage is always used alongside:

7. Coverage Closure Strategy

Coverage closure is not just about running more random tests. It is a structured process of analyzing coverage holes, understanding why they are uncovered, and deciding to either write a targeted test or justify an exclusion.

Step-by-Step Closure Process

  1. Run regression — collect the merged database from all existing tests.
  2. Identify holes — sort uncovered items by module/block. Focus on the deepest architectural layers first (controllers, arbiters, FSMs).
  3. Classify each hole — is it a test gap, an unreachable code path, or a configuration-specific path?
  4. Write targeted tests — for test gaps, write a directed test or a constrained random sequence that forces the specific scenario.
  5. Apply exclusions — for unreachable paths, add a documented exclusion with justification.
  6. Re-merge and repeat — run the new tests, re-merge, and check the new coverage percentage.
  7. Sign-off review — present the coverage report and exclusion rationale to the DV lead or customer for approval.

Targeted Test for an Uncovered Branch

// Coverage hole: the STALL→FETCH transition is never taken // Root cause: random tests never assert 'resume' in STALL state // Fix: write a directed sequence class stall_recovery_seq extends uvm_sequence; task body(); req_item req; // Step 1: get to FETCH state send_req(req); // Step 2: assert not-ready to force STALL drive_not_ready(); @(posedge clk); // now in STALL // Step 3: assert resume — covers STALL→FETCH arc drive_resume(); @(posedge clk); // transition fires endtask endclass

Reporting Coverage to Management

Coverage numbers should always be reported as two figures: gross coverage (raw percentage including exclusions) and net coverage (after removing excluded items). A project that shows 91% gross coverage with 8% documented exclusions is effectively at 99% net coverage. This distinction is important for sign-off decisions and customer reviews.

Gate criterion: Many SoC projects set a hard gate for tapeout at 90% net statement coverage, 90% net branch coverage, 80% net toggle coverage, and 95% FSM state coverage. Anything below these thresholds requires a waiver signed by the DV manager.

✓ Day 18 Key Takeaways

Frequently Asked Questions

What is the difference between line coverage and statement coverage?
Line coverage tracks whether each source line was executed at least once. Statement coverage tracks each individual statement — a single line can contain multiple statements (e.g., a = 1; b = 2;), so statement coverage is finer-grained. In practice, most EDA tools report them together, but statement coverage is the more precise metric for identifying dead code.
What does toggle coverage measure and why does it matter?
Toggle coverage tracks whether each bit of each net and register has transitioned both 0→1 and 1→0 during simulation. It ensures that no signal is stuck at a constant value throughout the test suite. Toggle coverage is particularly important for catching undriven outputs, stuck-at faults, and inputs that were never exercised in both polarities.
How do you merge multiple UCDB files from parallel simulation runs?
Questa uses the vcover merge command: vcover merge merged.ucdb run1.ucdb run2.ucdb run3.ucdb. VCS uses urg -dir simv.vdb run2.vdb -report urgReport to merge databases. After merging, generate an HTML report with vcover report -html merged.ucdb in Questa. Always merge before evaluating your coverage closure score.
Why does 100% code coverage not guarantee a bug-free design?
Code coverage only tells you which lines or branches were executed — it says nothing about whether the correct values were observed. You can achieve 100% line coverage with a test that drives all paths but never checks any outputs. Code coverage must be combined with functional coverage, assertions, and scoreboard checking to have confidence in verification completeness.
Next → Day 19
Formal Verification
Model checking, SVA in formal tools, BMC, k-induction, and JasperGold workflow.