Skip to content

AMBA CHI · Module 17 · CHI Verification

CHI Functional Coverage

Assertions prove rules held where checked; functional coverage measures what the tests exercised — the other half of knowing a design is verified. A coverage model samples coverpoints — opcodes, states, responses — reporting what fraction the tests hit, telling you what you have not tested. The critical point: covering each dimension independently does not mean their combinations were covered. A suite can hit every opcode and every state, 100% on both, yet never produce a specific opcode in a rare state — a dirty-invalidating snoop at a dirty line, where a snoop drops data. Without a cross of the interacting dimensions that combination is never measured, no test is written, and the bug escapes. Representative model, not the specification.

Advanced16 min readAMBA CHIFunctional CoverageCovergroupCross CoverageCoverpoint

Module 17 · Chapter 17.6 · CHI Verification

Project thread — 17.5 wrote assertions. 17.6 measures what the tests exercised; 17.7 assembles the UVM environment.

1. Learning Outcomes

By the end of this chapter you should be able to:

  • Explain that functional coverage measures what the tests exercised — the gaps.
  • Name the coverage building blocks — covergroup, coverpoint, bins, cross.
  • State that per-dimension coverage does not imply combination coverage.
  • Explain why a cross of interacting coverpoints exposes combination gaps.
  • Diagnose a bug that escapes because a risky opcode × state cross was uncovered.
  • Implement a representative coverage model in SystemVerilog, Verilog-2001, and VHDL.

2. Why Should I Learn This?

Assertions (Chapter 17.5) tell you a rule held wherever it was checked — but not whether the interesting cases were ever reached. Functional coverage is the other half: it measures what the tests actually exercised, so it tells you what you haven't tested. A suite with 100% passing assertions and low coverage has proven the rules on the cases it happened to hit — and left the risky, rare cases unexamined. Coverage is how you find those gaps before they ship as escaped bugs.

The trap that makes coverage misleading is combinations. It is easy to cover each dimension independently — every opcode, every cache state — and report 100% on both. But a bug often lives in a specific combination: a dirty-invalidating snoop arriving at a line held dirty (the exact case a snoop pipeline can mishandle, Chapter 16.2). Neither the opcode coverpoint nor the state coverpoint alone flags that the combination was never hit — each is fully covered on its own. Without cross coverage of the interacting dimensions, the dangerous combination goes unmeasured, no test is written for it, and the bug escapes while coverage reads complete. This chapter is coverage and why crosses are essential.

3. Key Terms

4. Previous Chapter Connection

This chapter is the completion of the assertion story from Chapter 17.5. There, the danger was a vacuous assertion — one whose antecedent never fired, so it checked nothing. Coverage is the systematic form of that same concern: it measures, across the whole suite, which cases were reached. An assertion's antecedent that never fires is a coverage hole; coverage makes such holes visible everywhere, not just per assertion.

It also directly targets Chapter 16.2's bug. That chapter's snoop-pipeline bug — dropping data on a dirty-invalidating snoop — hides in testing precisely because most snoops hit clean lines; the risky dirty case is rare. Coverage is how you guarantee that rare case is exercised: a cross of snoop-opcode × line-state has a bin for (dirty-invalidating snoop, dirty state), and if that bin is empty, the coverage report shows the gap — prompting a test that would trip the bug. This chapter is the tool that turns "we happened not to test it" into a visible hole, before Chapter 17.7 assembles the full environment.

5. Core Concept — cross the interacting dimensions

Functional coverage measures what the tests exercised; covering each dimension independently is not enough — a cross of interacting coverpoints is needed to expose combination gaps.

  • Coverage measures exercise. A covergroup samples coverpoints — opcode, cache state, response type — and reports which bins (values) the tests hit. It tells you what was not exercised.
  • Per-dimension coverage is incomplete. Hitting every opcode and every state reaches 100% on each coverpoint alone — but says nothing about which combinations occurred.
  • Bugs live in combinations. A dangerous case is often a specific opcode in a specific state (a dirty-invalidating snoop at a dirty line) — a combination, not a single value.
  • Cross exposes the gap. A cross of the interacting coverpoints (opcode × state) has a bin per combination; an empty cross bin reveals a combination that was never hit — even when both coverpoints read 100%.

