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. Executable Spec: One Bench per Rule

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

Executable Spec: One Bench per Rule

A directed bench that checks each of the six rules once, the six bugs it catches, and why a passing directed test is not yet verification.

An executable spec

Before building the environment, a directed bench checks each rule once. It is short, needs no classes, and fails with the rule's name. The interfaces here carry signals only; the environment lesson adds BFM classes to them.

Rule

Scenario in the bench

What judges it

R1, R2

Port 0 reads six words on ID 1 while port 1 reads six on ID 2, back to back

Per-{port, id} expected queues; every response is compared with the head of its queue

R3

Port 0 writes lanes 0 and 2 of word 3, port 1 then writes lanes 1, 3 and 2, then a read of the word

The model word must be CC DD BB AA and the read must return it

R4

Response ready held low, nine reads sent (eight table entries plus the response register)

Both request ports must drop ready; then the table must drain

R5

Three reads outstanding with ready low, reset, the same IDs used again, then all 16 words read

Expectations dropped at reset; no response for dropped reads; memory kept

R6

Both ports write back to back, six each

The log of accepted ports must never repeat a port twice in a row

The whole reference model is the monitor at the top of the bench: ref_mem is updated at write acceptance, and each accepted read pushes a snapshot of it onto the expected queue of its {port, id}. It is about thirty lines because the spec was written as six exact rules first. Every wait is bounded (200 cycles for a drain, 50 for an acceptance), so a broken design ends in a named rule failure and not a simulator timeout.

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
// Signal-only interfaces: the environment lesson adds BFM classes to them.
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;
endinterface

interface mc_rsp_if (input logic clk, input logic rst_n);
  logic        ready = 1'b0;
  logic        valid, port;
  logic [1:0]  id;
  logic [31:0] data;
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

The bench passes with 40 responses, 18 switches between IDs and 20 stalled response cycles. R4 fills the table with nine reads, eight entries plus the response register, and requires both ports to drop ready.

Mutant runs. Every planted bug fails this bench. BUG = 6 fails R1 with swapped data on ID 6 (expected 1f0f0f0f got 1e0e0e0e). BUG = 2 is first caught by the R1/R2 reads of words that port 1 wrote during setup (expected 11010101 got 11000101), and the R3 scenario catches it on its own: port 1's lane-2 write is lost (ID 3 expected ccddbbaa got ccaabbaa). BUG = 3 fails R5 with "2 IDs still waiting". BUG = 4 fails R6. BUG = 1 fails in the R4 table-full scenario, the first time entry 7 is used, with a response on ID 3 that has nothing outstanding. BUG = 5 fails R1 with out-of-order data on ID 6 and then repeated responses.

That result is real, and it is also the trap. The bugs were planted on rules this bench exercises, and each scenario happened to reach the corner. A directed test proves a rule once, at the timing someone thought of. The random environment in the next lessons proves the rules under combinations nobody listed, and coverage shows which combinations ran.

Check yourself Why does every wait in this bench stop after a fixed number of cycles?

A design that never answers would otherwise hang the simulation with no message. A bounded wait turns the same fault into a failure that names the rule (for example R4: 2 IDs still waiting).

Related topics

  • Previous: Spec and Plan

  • Next: Environment

  • Memory-Model Checker

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

Previous

262 / 275

Next: Environment

Up next

Up nextEnvironment

Sign in to save progress

Practice this lessonPrevious lessonReport an error

See also

  • Environment Self-Test
  • Checking
  • Testing the Checkers