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. Spec and Plan

Part 9 · Principal-Level Verification · Advanced

Spec and Plan

The controller's interface and rules, a risk ranking of its features, plan rows with closure criteria, and a directed bench that checks each rule once.

On this page

    The design

    ooo_memctl is a 16-word memory behind two request ports and one response port. Each request port carries a 2-bit ID, so there are eight ordering domains, one per {port, id}. Reads are held in an 8-entry table. Each read waits a pseudo-random 0 to 7 cycles before it may leave, and never leaves before an older read on its own ID; a ninth read can wait in the response register. The Part 8 FIFO capstone had one stream and a total order. This one has eight streams and a partial order, which is what changes the checking.

    Interface

    Signals

    Driven by

    Request port 0 and 1

    valid, write, id[1:0], addr[3:0] (word), wdata[31:0], wstrb[3:0]

    Testbench

    ready

    Controller

    Response port

    valid, port, id[1:0], data[31:0]

    Controller

    ready

    Testbench

    Global

    clk, rst_n (asynchronous, active low)

    Testbench

    The rules

    Rule

    Statement

    R1

    Read responses on the same {port, id} return in request order.

    R2

    Responses on different {port, id} may return in any order.

    R3

    A write applies its byte strobes when it is accepted. Writes are posted: they produce no response. A read returns the memory contents at the moment it was accepted.

    R4

    valid/ready on every port. A stalled response holds its payload. A request is accepted only when the read table has a free entry.

    R5

    Reset drops every outstanding read: no response is ever sent for it. Memory contents survive reset.

    R6

    When both ports request in the same cycle, acceptance alternates (round-robin). At most one request is accepted per cycle.

    R3 sets the ordering point. Because read data is fixed at acceptance, the reference model is updated on write acceptance and snapshotted on read acceptance, exactly as in the memory-model lesson. R5 tells the scoreboard what to do on reset: forget outstanding reads, keep the memory model. Writing these down before any code is what lets the checkers be exact.

    Features ranked by risk

    Rank features by how likely a bug is and how bad an escape would be. Likelihood comes from complexity and from how hard the state is to reach; impact comes from what software sees.

    Feature

    Likelihood

    Impact

    Rank

    Why

    Per-ID ordering with a shared table

    High

    High

    1

    Ordering state is spread over the table and per-ID counters

    Reset with reads in flight

    High

    High

    2

    Rarely reached by random traffic; state that survives reset is a classic bug

    Table full and response stalls

    Medium

    High

    3

    Last entry, allocation and release in one cycle

    Byte strobes per port

    Medium

    High

    4

    Silent data corruption

    Arbitration fairness

    Medium

    Medium

    5

    Starvation looks like low performance, not a failure

    Response ID and duplicate control

    Low

    High

    6

    A wrong ID corrupts another transaction's data

    Plan rows with closure criteria

    Each row names its stimulus, the checker that judges it, the coverage that proves it was exercised, and what "done" means. A row without a checker is a hope; a row without coverage cannot be closed.

    Row

    Feature

    Stimulus

    Checker

    Coverage

    Done when

    P1

    R1, R2 ordering

    Same-ID bursts, random latency

    ID scoreboard

    CV2 same-ID depth, CV3 cross-ID overtake

    0 mismatches; CV2 and CV3 at goal

    P2

    R3 strobes

    Partial writes on both ports, reads of hot words

    Memory model through the scoreboard

    CV4 lane x port, CV5 read-after-write

    All 8 lane bins and CV5 at goal

    P3

    R4 back-pressure

    Response ready 25-70%

    a_rsp_stable, drain

    CV1 depth bins, CV1b entry 7, CV6 stalls

    Table found full at least 3 times

    P4

    R5 reset

    Reset in the middle of a stalled phase

    Scoreboard flush, watchdog, drain

    CV8 reset busy, CV9 ID reuse after reset

    Both hit; nothing leaks after reset

    P5

    R6 fairness

    Both ports back to back

    a_fair (two assertions)

    CV7 wins per port

    No assertion failure; both ports win

    P6

    Checker quality

    Six planted bugs

    Each must fail the bench

    n/a

    Every bug caught; table recorded

    Common pitfall

    Plan rows that say "verify ordering" with no checker or done criterion. They cannot be closed and cannot be reviewed. Every row above names the object that fails the test and the number that ends the work.

    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.

    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.

    Interview

    Likely question: “How do you turn a spec into a verification plan?” Answer: number the rules, find the ordering point and the reset semantics because they decide the checker design, rank features by likelihood and impact, and write rows that name stimulus, checker, coverage and a measurable done criterion. Then prove each rule once with a directed test before trusting random runs.

    Check yourself Why does rule R3 matter more to the checker than to the designer?

    It fixes when expected read data is computed. If read data were fixed at response time, a write accepted between a read and its response would change the expected value, and the model would need to snapshot at a different point. Getting the ordering point wrong produces false failures or hides real ones.

    Check yourself The plan ranks reset with reads in flight second, above table-full. Why?

    Random traffic almost never asserts reset, so the state is unreachable unless a test does it on purpose, and state that wrongly survives reset corrupts behaviour long after. Table-full is reached naturally under heavy back-pressure.

    Related topics

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

    • Next: Environment

    • Spec to Verification Plan (Part 8)

    • From Spec to Coverage Plan

    Previous

    250 / 259

    Next: Environment

    Up next

    Up nextEnvironment

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Checking
    • Coverage and Closure
    • Bug Hunt and Sign-Off