The synthesis:

Functional coverage measures what the tests exercised. Covering each dimension independently (every opcode, every state) reaches 100% on each coverpoint alone but says nothing about their combinations — and bugs often live in a specific combination (a dirty-invalidating snoop at a dirty line). Without a cross of the interacting coverpoints, that combination goes unmeasured, so the bug escapes while coverage reads complete. Cross the interacting dimensions.

6. Engineering Mental Model — testing every ingredient but not the recipe

Think of testing a kitchen by checking every ingredient and every appliance individually.

  • You confirm every ingredient is fresh (each opcode covered) and every appliance works (each state covered). Both checklists reach 100% — everything individually is fine.
  • But a specific dish requires a specific ingredient cooked in a specific appliance — say, a soufflé in the convection oven. If that combination was never tried, you have not tested whether it works.
  • Your per-ingredient and per-appliance checklists both say 100%, so you declare the kitchen fully tested — and confidently serve the soufflé, which collapses, because the ingredient-in-that-appliance combination was never exercised.
  • A combination checklist — a grid of ingredient × appliance — would have shown an empty cell for (soufflé mix, convection oven), flagging the untested pairing before you served it.

The per-ingredient and per-appliance lists are single coverpoints; the ingredient × appliance grid is the cross. Individual 100% coverage hides the untested combination that the cross grid's empty cell reveals.

7. Anatomy of a CHI Coverage Model

A CHI covergroup with per-dimension coverpoints and the cross that makes them meaningful.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A CHI coverage model: per-dimension coverpoints AND the cross of interacting ones.
covergroup cg_snoop @(posedge clk);
  // Coverpoint 1: the snoop opcode.
  cp_op:    coverpoint snp_opcode {
    bins clean_snoop = {SNP_SHARED, SNP_CLEAN};
    bins inval_snoop = {SNP_UNIQUE};              // dirty-INVALIDATING snoop
  }
  // Coverpoint 2: the snooped line's state.
  cp_state: coverpoint line_state {
    bins clean = {SC, UC};
    bins dirty = {SD, UD};                        // the RARE, risky state
  }
  // CROSS: the COMBINATIONS -- this is what exposes the (inval_snoop, dirty) gap.
  x_op_state: cross cp_op, cp_state;              // bin per opcode x state pair
endgroup

The parts: a covergroup sampled on an event; coverpoints (cp_op, cp_state) each with bins grouping values; and — critically — the cross (x_op_state) that creates a bin for every combination of the two coverpoints. Without the cross, cp_op and cp_state can each reach 100% while the (inval_snoop, dirty) combination — the Chapter 16.2 bug case — is never hit. The cross's empty bin is what makes that gap visible. Coverage without crosses measures dimensions; coverage with crosses measures behaviors.

8. Coverpoints and the Cross

The CHI coverage dimensions and why they must be crossed.

CoverpointBins (examples)Alone tells you
Opcoderead, write, snoop typesevery opcode occurred
Cache stateI, SC, UC, SD, UDevery state occurred
ResponseComp, SnpResp, SnpRespData, RetryAckevery response occurred
Cross opcode × state(inval snoop, dirty), …which combinations occurred
Sequencestate-transition orderingswhich orderings occurred

The rule to carry: single coverpoints measure whether each value occurred; crosses measure whether each interaction occurred — and bugs live in interactions. A read occurred, a dirty state occurred, a data-bearing response occurred — each individually. But whether a dirty-invalidating snoop met a dirty line and produced a data-bearing response is a three-way interaction, invisible to any single coverpoint. The dangerous cases are almost always conjunctions of conditions, so the coverage model must cross the dimensions whose conjunction is risky — opcode × state, and sequences of states — or those cases go unmeasured.

9. Why Missing Crosses Hide Bugs

