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
      • The Layered Testbench
      • The Static Harness and Environment
      • Tests as Classes
      • Complete Mini Testbench
    • Stimulus: Generators, Transactions & Drivers
    • Monitors, Scoreboards & Reference Models
    • Self-Checking Tests & Regression
    • From Hand-Built TB to UVM
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Testbench Anatomy & Layering
  1. Topics
  2. SystemVerilog
  3. Testbench Architecture in SystemVerilog
  4. Testbench Anatomy & Layering
  5. Complete Mini Testbench

Part 6 · Testbench Architecture · Intermediate

Complete Mini Testbench

A complete class-based testbench for a small register file, with every race fixed.

On this page

    The target

    The DUT is a small synchronous register file on a valid/ready port. A write stores wdata at addr, and a read returns the stored value on rdata one cycle after the handshake.

    Diagram
    MINI TB ARCHITECTURE
    
      test --builds--> env
                        |
            +-----------+--------------+------------+
            v           v              v            v
         generator --> driver       monitor --> scoreboard
            rand txns   | vif.drv_cb   | vif.mon_cb  | model:
            (mailbox)   v              v  (mailbox)  | mem[addr]
                    +--------- rf_if ---------+      |
                    |  valid ready write      |      v
                    |  addr wdata rdata       |   read == model ?
                    +------------+------------+
                                 v
                              rf_dut

    Request

    Handshake

    Visible result

    Write

    valid and ready high, write 1

    mem[addr] updates, rdata cleared to 0

    Read

    valid and ready high, write 0

    rdata shows the stored word one cycle later

    Transaction, generator, driver

    One transaction class flows through every layer. The generator randomizes it and never sees a clock, and the driver owns all timing: clocking-block drives, the ready wait, and the return to idle.

    Piece

    Role

    Key line

    rf_txn

    Unit of communication between layers

    Static id counter pins each failure to one generator decision

    generator

    Scenario layer, clock-free

    Sets done_flag before triggering done

    driver

    Command layer, owns all timing

    Drives through drv_cb, waits ready, idles one cycle

    The generator takes address-window and error-rate knobs so tests can narrow stimulus without touching plumbing. Error-rate shaping itself is detailed in the stimulus topic; here the knob is only stored and forwarded.

    Monitor, scoreboard, environment, test, harness

    The complete file below runs in this course's checks. The driver waits for one clock edge before its first drive, so the first transaction always starts on a fresh edge after reset.

    rf_if.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
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    `timescale 1ns/1ps
    interface rf_if (input logic clk, input logic rst_n);
      logic        valid, ready, write;
      logic [3:0]  addr;
      logic [31:0] wdata, rdata;
    
      clocking drv_cb @(posedge clk);
        default input #1step output #2;
        output valid, write, addr, wdata;
        input  ready, rdata;
      endclocking
    
      clocking mon_cb @(posedge clk);
        default input #1step;
        input valid, ready, write, addr, wdata, rdata;
      endclocking
    endinterface
    
    module rf_dut (
      input  logic        clk,
      input  logic        rst_n,
      input  logic        valid,
      input  logic        write,
      input  logic [3:0]  addr,
      input  logic [31:0] wdata,
      output logic        ready,
      output logic [31:0] rdata
    );
      logic [31:0] mem [0:15];
      assign ready = 1'b1;
      always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
          rdata <= 32'h0;
          for (int i = 0; i < 16; i++) mem[i] <= 32'h0;
        end
        else if (valid && ready && write) begin
          mem[addr] <= wdata;
          rdata <= 32'h0;
        end
        else if (valid && ready && !write) rdata <= mem[addr];
      end
    endmodule
    
    class rf_txn;
      rand bit        write;
      rand bit [3:0]  addr;
      rand bit [31:0] wdata;
         logic [31:0] rdata;     // filled by the monitor; 4-state keeps X visible
      static int unsigned next_id = 0;
           int unsigned id;      // debug breadcrumb
      function new();
        id = next_id++;
      endfunction
      function string sprint();
        return $sformatf("txn%0d %s addr=%h wdata=%h rdata=%h",
                         id, write ? "WR" : "RD", addr, wdata, rdata);
      endfunction
    endclass
    
    class generator;
      mailbox #(rf_txn) out;
      int unsigned n;
      int unsigned err_pct = 0;
      bit [3:0] addr_lo = 0, addr_hi = 4'hF;
      bit done_flag = 0;
      event done;
      function new(mailbox #(rf_txn) out, int unsigned n);
        this.out = out; this.n = n;
      endfunction
      function void set_err_pct(int unsigned pct);
        err_pct = (pct > 100) ? 100 : pct;
      endfunction
      task run();
        repeat (n) begin
          rf_txn t = new();
          if (!t.randomize() with { addr inside {[addr_lo:addr_hi]}; }) begin
            $display("GENERATOR: randomize failed, stopping");
            $finish;
          end
          out.put(t);           // blocks if the driver is behind
        end
        done_flag = 1;
        -> done;
      endtask
    endclass
    
    class driver;
      mailbox #(rf_txn) in;
      virtual rf_if vif;
      function new(mailbox #(rf_txn) in);
        this.in = in;
      endfunction
      task run();
        @(vif.drv_cb);             // sync to a clock edge before the first drive
        forever begin
          rf_txn t;
          in.get(t);
          @(vif.drv_cb);
          vif.drv_cb.valid <= 1;
          vif.drv_cb.write <= t.write;
          vif.drv_cb.addr  <= t.addr;
          vif.drv_cb.wdata <= t.wdata;
          do @(vif.drv_cb); while (vif.drv_cb.ready !== 1);
          vif.drv_cb.valid <= 0;   // one idle cycle between txns
        end
      endtask
    endclass
    
    class monitor;
      mailbox #(rf_txn) out;
      virtual rf_if vif;
      int unsigned rd_latency = 1;   // DUT read-data latency in cycles
      function new(mailbox #(rf_txn) out);
        this.out = out;
      endfunction
      task run();
        forever begin
          rf_txn t = new();
          @(vif.mon_cb);
          if (vif.mon_cb.valid === 1 && vif.mon_cb.ready === 1) begin
            t.write = vif.mon_cb.write;
            t.addr  = vif.mon_cb.addr;
            t.wdata = vif.mon_cb.wdata;
            if (!t.write) begin
              repeat (rd_latency) @(vif.mon_cb);
              t.rdata = vif.mon_cb.rdata;
            end
            out.put(t);
          end
        end
      endtask
    endclass
    
    class scoreboard;
      mailbox #(rf_txn) in;
      int unsigned expected_n, seen_n, bad;
      bit [31:0] mem [bit [3:0]];   // associative array is the golden model
      function new(mailbox #(rf_txn) in, int unsigned expected_n);
        this.in = in; this.expected_n = expected_n;
      endfunction
      task run();
        forever begin
          rf_txn t;
          in.get(t);
          if (t.write) mem[t.addr] = t.wdata;
          else begin
            bit [31:0] exp = mem.exists(t.addr) ? mem[t.addr] : 32'h0;
            if (t.rdata !== exp) begin
              bad++;
              $display("SCOREBOARD: %s returned %h, model holds %h",
                       t.sprint(), t.rdata, exp);
            end
          end
          seen_n++;
        end
      endtask
      task wait_done();
        wait (seen_n >= expected_n);
      endtask
      function void report();
        if (seen_n != expected_n) begin
          bad++;
          $display("SCOREBOARD: saw %0d of %0d, some responses never arrived",
                   seen_n, expected_n);
        end
        if (bad == 0) $display("SCOREBOARD: %0d transactions matched", seen_n);
        else $display("SCOREBOARD: %0d bad results in %0d seen", bad, seen_n);
      endfunction
    endclass
    
    class env;
      virtual rf_if vif;
      int unsigned num_txns = 100;
      int unsigned timeout_cycles = 50000;   // 500 us: inside the 1 ms watchdog
      generator  gen;
      driver     drv;
      monitor    mon;
      scoreboard scb;
      mailbox #(rf_txn) gen2drv;
      mailbox #(rf_txn) mon2scb;
      function void build();
        gen2drv = new(1);   // bounded: stimulus stays lazy under backpressure
        mon2scb = new();    // unbounded: the monitor never stalls
        gen = new(gen2drv, num_txns);
        drv = new(gen2drv);
        mon = new(mon2scb);
        scb = new(mon2scb, num_txns);
        drv.vif = vif;
        mon.vif = vif;
      endfunction
      function void set_err_pct(int unsigned pct);
        gen.set_err_pct(pct);
      endfunction
      task run();
        bit finished = 0;
        fork
          begin
            fork
              gen.run(); drv.run(); mon.run(); scb.run();
            join_none
            wait (gen.done_flag);
            scb.wait_done();
            finished = 1;
          end
          begin
            repeat (timeout_cycles) @(posedge vif.clk);
            if (!finished) $display("ENV: end-of-test timeout reached");
          end
        join_any
        disable fork;
      endtask
      function void report();
        scb.report();
      endfunction
    endclass
    
    class base_test;
      env e;
      virtual rf_if vif;
      virtual task run();
        e = new(); e.vif = vif; e.build(); e.run(); e.report();
      endtask
    endclass
    

    Four legacy defects are fixed in this file. The generator uses a done flag, the monitor parameterizes read-data latency instead of assuming it, the environment bounds the end-of-test wait with a timeout, and the scoreboard report detects dropped transactions by comparing seen and expected counts. The DUT resets its memory, so a read of an unwritten address returns 0, not X, and rdata is a 4-state field, so any X still reaches the !== check.

    The monitor has one dependency to know. While it waits rd_latency cycles for read data, it does not look at the handshake on those edges. That is safe here only because the driver always idles one cycle between transactions. With back-to-back or pipelined traffic, fork the data capture so the handshake loop samples every edge.

    The listing's DUT ties ready high and its driver idles between transactions. This wave uses a DUT that stalls and back-to-back requests, to show the same rules under backpressure. Watch valid, ready, and rdata. The read handshake and the read data are one slot apart.

    • Slot 0: idle, all control low and rdata 0.

    • Slot 1: driver presents the write, valid 1 and write 1, DUT not yet ready.

    • Slot 2: ready joins, so the write completes here.

    • Slot 3: driver presents the read, valid 1 and write 0, and the handshake completes while rdata still reads 0.

    • Slot 4: valid has dropped and rdata shows dead, one cycle after the read handshake.

    • Slot 5: back to idle with rdata holding dead for the monitor to sample.

    Common pitfall

    A monitor that reads the generator transactions instead of the pins checks the testbench against itself. Counting the end of test on generated transactions while checking monitored ones drifts by the in-flight count, and == instead of !== lets X values slide through as matches.

    Interview

    Practice writing this stack from memory: interface with clocking blocks, harness, six classes, and the end-of-test logic.

    • Mailbox directions and the vif handoff must come out without hesitation.

    • End of test is stimulus done plus scoreboard drained plus a drain window, with a watchdog behind it.

    Testbench

    A module-only smoke variant of the same testbench that runs on Icarus and Verilator. Tasks stand in for the class layers. There is no separate monitor: the read task samples rdata at the pins, and the model is written from the stimulus values.

    rf_dut.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    `timescale 1ns/1ps
    module rf_dut (
      input  logic        clk,
      input  logic        rst_n,
      input  logic        valid,
      input  logic        write,
      input  logic [3:0]  addr,
      input  logic [31:0] wdata,
      output logic        ready,
      output logic [31:0] rdata
    );
      logic [31:0] mem [0:15];
      assign ready = 1'b1;
      always @(posedge clk or negedge rst_n) begin
        if (!rst_n) rdata <= 32'h0;
        else if (valid && write) begin
          mem[addr] <= wdata;
          rdata <= 32'h0;
        end
        else if (valid && !write) rdata <= mem[addr];
      end
    endmodule
    
    Check yourself The scoreboard reports seen_n below expected_n. What does that mean, and which mechanism catches it first?

    Some DUT responses never arrived: the monitor saw fewer transactions than the generator produced. The environment timeout branch fires first, and scb.report() then names the shortfall (saw N of M) instead of hanging.

    Check yourself Why does the monitor wait rd_latency cycles before sampling read data?

    The DUT returns read data one cycle after the handshake, so sampling immediately would compare the previous transaction data. The latency is a parameter because it is a DUT property, not a testbench constant.

    Related topics

    • Monitors and scoreboards

    • Bridge to UVM

    Previous

    166 / 259

    Next: Stimulus: Generators, Transactions & Drivers

    Up next

    Up nextStimulus: Generators, Transactions & Drivers

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Layered Testbench
    • The Static Harness and Environment
    clkvalidreadywriterdata0dead
    A generic write then read on a valid/ready port with a stalling DUT, read data one cycle after the handshake