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 |
R4 | Response ready held low, nine reads sent (eight table entries plus the response register) | Both request ports must drop |
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.
// 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.
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).