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. Designing a Coverage Model from the Spec

Part 8 · Checking & Coverage · Intermediate

Designing a Coverage Model from the Spec

From the DMA spec to a testplan to covergroups: what to cover, where to sample it, and why coverage must record what the DUT did.

On this page

    The DMA's coverage review looks finished. The clear covergroup reads 100%: DONE cleared, ERR cleared. Then the designer asks which test produced an ERR for that clear to act on. Nobody can name one, because none did. Every job in the regression ended with DONE, and the test helper run_job writes 0x6 to STATUS after every job, setting both write-1-to-clear bits whether or not they were set. Writing 1 to a bit that is already 0 does nothing in the hardware, and the coverage model counted it anyway.

    That bug is real: it was found while building this course's bench. It is the cleanest illustration of the question this lesson answers. How do you design a coverage model that measures what the DUT did, rather than what the testbench typed?

    The idea: coverage measures the plan, not the code

    Code coverage tells you which RTL lines, branches and toggles were exercised. It is collected automatically and knows nothing about intent. Functional coverage tells you which promises in the spec were exercised, and it exists only if you write it. The document that lists those promises is the testplan: one row per feature, each with the stimulus that exercises it, the check that proves it worked, and the coverage item that proves the stimulus really happened. A coverage item (or plan item) is a named thing to observe, such as "a job with LEN = 0". In SystemVerilog it usually becomes a bin of a coverpoint inside a covergroup.

    Think of a driving test. The examiner's sheet lists manoeuvres: parallel parking, a hill start, an emergency stop. A box is ticked only when the examiner saw the manoeuvre, never because the learner says they practised it. A coverage model is that sheet. The three rules that follow from the analogy carry the whole lesson: every element traces to a plan row; every sample comes from observed behaviour, not stimulus intent; and every sample is taken at the moment the fact becomes true.

    The picture: from a spec sentence to a sample point

    Flowchart: a spec sentence becomes a plan row, which is sampled in a class covergroup, an interface covergroup, a cover property or register coverage, all feeding one coverage database.
    Each plan row is sampled where its fact is visible. Most DMA facts are visible in one observed transaction; timing facts need per-cycle or temporal coverage in the interface.

    The decision in the middle is the design work. Asking "where is this fact visible?" before writing any code prevents the two most common model bugs: sampling a transaction-level covergroup for something that only exists between cycles, and sampling a value from the stimulus because it was convenient.

    Build it: the DMA testplan, row by row

    Step 1: list what to cover, by category

    Walk the spec (The DMA you will verify) and sort each promise into a category. The categories are a checklist that stops whole areas from being forgotten.

    Category

    On the DMA

    Plan items in dma_coverage

    Configuration

    LEN value classes, IRQ_EN

    len=0, len=1, len=2-4, len=5-15, len=16, len>16, irq_en=0, irq_en=1

    Stimulus shape

    Gaps between input beats; a packet shorter than LEN

    gap=0, gap=1-3, gap=4+, tlast=on-time, tlast=early

    Responses

    How the job ended, as read from STATUS

    outcome=done, outcome=err

    Errors

    Early TLAST; an unmapped address (PSLVERR)

    tlast=early, outcome=err; PSLVERR has no item yet

    Register side effects

    W1C clears of bits that were actually set

    clear=done, clear=err

    Timing and backpressure

    Output stalls; TVALID held while TREADY is low

    None: a cover property job (lesson 6)

    Interrupts

    irq rises only with IRQ_EN; the level holds until cleared

    irq_en via the GO write only. The level is a plan gap today: it needs two cover properties, irq rising while IRQ_EN is set and irq falling only after a STATUS write that clears DONE or ERR (SVA cover property vs covergroups shows the shape)

    The last column is the honest state of the bench. Seventeen plan items exist as code. Several promises in the spec (backpressure, PSLVERR, KEY or LEN written while busy, ERR surviving GO) have no item at all. A promise with no item can never appear as a hole, which is worse than a hole: the report goes green without ever having measured it.

    Step 2: write the plan as names the model can report

    dma_coverage keeps its plan items as a list of strings. The covergroups measure; the list gives every item a name that a log line can print.

    design.sv
    1
    2
    3
    4
    5
    // snippet: from dma_coverage in the course DMA bench
    string       plan[$] = '{"len=0", "len=1", "len=2-4", "len=5-15", "len=16", "len>16",
                             "irq_en=0", "irq_en=1", "outcome=done", "outcome=err",
                             "clear=done", "clear=err", "gap=0", "gap=1-3", "gap=4+",
                             "tlast=on-time", "tlast=early"};

    Each name reads like a testplan cell, so a hole in the log (plan holes: len=0 ...) maps back to a plan row without a lookup table. The lesson on crosses and traceability shows how these names map to bins.

    Step 3: choose where each fact is sampled

    Sampling site

    Good for

    DMA example

    Class covergroup fed by a monitor, with sample() arguments

    One fact per transaction

    job_cg at the GO write; status_cg on a STATUS read

    Covergroup in the interface, on the clock

    Per-cycle states and transitions

    TVALID/TREADY handshake states, stall-then-transfer

    cover property in the interface

    Sequences over several cycles

    TVALID held while TREADY is low for 3 cycles; an APB wait state

    Register coverage through the register model

    Which fields were written and read, with which values

    Covered in the register layer section

    Most of the DMA's plan is visible in single transactions: a GO write, a STATUS read, a beat. That is why dma_coverage is one component with four transaction covergroups. Timing facts are the exception, and they get their own lesson.

    Step 4: cover what the DUT did, not what the testbench sent

    For "how did the job end", two sources are available. The test's intent: it sent a 3-word packet to an 8-word job, so it expects ERR. Or the STATUS read the APB monitor observed. Only the second proves the DUT reached that state; a DUT that ignored TLAST would still have "covered" ERR under the first. The bench makes the observed choice everywhere:

    • job_cg is sampled when the APB monitor sees the GO write, using the LEN value latched from the last observed LEN write. Accesses that ended with PSLVERR are skipped.

    • The outcome comes from STATUS read data, never from what the test expected.

    • A clear counts only for a bit the last STATUS read showed as set (the fix for the opening bug).

    • Gaps between beats are derived from the times the input monitor saw each beat, not from the driver's idle knob, which is a request, not a measurement.

    Watch it run

    dma_check_test runs three random jobs with the checking environment, which adds dma_coverage. From the real run (Verilator with the open-source UVM library; times in ps; random values differ by simulator and seed):

    Diagram
    +UVM_TESTNAME=dma_check_test
    UVM_INFO dma_tb.sv(731) @ 0: uvm_test_top [JOB] key=90fdd8fd len=10 irq_en=1
    UVM_INFO dma_tb.sv(731) @ 645000: uvm_test_top [JOB] key=0e564b57 len=1 irq_en=1
    UVM_INFO dma_tb.sv(731) @ 885000: uvm_test_top [JOB] key=5a1793f4 len=3 irq_en=1
    UVM_INFO dma_tb.sv(1528) @ 1305000: uvm_test_top.env.cov [COV] covergroups: job 0.0%, status 0.0%, clear 0.0%, stream 0.0%
    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_INFO dma_tb.sv(411) @ 1305000: uvm_test_top.env.sb [SB] 14 words matched, 0 mismatched
    UVM_INFO dma_tb.sv(1639) @ 1305000: uvm_test_top.env [CMP] in-order comparator: 14 matches, 0 mismatches
    UVM_ERROR :    0
    UVM_FATAL :    0
    • covergroups: ... 0.0% is Verilator ignoring coverpoints. On a commercial simulator these four numbers are real.

    • plan items hit: 9 of 17 is the tally: LEN classes 1, 2-4 and 5-15, irq_en=1, outcome=done, clear=done, two gap classes and an on-time TLAST.

    • plan holes lists the eight items no job reached. Each has a different cause; the closure lesson works through them one by one.

    What breaks: the W1C clear that was never a clear

    The first version of the clear covergroup sampled the written bits directly. It looked reasonable: STATUS bits 1 and 2 are W1C, so a write with those bits set "clears" them.

    design.sv
    // bad-example: counts the write, not the clear
    if (t.write && t.addr == STATUS && t.data[2:1] != 0)
      clear_cg.sample(t.data[2:1]);   // 0x6 after a DONE-only job "covers" clear_err

    run_job writes 0x6 after every job, so every DONE-only job also counted a clear of ERR. The symptom was a covergroup at 100% and a plan row signed off with no ERR ever produced. The root cause is a model that sampled the testbench's action instead of the DUT's state change. The fix tracks what the last STATUS read showed and counts only bits that were set:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // snippet: from dma_coverage in the course DMA bench
    // A clear counts only for a bit that was actually set: writing 1 to an
    // already-clear W1C bit exercises nothing.
    if (t.write && t.addr == STATUS) begin
      bit [1:0] cleared = t.data[2:1] & {err_seen, done_seen};
      if (cleared != 0) begin
        clear_cg.sample(cleared);
        if (cleared[0]) hit("clear=done");
        if (cleared[1]) hit("clear=err");
      end
      done_seen &= !t.data[1];
      err_seen  &= !t.data[2];
    end

    The plain-SystemVerilog model below replays the three jobs of dma_check_test through both versions. Only the fixed one reports clear_err as the hole it really is.

    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
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    // The W1C coverage bug: counting a clear from the write data alone, versus
    // counting only bits that the last STATUS read showed as set.
    class clear_cov;
      bit naive;                       // 1: sample the written bits as they are
      bit done_seen, err_seen;         // as last read from STATUS
      int unsigned clear_done, clear_err;
      function new(bit naive);
        this.naive = naive;
      endfunction
      function void status_read(bit [2:0] rdata);
        done_seen = rdata[1];
        err_seen  = rdata[2];
      endfunction
      function void status_write(bit [2:0] wdata);
        bit [1:0] cleared = naive ? wdata[2:1] : (wdata[2:1] & {err_seen, done_seen});
        if (cleared[0]) clear_done++;
        if (cleared[1]) clear_err++;
        done_seen &= !wdata[1];
        err_seen  &= !wdata[2];
      endfunction
    endclass
    
    module tb_w1c_cov;
      int errors = 0;
      clear_cov naive_cov, fixed_cov;
      initial begin
        naive_cov = new(1);
        fixed_cov = new(0);
        // run_job, three times: every job ends with DONE, and the test writes 0x6
        // to clear DONE and ERR whether or not they are set.
        repeat (3) begin
          naive_cov.status_read(3'b010);  fixed_cov.status_read(3'b010);
          naive_cov.status_write(3'b110); fixed_cov.status_write(3'b110);
        end
        if (naive_cov.clear_err != 3) errors++;   // "clear ERR covered" with no ERR ever set
        if (fixed_cov.clear_err != 0) errors++;   // the honest answer: a hole
        if (fixed_cov.clear_done != 3) errors++;
        $display("naive model: clear_err hit %0d times; fixed model: %0d times",
                 naive_cov.clear_err, fixed_cov.clear_err);
        if (errors == 0) $display("PASS: only the fixed model reports clear_err as a hole");
        else $display("FAIL: %0d W1C coverage checks wrong", errors);
        $finish;
      end
    endmodule

    The general lesson: for every bin, name one test that should hit it and one that should not. A DONE-only regression must leave clear=err empty. If it does not, the model is wrong, however good the number looks.

    Interview

    "How would you build the functional coverage model for a new block?"

    A weak answer starts from the transaction class: "a covergroup on every field, crosses of everything, and we aim for 100%." That measures the stimulus generator, produces thousands of meaningless bins, and still misses every timing promise.

    A strong answer starts from the spec: "I turn each promise into a testplan row with a stimulus, a check and a coverage item. I sort the items by category (configuration, stimulus shape, responses, errors, side effects, timing, interrupts) so nothing is forgotten. For each item I decide where the fact is visible: a monitor transaction, a per-cycle state, a temporal sequence, or a register value. I sample observed behaviour only, at the moment it becomes true, and I name bins after plan rows. Then I review the model with the designer, and I prove each bin with a test that should hit it and one that should not, because a coverage model has bugs like any other code."

    Expect the follow-ups: How do you know the model is complete? (walk every spec sentence and register field, and check each has an item; a missing item is invisible) and How do you know it is correct? (negative checks, like the W1C clear that must stay empty in a DONE-only regression).

    Check yourself A colleague proposes sampling job_cg straight from the test's dma_job_cfg after randomize(). What could that coverage claim that the DUT never did?

    It would record the job the test meant to run. If the LEN or GO write failed, or the LEN write was ignored because the DMA was still busy, the DUT ran a different job or none, and the cfg-based coverage would still claim the LEN class. Sampling the observed GO write ties coverage to bus traffic. Note that dma_coverage latches LEN from every successful LEN write it observes, so a LEN write while busy would fool it too: the same model gap the spec page points out for the scoreboard.

    Check yourself Which promises of the DMA spec have no coverage item in dma_coverage, and why is that worse than a hole?

    Backpressure on the output, PSLVERR on unmapped addresses, KEY/LEN/GO written while busy, set-beats-clear on STATUS, and ERR surviving GO. A hole is visible: the tally lists it. A promise with no item never appears anywhere, so the tally can reach 17 of 17 while those promises were never measured.

    Check yourself Why is outcome=err sampled from a STATUS read rather than when the test sends a short packet?

    The short packet is stimulus intent; the STATUS read is evidence that the DUT set ERR. If the DUT ignored the early TLAST, intent-based coverage would still record the error outcome and hide the bug from the coverage review.

    Next: the plan is written down. Coverage Components and Sampling builds the component that listens to the monitors and feeds the covergroups.

    Related topics

    • Up: Coverage & Closure

    • The DMA you will verify

    • Stimulus, check and coverage

    • SystemVerilog: the coverage plan

    Previous

    137 / 280

    Next: Coverage Components and Sampling

    Up next

    Up nextCoverage Components and Sampling

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Bins, Illegal Bins and Ignore Bins
    • Crosses and Plan Traceability
    • Coverage Options That Change the Numbers