Code coverage is a structural metric that measures how much of your RTL design has been exercised by your test suite. Unlike functional coverage — which tracks whether design behaviors have been verified — code coverage tracks whether design code has been executed. Together, both metrics drive a verification closure strategy that gives teams confidence before tapeout.

This article introduces the key types of code coverage, explains how they are measured, and clarifies what code coverage can and cannot tell you about design quality.

What You'll Learn

  • What code coverage measures and how it is collected during simulation
  • The main types of RTL code coverage and what each one tracks
  • Why 100% code coverage is often impractical and what to do instead
  • How code coverage fits into a broader verification closure strategy

How Code Coverage Is Measured

During simulation, the EDA tool instruments the RTL source with tracking probes at compile time. As the testbench drives stimulus and the simulator executes, each probe records whether that piece of code was hit. At the end of the run, the tool writes a coverage database (commonly a .vdb or .ucdb file depending on the simulator) that accumulates results across all test runs.

A typical flow looks like this:

  1. Compile the RTL and testbench with coverage instrumentation enabled (e.g., -cm line+cond+toggle in VCS or -coverage in Questa).
  2. Run all tests. Each test appends its data to the shared coverage database.
  3. Merge the databases from all runs into a single report.
  4. Analyze coverage holes — areas that were never hit — and decide whether to add more tests or exclude unreachable code.

Coverage data is cumulative. A statement covered by one test remains covered even if later tests never hit it. Merge all regression runs before analyzing holes.

Where Is Code Coverage Used?

Code coverage is a standard exit criterion in block-level and chip-level verification. Most project verification plans define minimum thresholds — often 90–95% line and branch coverage — that must be met before a design block is signed off and handed to integration. Coverage-driven regression managers use hole analysis to guide stimulus generation and close remaining gaps efficiently.

Types of RTL Code Coverage

Each coverage type inspects a different structural dimension of the RTL. Enabling all types gives the most complete picture, though each adds some simulation overhead.

Statement (Line) Coverage

What it tracks: Whether each executable line of RTL was reached at least once.

This is the coarsest metric. A line is marked covered the first time the simulator executes it. If an else branch never runs, all statements inside it remain uncovered.


always_ff @(posedge clk) begin
    if (enable)
        data_out <= data_in;   // Line covered if enable=1 occurs
    else
        data_out <= '0;        // Line covered if enable=0 occurs
end

Branch Coverage

What it tracks: Whether every decision point (if, case, ternary) has been exercised for both true and false outcomes.

Branch coverage goes further than statement coverage. The key difference is how they handle if statements with no else. Consider this example:


// No else branch
if (enable)
    data_out <= data_in;   // Only line here

If enable is always 1, statement coverage reports 100% — the only line ran. But branch coverage flags a hole: the false path (enable=0, do nothing) was never taken. Branch coverage always tracks both directions of a decision, even when there is no explicit else. This makes it strictly more powerful than statement coverage — you can have 100% line coverage with significant branch holes.


// Both explicit branches must be hit for full branch coverage
if (overflow_flag)
    result <= MAX_VAL;   // Branch: true path
else
    result <= computed;  // Branch: false path

For case statements, every arm — including default — must be reached.

A case statement without a default arm creates unreachable branch coverage holes for undefined input values. Always include default in synthesizable code.

Quick Check

Q: A testbench always drives enable=1. An if (enable) block has no else. Which metric flags a hole — statement coverage, branch coverage, or both?

Show Answer

Branch coverage flags a hole. The single line inside the if was executed, so statement coverage sees 100%. But branch coverage requires the false path (enable=0) to also be taken, so it reports the missing direction as a hole.

Condition Coverage

What it tracks: Whether each Boolean sub-expression (operand) inside a compound condition has independently been seen as true and false at some point across all tests.

Branch coverage only checks whether the overall decision was true or false. It says nothing about the individual operands that make up that decision. Condition coverage goes one level deeper.


if (req_valid && !fifo_full)
    accept_req <= 1'b1;

Branch coverage requires this condition to have evaluated to true at least once and false at least once. Condition coverage additionally requires that req_valid was seen as both 0 and 1, and fifo_full was seen as both 0 and 1 — at some point across the test suite, not necessarily in the same test.

Expression Coverage

What it tracks: Whether each operand in a Boolean expression independently influenced the outcome — meaning flipping that operand alone changed the result.

