UVM-19: Functional Coverage — VLSI Trainers
VLSI Trainers UVM Series · UVM-19
UVM Series · UVM-19

Functional Coverage

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.

📋 Why Functional Coverage

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.

Coverage-Driven Verification Loop Constrained Random Tests Simulation + Monitor Coverage Measurement Coverage Analysis Gaps found? Add targeted constraints or directed tests → repeat until all bins hit vlsitrainers.com
Figure 1 — Coverage-driven verification loop. Random tests stimulate the DUT; the monitor observes transactions and samples covergroups. Coverage analysis identifies untested scenarios (gaps). Targeted constraints or directed tests are added to fill gaps. The loop repeats until all planned coverage goals are met.

📋 Covergroup Basics

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
A covergroup must be instantiated with new() before sampling. Unlike a class, a covergroup type does not automatically create an instance. Calling 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.

📋 Coverpoints and Bins

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 typeSyntaxWhat it measures
Explicit valuebins name = {value};Counts how many times that exact value was sampled
Rangebins name = {[lo:hi]};Counts how many times any value in range was sampled
Defaultbins other = default;Catches all values not covered by explicit bins
Transitionbins name = (a => b);Counts how many times value changed from a to b across consecutive samples
Ignoreignore_bins name = {val};Excludes these values from coverage calculation — not an error if never hit
Illegalillegal_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

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
Cross coverage grows multiplicatively — keep it manageable. A 4-way cross between four 8-value coverpoints creates 8 × 8 × 8 × 8 = 4096 bins. Most of these may be impossible or uninteresting. Use explicit bins in each coverpoint to limit the cross size, and use ignore_bins on the cross itself to exclude combinations that can never occur. Aim for cross bins that represent real protocol scenarios.

📋 Where to Place Covergroups

Covergroups can be declared inside classes or in module scope. In UVM, three placements are common:

PlacementOwnerSampling triggerPros / Cons
Inside monitoruvm_monitorCalled after ap.write(txn) in collect_transfer()Simple. Tightly coupled to observation. Monitor becomes harder to reuse.
Dedicated coverage subscriberuvm_subscriber #(T)Called inside write() when subscriber receives txnClean separation. Coverage model is independent. Easy to enable/disable.
Inside sequence itemuvm_sequence_itemSampled by monitor or subscriber after constructionReusable across projects. Harder to share covergroup database across runs.

📋 Dedicated Coverage Collector

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);

📋 Embedded in Monitor

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

📋 Coverage-Driven Verification Flow

Functional coverage only delivers value when it is connected to the test generation process. The typical flow:

  1. Define coverage model. Write covergroups that capture the verification intent — what scenarios must be exercised.
  2. Run random regression. Many tests with different seeds. Measure coverage after each run (or merge coverage databases).
  3. Identify gaps. Which bins have zero or low hit counts?
  4. Add inline constraints. For specific gaps, write a new sequence with an randomize() with {} constraint that forces the missing scenario.
  5. Repeat. Run the augmented regression until all bins reach their hit targets.
// ── 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

📋 Setting Coverage Goals

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
OptionDefaultWhat it controls
option.goal100Percentage of bins needed to declare this group/point covered
option.per_instance01 = track each instantiation separately; 0 = merge all instances
option.at_least1Minimum number of times each bin must be hit to count as covered
option.auto_bin_max64Maximum automatic bins for a coverpoint (avoids explosion)
option.comment“”Documentation string shown in coverage reports
type_option.merge_instances0Merge all instances for overall coverage calculation

📋 Common Pitfalls

PitfallSymptomFix
Forgetting to instantiate covergroupNull handle at sample(). Simulation fatal.Always cg_inst = new() in the constructor before any sampling.
Sampling before transaction fields are setCoverage 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 binsSimulator 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 explosionCross 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 configCoverage 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 earlyCoverage database shows 100% but important scenarios untested. False confidence.Review coverage model quality. Ensure bins represent real protocol scenarios, not just all possible values.

📋 Quick Reference

ItemKey fact
Covergroup purposeCount how many distinct scenarios have occurred. Answers “have we done enough testing?”
Instantiationcg_inst = new() in constructor — creates actual counters. Declaration alone does nothing.
Samplingcg_inst.sample() — call after all sample variables are set
Query coveragecg_inst.get_coverage() returns 0.0–100.0 as a real
Explicit binsbins name = {value}; or bins name = {[lo:hi]};
Default binbins other = default; catches all unspecified values
Transition binbins name = (a => b); — value change between consecutive samples
Ignore binignore_bins name = {v}; — excludes from coverage calculation
Illegal binillegal_bins name = {v}; — error if this value ever sampled
Conditional binbins name = {v} iff (expr); — only counts when guard is true
Cross coveragecx: cross cp_a, cp_b; — bins = product of all bin combinations
Recommended placementDedicated uvm_subscriber — cleanest separation from monitor and scoreboard
option.goalMinimum % of bins to consider group covered — default 100
option.at_leastMinimum hit count per bin — default 1
auto_bin_maxSet to limit automatic bins for wide fields — default 64
Scroll to Top