The gap, made explicit.

  • Each dimension reaches 100% easily. Random or directed tests naturally hit every opcode and every state over a long run — so per-dimension coverage climbs to 100% without effort.
  • 100% per dimension looks done. A coverage report showing 100% opcode and 100% state reads as complete — the team concludes the space is covered and stops writing tests.
  • But the combination was never forced. The rare combination — a dirty-invalidating snoop at a dirty line — requires a specific setup (make a line dirty, then snoop it uniquely). Random traffic may never align them.
  • The bug escapes. With no cross, the empty (inval snoop, dirty) combination is invisible, so no test targets it, the Chapter 16.2 data-loss bug is never exercised, and it ships — while coverage read 100%.

The point to carry:

Coverage is a map of the unknown, and its value is entirely in honesty about what it does not cover — so a coverage model that overstates completeness is worse than useless, because it converts a real gap into false confidence. Per-dimension coverage systematically overstates, because the number of combinations grows as the product of the dimensions while single coverpoints grow as the sum — so a space that is 100% covered on every axis can be a tiny fraction covered in the cross. This is the curse of dimensionality applied to verification: the interesting behaviors live in a high-dimensional combination space that per-axis coverage cannot see. The discipline is to identify the risky conjunctions — the specific opcode/state/response/sequence combinations where the design does something subtle — and cross exactly those, so the report reflects the behavior space, not the dimension space. It ties back to Chapter 17.5's vacuity and forward to the whole theme: a green number is only trustworthy if it measures the right thing, and per-dimension coverage measures the wrong thing — the axes, not the interactions where bugs hide. The most important coverage bins are the crosses you deliberately chose because a bug would live there.

10. Covering a Snoop — dimensions vs cross

The snoop coverage model from Chapter 16.2's bug, measured two ways.

  1. Per-dimension: opcode. Over the run, both clean snoops and the invalidating snoop occur. cp_op reaches 100%.
  2. Per-dimension: state. Both clean and dirty line states occur (many lines, some written). cp_state reaches 100%.
  3. Both coverpoints 100% — looks done. The report shows 100% opcode, 100% state. Without a cross, the team concludes snoop coverage is complete.
  4. The cross reveals the gap. x_op_state has four bins; the (inval_snoop, dirty) bin has zero hits — random traffic never lined up a unique snoop with a dirty line. The cross reads, say, 75%.
  5. The gap prompts the test. The empty cross bin flags the untested combination, so a directed test is written: dirty a line, then SnpUnique it — exercising the Chapter 16.2 case and catching the data-loss bug.

Per-dimension coverage said "done" while the risky combination was never hit; the cross exposed it and drove the test that caught the bug. The DebugLab is stopping at step 3 — no cross.

11. Checker / Monitor View — a coverage model with a cross

The SV covergroup crosses opcode and state; the synthesizable versions maintain per-bin counters and a cross matrix so an empty combination is a zero cell. Representative.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Representative CHI coverage model (educational) -- coverpoints AND cross.
covergroup cg_snoop @(posedge clk);
  cp_op:    coverpoint snp_opcode  { bins clean = {0,1}; bins inval = {2}; }
  cp_state: coverpoint line_state  { bins clean = {0,1}; bins dirty = {2,3}; }
  // The CROSS is what exposes the (inval, dirty) combination gap.
  x_op_state: cross cp_op, cp_state;
endgroup
cg_snoop cov = new();

The same measurement as a Verilog-2001 cross-bin matrix (rows = opcode class, cols = state class):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Representative coverage matrix (Verilog-2001): a 2x2 cross of opcode x state.
module chi_cov_cross (
  input        clk, rst_n, sample,
  input        op_is_inval,     // opcode class: invalidating snoop
  input        state_is_dirty,  // state class: dirty line
  output reg [31:0] cross_hits [0:1][0:1]   // [op][state] -- an empty cell = gap
);
  integer o, s;
  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (o=0;o<2;o=o+1) for (s=0;s<2;s=s+1) cross_hits[o][s] <= 32'd0;
    end else if (sample) begin
      // Increment exactly the crossed combination that occurred.
      cross_hits[op_is_inval][state_is_dirty] <= cross_hits[op_is_inval][state_is_dirty] + 1;
    end
  end
  // cross_hits[1][1] == 0 at end-of-test => (inval snoop, dirty) NEVER hit => GAP.
