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 |
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.
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 |
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 |
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.
// 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.
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.
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.