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
    • Testbench Anatomy & Layering
    • Stimulus: Generators, Transactions & Drivers
    • Monitors, Scoreboards & Reference Models
      • Monitor Design
      • Scoreboard Architecture
      • Compare and End-of-Test
      • Reference Models and Coverage
    • Self-Checking Tests & Regression
    • From Hand-Built TB to UVM
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Monitors, Scoreboards & Reference Models
  1. Topics
  2. SystemVerilog
  3. Testbench Architecture in SystemVerilog
  4. Monitors, Scoreboards & Reference Models
  5. Reference Models and Coverage

Part 6 · Testbench Architecture · Intermediate

Reference Models and Coverage

Predict outputs with a small model and sample coverage from observed transactions only.

On this page

    How detailed should the model be

    A reference model answers one question: given these inputs, what must the DUT output. The right level of detail is a design decision with a clear default.

    Level

    Cost

    Checks

    Risk

    Transaction-level functional

    Low

    Data integrity, ordering, flags

    Misses timing bugs.

    Cycle-approximate

    Medium

    Adds latency windows, throughput bounds

    Some timing escapes.

    Cycle-accurate

    Very high

    Exact per-cycle pin behavior

    Becomes a second RTL; clones spec bugs.

    Start transaction-level. Timing belongs in assertions beside the model, not inside it. Cycle accuracy is justified only when the spec itself is cycle-exact.

    A transaction-level model

    The next listing is classes with no top module, so this course's checks do not run it. The model consumes the input-monitor stream and its predict stays a pure function of one transaction.

    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
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    // not-run: classes only, no top module
    typedef enum logic [1:0] { OP_ADD, OP_SUB, OP_AND, OP_XOR } op_e;
    
    class alu_txn;
      op_e         op;
      logic [31:0] a, b;
      logic [31:0] result;   // filled by DUT (actual) or model (expected)
      logic        cout;      // ADD carry-out, SUB borrow (unsigned, spec 3.2.1)
    
      function string convert2string();
        return $sformatf("op=%s a=0x%08h b=0x%08h result=0x%08h cout=%0b",
                         op.name(), a, b, result, cout);
      endfunction
    endclass
    
    class alu_ref_model;
      mailbox #(alu_txn) mbx_in;    // from input monitor
      mailbox #(alu_txn) mbx_exp;   // to scoreboard
      bit stop;
    
      function new(mailbox #(alu_txn) mbx_in, mailbox #(alu_txn) mbx_exp);
        this.mbx_in  = mbx_in;
        this.mbx_exp = mbx_exp;
      endfunction
    
      // Spec 3.2: cout is UNSIGNED carry (ADD) or borrow (SUB: 1 when a < b).
      // Signed overflow is a different bit: carry-in XOR carry-out of the MSB.
      function void predict(alu_txn t);
        logic [32:0] wide;
        case (t.op)
          OP_ADD: begin
            wide     = {1'b0, t.a} + {1'b0, t.b};
            t.result = wide[31:0];
            t.cout   = wide[32];
          end
          OP_SUB: begin
            wide     = {1'b0, t.a} - {1'b0, t.b};
            t.result = wide[31:0];
            t.cout   = wide[32];   // borrow, not signed overflow
          end
          OP_AND: begin t.result = t.a & t.b; t.cout = 1'b0; end
          OP_XOR: begin t.result = t.a ^ t.b; t.cout = 1'b0; end
        endcase
      endfunction
    
      task run();
        while (!stop) begin
          alu_txn t;
          mbx_in.get(t);
          predict(t);
          mbx_exp.put(t);
        end
      endtask
    endclass

    Three habits keep the model honest. Tag each branch with its spec section, feed it from the input monitor rather than driver intent, and reset its state on mid-test reset.

    When model and RTL disagree, check both against the spec and trust neither side by default. For algorithm-heavy DUTs, import the existing C model through DPI-C instead of re-implementing it.

    Coverage from observed transactions

    Coverage must sample what the DUT did, never what the driver intended. The collector subscribes to the monitor stream like any other consumer.

    The next listing is not run in this course's checks: Verilator 5.048 ignores covergroup bins and Icarus 13 cannot parse a covergroup. Here bus_txn is the scoreboard lesson's transaction plus an op field of the model's op_e type. Arguments on sample remove the copy-then-sample dance, and the reset gate keeps reset-phase traffic out of the bins.

    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
    // not-run: covergroups: Verilator 5.048 ignores bins (COVERIGN) and Icarus 13 cannot parse them; bus_txn (with an op_e op field) is defined elsewhere
    // bus_txn here = the scoreboard lesson's transaction plus: op_e op;
    class bus_coverage;
      mailbox #(bus_txn) mbx_cov;
      bit          in_reset;
      bit          stop;
      int unsigned sample_count;
    
      covergroup cg with function sample(op_e op, int unsigned len,
                                         logic [1:0] resp);
        cp_op : coverpoint op {
          bins add_op     = {OP_ADD};
          bins sub_op     = {OP_SUB};
          bins logical[]  = {OP_AND, OP_XOR};
        }
        cp_len : coverpoint len {
          bins one_beat = {1};
          bins few      = {[2:4]};    // "small" is a reserved word, not a bin name
          bins many     = {[5:16]};
        }
        cp_resp : coverpoint resp {
          bins okay  = {0};
          bins error = {[1:3]};
        }
        x_op_len : cross cp_op, cp_len;
      endgroup
    
      function new(mailbox #(bus_txn) mbx_cov);
        this.mbx_cov = mbx_cov;
        cg = new();   // a covergroup handle starts null; construct it here
      endfunction
    
      task run();
        while (!stop) begin
          bus_txn tr;
          mbx_cov.get(tr);
          if (in_reset) continue;   // reset-phase traffic pollutes closure
          cg.sample(tr.op, tr.len, tr.resp);
          sample_count++;
        end
      endtask
    endclass

    Placement

    Verdict

    Why

    Separate collector on the monitor stream

    Default

    Bins vary per project; coverage stays optional.

    Inside the monitor

    Rare exception

    Only for protocol-universal bins every user wants.

    In the driver or generator

    Never

    That samples intent, and intent can lie.

    Common mistakes

    Common pitfall

    A cycle-accurate model by default doubles maintenance and clones spec misreadings. Model state that survives a mid-test reset fails every later compare in a cascade.

    Interview

    Likely questions: “How accurate should a reference model be?” (transaction-level by default) and “Where do you sample coverage and why?” (monitor stream, once per transaction, gated in reset).

    Check yourself The model and the RTL disagree on a SUB result. Which side do you fix?

    Neither yet. Recompute the case from the spec by hand first: the model encodes one reading and the RTL another, and often the model or the spec text, not the RTL, is what is wrong.

    Check yourself Coverage sampled in the driver reports 100 percent but the DUT dropped half the traffic. What happened?

    Coverage measured intent, not behavior. Sample from the monitor stream so dropped or transformed transactions never credit bins they never reached.

    Testbench

    An ALU, an independent model function, and manual coverage bins in plain Verilog.

    alu.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    module alu (
      input  logic [1:0]  op,          // 0 ADD, 1 SUB, 2 AND, 3 XOR
      input  logic [31:0] a, b,
      output logic [31:0] result,
      output logic        cout         // unsigned carry (ADD) or borrow (SUB)
    );
      timeunit 1ns; timeprecision 1ps;
      wire [32:0] sum  = {1'b0, a} + {1'b0, b};
      wire [32:0] diff = {1'b0, a} - {1'b0, b};
      assign result = (op == 2'd0) ? sum[31:0] :
                      (op == 2'd1) ? diff[31:0] :
                      (op == 2'd2) ? (a & b) : (a ^ b);
      assign cout   = (op == 2'd0) ? sum[32] :
                      (op == 2'd1) ? diff[32] : 1'b0;
    endmodule
    

    Related topics

    • Covergroup basics

    • DPI-C

    Previous

    178 / 259

    Next: Self-Checking Tests & Regression

    Up next

    Up nextSelf-Checking Tests & Regression

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Monitor Design
    • Scoreboard Architecture