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.
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.
| Dimension | Code Coverage | Functional Coverage |
|---|---|---|
| Who writes it | Tool (automatic) | Verification engineer (manual) |
| What it measures | RTL lines / branches reached | Design scenarios exercised |
| 100% guarantee | All code touched | All required scenarios seen |
| Miss corner cases? | Yes — easily | Only if you forgot to model them |
| SV construct | Simulator flags / plusargs | covergroup, coverpoint, cross |
| UVM home | EDA tool config | uvm_subscriber / uvm_coverage_model_e |
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.
| Keyword | Purpose | Counts toward coverage? |
|---|---|---|
bins | Normal tracked bin | Yes |
illegal_bins | Must-never-occur — flags error when hit | No (error) |
ignore_bins | Excluded from coverage calculation | No (silently skipped) |
bins name[] | Array of auto-named bins (one per value) | Yes — each bin individually |
wildcard bins | Matches values using X/Z wildcards | Yes |
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
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]}; }
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.
| Operator | Meaning |
|---|---|
binsof(cp.bin) | Select one specific bin from a coverpoint |
binsof(cp) | All bins of a coverpoint |
&& between binsof | Intersection — both conditions must match |
|| between binsof | Union — either condition matches |
! before binsof | Complement — all bins NOT in this set |
A covergroup is useless without a trigger that tells it when to capture the current values. SystemVerilog provides two mechanisms:
// 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.
// 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
sample() inside uvm_subscriber::write(). This guarantees bins only accumulate from complete, valid transactions — not from intermediate or idle signals.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).
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")
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.
Coverage-driven verification (CDV) is the methodology where you iterate test generation until coverage converges. The loop looks like this:
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.
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.
# 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
# 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
# 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
UCDB_LIST := $(wildcard runs/*/sim.ucdb)vcover merge merged.ucdb $(UCDB_LIST)| Step | Action | Tool |
|---|---|---|
| 1 | Run regression (N seeds) | Makefile / LSF |
| 2 | Each run writes .ucdb | Simulator auto |
| 3 | Merge all .ucdb files | vcover merge |
| 4 | View HTML / text report | vcover report |
| 5 | Identify zero-hit bins | Report analysis |
| 6 | Write directed tests | Verification engineer |
| 7 | Re-run and re-merge | Repeat until 100% |
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
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.covergroup holds coverpoints; each coverpoint tracks which values or ranges have been seen in bins.cross tracks combinations; limit to 2–3 coverpoints per cross, use ignore_bins to exclude illegal combos.sample() inside uvm_subscriber::write() — never automatic clock-edge sampling.get_coverage() in check_phase and fail the test if the threshold is not met.vcover merge (Questa), imc -merge (Xcelium), or urg -dir (VCS).uvm_subscriber already provides a write() callback connected to an analysis export, so no extra TLM wiring is needed.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.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.