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.

diagram
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

systemverilog
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
endclass

Code walkthrough

  1. A transaction exists only when valid AND ready are both high — sampling on valid alone counts stalled beats repeatedly.

  2. Both monitors sample through mon_cb — Preponed values, and structurally unable to drive.

  3. Reset cycles are skipped: in-flight data is discarded per spec rule 8, so it must not enter the model.

  4. 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

systemverilog
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
endclass

Code walkthrough

  1. The model IS the queue — push_back on observed push, pop_front on observed pop: in-order by construction (F-01).

  2. got !== exp (case inequality) — an X in out_data must fail, not match-by-luck under ==.

  3. Pop-with-empty-model is its own distinct error: a spurious out_valid, not a data mismatch.

  4. flush() on reset keeps the model aligned with spec rule 8 — the test calls it at reset assert.

  5. 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.