Day 15 / 25
← Day 14 Day 16 →
HomeVerificationDay 15 — UVM Coverage
Track 3 — UVM Architecture

UVM Coverage

By EcrioniX · Updated Jun 22, 2026

Coverage is the compass of verification — it tells you what you have tested, not just whether the RTL simulated. This day covers every coverage concept you need in UVM: covergroup anatomy, cross coverage, subscriber-based collection, get_coverage() in check_phase, and merging UCDB databases across regression runs.

20 min read Day 15 of 25 UVM / Functional Coverage
CODE COVERAGE Line / Statement Toggle Branch / Condition FSM State / Arc Tool-generated no user effort FUNCTIONAL COVERAGE covergroup / coverpoint bins (manual / auto) cross coverage illegal / ignore bins User-written in SV inside uvm_subscriber COVERAGE CLOSURE get_coverage() threshold check_phase error UCDB merge (vcover) coverage-driven loop Regression flow to 100% sign-off
Contents
  1. Functional vs Code Coverage
  2. Covergroup Basics — coverpoint and bins
  3. Automatic Binning and Manual bins
  4. Cross Coverage — bin explosion and ignore_bins
  5. Sampling — Automatic vs Manual sample()
  6. covergroup inside uvm_subscriber
  7. get_coverage() in check_phase
  8. Coverage-Driven Verification Loop
  9. UCDB Merge — vcover / urgReport
  10. Complete AXI4 Transfer Coverage Example
  11. FAQ

1. Functional vs Code Coverage

Simulation tools generate code coverage automatically by instrumenting the RTL. When every line, branch, toggle, and FSM arc is hit at least once, the tool reports 100% code coverage. This sounds good, but it says nothing about what stimulus was applied. A single random packet might touch every RTL line while never exercising a burst-length of 256, a narrow-byte-enable strobe, or back-to-back write-then-read with the same address.

Functional coverage is the answer. You write a covergroup that explicitly models what "interesting" looks like — which field values matter, which combinations are corner cases, which sequences must be seen. The simulator then tracks how much of that model has been exercised. Until every coverpoint bin is hit, verification is not complete, regardless of code coverage percentage.

DimensionCode CoverageFunctional Coverage
Who writes itTool (automatic)Verification engineer (manual)
What it measuresRTL lines / branches reachedDesign scenarios exercised
100% guaranteeAll code touchedAll required scenarios seen
Miss corner cases?Yes — easilyOnly if you forgot to model them
SV constructSimulator flags / plusargscovergroup, coverpoint, cross
UVM homeEDA tool configuvm_subscriber / uvm_coverage_model_e
Key insight: You need both. Code coverage finds dead RTL; functional coverage finds unstimulated scenarios. A mature sign-off plan requires both metrics to hit their thresholds — typically 90–100% code and 100% functional.

2. Covergroup Basics — coverpoint and bins

A covergroup is a SystemVerilog construct that defines one or more coverpoints. Each coverpoint watches a variable or expression and tracks which values (or ranges) have been seen. Each tracked value lives in a bin. When the simulator sees that value, the bin's hit count increments.

