Part 6 · Testbench Architecture · Intermediate

Scoreboard Architecture

Expected vs actual streams, in-order FIFO compare, out-of-order associative-array compare, and where expected transactions come from.

Two streams, one verdict

Every scoreboard reduces to the same shape: an expected stream (what the DUT should produce) and an actual stream (what the output monitor observed), with a compare engine in between. Everything else — FIFOs, associative arrays, timeouts — is bookkeeping around that core.

diagram
EXPECTED vs ACTUAL STREAM MODEL

  input monitor ──► reference model ──► expected stream ─┐
                                                          ▼
                                                   ┌────────────┐
                                                   │ scoreboard  │── match? ──► count
                                                   └────────────┘     │
                                                          ▲           └─ MISMATCH  error
  output monitor ─────────────────► actual stream ───────┘

  In-order DUT:      expected_q[$]  — pop front, compare 1:1
  Out-of-order DUT:  expected[id]   — look up by transaction id

Where does "expected" come from?

  • Reference model fed by the input monitor — the standard path: model transforms observed inputs into predicted outputs. Works for any DUT with defined functional behavior.

  • Monitor-on-input directly (pass-through DUTs) — for FIFOs, bridges, and switches the input transaction IS the expected output; no model needed.

  • Never from the generator/driver — the DUT may legally drop, reorder, or transform stimulus; predicting from intent instead of observed input bakes in wrong assumptions.


In-order compare: the FIFO scoreboard

When the DUT preserves ordering (FIFOs, pipelines, simple buses), the scoreboard is a queue: push expected at the back, and every actual transaction must match the front. Order violations surface automatically as data mismatches.

systemverilog
class fifo_scoreboard;
  mailbox #(bus_txn) mbx_exp;   // from ref model / input monitor
  mailbox #(bus_txn) mbx_act;   // from output monitor
  bus_txn   expected_q[$];
  int       match_count, mismatch_count;

  function new(mailbox #(bus_txn) mbx_exp, mailbox #(bus_txn) mbx_act);
    this.mbx_exp = mbx_exp;
    this.mbx_act = mbx_act;
  endfunction

  task run();
    fork
      forever begin                    // expected side: just enqueue
        bus_txn t;
        mbx_exp.get(t);
        expected_q.push_back(t);
      end
      forever begin                    // actual side: compare to front
        bus_txn act, exp;
        mbx_act.get(act);
        if (expected_q.size() == 0) begin
          mismatch_count++;
          $error("[SCB] actual txn with empty expected queue: %s",
                 act.convert2string());
          continue;
        end
        exp = expected_q.pop_front();
        if (exp.compare(act)) match_count++;
        else begin
          mismatch_count++;
          $error("[SCB] MISMATCH\n  exp: %s\n  act: %s",
                 exp.convert2string(), act.convert2string());
        end
      end
    join_none
  endtask
endclass

Out-of-order compare: associative array by id

DUTs that complete transactions out of order — multi-bank memories, out-of-order interconnects, anything with per-id channels — break the FIFO model. The fix: key expected transactions by an id field in an associative array and look up each actual transaction by its id.

systemverilog
class ooo_scoreboard;
  mailbox #(bus_txn) mbx_exp, mbx_act;
  bus_txn  expected[int];          // keyed by txn id
  int      match_count, mismatch_count, orphan_count;

  task run();
    fork
      forever begin
        bus_txn t;
        mbx_exp.get(t);
        if (expected.exists(t.id))
          $error("[SCB] duplicate expected id=%0d", t.id);
        expected[t.id] = t;
      end
      forever begin
        bus_txn act;
        mbx_act.get(act);
        if (!expected.exists(act.id)) begin
          orphan_count++;
          $error("[SCB] actual id=%0d has no expected entry: %s",
                 act.id, act.convert2string());
          continue;
        end
        if (expected[act.id].compare(act)) match_count++;
        else begin
          mismatch_count++;
          $error("[SCB] MISMATCH id=%0d\n  exp: %s\n  act: %s",
                 act.id, expected[act.id].convert2string(),
                 act.convert2string());
        end
        expected.delete(act.id);     // consumed — leftovers checked at EOT
      end
    join_none
  endtask
endclass

Choosing the structure

  • Queue (in-order): ordering is part of the spec — a reorder bug shows up as a compare mismatch for free.

  • Associative array (out-of-order): only data integrity per id is checked; if ordering rules exist per id, keep a queue per id (associative array of queues).

  • Either way, leftovers at end of test (non-empty queue, non-empty array) are failures — covered in End-of-Test Checks.

Interview angle

Expect: "Design a scoreboard for a DUT that returns responses out of order." Walk through the by-id associative array, mention the duplicate-id guard, deleting consumed entries, and checking the array is empty at end of test. Then mention the hybrid: an associative array of queues when each id must stay in order internally.

Key takeaways

  • Scoreboard = expected stream vs actual stream; the data structure follows the DUT's ordering contract.

  • In-order DUT → queue compare against the front; reorder bugs surface automatically.

  • Out-of-order DUT → associative array keyed by id, delete on consume, check empty at EOT.

  • Expected comes from a model fed by the input monitor — never from driver intent.

Common pitfalls

  • Feeding expected from the generator — DUT-legal drops/reorders become false mismatches.

  • Using a FIFO compare on an out-of-order DUT — random pass/fail depending on completion timing.

  • Forgetting to delete consumed associative-array entries — leaks and false leftover errors at EOT.

  • Comparing inside the monitor — couples observation to checking and kills reuse of the monitor.