endmodule

And in VHDL:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- Representative coverage matrix (VHDL): a 2x2 cross of opcode x state.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
 
entity chi_cov_cross is
  port (
    clk, rst_n     : in  std_logic;
    sample         : in  std_logic;
    op_is_inval    : in  std_logic;
    state_is_dirty : in  std_logic;
    hit_inval_dirty : out unsigned(31 downto 0)   -- the risky (inval,dirty) cell
  );
end entity;
 
architecture rtl of chi_cov_cross is
  type row_t is array (0 to 1) of unsigned(31 downto 0);
  type mat_t is array (0 to 1) of row_t;
  signal cross_hits : mat_t := (others => (others => (others => '0')));
begin
  hit_inval_dirty <= cross_hits(1)(1);   -- (inval snoop, dirty) combination count
  process (clk, rst_n)
    variable oi, si : integer range 0 to 1;
  begin
    if rst_n = '0' then
      cross_hits <= (others => (others => (others => '0')));
    elsif rising_edge(clk) then
      if sample = '1' then
        if op_is_inval = '1' then oi := 1; else oi := 0; end if;
        if state_is_dirty = '1' then si := 1; else si := 0; end if;
        cross_hits(oi)(si) <= cross_hits(oi)(si) + 1;   -- count the combination
      end if;
    end if;
  end process;
  -- cross_hits(1)(1) = 0 at end-of-test => the risky combination was never exercised.
end architecture;

In all three, the cross (the SV cross, the Verilog/VHDL matrix) counts each combination — so an empty cross bin (e.g. cross_hits[1][1] == 0, the invalidating-snoop-at-dirty case) is a visible gap. The DebugLab omits the cross, tracking only per-dimension counts that both reach 100%.

12. Assertion View — the risky cross must be hit

The properties formalize the discipline: each coverpoint and, crucially, the risky cross must be covered.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Coverage goals (checked at end-of-test).
// 1. Each coverpoint is fully covered (necessary, not sufficient).
//    cp_op coverage == 100%  AND  cp_state coverage == 100%
 
// 2. The CROSS is fully covered -- specifically the risky combination bin.
//    x_op_state coverage == 100%  ->  (inval, dirty) bin has >= 1 hit
cover_cross_inval_dirty: cover property (@(posedge clk) disable iff (!rst_n)
  (snp_opcode == SNP_UNIQUE) && (line_state == UD || line_state == SD));
 
// 3. End-of-test: fail if the risky cross bin is empty, even at 100% per dimension.
//    if (cross_hits[inval][dirty] == 0) -> coverage GAP -> write the directed test.

The system point, beyond the checks:

The cover_cross_inval_dirty property is the coverage-level expression of the module's central discipline — prove the interesting case actually ran — and it connects the three verification chapters into one loop. Chapter 17.5 warned that an assertion is worthless if its antecedent never fires; this chapter shows the complementary truth: even a well-written, non-vacuous assertion for the dirty-snoop rule (Chapter 16.2) is worthless if the test suite never produces a dirty-invalidating snoop, because the assertion's antecedent will never fire — and that is exactly what an empty cross bin reveals. So coverage and assertions are two views of the same gap: the empty cross bin (17.6) and the zero-hit antecedent (17.5) are the same hole seen from the coverage side and the assertion side. A complete methodology closes the loop: write the assertion for a risky behavior, cross-cover the conditions that trigger it, and gate the run on the cross bin being hit — so the assertion is guaranteed to have been exercised on its target case. A green suite is only trustworthy when its assertions are non-vacuous and its crosses are hit; either alone is false confidence. The most valuable single artifact in the environment is the list of risky crosses, deliberately chosen because a bug would live there.

  • What it proves: each coverpoint and the risky cross were exercised.
  • What it does not prove: the design is correct in those cases — that is the assertions/scoreboard.
  • Bug signature: 100% per-dimension coverage with an empty risky cross bin.

13. Testbench — a risky cross bin must not be empty at 100% per dimension

