Coverage-driven verification in UVM — covergroups inside components and objects, coverpoints, bins and ranges, cross coverage, sampling strategies, the dedicated coverage collector pattern, coverage goals, and how coverage drives test closure.
Constrained-random stimulus generates a very large number of transactions — but how do you know whether those transactions have exercised the important corner cases? Running 100,000 random APB transactions does not guarantee that you ever tested a read immediately after a write to the same register, or a write with only one byte lane active, or an access to the highest address in the map.
Functional coverage answers the question “have we done enough testing?” by counting how many times specific scenarios of interest have occurred during simulation. A covergroup defines a set of interesting conditions; the simulator counts how many of them have been hit. When all coverage bins are filled, you have confidence that all planned scenarios have been exercised.
This is the basis of coverage-driven verification: generate random stimulus, measure coverage, identify gaps, add targeted constraints or directed tests to fill the gaps, and repeat until coverage goals are met.
A covergroup is a SystemVerilog construct that defines a set of measurements to make. It must be instantiated before it can be sampled — declaring a covergroup type alone does not create a counter. Sampling is done by calling the covergroup’s sample() method.
// ── Covergroup declaration ──────────────────────────────── // A covergroup type — like a class, this is a template covergroup apb_transfer_cg; cp_rw: coverpoint txn.read_not_write { bins rd = {1}; bins wr = {0}; } cp_addr: coverpoint txn.addr { bins low_regs = {[12'h000:12'h00F]}; bins high_regs = {[12'h010:12'h01F]}; } endgroup // ── Instantiation — creates the actual counters ─────────── // Must be done in a constructor or initial block apb_transfer_cg cg_inst = new(); // ── Sampling — increments counters ─────────────────────── txn.addr = 12'h004; txn.read_not_write = 1; cg_inst.sample(); // increments rd bin in cp_rw; low_regs in cp_addr // ── Query coverage percentage ───────────────────────────── real pct = cg_inst.get_coverage(); // 0.0 to 100.0
sample() on an uninstantiated covergroup causes a null handle error at runtime. Always instantiate in the constructor of the component or object that owns the covergroup.A coverpoint measures how many distinct values a particular expression has taken. By default, UVM creates automatic bins for every possible value of the expression. For wide fields this creates an enormous number of bins — use explicit bins to define only the scenarios of interest.
// ── Coverpoint with automatic bins (all values counted) ── covergroup simple_cg; cp_rw: coverpoint txn.read_not_write; // auto bins: 0, 1 endgroup // ── Explicit value bins ─────────────────────────────────── covergroup apb_cg; cp_addr: coverpoint txn.addr { bins dir_reg = {12'h000}; // exact value bins data_reg = {12'h004}; bins ien_reg = {12'h008}; bins other = default; // everything else } // Range bins — multiple values in one bin cp_data: coverpoint txn.write_data { bins zero = {32'h0}; bins all_ones = {32'hFFFF_FFFF}; bins low_byte = {[32'h1:32'hFF]}; bins other = default; } // Transition bins — sequence of values over time cp_rw_sequence: coverpoint txn.read_not_write { bins wr_then_rd = (0 => 1); // write followed by read bins rd_then_wr = (1 => 0); // read followed by write } // Ignore specific values cp_pprot: coverpoint txn.pprot { ignore_bins rsvd = {3'b110, 3'b111}; // reserved — do not track } // Illegal bins — flag if ever seen cp_strobe: coverpoint txn.byte_en { illegal_bins all_zero_write = {4'b0000} iff (!txn.read_not_write); // writes must have ≥1 lane } endgroup
| Bin type | Syntax | What it measures |
|---|---|---|
| Explicit value | bins name = {value}; | Counts how many times that exact value was sampled |
| Range | bins name = {[lo:hi]}; | Counts how many times any value in range was sampled |
| Default | bins other = default; | Catches all values not covered by explicit bins |
| Transition | bins name = (a => b); | Counts how many times value changed from a to b across consecutive samples |
| Ignore | ignore_bins name = {val}; | Excludes these values from coverage calculation — not an error if never hit |
| Illegal | illegal_bins name = {val}; | Reports an error if these values are ever sampled |
| Conditional (iff) | bins name = {v} iff (expr); | Only counts the sample if the guard expression is true |
Cross coverage measures the combination of two or more coverpoints — it counts how many distinct pairings of values from each coverpoint have occurred. A cross between read/write and address verifies that both reads AND writes have been tested for EVERY register, not just some registers for each operation type.
covergroup apb_cross_cg;
cp_rw: coverpoint txn.read_not_write {
bins rd = {1}; bins wr = {0};
}
cp_addr: coverpoint txn.addr {
bins dir_reg = {12'h000};
bins data_reg = {12'h004};
bins ien_reg = {12'h008};
}
cp_err: coverpoint txn.error {
bins ok = {0}; bins err = {1};
}
// Cross: creates 2 × 3 = 6 bins
// rd×dir, rd×data, rd×ien, wr×dir, wr×data, wr×ien
cx_rw_addr: cross cp_rw, cp_addr;
// Three-way cross: 2 × 3 × 2 = 12 bins
cx_rw_addr_err: cross cp_rw, cp_addr, cp_err;
// Exclude specific cross bins to avoid expecting illegal combos
cx_rd_err: cross cp_rw, cp_err {
ignore_bins rd_no_err = binsof(cp_rw.rd) && binsof(cp_err.ok);
}
endgroup
ignore_bins on the cross itself to exclude combinations that can never occur. Aim for cross bins that represent real protocol scenarios.Covergroups can be declared inside classes or in module scope. In UVM, three placements are common:
| Placement | Owner | Sampling trigger | Pros / Cons |
|---|---|---|---|
| Inside monitor | uvm_monitor | Called after ap.write(txn) in collect_transfer() | Simple. Tightly coupled to observation. Monitor becomes harder to reuse. |
| Dedicated coverage subscriber | uvm_subscriber #(T) | Called inside write() when subscriber receives txn | Clean separation. Coverage model is independent. Easy to enable/disable. |
| Inside sequence item | uvm_sequence_item | Sampled by monitor or subscriber after construction | Reusable across projects. Harder to share covergroup database across runs. |
The recommended pattern: a dedicated apb_cov_collector component extends uvm_subscriber, declares its covergroups, and samples them in its write() function. This keeps coverage completely separate from the monitor and scoreboard.
class apb_cov_collector extends uvm_subscriber #(apb_seq_item); `uvm_component_utils(apb_cov_collector) apb_seq_item txn; // current transaction for covergroup // ── Covergroup: APB transfer characteristics ───────────── covergroup apb_xfer_cg; // Read/Write direction cp_rw: coverpoint txn.read_not_write { bins rd = {1}; bins wr = {0}; } // Register addresses exercised cp_addr: coverpoint txn.addr { bins gpio_data = {12'h000}; bins gpio_dir = {12'h004}; bins gpio_ien = {12'h008}; bins gpio_isr = {12'h00C}; bins other = default; } // Byte enables cp_strobe: coverpoint txn.byte_en { bins all_bytes = {4'b1111}; bins upper_word = {4'b1100}; bins lower_word = {4'b0011}; bins byte0 = {4'b0001}; bins byte3 = {4'b1000}; } // Error response observed cp_err: coverpoint txn.error; // Cross: read/write on every register cx_rw_addr: cross cp_rw, cp_addr; // Cross: error on reads vs writes cx_rw_err: cross cp_rw, cp_err; endgroup // ── Covergroup: data boundary values ───────────────────── covergroup data_corners_cg; cp_data: coverpoint txn.write_data { bins zero = {32'h0}; bins all_ones = {32'hFFFF_FFFF}; bins walking1 = {32'h8000_0000, 32'h0000_0001}; bins other = default; } endgroup function new(string name, uvm_component parent); super.new(name, parent); apb_xfer_cg = new(); // instantiate all covergroups data_corners_cg = new(); endfunction // write() called by analysis port on every transaction function void write(apb_seq_item t); txn = t; apb_xfer_cg.sample(); if (!t.read_not_write) // only sample data coverage on writes data_corners_cg.sample(); endfunction function void report_phase(uvm_phase phase); `uvm_info("COV", $sformatf( "APB transfer coverage: %.1f%% Data corners: %.1f%%", apb_xfer_cg.get_coverage(), data_corners_cg.get_coverage()), UVM_LOW) endfunction endclass // ── In env connect_phase: subscribe to agent ap ─────────── m_agent.ap.connect(m_cov.analysis_export);
For simpler environments or when the coverage is closely tied to the monitor’s internal state (e.g., counting specific signal patterns that don’t make it into the transaction object), embedding the covergroup directly in the monitor is acceptable:
class apb_monitor extends uvm_monitor; `uvm_component_utils(apb_monitor) virtual apb_if vif; uvm_analysis_port #(apb_seq_item) ap; apb_seq_item cov_txn; covergroup apb_protocol_cg; // How many wait states did PREADY insert? cp_wait: coverpoint cov_txn.wait_cycles { bins zero = {0}; bins one = {1}; bins two_plus = {[2:$]}; } cp_rw: coverpoint cov_txn.read_not_write; cx: cross cp_wait, cp_rw; endgroup function new(string name, uvm_component parent); super.new(name, parent); apb_protocol_cg = new(); endfunction task collect_transfer(); apb_seq_item txn; int wait_cyc = 0; @(posedge vif.PCLK); if (vif.PSELx && !vif.PENABLE) begin // Count wait cycles until PREADY @(posedge vif.PCLK); vif.PENABLE = 1; while (!vif.PREADY) begin @(posedge vif.PCLK); wait_cyc++; end txn = apb_seq_item::type_id::create("txn"); txn.addr = vif.PADDR; txn.read_not_write = ~vif.PWRITE; txn.error = vif.PSLVERR; txn.wait_cycles = wait_cyc; cov_txn = txn; apb_protocol_cg.sample(); ap.write(txn); end endtask endclass
Functional coverage only delivers value when it is connected to the test generation process. The typical flow:
randomize() with {} constraint that forces the missing scenario.// ── Targeted test for a coverage gap ───────────────────── // Gap found: never tested write with byte0-only strobe task body(); apb_seq_item item; repeat(20) begin item = apb_seq_item::type_id::create("item"); start_item(item); item.randomize() with { read_not_write == 0; // force write byte_en == 4'b0001; // force byte0 only — fills gap }; finish_item(item); end endtask // ── Check coverage programmatically in a test ───────────── function void check_phase(uvm_phase phase); real cov = m_env.m_cov.apb_xfer_cg.get_coverage(); if (cov < 95.0) `uvm_error("COV", $sformatf( "Coverage goal not met: %.1f%% (goal: 95%%)", cov)) else `uvm_info("COV", $sformatf( "Coverage goal met: %.1f%%", cov), UVM_LOW) endfunction
Every coverpoint and covergroup can have a target goal — the minimum percentage needed to declare a coverage pass. Goals are used to gate sign-off: the test suite is not complete until all covergroups meet their goals.
covergroup apb_xfer_cg; option.goal = 100; // 100% of bins must be hit option.per_instance = 1; // track each instance separately option.comment = "APB transfer coverage — sign-off requirement"; cp_rw: coverpoint txn.read_not_write { option.goal = 100; // both rd and wr must be seen bins rd = {1}; bins wr = {0}; } cp_addr: coverpoint txn.addr { option.goal = 90; // 90% of register bins must be hit bins dir_reg = {12'h000}; bins data_reg = {12'h004}; bins ien_reg = {12'h008}; bins isr_reg = {12'h00C}; bins other = default; } cx_rw_addr: cross cp_rw, cp_addr { option.goal = 80; // 80% of cross bins required } endgroup
| Option | Default | What it controls |
|---|---|---|
option.goal | 100 | Percentage of bins needed to declare this group/point covered |
option.per_instance | 0 | 1 = track each instantiation separately; 0 = merge all instances |
option.at_least | 1 | Minimum number of times each bin must be hit to count as covered |
option.auto_bin_max | 64 | Maximum automatic bins for a coverpoint (avoids explosion) |
option.comment | “” | Documentation string shown in coverage reports |
type_option.merge_instances | 0 | Merge all instances for overall coverage calculation |
| Pitfall | Symptom | Fix |
|---|---|---|
| Forgetting to instantiate covergroup | Null handle at sample(). Simulation fatal. | Always cg_inst = new() in the constructor before any sampling. |
| Sampling before transaction fields are set | Coverage bins get wrong values. Bins appear empty or incorrect. | Sample after all transaction fields are populated — after ap.write() returns in subscriber. |
| Wide field with auto bins | Simulator creates 2^32 bins for a 32-bit field. Out of memory or very slow. | Always use explicit bins for fields wider than ~8 bits. Set option.auto_bin_max = N. |
| Cross explosion | Cross of two 16-value points = 256 bins. Mostly unoccupied. Misleading low coverage %. | Limit coverpoints to ≤8 bins before crossing. Use ignore_bins for impossible combinations. |
| Coverage not enabled in agent config | Coverage collector built but never receives transactions. 0% coverage always. | Check that has_coverage=1 in agent config, and that connect_phase wires ap to coverage collector. |
| 100% coverage declared too early | Coverage database shows 100% but important scenarios untested. False confidence. | Review coverage model quality. Ensure bins represent real protocol scenarios, not just all possible values. |
| Item | Key fact |
|---|---|
| Covergroup purpose | Count how many distinct scenarios have occurred. Answers “have we done enough testing?” |
| Instantiation | cg_inst = new() in constructor — creates actual counters. Declaration alone does nothing. |
| Sampling | cg_inst.sample() — call after all sample variables are set |
| Query coverage | cg_inst.get_coverage() returns 0.0–100.0 as a real |
| Explicit bins | bins name = {value}; or bins name = {[lo:hi]}; |
| Default bin | bins other = default; catches all unspecified values |
| Transition bin | bins name = (a => b); — value change between consecutive samples |
| Ignore bin | ignore_bins name = {v}; — excludes from coverage calculation |
| Illegal bin | illegal_bins name = {v}; — error if this value ever sampled |
| Conditional bin | bins name = {v} iff (expr); — only counts when guard is true |
| Cross coverage | cx: cross cp_a, cp_b; — bins = product of all bin combinations |
| Recommended placement | Dedicated uvm_subscriber — cleanest separation from monitor and scoreboard |
| option.goal | Minimum % of bins to consider group covered — default 100 |
| option.at_least | Minimum hit count per bin — default 1 |
| auto_bin_max | Set to limit automatic bins for wide fields — default 64 |