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
    • Capstone: Out-of-Order Memory Controller
      • Spec and Plan
      • Environment
      • Checking
      • Coverage and Closure
      • Bug Hunt 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. Bug Hunt and Sign-Off

Part 9 · Principal-Level Verification · Advanced

Bug Hunt and Sign-Off

The complete self-checking bench, six planted bugs, which checker caught each one and at which cycle, the escapes when a piece is removed, and the sign-off review.

On this page

    The six planted bugs

    Each bug is a few lines in the controller, selected by the BUG parameter, and each breaks one rule from the spec.

    BUG

    Bug

    What the RTL does

    Rule

    1

    ID swap

    A response from table entry 7 carries entry 6's ID

    R1, R2

    2

    Strobe drop

    Port 1 writes ignore byte lane 2

    R3

    3

    Lost response after reset

    Per-ID completion counters are not reset, so post-reset reads on an ID wait for a completion count that no longer matches

    R5, R1

    4

    Starvation

    Port 0 always wins a contention

    R6

    5

    Duplicate response

    If entry 3 is released in the same cycle a new read is allocated, the release is lost and the read is answered again

    R1

    6

    Wrong order within an ID

    Port 1 entries ignore their ticket and leave as soon as their latency expires

    R1

    Bugs 1 and 5 only appear in a corner (a response from entry 7; a release of entry 3 in the same cycle as any read allocation). Bug 3 only appears after a reset with work in flight. Bug 4 does not hang anything. These are the shapes of real escapes.

    The complete bench

    One block: policy items, abstract BFMs, the controller, the checkers, the coverage mirror, the environment and the test. It is self-checking: it fails on any checker failure, on any coverage row below goal, and if the mid-traffic reset did not happen. It stops after 50 checker failures, since the first ones carry the information. To plant a bug, change parameter int BUG = 0 in ooo_memctl. +seed=N, +items=N and +empty_reads=N override the defaults of 11, 300 and 5.

    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
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    // ===================== 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
    
    // ===================== Checkers (from Checking Out-of-Order Traffic) =====================
    class id_scoreboard #(type T = logic [31:0]);
      string name;
      protected T exp_q[int][$];
      int n_added, n_matched, n_errors;
      function new(string name); this.name = name; endfunction
      virtual function void add_expected(int id, T item);
        exp_q[id].push_back(item);
        n_added++;
      endfunction
      virtual function void check_actual(int id, T item);
        T exp;
        if (!exp_q.exists(id) || exp_q[id].size() == 0) begin
          n_errors++;
          $display("%s: UNEXPECTED response for ID %0d with nothing outstanding (got %h)", name, id, item);
          return;
        end
        exp = exp_q[id].pop_front();
        if (exp_q[id].size() == 0) exp_q.delete(id);
        if (item !== exp) begin
          n_errors++;
          $display("%s: MISMATCH ID %0d expected %h got %h", name, id, exp, item);
        end else n_matched++;
      endfunction
      function int outstanding();
        int n = 0;
        foreach (exp_q[id]) n += exp_q[id].size();
        return n;
      endfunction
      function int outstanding_on(int id);
        return exp_q.exists(id) ? exp_q[id].size() : 0;
      endfunction
      virtual function void flush();               // reset: outstanding work is dropped by spec
        exp_q.delete();
      endfunction
    endclass
    
    class drain_scoreboard #(type T = logic [31:0]) extends id_scoreboard #(T);
      protected longint issued_at[int][$];
      longint last_progress;
      int     max_lat;
      int     leaked[$], stuck[$];
      function new(string name, int max_lat);
        super.new(name);
        this.max_lat = max_lat;
      endfunction
      function void add_at(longint now, int id, T item);
        add_expected(id, item);
        issued_at[id].push_back(now);
      endfunction
      function void check_at(longint now, int id, T item);
        check_actual(id, item);
        if (issued_at.exists(id)) begin
          void'(issued_at[id].pop_front());
          if (issued_at[id].size() == 0) issued_at.delete(id);
        end
        last_progress = now;
      endfunction
      // Returns how many items passed max_lat on this cycle (each counted once).
      function int watchdog(longint now);
        int n = 0;
        foreach (issued_at[id])
          if (now - issued_at[id][0] == max_lat) begin
            n++;
            $display("%s: ID %0d waited %0d cycles (issued at %0d)", name, id, max_lat, issued_at[id][0]);
          end
        return n;
      endfunction
      virtual function void flush();
        super.flush();
        issued_at.delete();
      endfunction
      function void classify(longint now);
        foreach (issued_at[id])
          foreach (issued_at[id][k])
            if (last_progress - issued_at[id][k] > max_lat) leaked.push_back(id);
            else stuck.push_back(id);
        foreach (leaked[i]) $display("%s: LEAK ID %0d never answered", name, leaked[i]);
        if (stuck.size() != 0) $display("%s: STUCK IDs %p, no response for %0d cycles", name, stuck, now - last_progress);
      endfunction
    endclass
    
    class mem_model;
      protected bit [7:0] bytes[bit [31:0]];
      function void write(bit [31:0] addr, bit [31:0] data, bit [3:0] strb);
        for (int b = 0; b < 4; b++) if (strb[b]) bytes[addr + b] = data[8*b +: 8];
      endfunction
      function bit [31:0] read(bit [31:0] addr);
        bit [31:0] w;
        for (int b = 0; b < 4; b++) w[8*b +: 8] = bytes.exists(addr + b) ? bytes[addr + b] : 8'h00;
        return w;
      endfunction
    endclass
    
    // ===================== Coverage mirror (covergroups are ignored by Verilator) =====================
    class cap_cov;
      int depth_bins [4];      // read found 0 | 1-3 | 4-7 | 8+ reads outstanding (8+ = took the last free entry)
      int entry7;              // white-box: read allocated table entry 7 (fed from the DUT's free_idx)
      int same_id_deep;        // read found >= 2 reads outstanding on its own ID
      int overtakes;           // response overtook an older read on another ID
      int strobe_lane [2][4];  // partial write per port and byte lane
      int raw_close;           // read of a word written <= 3 cycles earlier
      int long_stall;          // response held >= 3 cycles by back-pressure
      int contention [2];      // both ports valid, port p accepted
      int reset_busy;          // reset with >= 4 reads outstanding
      int reuse_after_reset;   // read on an ID that had reads outstanding at reset
      protected longint acc_no[int][$];
      protected longint next_no;
      protected longint wr_cycle[int];
      protected bit     was_busy_at_reset[int];
    
      function void on_read(longint now, int pid, int depth, int depth_id, int addr);
        depth_bins[depth == 0 ? 0 : depth <= 3 ? 1 : depth <= 7 ? 2 : 3]++;
        if (depth_id >= 2) same_id_deep++;
        if (wr_cycle.exists(addr) && now - wr_cycle[addr] <= 3) raw_close++;
        if (was_busy_at_reset.exists(pid)) begin reuse_after_reset++; was_busy_at_reset.delete(pid); end
        acc_no[pid].push_back(next_no++);
      endfunction
      function void on_write(longint now, int port, int addr, bit [3:0] strb);
        wr_cycle[addr] = now;
        if (strb != 4'hF) for (int b = 0; b < 4; b++) if (strb[b]) strobe_lane[port][b]++;
      endfunction
      function void on_rsp(int pid);
        longint mine;
        if (!acc_no.exists(pid)) return;
        mine = acc_no[pid].pop_front();
        if (acc_no[pid].size() == 0) acc_no.delete(pid);
        foreach (acc_no[o]) if (acc_no[o][0] < mine) begin overtakes++; break; end
      endfunction
      function void on_reset(int outstanding);
        if (outstanding >= 4) reset_busy++;
        foreach (acc_no[p]) was_busy_at_reset[p] = 1;
        acc_no.delete();
      endfunction
      // Each plan row with its goal; returns the number of rows below goal.
      function int report();
        int holes = 0;
        holes += row("CV1 depth 0",            depth_bins[0], 5);
        holes += row("CV1 depth 1-3",          depth_bins[1], 10);
        holes += row("CV1 depth 4-7",          depth_bins[2], 10);
        holes += row("CV1 depth 8+ (full)",    depth_bins[3], 3);
        holes += row("CV1b entry 7 allocated", entry7, 3);
        holes += row("CV2 same-ID depth >= 2", same_id_deep, 10);
        holes += row("CV3 cross-ID overtake",  overtakes, 10);
        for (int p = 0; p < 2; p++) for (int b = 0; b < 4; b++)
          holes += row($sformatf("CV4 partial strobe p%0d lane %0d", p, b), strobe_lane[p][b], 5);
        holes += row("CV5 read-after-write <= 3", raw_close, 10);
        holes += row("CV6 stall >= 3 cycles",  long_stall, 5);
        holes += row("CV7 contention p0 wins", contention[0], 5);
        holes += row("CV7 contention p1 wins", contention[1], 5);
        holes += row("CV8 reset with >= 4 out", reset_busy, 1);
        holes += row("CV9 ID reuse after reset", reuse_after_reset, 1);
        return holes;
      endfunction
      protected function int row(string name, int hits, int goal);
        $display("  %-30s %5d / %0d%s", name, hits, goal, hits < goal ? "  <- HOLE" : "");
        return hits < goal;
      endfunction
    endclass
    
    // ===================== Environment =====================
    class cap_env;
      req_bfm  req [2];
      rsp_bfm  rsp;
      drain_scoreboard #(logic [31:0]) sb;
      mem_model mm;
      cap_cov   cov;
      cap_item  item [2];
      longint   cycle;
      int       ready_pct = 70, gap_pct = 20, resets, reads, writes;
      bit       in_rst;
      int       fails[string];
      longint   first_fail[string];
    
      function new(req_bfm r0, req_bfm r1, rsp_bfm rs);
        req[0] = r0; req[1] = r1; rsp = rs;
        sb = new("sb", 200);
        mm = new(); cov = new();
        foreach (item[p]) item[p] = new();      // seeded from the calling thread
      endfunction
    
      // Every checker reports through here, so the first failure is timestamped.
      function void flag(string chk, string msg);
        int total = 0;
        if (!fails.exists(chk)) begin
          fails[chk] = 0; first_fail[chk] = cycle;
          $display("[cycle %0d] FIRST FAIL from %s: %s", cycle, chk, msg);
        end
        fails[chk]++;
        foreach (fails[c]) total += fails[c];
        if (total >= 50) begin                     // stop early: the first failures matter
          summary();
          $fatal(1, "FAIL: stopped after %0d checker failures", total);
        end
      endfunction
    
      function void summary();
        foreach (fails[c]) $display("checker %-12s %4d fails, first at cycle %0d", c, fails[c], first_fail[c]);
      endfunction
    
      task monitor();
        bit [2:0]  pid;
        bit [31:0] data;
        int        e, stall_run;
        mc_item    t;
        forever begin
          rsp.tick();
          cycle++;
          if (rsp.in_reset()) begin
            if (!in_rst && reads + writes != 0) begin   // a reset after traffic started
              resets++;
              $display("[cycle %0d] reset with %0d reads outstanding", cycle, sb.outstanding());
              cov.on_reset(sb.outstanding());
              sb.flush();                          // spec: reset drops outstanding reads
            end
            in_rst = 1; stall_run = 0;
            continue;
          end
          in_rst = 0;
          if (rsp.sample(pid, data)) begin         // responses belong to earlier edges
            e = sb.n_errors;
            sb.check_at(cycle, pid, data);
            if (sb.n_errors != e) flag("scoreboard", $sformatf("pid %0d data %h", pid, data));
            cov.on_rsp(pid);
          end
          stall_run = rsp.stalled() ? stall_run + 1 : 0;
          if (stall_run == 3) cov.long_stall++;
          if (req[0].waiting() || req[1].waiting()) begin
            t = req[0].sample(); if (t != null && req[1].waiting()) cov.contention[0]++;
            t = req[1].sample(); if (t != null && req[0].waiting()) cov.contention[1]++;
          end
          for (int p = 0; p < 2; p++) begin
            t = req[p].sample();
            if (t == null) continue;
            if (t.write) begin
              writes++;
              mm.write(32'(t.addr) * 4, t.wdata, t.wstrb);     // ordering point = acceptance
              cov.on_write(cycle, p, t.addr, t.wstrb);
            end else begin
              reads++;
              cov.on_read(cycle, {p[0], t.id}, sb.outstanding(), sb.outstanding_on({p[0], t.id}), t.addr);
              sb.add_at(cycle, {p[0], t.id}, mm.read(32'(t.addr) * 4));  // snapshot at acceptance
            end
          end
          if (sb.watchdog(cycle) != 0) flag("watchdog", "a read passed the latency limit");
        end
      endtask
    
      // Response back-pressure, bounded so legal latency stays bounded.
      task sink();
        int run = 0;
        forever begin
          bit r = ($urandom_range(0, 99) < ready_pct) || run >= 6;
          run = r ? 0 : run + 1;
          rsp.drive_ready(r);
        end
      endtask
    
      task port_traffic(int p, int n);
        bit ok;
        int waited;
        for (int i = 0; i < n; i++) begin
          if (!item[p].randomize()) flag("stimulus", "randomize failed");
          req[p].drive(item[p], ok, waited);
          if (!ok) begin
            if (rsp.in_reset()) break;               // reset dropped this request: stop the phase
            flag("driver", $sformatf("port %0d not accepted for %0d cycles", p, waited));
            break;
          end
          if ($urandom_range(0, 99) < gap_pct) req[p].idle();
        end
        req[p].idle();
      endtask
    
      task traffic(int n);
        fork
          port_traffic(0, n);
          port_traffic(1, n);
        join
      endtask
    
      // Directed: wait for the table to empty, then send one read; n times (CV1 depth 0).
      task empty_table_reads(int n);
        foreach (item[p]) item[p].wr_pct = 0;
        for (int i = 0; i < n; i++) begin
          int waited = 0;
          while (sb.outstanding() != 0 && waited < 200) begin rsp.tick(); waited++; end
          port_traffic(i % 2, 1);
        end
      endtask
    
      task drain(int limit);
        int waited = 0;
        while (sb.outstanding() != 0 && waited < limit) begin rsp.tick(); waited++; end
        sb.classify(cycle);
        if (sb.leaked.size() != 0 || sb.stuck.size() != 0) flag("drain", "reads still outstanding at end of test");
      endtask
    endclass
    

    With BUG = 0 the bench passes in about 2 seconds of wall time on Verilator 5.048: 735 reads and 563 writes over 1,490 cycles, a reset at cycle 820 with 8 reads outstanding, and every coverage row at goal (for example 58 reads took the last free entry, entry 7 was allocated 30 times, 189 responses overtook an older read, 3 IDs were reused after the reset). Seeds 1 to 30 and 99 also pass.

    Which checker caught each bug, and when

    Each bug was run by editing the default parameter. The cycle is the monitor's cycle count from the start of simulation.

    BUG

    First checker

    Cycle

    First message

    Also fired

    1 ID swap

    scoreboard and a_no_orphan

    142

    UNEXPECTED response for ID 2 with nothing outstanding

    Mismatches on ID 0 after it

    2 strobe drop

    scoreboard (expected data from the memory model)

    41

    MISMATCH ID 6 expected 650c7b25 got 651f7b25

    Only the scoreboard

    3 lost after reset

    watchdog

    1024

    ID 1 waited 200 cycles (issued at 824)

    driver at 1238, drain at 4644

    4 starvation

    a_fair

    5

    port 0 accepted twice while port 1 waited

    Only a_fair

    5 duplicate

    scoreboard, then a_no_orphan

    41, 42

    MISMATCH on ID 6, then UNEXPECTED on ID 6

    -

    6 order within ID

    scoreboard

    31

    MISMATCH ID 7 (two values swapped)

    Only the scoreboard

    Three things in the table are worth a reviewer's attention.

    • Bug 1 needed a nearly full table. The ID mux bug is in entry 7, and the table hands out the lowest free entry, so entry 7 is allocated only when entries 0 to 6 are busy (7 or more reads outstanding). CV1's 8+ bin is not the evidence: in the passing run entry 7 was allocated 30 times, 20 of them by reads that found 7 outstanding (the 4-7 bin) and 10 by reads that found 8, while only 10 of the 58 reads in the 8+ bin took entry 7. The white-box row CV1b counts entry 7 directly; a test with 0 there could not have found this bug.

    • Bug 3 was found 204 cycles after the reset, by the watchdog, not by a data compare. Liveness checks matter: without the watchdog and the bounded drain, a lost response is a silent timeout.

    • Bugs 2, 4 and 6 have exactly one checker each. Removing that one checker lets the bug through. The plan should say so, and those checkers deserve the most review.

    Escapes when one piece is missing

    Mutation of the bench, not the design, shows what each piece is worth. Two runs from the previous lesson make the point: with the mid-traffic reset removed, BUG = 3 fires no checker; coverage rows CV8 and CV9 report holes and the bench's "reset did not happen" check fails. With the fairness assertions removed, BUG = 4 fires nothing and only CV7 (port 1 wins) reports a hole. The bench still fails in both cases because unmet coverage goals and the test's own sanity checks fail it. That is the reason to make coverage goals part of the pass criterion rather than a report someone reads later.

    Common pitfall

    A regression that passes because it never reached the bug. A bench whose pass line ignores coverage would have passed both experiments above. Fail the test when a plan row is below goal, or at least fail the regression's sign-off report.

    Sign-off review

    A sign-off review asks for evidence, not assurances. For this capstone:

    Question

    Evidence here

    Is every rule owned by a checker?

    Checker table in Checking: R1-R6 each have at least one owner

    Do the checkers work?

    Checker unit test (7 cases) and six planted bugs, each caught, with cycle numbers

    Did the stimulus reach the risky states?

    Every coverage row at goal in the full run on 31 seeds; CV1 8+, CV1b, CV2, CV3, CV8 and CV9 are the high-risk rows

    Are the holes understood?

    CV1 depth 0 was structural; the directed phase D closes it on every seed; the per-test vs regression meaning is recorded per row

    Can the bench hang or pass vacuously?

    Bounded drivers, bounded drain, a global timeout, activity gates (CV2, CV3), and a check that the reset happened

    Are tool limits recorded?

    Covergroups mirrored; cover properties need --coverage-user; local-variable assertion not run on Verilator 5.048; clocking-block drives (a Verilator 5.048 workaround, and good style)

    Is it reproducible?

    Fixed seed 11 by default; +seed and +items for other runs; all results in this topic regenerated from the code shown

    What this capstone does not prove is also part of sign-off: no formal proof of the ordering rule, no coverage of an ID reused while a same-ID response is stalled (no row defines it), CV10 and CV11 not enforced on Verilator (they exist only as cover properties), and one design configuration. A principal engineer lists these as known gaps with an owner, rather than leaving them for someone to discover.

    Interview

    Likely question: “How do you decide a block is ready to sign off?” Answer: every plan row is closed or waived with a reason; every checker has been shown to fail on a planted bug; coverage goals are part of the pass criterion; the bench cannot hang or pass on no traffic; and the known gaps are written down with owners. Give one example of a hole that turned out to be a bug.

    Check yourself BUG = 3 is caught by the watchdog 204 cycles after the reset. What would catch it sooner, and what would it cost?

    A tighter watchdog limit, or the ticket assertion with a smaller MAX_LAT, would report it closer to the first stuck read. The cost is false failures when back-pressure legally delays a response, so the limit must stay above the worst legal latency (up to 69 cycles measured here).

    Check yourself Only the scoreboard catches BUG = 6. Name a second, independent checker you could add.

    The ticket assertion from the Checking lesson on a simulator that supports local variables, or an auxiliary-code assertion that records, per ID, the accept order of each read's sequence number and checks that responses retire them in order. Either would catch the swap without the scoreboard.

    Related topics

    • Previous: Coverage and Closure

    • Capstone topic overview

    • Sign-Off Metrics and Review

    • Next topic: Escaped Bug Analysis

    Previous

    254 / 259

    Next: Principal Judgment

    Up next

    Up nextPrincipal Judgment

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Spec and Plan
    • Environment
    • Checking