Hits every opcode and state (per-dimension 100%) but never the (inval, dirty) combination, and confirms the cross cell exposes it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_chi_cov_cross;
  logic clk = 0, rst_n = 0, sample, op_is_inval, state_is_dirty;
  logic [31:0] cross_hits [0:1][0:1];
  int errors = 0;
 
  chi_cov_cross dut (.*);
  always #5 clk = ~clk;
 
  initial begin
    sample = 0; op_is_inval = 0; state_is_dirty = 0;
    @(posedge clk) rst_n = 1;
 
    // Exercise per-dimension: inval opcode occurs (with CLEAN state); dirty state occurs
    // (with CLEAN opcode) -- both dimensions reach their values, but NEVER together.
    @(posedge clk) begin sample=1; op_is_inval=1; state_is_dirty=0; end  // inval + clean
    @(posedge clk) begin op_is_inval=0; state_is_dirty=1; end            // clean + dirty
    @(posedge clk) begin op_is_inval=0; state_is_dirty=0; end            // clean + clean
    @(posedge clk) sample=0;
    #1;
    // Per dimension: inval opcode hit, dirty state hit -> "100%" on each axis.
    // But the RISKY combination (inval, dirty) = cross_hits[1][1] is ZERO.
    if (cross_hits[1][1] != 0) begin errors++; $display("FAIL risky cross unexpectedly hit"); end
    else $display("PASS gap exposed: (inval, dirty) cross bin = %0d (never hit)", cross_hits[1][1]);
    $display("INFO per-dimension looked done, but the cross reveals the hole");
 
    // Now write the DIRECTED test for the combination -> hit (inval, dirty).
    @(posedge clk) begin sample=1; op_is_inval=1; state_is_dirty=1; end  // the risky case!
    @(posedge clk) sample=0;
    #1;
    if (cross_hits[1][1] == 0) begin errors++; $display("FAIL risky cross still empty"); end
    else $display("PASS risky combination now exercised: cross_hits[1][1] = %0d", cross_hits[1][1]);
 
    if (errors == 0) $display("ALL TESTS PASSED");
    else             $display("%0d FAILURE(S)", errors);
    $finish;
  end
endmodule

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
PASS gap exposed: (inval, dirty) cross bin = 0 (never hit)
INFO per-dimension looked done, but the cross reveals the hole
PASS risky combination now exercised: cross_hits[1][1] = 1
ALL TESTS PASSED

14. DebugLab — a coverage model with no cross

1

A coverage model with no cross

NO CROSS COVERAGE -> RISKY OPCODE-IN-RARE-STATE COMBINATION NEVER MEASURED -> BUG ESCAPES AT 100% PER-DIMENSION
Symptom

A bug ships in a rare opcode-in-a-state combination despite a coverage report showing 100%. The per-dimension coverage (opcode, state) is complete, yet the specific failing combination — a dirty-invalidating snoop at a dirty line — was never exercised in any test. No directed test targeted it, because nothing flagged it as missing.

Evidence

Both dimensions covered; the combination never hit:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
coverpoints: cp_op (opcodes) -> 100%    cp_state (states) -> 100%    NO CROSS
random run: inval snoop occurs (on clean lines); dirty state occurs (on reads)
  -> BUT a unique/invalidating snoop AND a dirty line never align
  -> combination (inval_snoop, dirty) NEVER produced
no cross coverpoint -> the empty (inval, dirty) bin is INVISIBLE
report: "snoop coverage 100%" -> no directed test written -> 16.2 bug escapes
correct: cross cp_op x cp_state -> (inval, dirty) bin = 0 -> gap flagged -> test written -> bug caught

Each axis was full; the untested corner of the grid was hidden.

First Divergence

The coverage model had per-dimension coverpoints but no cross of the interacting dimensions. From that point the risky combination was unmeasured and invisible, even at 100% per axis.

Root Cause

