Part 8 · Senior & Interview Prep · Intermediate
Step 4: Monitor, Scoreboard & Model
Input and output monitors, the queue-based reference model, and a scoreboard with in-order compare and drain checks — complete code.
Checking architecture
Two passive monitors watch the pins through mon_cb 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; 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 is what 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()Monitors — complete code
class fifo_in_mon #(parameter int WIDTH = 8);
virtual fifo_if #(WIDTH) vif;
mailbox #(bit [WIDTH-1:0]) mb_in;
function new(virtual fifo_if #(WIDTH) vif,
mailbox #(bit [WIDTH-1:0]) mb);
this.vif = vif; 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) vif;
mailbox #(bit [WIDTH-1:0]) mb_out;
function new(virtual fifo_if #(WIDTH) vif,
mailbox #(bit [WIDTH-1:0]) mb);
this.vif = vif; 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
endclassCode walkthrough
A transaction exists only when valid AND ready are both high — sampling on valid alone counts stalled beats repeatedly.
Both monitors sample through mon_cb — Preponed values, and structurally unable to drive.
Reset cycles are skipped: in-flight data is discarded per spec rule 8, so it must not enter the model.
Monitors publish plain data words here — the flags are checked by assertions (step 5), keeping each checker single-purpose.
Scoreboard with queue model and drain check — complete code
class fifo_sb #(parameter int WIDTH = 8);
mailbox #(bit [WIDTH-1:0]) mb_in, mb_out;
bit [WIDTH-1:0] exp_q[$]; // the reference model: a queue
int n_checked, n_errors;
function new(mailbox #(bit [WIDTH-1:0]) mi,
mailbox #(bit [WIDTH-1:0]) mo);
mb_in = mi; mb_out = mo;
endfunction
task run();
fork
model_thread();
check_thread();
join_none
endtask
task model_thread();
bit [WIDTH-1:0] d;
forever begin
mb_in.get(d);
exp_q.push_back(d); // model: push enqueues expectation
end
endtask
task check_thread();
bit [WIDTH-1:0] got, exp;
forever begin
mb_out.get(got);
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: mismatch #%0d 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
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 checked, %0d errors, %0d still in FIFO",
n_checked, n_errors, exp_q.size());
if (n_errors == 0) $display("SB: *** PASS ***");
else $display("SB: *** FAIL ***");
endfunction
endclassCode walkthrough
The model IS the queue — push_back on observed push, pop_front on observed pop: in-order by construction (F-01).
got !== exp (case inequality) — an X in out_data must fail, not match-by-luck under ==.
Pop-with-empty-model is its own distinct error: a spurious out_valid, not a data mismatch.
flush() on reset keeps the model aligned with spec rule 8 — the test calls it at reset assert.
The drain check compares leftover model entries with the DUT's final occupancy — catching lost pushes that no pop ever exposes.
Key takeaways
Handshake-qualified sampling (valid && ready) — the transaction exists only then.
A queue is the entire FIFO reference model; in-order compare comes free.
Distinguish spurious-pop, data-mismatch, and drain-leftover — three different bug signatures.
Use !== so X propagation fails loudly instead of matching by luck.
Common pitfalls
Sampling on valid alone — every stalled beat double-counts into the model.
Forgetting flush() on mid-test reset — every post-reset compare misfires.
Skipping the drain check — a push the DUT swallowed without storing passes silently.