covergroup cg_axi_write;

  // Coverpoint on burst length field (3 bits)
  coverpoint axlen {
    bins single   = {0};             // 1-beat burst
    bins four     = {3};             // 4-beat burst
    bins eight    = {7};             // 8-beat burst
    bins sixteen  = {15};            // 16-beat burst
    bins others[] = default;         // all remaining values
  }

  // Coverpoint on burst type (2 bits)
  coverpoint axburst {
    bins fixed  = {2'b00};
    bins incr   = {2'b01};
    bins wrap   = {2'b10};
    illegal_bins rsvd = {2'b11};   // must never appear
  }

endgroup

The covergroup above defines two coverpoints. axlen has four named bins plus a catch-all others[] that auto-creates one bin per remaining value. axburst has three valid bins and one illegal_bins entry — if the simulator samples the reserved value 2'b11, it flags a violation and increments an illegal counter rather than a hit counter.

Bin types at a glance

KeywordPurposeCounts toward coverage?
binsNormal tracked binYes
illegal_binsMust-never-occur — flags error when hitNo (error)
ignore_binsExcluded from coverage calculationNo (silently skipped)
bins name[]Array of auto-named bins (one per value)Yes — each bin individually
wildcard binsMatches values using X/Z wildcardsYes

3. Automatic Binning and Manual bins

When you write a coverpoint with no bins clauses, the tool performs automatic binning: it creates one bin per legal value of the expression's type. For a 2-bit signal that is four bins; for an 8-bit signal that is 256 bins. This is convenient for small signals but causes a bin explosion on wider fields.

covergroup cg_auto;
  // 8-bit field: auto creates 256 bins — probably not what you want
  coverpoint data_byte;
endgroup

covergroup cg_manual;
  // Group the 256 values into meaningful ranges instead
  coverpoint data_byte {
    bins zero      = {8'h00};
    bins all_ones  = {8'hFF};
    bins low       = {[8'h01:8'h3F]};
    bins mid       = {[8'h40:8'hBF]};
    bins high      = {[8'hC0:8'hFE]};
  }
endgroup
Auto-binning trap: A 32-bit address field with auto-binning creates over 4 billion bins. The simulator will silently cap at a tool-defined limit or run out of memory. Always write manual bins for wide fields.

The [] suffix on a bin name auto-generates individual bins for each value in the list or range:

coverpoint resp_code {
  // Creates bins: resp_code[0], resp_code[1], resp_code[2], resp_code[3]
  bins all_resp[] = {[0:3]};
}

4. Cross Coverage — bin explosion and ignore_bins

Cross coverage tracks combinations of two or more coverpoints. If you have a 3-bin coverpoint for axburst and a 4-bin coverpoint for axsize, their cross produces 3 × 4 = 12 cross bins. Every combination must be seen before the cross coverpoint reaches 100%.

covergroup cg_axi_cross;

  coverpoint axburst {
    bins fixed = {0}; bins incr = {1}; bins wrap = {2};
  }

  coverpoint axsize {          // AXI size: 0=1B, 1=2B, 2=4B, 3=8B
    bins b1 = {0}; bins b2 = {1};
    bins b4 = {2}; bins b8 = {3};
  }

  // Cross: 3 bursts × 4 sizes = 12 bins
  x_burst_size: cross axburst, axsize {
    // FIXED burst with 8-byte transfers is meaningless for most DUTs
    ignore_bins fixed_8B = binsof(axburst.fixed) && binsof(axsize.b8);
  }

endgroup

The binsof() operator selects specific bins from a coverpoint for use inside cross ignore/illegal clauses. Without it, writing the filter condition manually would require a complex value expression.

Bin explosion: 8 coverpoints each with 8 bins crossed together = 88 = 16 million bins. Never cross more than 2–3 coverpoints at once. Break complex coverage models into multiple covergroups.

Useful cross bin operators

OperatorMeaning
binsof(cp.bin)Select one specific bin from a coverpoint
binsof(cp)All bins of a coverpoint
&& between binsofIntersection — both conditions must match
|| between binsofUnion — either condition matches
! before binsofComplement — all bins NOT in this set

5. Sampling — Automatic vs Manual sample()

A covergroup is useless without a trigger that tells it when to capture the current values. SystemVerilog provides two mechanisms:

Automatic sampling with an event or clocking block

// Samples on every positive edge of clk
covergroup cg_auto @(posedge clk);
  coverpoint req;
  coverpoint ack;
endgroup

// Instantiation — sampling begins automatically
cg_auto cg_inst = new();

Automatic sampling fires on every clock edge. This works for simple signal-level checks but in a UVM testbench the monitor fires transactions at irregular intervals — the covergroup would sample on idle cycles where fields have meaningless hold values.

Manual sample()

// No trigger — sampling is under programmer control
covergroup cg_txn;
  coverpoint tr.axlen;
  coverpoint tr.axburst;
  coverpoint tr.axsize;
endgroup

// Call sample() when a full transaction has been captured
function void write(axi_txn tr);
  cg_inst.sample();   // captures all coverpoint expressions at this moment
endfunction
Best practice in UVM: Always use manual sample() inside uvm_subscriber::write(). This guarantees bins only accumulate from complete, valid transactions — not from intermediate or idle signals.

6. covergroup inside uvm_subscriber

uvm_subscriber is a UVM component that extends uvm_component and provides a parameterised analysis_export and abstract write() method. It is the canonical home for coverage in a UVM testbench because:

class axi_coverage extends uvm_subscriber #(axi_txn);
  `uvm_component_utils(axi_coverage)

  // ── Transaction handle (set in write, used by coverpoints) ──
  axi_txn tr;

  // ── Covergroup defined inside the class ──────────────────
  covergroup axi_write_cg;

    cp_len: coverpoint tr.axlen {
      bins single   = {0};
      bins burst4   = {3};
      bins burst8   = {7};
      bins burst16  = {15};
      bins others[] = default;
    }

    cp_burst: coverpoint tr.axburst {
      bins fixed  = {2'b00};
      bins incr   = {2'b01};
      bins wrap   = {2'b10};
      illegal_bins rsvd = {2'b11};
    }

    cp_size: coverpoint tr.axsize {
      bins byte1 = {0}; bins byte2 = {1};
      bins byte4 = {2}; bins byte8 = {3};
    }

    cp_resp: coverpoint tr.bresp {
      bins okay   = {2'b00};
      bins exokay = {2'b01};
      bins slverr = {2'b10};
      bins decerr = {2'b11};
    }

    // Cross: burst type vs size — 3x4=12 bins
    x_burst_size: cross cp_burst, cp_size {
      ignore_bins fixed_8B =
        binsof(cp_burst.fixed) && binsof(cp_size.byte8);
    }

  endgroup : axi_write_cg

  // ── Constructor: instantiate covergroup here ─────────────
  function new(string name, uvm_component parent);
    super.new(name, parent);
    axi_write_cg = new();   // covergroup constructor
  endfunction

  // ── write() called by monitor's analysis port ────────────
  function void write(axi_txn t);
    tr = t;
    axi_write_cg.sample();  // sample ALL coverpoints now
  endfunction

endclass

Notice that the coverpoint expressions reference tr.axlen etc. — a class member. Because sample() is called immediately after assigning tr = t, the coverpoints see the current transaction fields. This is the correct pattern; do not pass fields as constructor arguments (the covergroup captures the variable reference, not the value at construction time).

7. get_coverage() in check_phase

After simulation ends, UVM's check_phase is the right place to evaluate whether coverage targets were met. Use the built-in $get_coverage() system function or the covergroup's .get_coverage() method to query the current hit percentage.

function void check_phase(uvm_phase phase);
  real cov;

  // Query this covergroup's coverage (0.0 – 100.0)
  cov = axi_write_cg.get_coverage();

  `uvm_info("COV",
    $sformatf("AXI write coverage = %.1f%%", cov),
    UVM_MEDIUM)

  // Fail the test if coverage threshold not met
  if (cov < 95.0)
    `uvm_error("COV_FAIL",
      $sformatf("Coverage %.1f%% below 95%% threshold", cov))

endfunction

You can also query individual coverpoints and cross bins:

// Individual coverpoint coverage
cov = axi_write_cg.cp_len.get_coverage();

// Check whether a specific bin was hit
if (!axi_write_cg.cp_burst.wrap.get_coverage())
  `uvm_warning("COV", "WRAP burst never seen")
Typical threshold
95–100%
AXI burst bins
3
Cross bins (3x4)
11
After ignore_bins
11 / 12

A typical project defines a coverage closure target in a coverage plan (also called a verification plan or vPlan). The plan lists every covergroup, its threshold, and which test is responsible for hitting it. The check_phase check enforces this plan automatically during regression.

8. Coverage-Driven Verification Loop

Coverage-driven verification (CDV) is the methodology where you iterate test generation until coverage converges. The loop looks like this:

Constrained Random Test rand + constraints UVM Testbench Simulation drive + monitor Coverage Collection subscriber + sample Coverage Analysis check_phase / UCDB Uncovered bins? Add directed test or relax constraints Sign-off 100% bins

When coverage stalls — usually after 70–80% with random tests — you add directed tests that explicitly exercise the missing bins. For example, if the WRAP burst bin is never hit, write a sequence that forces axburst = 2 and appropriate length constraints to satisfy the AXI WRAP alignment rules.

9. UCDB Merge — vcover / urgReport

Each simulation run writes a UCDB (Unified Coverage Database) file — .ucdb in ModelSim/Questa, .vdb in Cadence Xcelium. To get the aggregate coverage picture across all regression runs you merge these databases.

Questa / ModelSim (vcover)

# Merge three run databases into one
vcover merge merged.ucdb run_seed1.ucdb run_seed2.ucdb run_seed3.ucdb

# Generate a text report from the merged database
vcover report -detail merged.ucdb

# Generate an HTML report
vcover report -html -htmldir ./cov_html merged.ucdb

Cadence Xcelium (imc)

# Merge VDB directories
imc -load run1.vdb -merge run2.vdb -out merged.vdb

# Text report
imc -load merged.vdb -report_file cov_report.txt -show ratios

Synopsys VCS (urgReport)

# Merge multiple .vdb directories
urg -dir run1.vdb -dir run2.vdb -dir run3.vdb \
    -report summary -format text -dbname merged.vdb

# Detailed per-instance report
urgReport -dir merged.vdb -format html -output ./cov_html
Automation tip: In a regression Makefile, loop over all seed directories and build the merge command dynamically:
UCDB_LIST := $(wildcard runs/*/sim.ucdb)
vcover merge merged.ucdb $(UCDB_LIST)

Coverage merge workflow

StepActionTool
1Run regression (N seeds)Makefile / LSF
2Each run writes .ucdbSimulator auto
3Merge all .ucdb filesvcover merge
4View HTML / text reportvcover report
5Identify zero-hit binsReport analysis
6Write directed testsVerification engineer
7Re-run and re-mergeRepeat until 100%

10. Complete AXI4 Transfer Coverage Example

The following shows a production-style AXI4 write-channel coverage subscriber. It models length, burst type, size, byte enables, response code, and their relevant crosses. The check_phase enforces the coverage gate.

// ─── axi_txn.sv ─────────────────────────────────────────
class axi_txn extends uvm_sequence_item;
  `uvm_object_utils(axi_txn)

  rand logic [7:0]  axlen;    // burst length - 1
  rand logic [1:0]  axburst;  // 0=FIXED, 1=INCR, 2=WRAP
  rand logic [2:0]  axsize;   // bytes per beat = 2^axsize
  rand logic [3:0]  wstrb;    // write strobes (4-bit for 32-bit bus)
       logic [1:0]  bresp;    // write response

  function new(string name = "axi_txn");
    super.new(name);
  endfunction
endclass


// ─── axi_write_coverage.sv ───────────────────────────────
class axi_write_coverage extends uvm_subscriber #(axi_txn);
  `uvm_component_utils(axi_write_coverage)

  axi_txn tr;  // current transaction — coverpoints reference this

  covergroup axi_write_cg;

    // ── Burst length ─────────────────────────────────────
    cp_len: coverpoint tr.axlen {
      bins single   = {8'h00};
      bins burst4   = {8'h03};
      bins burst8   = {8'h07};
      bins burst16  = {8'h0F};
      bins burst32  = {8'h1F};
      bins burst256 = {8'hFF};
      bins other[]  = default;
    }

    // ── Burst type ───────────────────────────────────────
    cp_burst: coverpoint tr.axburst {
      bins fixed  = {2'b00};
      bins incr   = {2'b01};
      bins wrap   = {2'b10};
      illegal_bins rsvd = {2'b11};
    }

    // ── Transfer size ────────────────────────────────────
    cp_size: coverpoint tr.axsize {
      bins byte1 = {3'b000};
      bins byte2 = {3'b001};
      bins byte4 = {3'b010};
      bins byte8 = {3'b011};
    }

    // ── Write strobe patterns ────────────────────────────
    cp_strb: coverpoint tr.wstrb {
      bins all_zero  = {4'b0000};
      bins byte0     = {4'b0001};
      bins byte1     = {4'b0010};
      bins byte2     = {4'b0100};
      bins byte3     = {4'b1000};
      bins halfword0 = {4'b0011};
      bins halfword1 = {4'b1100};
      bins all_ones  = {4'b1111};
      bins other[]   = default;
    }

    // ── Write response ───────────────────────────────────
    cp_resp: coverpoint tr.bresp {
      bins okay   = {2'b00};
      bins exokay = {2'b01};
      bins slverr = {2'b10};
      bins decerr = {2'b11};
    }

    // ── Cross: burst type × size ─────────────────────────
    x_burst_size: cross cp_burst, cp_size {
      // FIXED burst with 8-byte size is illegal in AXI4
      ignore_bins fixed_8B =
        binsof(cp_burst.fixed) && binsof(cp_size.byte8);
    }

    // ── Cross: length × burst type ───────────────────────
    x_len_burst: cross cp_len, cp_burst {
      // WRAP requires len = 1, 3, 7, or 15; exclude others here
      ignore_bins wrap_single =
        binsof(cp_burst.wrap) && binsof(cp_len.single);
      ignore_bins wrap_256 =
        binsof(cp_burst.wrap) && binsof(cp_len.burst256);
    }

  endgroup : axi_write_cg

  // ── Constructor ──────────────────────────────────────────
  function new(string name, uvm_component parent);
    super.new(name, parent);
    axi_write_cg = new();
  endfunction

  // ── Analysis write() ─────────────────────────────────────
  function void write(axi_txn t);
    tr = t;
    axi_write_cg.sample();
  endfunction

  // ── Coverage gate in check_phase ─────────────────────────
  function void check_phase(uvm_phase phase);
    real cov = axi_write_cg.get_coverage();
    `uvm_info("AXI_COV",
      $sformatf("AXI4 write coverage = %.2f%%", cov), UVM_LOW)
    if (cov < 95.0)
      `uvm_error("AXI_COV",
        $sformatf("Coverage %.2f%% is below 95%% threshold", cov))
  endfunction

endclass


// ─── env.sv: wire subscriber to monitor ──────────────────
class axi_env extends uvm_env;
  `uvm_component_utils(axi_env)

  axi_monitor        mon;
  axi_write_coverage cov;
  axi_scoreboard     sb;

  function void build_phase(uvm_phase phase);
    mon = axi_monitor::type_id::create("mon", this);
    cov = axi_write_coverage::type_id::create("cov", this);
    sb  = axi_scoreboard::type_id::create("sb",  this);
  endfunction

  function void connect_phase(uvm_phase phase);
    // Monitor broadcasts to BOTH coverage and scoreboard
    mon.ap.connect(cov.analysis_export);
    mon.ap.connect(sb.analysis_export);
  endfunction

endclass
Key pattern: mon.ap is a uvm_analysis_port. Connecting it to both cov.analysis_export and sb.analysis_export is a fan-out — both receive every transaction the monitor emits. This is the standard UVM multi-subscriber pattern.

Day 15 Key Takeaways

FAQ

What is the difference between functional coverage and code coverage?
Code coverage (line, toggle, branch, FSM) measures which RTL statements were executed — it answers whether the simulator touched the code. Functional coverage (covergroup/coverpoint) measures whether the design was exercised with the correct scenarios — it answers whether meaningful corner-cases were tested. 100% code coverage does not mean all interesting behaviour was stimulated; functional coverage closes that gap by letting the verification engineer explicitly model what matters.
Why put a covergroup inside a uvm_subscriber instead of the monitor?
Separation of concerns: the monitor's job is to observe the bus and emit transactions; the subscriber's job is to collect coverage from those transactions. This way you can independently enable or disable coverage collection per simulation run, swap in different coverage models, or reuse the monitor in a scoreboard context without dragging coverage overhead. uvm_subscriber already provides a write() callback connected to an analysis export, so no extra TLM wiring is needed.
When should I call sample() manually versus using automatic sampling?
Automatic sampling (triggered by a clocking-block event or an event inside the covergroup) is convenient but fires on every clock edge, which can pollute bins with idle-cycle noise. Manual sample() inside the subscriber's write() method fires exactly when a complete transaction has been captured, so bins only accumulate meaningful data. Prefer manual sample() in UVM flows; reserve automatic sampling for simple signal-level checks.
How do I merge coverage across multiple simulation runs?
Each run generates a UCDB (.ucdb) file. Use the vcover merge command — for example: vcover merge merged.ucdb run1.ucdb run2.ucdb run3.ucdb — to union the hit counts. Then run vcover report -html -htmldir ./html merged.ucdb to view combined coverage. In Cadence Xcelium the equivalent is imc -load run1.vdb -merge run2.vdb -out merged.vdb. In Synopsys VCS use urg -dir run1.vdb -dir run2.vdb -dbname merged.vdb.