This is where expression coverage differs from condition coverage. Condition coverage only checks that each operand was seen in both states at some point. It does not verify that the operand actually mattered to the result. Expression coverage closes this gap by requiring a test pair for each operand: one where the operand is 0 and causes the expression to change, and one where it is 1 and does the same — with all other operands held constant.


if (a && b && c)
    grant <= 1'b1;

For expression coverage to be satisfied, you need to demonstrate that each of a, b, and c independently controls the outcome. The critical scenario it catches is masking: if a is always 0 in your test suite, the AND expression is always false regardless of b and c. Their values are masked and irrelevant. Condition coverage would still pass (it saw b and c toggle), but expression coverage flags the hole because neither b nor c ever had the chance to change the outcome on their own.

Expression coverage is closely related to MC/DC (Modified Condition/Decision Coverage), which is mandated for safety-critical avionics software under DO-178C. The underlying concept — proving each operand independently influences the result — is the same.

In practice, enable branch + condition + expression together. The combined runtime overhead is small, and the three metrics together catch significantly more logic holes than any one of them alone.

Toggle Coverage

What it tracks: Whether each signal bit has transitioned both 0→1 and 1→0 at least once during simulation.

Toggle coverage is particularly useful for identifying signals that are stuck at a constant value throughout the test suite. A signal that never toggles may indicate a missing stimulus scenario or, in some cases, dead logic in the design.


// If reset_n is driven low only at time 0 and never toggled again,
// the 1->0 transition is missing from toggle coverage
input logic reset_n

Toggle coverage on large buses can generate huge numbers of bins. It is common practice to exclude memory arrays and clock-gating cells from toggle coverage to keep runtime and report size manageable.

FSM Coverage

What it tracks: Whether all states and all legal state transition arcs in a finite state machine have been exercised.

EDA tools can automatically detect FSM state registers by recognizing the encoding pattern and extract the full state graph from the RTL. FSM coverage then reports two separate metrics: state coverage (which states were entered) and transition arc coverage (which directed edges between states were taken).

State Coverage vs Transition Arc Coverage

These two sub-metrics are independent and both matter. Consider a simple arbiter FSM:


typedef enum logic [1:0] {
    IDLE  = 2'b00,
    REQ_A = 2'b01,
    REQ_B = 2'b10,
    GRANT = 2'b11
} arb_state_t;

Suppose the testbench always sequences IDLE → REQ_A → GRANT → IDLE. All four states are visited, so state coverage reports 100%. But the transition arcs IDLE → REQ_B and REQ_B → GRANT were never taken. Transition arc coverage flags these as holes — meaning the entire scenario where port B wins arbitration was never tested.

This is the critical insight: visiting a state is not the same as exercising every way to reach it or leave it. A state like GRANT might be reachable from both REQ_A and REQ_B, but if only one of those arcs is ever taken, the other represents an untested protocol path.

FSM transition arc coverage directly maps to protocol scenario coverage. Each uncovered arc typically corresponds to a specific sequence of events — a request type, an error condition, a timeout — that the testbench has never generated.

Coverage tools report transition arcs as source-destination pairs, for example:


// Covered arcs
IDLE    -> REQ_A  : covered
REQ_A   -> GRANT  : covered
GRANT   -> IDLE   : covered

// Uncovered arcs (holes)
IDLE    -> REQ_B  : NOT covered
REQ_B   -> GRANT  : NOT covered

Each uncovered arc drives a specific targeted test: force the FSM into the source state and apply the input condition that triggers that transition.

Why FSM Coverage Matters in Protocol Design

Bus protocol controllers — such as those implementing AXI, PCIe, or USB handshake sequences — use state machines with tens of states and dozens of transitions. A controller might have 12 states but 40+ legal arcs between them. Achieving full state coverage with a handful of tests is straightforward; closing all transition arcs requires systematic scenario generation, particularly for error-recovery and timeout paths that normal operation never triggers.

Quick Check

Q: An FSM has 8 states and all states are visited. Does that guarantee full FSM coverage?

Show Answer

No. FSM coverage also requires that all transition arcs between states are exercised. A state can be entered via one path while other arcs leading into or out of it remain untested. Full FSM coverage requires both 100% state coverage and 100% transition arc coverage.

Coverage Types at a Glance

Coverage TypeWhat It ChecksTypical Use
StatementEvery line executed at least onceBaseline completeness check
BranchBoth sides of every decision pointLogic path verification
ConditionEach Boolean sub-expression independently toggledComplex conditional logic
ExpressionAll operand combinations in expressionsMasking detection
ToggleEach signal bit transitions both 0→1 and 1→0Stuck-at signal detection
FSMAll states and transitions visitedProtocol state machine verification

