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. Coverage Components and Sampling

Part 8 · Checking & Coverage · Intermediate

Coverage Components and Sampling

A coverage component on analysis imps, sample-with-arguments covergroups, distinct instance names, and timing derived from observed beats.

On this page

    Suppose you put the stream covergroup in the driver, where axis_item.idle is right at hand. The item's constraint gives idle == 0 a weight of 6 in 10, so a bin for back-to-back beats would fill within the first few beats of every test, and the plan row "back-to-back beats" would be signed off. On the pins, not one pair of beats in the whole regression was back to back: this driver always drops TVALID for at least a clock between beats. The covergroup would be measuring the knob, not the bus.

    The DMA bench's real coverage component, dma_coverage, reports that row as a hole in every test, and that hole led to a driver fix. Where should coverage listen, and what exactly should it sample?

    The idea: coverage is a passive listener on observed traffic

    Picture a traffic survey. You can read the bus company's timetable, or you can lay a counting cable across the road. Only the cable tells you how many buses actually passed, and when. A monitor is the cable: it turns pin activity into transactions and broadcasts them on an analysis port. A coverage component subscribes to those ports exactly as the scoreboard does. It never drives, never raises an objection, and never reads anything the stimulus side knows but the pins do not show.

    UVM's uvm_subscriber #(T) is a component with one built-in analysis export and a write(T t) method to fill in. It fits a coverage collector that listens to one stream. dma_coverage needs two streams of different types (APB transfers and input beats), so it extends uvm_component and declares two analysis imps with uvm_analysis_imp_decl, the same pattern the scoreboard uses (several analysis inputs).

    The picture

    Flowchart: APB, input and output agent ports fan out to the scoreboard, the predictor and comparator, and the coverage component's two exports, which feed four covergroups.
    dma_env_check: the same agent ports feed the scoreboard, the predictor and the coverage component. Coverage sees exactly what the checkers see, and nothing from the sequences.

    Read the arrows. The coverage component gets the same observations as the checkers, through the same fan-out. Nothing reaches it from a sequence or a driver, so nothing it records can be intent.

    Build it: dma_coverage, one decision at a time

    Step 1: one analysis imp per stream

    Each uvm_analysis_imp_decl(SUFFIX) macro declares an imp class whose write calls write_SUFFIX in the owner. Two macros give two differently named methods in one component.

    design.sv
    // snippet: the declarations just above dma_coverage in the bench
    `uvm_analysis_imp_decl(_cov_apb)    // the imp calls write_cov_apb(apb_item)
    `uvm_analysis_imp_decl(_cov_in)     // the imp calls write_cov_in(axis_item)
    design.sv
    // snippet: from dma_coverage in the course DMA bench
    uvm_analysis_imp_cov_apb #(apb_item,  dma_coverage) apb_export;
    uvm_analysis_imp_cov_in  #(axis_item, dma_coverage) in_export;

    Step 2: covergroups that take their sample as arguments

    A covergroup declared with function sample(...) receives its values at the moment of the call. Nothing is read from class fields that may have changed in between, the sample point is visible in the code, and the same covergroup can be fed from different places. Two of the four:

    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
    // snippet: from dma_coverage in the course DMA bench
    covergroup job_cg with function sample(bit [15:0] len, bit irq); option.per_instance = 1;
      cp_len: coverpoint len {
        bins zero    = {0};
        bins one     = {1};
        bins few     = {[2:4]};
        bins many    = {[5:15]};
        bins max_pkt = {16};
        bins over    = {[17:$]};
      }
      cp_irq: coverpoint irq;
      len_x_irq: cross cp_len, cp_irq;
    endgroup
    
    covergroup stream_cg with function sample(int unsigned gap, bit early); option.per_instance = 1;
      cp_gap: coverpoint gap {
        bins back_to_back = {0};
        bins short_gap    = {[1:3]};
        bins long_gap     = {[4:$]};
      }
      cp_tlast: coverpoint early {
        bins on_time = {0};
        bins early   = {1};
      }
    endgroup

    A covergroup declared inside a class is an embedded covergroup, and the language allows it to be constructed only in the class constructor. Forget the new() and the first sample() is a null-handle crash.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // snippet: from dma_coverage in the course DMA bench
    function new(string name, uvm_component parent);
      super.new(name, parent);
      apb_export = new("apb_export", this);
      in_export  = new("in_export", this);
      job_cg     = new();
      status_cg  = new();
      clear_cg   = new();
      stream_cg  = new();
    endfunction

    Step 3: a distinct name for every instance

    Every covergroup instance needs its own name, or the report cannot tell instances apart. Put this component in each half of the two-DMA subsystem bench and, without names, both instances report under the same covergroup; with set_inst_name and the component's full path they become uvm_test_top.env.dma0.cov.job_cg and uvm_test_top.env.dma1.cov.job_cg. Names built from the hierarchy are also stable from run to run, which a regression merge depends on.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    // snippet: from dma_coverage in the course DMA bench
    function void build_phase(uvm_phase phase);
      super.build_phase(phase);
      job_cg.set_inst_name({get_full_name(), ".job_cg"});        // distinct names per instance
      status_cg.set_inst_name({get_full_name(), ".status_cg"});
      clear_cg.set_inst_name({get_full_name(), ".clear_cg"});
      stream_cg.set_inst_name({get_full_name(), ".stream_cg"});
    endfunction

    Step 4: sample observed traffic at the moment a fact becomes true

    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
    // snippet: from dma_coverage in the course DMA bench
    virtual function void write_cov_apb(apb_item t);
      if (t.slverr) return;
      if (t.write && t.addr == LEN) len_reg = t.data[15:0];
      if (t.write && t.addr == CTRL) begin
        irq_en = t.data[1];
        if (t.data[0]) begin                  // GO: one job with this length
          job_cg.sample(len_reg, irq_en);
          hit(len_bin(len_reg));
          hit(irq_en ? "irq_en=1" : "irq_en=0");
          beats_in_job = 0;
        end
      end
      // 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
      if (!t.write && t.addr == STATUS) begin
        done_seen = t.rdata[1];
        err_seen  = t.rdata[2];
        if (t.rdata[2:1] != 0) begin
          status_cg.sample(t.rdata[1], t.rdata[2]);
          hit(t.rdata[2] ? "outcome=err" : "outcome=done");
        end
      end
    endfunction
    • PSLVERR transfers are skipped. A failed access changed nothing in the DUT, so it must not cover anything.

    • LEN is latched, not sampled. A LEN write is configuration; the job only exists when GO is written, so job_cg samples at GO with the latched value.

    • The outcome is sampled on a STATUS read that shows DONE or ERR. Reads of an idle STATUS say nothing about how a job ended.

    • A clear is sampled only for bits that were set, the fix described in the model design lesson.

    Step 5: derive timing from observed beats

    The stream side has no field that says "gap". The input monitor publishes a beat when it sees TVALID and TREADY high on a clock edge, so the gap is computed from the times of consecutive beats.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // snippet: from dma_coverage in the course DMA bench
    virtual function void write_cov_in(axis_item t);
      int unsigned gap;
      bit early;
      gap = (beats_in_job == 0) ? 0 : int'(($time - last_beat_t) / 10ns) - 1;
      last_beat_t = $time;
      beats_in_job++;
      early = t.last && (beats_in_job < len_reg);
      stream_cg.sample(gap, early);
      if (beats_in_job > 1) hit(gap == 0 ? "gap=0" : (gap <= 3 ? "gap=1-3" : "gap=4+"));
      if (t.last) hit(early ? "tlast=early" : "tlast=on-time");
    endfunction

    Two things to notice. The 10ns is the clock period written into the formula, so a clock change would silently rescale every bin; a sturdier version has the monitor count clocks between transfers and publish the count as an observed field. And the gap counts every clock without a transfer, whether TVALID was low (the source was idle) or TREADY was low (the DMA stalled it). This covergroup cannot tell an idle from a stall; lesson 6 shows what can.

    Step 6: connect it in the env

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // snippet: from dma_env_check in the course DMA bench
    function void connect_phase(uvm_phase phase);
      super.connect_phase(phase);
      apb.ap.connect(pred.apb_export);
      in_agt.ap.connect(pred.in_export);
      if (use_class_cmp) begin
        pred.exp_ap.connect(class_cmp.before_export);     // expected
        out_agt.ap.connect(class_cmp.after_export);       // actual
      end else begin
        pred.exp_ap.connect(cmp.before_export);
        out_agt.ap.connect(cmp.after_export);
      end
      apb.ap.connect(cov.apb_export);
      in_agt.ap.connect(cov.in_export);
    endfunction

    The connections use the agent-level ap, never agt.mon.ap, so the env does not depend on what is inside an agent. The coverage component is one more subscriber on ports that already fan out to the predictor and scoreboard.

    Watch it run

    Diagram
    +UVM_TESTNAME=dma_check_test
    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
    • The report comes from uvm_test_top.env.cov, the component's full name, which is also the prefix of every covergroup instance name.

    • gap=1-3 and gap=4+ were hit; gap=0 was not, in 14 beats with idle == 0 drawn for most of them.

    • The 0.0% figures are Verilator ignoring coverpoints; the tally is the evidence on this simulator.

    What breaks: coverage that samples the knob

    Here is the stream driver's loop. After each handshake it drops TVALID, and the next beat waits for at least one more clock edge before raising it again:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // snippet: from axis_driver in the course DMA bench
    task run_phase(uvm_phase phase);
      vif.tvalid <= 1'b0;
      @(posedge vif.rst_n);
      forever begin
        axis_item beat;
        seq_item_port.get_next_item(req);
        beat = pre_drive(req);
        if (beat != null) begin
          repeat (beat.idle) @(posedge vif.clk);
          @(posedge vif.clk);
          vif.tdata  <= beat.data;
          vif.tlast  <= beat.last;
          vif.tvalid <= 1'b1;
          do @(posedge vif.clk); while (!vif.tready);   // the beat moves on TVALID && TREADY
          vif.tvalid <= 1'b0;
        end
        seq_item_port.item_done();
      end
    endtask

    So idle == 0 means "no extra idle clocks" in the driver's own terms, and the smallest gap the bus can ever show is one clock. A covergroup sampling idle in the driver would mark back-to-back as covered on roughly 60% of beats. dma_coverage, sampling observed beat times, reports gap=0 as a hole in every test. Coverage did its real job here: it exposed a driver limitation that no checker could, because every beat that did arrive was correct. The closure lesson shows the driver change that closes it.

    • A field the monitor fills is observation; a field only the driver reads is intent. axis_item carries both kinds, so check which one a covergroup samples.

    • Passive reuse breaks driver-side coverage. At subsystem or chip level an agent goes passive and its driver disappears; coverage on monitors keeps working.

    Interview

    "Why should coverage sample the monitor and not the driver or the sequence?"

    A weak answer: "Because monitors are passive, and that's the UVM methodology." True, but it gives no reason an interviewer can probe.

    A strong answer gives the failure: "Driver and sequence fields are intent. The DUT or the driver itself can turn intent into something else: a failed register write, a dropped beat, a gap the driver adds on its own. On our DMA bench, the item's idle knob said back-to-back beats happened 60% of the time; the monitor showed they never did, because the driver always dropped TVALID between beats. Monitor-side coverage also survives passive reuse at chip level, where the driver is gone. So I subscribe a coverage component to the agents' analysis ports, sample with arguments at the moment a fact becomes true, and give each covergroup instance a hierarchical name."

    Expect the follow-ups: Is stimulus-side coverage ever useful? (yes, to check that a generator can produce a case at all, but label it stimulus coverage, never DUT sign-off) and How do you cover something the monitor does not publish, such as a stall? (a per-cycle covergroup or cover property in the interface, or a monitor that publishes observed stall counts).

    Check yourself The two-DMA subsystem env reuses dma_env twice. If each copy had a dma_coverage without the set_inst_name calls, what would the coverage report show?

    Two instances of each covergroup under the same name, so dma0's holes and dma1's holes could not be told apart, and a merge across runs could not line instances up reliably. Naming each instance from get_full_name() gives ...dma0.cov.job_cg and ...dma1.cov.job_cg.

    Check yourself Why does write_cov_apb return at once when t.slverr is set?

    A transfer that ended with PSLVERR did not change any register, so counting it could cover a job or a clear that never happened. On this DMA, PSLVERR comes only from unmapped addresses, so the guard matters most when a later test injects bus errors.

    Check yourself stream_cg reports gap=4+ as hit. Does that prove the DMA was tested under backpressure?

    No. In dma_check_test the output's TREADY is tied high, so nothing ever stalls the DMA. The long gaps come from the driver's random idle clocks on the input. Because the gap counts every clock without a transfer, it cannot tell an idle source from a stalled one. A stall needs coverage of TVALID high with TREADY low.

    Next: the component samples the right traffic. Bins, Illegal Bins and Ignore Bins decides which values count, which are ignored, and which should never happen at all.

    Related topics

    • Up: Coverage & Closure

    • Several analysis inputs

    • The DMA bench: full source

    • SystemVerilog: sampling strategies

    Previous

    138 / 280

    Next: Bins, Illegal Bins and Ignore Bins

    Up next

    Up nextBins, Illegal Bins and Ignore Bins

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Crosses and Plan Traceability
    • Coverage Options That Change the Numbers
    • SVA cover property vs Covergroups