A router or crossbar with N inputs and M outputs carries up to N times M independent streams. Order is normally kept within a stream (one source to one destination). Items from different sources may interleave at a shared output in any order the arbiter chooses.
So the ordering key is the pair (source, destination). The scoreboard needs three parts:
A routing predictor that computes the destination from the input item, written from the spec.
One queue per stream, filled by the input monitor through the predictor.
Per-stream accounting: how many items each stream sent and received, checked at the end.
in0 --+ predictor: addr -> dst
| +-----------+ stream key = src * 3 + dst
in1 --+-->| router |--> out0 exp_q[0], exp_q[3]
| 2 x 3 |--> out1 exp_q[1], exp_q[4]
+-----------+--> out2 exp_q[2], exp_q[5]The predictor duplicates the address map. That is the point: it must come from the specification, not from the RTL. A predictor copied from the RTL agrees with every RTL bug.
The router here maps 00-3F to output 0, 40-9F to output 1 and A0-FF to output 2. Input 0 wins a shared output; input 1 stalls with in_ready = 0. The planted bug (MISROUTE = 1) sends 90-9F to output 2.
// 2-in, 3-out router. Address map: 00-3F -> out 0, 40-9F -> out 1, A0-FF -> out 2.
// Input 0 wins a shared output; input 1 then stalls (in1_ready = 0).
module router_2x3 #(parameter bit MISROUTE = 0) (
input logic clk, rst_n,
input logic in_valid [2],
input logic [7:0] in_addr [2],
input logic [7:0] in_data [2],
output logic in_ready [2],
output logic out_valid[3],
output logic [7:0] out_addr [3],
output logic [7:0] out_data [3],
output logic out_src [3]
);
function automatic logic [1:0] dest(logic [7:0] a);
if (a < 8'h40) return 0;
if (MISROUTE && a >= 8'h90 && a < 8'hA0) return 2; // planted bug
if (a < 8'hA0) return 1;
return 2;
endfunction
logic [1:0] d0, d1;
assign d0 = dest(in_addr[0]);
assign d1 = dest(in_addr[1]);
assign in_ready[0] = 1'b1;
assign in_ready[1] = !(in_valid[0] && d0 == d1);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int k = 0; k < 3; k++) begin
out_valid[k] <= 0; out_addr[k] <= 0; out_data[k] <= 0; out_src[k] <= 0;
end
end else begin
for (int k = 0; k < 3; k++) begin
out_valid[k] <= 0;
if (in_valid[0] && d0 == 2'(k)) begin
out_valid[k] <= 1; out_addr[k] <= in_addr[0]; out_data[k] <= in_data[0]; out_src[k] <= 0;
end else if (in_valid[1] && in_ready[1] && d1 == 2'(k)) begin
out_valid[k] <= 1; out_addr[k] <= in_addr[1]; out_data[k] <= in_data[1]; out_src[k] <= 1;
end
end
end
end
endmodule
typedef struct packed { logic [7:0] addr; logic [7:0] data; } pkt_t;
// Routing predictor + one in-order queue per (source, destination) stream.
class stream_scoreboard #(int NS = 2, int ND = 3);
pkt_t exp_q[int][$]; // key = src * ND + dst: one queue per stream
int sent[NS][ND], got[NS][ND];
int n_errors;
// The predictor: the same address map, written from the spec, not the RTL.
function int route(pkt_t p);
if (p.addr < 8'h40) return 0;
if (p.addr < 8'hA0) return 1;
return 2;
endfunction
function void add_input(int src, pkt_t p);
int d = route(p);
exp_q[src * ND + d].push_back(p);
sent[src][d]++;
endfunction
function void check_output(int dst, int src, pkt_t p);
pkt_t e;
got[src][dst]++;
if (exp_q[src * ND + dst].size() == 0) begin
n_errors++;
$display("ERROR out%0d: unexpected pkt from in%0d addr %h (stream in%0d->out%0d empty)",
dst, src, p.addr, src, dst);
return;
end
e = exp_q[src * ND + dst].pop_front();
if (p !== e) begin
n_errors++;
$display("ERROR in%0d->out%0d: expected %h/%h got %h/%h", src, dst, e.addr, e.data, p.addr, p.data);
end
endfunction
function void final_report();
for (int s = 0; s < NS; s++)
for (int d = 0; d < ND; d++) begin
$display("stream in%0d->out%0d: sent %0d got %0d left %0d",
s, d, sent[s][d], got[s][d], exp_q[s * ND + d].size());
if (exp_q[s * ND + d].size() != 0 || sent[s][d] != got[s][d]) n_errors++;
end
endfunction
endclass
With MISROUTE(0) the bench passes: 120 packets, six streams in order and empty, 22 stalls on input 1. The stall count is checked, because a run with no contention never tests the arbitration path.
Set MISROUTE(1) on the dut instance and the bench fails. The first error names the bug exactly: out2: unexpected pkt from in0 addr 9d (stream in0->out2 empty). The accounting then shows both inputs' ->out1 streams short and their ->out2 streams over by the same count (5 for in0, 4 for in1).
Note what follows the first error: 17 in0->out1 mismatches. The stream lost an item, so every later comparison is against a stale head. Read the first error, as in Failure Triage Method. Some teams resynchronize on a mismatch by searching ahead in the stream queue and reporting the skipped items as dropped; that shortens logs but must never hide the first error.
This router reports out_src, so the scoreboard looks up one stream. Many real designs do not. Then an output item could belong to any stream that feeds that output, and the scoreboard must match it against the head of each candidate stream:
// snippet: no source tag on the output. Only stream heads may match.
function void check_untagged(int dst, pkt_t p);
for (int s = 0; s < NS; s++) begin
int key = s * ND + dst;
if (exp_q.exists(key) && exp_q[key].size() != 0 && exp_q[key][0] === p) begin
void'(exp_q[key].pop_front());
return;
end
end
n_errors++;
$display("ERROR out%0d: %h matches no stream head", dst, p);
endfunctionMatching only the heads keeps in-stream order checked. If two heads can be equal, the match is ambiguous; give the stimulus unique payloads (a sequence field in the data) so it never happens.
Symptom at the end | Likely cause |
|---|---|
One stream short, another over by the same count | Misroute |
One stream short, nothing over | Drop |
One stream over, nothing short | Duplicate or predictor bug |
Counts right, data mismatches in one stream | Reorder or corruption within the stream |
stalls > 0?Without contention, input 1 never stalls, and the scoreboard never sees two sources interleaving at one output. A pass would then say nothing about the shared-output case.
dest() function, including its bug. What does the scoreboard report for the misroute?Nothing. Predictor and DUT agree, so every item lands in the expected stream. The predictor must come from the spec.