100% Code Coverage — Achievable or Not?

100% code coverage is a well-defined goal but rarely achievable in practice for a complete SoC or even a complex block. There are two main reasons for this:

  • Unreachable code: Some branches exist to handle theoretically impossible input combinations — for example, the default arm of a case on a 2-bit FSM with only three defined states. Simulation can never reach these, regardless of how many tests you run.
  • Complexity and simulation time: Exhaustively exercising every path through a large design would require an impractical number of test cycles.

The standard practice is to reach a project-defined threshold (commonly 90–95% for line/branch, with separate targets per metric) and then formally exclude any legitimately unreachable code from the coverage calculation. Exclusions must be reviewed and justified — they are not a way to paper over inadequate testing.

Coverage exclusions should be documented in the verification plan and reviewed by the lead verification engineer. Undocumented exclusions are a common source of audit findings during tapeout review.

Why Code Coverage Alone Is Not Enough

A critical principle in verification: code coverage measures execution, not correctness. A test that drives a signal to the wrong value but still exercises the line of code counts as covered. The following limitations apply to all structural coverage metrics:

  • No output checking: Code coverage does not verify that the DUT produced the correct output. A buggy datapath can achieve 100% line coverage with a testbench that never checks results.
  • No semantic awareness: Coverage tools see RTL lines and branches, not design intent. A missing feature that was never coded produces no coverage hole.
  • Corner cases may be unreachable by random stimulus: Certain error conditions and boundary values are difficult to hit with constrained-random generation alone, even with high coverage numbers.
  • No quality assessment of tests: A single poorly constructed test that sweeps through all branches does not validate the design as thoroughly as a suite of targeted, well-specified tests — yet both can show 100% coverage.

High code coverage does not imply a bug-free design. Always pair code coverage with functional coverage, assertions, and a well-defined verification plan to achieve meaningful verification closure.

Code Coverage in a Verification Closure Flow

Production verification flows use code coverage as a necessary but not sufficient condition for signoff. A typical closure checklist requires all structural coverage metrics to exceed their thresholds and all functional coverage points to be closed and all formal properties to pass — only the combination gives sufficient confidence to release a block for integration.

Quick Check

Q: A testbench achieves 98% branch coverage but the DUT has a known output-correctness bug. What is the most likely reason code coverage did not catch it?

Show Answer

Code coverage only tracks whether branches were executed, not whether the output was correct. Without a checker or assertion validating the DUT's output against the expected value, the bug is invisible to coverage tools.

Common Mistakes

Mistake #1: Treating code coverage as the sole verification closure metric

Teams sometimes declare verification done when branch and line coverage targets are met, without closing functional coverage or reviewing assertion results. Code coverage confirms structural execution — it says nothing about whether the design behaves correctly. A verification plan must define closure criteria across all metric types.

Mistake #2: Using blanket exclusions to inflate coverage numbers

Excluding large blocks of code because they are "hard to hit" — rather than formally unreachable — defeats the purpose of coverage. Every exclusion should be tied to a documented justification (e.g., "this default arm guards against a 3-bit value on a 2-state enum; synthesis tool guarantees this is unreachable").

Mistake #3: Analyzing coverage per-run instead of merging all runs

Coverage from a single test run shows only what that test hit. Meaningful analysis requires merging the databases from the entire regression suite. Reporting per-run numbers makes coverage look lower than it actually is and leads to redundant test development.

Mistake #4: Ignoring toggle coverage on interface signals

Toggle coverage on DUT inputs and outputs is particularly valuable — if an input signal never transitions, the stimulus is incomplete, and entire classes of scenarios remain untested. Reviewing toggle holes on the interface first is an efficient way to identify large verification gaps quickly.

Summary

In this tutorial, you learned:

  • Code coverage is collected by simulator instrumentation and stored in a coverage database that accumulates across all regression runs
  • The main types — statement, branch, condition, expression, toggle, and FSM — each capture a different structural dimension of RTL execution
  • 100% code coverage is rarely achievable; project-defined thresholds with documented exclusions are the practical standard
  • Code coverage measures execution, not correctness — it must be paired with functional coverage, assertions, and a solid verification plan for meaningful closure
Did you find what you were looking for?

Quiz

No quiz questions available for this article.