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.
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.
Modern EDA simulators collect six main structural coverage metrics. Understanding each is essential for interpreting coverage reports and crafting targeted tests to fill gaps.
| Metric | What It Tracks | Granularity | Typical Target |
|---|---|---|---|
| Line | Each source line executed at least once | Per line | 100% |
| Statement | Each statement (finer than line if multiple per line) | Per statement | 100% |
| Branch | Both true and false outcomes of every if/case/ternary | Per branch arm | 100% |
| Condition | Each sub-expression in a Boolean condition evaluated true and false | Per condition | 90%+ |
| Expression | All unique combinations of condition values (MC/DC or full) | Per expression | 90%+ |
| Toggle | Each net/register bit transitions 0→1 and 1→0 | Per bit | 85–95% |
| FSM | Each state visited, each arc (transition) taken | Per state/arc | 100% states, 90%+ arcs |
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 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.
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 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.
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
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.
# 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
# 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
# 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
-cm_hier cover.cfg (VCS) or -covfile cover.tcl (Questa) to restrict which modules are instrumented.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.
// 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
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
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.
# 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 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
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.
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.
| Metric | Easy to Close? | Value Added | Common Exclusions |
|---|---|---|---|
| Line/Statement | Yes (95%+) | Dead code detection | Debug blocks, synthesis pragmas |
| Branch | Moderate | Untested code paths | Impossible parameter branches |
| Condition/Expression | Hard | Complex logic bugs | Physically impossible combos |
| Toggle | Hard (bit-level) | Stuck-at, unused signals | Tie-offs, reserved bits |
| FSM | Moderate | Unreachable states/arcs | Error recovery sequences |
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:
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.
// 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
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.
// coverage off/on) keep RTL clean; exclusion files at report-time keep RTL pristine — always document the reason.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.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.