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

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here5
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive39
  • SystemVerilog Assertions — Interview-Critical Deep Dive41
  • Functional Coverage in SystemVerilog25
  • Testbench Architecture in SystemVerilog24
  • Advanced Language & Integration26
  • Senior & Interview Preparation23
  • Principal-Level Verification34
    • Reusable Testbench Architecture
    • Checking Out-of-Order Traffic
    • Assertion Depth
    • Coverage Modelling for Real Features
    • Capstone: Out-of-Order Memory Controller
      • Spec and Plan
      • Executable Spec: One Bench per Rule
      • Environment
      • Environment Self-Test
      • Checking
      • Testing the Checkers
      • Coverage and Closure
      • Merging Runs and Reading Holes
      • Bug Hunt: Six Bugs and the Full Bench
      • Bug Hunt Results and Sign-Off
    • Principal Judgment
Loading lesson

←Capstone: Out-of-Order Memory Controller
  1. Topics
  2. SystemVerilog
  3. Principal-Level Verification
  4. Capstone: Out-of-Order Memory Controller
  5. Environment Self-Test
On this page

    Part 9 of 9, lesson 23 of 34Part 9 of 9/Principal-Level Verification/Lesson 23 of 34Advanced

    Environment Self-Test

    A bench that proves the drivers, monitor and phases agree item by item before any checker is trusted, and the plumbing bug it catches.

    Why test the plumbing first

    A checker can only be as right as the monitor that feeds it. If the driver and the monitor disagree about what was sent, every later failure is ambiguous: is the design wrong, or the testbench? The previous lesson built the drivers, monitor, policy layers and phases; this lesson runs them against the real controller with no checkers and demands that they agree.

    Runnable example: the self-test

    Before trusting any checker, prove the plumbing. This bench runs the real item, BFMs and controller with a self-test environment in place of the checkers. It requires that every item a driver offered is seen once by the monitor, in order, that at most one request is accepted per cycle, that the write share follows each port's policy, and that every accepted read gets a response.

    mc_req_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
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    // ===================== Transactions and policy layers =====================
    class mc_item;
      rand bit        write;
      rand bit [1:0]  id;
      rand bit [3:0]  addr;           // word address: 16 words
      rand bit [31:0] wdata;
      rand bit [3:0]  wstrb;
      constraint c_legal { write -> wstrb != 4'h0; !write -> wstrb == 4'h0; }
    endclass
    
    // Compile-time policy layers (see Policy Classes): each rule written once.
    class hot_window #(type BASE = mc_item) extends BASE;
      bit [3:0] win_lo = 4'd0, win_hi = 4'd15;
      constraint c_hot { addr inside {[win_lo:win_hi]}; }
    endclass
    
    class write_mix #(type BASE = mc_item) extends BASE;
      int unsigned wr_pct = 50;
      constraint c_mix { write dist {1'b1 := wr_pct, 1'b0 := 100 - wr_pct}; }
    endclass
    
    class id_focus #(type BASE = mc_item) extends BASE;
      bit [1:0]    focus_id = 2'd0;
      int unsigned focus_pct = 0;
      rand bit     on_focus;
      constraint c_focus { on_focus dist {1'b1 := focus_pct, 1'b0 := 100 - focus_pct};
                           on_focus -> id == focus_id; }
    endclass
    
    typedef id_focus #(write_mix #(hot_window #(mc_item))) cap_item;
    
    // ===================== Abstract BFM API (see Abstract BFM) =====================
    virtual class req_bfm;
      pure virtual function int unsigned port_num();
      pure virtual task drive(mc_item it, output bit accepted, output int waited);
      pure virtual task idle();
      pure virtual function mc_item sample();     // handshake this edge, else null
      pure virtual function bit waiting();        // valid && !ready this edge
    endclass
    
    virtual class rsp_bfm;
      pure virtual task tick();                   // next rising edge
      pure virtual function bit in_reset();
      pure virtual task drive_ready(bit r);
      pure virtual function bit sample(output bit [2:0] pid, output bit [31:0] data);
      pure virtual function bit stalled();        // valid && !ready this edge
    endclass
    
    interface mc_req_if #(parameter int PORT = 0) (input logic clk, input logic rst_n);
      logic        valid = 1'b0, write = 1'b0;
      logic [1:0]  id = '0;
      logic [3:0]  addr = '0;
      logic [31:0] wdata = '0;
      logic [3:0]  wstrb = '0;
      logic        ready;                         // driven by the DUT
    
      // Drives go through a clocking block on the falling edge (see the lesson text).
      clocking drv @(negedge clk);
        output valid, write, id, addr, wdata, wstrb;
      endclocking
    
      class bfm_impl extends req_bfm;
        function int unsigned port_num(); return PORT; endfunction
        // Drive on the falling edge; the handshake happens at a rising edge.
        task drive(mc_item it, output bit accepted, output int waited);
          accepted = 0; waited = 0;
          @(drv);
          drv.valid <= 1'b1; drv.write <= it.write; drv.id <= it.id; drv.addr <= it.addr;
          drv.wdata <= it.wdata; drv.wstrb <= it.wstrb;
          forever begin
            @(posedge clk);
            if (!rst_n)    begin drv.valid <= 1'b0; return; end     // reset drops the request
            if (ready)     begin accepted = 1; return; end
            if (++waited > 400) begin drv.valid <= 1'b0; return; end // never hang the test
          end
        endtask
        task idle(); @(drv); drv.valid <= 1'b0; endtask
        function mc_item sample();
          mc_item t;
          if (!(rst_n && valid && ready)) return null;
          t = new();
          t.write = write; t.id = id; t.addr = addr; t.wdata = wdata; t.wstrb = wstrb;
          return t;
        endfunction
        function bit waiting(); return rst_n && valid && !ready; endfunction
      endclass
    
      bfm_impl bfm;
      function req_bfm get_bfm();
        if (bfm == null) bfm = new();
        return bfm;
      endfunction
    endinterface
    
    interface mc_rsp_if (input logic clk, input logic rst_n);
      logic        ready = 1'b0;                  // driven by the testbench
      logic        valid, port;
      logic [1:0]  id;
      logic [31:0] data;
    
      clocking drv @(negedge clk);
        output ready;
      endclocking
    
      class bfm_impl extends rsp_bfm;
        task tick(); @(posedge clk); endtask
        function bit in_reset(); return !rst_n; endfunction
        task drive_ready(bit r); @(drv); drv.ready <= r; endtask
        function bit sample(output bit [2:0] pid, output bit [31:0] d);
          pid = {port, id}; d = data;
          return rst_n && valid && ready;
        endfunction
        function bit stalled(); return rst_n && valid && !ready; endfunction
      endclass
    
      bfm_impl bfm;
      function rsp_bfm get_bfm();
        if (bfm == null) bfm = new();
        return bfm;
      endfunction
    endinterface
    
    
    // ===================== DUT: ID-tagged out-of-order memory controller =====================
    // Two request ports, one response port, valid/ready on all three.
    // Reads return in order per {port, id}, in any order across them.
    // Writes apply byte strobes at acceptance. Reset drops all outstanding reads;
    // memory contents survive reset. BUG selects a planted bug (0 = correct).
    module ooo_memctl #(parameter int BUG = 0) (
      input logic clk, rst_n,
      mc_req_if   p0, p1,
      mc_rsp_if   rsp
    );
      localparam int NS = 8;                      // read table entries
      logic [31:0] mem [16];
      logic        busy  [NS];
      logic [2:0]  s_pid [NS];                    // {port, id}
      logic [31:0] s_data[NS];
      logic [2:0]  s_tmr [NS];
      logic [3:0]  s_seq [NS];
      logic [3:0]  issue_cnt [8], done_cnt [8];   // per {port, id}
      logic        last_p;                        // port that won last
      logic [2:0]  rr;                            // response pick pointer
      logic [7:0]  lfsr;
    
      // Request side: one acceptance per cycle, round-robin on contention,
      // and only while the read table has a free entry.
      logic free, both, win, hs0, hs1, acc;
      logic [2:0] free_idx;
      always_comb begin
        free = 1'b0; free_idx = '0;
        for (int s = NS - 1; s >= 0; s--) if (!busy[s]) begin free = 1'b1; free_idx = 3'(s); end
        both = p0.valid && p1.valid;
        win  = (BUG == 4) ? 1'b0 : ~last_p;       // BUG 4: port 0 always wins
      end
      assign p0.ready = rst_n && free && !(both && win);
      assign p1.ready = rst_n && free && !(both && !win);
      assign hs0 = p0.valid && p0.ready;
      assign hs1 = p1.valid && p1.ready;
      assign acc = hs0 || hs1;
    
      logic        a_port, a_write;
      logic [1:0]  a_id;
      logic [3:0]  a_addr, a_wstrb;
      logic [31:0] a_wdata;
      always_comb begin
        a_port  = hs1;
        a_write = hs1 ? p1.write : p0.write;
        a_id    = hs1 ? p1.id    : p0.id;
        a_addr  = hs1 ? p1.addr  : p0.addr;
        a_wdata = hs1 ? p1.wdata : p0.wdata;
        a_wstrb = hs1 ? p1.wstrb : p0.wstrb;
      end
    
      // Response side: an entry is eligible when its latency has passed and it is
      // the oldest outstanding read of its {port, id}.
      logic       elig [NS];
      logic       found, load, keep_dup;
      logic [2:0] pick;
      always_comb begin
        for (int s = 0; s < NS; s++)
          elig[s] = busy[s] && s_tmr[s] == 0 &&
                    (s_seq[s] == done_cnt[s_pid[s]] || (BUG == 6 && s_pid[s][2]));  // BUG 6: port 1 ignores order
        found = 1'b0; pick = '0;
        for (int k = NS - 1; k >= 0; k--) begin
          automatic logic [2:0] s = rr + 3'(k);
          if (elig[s]) begin found = 1'b1; pick = s; end
        end
        load     = found && (!rsp.valid || rsp.ready);
        // BUG 5: when slot 3 is released in the same cycle a new read is allocated,
        // the release is lost and the entry is sent again later.
        keep_dup = (BUG == 5) && load && pick == 3'd3 && acc && !a_write;
      end
    
      always_ff @(posedge clk)                    // memory is not reset
        if (acc && a_write)
          for (int b = 0; b < 4; b++)
            if (a_wstrb[b] && !(BUG == 2 && a_port && b == 2))           // BUG 2: port 1 drops lane 2
              mem[a_addr][8*b +: 8] <= a_wdata[8*b +: 8];
    
      always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
          for (int s = 0; s < NS; s++) begin busy[s] <= 1'b0; s_tmr[s] <= '0; end
          for (int i = 0; i < 8; i++) begin
            issue_cnt[i] <= '0;
            if (BUG != 3) done_cnt[i] <= '0;      // BUG 3: order state survives reset
          end
          last_p <= 1'b0; rr <= '0; lfsr <= 8'hB5;
          rsp.valid <= 1'b0; rsp.port <= 1'b0; rsp.id <= '0; rsp.data <= '0;
        end else begin
          lfsr <= {lfsr[6:0], lfsr[7] ^ lfsr[5] ^ lfsr[4] ^ lfsr[3]};
          for (int s = 0; s < NS; s++) if (busy[s] && s_tmr[s] != 0) s_tmr[s] <= s_tmr[s] - 3'd1;
          if (acc) last_p <= hs1;
          if (acc && !a_write) begin
            busy[free_idx]   <= 1'b1;
            s_pid[free_idx]  <= {a_port, a_id};
            s_data[free_idx] <= mem[a_addr];       // read data is fixed at acceptance
            s_tmr[free_idx]  <= lfsr[2:0];
            s_seq[free_idx]  <= issue_cnt[{a_port, a_id}];
            issue_cnt[{a_port, a_id}] <= issue_cnt[{a_port, a_id}] + 4'd1;
          end
          if (rsp.valid && rsp.ready) rsp.valid <= 1'b0;
          if (load) begin
            rsp.valid <= 1'b1;
            rsp.port  <= s_pid[pick][2];
            rsp.id    <= (BUG == 1 && pick == 3'd7) ? s_pid[6][1:0] : s_pid[pick][1:0];  // BUG 1
            rsp.data  <= s_data[pick];
            rr        <= pick + 3'd1;
            if (!keep_dup) begin
              busy[pick] <= 1'b0;
              done_cnt[s_pid[pick]] <= done_cnt[s_pid[pick]] + 4'd1;
            end
          end
        end
      end
    endmodule
    
    
    // Environment self-test: proves the plumbing before any checker is trusted.
    class env_selftest;
      req_bfm  req [2];
      rsp_bfm  rsp;
      cap_item item [2];
      mc_item  sent [2][$];        // what each driver offered, oldest first
      int      errors, seen [2], reads_acc, rsps, ready_pct = 50, stalled, wr [2], n_acc [2];
      longint  cycle;
    
      function new(req_bfm r0, req_bfm r1, rsp_bfm rs);
        req[0] = r0; req[1] = r1; rsp = rs;
        foreach (item[p]) item[p] = new();
      endfunction
    
      function void err(string msg);
        errors++;
        if (errors <= 5) $display("[cycle %0d] %s", cycle, msg);
      endfunction
    
      // Monitor: every acceptance must match the driver's record, in order.
      task monitor();
        mc_item t, s;
        bit [2:0] pid; bit [31:0] d;
        int acc_this_cycle;
        forever begin
          rsp.tick();
          acc_this_cycle = 0;
          cycle++;
          if (rsp.sample(pid, d)) rsps++;
          if (rsp.stalled()) stalled++;
          for (int p = 0; p < 2; p++) begin
            t = req[p].sample();
            if (t == null) continue;
            acc_this_cycle++;
            seen[p]++;
            if (!t.write) reads_acc++;
            if (sent[p].size() == 0) begin err($sformatf("port %0d: monitor saw an item the driver never sent", p)); continue; end
            s = sent[p].pop_front();
            if ({t.write, t.id, t.addr, t.wdata, t.wstrb} != {s.write, s.id, s.addr, s.wdata, s.wstrb})
              err($sformatf("port %0d: monitor and driver disagree", p));
          end
          if (acc_this_cycle > 1) err("two acceptances in one cycle");
        end
      endtask
    
      task sink();
        forever rsp.drive_ready($urandom_range(0, 99) < ready_pct);
      endtask
    
      // Driver loop for one port; records each item it believes was accepted.
      task port_traffic(int p, int n, bit [3:0] lo, bit [3:0] hi);
        bit ok; int waited;
        mc_item copy;
        for (int i = 0; i < n; i++) begin
          if (!item[p].randomize()) err("randomize failed");
          if (!(item[p].addr inside {[lo:hi]})) err($sformatf("port %0d: addr %0d outside the window", p, item[p].addr));
          copy = new();
          copy.write = item[p].write; copy.id = item[p].id; copy.addr = item[p].addr;
          copy.wdata = item[p].wdata; copy.wstrb = item[p].wstrb;
          sent[p].push_back(copy);         // record before driving: the monitor may run first
          req[p].drive(item[p], ok, waited);
          if (!ok) begin void'(sent[p].pop_back()); err($sformatf("port %0d: not accepted", p)); break; end
          n_acc[p]++;
          wr[p] += item[p].write;
        end
        req[p].idle();
      endtask
    endclass
    

    The bench passes: 400 items agree, write shares are 79% and 16% against policies of 80% and 20% (within the bench's tolerance for 200 draws), and 200 response cycles were stalled.

    Mutant runs. A driver that treats valid as acceptance instead of waiting for ready fails at once with "monitor and driver disagree" on both ports. Removing the clocking blocks and driving the pins directly fails with "two acceptances in one cycle" from cycle 5 on, the Verilator issue described above.

    Recording the item after the driver returns. The driver and the monitor both wake at the same rising edge, and their order is not defined, so the monitor can see the handshake before the driver has recorded it. The self-test failed this way on its first run. The fix is to record the item before driving it and remove the record if it was not accepted.

    Likely question: “Your testbench passes but you are not sure the driver and monitor agree. How do you check?” Answer: run a self-test that compares what each driver offered with what the monitor saw, item by item and in order, and add structural checks such as one acceptance per cycle. Then break the driver on purpose and confirm the self-test fails.

    Check yourself The self-test reports write shares of 79% and 16% against policies of 80% and 20%. Why does the bench accept a band instead of the exact policy?

    Each share is a count out of about 200 random draws, so it varies around the policy. A band still fails when a policy is ignored or applied to the wrong port, which is what the test is for.

    Related topics

    • Previous: Environment

    • Next: Checking

    • Abstract BFM

    • Capstone: Out-of-Order Memory Controller (topic)

    Previous

    264 / 275

    Next: Checking

    Up next

    Up nextChecking

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Spec and Plan
    • Executable Spec: One Bench per Rule
    • Testing the Checkers