SystemVerilog · Module 11
Cross Coverage
The Cartesian product of two or more coverpoints' bins — measuring feature combinations rather than features in isolation. Two-way and N-way cross, binsof / intersect bin selection, illegal and ignore bins inside crosses, the cross-bin explosion trap, and why crossing auto-binned coverpoints is a project-killer.
Module 11 · Page 11.7
Why Cross Coverage Exists
Individual coverpoints answer "did each feature get exercised?" Cross coverage answers "did the features get exercised together?" An opcode coverpoint at 100% proves every opcode was sampled. An operand coverpoint at 100% proves every operand class was sampled. Neither tells you whether the dangerous combinations — DIV with operand zero, MUL with maximum operand, WRITE to the reserved region — were ever exercised in the same transaction.
Protocol bugs at silicon almost never live inside a single feature. They live in the intersections: burst-type WRAP combined with size doubleword; write-strobe 1100 combined with the unaligned address; back-pressure asserted concurrently with last-beat. The function of cross coverage is to make those intersections measurable, named, and reportable as their own verification-plan rows.
Syntax — The cross Keyword
A cross declaration appears inside a covergroup, after the coverpoints it references. Both (or all) coverpoints must be labelled — cross references coverpoints by name.
covergroup cg_basic;
// Step 1 — define labelled coverpoints
cp_opcode: coverpoint opcode;
cp_mode: coverpoint mode;
// Step 2 — cross them; one bin per combination
cross cp_opcode, cp_mode;
endgroup
// The cross above is implicitly named "cp_opcode X cp_mode".
// To set options on it or use it in binsof, label the cross too:
covergroup cg_named;
cp_opcode: coverpoint opcode;
cp_mode: coverpoint mode;
// Labelled cross — required for option.* and for binsof references
cr_op_mode: cross cp_opcode, cp_mode;
endgroupThe reading rule: a labelled cross is the cross-coverage equivalent of a named bin. All the named-bin discipline from Module 11.4 applies. The cross's label is the verification-plan row name; the per-cross option.comment cites the plan section; per-cross option.weight/option.at_least/option.goal apply to the cross as a whole.
A Working Example — ALU Two-Way and Three-Way Cross
The pattern below shows a complete ALU coverage model with the dangerous combinations made measurable.
typedef enum bit [2:0] {
ADD = 3'b000, SUB = 3'b001, MUL = 3'b010, DIV = 3'b011
} opcode_e;
class alu_txn;
rand opcode_e opcode;
rand bit [7:0] operand_a;
rand bit [7:0] operand_b;
rand bit [1:0] mode;
covergroup cg_alu;
// ── Coverpoints with explicit named bins ──────────────────
cp_opcode: coverpoint opcode {
bins arith = {ADD, SUB}; // grouped arithmetic
bins mul = {MUL};
bins div = {DIV};
}
cp_op_a: coverpoint operand_a {
bins zero = {0};
bins small = {[1:15]};
bins medium = {[16:127]};
bins large = {[128:255]};
}
cp_mode: coverpoint mode {
bins normal = {2'b00};
bins saturate = {2'b01};
bins overflow = {2'b10};
}
// ── Two-way cross — 3 × 4 = 12 bins ────────────────────────
cr_op_opa: cross cp_opcode, cp_op_a;
// ── Two-way cross — 3 × 3 = 9 bins ─────────────────────────
cr_op_mode: cross cp_opcode, cp_mode;
// ── Three-way cross — 3 × 4 × 3 = 36 bins ──────────────────
cr_all: cross cp_opcode, cp_op_a, cp_mode;
option.comment = "vplan §8 — ALU feature crosses";
endgroup
function new();
cg_alu = new();
endfunction
function void post_randomize();
cg_alu.sample();
endfunction
endclassWhat this code does. Defines a transaction class with three coverpoints, each carrying explicit named bins (per Module 11.4 discipline). Three crosses: opcode × operand-class (12 bins), opcode × mode (9 bins), and the three-way opcode × operand × mode (36 bins). The three-way cross is the audit document for "every combination of operation, operand class, and mode was exercised together."
How to simulate it. Drive 5000 randomised alu_txn objects under any IEEE 1800-compatible simulator with coverage enabled.
Expected output (urg-style, partial).
Group cg_alu coverage 77.8%
cp_opcode 100.0%
cp_op_a 100.0%
cp_mode 100.0%
cr_op_opa <arith,zero> 100.0% ( 92 hits )
<arith,small> 100.0% ( 387 hits )
<arith,medium> 100.0% ( 412 hits )
<arith,large> 0.0% ( 0 hits ) ← gap
<mul,zero> 100.0% ( 31 hits )
<mul,small> 100.0% ( 198 hits )
<mul,medium> 0.0% ( 0 hits ) ← gap
<mul,large> 0.0% ( 0 hits ) ← gap
<div,zero> 0.0% ( 0 hits ) ← gap (DIV-by-0 — critical!)
<div,small> 100.0% ( 87 hits )
<div,medium> 100.0% ( 102 hits )
<div,large> 100.0% ( 41 hits )
cr_op_mode 100.0% (9/9 bins)
cr_all ... 24/36 bins hit ... 66.7%What to read out of this. Every individual coverpoint is at 100% — by isolated-feature measurement, the regression looks complete. But cr_op_opa is at 66.7% with four named gaps, the most critical of which is <div,zero> — the divide-by-zero scenario that the verification plan explicitly calls out as mandatory. Code coverage and individual coverpoint coverage both reported "done" while the canonical bug class went untested. The cross report is the only metric that surfaces the gap, which is the entire point of cross coverage.
Bin Selection — binsof and intersect
By default, cross creates one bin for every combination of every bin in every crossed coverpoint. Often the verification plan needs only a subset — specific bin combinations, with the rest excluded or grouped. The binsof construct names the subset.
binsof syntax
covergroup cg_filter;
cp_opcode: coverpoint opcode {
bins read_ops = {READ, READ_BURST};
bins write_ops = {WRITE, WRITE_BURST};
}
cp_burst: coverpoint burst_len {
bins single = {1};
bins burst = {[2:16]};
}
// Only count read × burst-length combinations; ignore write × burst.
cr_read_only: cross cp_opcode, cp_burst {
bins read_with_any = binsof(cp_opcode.read_ops) && binsof(cp_burst);
}
endgroup
// Without binsof: 4 cross bins (read × single, read × burst, write × single, write × burst)
// With binsof: 2 cross bins (read × single, read × burst)
// The write_ops × * combinations are excluded from this cross entirely.The semantic rule. binsof(cp.bin_name) selects one specific bin from a coverpoint. binsof(cp) selects all bins from a coverpoint. The && operator combines selections from different coverpoints into a single cross-bin definition. Each bins name = expr; inside a cross body creates one named cross bin whose value space is the combination described by the expression.
intersect — selecting bins by value rather than by name
intersect lets you select bins by the values they cover rather than by their declared names. Useful when you need to scope a cross-bin filter to a specific value or range without referring to a bin name.
covergroup cg_intersect;
cp_opcode: coverpoint opcode {
bins add = {ADD}; bins sub = {SUB};
bins mul = {MUL}; bins div = {DIV};
}
cp_size: coverpoint operand_size {
bins byte_sz = {8};
bins half_sz = {16};
bins word_sz = {32};
bins large_sz = {[33:64]};
}
// ── Exclude the architecturally-impossible DIV-with-large combination
cr_op_size: cross cp_opcode, cp_size {
ignore_bins div_large = binsof(cp_opcode) intersect {DIV} &&
binsof(cp_size.large_sz);
}
endgroupReading rule. binsof(cp_opcode) intersect {DIV} means "the bin in cp_opcode that contains the value DIV" — which is the bin named div. The form is functionally equivalent to binsof(cp_opcode.div) in this case, but intersect is more flexible: it works against any value set, not just a named bin, and it composes with ranges (intersect {[ADD:SUB]}).
The two selection mechanisms compose. Use binsof(cp.name) when you have a bin name; use binsof(cp) intersect {…} when you want to select by value or range. Production environments use both, depending on whether the filter is "this specific named scenario" or "any value in this range."
Illegal and Ignore Bins Inside Crosses
The same illegal_bins / ignore_bins discipline from Module 11.5 applies inside crosses. The semantics are the same — illegal cross bins fire runtime violations when hit, ignore cross bins are silently excluded from the cross's denominator — but the value space is a cross combination, not a single value.
covergroup cg_protocol;
cp_cmd: coverpoint cmd {
bins rd = {CMD_READ};
bins wr = {CMD_WRITE};
bins rfr = {CMD_REFRESH};
}
cp_addr_region: coverpoint addr_region {
bins csr = {REGION_CSR};
bins ram = {REGION_RAM};
bins reserved = {REGION_RESERVED};
}
cr_cmd_region: cross cp_cmd, cp_addr_region {
// ── Reserved region must never be accessed by anything ────
illegal_bins illegal_to_reserved =
binsof(cp_addr_region.reserved);
// ── REFRESH can only target RAM; other combinations are
// architecturally impossible — out of scope, not violation
ignore_bins rfr_not_ram =
binsof(cp_cmd.rfr) && !binsof(cp_addr_region.ram);
}
endgroupWhat this does. Three cmd bins × three region bins = nine total combinations. The illegal cross bin covers every cmd × reserved combination (three combinations: rd × reserved, wr × reserved, rfr × reserved); if any of those fire, the simulator raises a violation. The ignore cross bin covers every rfr × non-ram combination (two combinations: rfr × csr, rfr × reserved); those are excluded from the cross's denominator entirely.
Net counted bins. Nine total – three illegal – two ignored = four counted cross bins (rd × csr, rd × ram, wr × csr, wr × ram). The cross's 100% calculation uses only those four.
Cross Options and Weights
A labelled cross supports the same per-coverpoint options as a coverpoint: weight, at_least, goal, and comment.
covergroup cg;
cp_addr: coverpoint addr {
bins csr = {[0:127]};
bins ram = {[128:255]};
}
cp_data: coverpoint data {
bins zero = {0};
bins nonzero = {[1:255]};
}
cp_write: coverpoint write;
// ── Critical cross — high weight, 5 hits per cross bin required
cr_addr_data: cross cp_addr, cp_data {
option.weight = 10;
option.at_least = 5;
option.goal = 100;
option.comment = "vplan §3.1 — addr × data combinations, "
+ "critical for power-on access ordering";
}
// ── Standard cross — default options
cr_addr_write: cross cp_addr, cp_write {
option.comment = "vplan §3.2 — addr × write direction";
}
endgroupThe option.weight = 10 makes cr_addr_data contribute ten times the influence of cr_addr_write to the headline covergroup percentage. The option.at_least = 5 raises the per-bin closure bar — a cross bin in cr_addr_data reports "covered" only after five hits, not one. This is the right shape for crosses the verification plan flags as critical: the dashboard feels the gap when a critical cross is incomplete.
The Cross-Bin Explosion Problem
Cross coverage scales as the product of the crossed coverpoints' bin counts. The growth is unforgiving.
| Cross | Bins each | Total cross bins | Achievability |
|---|---|---|---|
| Two-way A × B | 3, 3 | 9 | Easy |
| Two-way A × B | 4, 4 | 16 | Easy |
| Two-way A × B | 8, 8 | 64 | Moderate |
| Three-way A × B × C | 3, 3, 3 | 27 | Moderate |
| Three-way A × B × C | 4, 4, 4 | 64 | Hard — needs ignore_bins discipline |
| Three-way A × B × C | 8, 8, 8 | 512 | Likely-impossible at sign-off |
| Two-way, auto-binned 8-bit fields | 64, 64 | 4,096 | Effectively impossible |
| Two-way, auto-binned 32-bit fields | 64, 64 | 4,096 | Effectively impossible |
The single most expensive verification mistake in cross coverage is crossing coverpoints that have unnecessarily many bins. The fix is upstream, in the coverpoint design: explicit named bins that group values into spec-meaningful regions, before the cross is declared.
Four discipline rules for managing cross size
// ── Rule 1 — Cross only EXPLICIT-binned coverpoints ─────────────────
covergroup cg_good;
cp_addr: coverpoint paddr {
bins csr = {[32'h0000_0000 : 32'h0000_00FF]}; // 1 bin
bins ram = {[32'h1000_0000 : 32'h1FFF_FFFF]}; // 1 bin
bins mmio = {[32'h2000_0000 : 32'h2FFF_FFFF]}; // 1 bin
}
cp_op: coverpoint cmd {
bins rd = {CMD_READ}; bins wr = {CMD_WRITE};
}
cr_addr_op: cross cp_addr, cp_op; // 3 × 2 = 6 bins — tractable
endgroup
// ── Rule 2 — NEVER cross auto-binned coverpoints ───────────────────
covergroup cg_bad;
cp_addr: coverpoint paddr; // ← auto bins on 32-bit → 64 bins
cp_data: coverpoint pwdata; // ← auto bins on 32-bit → 64 bins
cross cp_addr, cp_data; // ← 64 × 64 = 4096 cross bins
// Effectively impossible to close
endgroup
// ── Rule 3 — Use binsof / ignore_bins to scope the cross ───────────
covergroup cg_scoped;
cp_op: coverpoint cmd {
bins rd = {READ}; bins wr = {WRITE};
bins broadcast = {BROADCAST}; // exists, but not in this cross
}
cp_addr: coverpoint addr_region {
bins csr = {CSR}; bins ram = {RAM};
}
// Cross excludes the broadcast operation — keeps the bin count down
cr_op_addr: cross cp_op, cp_addr {
ignore_bins drop_broadcast = binsof(cp_op.broadcast);
}
endgroup
// ── Rule 4 — Prefer two-way crosses; reserve three-way for critical
covergroup cg_disciplined;
cp_op: coverpoint cmd; // 4 bins
cp_addr: coverpoint region; // 4 bins
cp_burst: coverpoint burst_len; // 4 bins
// Two two-way crosses (4×4 + 4×4 = 32 bins total)
cr_op_addr: cross cp_op, cp_addr;
cr_op_burst: cross cp_op, cp_burst;
// NOT a three-way cross unless the spec specifically calls out
// the op × addr × burst combination as a critical scenario.
endgroupThe pattern that scales: every coverpoint that participates in a cross has fewer than 10 bins, and those bins are explicit-named and spec-aligned. Three-way crosses appear only when the spec calls out the three-feature interaction as a critical scenario, never as a "completeness gesture." Auto-binned coverpoints never appear in a cross at all.
Debug Lab — "The Cross Bin Is at 0% but Both Coverpoints Are at 100%"
A failure mode that catches every team at least once: each axis of the cross is fully covered, but the specific combination is still empty. The expectation "if A and B are each fully exercised, every (A, B) combination must be too" is structurally wrong.
class apb_txn;
rand bit [31:0] paddr;
rand bit pwrite;
constraint c_addr {
paddr inside { [32'h0000_0000 : 32'h0000_00FF],
[32'h1000_0000 : 32'h1FFF_FFFF],
[32'h2000_0000 : 32'h2FFF_FFFF] };
}
constraint c_dir {
// ── BUG: write biased to 99%, read to 1%; reads will only
// realistically appear with mid/high addresses
pwrite dist { 1'b1 := 99, 1'b0 := 1 };
}
covergroup cg;
cp_addr: coverpoint paddr {
bins csr = {[32'h0000_0000 : 32'h0000_00FF]};
bins ram = {[32'h1000_0000 : 32'h1FFF_FFFF]};
bins mmio = {[32'h2000_0000 : 32'h2FFF_FFFF]};
}
cp_dir: coverpoint pwrite {
bins read = {1'b0};
bins write = {1'b1};
}
cr_addr_dir: cross cp_addr, cp_dir;
endgroup
endclass
// After 5000 transactions:
// cp_addr 100.0% (csr, ram, mmio all hit hundreds of times)
// cp_dir 100.0% (read hit ~50 times, write hit ~4950 times)
// cr_addr_dir 50.0% (csr × write, ram × write, mmio × write at 100%;
// csr × read at 100% — only because the few
// reads happened to land there;
// ram × read at 0%, mmio × read at 0%)The covergroup report shows cp_addr 100%, cp_dir 100%. The verification engineer believes the individual features are fully covered. The cross cr_addr_dir reports 50% — three of six combinations are empty. The natural first reaction is "the cross definition is wrong" — but the cross is correct, the coverpoints are correct, and the gap is in the joint distribution the test produces, not in the model.
The constraint c_dir biases pwrite heavily toward 1 (write). The constraint c_addr distributes addresses uniformly across three regions. The joint distribution that the constraint solver produces is therefore: ~99% of transactions are writes (spread across all three address regions), ~1% are reads (spread thinly across the same three regions). With 5000 transactions, ~50 reads — far too few to reliably hit all three address regions. The csr × read combination got lucky; ram × read and mmio × read did not.
The conceptual mistake is the implicit assumption "100% on A and 100% on B implies 100% on the cross." It does not — a cross of M × N bins needs each combination to be sampled at least option.at_least times. A skewed joint distribution can leave most combinations empty while both single coverpoints fill quickly. This is precisely why cross coverage exists as a separate metric.
Two valid fixes, depending on the verification plan's intent.
Fix A — if the read/write distribution should be balanced for coverage closure: rebalance the constraint to give reads enough mass to hit every address region.
constraint c_dir {
pwrite dist { 1'b1 := 50, 1'b0 := 50 }; // 50/50 read vs write
}Fix B — if the read distribution should remain skewed but the cross must still close: add directed tests or a coverage-driven sequence that targets the empty cross bins specifically.
class cross_closure_sequence extends uvm_sequence #(apb_txn);
task body();
apb_txn t;
// Three directed reads — one per region — to close the cross gap
foreach (regions[i]) begin
`uvm_do_with(t, {
paddr inside { regions[i] };
pwrite == 1'b0;
})
end
endtask
endclassThe discipline rule that prevents recurrence: treat coverpoint percentages as necessary but insufficient evidence of cross-coverage completeness. The cross has its own closure number; don't infer it from the per-coverpoint numbers. For every cross in the model, run a regression-level audit that compares the cross's percentage against the product-of-individual-percentages — a large gap (cross 50% with both axes 100%) signals a joint-distribution skew that needs either a constraint rebalance or directed-test coverage.
Industry Context — Where Cross Coverage Lives
- AXI / AHB / APB verification. Every shipping AMBA VIP includes a cross of
cmd × addr-region,cmd × burst-length,cmd × strobe-shape, andburst-type × burst-length. These are the combinations where protocol bugs concentrate; the cross report is the audit document for "every documented combination was exercised." - Memory controller verification. Cross of
cmd × bank × rankfor DDR/HBM; cross ofrefresh-state × read-statefor refresh interaction. The verification-plan sign-off rows directly map to these crosses. - Cache coherence verification. Cross of
transaction-type × cache-state × snoop-responsefor MESI/MOESI protocols. Three-way cross is justified here because every (txn, state, snoop) tuple is a spec-defined coherence scenario. - Cryptographic IP verification. Cross of
mode × key-length × payload-classfor AES/SHA/RSA cores. Cross gaps here often map directly to NIST CAVP certification holes. - UVM scoreboard coverage. Cross of
transaction-direction × expected-vs-actual-statusto verify every direction got compared against every status outcome — a structural audit of the scoreboard's reach. - DO-254 / ARP4754A flows. Crosses are the canonical way to render "feature interaction" verification-plan rows; the traceability matrix maps cross labels to spec sections, and the cross's
option.commentcarries the citation.
The structural fact behind these uses: in any IP whose behaviour is governed by feature interactions (and most non-trivial IP is), individual-feature coverage cannot prove the spec was met. Cross coverage is the only metric that can.
Interview Q&A — Ten Questions You Will Be Asked
Combinations of features rather than features in isolation. A cross of two coverpoints with M and N bins creates M × N cross bins, one per (a-bin, b-bin) combination. The cross is "covered" when every combination has been hit at least option.at_least times. Where individual coverpoints answer "was each feature exercised," cross coverage answers "were the features exercised together."
Coding Guidelines — Seven Rules for Cross-Coverage Discipline
- Label every cross. Unlabelled crosses cannot be referenced by
binsof, cannot carryoption.*settings, and report under tool-generated names. The cost is one word; the payoff is that the cross is now a first-class participant in the coverage discipline. - Never cross auto-binned coverpoints. Wide-field auto bins create dozens of bins per axis, and the cross becomes effectively impossible to close. Every coverpoint that feeds a cross has explicit named bins matched to the verification plan.
- Two-way crosses by default; three-way only with a spec citation. Three-way crosses scale by multiplication and consume regression budget exponentially. Reserve them for combinations the spec explicitly calls out as critical, and cite the spec section in
option.comment. - Use
ignore_binsto drop architecturally-impossible combinations — with a documented reason. Every crossignore_binsdeclaration cites the architectural constraint that makes the combination impossible. Without the citation, the ignore form becomes the gap-hiding tool the dashboard reads but the spec audit doesn't. - Use
illegal_binsinside crosses for "must never happen" combinations. The cross illegal-bin form is the canonical tool for documenting spec-forbidden combinations (cmd × reserved-region, op × illegal-mode). The violation report names both offending values — more informative than a single-coverpoint illegal bin. - Weight critical crosses higher than routine ones.
option.weightcontrols how much each item contributes to the covergroup's headline number; weighting the spec-critical crosses higher makes the dashboard feel the gap when they're incomplete. Combined withoption.at_least, the weighting expresses the spec's priorities directly in the coverage model. - A cross at 0% with both axes at 100% is a distribution problem, not a cross problem. Don't remove the cross bin or relax the closure rule. Either rebalance the constraint, add
solve beforeto fix satisfying-set skew, or write a directed sequence that targets the empty combinations. The cross is doing its job — surfacing the gap.
Summary
Cross coverage measures the Cartesian product of crossed coverpoints' bins — turning "each feature was exercised" into "every spec-defined feature combination was exercised together." The construct is straightforward (cross cp_a, cp_b;); the discipline is structural — every cross is labelled, every participating coverpoint has explicit named bins, binsof and ignore_bins scope the cross to the verification plan's relevant combinations, illegal cross bins document spec-forbidden combinations, and per-cross weights reflect spec priorities. The single most common failure mode is reading 100% on the individual axes as "cross-coverage done" — a skewed joint distribution can leave most cross combinations empty even when each coverpoint is fully covered. Module 11.8 builds on this with the coverage-options vocabulary — at_least, weight, goal, comment, per_instance, and the other knobs that tune coverage models toward sign-off.