Per-dimension coverage measures whether each value occurred, not whether their combinations did, so a risky opcode-in-a-rare-state combination can be untested at 100% per axis; cross coverage of the interacting dimensions is required to see it. The number of combinations grows as the product of the dimensions while single coverpoints grow as the sum, so a space 100% covered on every axis can be a tiny fraction covered in the cross — the curse of dimensionality applied to verification. Bugs live in conjunctions of conditions (a specific opcode at a specific state), which no single coverpoint can see. The fix is to identify the risky conjunctions and cross exactly those, so the report reflects the behavior space, not the dimension space. This ties to Chapter 17.5: an empty cross bin and a zero-hit assertion antecedent are the same gap from two sides — even a correct dirty-snoop assertion (Chapter 16.2) never fires if the combination is never produced.

Fix

Add cross coverage of the interacting coverpoints — opcode × state (and sequences) — so the untested combination appears as an empty cross bin, exactly as the cross matrix's [inval][dirty] cell does. Gate the run on the risky cross bins being hit, not just per-dimension 100%. Cross the dimensions whose conjunction is risky; the empty bin drives the directed test that catches the bug.

15. Common Mistakes

  • No cross coverage. Assumption: per-dimension 100% is complete. Bug: combination bug escapes (the DebugLab). Prevention: cross interacting coverpoints.
  • Crossing everything. Assumption: more crosses is better. Bug: huge, unhittable bin space. Prevention: cross only the risky conjunctions.
  • Trusting per-dimension 100%. Assumption: axes done means space done. Bug: combinations unmeasured. Prevention: gate on cross bins.
  • Ignoring sequences. Assumption: single states suffice. Bug: transition-order bugs missed. Prevention: cover state-transition sequences.
  • Coverage without assertions. Assumption: hitting a case verifies it. Bug: no check on the case. Prevention: pair coverage with assertions.
  • Not linking coverage to risk. Assumption: any bins will do. Bug: irrelevant coverage. Prevention: cross where bugs would live.

16. Engineering Checklist

  • Define coverpoints for opcode, cache state, and response type.
  • Add cross coverage of the interacting dimensions (opcode × state).
  • Include sequence coverage for state-transition orderings.
  • Identify the risky conjunctions and cross exactly those.
  • Gate the run on the risky cross bins being hit — not per-dimension 100%.
  • Pair coverage with the assertions that check those cases (Chapter 17.5).

17. Key Takeaways

  • Functional coverage measures what the tests exercised — the gaps.
  • Per-dimension coverage does not imply combination coverage.
  • Bugs often live in a specific combination (opcode × state).
  • A cross of interacting coverpoints exposes the empty combination.
  • 100% per dimension can hide an untested risky combination — the bug escapes.
  • Cross where bugs would live; the model here is representative.

18. Quick Revision

CHI functional coverage. Functional coverage measures what the tests actually exercised — the complement to assertions, which prove rules held where checked. A covergroup samples coverpoints (opcode, cache state, response type), binning and counting their values, so coverage tells you what you have not tested. The critical point: covering each dimension independently reaches 100% on each coverpoint alone but says nothing about their combinations. Bugs often live in a specific combination — a dirty-invalidating snoop at a line held dirty (the Chapter 16.2 data-loss case) — which requires a specific alignment random traffic may never create. Without a cross of the interacting coverpoints (opcode × state), that combination is unmeasured and invisible: the report reads 100% per axis, no directed test is written, and the bug escapes. The failure to avoid: per-dimension coverpoints with no cross. Combinations grow as the product of dimensions while single coverpoints grow as the sum, so a space 100% covered on every axis can be barely covered in the cross — the curse of dimensionality. The fix: identify the risky conjunctions and cross exactly those, gating the run on the risky cross bins being hit. An empty cross bin and a zero-hit assertion antecedent (Chapter 17.5) are the same gap from two sides — a correct assertion never fires if its combination is never produced. Cross where bugs would live. Representative model; 17.7 assembles the full UVM environment.

Coming Next

Chapter 17.7 — UVM Architecture for CHI. The checks, scoreboards, references, assertions, and coverage all need a home; the last chapter of the module assembles them. Chapter 17.7 covers UVM architecture for CHI — the multi-agent environment with an agent per interface feeding a central scoreboard and coherency checker, and why a per-agent-only environment with no system-level checker cannot see the cross-agent coherence violations that matter most.