Every Part 4 testbench caught most of its bugs with messages such as "unexpected flit", "flit 1239, expected 8d57 from input 3" and "flits still pending". None of those tests contained a check written for that bug. All of them came from the scoreboard inside the shared environment, quietly comparing every flit DCRouter sent with the flit it should have sent.
How does a scoreboard decide whether every output was right?
The obvious scoreboard keeps a list of expected outputs and compares each actual output with the next one on the list. For a design that reorders legally, such as a router interleaving packets from four inputs, "the next one on the list" is the wrong expectation and the scoreboard reports false failures. Its structure must follow the specification's ordering promise (lesson 2.7), and it must also notice outputs that never come.
Part | DCRouter |
|---|---|
Input monitor | Every flit accepted (in_vld and in_rdy) is a fact about what must come out |
Prediction | The reference model: here simple, the same flit at output dst, with out_err expected on the tail of a faulty packet |
Matching structure | One queue per flow (input, output), because order is promised only within a flow |
Output monitor | Every flit sent (out_vld) is compared with the oldest prediction of its flow |
End-of-test check | Every queue must be empty; anything left was lost (lesson 5.7) |
Prediction, at the inputs: each accepted flit goes into the queue of its flow, with the error flag expected on a faulty tail.
logic [16:0] exp_q [4][4][$]; // expected {err on tail, data} per (input, output)
for (genvar i = 0; i < 4; i++) begin : g_src
always @(posedge clk) if (!rst) begin
if (in_vld[i] && in_rdy[i]) exp_q[i][drv[i].dst].push_back({drv[i].last && drv[i].err, drv[i].data});
if (!(in_vld[i] && !in_rdy[i])) begin // never change valid data while stalled
in_vld[i] <= 0;
if (fq[i].size() > 0 && $urandom_range(0, 99) >= gap_pct[i]) begin
drv[i] = fq[i].pop_front();
in_vld[i] <= 1;
in_data[16*i +: 16] <= drv[i].data;
in_last[i] <= drv[i].last;
end
end
end
endComparison, at the outputs: the header of each packet names its input (the testbench put the source in the header's free bits), so every later flit of the packet is matched against that flow's queue.
// ---------------- sinks and scoreboard ----------------
int drain_pct [4] = '{100, 100, 100, 100};
int occ [4] = '{0, 0, 0, 0}, got_pkts [4] = '{0, 0, 0, 0}, got_flits [4] = '{0, 0, 0, 0};
int cur_src [4];
bit in_pkt [4] = '{0, 0, 0, 0};
for (genvar o = 0; o < 4; o++) begin : g_snk
always @(posedge clk) begin
int n;
out_crd[o] <= 0;
n = occ[o];
if (!rst && out_vld[o]) begin
logic [15:0] d;
logic [16:0] e;
d = out_data[16*o +: 16];
n++; got_flits[o]++;
if (!in_pkt[o]) begin cur_src[o] = int'(d[8:7]); in_pkt[o] = 1; end
if (exp_q[cur_src[o]][o].size() == 0) fail($sformatf("out%0d: unexpected flit %h", o, d));
else begin
e = exp_q[cur_src[o]][o].pop_front();
if (e[15:0] !== d) fail($sformatf("out%0d: flit %h, expected %h from input %0d", o, d, e[15:0], cur_src[o]));
if (out_err[o] !== (out_last[o] && e[16])) fail($sformatf("out%0d: out_err %0b on flit %h, expected %0b", o, out_err[o], d, out_last[o] && e[16]));
end
if (out_last[o]) begin in_pkt[o] = 0; got_pkts[o]++; end
end
if (n > 4) fail($sformatf("out%0d: %0d flits in a 4-slot sink (credit overrun)", o, n));
if (n > 0 && $urandom_range(0, 99) < drain_pct[o]) begin n--; out_crd[o] <= 1; end
occ[o] = n;
end
endFailure message (from the Part 4 runs) | What happened |
|---|---|
BUG_24 caught ERROR @785: out0: unexpected flit ff82 | A flit arrived at an output where no flow expected one: misrouted (BUG_24) |
BUG_2 caught ERROR @135: out1: flit 0000, expected 3b10 from input 3 | The right flow, the wrong data: a corrupted or skipped flit (BUG_2) |
BUG_13 caught ERROR @36725: out2: out_err 0 on flit 0084, expected 1 | The right data, the wrong error flag (BUG_13) |
BUG_21 caught ERROR @439965: 92 flits still pending after 40000 cycles (lost or stuck) | Predictions never matched: flits lost or stuck (BUG_21) |
Each kind of mismatch has its own message, and each names the output, the flit and, where known, the input: enough to start debugging without opening a waveform.
Where predictions come from: from accepted inputs, never from the design's internals (lesson 3.4).
How deep the model is: transaction results, not cycles (lesson 5.5).
How outputs are matched: by the specification's ordering domain, here the flow (lesson 5.6).
How identity is carried: a source and sequence number in a free field makes matching unambiguous.
What happens at the end: leftovers are failures (lesson 5.7).
Practice | Purpose |
|---|---|
Monitors separate from drivers | Predictions come from what the design actually accepted |
Matching keyed by ID, address or flow | Legal reordering never fails a test |
Reference models shared with architecture teams | One model of intent, used by everyone |
Detailed mismatch messages | Debug starts from the log, not the waveform |
A scoreboard (illustrative) predicted outputs from the driver's intended transactions instead of from what the design accepted. When the design back-pressured and the driver dropped a stalled transaction instead of holding it, a testbench bug, the scoreboard still expected it, and every run with heavy back-pressure failed with a lost packet the design never received. Predicting from accepted inputs makes the scoreboard judge the design, not the driver's intentions.
Order is promised only within a flow; packets from different inputs may interleave at an output in any order, so a single queue per output would report false failures.
The testbench writes the source input into the header's free bits, and the comparison reads it from each header.
A scoreboard predicts from accepted inputs, matches every output, and fails on leftovers.
Its matching structure must follow the specification's ordering promise.
Distinct failure messages for misrouted, corrupted, mis-flagged and lost outputs make debug fast.