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.
Each bug was run by editing the default parameter. The cycle is the monitor's cycle count from the start of simulation.
BUG | First checker | Cycle | First message | Also fired |
|---|---|---|---|---|
1 ID swap | scoreboard and a_no_orphan | 142 | UNEXPECTED response for ID 2 with nothing outstanding | Mismatches on ID 0 after it |
2 strobe drop | scoreboard (expected data from the memory model) | 41 | MISMATCH ID 6 expected 650c7b25 got 651f7b25 | Only the scoreboard |
3 lost after reset | watchdog | 1024 | ID 1 waited 200 cycles (issued at 824) | driver at 1238, drain at 4644 |
4 starvation | a_fair | 5 | port 0 accepted twice while port 1 waited | Only a_fair |
5 duplicate | scoreboard, then a_no_orphan | 41, 42 | MISMATCH on ID 6, then UNEXPECTED on ID 6 | - |
6 order within ID | scoreboard | 31 | MISMATCH ID 7 (two values swapped) | Only the scoreboard |
Three things in the table are worth a reviewer's attention.
Bug 1 needed a nearly full table. The ID mux bug is in entry 7, and the table hands out the lowest free entry, so entry 7 is allocated only when entries 0 to 6 are busy (7 or more reads outstanding). CV1's 8+ bin is not the evidence: in the passing run entry 7 was allocated 30 times, 20 of them by reads that found 7 outstanding (the 4-7 bin) and 10 by reads that found 8, while only 10 of the 58 reads in the 8+ bin took entry 7. The white-box row CV1b counts entry 7 directly; a test with 0 there could not have found this bug.
Bug 3 was found 204 cycles after the reset, by the watchdog, not by a data compare. Liveness checks matter: without the watchdog and the bounded drain, a lost response is a silent timeout.
Bugs 2, 4 and 6 have exactly one checker each. Removing that one checker lets the bug through. The plan should say so, and those checkers deserve the most review.
Mutation of the bench, not the design, shows what each piece is worth. Two runs from the previous lesson make the point: with the mid-traffic reset removed, BUG = 3 fires no checker; coverage rows CV8 and CV9 report holes and the bench's "reset did not happen" check fails. With the fairness assertions removed, BUG = 4 fires nothing and only CV7 (port 1 wins) reports a hole. The bench still fails in both cases because unmet coverage goals and the test's own sanity checks fail it. That is the reason to make coverage goals part of the pass criterion rather than a report someone reads later.
A sign-off review asks for evidence, not assurances. For this capstone:
Question | Evidence here |
|---|---|
Is every rule owned by a checker? | Checker table in Checking: R1-R6 each have at least one owner |
Do the checkers work? | Checker unit test (7 cases) and six planted bugs, each caught, with cycle numbers |
Did the stimulus reach the risky states? | Every coverage row at goal in the full run on 31 seeds; CV1 8+, CV1b, CV2, CV3, CV8 and CV9 are the high-risk rows |
Are the holes understood? | CV1 depth 0 was structural; the directed phase D closes it on every seed; the per-test vs regression meaning is recorded per row |
Can the bench hang or pass vacuously? | Bounded drivers, bounded drain, a global timeout, activity gates (CV2, CV3), and a check that the reset happened |
Are tool limits recorded? | Covergroups mirrored; cover properties need --coverage-user; local-variable assertion not run on Verilator 5.048; clocking-block drives (a Verilator 5.048 workaround, and good style) |
Is it reproducible? | Fixed seed 11 by default; +seed and +items for other runs; all results in this topic regenerated from the code shown |
What this capstone does not prove is also part of sign-off: no formal proof of the ordering rule, no coverage of an ID reused while a same-ID response is stalled (no row defines it), CV10 and CV11 not enforced on Verilator (they exist only as cover properties), and one design configuration. A principal engineer lists these as known gaps with an owner, rather than leaving them for someone to discover.
A tighter watchdog limit, or the ticket assertion with a smaller MAX_LAT, would report it closer to the first stuck read. The cost is false failures when back-pressure legally delays a response, so the limit must stay above the worst legal latency (up to 69 cycles measured here).
The ticket assertion from the Checking lesson on a simulator that supports local variables, or an auxiliary-code assertion that records, per ID, the accept order of each read's sequence number and checks that responses retire them in order. Either would catch the swap without the scoreboard.