Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
    • Scoreboard Architecture: Expected vs Actual
    • Reference Models: Predictors, DPI-C and Built-in Comparators
    • Coverage & Closure
      • Designing a Coverage Model from the Spec
      • Coverage Components and Sampling
      • Bins, Illegal Bins and Ignore Bins
      • Crosses and Plan Traceability
      • Coverage Options That Change the Numbers
      • SVA cover property vs Covergroups
      • Regression Merge and the Closure Loop
      • Closing Coverage: The Holes Random Never Hits
      • Reporting and Verbosity Control
      • Code Coverage and How It Closes with Functional Coverage
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Coverage & Closure
  1. Topics
  2. UVM Verification
  3. Checking & Coverage in UVM
  4. Coverage & Closure
  5. Crosses and Plan Traceability

Part 8 · Checking & Coverage · Intermediate

Crosses and Plan Traceability

The len_x_irq cross, pruning with binsof and ignore_bins, mapping plan rows to coverpoints, and the plan-item tally.

On this page

    Merge dma_check_test, dma_zero_len_test and the closure test from the closure lesson, and both coverpoints of job_cg are complete: every LEN class has been run, and so have both IRQ_EN values. The cross len_x_irq sits at 6 of 12. A manager reads that as "half the combinations untested". A designer says half of them do not matter.

    Both could be right, and the only way to settle it is the plan. This lesson is about crosses, about pruning them to the cells that carry risk, and about the bookkeeping that ties every bin to a plan row.

    The idea: single values are not combinations

    A cross records which combinations of coverpoint bins were sampled together. Think of a café menu: knowing that every drink and every pastry was sold at some point does not tell you that anyone bought the breakfast deal. Bugs live in combinations because designers write the special cases separately: the zero-length path, the interrupt path. Whether they work together is a separate question.

    A cross of two coverpoints has one cross bin (a cell) per pair of their bins: cp_len has 6 bins and cp_irq has 2, so len_x_irq has 12 cells. Values removed by ignore or illegal bins on a coverpoint never reach the cross. Inside a cross, binsof(cp) selects cells by the bins of one coverpoint, intersect {...} narrows that to bins whose values overlap a set, and ignore_bins or named bins use those selections to prune or name cells.

    The picture: the cross after three tests

    cp_len bin

    irq_en = 0 (auto[0])

    irq_en = 1 (auto[1])

    zero

    hole

    dma_zero_len_test

    one

    hole

    dma_check_test (len 1)

    few (2-4)

    hole

    dma_check_test (len 3)

    many (5-15)

    hole

    dma_check_test (len 10), closure (len 8)

    max_pkt (16)

    closure (len 16, polled)

    hole

    over (17+)

    hole

    closure (len 20)

    Six of twelve. The only IRQ_EN = 0 job ran with LEN = 16. The tally says irq_en=0 is covered; the cross says that one polled job never met the lengths where the interrupt logic behaves differently. That is exactly the information a cross exists to give.

    Build it: from a full cross to a traceable one

    Step 1: ask which cells carry risk

    The interrupt is irq = IRQ_EN && (DONE || ERR). LEN matters to it in only two ways: a zero-length job sets DONE in the GO cycle, so the interrupt rises at once, and a one-word job finishes on its first beat. Every longer job ends the same way. So the plan row "interrupt behaviour versus job length" needs zero, one and a normal length, each with IRQ_EN on and off: 6 cells, not 12.

    Step 2: prune the cross to those cells

    Two equivalent styles. The first keeps automatic cells and removes the ones the plan does not need; the second names every wanted cell and drops the rest. Both are variants of job_cg, not bench code:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // snippet: plan-pruned crosses for job_cg (variants, not in the bench)
    cp_irq: coverpoint irq { bins off = {0}; bins on = {1}; }
    
    // Style 1: remove cells the plan does not need
    len_x_irq: cross cp_len, cp_irq {
      ignore_bins no_irq_effect = binsof(cp_len) intersect {[2:4], 16, [17:$]};
    }
    
    // Style 2: name the cells, ignore everything else (portable)
    len_x_irq_named: cross cp_len, cp_irq {
      bins zero_off = binsof(cp_len.zero) && binsof(cp_irq.off);
      bins zero_on  = binsof(cp_len.zero) && binsof(cp_irq.on);
      bins one_off  = binsof(cp_len.one)  && binsof(cp_irq.off);
      bins one_on   = binsof(cp_len.one)  && binsof(cp_irq.on);
      bins many_off = binsof(cp_len.many) && binsof(cp_irq.off);
      bins many_on  = binsof(cp_len.many) && binsof(cp_irq.on);
      ignore_bins rest = !binsof(cp_len.zero) && !binsof(cp_len.one) && !binsof(cp_len.many);
    }

    In style 1, binsof(cp_len) intersect {[2:4], 16, [17:$]} selects the few, max_pkt and over bins, and every cell built from them is ignored. In style 2, a cross bin defined with bins groups everything its expression selects into one bin, which is why each named cell spells out both coverpoints. The final ignore_bins rest removes any automatic cell left over. Some teams set option.cross_auto_bin_max = 0 for the same effect, but tool support for that setting varies, and the negated binsof works everywhere.

    Step 3: map plan rows to the model

    Traceability is a table that a reviewer can check line by line. Rows come from the spec page's corner-case list; the middle column is the model element; the last column is what the bench's tally prints.

    Plan row

    Model element

    Tally items

    Data is word ^ KEY across LEN classes

    job_cg.cp_len

    len=0 … len>16

    Interrupt only when enabled, versus job length

    cp_irq, len_x_irq

    irq_en=0, irq_en=1 (no cross items)

    Early TLAST ends the job with ERR

    stream_cg.cp_tlast, status_cg.cp_outcome

    tlast=early, outcome=err

    DONE and ERR are W1C

    clear_cg.cp_clear

    clear=done, clear=err (no item for clear_both)

    Input gaps of every size

    stream_cg.cp_gap

    gap=0, gap=1-3, gap=4+

    ERR survives GO

    status_cg.cp_outcome regular bin both

    none

    Backpressure loses nothing

    none in dma_coverage

    none

    KEY and LEN ignored while busy

    none

    none

    Flowchart: a plan row maps to cp_len and cp_irq bins, which feed the cross and the tally items; the cross goes to the coverage database, the tally to a log line.
    One plan row feeds two reports. The covergroup (with its cross) goes to the coverage database; the tally projects only the single coverpoints into a log line.

    Step 4: the plan-item tally

    The tally exists because this course's simulator ignores coverpoints, but it earns its place on any simulator: one grep-able line per test that names its holes. It is a projection, though. It has no cross cells and no clear_both, so "9 of 17" is a count of named items, never a covergroup percentage.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // snippet: from dma_coverage in the course DMA bench
    function void hit(string item);
      plan_hits[item]++;
    endfunction
    
    function string len_bin(bit [15:0] len);
      if (len == 0) return "len=0";
      if (len == 1) return "len=1";
      if (len <= 4) return "len=2-4";
      if (len <= 15) return "len=5-15";
      if (len == 16) return "len=16";
      return "len>16";
    endfunction
    
    function void report_phase(uvm_phase phase);
      string holes = "";
      `uvm_info("COV", $sformatf("covergroups: job %0.1f%%, status %0.1f%%, clear %0.1f%%, stream %0.1f%%",
                job_cg.get_inst_coverage(), status_cg.get_inst_coverage(),
                clear_cg.get_inst_coverage(), stream_cg.get_inst_coverage()), UVM_LOW)
      foreach (plan[i]) if (!plan_hits.exists(plan[i])) holes = {holes, " ", plan[i]};
      `uvm_info("COV", $sformatf("plan items hit: %0d of %0d", plan_hits.num(), plan.size()), UVM_LOW)
      if (holes != "") `uvm_info("COV", {"plan holes:", holes}, UVM_LOW)
    endfunction

    Watch it run: three tests, three tallies

    Diagram
    +UVM_TESTNAME=dma_check_test
    UVM_INFO dma_tb.sv(1530) @ 1305000: uvm_test_top.env.cov [COV] plan items hit: 9 of 17
    UVM_INFO dma_tb.sv(1531) @ 1305000: uvm_test_top.env.cov [COV] plan holes: len=0 len=16 len>16 irq_en=0 outcome=err clear=err gap=0 tlast=early
    +UVM_TESTNAME=dma_cmp_trap_test
    UVM_INFO dma_tb.sv(1530) @ 655000: uvm_test_top.env.cov [COV] plan items hit: 7 of 17
    UVM_INFO dma_tb.sv(1531) @ 655000: uvm_test_top.env.cov [COV] plan holes: len=0 len=1 len=2-4 len=16 len>16 irq_en=0 outcome=err clear=err gap=0 tlast=early
    +UVM_TESTNAME=dma_zero_len_test
    UVM_INFO dma_tb.sv(1530) @ 325000: uvm_test_top.env.cov [COV] plan items hit: 4 of 17
    UVM_INFO dma_tb.sv(1531) @ 325000: uvm_test_top.env.cov [COV] plan holes: len=1 len=2-4 len=5-15 len=16 len>16 irq_en=0 outcome=err clear=err gap=0 gap=1-3 gap=4+ tlast=on-time tlast=early
    • dma_cmp_trap_test runs one 8-word job, so it misses two more LEN classes than the random test.

    • dma_zero_len_test hits exactly four items (len=0, irq_en=1, outcome=done, clear=done) and no stream items at all, because no beat moves.

    • Each log is per test. The regression's answer is the union, which the merge lesson builds.

    What breaks: the plan row with no bin

    Look again at the last two rows of the traceability table. "KEY and LEN ignored while busy" is in the spec's corner-case list, and dma_coverage has no item for it. That gap is not hypothetical. The bench's predictor and scoreboard both take a new KEY at once, while the DMA ignores KEY writes during a job; no test writes KEY mid-job, so nothing ever showed it. A scratch run that does gives mismatches in both checkers. Coverage could have pointed straight at the missing test, but only if the row had a bin. As it stands, the tally will one day print 17 of 17 with that row never exercised.

    The rule that prevents it: every plan row gets a model element before the regression is trusted, even an empty one, so it can show up as a hole. The opposite failure is the cross explosion: crossing cp_len (6), cp_gap (3), cp_tlast (2) and cp_irq (2) gives 72 cells, many impossible (a zero-length job has no beats, so it has no gap). An unprunable cross is never closed, and an unclosable number teaches the team to stop reading the report.

    Interview

    "How do you keep cross coverage meaningful, and how do you trace coverage back to the testplan?"

    A weak answer: "I cross the important fields and filter with binsof." Which fields, why, and how does anyone check?

    A strong answer: "I cross only fields the spec makes interact, and I prune to the cells that carry risk. On our DMA, LEN changes the interrupt only for zero and one-word jobs, so the IRQ-versus-length cross needs 6 cells, not 12. I name bins after plan rows, keep a traceability table from row to coverpoint to bin, and make sure every row has at least one element, because a row with no bin can never show up as a hole. The regression report then reads as plan evidence, row by row."

    Expect the follow-ups: How do you exclude cells portably? (an ignore_bins with a negated binsof, rather than relying on cross_auto_bin_max = 0) and What if two related facts are sampled in different covergroups? (list both in the plan row, or carry one fact into the other's sample, for example an early-TLAST flag passed to the status sample).

    Check yourself With the 6-cell plan cross, which cells did the three merged tests hit?

    zero_on (zero-length test), one_on (len 1), many_on (len 10 and len 8). The polled IRQ_EN = 0 job used LEN = 16, which this cross ignores. So 3 of 6, with all three IRQ_EN = 0 cells open, even though the tally reports irq_en=0 as covered.

    Check yourself cp_len gains ignore_bins huge = {[1024:$]};. What happens to len_x_irq?

    Values in the ignore range never reach the cross, so cells in the over row count only lengths 17 to 1023. A cross is built from the coverpoints' bins after their ignore and illegal bins are applied.

    Check yourself Why can the bench's tally never report a hole in len_x_irq?

    The tally names only single-coverpoint items (LEN classes, IRQ_EN values and so on). It has no items for cross cells, so cross holes are visible only in a covergroup report from a simulator that collects coverpoints.

    Next: the same hits can still produce different percentages. Coverage Options That Change the Numbers shows which settings move the number and which move the hits.

    Related topics

    • Up: Coverage & Closure

    • SystemVerilog: binsof and intersect

    • SystemVerilog: cross explosion

    Previous

    140 / 280

    Next: Coverage Options That Change the Numbers

    Up next

    Up nextCoverage Options That Change the Numbers

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Designing a Coverage Model from the Spec
    • Coverage Components and Sampling
    • SVA cover property vs Covergroups