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.
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.
// ===================== 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.
A regression that passes because it never reached the bug. With the mid-traffic reset removed, BUG = 3 fires no checker at all; only the coverage holes and the reset-happened check fail the run.