A checker can only be as right as the monitor that feeds it. If the driver and the monitor disagree about what was sent, every later failure is ambiguous: is the design wrong, or the testbench? The previous lesson built the drivers, monitor, policy layers and phases; this lesson runs them against the real controller with no checkers and demands that they agree.
Before trusting any checker, prove the plumbing. This bench runs the real item, BFMs and controller with a self-test environment in place of the checkers. It requires that every item a driver offered is seen once by the monitor, in order, that at most one request is accepted per cycle, that the write share follows each port's policy, and that every accepted read gets a response.
// ===================== 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
// Environment self-test: proves the plumbing before any checker is trusted.
class env_selftest;
req_bfm req [2];
rsp_bfm rsp;
cap_item item [2];
mc_item sent [2][$]; // what each driver offered, oldest first
int errors, seen [2], reads_acc, rsps, ready_pct = 50, stalled, wr [2], n_acc [2];
longint cycle;
function new(req_bfm r0, req_bfm r1, rsp_bfm rs);
req[0] = r0; req[1] = r1; rsp = rs;
foreach (item[p]) item[p] = new();
endfunction
function void err(string msg);
errors++;
if (errors <= 5) $display("[cycle %0d] %s", cycle, msg);
endfunction
// Monitor: every acceptance must match the driver's record, in order.
task monitor();
mc_item t, s;
bit [2:0] pid; bit [31:0] d;
int acc_this_cycle;
forever begin
rsp.tick();
acc_this_cycle = 0;
cycle++;
if (rsp.sample(pid, d)) rsps++;
if (rsp.stalled()) stalled++;
for (int p = 0; p < 2; p++) begin
t = req[p].sample();
if (t == null) continue;
acc_this_cycle++;
seen[p]++;
if (!t.write) reads_acc++;
if (sent[p].size() == 0) begin err($sformatf("port %0d: monitor saw an item the driver never sent", p)); continue; end
s = sent[p].pop_front();
if ({t.write, t.id, t.addr, t.wdata, t.wstrb} != {s.write, s.id, s.addr, s.wdata, s.wstrb})
err($sformatf("port %0d: monitor and driver disagree", p));
end
if (acc_this_cycle > 1) err("two acceptances in one cycle");
end
endtask
task sink();
forever rsp.drive_ready($urandom_range(0, 99) < ready_pct);
endtask
// Driver loop for one port; records each item it believes was accepted.
task port_traffic(int p, int n, bit [3:0] lo, bit [3:0] hi);
bit ok; int waited;
mc_item copy;
for (int i = 0; i < n; i++) begin
if (!item[p].randomize()) err("randomize failed");
if (!(item[p].addr inside {[lo:hi]})) err($sformatf("port %0d: addr %0d outside the window", p, item[p].addr));
copy = new();
copy.write = item[p].write; copy.id = item[p].id; copy.addr = item[p].addr;
copy.wdata = item[p].wdata; copy.wstrb = item[p].wstrb;
sent[p].push_back(copy); // record before driving: the monitor may run first
req[p].drive(item[p], ok, waited);
if (!ok) begin void'(sent[p].pop_back()); err($sformatf("port %0d: not accepted", p)); break; end
n_acc[p]++;
wr[p] += item[p].write;
end
req[p].idle();
endtask
endclass
The bench passes: 400 items agree, write shares are 79% and 16% against policies of 80% and 20% (within the bench's tolerance for 200 draws), and 200 response cycles were stalled.
Mutant runs. A driver that treats valid as acceptance instead of waiting for ready fails at once with "monitor and driver disagree" on both ports. Removing the clocking blocks and driving the pins directly fails with "two acceptances in one cycle" from cycle 5 on, the Verilator issue described above.
Each share is a count out of about 200 random draws, so it varies around the policy. A band still fails when a policy is ignored or applied to the wrong port, which is what the test is for.