The previous lesson assigned every rule an owner. An owner that has never been seen to fail is a belief, not evidence: a scoreboard with a wrong key, or a reset flush that was never wired, stays quiet on a broken design and looks identical to a healthy one. The cheapest proof is a test of the checkers themselves, with no design in the loop.
Checkers are code and need tests. This bench has no design. It drives the pins the capstone monitor watches from a script, one case per failure mode, and requires each case to be flagged by exactly the checkers expected, and the legal case by none.
// ===================== 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, late;
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
The bench passes. The log shows each case and what fired:
Case | Scoreboard | a_no_orphan | Leak |
|---|---|---|---|
Legal cross-ID reorder | - | - | - |
Within-ID swap (bug 6) | MISMATCH x2 | - | - |
Duplicate response (bug 5) | UNEXPECTED | fires | - |
ID swap (bug 1) | UNEXPECTED | fires | - |
Strobe lane dropped (bug 2) | MISMATCH 88bb8888 vs 88888888 | - | - |
Response after reset | UNEXPECTED | fires | - |
Lost response (bug 3) | - | - | LEAK ID 0 |
Mutant run. Removing sb.flush() from the reset branch fails the bench on the reset case: the stale response matches the dropped read, the scoreboard stays quiet, and only a_no_orphan fires. That is exactly the escape a missing flush causes in a real regression, where a design that wrongly answers a dropped read would pass.
None in this bench: during reset nothing is sampled, and the controller holds rsp.valid low in reset. If the spec allowed responses during reset, the monitor would need a rule for them; R5 says outstanding reads are dropped, so a response for one after reset is UNEXPECTED.
A checker that fires on everything also passes every failing case. The legal case proves the checkers are not simply noisy, and it catches a scoreboard that searches or compares across IDs.