Two passive monitors watch the pins and publish observed events into mailboxes. The scoreboard owns a queue-based reference model: a push observed on the input side enqueues the expected word, and a pop observed on the output side dequeues and compares.
The stack never looks at generator intent. It checks what the DUT actually did, which lets it ride along passively at subsystem level later.
pins ──► in_mon ──(WIDTH-bit word)──► mb_in ──┐
(in_valid && in_ready) │ scoreboard
│ exp_q[$] ◄─ model
pins ──► out_mon ──(WIDTH-bit word)──► mb_out ──┘ pop → compare pop_front()The monitors below are class declarations that need fifo_if, with no top module, so this course's checks do not run them.
// not-run: fragment: needs fifo_if from the harness lesson and has no top module
class fifo_in_mon #(parameter int WIDTH = 8);
virtual fifo_if #(WIDTH).MON vif; // input-only view
mailbox #(logic [WIDTH-1:0]) mb_in; // 4-state: keeps X
function new(virtual fifo_if #(WIDTH).MON v,
mailbox #(logic [WIDTH-1:0]) mb);
vif = v;
mb_in = mb;
endfunction
task run();
forever begin
@(vif.mon_cb);
if (!vif.rst_n) continue; // ignore reset cycles
if (vif.mon_cb.in_valid && vif.mon_cb.in_ready) // a push happened
mb_in.put(vif.mon_cb.in_data);
end
endtask
endclass
class fifo_out_mon #(parameter int WIDTH = 8);
virtual fifo_if #(WIDTH).MON vif; // input-only view
mailbox #(logic [WIDTH-1:0]) mb_out; // 4-state: keeps X
function new(virtual fifo_if #(WIDTH).MON v,
mailbox #(logic [WIDTH-1:0]) mb);
vif = v;
mb_out = mb;
endfunction
task run();
forever begin
@(vif.mon_cb);
if (!vif.rst_n) continue;
if (vif.mon_cb.out_valid && vif.mon_cb.out_ready) // a pop happened
mb_out.put(vif.mon_cb.out_data);
end
endtask
endclassA transaction exists only when valid and ready are both high. Sampling on valid alone counts stalled beats repeatedly.
Both monitors hold the MON modport view, which exposes only the input-only clocking block and rst_n, so they cannot drive by construction.
Reset cycles are skipped. In-flight data is discarded per spec rule 8, so it must not enter the model.
Flags stay out of the monitors. Assertions own the flags, keeping each checker single-purpose.
The scoreboard below is a class declaration with no top module, so this course's checks do not run it.
// not-run: fragment: class declaration only, no top module
class fifo_sb #(parameter int WIDTH = 8);
mailbox #(logic [WIDTH-1:0]) mb_in, mb_out;
logic [WIDTH-1:0] exp_q[$]; // the reference model: a 4-state queue
int n_checked, n_errors;
int unsigned n_push, n_pop; // observed handshake totals
function new(mailbox #(logic [WIDTH-1:0]) mi,
mailbox #(logic [WIDTH-1:0]) mo);
mb_in = mi;
mb_out = mo;
endfunction
task run();
fork
model_thread();
check_thread();
join_none
endtask
task model_thread();
logic [WIDTH-1:0] d;
forever begin
mb_in.get(d);
exp_q.push_back(d); // model: push enqueues expectation
n_push++;
end
endtask
task check_thread();
logic [WIDTH-1:0] got, exp;
forever begin
mb_out.get(got);
n_pop++;
if (exp_q.size() == 0) begin
n_errors++;
$error("SB: pop with empty model — spurious out_valid? got=0x%0h", got);
continue;
end
exp = exp_q.pop_front(); // model: pop dequeues, in order
n_checked++;
if (got !== exp) begin
n_errors++;
$error("SB: data word %0d differs: exp=0x%0h got=0x%0h", n_checked, exp, got);
end
end
endtask
// called on reset: spec rule 8 — in-flight data discarded
function void flush();
exp_q.delete();
endfunction
// end-of-test drain check: leftovers mean lost data or a dead pop path.
// dut_count_now comes from a hierarchical probe (tb_top.dut.count) or the
// L4 coverage mirror: the pins carry no count, so the test supplies it.
function void report(int dut_count_now);
if (exp_q.size() != dut_count_now)
$error("SB: drain check failed — model holds %0d, DUT count %0d",
exp_q.size(), dut_count_now);
$display("SB: %0d pushed, %0d popped, %0d checked, %0d still in FIFO",
n_push, n_pop, n_checked, exp_q.size());
endfunction
endclassThe model is the queue. Push-back on observed push and pop-front on observed pop give in-order compare for free (F-01).
The data path stays 4-state from monitor to compare: logic mailboxes, queue and locals. Case inequality then makes an X in out_data fail loudly instead of matching by luck. A bit anywhere on the path would turn the X into 0 first.
Pop-with-empty-model is its own error: a spurious out_valid, not a data difference.
The drain check compares leftover model entries against DUT occupancy, catching pushes the DUT swallowed without storing.
No. A transfer needs both high, so this beat never moved. Publishing it would double-count the model when ready arrives.
Every later compare. The model still holds discarded words while the DUT restarted empty, so matching data reports differences.