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 Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
    • Reusable Testbench Architecture
    • Checking Out-of-Order Traffic
    • Assertion Depth
    • Coverage Modelling for Real Features
      • Transaction and Temporal Coverage
      • Coverage Filters
      • Assertion-Backed Coverage
      • Closure at Scale
    • Capstone: Out-of-Order Memory Controller
    • Principal Judgment
Loading lesson

‹Coverage Modelling for Real Features
  1. Topics
  2. SystemVerilog
  3. Principal-Level Verification
  4. Coverage Modelling for Real Features
  5. Transaction and Temporal Coverage

Part 9 · Principal-Level Verification · Advanced

Transaction and Temporal Coverage

Cover transactions and derived state, such as outstanding depth and ID reuse distance, instead of raw signals.

On this page

    Signals are the wrong thing to cover

    A covergroup clocked on posedge clk with a coverpoint on req_id counts idle cycles, counts the same request many times while it waits, and fills in the first test. It answers "did these values appear on the pins", which is rarely the question.

    The questions that find bugs in an out-of-order design are about state the design keeps internally:

    • Outstanding depth. How many requests were in flight when a new one arrived? Table-full and one-below-full are where allocation bugs live.

    • ID reuse distance. How many requests passed between two uses of the same ID? Reusing an ID right after its response tests that the old entry was fully released.

    • Response order. Did a later request ever complete first? A design that happens to answer in order never tests the reordering logic.

    • Latency range. Did responses arrive at the minimum and the maximum latency the spec allows?

    None of these is a signal. Each is computed by a monitor from a stream of handshakes, which is why the model belongs on transactions.

    Sample when the transaction happens

    Sample once per event that matters, with the values that describe it. The with function sample(...) form makes the arguments explicit, so the covergroup cannot read a variable that has already moved on. Sampling strategies covers the basic forms; here the choice of event is the design decision.

    Sampling choice

    Effect

    Every clock

    Idle cycles and waiting cycles count as hits. Bin counts measure time, not traffic.

    On request accept

    One sample per request. Depth here means "the table the request found".

    On response

    Good for latency and order bins. Depth here means "what was still in flight when this one finished".

    Depth is sampled before the accepted request is added. Sampling after the increment shifts every bin by one and hides the empty-table case. The bench below catches exactly that mistake.

    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
    // not-run: covergroups are parsed but ignored by Verilator 5.048 (COVERIGN warnings, get_coverage() returns 0) and rejected by Icarus 13; needs a commercial simulator
    class ooo_cov;
      // One sample per accepted request; the monitor computes the arguments.
      covergroup cg with function sample(int unsigned depth, int unsigned reuse_d, bit first);
        option.per_instance = 1;
        cp_depth: coverpoint depth {
          bins empty      = {0};
          bins one        = {1};
          bins two        = {2};
          bins all_but_me = {3};   // 4 IDs: 3 is the deepest table a request can find
        }
        cp_reuse: coverpoint reuse_d iff (!first) {
          bins b2b  = {1};         // same ID as the previous request
          bins near = {[2:3]};
          bins far  = {[4:$]};
        }
        x_depth_reuse: cross cp_depth, cp_reuse;
      endgroup
    
      function new();
        cg = new();
      endfunction
    endclass

    The iff (!first) guard keeps the first use of each ID out of the reuse bins: it has no distance, and forcing it into a bin would inflate the "far" count. In UVM the same covergroup sits in a subscriber whose write() computes the derived values and calls cg.sample().

    A runnable model: derived depth and reuse distance

    The DUT is a small responder: one outstanding request per ID, four IDs, latency 2*id+1 cycles, so higher IDs answer later and responses come back out of order. It exports dbg_outstanding so the monitor's derived depth can be cross-checked against the design's own count.

    Verilator 5.048 parses the covergroup above but ignores it (it prints COVERIGN warnings and get_coverage() returns 0), and Icarus 13 rejects it. The bench therefore uses a class that mirrors the bins one for one, with the same ranges, and checks its counts. It also counts samples that fall in no reuse bin, which must be 0.

    ooo_rsp.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
    45
    46
    47
    48
    49
    50
    51
    52
    53
    // Out-of-order responder: one outstanding request per ID, latency 2*id+1.
    module ooo_rsp (
      input  logic       clk,
      input  logic       rst_n,
      input  logic       req_valid,
      input  logic [1:0] req_id,
      output logic       req_ready,
      output logic       rsp_valid,
      output logic [1:0] rsp_id,
      output logic [2:0] dbg_outstanding
    );
      timeunit 1ns; timeprecision 1ps;
      logic [3:0] busy;
      logic [2:0] timer [4];
      logic [3:0] done;
    
      always_comb begin
        for (int i = 0; i < 4; i++) done[i] = busy[i] && timer[i] == 0;
        rsp_valid = |done;
        rsp_id    = done[0] ? 2'd0 : done[1] ? 2'd1 : done[2] ? 2'd2 : 2'd3;
        req_ready = !busy[req_id];
        dbg_outstanding = 3'(busy[0]) + 3'(busy[1]) + 3'(busy[2]) + 3'(busy[3]);
      end
    
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin
          busy <= '0;
          for (int i = 0; i < 4; i++) timer[i] <= '0;
        end else begin
          for (int i = 0; i < 4; i++) if (busy[i] && timer[i] != 0) timer[i] <= timer[i] - 3'd1;
          if (rsp_valid) busy[rsp_id] <= 1'b0;
          if (req_valid && req_ready) begin
            busy[req_id]  <= 1'b1;
            timer[req_id] <= 3'(2 * req_id + 1);
          end
        end
    endmodule
    
    // Procedural mirror of the covergroup bins (Verilator 5.048 ignores covergroups).
    class ooo_cov_mirror;
      int depth_bins [4];   // outstanding before accept: 0, 1, 2, 3
      int reuse_bins [3];   // b2b (1), near (2-3), far (4+)
      int first_use;        // first use of an ID: no distance, not sampled
      int no_bin;           // a distance that falls in no reuse bin (0 or negative)
      function void sample(int depth, int reuse_d, bit first);
        depth_bins[depth]++;
        if (first)                          first_use++;
        else if (reuse_d == 1)                 reuse_bins[0]++;
        else if (reuse_d >= 2 && reuse_d <= 3) reuse_bins[1]++;
        else if (reuse_d >= 4)                 reuse_bins[2]++;
        else                                   no_bin++;
      endfunction
    endclass
    

    Each count was worked out by hand from the ID order and the latencies, then confirmed by the run. The ten requests use IDs 0, 0, 1, 2, 3, 1, 0, 2, 3, 3. Reuse distances for the six repeated IDs are 1, 3, 5, 4, 4 and 1, so the reuse bins are 2, 1 and 3. Depth reaches 3 only once, when ID 0 arrives while IDs 1, 2 and 3 are all busy.

    Three changes make the bench fail. Moving last_req[req_id] = req_count above the distance calculation makes every distance 0, which is in no reuse bin (the covergroup's near starts at 2): the reuse bins read 0, 0, 0, all six repeated uses land in no bin, and the test fails. That is a coverage bug that would otherwise report a healthy-looking model. Forcing req_ready to 1 in the DUT lets an ID be reused while busy, and the derived depth stops matching dbg_outstanding. Incrementing the monitor count before the compare also fails on the first request.

    Common pitfall

    Coverage code has bugs too, and they are silent: a wrong bin still fills. Give each derived value a cross-check (here, the DUT's own outstanding count) and a directed test with known bin counts before trusting the numbers from random runs.

    Temporal quantities

    Reuse distance is a temporal measure expressed as a number, so a covergroup can bin it. The same works for latency (cycles from accept to response), burst spacing, and time since reset. Transition bins on a derived value cover order: bins drain = (3 => 2 => 1 => 0); on depth sampled at each response proves the table drained from its deepest point. Multi-cycle scenarios that are not a single number are easier as cover property, covered in Assertion-Backed Coverage.

    Interview

    Question: "Your ID coverage is 100% but a bug escaped where an ID was reused one cycle after its response. What was missing?" Answer: the model covered ID values, not reuse distance. Add a monitor-derived distance from response to reuse, with a bin for the minimum the design allows, and cross it with outstanding depth so reuse under a full table is also covered.

    Check yourself Why sample outstanding depth before adding the accepted request?

    The bin should describe the table the request found. Sampling after the increment shifts every value by one, so the empty-table case (depth 0) can never be hit and the full case looks reached one request early.

    Check yourself A depth bin for 4 never fills in this design. Is that a hole?

    No. With four IDs and one outstanding request per ID, the deepest table a new request can find is 3; a fourth request must wait. A bin for 4 at accept time is unreachable and belongs out of the model, with the reason written down.

    Related topics

    • Next: Coverage Filters

    • Sampling: Clocked, Event & sample()

    • Transition Bins

    Previous

    246 / 259

    Next: Coverage Filters

    Up next

    Up nextCoverage Filters

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Assertion-Backed Coverage
    • Closure at Scale