USB · Module 24
USB Scoreboards
Two transfers carrying the same bytes are indistinguishable to a scoreboard that matches on value, so a duplicate delivery and a lost transfer cancel out — identity finds the partner, value checks it.
Chapter 24.1 asked "was that legal?" and 24.2 asked "did it happen in time?" This chapter asks the third question: "was that mine?"
1. Match on Identity, Not on Value
The obvious scoreboard keeps a list of expected payloads and, when something arrives, searches the list for a payload that matches.
That scoreboard cannot count.
issue tag 3 carrying 0x5A
issue tag 6 carrying 0x5A <-- the SAME bytes. Very common:
zero-length reports, keepalives,
status polls, a cleared buffer.
tag 3 completes ... TWICE. a DUPLICATE delivery
tag 6 never completes. a LOST transfer
A value-matching scoreboard sees two arrivals of 0x5A and
two expectations of 0x5A. Everything matches. Both entries
are retired.
The report is clean, and two real bugs have cancelled
each other out.2. It Cannot Be a FIFO, Because Completion Is Out of Order
Different endpoints complete independently: a bulk transfer queued first can finish long after an interrupt transfer queued later. A scoreboard built as a queue reports an order violation on every interleaving, which is most of them.
But within an endpoint, order does hold — and that is worth checking, because a controller that completes an endpoint's transfers out of order has corrupted a stream that the host will not verify.
| out of order across endpoints | legal, and constant |
| out of order within an endpoint | a bug, and silent |
3. A Retired Tag Is Not an Empty Tag
When a completion arrives for a tag that nothing is outstanding on, there are two very different situations:
the tag was NEVER issued -> UNEXPECTED
something invented a completion
out of nothing
the tag was issued and has
ALREADY completed -> DUPLICATE
something delivered twiceA table with one valid bit cannot tell them apart and reports both as "unexpected", which sends everybody looking in the wrong place. So each tag has three states, and the third exists purely so that the report names the right bug.
4. A Retry Is Not a Duplicate
USB retries. A NAKed OUT is re-sent with the same data and the same identity, and that is the protocol working. A scoreboard that treats the re-send as a second delivery reports a duplicate on every flow-controlled transfer — and per 24.1 §14, a scoreboard that cries wolf is a scoreboard somebody switches off.
So a completion flagged as a retry is counted and does not retire the entry. Only the final delivery does.
5. Every Departure Consumes Its Place in the Order
This is the one that is easy to get wrong and produces the worst symptom. A transfer can leave the table four ways: matched, payload wrong, out of order, or never returned at all. Only the first two look like progress, and the tempting implementation advances the endpoint's expected sequence number only on a match.
6. An Identity Is Reusable, Which Bounds How Late a Duplicate Can Be Caught
A finite tag space only works because tags are recycled: a retired tag can be issued again. That is legal and necessary, and it has a consequence worth stating rather than discovering.
An identity must be unique among the transfers that can be in flight at the same time — not unique for the whole run.
So a duplicate delivery of the first use of a tag, arriving after the tag has been re-issued, is attributed to the second use. That is not a defect in the scoreboard; it is the price of a finite identity space, and the way to buy margin is more tags, not cleverer bookkeeping.
7. What We Are Building
usb_scoreboard — two channels, one table, and six ways a transfer can be wrong
usb_scoreboard #(N_TAG = 8, TW = 3, DW = 8)
issue channel completion channel
------------- ------------------
exp_valid act_valid
exp_tag IDENTITY act_tag IDENTITY
exp_data act_data
exp_ep act_ep
act_retry a re-send, not a result
eot drain and report
ONE EVENT PER CHANNEL, NOT ONE PER CYCLE. An issue and a
completion can happen in the same cycle and can BOTH be
wrong, so a single reporting slot would have to drop one.
Each channel gets its own pulse; the counters, which can
advance twice in a cycle, are the source of truth.8. Verilog-2005 Implementation
// usb_scoreboard -- matching what came back against what went out, and the
// one decision that determines whether the scoreboard is worth having.
//
// MATCH ON IDENTITY, NOT ON VALUE
//
// The obvious scoreboard keeps a list of expected payloads, and when
// something arrives it searches the list for a payload that matches.
//
// That scoreboard cannot count.
//
// issue tag 3 carrying 0x5A
// issue tag 6 carrying 0x5A <-- the SAME bytes. Very common:
// zero-length reports, keepalives,
// status polls, a cleared buffer.
//
// tag 3 completes ... twice. A DUPLICATE delivery.
// tag 6 never completes. A LOST transfer.
//
// A value-matching scoreboard sees two arrivals of 0x5A and two expectations
// of 0x5A. Everything matches. Both entries are retired. The report is clean,
// and two real bugs have cancelled each other out.
//
// A DUPLICATE AND A LOSS ARE INDISTINGUISHABLE FROM A
// PAIR OF CORRECT TRANSFERS IF YOU MATCH ON VALUE.
//
// So every entry carries an IDENTITY -- here a tag, in a real environment a
// transfer handle, a TRB pointer, a (endpoint, sequence) pair -- and the
// value is checked AFTER the identity has found the pair. Identity finds the
// partner; value checks the partner. Those are two different jobs and doing
// them with one comparison does neither.
//
// IT CANNOT BE A FIFO, BECAUSE COMPLETION IS OUT OF ORDER
//
// Different endpoints complete independently: a bulk transfer queued first
// can finish long after an interrupt transfer queued later. A scoreboard
// built as a queue reports an order violation on every interleaving, which
// is most of them.
//
// But WITHIN an endpoint, order does hold, and that is worth checking --
// because a controller that completes an endpoint's transfers out of order
// has corrupted a stream that the host will not verify.
//
// out of order ACROSS endpoints legal, and common
// out of order WITHIN an endpoint a bug, and silent
//
// AND EVERY DEPARTURE CONSUMES ITS PLACE IN THE ORDER
//
// This is the part that is easy to get wrong and produces the worst symptom.
// A transfer can leave the table four ways: matched, payload wrong, out of
// order, or never returned at all. Only the first two look like "progress",
// and the tempting implementation advances the endpoint's expected sequence
// number only on a match.
//
// Do that and ONE lost transfer leaves a permanent hole. The endpoint's
// expected sequence number is stuck one behind for the rest of the run, so
// EVERY subsequent completion on that endpoint is reported as out of order:
//
// 1 missing transfer -> 1 missing + N order violations
//
// and the report is then useless for finding the one that mattered. However
// a transfer leaves, it is gone, and its place in the order goes with it.
//
// A RETIRED TAG IS NOT AN EMPTY TAG
//
// This is the distinction that makes a duplicate visible at all. When a
// completion arrives for a tag that nothing is outstanding on, there are two
// very different situations:
//
// the tag was NEVER issued -> UNEXPECTED. Something invented a
// completion out of nothing.
// the tag was issued and has
// ALREADY completed -> DUPLICATE. Something delivered twice.
//
// A table with one valid bit cannot tell them apart and reports both as
// "unexpected", which sends everybody looking in the wrong place. So each
// tag has THREE states, and the third one exists purely so that the report
// names the right bug.
//
// ...AND AN IDENTITY IS REUSABLE, WHICH BOUNDS HOW LATE A DUPLICATE CAN BE
// CAUGHT
//
// A finite tag space only works because tags are recycled: a retired tag can
// be issued again. That is legal and necessary, and it has a consequence
// worth stating rather than discovering.
//
// An identity must be unique among the transfers that can be in
// flight AT THE SAME TIME -- not unique for the whole run.
//
// So a duplicate delivery of the FIRST use of a tag, arriving after the tag
// has been re-issued, is attributed to the second use. That is not a defect
// in the scoreboard; it is the price of a finite identity space, and the way
// to buy more margin is more tags, not cleverer bookkeeping.
//
// A RETRY IS NOT A DUPLICATE
//
// USB retries. A NAKed OUT is re-sent with the same data and the same
// identity, and that is the protocol working. A scoreboard that treats the
// re-send as a second delivery reports a duplicate on every flow-controlled
// transfer -- and per chapter 24.1, a scoreboard that cries wolf is a
// scoreboard somebody switches off.
//
// So a completion flagged as a retry is counted and does NOT retire the
// entry. Only the final delivery retires it.
//
// ONE EVENT PER CHANNEL, NOT ONE PER CYCLE
//
// An issue and a completion can happen in the same cycle and can BOTH be
// wrong, so a single reporting slot would have to drop one. Each channel
// gets its own pulse instead: the completion channel, the issue channel, and
// the drain. Every pulse is then single-valued, and the counters -- which
// can advance twice in a cycle -- are the source of truth.
module usb_scoreboard #(
parameter integer N_TAG = 8, // identities trackable at once
parameter integer TW = 3, // tag width: 2**TW == N_TAG
parameter integer DW = 8 // payload width
) (
input wire clk,
input wire rst_n,
// ---- the issue channel: a transfer was handed to the design ----
input wire exp_valid,
input wire [TW-1:0] exp_tag,
input wire [DW-1:0] exp_data,
input wire exp_ep,
// ---- the completion channel: something came back ----
input wire act_valid,
input wire [TW-1:0] act_tag,
input wire [DW-1:0] act_data,
input wire act_ep,
input wire act_retry, // this delivery is a re-send, not a result
input wire eot, // end of test: drain and report
output wire [TW:0] outstanding,
output wire [1:0] tag_state, // the state of the tag named by act_tag
output wire match_pulse,
output wire act_err_pulse,
output wire [2:0] act_err_code,
output wire exp_err_pulse, // only one cause: a reissued tag
output wire miss_pulse, // one per survivor of the drain
output reg [31:0] n_issued,
output reg [31:0] n_matched,
output reg [31:0] n_retries,
output reg [31:0] n_mismatch,
output reg [31:0] n_unexpected,
output reg [31:0] n_duplicate,
output reg [31:0] n_reissue,
output reg [31:0] n_missing,
output reg [31:0] n_order
);
// ---- Three states per tag, and the third one is the whole point. ----
localparam [1:0] T_EMPTY = 2'd0, // never issued, or long retired
T_OUTSTANDING = 2'd1, // issued, not yet completed
T_RETIRED = 2'd2; // completed -- a further completion
// is a DUPLICATE, not a surprise
localparam [2:0] E_NONE = 3'd0,
E_MISMATCH = 3'd1, // identity matched, payload did not
E_UNEXPECTED = 3'd2, // a completion for a tag never issued
E_DUPLICATE = 3'd3, // a second completion for one tag
E_ORDER = 3'd4; // out of order WITHIN an endpoint
reg [1:0] st_r [0:N_TAG-1];
reg [DW-1:0] dat_r [0:N_TAG-1];
reg ep_r [0:N_TAG-1];
reg [3:0] seq_r [0:N_TAG-1];
// Per-endpoint sequence numbers. Issue order is recorded on the way in and
// checked on the way out, which is what makes "in order within an
// endpoint" a property rather than an aspiration.
reg [3:0] iss_seq_r [0:1];
reg [3:0] cmp_seq_r [0:1];
reg [TW:0] cnt_r;
reg [2:0] aec_r;
reg mat_r, aer_r, eer_r, mis_r;
// The drain walks the table in tag order. A pointer rather than a search,
// so the drain is bounded and reports the survivors in a stable order.
reg [TW:0] drain_r;
assign outstanding = cnt_r;
assign tag_state = st_r[act_tag];
assign match_pulse = mat_r;
assign act_err_pulse = aer_r;
assign act_err_code = aec_r;
assign exp_err_pulse = eer_r;
assign miss_pulse = mis_r;
integer i;
reg [1:0] st_n [0:N_TAG-1];
reg [DW-1:0] dat_n [0:N_TAG-1];
reg ep_n [0:N_TAG-1];
reg [3:0] seq_n [0:N_TAG-1];
reg [3:0] iss_n [0:1];
reg [3:0] cmp_n [0:1];
reg [TW:0] cnt_n, drain_n;
reg [2:0] aec_n;
reg mat_n, aer_n, eer_n, mis_n;
reg ret_n;
always @* begin
for (i = 0; i < N_TAG; i = i + 1) begin
st_n[i] = st_r[i];
dat_n[i] = dat_r[i];
ep_n[i] = ep_r[i];
seq_n[i] = seq_r[i];
end
iss_n[0] = iss_seq_r[0]; iss_n[1] = iss_seq_r[1];
cmp_n[0] = cmp_seq_r[0]; cmp_n[1] = cmp_seq_r[1];
cnt_n = cnt_r;
drain_n = drain_r;
aec_n = E_NONE;
mat_n = 1'b0; aer_n = 1'b0; eer_n = 1'b0; mis_n = 1'b0;
ret_n = 1'b0;
if (eot) begin
// ---- THE DRAIN. Whatever is still outstanding was never completed.
//
// It is not "in flight" and it is not "inconclusive": the run is over.
// Chapter 24.2 makes the same point about assertion obligations, and
// the failure here is worse, because a missing completion means data
// the design accepted and never returned.
if (drain_r < N_TAG[TW:0]) begin
if (st_r[drain_r[TW-1:0]] == T_OUTSTANDING) begin
st_n[drain_r[TW-1:0]] = T_EMPTY;
cnt_n = cnt_n - 1'b1;
mis_n = 1'b1;
cmp_n[ep_r[drain_r[TW-1:0]]] =
cmp_seq_r[ep_r[drain_r[TW-1:0]]] + 4'd1;
end else begin
// A RETIRED tag is returned to EMPTY silently. It is not a
// failure -- it completed -- and leaving it RETIRED for ever
// would mean each identity could be used exactly once.
st_n[drain_r[TW-1:0]] = T_EMPTY;
end
drain_n = drain_r + 1'b1;
end
end else begin
drain_n = {(TW+1){1'b0}};
// ---- THE COMPLETION CHANNEL ----
if (act_valid) begin
case (st_r[act_tag])
T_OUTSTANDING: begin
if (act_data !== dat_r[act_tag]) begin
// Identity found the partner; the payload is wrong. This is
// the check everybody writes, and it only works because the
// identity found the RIGHT partner first.
aer_n = 1'b1; aec_n = E_MISMATCH;
st_n[act_tag] = T_RETIRED;
cnt_n = cnt_n - 1'b1;
cmp_n[ep_r[act_tag]] = cmp_seq_r[ep_r[act_tag]] + 4'd1;
end else if (act_ep != ep_r[act_tag]) begin
// The tag came back on an endpoint it was not issued on. Not
// a payload error and not a duplicate: the routing is wrong.
aer_n = 1'b1; aec_n = E_UNEXPECTED;
st_n[act_tag] = T_RETIRED;
cnt_n = cnt_n - 1'b1;
cmp_n[ep_r[act_tag]] = cmp_seq_r[ep_r[act_tag]] + 4'd1;
end else if (act_retry) begin
// ---- A RETRY IS NOT A DELIVERY. ----
//
// The entry stays outstanding, nothing is retired, and no
// error is raised. Counting it lets the report distinguish
// "the bus is busy" from "the bus is broken".
ret_n = 1'b1;
end else if (seq_r[act_tag] != cmp_seq_r[act_ep]) begin
// ---- Out of order WITHIN an endpoint. ----
aer_n = 1'b1; aec_n = E_ORDER;
st_n[act_tag] = T_RETIRED;
cnt_n = cnt_n - 1'b1;
cmp_n[act_ep] = cmp_seq_r[act_ep] + 4'd1;
end else begin
mat_n = 1'b1;
st_n[act_tag] = T_RETIRED;
cnt_n = cnt_n - 1'b1;
cmp_n[act_ep] = cmp_seq_r[act_ep] + 4'd1;
end
end
T_RETIRED: begin
// ---- A SECOND completion for a tag that already completed.
// ---- This is the case a one-bit valid flag cannot name.
aer_n = 1'b1; aec_n = E_DUPLICATE;
end
default: begin
// Never issued. Something produced a completion out of nothing.
aer_n = 1'b1; aec_n = E_UNEXPECTED;
end
endcase
end
// ---- THE ISSUE CHANNEL ----
if (exp_valid) begin
if (st_r[exp_tag] == T_OUTSTANDING) begin
// ---- A tag reused while it is still outstanding. ----
//
// The identity space has been violated, and the consequence is
// that the earlier transfer becomes untrackable -- it can never be
// matched, because its identity now belongs to something else. A
// scoreboard that silently overwrites loses that transfer AND
// reports nothing, which is the worst of both.
eer_n = 1'b1;
end else begin
st_n[exp_tag] = T_OUTSTANDING;
dat_n[exp_tag] = exp_data;
ep_n[exp_tag] = exp_ep;
seq_n[exp_tag] = iss_seq_r[exp_ep];
iss_n[exp_ep] = iss_seq_r[exp_ep] + 4'd1;
// Only counted here if the completion channel did not already
// retire this same tag in this cycle -- which it cannot, because
// a tag that was OUTSTANDING is caught above.
cnt_n = cnt_n + 1'b1;
end
end
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (i = 0; i < N_TAG; i = i + 1) begin
st_r[i] <= T_EMPTY;
dat_r[i] <= {DW{1'b0}};
ep_r[i] <= 1'b0;
seq_r[i] <= 4'd0;
end
iss_seq_r[0] <= 4'd0; iss_seq_r[1] <= 4'd0;
cmp_seq_r[0] <= 4'd0; cmp_seq_r[1] <= 4'd0;
cnt_r <= {(TW+1){1'b0}};
drain_r <= {(TW+1){1'b0}};
aec_r <= E_NONE;
mat_r <= 1'b0;
aer_r <= 1'b0;
eer_r <= 1'b0;
mis_r <= 1'b0;
n_issued <= 32'd0;
n_matched <= 32'd0;
n_retries <= 32'd0;
n_mismatch <= 32'd0;
n_unexpected <= 32'd0;
n_duplicate <= 32'd0;
n_reissue <= 32'd0;
n_missing <= 32'd0;
n_order <= 32'd0;
end else begin
for (i = 0; i < N_TAG; i = i + 1) begin
st_r[i] <= st_n[i];
dat_r[i] <= dat_n[i];
ep_r[i] <= ep_n[i];
seq_r[i] <= seq_n[i];
end
iss_seq_r[0] <= iss_n[0]; iss_seq_r[1] <= iss_n[1];
cmp_seq_r[0] <= cmp_n[0]; cmp_seq_r[1] <= cmp_n[1];
cnt_r <= cnt_n;
drain_r <= drain_n;
aec_r <= aec_n;
mat_r <= mat_n;
aer_r <= aer_n;
eer_r <= eer_n;
mis_r <= mis_n;
if (exp_valid && !eot && !eer_n) n_issued <= n_issued + 32'd1;
if (mat_n) n_matched <= n_matched + 32'd1;
if (ret_n) n_retries <= n_retries + 32'd1;
if (eer_n) n_reissue <= n_reissue + 32'd1;
if (mis_n) n_missing <= n_missing + 32'd1;
// The per-cause counters on the completion channel are driven by the
// SAME pulse as the channel's error, so they sum to it (chapter 23.4).
if (aer_n) begin
case (aec_n)
E_MISMATCH: n_mismatch <= n_mismatch + 32'd1;
E_UNEXPECTED: n_unexpected <= n_unexpected + 32'd1;
E_DUPLICATE: n_duplicate <= n_duplicate + 32'd1;
E_ORDER: n_order <= n_order + 32'd1;
default: ;
endcase
end
end
end
endmodule9. SystemVerilog Implementation
// usb_scoreboard -- matching what came back against what went out, and the
// one decision that determines whether the scoreboard is worth having.
//
// MATCH ON IDENTITY, NOT ON VALUE
//
// The obvious scoreboard keeps a list of expected payloads, and when
// something arrives it searches the list for a payload that matches.
//
// That scoreboard cannot count.
//
// issue tag 3 carrying 0x5A
// issue tag 6 carrying 0x5A <-- the SAME bytes. Very common:
// zero-length reports, keepalives,
// status polls, a cleared buffer.
//
// tag 3 completes ... twice. A DUPLICATE delivery.
// tag 6 never completes. A LOST transfer.
//
// A value-matching scoreboard sees two arrivals of 0x5A and two expectations
// of 0x5A. Everything matches. Both entries are retired. The report is clean,
// and two real bugs have cancelled each other out.
//
// A DUPLICATE AND A LOSS ARE INDISTINGUISHABLE FROM A
// PAIR OF CORRECT TRANSFERS IF YOU MATCH ON VALUE.
//
// So every entry carries an IDENTITY -- here a tag, in a real environment a
// transfer handle, a TRB pointer, a (endpoint, sequence) pair -- and the
// value is checked AFTER the identity has found the pair. Identity finds the
// partner; value checks the partner. Those are two different jobs and doing
// them with one comparison does neither.
//
// IT CANNOT BE A FIFO, BECAUSE COMPLETION IS OUT OF ORDER
//
// Different endpoints complete independently: a bulk transfer queued first
// can finish long after an interrupt transfer queued later. A scoreboard
// built as a queue reports an order violation on every interleaving, which
// is most of them.
//
// But WITHIN an endpoint, order does hold, and that is worth checking --
// because a controller that completes an endpoint's transfers out of order
// has corrupted a stream that the host will not verify.
//
// out of order ACROSS endpoints legal, and common
// out of order WITHIN an endpoint a bug, and silent
//
// AND EVERY DEPARTURE CONSUMES ITS PLACE IN THE ORDER
//
// This is the part that is easy to get wrong and produces the worst symptom.
// A transfer can leave the table four ways: matched, payload wrong, out of
// order, or never returned at all. Only the first two look like "progress",
// and the tempting implementation advances the endpoint's expected sequence
// number only on a match.
//
// Do that and ONE lost transfer leaves a permanent hole. The endpoint's
// expected sequence number is stuck one behind for the rest of the run, so
// EVERY subsequent completion on that endpoint is reported as out of order:
//
// 1 missing transfer -> 1 missing + N order violations
//
// and the report is then useless for finding the one that mattered. However
// a transfer leaves, it is gone, and its place in the order goes with it.
//
// A RETIRED TAG IS NOT AN EMPTY TAG
//
// This is the distinction that makes a duplicate visible at all. When a
// completion arrives for a tag that nothing is outstanding on, there are two
// very different situations:
//
// the tag was NEVER issued -> UNEXPECTED. Something invented a
// completion out of nothing.
// the tag was issued and has
// ALREADY completed -> DUPLICATE. Something delivered twice.
//
// A table with one valid bit cannot tell them apart and reports both as
// "unexpected", which sends everybody looking in the wrong place. So each
// tag has THREE states, and the third one exists purely so that the report
// names the right bug.
//
// ...AND AN IDENTITY IS REUSABLE, WHICH BOUNDS HOW LATE A DUPLICATE CAN BE
// CAUGHT
//
// A finite tag space only works because tags are recycled: a retired tag can
// be issued again. That is legal and necessary, and it has a consequence
// worth stating rather than discovering.
//
// An identity must be unique among the transfers that can be in
// flight AT THE SAME TIME -- not unique for the whole run.
//
// So a duplicate delivery of the FIRST use of a tag, arriving after the tag
// has been re-issued, is attributed to the second use. That is not a defect
// in the scoreboard; it is the price of a finite identity space, and the way
// to buy more margin is more tags, not cleverer bookkeeping.
//
// A RETRY IS NOT A DUPLICATE
//
// USB retries. A NAKed OUT is re-sent with the same data and the same
// identity, and that is the protocol working. A scoreboard that treats the
// re-send as a second delivery reports a duplicate on every flow-controlled
// transfer -- and per chapter 24.1, a scoreboard that cries wolf is a
// scoreboard somebody switches off.
//
// So a completion flagged as a retry is counted and does NOT retire the
// entry. Only the final delivery retires it.
//
// ONE EVENT PER CHANNEL, NOT ONE PER CYCLE
//
// An issue and a completion can happen in the same cycle and can BOTH be
// wrong, so a single reporting slot would have to drop one. Each channel
// gets its own pulse instead: the completion channel, the issue channel, and
// the drain. Every pulse is then single-valued, and the counters -- which
// can advance twice in a cycle -- are the source of truth.
package usb_sb_pkg;
// Three states per tag, and the third one is the whole point: it is what
// lets the report say DUPLICATE where a one-bit valid flag can only say
// "unexpected", which sends everybody looking in the wrong place.
typedef enum logic [1:0] {
T_EMPTY = 2'd0, // never issued, or returned by the drain
T_OUTSTANDING = 2'd1, // issued, not yet completed
T_RETIRED = 2'd2 // completed -- a further completion is a
// DUPLICATE, not a surprise
} tag_state_e;
typedef enum logic [2:0] {
E_NONE = 3'd0,
E_MISMATCH = 3'd1, // identity matched, payload did not
E_UNEXPECTED = 3'd2, // a completion for a tag never issued
E_DUPLICATE = 3'd3, // a second completion for one identity
E_ORDER = 3'd4 // out of order WITHIN an endpoint
} sb_err_e;
endpackage
module usb_scoreboard
import usb_sb_pkg::*;
#(
parameter int N_TAG = 8, // identities trackable at once
parameter int TW = 3, // tag width: 2**TW == N_TAG
parameter int DW = 8 // payload width
) (
input logic clk,
input logic rst_n,
// ---- the issue channel: a transfer was handed to the design ----
input logic exp_valid,
input logic[TW-1:0] exp_tag,
input logic[DW-1:0] exp_data,
input logic exp_ep,
// ---- the completion channel: something came back ----
input logic act_valid,
input logic[TW-1:0] act_tag,
input logic[DW-1:0] act_data,
input logic act_ep,
input logic act_retry, // this delivery is a re-send, not a result
input logic eot, // end of test: drain and report
output logic[TW:0] outstanding,
output tag_state_e tag_state, // the state of the tag named by act_tag
output logic match_pulse,
output logic act_err_pulse,
output sb_err_e act_err_code,
output logic exp_err_pulse, // only one cause: a reissued tag
output logic miss_pulse, // one per survivor of the drain
output logic [31:0] n_issued,
output logic [31:0] n_matched,
output logic [31:0] n_retries,
output logic [31:0] n_mismatch,
output logic [31:0] n_unexpected,
output logic [31:0] n_duplicate,
output logic [31:0] n_reissue,
output logic [31:0] n_missing,
output logic [31:0] n_order
);
tag_state_e st_r [N_TAG];
logic [DW-1:0] dat_r [N_TAG];
logic ep_r [N_TAG];
logic [3:0] seq_r [N_TAG];
// Per-endpoint sequence numbers. Issue order is recorded on the way in and
// checked on the way out, which is what makes "in order within an
// endpoint" a property rather than an aspiration.
logic [3:0] iss_seq_r [2];
logic [3:0] cmp_seq_r [2];
logic [TW:0] cnt_r;
sb_err_e aec_r;
logic mat_r, aer_r, eer_r, mis_r;
// The drain walks the table in tag order. A pointer rather than a search,
// so the drain is bounded and reports the survivors in a stable order.
logic [TW:0] drain_r;
assign outstanding = cnt_r;
assign tag_state = st_r[act_tag];
assign match_pulse = mat_r;
assign act_err_pulse = aer_r;
assign act_err_code = aec_r;
assign exp_err_pulse = eer_r;
assign miss_pulse = mis_r;
int i;
tag_state_e st_n [N_TAG];
logic [DW-1:0] dat_n [N_TAG];
logic ep_n [N_TAG];
logic [3:0] seq_n [N_TAG];
logic [3:0] iss_n [2];
logic [3:0] cmp_n [2];
logic [TW:0] cnt_n, drain_n;
sb_err_e aec_n;
logic mat_n, aer_n, eer_n, mis_n;
logic ret_n;
always_comb begin
for (i = 0; i < N_TAG; i++) begin
st_n[i] = st_r[i];
dat_n[i] = dat_r[i];
ep_n[i] = ep_r[i];
seq_n[i] = seq_r[i];
end
iss_n[0] = iss_seq_r[0]; iss_n[1] = iss_seq_r[1];
cmp_n[0] = cmp_seq_r[0]; cmp_n[1] = cmp_seq_r[1];
cnt_n = cnt_r;
drain_n = drain_r;
aec_n = E_NONE;
mat_n = 1'b0; aer_n = 1'b0; eer_n = 1'b0; mis_n = 1'b0;
ret_n = 1'b0;
if (eot) begin
// ---- THE DRAIN. Whatever is still outstanding was never completed.
//
// It is not "in flight" and it is not "inconclusive": the run is over.
// Chapter 24.2 makes the same point about assertion obligations, and
// the failure here is worse, because a missing completion means data
// the design accepted and never returned.
if (drain_r < (TW+1)'(N_TAG)) begin
if (st_r[drain_r[TW-1:0]] == T_OUTSTANDING) begin
st_n[drain_r[TW-1:0]] = T_EMPTY;
cnt_n = cnt_n - 1'b1;
mis_n = 1'b1;
cmp_n[ep_r[drain_r[TW-1:0]]] =
cmp_seq_r[ep_r[drain_r[TW-1:0]]] + 4'd1;
end else begin
// A RETIRED tag is returned to EMPTY silently. It is not a
// failure -- it completed -- and leaving it RETIRED for ever
// would mean each identity could be used exactly once.
st_n[drain_r[TW-1:0]] = T_EMPTY;
end
drain_n = drain_r + 1'b1;
end
end else begin
drain_n = '0;
// ---- THE COMPLETION CHANNEL ----
if (act_valid) begin
case (st_r[act_tag])
T_OUTSTANDING: begin
if (act_data !== dat_r[act_tag]) begin
// Identity found the partner; the payload is wrong. This is
// the check everybody writes, and it only works because the
// identity found the RIGHT partner first.
aer_n = 1'b1; aec_n = E_MISMATCH;
st_n[act_tag] = T_RETIRED;
cnt_n = cnt_n - 1'b1;
cmp_n[ep_r[act_tag]] = cmp_seq_r[ep_r[act_tag]] + 4'd1;
end else if (act_ep != ep_r[act_tag]) begin
// The tag came back on an endpoint it was not issued on. Not
// a payload error and not a duplicate: the routing is wrong.
aer_n = 1'b1; aec_n = E_UNEXPECTED;
st_n[act_tag] = T_RETIRED;
cnt_n = cnt_n - 1'b1;
cmp_n[ep_r[act_tag]] = cmp_seq_r[ep_r[act_tag]] + 4'd1;
end else if (act_retry) begin
// ---- A RETRY IS NOT A DELIVERY. ----
//
// The entry stays outstanding, nothing is retired, and no
// error is raised. Counting it lets the report distinguish
// "the bus is busy" from "the bus is broken".
ret_n = 1'b1;
end else if (seq_r[act_tag] != cmp_seq_r[act_ep]) begin
// ---- Out of order WITHIN an endpoint. ----
aer_n = 1'b1; aec_n = E_ORDER;
st_n[act_tag] = T_RETIRED;
cnt_n = cnt_n - 1'b1;
cmp_n[act_ep] = cmp_seq_r[act_ep] + 4'd1;
end else begin
mat_n = 1'b1;
st_n[act_tag] = T_RETIRED;
cnt_n = cnt_n - 1'b1;
cmp_n[act_ep] = cmp_seq_r[act_ep] + 4'd1;
end
end
T_RETIRED: begin
// ---- A SECOND completion for a tag that already completed.
// ---- This is the case a one-bit valid flag cannot name.
aer_n = 1'b1; aec_n = E_DUPLICATE;
end
default: begin
// Never issued. Something produced a completion out of nothing.
aer_n = 1'b1; aec_n = E_UNEXPECTED;
end
endcase
end
// ---- THE ISSUE CHANNEL ----
if (exp_valid) begin
if (st_r[exp_tag] == T_OUTSTANDING) begin
// ---- A tag reused while it is still outstanding. ----
//
// The identity space has been violated, and the consequence is
// that the earlier transfer becomes untrackable -- it can never be
// matched, because its identity now belongs to something else. A
// scoreboard that silently overwrites loses that transfer AND
// reports nothing, which is the worst of both.
eer_n = 1'b1;
end else begin
st_n[exp_tag] = T_OUTSTANDING;
dat_n[exp_tag] = exp_data;
ep_n[exp_tag] = exp_ep;
seq_n[exp_tag] = iss_seq_r[exp_ep];
iss_n[exp_ep] = iss_seq_r[exp_ep] + 4'd1;
// Only counted here if the completion channel did not already
// retire this same tag in this cycle -- which it cannot, because
// a tag that was OUTSTANDING is caught above.
cnt_n = cnt_n + 1'b1;
end
end
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (i = 0; i < N_TAG; i++) begin
st_r[i] <= T_EMPTY;
dat_r[i] <= '0;
ep_r[i] <= 1'b0;
seq_r[i] <= 4'd0;
end
iss_seq_r[0] <= 4'd0; iss_seq_r[1] <= 4'd0;
cmp_seq_r[0] <= 4'd0; cmp_seq_r[1] <= 4'd0;
cnt_r <= '0;
drain_r <= '0;
aec_r <= E_NONE;
mat_r <= 1'b0;
aer_r <= 1'b0;
eer_r <= 1'b0;
mis_r <= 1'b0;
n_issued <= 32'd0;
n_matched <= 32'd0;
n_retries <= 32'd0;
n_mismatch <= 32'd0;
n_unexpected <= 32'd0;
n_duplicate <= 32'd0;
n_reissue <= 32'd0;
n_missing <= 32'd0;
n_order <= 32'd0;
end else begin
for (i = 0; i < N_TAG; i++) begin
st_r[i] <= st_n[i];
dat_r[i] <= dat_n[i];
ep_r[i] <= ep_n[i];
seq_r[i] <= seq_n[i];
end
iss_seq_r[0] <= iss_n[0]; iss_seq_r[1] <= iss_n[1];
cmp_seq_r[0] <= cmp_n[0]; cmp_seq_r[1] <= cmp_n[1];
cnt_r <= cnt_n;
drain_r <= drain_n;
aec_r <= aec_n;
mat_r <= mat_n;
aer_r <= aer_n;
eer_r <= eer_n;
mis_r <= mis_n;
if (exp_valid && !eot && !eer_n) n_issued <= n_issued + 32'd1;
if (mat_n) n_matched <= n_matched + 32'd1;
if (ret_n) n_retries <= n_retries + 32'd1;
if (eer_n) n_reissue <= n_reissue + 32'd1;
if (mis_n) n_missing <= n_missing + 32'd1;
// The per-cause counters on the completion channel are driven by the
// SAME pulse as the channel's error, so they sum to it (chapter 23.4).
if (aer_n) begin
case (aec_n)
E_MISMATCH: n_mismatch <= n_mismatch + 32'd1;
E_UNEXPECTED: n_unexpected <= n_unexpected + 32'd1;
E_DUPLICATE: n_duplicate <= n_duplicate + 32'd1;
E_ORDER: n_order <= n_order + 32'd1;
default: ;
endcase
end
end
end
endmodule10. VHDL-2008 Implementation
-- usb_scoreboard -- matching what came back against what went out, and the
-- one decision that determines whether the scoreboard is worth having.
--
-- MATCH ON IDENTITY, NOT ON VALUE
--
-- The obvious scoreboard keeps a list of expected payloads, and when
-- something arrives it searches the list for a payload that matches.
--
-- That scoreboard cannot count.
--
-- issue tag 3 carrying 0x5A
-- issue tag 6 carrying 0x5A <-- the SAME bytes. Very common:
-- zero-length reports, keepalives,
-- status polls, a cleared buffer.
--
-- tag 3 completes ... twice. A DUPLICATE delivery.
-- tag 6 never completes. A LOST transfer.
--
-- A value-matching scoreboard sees two arrivals of 0x5A and two expectations
-- of 0x5A. Everything matches. Both entries are retired. The report is clean,
-- and two real bugs have cancelled each other out.
--
-- A DUPLICATE AND A LOSS ARE INDISTINGUISHABLE FROM A
-- PAIR OF CORRECT TRANSFERS IF YOU MATCH ON VALUE.
--
-- So every entry carries an IDENTITY -- here a tag, in a real environment a
-- transfer handle, a TRB pointer, a (endpoint, sequence) pair -- and the
-- value is checked AFTER the identity has found the pair. Identity finds the
-- partner; value checks the partner. Those are two different jobs and doing
-- them with one comparison does neither.
--
-- IT CANNOT BE A FIFO, BECAUSE COMPLETION IS OUT OF ORDER
--
-- Different endpoints complete independently: a bulk transfer queued first
-- can finish long after an interrupt transfer queued later. A scoreboard
-- built as a queue reports an order violation on every interleaving, which
-- is most of them.
--
-- But WITHIN an endpoint, order does hold, and that is worth checking --
-- because a controller that completes an endpoint's transfers out of order
-- has corrupted a stream that the host will not verify.
--
-- out of order ACROSS endpoints legal, and common
-- out of order WITHIN an endpoint a bug, and silent
--
-- AND EVERY DEPARTURE CONSUMES ITS PLACE IN THE ORDER
--
-- This is the part that is easy to get wrong and produces the worst symptom.
-- A transfer can leave the table four ways: matched, payload wrong, out of
-- order, or never returned at all. Only the first two look like "progress",
-- and the tempting implementation advances the endpoint's expected sequence
-- number only on a match.
--
-- Do that and ONE lost transfer leaves a permanent hole. The endpoint's
-- expected sequence number is stuck one behind for the rest of the run, so
-- EVERY subsequent completion on that endpoint is reported as out of order:
--
-- 1 missing transfer -> 1 missing + N order violations
--
-- and the report is then useless for finding the one that mattered. However
-- a transfer leaves, it is gone, and its place in the order goes with it.
--
-- A RETIRED TAG IS NOT AN EMPTY TAG
--
-- This is the distinction that makes a duplicate visible at all. When a
-- completion arrives for a tag that nothing is outstanding on, there are two
-- very different situations:
--
-- the tag was NEVER issued -> UNEXPECTED. Something invented a
-- completion out of nothing.
-- the tag was issued and has
-- ALREADY completed -> DUPLICATE. Something delivered twice.
--
-- A table with one valid bit cannot tell them apart and reports both as
-- "unexpected", which sends everybody looking in the wrong place. So each
-- tag has THREE states, and the third one exists purely so that the report
-- names the right bug.
--
-- ...AND AN IDENTITY IS REUSABLE, WHICH BOUNDS HOW LATE A DUPLICATE CAN BE
-- CAUGHT
--
-- A finite tag space only works because tags are recycled: a retired tag can
-- be issued again. That is legal and necessary, and it has a consequence
-- worth stating rather than discovering.
--
-- An identity must be unique among the transfers that can be in
-- flight AT THE SAME TIME -- not unique for the whole run.
--
-- So a duplicate delivery of the FIRST use of a tag, arriving after the tag
-- has been re-issued, is attributed to the second use. That is not a defect
-- in the scoreboard; it is the price of a finite identity space, and the way
-- to buy more margin is more tags, not cleverer bookkeeping.
--
-- A RETRY IS NOT A DUPLICATE
--
-- USB retries. A NAKed OUT is re-sent with the same data and the same
-- identity, and that is the protocol working. A scoreboard that treats the
-- re-send as a second delivery reports a duplicate on every flow-controlled
-- transfer -- and per chapter 24.1, a scoreboard that cries wolf is a
-- scoreboard somebody switches off.
--
-- So a completion flagged as a retry is counted and does NOT retire the
-- entry. Only the final delivery retires it.
--
-- ONE EVENT PER CHANNEL, NOT ONE PER CYCLE
--
-- An issue and a completion can happen in the same cycle and can BOTH be
-- wrong, so a single reporting slot would have to drop one. Each channel
-- gets its own pulse instead: the completion channel, the issue channel, and
-- the drain. Every pulse is then single-valued, and the counters -- which
-- can advance twice in a cycle -- are the source of truth.
library ieee;
use ieee.std_logic_1164.all;
package usb_sb_pkg is
-- Three states per tag, and the third one is the whole point: it is what
-- lets the report say DUPLICATE where a one-bit valid flag can only say
-- "unexpected", which sends everybody looking in the wrong place.
type tag_state_t is (T_EMPTY, T_OUTSTANDING, T_RETIRED);
type sb_err_t is (E_NONE, E_MISMATCH, E_UNEXPECTED, E_DUPLICATE, E_ORDER);
function ts_code (s : tag_state_t) return std_logic_vector;
function se_code (e : sb_err_t) return std_logic_vector;
end package usb_sb_pkg;
package body usb_sb_pkg is
-- Written out rather than derived from position, so the encodings are
-- pinned to the same numbers the Verilog and SystemVerilog use.
function ts_code (s : tag_state_t) return std_logic_vector is
begin
case s is
when T_EMPTY => return "00";
when T_OUTSTANDING => return "01";
when T_RETIRED => return "10";
end case;
end function;
function se_code (e : sb_err_t) return std_logic_vector is
begin
case e is
when E_NONE => return "000";
when E_MISMATCH => return "001";
when E_UNEXPECTED => return "010";
when E_DUPLICATE => return "011";
when E_ORDER => return "100";
end case;
end function;
end package body usb_sb_pkg;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_sb_pkg.all;
entity usb_scoreboard is
generic (
N_TAG : integer := 8; -- identities trackable at once
TW : integer := 3; -- tag width: 2**TW = N_TAG
DW : integer := 8 -- payload width
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- ---- the issue channel: a transfer was handed to the design ----
exp_valid : in std_logic;
exp_tag : in std_logic_vector(TW-1 downto 0);
exp_data : in std_logic_vector(DW-1 downto 0);
exp_ep : in std_logic;
-- ---- the completion channel: something came back ----
act_valid : in std_logic;
act_tag : in std_logic_vector(TW-1 downto 0);
act_data : in std_logic_vector(DW-1 downto 0);
act_ep : in std_logic;
act_retry : in std_logic; -- a re-send, not a result
eot : in std_logic; -- end of test: drain and report
outstanding : out std_logic_vector(TW downto 0);
tag_state : out std_logic_vector(1 downto 0);
match_pulse : out std_logic;
act_err_pulse : out std_logic;
act_err_code : out std_logic_vector(2 downto 0);
exp_err_pulse : out std_logic; -- only one cause: a reissued tag
miss_pulse : out std_logic; -- one per survivor of the drain
n_issued : out std_logic_vector(31 downto 0);
n_matched : out std_logic_vector(31 downto 0);
n_retries : out std_logic_vector(31 downto 0);
n_mismatch : out std_logic_vector(31 downto 0);
n_unexpected : out std_logic_vector(31 downto 0);
n_duplicate : out std_logic_vector(31 downto 0);
n_reissue : out std_logic_vector(31 downto 0);
n_missing : out std_logic_vector(31 downto 0);
n_order : out std_logic_vector(31 downto 0)
);
end entity usb_scoreboard;
architecture rtl of usb_scoreboard is
type st_arr is array (0 to N_TAG-1) of tag_state_t;
type dat_arr is array (0 to N_TAG-1) of std_logic_vector(DW-1 downto 0);
type seq_arr is array (0 to N_TAG-1) of unsigned(3 downto 0);
type ep_arr is array (0 to N_TAG-1) of std_logic;
type eps_arr is array (0 to 1) of unsigned(3 downto 0);
signal st_r : st_arr := (others => T_EMPTY);
signal dat_r : dat_arr := (others => (others => '0'));
signal ep_r : ep_arr := (others => '0');
signal seq_r : seq_arr := (others => (others => '0'));
-- Per-endpoint sequence numbers. Issue order is recorded on the way in and
-- checked on the way out, which is what makes "in order within an
-- endpoint" a property rather than an aspiration.
signal iss_seq_r : eps_arr := (others => (others => '0'));
signal cmp_seq_r : eps_arr := (others => (others => '0'));
signal cnt_r : unsigned(TW downto 0) := (others => '0');
signal aec_r : sb_err_t := E_NONE;
signal mat_r, aer_r, eer_r, mis_r : std_logic := '0';
-- The drain walks the table in tag order. A pointer rather than a search,
-- so the drain is bounded and reports the survivors in a stable order.
signal drain_r : unsigned(TW downto 0) := (others => '0');
-- Accumulators are held as unsigned rather than as range-constrained
-- integers: a constrained integer aborts simulation on overflow, which
-- turns a mutation into a crash instead of a measured kill.
signal c_i, c_m, c_rt, c_mm : unsigned(31 downto 0) := (others => '0');
signal c_un, c_dp, c_ri, c_ms, c_or : unsigned(31 downto 0) := (others => '0');
begin
outstanding <= std_logic_vector(cnt_r);
tag_state <= ts_code(st_r(to_integer(unsigned(act_tag))));
match_pulse <= mat_r;
act_err_pulse <= aer_r;
act_err_code <= se_code(aec_r);
exp_err_pulse <= eer_r;
miss_pulse <= mis_r;
n_issued <= std_logic_vector(c_i);
n_matched <= std_logic_vector(c_m);
n_retries <= std_logic_vector(c_rt);
n_mismatch <= std_logic_vector(c_mm);
n_unexpected <= std_logic_vector(c_un);
n_duplicate <= std_logic_vector(c_dp);
n_reissue <= std_logic_vector(c_ri);
n_missing <= std_logic_vector(c_ms);
n_order <= std_logic_vector(c_or);
process (clk, rst_n)
variable at_i, et_i, dr_i : integer;
variable sat, set_st : tag_state_t;
variable w_at : tag_state_t;
variable w_at_en, w_et_en : boolean;
variable ncnt, ndrn : unsigned(TW downto 0);
variable niss, ncmp : eps_arr;
variable naec : sb_err_t;
variable nmat, naer, neer, nmis, nret : std_logic;
variable aep_i, eep_i : integer;
begin
if rst_n = '0' then
st_r <= (others => T_EMPTY);
dat_r <= (others => (others => '0'));
ep_r <= (others => '0');
seq_r <= (others => (others => '0'));
iss_seq_r <= (others => (others => '0'));
cmp_seq_r <= (others => (others => '0'));
cnt_r <= (others => '0');
drain_r <= (others => '0');
aec_r <= E_NONE;
mat_r <= '0'; aer_r <= '0'; eer_r <= '0'; mis_r <= '0';
c_i <= (others => '0'); c_m <= (others => '0');
c_rt <= (others => '0'); c_mm <= (others => '0');
c_un <= (others => '0'); c_dp <= (others => '0');
c_ri <= (others => '0'); c_ms <= (others => '0');
c_or <= (others => '0');
elsif rising_edge(clk) then
at_i := to_integer(unsigned(act_tag));
et_i := to_integer(unsigned(exp_tag));
aep_i := 0; if act_ep = '1' then aep_i := 1; end if;
eep_i := 0; if exp_ep = '1' then eep_i := 1; end if;
-- BOTH channels read the table as it stands at the start of the cycle.
sat := st_r(at_i);
set_st := st_r(et_i);
w_at_en := false; w_et_en := false; w_at := T_EMPTY;
ncnt := cnt_r; ndrn := drain_r; naec := E_NONE;
niss := iss_seq_r; ncmp := cmp_seq_r;
nmat := '0'; naer := '0'; neer := '0'; nmis := '0'; nret := '0';
if eot = '1' then
-- ---- THE DRAIN. Whatever is still outstanding was never completed.
--
-- It is not "in flight" and it is not "inconclusive": the run is
-- over. Chapter 24.2 makes the same point about assertion
-- obligations, and the failure here is worse, because a missing
-- completion means data the design accepted and never returned.
if drain_r < to_unsigned(N_TAG, TW+1) then
dr_i := to_integer(drain_r);
if st_r(dr_i) = T_OUTSTANDING then
st_r(dr_i) <= T_EMPTY;
ncnt := ncnt - 1;
nmis := '1';
if ep_r(dr_i) = '1' then ncmp(1) := cmp_seq_r(1) + 1;
else ncmp(0) := cmp_seq_r(0) + 1;
end if;
else
-- A RETIRED tag is returned to EMPTY silently. It is not a
-- failure -- it completed -- and leaving it RETIRED for ever
-- would mean each identity could be used exactly once.
st_r(dr_i) <= T_EMPTY;
end if;
ndrn := drain_r + 1;
end if;
else
ndrn := (others => '0');
-- ---- THE COMPLETION CHANNEL ----
if act_valid = '1' then
case sat is
when T_OUTSTANDING =>
if act_data /= dat_r(at_i) then
-- Identity found the partner; the payload is wrong. This is
-- the check everybody writes, and it only works because the
-- identity found the RIGHT partner first.
naer := '1'; naec := E_MISMATCH;
w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
if ep_r(at_i) = '1' then ncmp(1) := cmp_seq_r(1) + 1;
else ncmp(0) := cmp_seq_r(0) + 1;
end if;
elsif act_ep /= ep_r(at_i) then
-- The tag came back on an endpoint it was not issued on.
-- Not a payload error and not a duplicate: the routing is
-- wrong.
naer := '1'; naec := E_UNEXPECTED;
w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
if ep_r(at_i) = '1' then ncmp(1) := cmp_seq_r(1) + 1;
else ncmp(0) := cmp_seq_r(0) + 1;
end if;
elsif act_retry = '1' then
-- ---- A RETRY IS NOT A DELIVERY. ----
--
-- The entry stays outstanding, nothing is retired, and no
-- error is raised. Counting it lets the report distinguish
-- "the bus is busy" from "the bus is broken".
nret := '1';
elsif seq_r(at_i) /= cmp_seq_r(aep_i) then
-- ---- Out of order WITHIN an endpoint. ----
naer := '1'; naec := E_ORDER;
w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
ncmp(aep_i) := cmp_seq_r(aep_i) + 1;
else
nmat := '1';
w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
ncmp(aep_i) := cmp_seq_r(aep_i) + 1;
end if;
when T_RETIRED =>
-- ---- A SECOND completion for a tag that already completed.
-- ---- This is the case a one-bit valid flag cannot name.
naer := '1'; naec := E_DUPLICATE;
when others =>
-- Never issued. Something produced a completion out of
-- nothing.
naer := '1'; naec := E_UNEXPECTED;
end case;
end if;
-- ---- THE ISSUE CHANNEL ----
if exp_valid = '1' then
if set_st = T_OUTSTANDING then
-- ---- A tag reused while it is still outstanding. ----
--
-- The identity space has been violated, and the consequence is
-- that the earlier transfer becomes untrackable -- it can never
-- be matched, because its identity now belongs to something
-- else. A scoreboard that silently overwrites loses that
-- transfer AND reports nothing, which is the worst of both.
neer := '1';
else
w_et_en := true;
ncnt := ncnt + 1;
end if;
end if;
-- Applied in the design's order: the completion channel's write
-- first, the issue channel's second, so an issue to the same tag
-- wins -- which it can only do if that tag was not outstanding.
if w_at_en then st_r(at_i) <= w_at; end if;
if w_et_en then
st_r(et_i) <= T_OUTSTANDING;
dat_r(et_i) <= exp_data;
ep_r(et_i) <= exp_ep;
seq_r(et_i) <= iss_seq_r(eep_i);
niss(eep_i) := iss_seq_r(eep_i) + 1;
end if;
end if;
cnt_r <= ncnt;
drain_r <= ndrn;
aec_r <= naec;
mat_r <= nmat;
aer_r <= naer;
eer_r <= neer;
mis_r <= nmis;
iss_seq_r <= niss;
cmp_seq_r <= ncmp;
if exp_valid = '1' and eot = '0' and neer = '0' then c_i <= c_i + 1; end if;
if nmat = '1' then c_m <= c_m + 1; end if;
if nret = '1' then c_rt <= c_rt + 1; end if;
if neer = '1' then c_ri <= c_ri + 1; end if;
if nmis = '1' then c_ms <= c_ms + 1; end if;
-- The per-cause counters on the completion channel are driven by the
-- SAME pulse as the channel's error, so they sum to it (chapter 23.4).
if naer = '1' then
case naec is
when E_MISMATCH => c_mm <= c_mm + 1;
when E_UNEXPECTED => c_un <= c_un + 1;
when E_DUPLICATE => c_dp <= c_dp + 1;
when E_ORDER => c_or <= c_or + 1;
when others => null;
end case;
end if;
end if;
end process;
end architecture rtl;11. Both Channels Read the Table as It Was
The completion channel retires an entry and the issue channel records one, and they can name the same tag in the same cycle. The design reads its registered table in both, so both see the state as it stood at the start of the cycle.
The first shadow model did not: it mutated its table inside the completion branch and then read it in the issue branch.
12. Seeing the Identity Test
Two transfers with identical payloads: one delivered twice, one never
usb_scoreboard — a duplicate and a loss, which a value-matcher cancels
10 cycles13. The Testbenches
Two exhaustive sweeps — every tag state crossed with all 16 input combinations (48 pairs), and every table occupancy 0 to 8 — plus the phase the chapter exists for:
// ---- Phase C: THE IDENTITY TEST. Two transfers, SAME payload; one
// ---- delivered twice, the other never.
for (k = 0; k < 40; k = k + 1) begin
b_m = n_matched;
issue(3'd3, 8'h5A, 1'b0);
issue(3'd6, 8'h5A, 1'b0); // the SAME bytes
complete(3'd3, 8'h5A, 1'b0, 1'b0); // match
b_e = n_duplicate;
complete(3'd3, 8'h5A, 1'b0, 1'b0); // DELIVERED TWICE
check(n_duplicate == b_e + 1,
"a second delivery of the same identity was not reported as a duplicate -- a scoreboard that matches on payload retires the OTHER transfer instead and both bugs vanish");
b_e = n_missing;
drain; // tag 6 never completed
check(n_missing == b_e + 1,
"a transfer that never completed was not reported as missing");
check(n_matched == b_m + 1,
"more matches were recorded than transfers actually completed");
end...and its false-positive counterpart, which is why the scoreboard cannot be a queue:
issue(3'd0, 8'h70, 1'b0); // endpoint 0 first
issue(3'd1, 8'h71, 1'b1); // endpoint 1 second
complete(3'd1, 8'h71, 1'b1, 1'b0); // ...and it finishes FIRST
complete(3'd0, 8'h70, 1'b0, 1'b0);
check(n_order + n_unexpected + n_mismatch == b_e,
"legal cross-endpoint reordering was reported as an error -- a scoreboard built as a queue flags every interleaving, which is most of them");And one check that knows nothing about any particular bug:
// ---- CONSERVATION. Every transfer that was successfully issued left
// ---- the table exactly once: matched, reported wrong, reported out of
// ---- order, or reported missing at the drain. A scoreboard whose
// ---- inputs and outputs do not balance has lost track of something,
// ---- and this is the one check that notices without knowing WHICH.
check(n_issued === n_matched + n_mismatch + n_order + n_missing + n_xdiv,
"the transfers that left the table do not account for the transfers that entered it -- something was lost without being reported");13.1 Verilog testbench
// Testbench for usb_scoreboard (Verilog-2005).
//
// THE TEST THAT DEFINES THE CHAPTER
//
// Two transfers carrying the SAME PAYLOAD. One of them is delivered twice;
// the other is never delivered at all.
//
// issue tag 3, 0x5A issue tag 6, 0x5A
// complete tag 3 complete tag 3 AGAIN
// ...and tag 6 never completes.
//
// an identity-matching scoreboard 1 match, 1 DUPLICATE, 1 MISSING
// a value-matching scoreboard 2 matches, 0 errors
//
// The second one is not merely weaker. It is wrong in a way that cannot be
// fixed by running longer or by adding payloads, because the two bugs
// ANNIHILATE each other in its bookkeeping. That is mutation S1, and the
// three-line phase below is the whole argument for tagging.
//
// AND THE FALSE-POSITIVE SIDE
//
// Completion out of order ACROSS endpoints is legal and constant -- a bulk
// transfer queued first finishes after an interrupt transfer queued later.
// The suite drives that deliberately and requires ZERO errors, because a
// scoreboard that flags it is a scoreboard nobody leaves enabled.
//
// WHAT IS EXHAUSTIVE HERE
//
// 1. Every tag state (EMPTY / OUTSTANDING / RETIRED) crossed with all 16
// combinations of {exp_valid, act_valid, act_retry, eot} = 48 pairs.
// 2. Every table occupancy from 0 to N_TAG = 9, each reached by real
// issues.
`timescale 1ns/1ps
module tb_sb_v;
localparam integer N_TAG = 8;
localparam integer TW = 3;
localparam integer DW = 8;
localparam [1:0] T_EMPTY=2'd0, T_OUTSTANDING=2'd1, T_RETIRED=2'd2;
localparam [2:0] E_NONE=3'd0, E_MISMATCH=3'd1, E_UNEXPECTED=3'd2,
E_DUPLICATE=3'd3, E_ORDER=3'd4;
reg clk = 1'b0, rst_n = 1'b0;
reg exp_valid = 1'b0, act_valid = 1'b0, act_retry = 1'b0, eot = 1'b0;
reg exp_ep = 1'b0, act_ep = 1'b0;
reg [TW-1:0] exp_tag = 3'd0, act_tag = 3'd0;
reg [DW-1:0] exp_data = 8'd0, act_data = 8'd0;
wire [TW:0] outstanding;
wire [1:0] tag_state;
wire [2:0] act_err_code;
wire match_pulse, act_err_pulse, exp_err_pulse, miss_pulse;
wire [31:0] n_issued, n_matched, n_retries, n_mismatch, n_unexpected,
n_duplicate, n_reissue, n_missing, n_order;
usb_scoreboard #(.N_TAG(N_TAG), .TW(TW), .DW(DW)) dut (
.clk(clk), .rst_n(rst_n),
.exp_valid(exp_valid), .exp_tag(exp_tag), .exp_data(exp_data), .exp_ep(exp_ep),
.act_valid(act_valid), .act_tag(act_tag), .act_data(act_data),
.act_ep(act_ep), .act_retry(act_retry), .eot(eot),
.outstanding(outstanding), .tag_state(tag_state),
.match_pulse(match_pulse), .act_err_pulse(act_err_pulse),
.act_err_code(act_err_code), .exp_err_pulse(exp_err_pulse),
.miss_pulse(miss_pulse),
.n_issued(n_issued), .n_matched(n_matched), .n_retries(n_retries),
.n_mismatch(n_mismatch), .n_unexpected(n_unexpected),
.n_duplicate(n_duplicate), .n_reissue(n_reissue),
.n_missing(n_missing), .n_order(n_order)
);
always #5 clk = ~clk;
integer errors = 0, checks = 0;
task check(input cond, input [1023:0] msg);
begin
checks = checks + 1;
if (!cond) begin
errors = errors + 1;
if (errors <= 25)
$display("FAIL @%0t: %0s | out=%0d tst=%0d m=%b ae=%b(%0d) ee=%b ms=%b",
$time, msg, outstanding, tag_state, match_pulse,
act_err_pulse, act_err_code, exp_err_pulse, miss_pulse);
end
end
endtask
// ------------------------------------------------------------------
// The shadow scoreboard. Its own table, its own sequence numbers.
// ------------------------------------------------------------------
reg [1:0] m_st [0:N_TAG-1];
reg [DW-1:0] m_dat [0:N_TAG-1];
reg m_ep [0:N_TAG-1];
reg [3:0] m_seq [0:N_TAG-1];
reg [3:0] m_iss [0:1];
reg [3:0] m_cmp [0:1];
reg [TW:0] m_cnt, m_drain;
reg [2:0] m_aec;
reg m_mat, m_aer, m_eer, m_mis;
integer m_i, m_m, m_rt, m_mm, m_un, m_dp, m_ri, m_ms, m_or;
integer n_xdiv; // completions retired by a wrong-endpoint error
integer seen_ts [0:47]; // 3 tag states x 16 input combinations
integer seen_oc [0:8]; // occupancy 0..N_TAG
integer n_ts, n_oc, n_steps;
task model_reset;
integer j;
begin
for (j = 0; j < N_TAG; j = j + 1) begin
m_st[j] = T_EMPTY; m_dat[j] = 8'd0; m_ep[j] = 1'b0; m_seq[j] = 4'd0;
end
m_iss[0] = 4'd0; m_iss[1] = 4'd0;
m_cmp[0] = 4'd0; m_cmp[1] = 4'd0;
m_cnt = 4'd0; m_drain = 4'd0; m_aec = E_NONE;
m_mat = 1'b0; m_aer = 1'b0; m_eer = 1'b0; m_mis = 1'b0;
m_i = 0; m_m = 0; m_rt = 0; m_mm = 0; m_un = 0;
m_dp = 0; m_ri = 0; m_ms = 0; m_or = 0;
for (j = 0; j < 48; j = j + 1) seen_ts[j] = 0;
for (j = 0; j < 9; j = j + 1) seen_oc[j] = 0;
n_ts = 0; n_oc = 0; n_steps = 0; n_xdiv = 0;
end
endtask
integer idx, scan;
task step(input ev, input [TW-1:0] et, input [DW-1:0] ed, input eep,
input av, input [TW-1:0] at, input [DW-1:0] ad, input aep,
input ar, input eo);
reg [1:0] nst, sat, set_, w_at;
reg w_at_en, w_et_en;
reg [3:0] ncmp0, ncmp1, niss0, niss1;
reg [TW:0] ncnt, ndrn;
reg [2:0] naec;
reg nmat, naer, neer, nmis, nret;
begin
exp_valid = ev; exp_tag = et; exp_data = ed; exp_ep = eep;
act_valid = av; act_tag = at; act_data = ad; act_ep = aep;
act_retry = ar; eot = eo;
#1;
check(outstanding === m_cnt, "outstanding disagrees with the shadow scoreboard");
check(tag_state === m_st[at], "tag_state disagrees -- the three-state table is the thing that names a duplicate");
check(match_pulse === m_mat, "the match pulse disagrees");
check(act_err_pulse === m_aer, "the completion-channel error pulse disagrees");
check(act_err_code === m_aec, "act_err_code disagrees");
check(exp_err_pulse === m_eer, "the issue-channel error pulse disagrees");
check(miss_pulse === m_mis, "the drain pulse disagrees");
check(!(match_pulse && act_err_pulse),
"a completion was reported as both a match and an error");
check(outstanding <= N_TAG[TW:0],
"more transfers are outstanding than there are identities");
// ---- the occupancy must equal the number of OUTSTANDING entries.
// ---- A count kept separately from the table can drift from it, and
// ---- then neither can be trusted.
idx = 0;
for (scan = 0; scan < N_TAG; scan = scan + 1)
if (m_st[scan] == T_OUTSTANDING) idx = idx + 1;
check(m_cnt === idx[TW:0],
"the occupancy counter disagrees with the table it is supposed to summarise");
idx = m_st[at] * 16 + (ev ? 8 : 0) + (av ? 4 : 0) + (ar ? 2 : 0) + (eo ? 1 : 0);
if (idx < 48) begin
if (seen_ts[idx] == 0) begin seen_ts[idx] = 1; n_ts = n_ts + 1; end
end
if (seen_oc[m_cnt] == 0) begin seen_oc[m_cnt] = 1; n_oc = n_oc + 1; end
n_steps = n_steps + 1;
// ---- advance the shadow scoreboard ----
ncnt = m_cnt; ndrn = m_drain; naec = E_NONE;
nmat = 1'b0; naer = 1'b0; neer = 1'b0; nmis = 1'b0; nret = 1'b0;
niss0 = m_iss[0]; niss1 = m_iss[1];
ncmp0 = m_cmp[0]; ncmp1 = m_cmp[1];
// BOTH channels read the state as it stands at the START of the
// cycle, exactly as the design reads its registered table. Mutating
// the model's table inside the completion branch and then reading it
// in the issue branch is the classic read-after-write model bug: it
// diverges only when the two channels name the SAME tag in the same
// cycle, which random stimulus finds and directed stimulus does not.
sat = m_st[at];
set_ = m_st[et];
w_at_en = 1'b0; w_et_en = 1'b0; w_at = T_EMPTY;
if (eo) begin
if (m_drain < N_TAG[TW:0]) begin
if (m_st[m_drain[TW-1:0]] == T_OUTSTANDING) begin
m_st[m_drain[TW-1:0]] = T_EMPTY;
ncnt = ncnt - 1'b1;
nmis = 1'b1;
if (m_ep[m_drain[TW-1:0]]) ncmp1 = m_cmp[1] + 4'd1;
else ncmp0 = m_cmp[0] + 4'd1;
end else begin
m_st[m_drain[TW-1:0]] = T_EMPTY;
end
ndrn = m_drain + 1'b1;
end
end else begin
ndrn = {(TW+1){1'b0}};
if (av) begin
nst = sat;
if (nst == T_OUTSTANDING) begin
if (ad !== m_dat[at]) begin
naer = 1'b1; naec = E_MISMATCH;
w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
if (m_ep[at]) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
end else if (aep != m_ep[at]) begin
naer = 1'b1; naec = E_UNEXPECTED;
w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
if (m_ep[at]) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
// Retired by a wrong-endpoint completion: it left the table,
// so the conservation check at the end has to know about it.
n_xdiv = n_xdiv + 1;
end else if (ar) begin
nret = 1'b1;
end else if (m_seq[at] != m_cmp[aep]) begin
naer = 1'b1; naec = E_ORDER;
w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
if (aep) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
end else begin
nmat = 1'b1;
w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
if (aep) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
end
end else if (nst == T_RETIRED) begin
naer = 1'b1; naec = E_DUPLICATE;
end else begin
naer = 1'b1; naec = E_UNEXPECTED;
end
end
if (ev) begin
if (set_ == T_OUTSTANDING) begin
neer = 1'b1;
end else begin
w_et_en = 1'b1;
ncnt = ncnt + 1'b1;
end
end
// Applied in the design's order: the completion channel's write
// first, the issue channel's second, so an issue to the same tag
// wins -- which it can only do if that tag was not outstanding.
if (w_at_en) m_st[at] = w_at;
if (w_et_en) begin
m_st[et] = T_OUTSTANDING;
m_dat[et] = ed;
m_ep[et] = eep;
m_seq[et] = m_iss[eep];
if (eep) niss1 = m_iss[1] + 4'd1; else niss0 = m_iss[0] + 4'd1;
end
end
m_cnt = ncnt; m_drain = ndrn; m_aec = naec;
m_mat = nmat; m_aer = naer; m_eer = neer; m_mis = nmis;
m_iss[0] = niss0; m_iss[1] = niss1;
m_cmp[0] = ncmp0; m_cmp[1] = ncmp1;
if (ev && !eo && !neer) m_i = m_i + 1;
if (nmat) m_m = m_m + 1;
if (nret) m_rt = m_rt + 1;
if (neer) m_ri = m_ri + 1;
if (nmis) m_ms = m_ms + 1;
if (naer) begin
case (naec)
E_MISMATCH: m_mm = m_mm + 1;
E_UNEXPECTED: m_un = m_un + 1;
E_DUPLICATE: m_dp = m_dp + 1;
E_ORDER: m_or = m_or + 1;
default: ;
endcase
end
@(posedge clk); #1;
exp_valid = 1'b0; act_valid = 1'b0; act_retry = 1'b0; eot = 1'b0;
end
endtask
task nop(input integer n);
integer j;
begin
for (j = 0; j < n; j = j + 1)
step(1'b0,3'd0,8'd0,1'b0, 1'b0,3'd0,8'd0,1'b0, 1'b0,1'b0);
end
endtask
task issue(input [TW-1:0] t, input [DW-1:0] d, input e);
begin step(1'b1,t,d,e, 1'b0,3'd0,8'd0,1'b0, 1'b0,1'b0); end
endtask
task complete(input [TW-1:0] t, input [DW-1:0] d, input e, input r);
begin step(1'b0,3'd0,8'd0,1'b0, 1'b1,t,d,e, r,1'b0); end
endtask
// Drain the table the way the design provides for. The drain visits one
// tag per cycle, so it takes N_TAG cycles -- a bounded walk, not a search.
task drain;
integer j;
begin
for (j = 0; j < N_TAG + 2; j = j + 1)
step(1'b0,3'd0,8'd0,1'b0, 1'b0,3'd0,8'd0,1'b0, 1'b0,1'b1);
check(outstanding === 4'd0, "the drain did not empty the table");
nop(1);
end
endtask
// A clean scoreboard: every tag EMPTY, both sequence spaces aligned.
// A pristine table, reached the way the design provides for: the drain
// visits every tag, reports the outstanding ones and returns the retired
// ones to EMPTY. Nothing is forced.
task fresh;
integer j;
begin
drain;
for (j = 0; j < N_TAG; j = j + 1)
check(m_st[j] === T_EMPTY,
"the drain did not return every tag to EMPTY -- a retired identity that stays retired can be used only once");
end
endtask
integer k, j2, b_m, b_e, ts, cb, oc;
initial begin
model_reset;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
// ---- Phase A: the state after reset ----
check(outstanding === 4'd0, "reset left transfers outstanding");
check(match_pulse === 1'b0, "reset asserted a match");
check(act_err_pulse === 1'b0, "reset asserted a completion error");
check(n_matched === 32'd0, "reset left the match counter non-zero");
// ---- Phase B: clean traffic, in order, on both endpoints. ZERO errors.
for (k = 0; k < 60; k = k + 1) begin
b_m = n_matched;
b_e = n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order;
issue(3'd0, 8'h11, 1'b0);
issue(3'd1, 8'h22, 1'b0);
complete(3'd0, 8'h11, 1'b0, 1'b0);
complete(3'd1, 8'h22, 1'b0, 1'b0);
check(n_matched == b_m + 2, "clean in-order traffic was not matched");
check(n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order == b_e,
"clean in-order traffic produced an error");
drain;
end
// ---- Phase C: THE IDENTITY TEST. Two transfers, SAME payload; one
// ---- delivered twice, the other never.
for (k = 0; k < 40; k = k + 1) begin
b_m = n_matched;
issue(3'd3, 8'h5A, 1'b0);
issue(3'd6, 8'h5A, 1'b0); // the SAME bytes
complete(3'd3, 8'h5A, 1'b0, 1'b0); // match
b_e = n_duplicate;
complete(3'd3, 8'h5A, 1'b0, 1'b0); // DELIVERED TWICE
check(n_duplicate == b_e + 1,
"a second delivery of the same identity was not reported as a duplicate -- a scoreboard that matches on payload retires the OTHER transfer instead and both bugs vanish");
b_e = n_missing;
drain; // tag 6 never completed
check(n_missing == b_e + 1,
"a transfer that never completed was not reported as missing");
check(n_matched == b_m + 1,
"more matches were recorded than transfers actually completed");
end
// ---- Phase D: identity matched, payload wrong. Repeated across every
// ---- single-bit corruption, so the comparison is checked on each bit
// ---- rather than on one arbitrary pair of values.
for (k = 0; k < DW; k = k + 1) begin
b_e = n_mismatch;
issue(3'd2, 8'hA5, 1'b0);
complete(3'd2, 8'hA5 ^ (8'd1 << k), 1'b0, 1'b0);
check(n_mismatch == b_e + 1,
"a wrong payload on a matched identity was accepted -- a comparison that covers only part of the payload passes most corruptions");
drain;
end
// ---- Phase E: a completion for a tag that was never issued. ----
fresh;
b_e = n_unexpected;
complete(3'd7, 8'h33, 1'b0, 1'b0);
check(n_unexpected == b_e + 1,
"a completion for a never-issued identity was accepted");
// ---- Phase F: a tag reused while still outstanding. ----
fresh;
b_e = n_reissue;
issue(3'd4, 8'h44, 1'b0);
issue(3'd4, 8'h55, 1'b0);
check(n_reissue == b_e + 1,
"an identity was reused while still outstanding and the first transfer was silently overwritten");
check(outstanding === 4'd1,
"the reissued transfer displaced the one already being tracked");
drain;
// ---- Phase G: out of order WITHIN an endpoint is a bug. ----
for (k = 0; k < 40; k = k + 1) begin
fresh;
b_e = n_order;
issue(3'd0, 8'h60, 1'b0);
issue(3'd1, 8'h61, 1'b0);
complete(3'd1, 8'h61, 1'b0, 1'b0); // the SECOND one first
check(n_order == b_e + 1,
"completion out of order within one endpoint was accepted -- the stream is corrupted and the host will not check it");
drain;
end
// ---- Phase H: out of order ACROSS endpoints is LEGAL. ----
// ---- This is the false-positive check, and it is the reason the
// ---- scoreboard cannot be a queue.
fresh;
for (k = 0; k < 40; k = k + 1) begin
b_m = n_matched;
b_e = n_order + n_unexpected + n_mismatch;
issue(3'd0, 8'h70, 1'b0); // endpoint 0 first
issue(3'd1, 8'h71, 1'b1); // endpoint 1 second
complete(3'd1, 8'h71, 1'b1, 1'b0); // ...and it finishes FIRST
complete(3'd0, 8'h70, 1'b0, 1'b0);
check(n_matched == b_m + 2,
"legal cross-endpoint reordering was not matched");
check(n_order + n_unexpected + n_mismatch == b_e,
"legal cross-endpoint reordering was reported as an error -- a scoreboard built as a queue flags every interleaving, which is most of them");
drain;
end
// ---- Phase I: a RETRY is not a delivery. ----
fresh;
for (k = 1; k <= 5; k = k + 1) begin
b_m = n_matched;
b_e = n_duplicate + n_order;
issue(3'd5, 8'h80, 1'b0);
for (j2 = 0; j2 < k; j2 = j2 + 1)
complete(3'd5, 8'h80, 1'b0, 1'b1); // k re-sends
check(outstanding === 4'd1,
"a retry retired the transfer -- the real delivery will then look like a duplicate");
complete(3'd5, 8'h80, 1'b0, 1'b0); // the real delivery
check(n_matched == b_m + 1, "the delivery after a run of retries was not matched");
check(n_duplicate + n_order == b_e,
"a retry was reported as a duplicate -- every flow-controlled transfer on a busy bus would be");
drain;
end
// ---- Phase J: THE DRAIN. Data the design accepted and never returned.
fresh;
for (oc = 1; oc <= N_TAG; oc = oc + 1) begin
b_e = n_missing;
b_m = n_matched;
for (k = 0; k < oc; k = k + 1) issue(k[TW-1:0], 8'h90 + k[DW-1:0], 1'b0);
check(outstanding === oc[TW:0], "the table did not accept the issues offered to it");
drain;
check(n_missing == b_e + oc,
"transfers still outstanding at end of test were not reported -- the run is over and nothing more is coming");
check(n_matched == b_m, "a transfer that never came back was counted as a match");
fresh;
end
// ---- Phase K: EXHAUSTIVE. Every tag state x every input combination. --
for (ts = 0; ts < 3; ts = ts + 1) begin
for (cb = 0; cb < 16; cb = cb + 1) begin
fresh;
// put tag 2 into the state under test, using the design's own means
case (ts)
0: ; // EMPTY
1: issue(3'd2, 8'hC0, 1'b0); // OUTSTANDING
2: begin issue(3'd2, 8'hC0, 1'b0);
complete(3'd2, 8'hC0, 1'b0, 1'b0); end // RETIRED
endcase
check(m_st[2] === ts[1:0],
"the sweep could not reach the tag state it meant to reach");
step(cb[3],3'd2,8'hC0,1'b0, cb[2],3'd2,8'hC0,1'b0, cb[1],cb[0]);
step(cb[3],3'd2,8'hC0,1'b0, cb[2],3'd2,8'hC0,1'b0, cb[1],cb[0]);
end
end
// ---- Phase L: random ----
for (k = 0; k < 34000; k = k + 1)
step(($unsigned($random) % 100) < 30, $random, $random, $random,
($unsigned($random) % 100) < 30, $random, $random, $random,
($unsigned($random) % 100) < 20,
($unsigned($random) % 1000) < 7);
// ---- Phase M: and clean traffic afterwards, so the scoreboard is shown
// ---- to still work rather than merely to have stopped.
fresh;
b_m = n_matched;
b_e = n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order;
for (k = 0; k < 120; k = k + 1) begin
issue(3'd0, 8'hE0, 1'b0);
issue(3'd1, 8'hE1, 1'b1);
complete(3'd1, 8'hE1, 1'b1, 1'b0);
complete(3'd0, 8'hE0, 1'b0, 1'b0);
drain;
end
check(n_matched == b_m + 240, "the scoreboard stopped matching clean traffic");
check(n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order == b_e,
"clean traffic after the random phase produced errors");
// ---- Final agreement ----
check(n_issued === m_i[31:0], "n_issued disagrees with the model");
check(n_matched === m_m[31:0], "n_matched disagrees with the model");
check(n_retries === m_rt[31:0], "n_retries disagrees");
check(n_mismatch === m_mm[31:0], "n_mismatch disagrees");
check(n_unexpected === m_un[31:0], "n_unexpected disagrees");
check(n_duplicate === m_dp[31:0], "n_duplicate disagrees");
check(n_reissue === m_ri[31:0], "n_reissue disagrees");
check(n_missing === m_ms[31:0], "n_missing disagrees");
check(n_order === m_or[31:0], "n_order disagrees");
// ---- CONSERVATION. Every transfer that was successfully issued left
// ---- the table exactly once: matched, reported wrong, reported out of
// ---- order, or reported missing at the drain. A scoreboard whose
// ---- inputs and outputs do not balance has lost track of something,
// ---- and this is the one check that notices without knowing WHICH.
check(n_issued === n_matched + n_mismatch + n_order + n_missing + n_xdiv,
"the transfers that left the table do not account for the transfers that entered it -- something was lost without being reported");
check(n_ts == 48, "not every tag state was crossed with every input combination");
check(n_oc == 9, "not every table occupancy was reached");
check(n_matched > 32'd0, "nothing was ever matched");
check(n_mismatch > 32'd0, "a payload mismatch was never seen");
check(n_unexpected > 32'd0, "an unexpected completion was never seen");
check(n_duplicate > 32'd0, "a duplicate delivery was never seen");
check(n_reissue > 32'd0, "a reused identity was never seen");
check(n_missing > 32'd0, "a missing completion was never seen");
check(n_order > 32'd0, "an in-endpoint order violation was never seen");
check(n_retries > 32'd0, "a retry was never seen");
$display("REACH tagstate-x-input=%0d/48 occupancy=%0d/9 steps=%0d",
n_ts, n_oc, n_steps);
$display("COUNTERS issued=%0d matched=%0d retries=%0d mismatch=%0d unexpected=%0d duplicate=%0d reissue=%0d missing=%0d order=%0d",
n_issued, n_matched, n_retries, n_mismatch, n_unexpected,
n_duplicate, n_reissue, n_missing, n_order);
$display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
$finish;
end
endmodule13.2 SystemVerilog testbench
// Testbench for usb_scoreboard (SystemVerilog).
//
// THE TEST THAT DEFINES THE CHAPTER
//
// Two transfers carrying the SAME PAYLOAD. One of them is delivered twice;
// the other is never delivered at all.
//
// issue tag 3, 0x5A issue tag 6, 0x5A
// complete tag 3 complete tag 3 AGAIN
// ...and tag 6 never completes.
//
// an identity-matching scoreboard 1 match, 1 DUPLICATE, 1 MISSING
// a value-matching scoreboard 2 matches, 0 errors
//
// The second one is not merely weaker. It is wrong in a way that cannot be
// fixed by running longer or by adding payloads, because the two bugs
// ANNIHILATE each other in its bookkeeping. That is mutation S1, and the
// three-line phase below is the whole argument for tagging.
//
// AND THE FALSE-POSITIVE SIDE
//
// Completion out of order ACROSS endpoints is legal and constant -- a bulk
// transfer queued first finishes after an interrupt transfer queued later.
// The suite drives that deliberately and requires ZERO errors, because a
// scoreboard that flags it is a scoreboard nobody leaves enabled.
//
// WHAT IS EXHAUSTIVE HERE
//
// 1. Every tag state (EMPTY / OUTSTANDING / RETIRED) crossed with all 16
// combinations of {exp_valid, act_valid, act_retry, eot} = 48 pairs.
// 2. Every table occupancy from 0 to N_TAG = 9, each reached by real
// issues.
`timescale 1ns/1ps
module tb_sb_sv;
import usb_sb_pkg::*;
localparam int N_TAG = 8;
localparam int TW = 3;
localparam int DW = 8;
logic clk = 1'b0, rst_n = 1'b0;
logic exp_valid = 1'b0, act_valid = 1'b0, act_retry = 1'b0, eot = 1'b0;
logic exp_ep = 1'b0, act_ep = 1'b0;
logic [TW-1:0] exp_tag = '0, act_tag = '0;
logic [DW-1:0] exp_data = '0, act_data = '0;
logic [TW:0] outstanding;
tag_state_e tag_state;
sb_err_e act_err_code;
logic match_pulse, act_err_pulse, exp_err_pulse, miss_pulse;
logic [31:0] n_issued, n_matched, n_retries, n_mismatch, n_unexpected,
n_duplicate, n_reissue, n_missing, n_order;
usb_scoreboard #(.N_TAG(N_TAG), .TW(TW), .DW(DW)) dut (.*);
always #5 clk = ~clk;
int errors = 0, checks = 0;
task automatic check(input logic cond, input string msg);
begin
checks++;
if (!cond) begin
errors++;
if (errors <= 25)
$display("FAIL @%0t: %0s | out=%0d tst=%0d m=%b ae=%b(%0d) ee=%b ms=%b",
$time, msg, outstanding, tag_state, match_pulse,
act_err_pulse, act_err_code, exp_err_pulse, miss_pulse);
end
end
endtask
// ------------------------------------------------------------------
// The shadow scoreboard. Its own table, its own sequence numbers.
// ------------------------------------------------------------------
tag_state_e m_st [N_TAG];
logic [DW-1:0] m_dat [N_TAG];
logic m_ep [N_TAG];
logic [3:0] m_seq [N_TAG];
logic [3:0] m_iss [2];
logic [3:0] m_cmp [2];
logic [TW:0] m_cnt, m_drain;
sb_err_e m_aec;
logic m_mat, m_aer, m_eer, m_mis;
int m_i, m_m, m_rt, m_mm, m_un, m_dp, m_ri, m_ms, m_or;
int n_xdiv; // completions retired by a wrong-endpoint error
int seen_ts [48]; // 3 tag states x 16 input combinations
int seen_oc [9]; // occupancy 0..N_TAG
int n_ts, n_oc, n_steps;
task automatic model_reset();
int j;
begin
for (j = 0; j < N_TAG; j++) begin
m_st[j] = T_EMPTY; m_dat[j] = 8'd0; m_ep[j] = 1'b0; m_seq[j] = 4'd0;
end
m_iss[0] = 4'd0; m_iss[1] = 4'd0;
m_cmp[0] = 4'd0; m_cmp[1] = 4'd0;
m_cnt = 4'd0; m_drain = 4'd0; m_aec = E_NONE;
m_mat = 1'b0; m_aer = 1'b0; m_eer = 1'b0; m_mis = 1'b0;
m_i = 0; m_m = 0; m_rt = 0; m_mm = 0; m_un = 0;
m_dp = 0; m_ri = 0; m_ms = 0; m_or = 0;
for (j = 0; j < 48; j++) seen_ts[j] = 0;
for (j = 0; j < 9; j = j + 1) seen_oc[j] = 0;
n_ts = 0; n_oc = 0; n_steps = 0; n_xdiv = 0;
end
endtask
int idx, scan;
task automatic step(input logic ev, input logic [TW-1:0] et,
input logic [DW-1:0] ed, input logic eep,
input logic av, input logic [TW-1:0] at,
input logic [DW-1:0] ad, input logic aep,
input logic ar, input logic eo);
tag_state_e nst, sat, set_, w_at;
logic w_at_en, w_et_en;
logic [3:0] ncmp0, ncmp1, niss0, niss1;
logic [TW:0] ncnt, ndrn;
sb_err_e naec;
logic nmat, naer, neer, nmis, nret;
begin
exp_valid = ev; exp_tag = et; exp_data = ed; exp_ep = eep;
act_valid = av; act_tag = at; act_data = ad; act_ep = aep;
act_retry = ar; eot = eo;
#1;
check(outstanding === m_cnt, "outstanding disagrees with the shadow scoreboard");
check(tag_state === m_st[at], "tag_state disagrees -- the three-state table is the thing that names a duplicate");
check(match_pulse === m_mat, "the match pulse disagrees");
check(act_err_pulse === m_aer, "the completion-channel error pulse disagrees");
check(act_err_code === m_aec, "act_err_code disagrees");
check(exp_err_pulse === m_eer, "the issue-channel error pulse disagrees");
check(miss_pulse === m_mis, "the drain pulse disagrees");
check(!(match_pulse && act_err_pulse),
"a completion was reported as both a match and an error");
check(outstanding <= (TW+1)'(N_TAG),
"more transfers are outstanding than there are identities");
// ---- the occupancy must equal the number of OUTSTANDING entries.
// ---- A count kept separately from the table can drift from it, and
// ---- then neither can be trusted.
idx = 0;
for (scan = 0; scan < N_TAG; scan++)
if (m_st[scan] == T_OUTSTANDING) idx = idx + 1;
check(m_cnt === idx[TW:0],
"the occupancy counter disagrees with the table it is supposed to summarise");
idx = int'(m_st[at]) * 16 + (ev ? 8 : 0) + (av ? 4 : 0) + (ar ? 2 : 0) + (eo ? 1 : 0);
if (idx < 48) begin
if (seen_ts[idx] == 0) begin seen_ts[idx] = 1; n_ts = n_ts + 1; end
end
if (seen_oc[m_cnt] == 0) begin seen_oc[m_cnt] = 1; n_oc = n_oc + 1; end
n_steps++;
// ---- advance the shadow scoreboard ----
ncnt = m_cnt; ndrn = m_drain; naec = E_NONE;
nmat = 1'b0; naer = 1'b0; neer = 1'b0; nmis = 1'b0; nret = 1'b0;
niss0 = m_iss[0]; niss1 = m_iss[1];
ncmp0 = m_cmp[0]; ncmp1 = m_cmp[1];
// BOTH channels read the state as it stands at the START of the
// cycle, exactly as the design reads its registered table. Mutating
// the model's table inside the completion branch and then reading it
// in the issue branch is the classic read-after-write model bug: it
// diverges only when the two channels name the SAME tag in the same
// cycle, which random stimulus finds and directed stimulus does not.
sat = m_st[at];
set_ = m_st[et];
w_at_en = 1'b0; w_et_en = 1'b0; w_at = T_EMPTY;
if (eo) begin
if (m_drain < (TW+1)'(N_TAG)) begin
if (m_st[m_drain[TW-1:0]] == T_OUTSTANDING) begin
m_st[m_drain[TW-1:0]] = T_EMPTY;
ncnt = ncnt - 1'b1;
nmis = 1'b1;
if (m_ep[m_drain[TW-1:0]]) ncmp1 = m_cmp[1] + 4'd1;
else ncmp0 = m_cmp[0] + 4'd1;
end else begin
m_st[m_drain[TW-1:0]] = T_EMPTY;
end
ndrn = m_drain + 1'b1;
end
end else begin
ndrn = '0;
if (av) begin
nst = sat;
if (nst == T_OUTSTANDING) begin
if (ad !== m_dat[at]) begin
naer = 1'b1; naec = E_MISMATCH;
w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
if (m_ep[at]) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
end else if (aep != m_ep[at]) begin
naer = 1'b1; naec = E_UNEXPECTED;
w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
if (m_ep[at]) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
// Retired by a wrong-endpoint completion: it left the table,
// so the conservation check at the end has to know about it.
n_xdiv++;
end else if (ar) begin
nret = 1'b1;
end else if (m_seq[at] != m_cmp[aep]) begin
naer = 1'b1; naec = E_ORDER;
w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
if (aep) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
end else begin
nmat = 1'b1;
w_at_en = 1'b1; w_at = T_RETIRED; ncnt = ncnt - 1'b1;
if (aep) ncmp1 = m_cmp[1] + 4'd1; else ncmp0 = m_cmp[0] + 4'd1;
end
end else if (nst == T_RETIRED) begin
naer = 1'b1; naec = E_DUPLICATE;
end else begin
naer = 1'b1; naec = E_UNEXPECTED;
end
end
if (ev) begin
if (set_ == T_OUTSTANDING) begin
neer = 1'b1;
end else begin
w_et_en = 1'b1;
ncnt = ncnt + 1'b1;
end
end
// Applied in the design's order: the completion channel's write
// first, the issue channel's second, so an issue to the same tag
// wins -- which it can only do if that tag was not outstanding.
if (w_at_en) m_st[at] = w_at;
if (w_et_en) begin
m_st[et] = T_OUTSTANDING;
m_dat[et] = ed;
m_ep[et] = eep;
m_seq[et] = m_iss[eep];
if (eep) niss1 = m_iss[1] + 4'd1; else niss0 = m_iss[0] + 4'd1;
end
end
m_cnt = ncnt; m_drain = ndrn; m_aec = naec;
m_mat = nmat; m_aer = naer; m_eer = neer; m_mis = nmis;
m_iss[0] = niss0; m_iss[1] = niss1;
m_cmp[0] = ncmp0; m_cmp[1] = ncmp1;
if (ev && !eo && !neer) m_i = m_i + 1;
if (nmat) m_m = m_m + 1;
if (nret) m_rt = m_rt + 1;
if (neer) m_ri = m_ri + 1;
if (nmis) m_ms = m_ms + 1;
if (naer) begin
case (naec)
E_MISMATCH: m_mm = m_mm + 1;
E_UNEXPECTED: m_un = m_un + 1;
E_DUPLICATE: m_dp = m_dp + 1;
E_ORDER: m_or = m_or + 1;
default: ;
endcase
end
@(posedge clk); #1;
exp_valid = 1'b0; act_valid = 1'b0; act_retry = 1'b0; eot = 1'b0;
end
endtask
task automatic nop(input int n);
repeat (n) step(1'b0,3'd0,8'd0,1'b0, 1'b0,3'd0,8'd0,1'b0, 1'b0,1'b0);
endtask
task automatic issue(input logic [TW-1:0] t, input logic [DW-1:0] d,
input logic e);
step(1'b1,t,d,e, 1'b0,3'd0,8'd0,1'b0, 1'b0,1'b0);
endtask
task automatic complete(input logic [TW-1:0] t, input logic [DW-1:0] d,
input logic e, input logic r);
step(1'b0,3'd0,8'd0,1'b0, 1'b1,t,d,e, r,1'b0);
endtask
// Drain the table the way the design provides for. The drain visits one
// tag per cycle, so it takes N_TAG cycles -- a bounded walk, not a search.
task automatic drain();
repeat (N_TAG + 2)
step(1'b0,3'd0,8'd0,1'b0, 1'b0,3'd0,8'd0,1'b0, 1'b0,1'b1);
check(outstanding === '0, "the drain did not empty the table");
nop(1);
endtask
// A clean scoreboard: every tag EMPTY, both sequence spaces aligned.
// A pristine table, reached the way the design provides for: the drain
// visits every tag, reports the outstanding ones and returns the retired
// ones to EMPTY. Nothing is forced.
task automatic fresh();
drain();
for (int j = 0; j < N_TAG; j++)
check(m_st[j] === T_EMPTY,
"the drain did not return every tag to EMPTY -- a retired identity that stays retired can be used only once");
endtask
int k, j2, b_m, b_e, ts, cb, oc;
initial begin
model_reset();
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
// ---- Phase A: the state after reset ----
check(outstanding === 4'd0, "reset left transfers outstanding");
check(match_pulse === 1'b0, "reset asserted a match");
check(act_err_pulse === 1'b0, "reset asserted a completion error");
check(n_matched === 32'd0, "reset left the match counter non-zero");
// ---- Phase B: clean traffic, in order, on both endpoints. ZERO errors.
for (k = 0; k < 60; k++) begin
b_m = n_matched;
b_e = n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order;
issue(3'd0, 8'h11, 1'b0);
issue(3'd1, 8'h22, 1'b0);
complete(3'd0, 8'h11, 1'b0, 1'b0);
complete(3'd1, 8'h22, 1'b0, 1'b0);
check(n_matched == b_m + 2, "clean in-order traffic was not matched");
check(n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order == b_e,
"clean in-order traffic produced an error");
drain();
end
// ---- Phase C: THE IDENTITY TEST. Two transfers, SAME payload; one
// ---- delivered twice, the other never.
for (k = 0; k < 40; k++) begin
b_m = n_matched;
issue(3'd3, 8'h5A, 1'b0);
issue(3'd6, 8'h5A, 1'b0); // the SAME bytes
complete(3'd3, 8'h5A, 1'b0, 1'b0); // match
b_e = n_duplicate;
complete(3'd3, 8'h5A, 1'b0, 1'b0); // DELIVERED TWICE
check(n_duplicate == b_e + 1,
"a second delivery of the same identity was not reported as a duplicate -- a scoreboard that matches on payload retires the OTHER transfer instead and both bugs vanish");
b_e = n_missing;
drain(); // tag 6 never completed
check(n_missing == b_e + 1,
"a transfer that never completed was not reported as missing");
check(n_matched == b_m + 1,
"more matches were recorded than transfers actually completed");
end
// ---- Phase D: identity matched, payload wrong. Repeated across every
// ---- single-bit corruption, so the comparison is checked on each bit
// ---- rather than on one arbitrary pair of values.
for (k = 0; k < DW; k++) begin
b_e = n_mismatch;
issue(3'd2, 8'hA5, 1'b0);
complete(3'd2, 8'hA5 ^ (8'd1 << k), 1'b0, 1'b0);
check(n_mismatch == b_e + 1,
"a wrong payload on a matched identity was accepted -- a comparison that covers only part of the payload passes most corruptions");
drain();
end
// ---- Phase E: a completion for a tag that was never issued. ----
fresh();
b_e = n_unexpected;
complete(3'd7, 8'h33, 1'b0, 1'b0);
check(n_unexpected == b_e + 1,
"a completion for a never-issued identity was accepted");
// ---- Phase F: a tag reused while still outstanding. ----
fresh();
b_e = n_reissue;
issue(3'd4, 8'h44, 1'b0);
issue(3'd4, 8'h55, 1'b0);
check(n_reissue == b_e + 1,
"an identity was reused while still outstanding and the first transfer was silently overwritten");
check(outstanding === 4'd1,
"the reissued transfer displaced the one already being tracked");
drain();
// ---- Phase G: out of order WITHIN an endpoint is a bug. ----
for (k = 0; k < 40; k++) begin
fresh();
b_e = n_order;
issue(3'd0, 8'h60, 1'b0);
issue(3'd1, 8'h61, 1'b0);
complete(3'd1, 8'h61, 1'b0, 1'b0); // the SECOND one first
check(n_order == b_e + 1,
"completion out of order within one endpoint was accepted -- the stream is corrupted and the host will not check it");
drain();
end
// ---- Phase H: out of order ACROSS endpoints is LEGAL. ----
// ---- This is the false-positive check, and it is the reason the
// ---- scoreboard cannot be a queue.
fresh();
for (k = 0; k < 40; k++) begin
b_m = n_matched;
b_e = n_order + n_unexpected + n_mismatch;
issue(3'd0, 8'h70, 1'b0); // endpoint 0 first
issue(3'd1, 8'h71, 1'b1); // endpoint 1 second
complete(3'd1, 8'h71, 1'b1, 1'b0); // ...and it finishes FIRST
complete(3'd0, 8'h70, 1'b0, 1'b0);
check(n_matched == b_m + 2,
"legal cross-endpoint reordering was not matched");
check(n_order + n_unexpected + n_mismatch == b_e,
"legal cross-endpoint reordering was reported as an error -- a scoreboard built as a queue flags every interleaving, which is most of them");
drain();
end
// ---- Phase I: a RETRY is not a delivery. ----
fresh();
for (k = 1; k <= 5; k++) begin
b_m = n_matched;
b_e = n_duplicate + n_order;
issue(3'd5, 8'h80, 1'b0);
for (j2 = 0; j2 < k; j2++)
complete(3'd5, 8'h80, 1'b0, 1'b1); // k re-sends
check(outstanding === 4'd1,
"a retry retired the transfer -- the real delivery will then look like a duplicate");
complete(3'd5, 8'h80, 1'b0, 1'b0); // the real delivery
check(n_matched == b_m + 1, "the delivery after a run of retries was not matched");
check(n_duplicate + n_order == b_e,
"a retry was reported as a duplicate -- every flow-controlled transfer on a busy bus would be");
drain();
end
// ---- Phase J: THE DRAIN. Data the design accepted and never returned.
fresh();
for (oc = 1; oc <= N_TAG; oc++) begin
b_e = n_missing;
b_m = n_matched;
for (k = 0; k < oc; k++) issue(3'(k), 8'h90 + 8'(k), 1'b0);
check(outstanding === (TW+1)'(oc), "the table did not accept the issues offered to it");
drain();
check(n_missing == b_e + oc,
"transfers still outstanding at end of test were not reported -- the run is over and nothing more is coming");
check(n_matched == b_m, "a transfer that never came back was counted as a match");
fresh();
end
// ---- Phase K: EXHAUSTIVE. Every tag state x every input combination. --
for (ts = 0; ts < 3; ts++) begin
for (cb = 0; cb < 16; cb++) begin
fresh();
// put tag 2 into the state under test, using the design's own means
case (ts)
0: ; // EMPTY
1: issue(3'd2, 8'hC0, 1'b0); // OUTSTANDING
2: begin issue(3'd2, 8'hC0, 1'b0);
complete(3'd2, 8'hC0, 1'b0, 1'b0); end // RETIRED
endcase
check(m_st[2] === tag_state_e'(ts),
"the sweep could not reach the tag state it meant to reach");
step(1'(cb[3]),3'd2,8'hC0,1'b0, 1'(cb[2]),3'd2,8'hC0,1'b0,
1'(cb[1]),1'(cb[0]));
step(1'(cb[3]),3'd2,8'hC0,1'b0, 1'(cb[2]),3'd2,8'hC0,1'b0,
1'(cb[1]),1'(cb[0]));
end
end
// ---- Phase L: random ----
for (k = 0; k < 34000; k++)
step($urandom_range(0,99) < 30, 3'($urandom()), 8'($urandom()), 1'($urandom()),
$urandom_range(0,99) < 30, 3'($urandom()), 8'($urandom()), 1'($urandom()),
$urandom_range(0,99) < 20,
$urandom_range(0,999) < 7);
// ---- Phase M: and clean traffic afterwards, so the scoreboard is shown
// ---- to still work rather than merely to have stopped.
fresh();
b_m = n_matched;
b_e = n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order;
for (k = 0; k < 120; k++) begin
issue(3'd0, 8'hE0, 1'b0);
issue(3'd1, 8'hE1, 1'b1);
complete(3'd1, 8'hE1, 1'b1, 1'b0);
complete(3'd0, 8'hE0, 1'b0, 1'b0);
drain();
end
check(n_matched == b_m + 240, "the scoreboard stopped matching clean traffic");
check(n_mismatch + n_unexpected + n_duplicate + n_reissue + n_order == b_e,
"clean traffic after the random phase produced errors");
// ---- Final agreement ----
check(n_issued === 32'(m_i), "n_issued disagrees with the model");
check(n_matched === 32'(m_m), "n_matched disagrees with the model");
check(n_retries === 32'(m_rt), "n_retries disagrees");
check(n_mismatch === 32'(m_mm), "n_mismatch disagrees");
check(n_unexpected === 32'(m_un), "n_unexpected disagrees");
check(n_duplicate === 32'(m_dp), "n_duplicate disagrees");
check(n_reissue === 32'(m_ri), "n_reissue disagrees");
check(n_missing === 32'(m_ms), "n_missing disagrees");
check(n_order === 32'(m_or), "n_order disagrees");
// ---- CONSERVATION. Every transfer that was successfully issued left
// ---- the table exactly once: matched, reported wrong, reported out of
// ---- order, or reported missing at the drain. A scoreboard whose
// ---- inputs and outputs do not balance has lost track of something,
// ---- and this is the one check that notices without knowing WHICH.
check(n_issued === n_matched + n_mismatch + n_order + n_missing + 32'(n_xdiv),
"the transfers that left the table do not account for the transfers that entered it -- something was lost without being reported");
check(n_ts == 48, "not every tag state was crossed with every input combination");
check(n_oc == 9, "not every table occupancy was reached");
check(n_matched > 32'd0, "nothing was ever matched");
check(n_mismatch > 32'd0, "a payload mismatch was never seen");
check(n_unexpected > 32'd0, "an unexpected completion was never seen");
check(n_duplicate > 32'd0, "a duplicate delivery was never seen");
check(n_reissue > 32'd0, "a reused identity was never seen");
check(n_missing > 32'd0, "a missing completion was never seen");
check(n_order > 32'd0, "an in-endpoint order violation was never seen");
check(n_retries > 32'd0, "a retry was never seen");
$display("REACH tagstate-x-input=%0d/48 occupancy=%0d/9 steps=%0d",
n_ts, n_oc, n_steps);
$display("COUNTERS issued=%0d matched=%0d retries=%0d mismatch=%0d unexpected=%0d duplicate=%0d reissue=%0d missing=%0d order=%0d",
n_issued, n_matched, n_retries, n_mismatch, n_unexpected,
n_duplicate, n_reissue, n_missing, n_order);
$display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
$finish;
end
endmodule13.3 VHDL testbench
-- Testbench for usb_scoreboard (VHDL-2008).
--
-- THE TEST THAT DEFINES THE CHAPTER
--
-- Two transfers carrying the SAME PAYLOAD. One of them is delivered twice;
-- the other is never delivered at all.
--
-- issue tag 3, 0x5A issue tag 6, 0x5A
-- complete tag 3 complete tag 3 AGAIN
-- ...and tag 6 never completes.
--
-- an identity-matching scoreboard 1 match, 1 DUPLICATE, 1 MISSING
-- a value-matching scoreboard 2 matches, 0 errors
--
-- The second one is not merely weaker. It is wrong in a way that cannot be
-- fixed by running longer or by adding payloads, because the two bugs
-- ANNIHILATE each other in its bookkeeping. That is mutation S1, and the
-- short phase below is the whole argument for tagging.
--
-- AND THE FALSE-POSITIVE SIDE
--
-- Completion out of order ACROSS endpoints is legal and constant -- a bulk
-- transfer queued first finishes after an interrupt transfer queued later.
-- The suite drives that deliberately and requires ZERO errors, because a
-- scoreboard that flags it is a scoreboard nobody leaves enabled.
--
-- WHAT IS EXHAUSTIVE HERE
--
-- 1. Every tag state (EMPTY / OUTSTANDING / RETIRED) crossed with all 16
-- combinations of (exp_valid, act_valid, act_retry, eot) = 48 pairs.
-- 2. Every table occupancy from 0 to N_TAG = 9, each reached by real
-- issues.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_sb_pkg.all;
entity tb_sb_vhdl is
end entity tb_sb_vhdl;
architecture sim of tb_sb_vhdl is
constant N_TAG : integer := 8;
constant TW : integer := 3;
constant DW : integer := 8;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : boolean := false;
signal exp_valid, act_valid, act_retry, eot : std_logic := '0';
signal exp_ep, act_ep : std_logic := '0';
signal exp_tag, act_tag : std_logic_vector(TW-1 downto 0) := (others => '0');
signal exp_data, act_data : std_logic_vector(DW-1 downto 0) := (others => '0');
signal outstanding : std_logic_vector(TW downto 0);
signal tag_state : std_logic_vector(1 downto 0);
signal act_err_code : std_logic_vector(2 downto 0);
signal match_pulse, act_err_pulse, exp_err_pulse, miss_pulse : std_logic;
signal n_issued, n_matched, n_retries, n_mismatch : std_logic_vector(31 downto 0);
signal n_unexpected, n_duplicate, n_reissue : std_logic_vector(31 downto 0);
signal n_missing, n_order : std_logic_vector(31 downto 0);
begin
dut : entity work.usb_scoreboard
generic map (N_TAG => N_TAG, TW => TW, DW => DW)
port map (
clk => clk, rst_n => rst_n,
exp_valid => exp_valid, exp_tag => exp_tag, exp_data => exp_data,
exp_ep => exp_ep,
act_valid => act_valid, act_tag => act_tag, act_data => act_data,
act_ep => act_ep, act_retry => act_retry, eot => eot,
outstanding => outstanding, tag_state => tag_state,
match_pulse => match_pulse, act_err_pulse => act_err_pulse,
act_err_code => act_err_code, exp_err_pulse => exp_err_pulse,
miss_pulse => miss_pulse,
n_issued => n_issued, n_matched => n_matched, n_retries => n_retries,
n_mismatch => n_mismatch, n_unexpected => n_unexpected,
n_duplicate => n_duplicate, n_reissue => n_reissue,
n_missing => n_missing, n_order => n_order
);
clk <= (not clk) after 5 ns when not done else '0';
stim : process
type st_arr is array (0 to N_TAG-1) of tag_state_t;
type dat_arr is array (0 to N_TAG-1) of std_logic_vector(DW-1 downto 0);
type seq_arr is array (0 to N_TAG-1) of unsigned(3 downto 0);
type ep_arr is array (0 to N_TAG-1) of std_logic;
type eps_arr is array (0 to 1) of unsigned(3 downto 0);
type ts_arr is array (0 to 47) of integer;
type oc_arr is array (0 to 8) of integer;
variable errors, checks : integer := 0;
-- ---- The shadow scoreboard. Its own table, its own sequence numbers. --
variable m_st : st_arr := (others => T_EMPTY);
variable m_dat : dat_arr := (others => (others => '0'));
variable m_ep : ep_arr := (others => '0');
variable m_seq : seq_arr := (others => (others => '0'));
variable m_iss, m_cmp : eps_arr := (others => (others => '0'));
variable m_cnt, m_drain : unsigned(TW downto 0) := (others => '0');
variable m_aec : sb_err_t := E_NONE;
variable m_mat, m_aer, m_eer, m_mis : std_logic := '0';
variable m_i, m_m, m_rt, m_mm : integer := 0;
variable m_un, m_dp, m_ri, m_ms, m_or : integer := 0;
variable n_xdiv : integer := 0;
variable seen_ts : ts_arr := (others => 0);
variable seen_oc : oc_arr := (others => 0);
variable n_ts, n_oc, n_steps : integer := 0;
-- A deterministic LFSR, so a rerun reproduces exactly the same traffic.
variable lfsr : unsigned(31 downto 0) := x"1DEAD5B0";
impure function rnd32 return unsigned is
begin
lfsr := lfsr(30 downto 0) &
(lfsr(31) xor lfsr(21) xor lfsr(1) xor lfsr(0));
return lfsr;
end function;
-- Only the low 30 bits are converted: a full 32-bit unsigned does not
-- fit in VHDL's INTEGER, and to_integer aborts the run rather than
-- wrapping.
impure function rnd_nat return integer is
variable u : unsigned(31 downto 0);
begin
u := rnd32;
return to_integer(u(29 downto 0));
end function;
impure function rnd_slv (w : integer) return std_logic_vector is
variable u : unsigned(31 downto 0);
begin
u := rnd32;
return std_logic_vector(u(w-1 downto 0));
end function;
impure function rnd_bit return std_logic is
variable u : unsigned(31 downto 0);
begin
u := rnd32;
return u(7);
end function;
impure function rnd_lt (pct, base : integer) return std_logic is
begin
if (rnd_nat mod base) < pct then return '1'; else return '0'; end if;
end function;
procedure chk (cond : boolean; msg : string) is
begin
checks := checks + 1;
if not cond then
errors := errors + 1;
if errors <= 25 then
report "FAIL: " & msg &
" | out=" & integer'image(to_integer(m_cnt)) &
" code=" & integer'image(sb_err_t'pos(m_aec))
severity note;
end if;
end if;
end procedure;
procedure step (ev : std_logic; et : std_logic_vector(TW-1 downto 0);
ed : std_logic_vector(DW-1 downto 0); eep : std_logic;
av : std_logic; at : std_logic_vector(TW-1 downto 0);
ad : std_logic_vector(DW-1 downto 0); aep : std_logic;
ar, eo : std_logic) is
variable at_i, et_i, dr_i, idx : integer;
variable sat, set_st, w_at : tag_state_t;
variable w_at_en, w_et_en : boolean;
variable ncnt, ndrn : unsigned(TW downto 0);
variable niss, ncmp : eps_arr;
variable naec : sb_err_t;
variable nmat, naer, neer, nmis, nret : std_logic;
variable aep_i, eep_i : integer;
begin
exp_valid <= ev; exp_tag <= et; exp_data <= ed; exp_ep <= eep;
act_valid <= av; act_tag <= at; act_data <= ad; act_ep <= aep;
act_retry <= ar; eot <= eo;
wait for 1 ns;
at_i := to_integer(unsigned(at));
et_i := to_integer(unsigned(et));
aep_i := 0; if aep = '1' then aep_i := 1; end if;
eep_i := 0; if eep = '1' then eep_i := 1; end if;
chk(unsigned(outstanding) = m_cnt,
"outstanding disagrees with the shadow scoreboard");
chk(tag_state = ts_code(m_st(at_i)),
"tag_state disagrees -- the three-state table is the thing that names a duplicate");
chk(match_pulse = m_mat, "the match pulse disagrees");
chk(act_err_pulse = m_aer, "the completion-channel error pulse disagrees");
chk(act_err_code = se_code(m_aec), "act_err_code disagrees");
chk(exp_err_pulse = m_eer, "the issue-channel error pulse disagrees");
chk(miss_pulse = m_mis, "the drain pulse disagrees");
chk(not (match_pulse = '1' and act_err_pulse = '1'),
"a completion was reported as both a match and an error");
chk(unsigned(outstanding) <= to_unsigned(N_TAG, TW+1),
"more transfers are outstanding than there are identities");
-- ---- the occupancy must equal the number of OUTSTANDING entries.
-- ---- A count kept separately from the table can drift from it, and
-- ---- then neither can be trusted.
idx := 0;
for scan in 0 to N_TAG - 1 loop
if m_st(scan) = T_OUTSTANDING then idx := idx + 1; end if;
end loop;
chk(m_cnt = to_unsigned(idx, TW+1),
"the occupancy counter disagrees with the table it is supposed to summarise");
idx := tag_state_t'pos(m_st(at_i)) * 16;
if ev = '1' then idx := idx + 8; end if;
if av = '1' then idx := idx + 4; end if;
if ar = '1' then idx := idx + 2; end if;
if eo = '1' then idx := idx + 1; end if;
if idx < 48 then
if seen_ts(idx) = 0 then seen_ts(idx) := 1; n_ts := n_ts + 1; end if;
end if;
if seen_oc(to_integer(m_cnt)) = 0 then
seen_oc(to_integer(m_cnt)) := 1; n_oc := n_oc + 1;
end if;
n_steps := n_steps + 1;
-- ---- advance the shadow scoreboard ----
ncnt := m_cnt; ndrn := m_drain; naec := E_NONE;
nmat := '0'; naer := '0'; neer := '0'; nmis := '0'; nret := '0';
niss := m_iss; ncmp := m_cmp;
-- BOTH channels read the state as it stands at the START of the
-- cycle, exactly as the design reads its registered table. Mutating
-- the model's table inside the completion branch and then reading it
-- in the issue branch is the classic read-after-write model bug: it
-- diverges only when the two channels name the SAME tag in the same
-- cycle, which random stimulus finds and directed stimulus does not.
sat := m_st(at_i);
set_st := m_st(et_i);
w_at_en := false; w_et_en := false; w_at := T_EMPTY;
if eo = '1' then
if m_drain < to_unsigned(N_TAG, TW+1) then
dr_i := to_integer(m_drain);
if m_st(dr_i) = T_OUTSTANDING then
m_st(dr_i) := T_EMPTY;
ncnt := ncnt - 1;
nmis := '1';
if m_ep(dr_i) = '1' then ncmp(1) := m_cmp(1) + 1;
else ncmp(0) := m_cmp(0) + 1;
end if;
else
m_st(dr_i) := T_EMPTY;
end if;
ndrn := m_drain + 1;
end if;
else
ndrn := (others => '0');
if av = '1' then
case sat is
when T_OUTSTANDING =>
if ad /= m_dat(at_i) then
naer := '1'; naec := E_MISMATCH;
w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
if m_ep(at_i) = '1' then ncmp(1) := m_cmp(1) + 1;
else ncmp(0) := m_cmp(0) + 1;
end if;
elsif aep /= m_ep(at_i) then
naer := '1'; naec := E_UNEXPECTED;
w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
if m_ep(at_i) = '1' then ncmp(1) := m_cmp(1) + 1;
else ncmp(0) := m_cmp(0) + 1;
end if;
-- Retired by a wrong-endpoint completion: it left the table,
-- so the conservation check at the end has to know about it.
n_xdiv := n_xdiv + 1;
elsif ar = '1' then
nret := '1';
elsif m_seq(at_i) /= m_cmp(aep_i) then
naer := '1'; naec := E_ORDER;
w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
ncmp(aep_i) := m_cmp(aep_i) + 1;
else
nmat := '1';
w_at_en := true; w_at := T_RETIRED; ncnt := ncnt - 1;
ncmp(aep_i) := m_cmp(aep_i) + 1;
end if;
when T_RETIRED =>
naer := '1'; naec := E_DUPLICATE;
when others =>
naer := '1'; naec := E_UNEXPECTED;
end case;
end if;
if ev = '1' then
if set_st = T_OUTSTANDING then
neer := '1';
else
w_et_en := true;
ncnt := ncnt + 1;
end if;
end if;
-- Applied in the design's order: the completion channel's write
-- first, the issue channel's second, so an issue to the same tag
-- wins -- which it can only do if that tag was not outstanding.
if w_at_en then m_st(at_i) := w_at; end if;
if w_et_en then
m_st(et_i) := T_OUTSTANDING;
m_dat(et_i) := ed;
m_ep(et_i) := eep;
m_seq(et_i) := m_iss(eep_i);
niss(eep_i) := m_iss(eep_i) + 1;
end if;
end if;
m_cnt := ncnt; m_drain := ndrn; m_aec := naec;
m_mat := nmat; m_aer := naer; m_eer := neer; m_mis := nmis;
m_iss := niss; m_cmp := ncmp;
if ev = '1' and eo = '0' and neer = '0' then m_i := m_i + 1; end if;
if nmat = '1' then m_m := m_m + 1; end if;
if nret = '1' then m_rt := m_rt + 1; end if;
if neer = '1' then m_ri := m_ri + 1; end if;
if nmis = '1' then m_ms := m_ms + 1; end if;
if naer = '1' then
case naec is
when E_MISMATCH => m_mm := m_mm + 1;
when E_UNEXPECTED => m_un := m_un + 1;
when E_DUPLICATE => m_dp := m_dp + 1;
when E_ORDER => m_or := m_or + 1;
when others => null;
end case;
end if;
wait until rising_edge(clk);
wait for 1 ns;
exp_valid <= '0'; act_valid <= '0'; act_retry <= '0'; eot <= '0';
end procedure;
constant Z3 : std_logic_vector(TW-1 downto 0) := (others => '0');
constant Z8 : std_logic_vector(DW-1 downto 0) := (others => '0');
procedure nop (n : integer) is
begin
for j in 1 to n loop
step('0',Z3,Z8,'0', '0',Z3,Z8,'0', '0','0');
end loop;
end procedure;
procedure issue (t : std_logic_vector(TW-1 downto 0);
d : std_logic_vector(DW-1 downto 0); e : std_logic) is
begin
step('1',t,d,e, '0',Z3,Z8,'0', '0','0');
end procedure;
procedure complete (t : std_logic_vector(TW-1 downto 0);
d : std_logic_vector(DW-1 downto 0);
e, r : std_logic) is
begin
step('0',Z3,Z8,'0', '1',t,d,e, r,'0');
end procedure;
-- Drain the table the way the design provides for. The drain visits one
-- tag per cycle, so it takes N_TAG cycles -- a bounded walk, not a
-- search.
procedure drain is
begin
for j in 1 to N_TAG + 2 loop
step('0',Z3,Z8,'0', '0',Z3,Z8,'0', '0','1');
end loop;
chk(unsigned(outstanding) = 0, "the drain did not empty the table");
nop(1);
end procedure;
-- A pristine table, reached the way the design provides for: the drain
-- visits every tag, reports the outstanding ones and returns the retired
-- ones to EMPTY. Nothing is forced.
procedure fresh is
begin
drain;
for j in 0 to N_TAG - 1 loop
chk(m_st(j) = T_EMPTY,
"the drain did not return every tag to EMPTY -- a retired identity that stays retired can be used only once");
end loop;
end procedure;
function tg (n : integer) return std_logic_vector is
begin
return std_logic_vector(to_unsigned(n, TW));
end function;
function dt (n : integer) return std_logic_vector is
begin
return std_logic_vector(to_unsigned(n mod 256, DW));
end function;
variable b_m, b_e : integer := 0;
variable ev_v, av_v, ar_v, eo_v : std_logic;
variable ln : line;
begin
wait for 33 ns;
rst_n <= '1';
wait until rising_edge(clk);
wait for 1 ns;
-- ---- Phase A: the state after reset ----
chk(unsigned(outstanding) = 0, "reset left transfers outstanding");
chk(match_pulse = '0', "reset asserted a match");
chk(act_err_pulse = '0', "reset asserted a completion error");
chk(unsigned(n_matched) = 0, "reset left the match counter non-zero");
-- ---- Phase B: clean traffic, in order, on both endpoints. ZERO errors.
for k in 0 to 59 loop
b_m := to_integer(unsigned(n_matched));
b_e := to_integer(unsigned(n_mismatch)) + to_integer(unsigned(n_unexpected))
+ to_integer(unsigned(n_duplicate)) + to_integer(unsigned(n_reissue))
+ to_integer(unsigned(n_order));
issue(tg(0), dt(16#11#), '0');
issue(tg(1), dt(16#22#), '0');
complete(tg(0), dt(16#11#), '0', '0');
complete(tg(1), dt(16#22#), '0', '0');
chk(to_integer(unsigned(n_matched)) = b_m + 2,
"clean in-order traffic was not matched");
chk(to_integer(unsigned(n_mismatch)) + to_integer(unsigned(n_unexpected))
+ to_integer(unsigned(n_duplicate)) + to_integer(unsigned(n_reissue))
+ to_integer(unsigned(n_order)) = b_e,
"clean in-order traffic produced an error");
drain;
end loop;
-- ---- Phase C: THE IDENTITY TEST. Two transfers, SAME payload; one
-- ---- delivered twice, the other never.
for k in 0 to 39 loop
b_m := to_integer(unsigned(n_matched));
issue(tg(3), dt(16#5A#), '0');
issue(tg(6), dt(16#5A#), '0'); -- the SAME bytes
complete(tg(3), dt(16#5A#), '0', '0'); -- match
b_e := to_integer(unsigned(n_duplicate));
complete(tg(3), dt(16#5A#), '0', '0'); -- DELIVERED TWICE
chk(to_integer(unsigned(n_duplicate)) = b_e + 1,
"a second delivery of the same identity was not reported as a duplicate -- a scoreboard that matches on payload retires the OTHER transfer instead and both bugs vanish");
b_e := to_integer(unsigned(n_missing));
drain; -- tag 6 never completed
chk(to_integer(unsigned(n_missing)) = b_e + 1,
"a transfer that never completed was not reported as missing");
chk(to_integer(unsigned(n_matched)) = b_m + 1,
"more matches were recorded than transfers actually completed");
end loop;
-- ---- Phase D: identity matched, payload wrong. Repeated across every
-- ---- single-bit corruption, so the comparison is checked on each bit
-- ---- rather than on one arbitrary pair of values.
for k in 0 to DW - 1 loop
b_e := to_integer(unsigned(n_mismatch));
issue(tg(2), dt(16#A5#), '0');
complete(tg(2), std_logic_vector(unsigned(dt(16#A5#))
xor shift_left(to_unsigned(1, DW), k)), '0', '0');
chk(to_integer(unsigned(n_mismatch)) = b_e + 1,
"a wrong payload on a matched identity was accepted -- a comparison that covers only part of the payload passes most corruptions");
drain;
end loop;
-- ---- Phase E: a completion for a tag that was never issued. ----
fresh;
b_e := to_integer(unsigned(n_unexpected));
complete(tg(7), dt(16#33#), '0', '0');
chk(to_integer(unsigned(n_unexpected)) = b_e + 1,
"a completion for a never-issued identity was accepted");
-- ---- Phase F: a tag reused while still outstanding. ----
fresh;
b_e := to_integer(unsigned(n_reissue));
issue(tg(4), dt(16#44#), '0');
issue(tg(4), dt(16#55#), '0');
chk(to_integer(unsigned(n_reissue)) = b_e + 1,
"an identity was reused while still outstanding and the first transfer was silently overwritten");
chk(unsigned(outstanding) = 1,
"the reissued transfer displaced the one already being tracked");
drain;
-- ---- Phase G: out of order WITHIN an endpoint is a bug. ----
for k in 0 to 39 loop
fresh;
b_e := to_integer(unsigned(n_order));
issue(tg(0), dt(16#60#), '0');
issue(tg(1), dt(16#61#), '0');
complete(tg(1), dt(16#61#), '0', '0'); -- the SECOND one first
chk(to_integer(unsigned(n_order)) = b_e + 1,
"completion out of order within one endpoint was accepted -- the stream is corrupted and the host will not check it");
drain;
end loop;
-- ---- Phase H: out of order ACROSS endpoints is LEGAL. ----
-- ---- This is the false-positive check, and it is the reason the
-- ---- scoreboard cannot be a queue.
fresh;
for k in 0 to 39 loop
b_m := to_integer(unsigned(n_matched));
b_e := to_integer(unsigned(n_order)) + to_integer(unsigned(n_unexpected))
+ to_integer(unsigned(n_mismatch));
issue(tg(0), dt(16#70#), '0'); -- endpoint 0 first
issue(tg(1), dt(16#71#), '1'); -- endpoint 1 second
complete(tg(1), dt(16#71#), '1', '0'); -- ...and it finishes FIRST
complete(tg(0), dt(16#70#), '0', '0');
chk(to_integer(unsigned(n_matched)) = b_m + 2,
"legal cross-endpoint reordering was not matched");
chk(to_integer(unsigned(n_order)) + to_integer(unsigned(n_unexpected))
+ to_integer(unsigned(n_mismatch)) = b_e,
"legal cross-endpoint reordering was reported as an error -- a scoreboard built as a queue flags every interleaving, which is most of them");
drain;
end loop;
-- ---- Phase I: a RETRY is not a delivery. ----
fresh;
for k in 1 to 5 loop
b_m := to_integer(unsigned(n_matched));
b_e := to_integer(unsigned(n_duplicate)) + to_integer(unsigned(n_order));
issue(tg(5), dt(16#80#), '0');
for j in 1 to k loop
complete(tg(5), dt(16#80#), '0', '1'); -- k re-sends
end loop;
chk(unsigned(outstanding) = 1,
"a retry retired the transfer -- the real delivery will then look like a duplicate");
complete(tg(5), dt(16#80#), '0', '0'); -- the real delivery
chk(to_integer(unsigned(n_matched)) = b_m + 1,
"the delivery after a run of retries was not matched");
chk(to_integer(unsigned(n_duplicate)) + to_integer(unsigned(n_order)) = b_e,
"a retry was reported as a duplicate -- every flow-controlled transfer on a busy bus would be");
drain;
end loop;
-- ---- Phase J: THE DRAIN. Data the design accepted and never returned.
fresh;
for oc in 1 to N_TAG loop
b_e := to_integer(unsigned(n_missing));
b_m := to_integer(unsigned(n_matched));
for k in 0 to oc - 1 loop
issue(tg(k), dt(16#90# + k), '0');
end loop;
chk(unsigned(outstanding) = to_unsigned(oc, TW+1),
"the table did not accept the issues offered to it");
drain;
chk(to_integer(unsigned(n_missing)) = b_e + oc,
"transfers still outstanding at end of test were not reported -- the run is over and nothing more is coming");
chk(to_integer(unsigned(n_matched)) = b_m,
"a transfer that never came back was counted as a match");
fresh;
end loop;
-- ---- Phase K: EXHAUSTIVE. Every tag state x every input combination. --
for ts in 0 to 2 loop
for cb in 0 to 15 loop
fresh;
-- put tag 2 into the state under test, using the design's own means
case ts is
when 0 => null; -- EMPTY
when 1 => issue(tg(2), dt(16#C0#), '0'); -- OUTSTANDING
when others => issue(tg(2), dt(16#C0#), '0');
complete(tg(2), dt(16#C0#), '0', '0'); -- RETIRED
end case;
chk(m_st(2) = tag_state_t'val(ts),
"the sweep could not reach the tag state it meant to reach");
if (cb / 8) mod 2 = 1 then ev_v := '1'; else ev_v := '0'; end if;
if (cb / 4) mod 2 = 1 then av_v := '1'; else av_v := '0'; end if;
if (cb / 2) mod 2 = 1 then ar_v := '1'; else ar_v := '0'; end if;
if cb mod 2 = 1 then eo_v := '1'; else eo_v := '0'; end if;
step(ev_v,tg(2),dt(16#C0#),'0', av_v,tg(2),dt(16#C0#),'0', ar_v,eo_v);
step(ev_v,tg(2),dt(16#C0#),'0', av_v,tg(2),dt(16#C0#),'0', ar_v,eo_v);
end loop;
end loop;
-- ---- Phase L: random ----
for k in 0 to 33999 loop
ev_v := rnd_lt(30, 100);
av_v := rnd_lt(30, 100);
ar_v := rnd_lt(20, 100);
eo_v := rnd_lt(7, 1000);
step(ev_v, rnd_slv(TW), rnd_slv(DW), rnd_bit,
av_v, rnd_slv(TW), rnd_slv(DW), rnd_bit, ar_v, eo_v);
end loop;
-- ---- Phase M: and clean traffic afterwards, so the scoreboard is shown
-- ---- to still work rather than merely to have stopped.
fresh;
b_m := to_integer(unsigned(n_matched));
b_e := to_integer(unsigned(n_mismatch)) + to_integer(unsigned(n_unexpected))
+ to_integer(unsigned(n_duplicate)) + to_integer(unsigned(n_reissue))
+ to_integer(unsigned(n_order));
for k in 0 to 119 loop
issue(tg(0), dt(16#E0#), '0');
issue(tg(1), dt(16#E1#), '1');
complete(tg(1), dt(16#E1#), '1', '0');
complete(tg(0), dt(16#E0#), '0', '0');
drain;
end loop;
chk(to_integer(unsigned(n_matched)) = b_m + 240,
"the scoreboard stopped matching clean traffic");
chk(to_integer(unsigned(n_mismatch)) + to_integer(unsigned(n_unexpected))
+ to_integer(unsigned(n_duplicate)) + to_integer(unsigned(n_reissue))
+ to_integer(unsigned(n_order)) = b_e,
"clean traffic after the random phase produced errors");
-- ---- Final agreement ----
chk(to_integer(unsigned(n_issued)) = m_i, "n_issued disagrees with the model");
chk(to_integer(unsigned(n_matched)) = m_m, "n_matched disagrees with the model");
chk(to_integer(unsigned(n_retries)) = m_rt, "n_retries disagrees");
chk(to_integer(unsigned(n_mismatch)) = m_mm, "n_mismatch disagrees");
chk(to_integer(unsigned(n_unexpected)) = m_un, "n_unexpected disagrees");
chk(to_integer(unsigned(n_duplicate)) = m_dp, "n_duplicate disagrees");
chk(to_integer(unsigned(n_reissue)) = m_ri, "n_reissue disagrees");
chk(to_integer(unsigned(n_missing)) = m_ms, "n_missing disagrees");
chk(to_integer(unsigned(n_order)) = m_or, "n_order disagrees");
-- ---- CONSERVATION. Every transfer that was successfully issued left
-- ---- the table exactly once: matched, reported wrong, reported out of
-- ---- order, or reported missing at the drain. A scoreboard whose
-- ---- inputs and outputs do not balance has lost track of something,
-- ---- and this is the one check that notices without knowing WHICH.
chk(to_integer(unsigned(n_issued)) =
to_integer(unsigned(n_matched)) + to_integer(unsigned(n_mismatch))
+ to_integer(unsigned(n_order)) + to_integer(unsigned(n_missing))
+ n_xdiv,
"the transfers that left the table do not account for the transfers that entered it -- something was lost without being reported");
chk(n_ts = 48, "not every tag state was crossed with every input combination");
chk(n_oc = 9, "not every table occupancy was reached");
chk(to_integer(unsigned(n_matched)) > 0, "nothing was ever matched");
chk(to_integer(unsigned(n_mismatch)) > 0, "a payload mismatch was never seen");
chk(to_integer(unsigned(n_unexpected)) > 0, "an unexpected completion was never seen");
chk(to_integer(unsigned(n_duplicate)) > 0, "a duplicate delivery was never seen");
chk(to_integer(unsigned(n_reissue)) > 0, "a reused identity was never seen");
chk(to_integer(unsigned(n_missing)) > 0, "a missing completion was never seen");
chk(to_integer(unsigned(n_order)) > 0, "an in-endpoint order violation was never seen");
chk(to_integer(unsigned(n_retries)) > 0, "a retry was never seen");
write(ln, string'("REACH tagstate-x-input=") & integer'image(n_ts) &
"/48 occupancy=" & integer'image(n_oc) &
"/9 steps=" & integer'image(n_steps));
writeline(output, ln);
write(ln, string'("COUNTERS issued=") & integer'image(to_integer(unsigned(n_issued))) &
" matched=" & integer'image(to_integer(unsigned(n_matched))) &
" retries=" & integer'image(to_integer(unsigned(n_retries))) &
" mismatch=" & integer'image(to_integer(unsigned(n_mismatch))) &
" unexpected=" & integer'image(to_integer(unsigned(n_unexpected))) &
" duplicate=" & integer'image(to_integer(unsigned(n_duplicate))) &
" reissue=" & integer'image(to_integer(unsigned(n_reissue))) &
" missing=" & integer'image(to_integer(unsigned(n_missing))) &
" order=" & integer'image(to_integer(unsigned(n_order))));
writeline(output, ln);
if errors = 0 then
write(ln, string'("PASS: 0 errors in ") & integer'image(checks) & " checks");
else
write(ln, string'("FAIL: ") & integer'image(errors) & " errors in " &
integer'image(checks) & " checks");
end if;
writeline(output, ln);
done <= true;
wait;
end process;
end architecture sim;14. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| tag state x input | 48 / 48 | 48 / 48 | 48 / 48 |
| table occupancy | 9 / 9 | 9 / 9 | 9 / 9 |
| Steps | 40048 | 40048 | 40048 |
| Checks executed | 402204 | 402204 | 402204 |
| transfers issued | 5839 | 5881 | 5774 |
| matched | 506 | 506 | 527 |
| retries (not duplicates) | 21 | 21 | 65 |
| — mismatch | 5006 | 5057 | 4868 |
| — unexpected | 259 | 230 | 223 |
| — duplicate | 4963 | 4935 | 4976 |
| — reissue | 4954 | 5023 | 5201 |
| — missing | 261 | 261 | 266 |
| — in-endpoint order | 47 | 45 | 113 |
| Result | PASS | PASS | PASS |
The error counts dwarf the match count because the random phase drives both channels independently with random tags and payloads — almost nothing lines up by chance, which is the point of having it. The directed phases are where matches come from, and the conservation check ties the two populations together.
15. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| S5 | a reused identity silently overwrites the one in flight | 58429 | 59904 | 60712 |
| S2 | no RETIRED state — a duplicate is reported as "unexpected" | 21788 | 21861 | 21516 |
| S1 | match on VALUE instead of identity | 3425 | 2986 | 563 |
| S7 | only the low nibble of the payload is compared | 928 | 891 | 8389 |
| S4 | a retry retires the transfer | 112 | 112 | 756 |
| S6 | the drain does not report what it found | 312 | 312 | 317 |
| S3 | in-endpoint order is not checked | 184 | 178 | 382 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages, all counts distinct.
S5 is the largest, which is right: overwriting an identity that is still in flight loses the earlier transfer and reports nothing, so it corrupts the table and every subsequent decision made from it.
S2 is the mis-classification mutation. It misses nothing — a duplicate is still reported — but it reports it as "unexpected", which sends the investigation to the wrong place. It scores 21 000 because the suite checks the code, not merely the pulse. A suite that only counted errors would score it zero.
16. Debugging Walkthrough: The Regression That Went Green When the Bug Got Worse
The report. A USB device controller has a scoreboard that has been clean for months. A firmware change then makes a bulk endpoint occasionally deliver the same packet twice. The scoreboard stays clean. A week later a second bug drops packets on the same endpoint — and the scoreboard still stays clean.
Step 1 — is the scoreboard connected? Yes: it reports matches, and the match count is right.
Step 2 — inject a deliberate corruption. Change one byte of one payload. The scoreboard catches it instantly. So it is working, for some definition of working.
Step 3 — inject a deliberate duplicate. It is caught... sometimes. Duplicating a packet with a unique payload is caught; duplicating one whose payload also occurs elsewhere in flight is not.
Step 4 — read the matching code. It searches the expected list for a payload equal to the one that arrived. It is a value matcher.
Step 5 — and that is why two bugs were quieter than one. The duplicate consumed an expectation belonging to a different transfer. On its own that left one expectation unmatched at the end of the test, which the drain would have reported — except the drain was only checking for an empty list, and the second bug (a dropped packet) supplied exactly the missing arrival to balance it.
Step 6 — the two bugs were each other's alibi. Adding the second bug made the report cleaner, because the counts now balanced. That is the signature to remember.
17. UVM: Where the Real Identity Comes From
// In a UVM environment the identity is not invented by the scoreboard -- it
// is whatever the DESIGN already uses to correlate a request with its
// completion, and the scoreboard's job is to use the same one.
//
// xHCI the TRB pointer in the Transfer Event
// a device (endpoint, data toggle, frame)
// an AXI bridge the transaction ID
// a DMA engine the descriptor address
//
// Where no such field exists, one must be ADDED to the monitors -- a
// sequence number stamped by the driver and carried through -- and that is
// a real cost that is worth paying. The alternative is the scoreboard in
// section 1.
class usb_xfer_item extends uvm_sequence_item;
`uvm_object_utils(usb_xfer_item)
// ---- THE IDENTITY. Not the payload. ----
rand bit [15:0] xfer_id;
rand bit [3:0] ep;
rand byte unsigned payload[];
rand bit is_retry;
constraint c_len { payload.size() inside {[0:64]}; }
function new(string name = "usb_xfer_item"); super.new(name); endfunction
// Deliberately NOT do_compare()'s default, which compares every field.
// Identity comparison and payload comparison are separate operations and
// the class exposes them separately so that a scoreboard cannot
// accidentally conflate them.
function bit same_identity(usb_xfer_item other);
return (xfer_id == other.xfer_id) && (ep == other.ep);
endfunction
function bit same_payload(usb_xfer_item other);
if (payload.size() != other.payload.size()) return 0;
foreach (payload[i]) if (payload[i] != other.payload[i]) return 0;
return 1;
endfunction
endclass
class usb_xfer_scoreboard extends uvm_scoreboard;
`uvm_component_utils(usb_xfer_scoreboard)
uvm_analysis_imp_exp #(usb_xfer_item, usb_xfer_scoreboard) exp_ap;
uvm_analysis_imp_act #(usb_xfer_item, usb_xfer_scoreboard) act_ap;
// An ASSOCIATIVE ARRAY keyed by identity, not a queue. Out-of-order
// completion across endpoints is legal, so a queue is wrong; and a
// linear search over payloads is the bug in section 1.
usb_xfer_item outstanding[bit [15:0]];
// The identities that have already completed. This is the third state
// from the hardware table, and it is what lets a duplicate be NAMED.
bit retired[bit [15:0]];
// Per-endpoint issue and completion order.
int unsigned iss_seq[bit [3:0]];
int unsigned cmp_seq[bit [3:0]];
int unsigned xfer_seq[bit [15:0]];
int unsigned n_issued, n_matched, n_retries;
int unsigned n_err[string];
function new(string name, uvm_component parent);
super.new(name, parent);
exp_ap = new("exp_ap", this);
act_ap = new("act_ap", this);
endfunction
function void flag(string cause, string detail);
n_err[cause]++;
`uvm_error("SB", $sformatf("%s: %s", cause, detail))
endfunction
// ---- the issue channel ----
function void write_exp(usb_xfer_item t);
if (outstanding.exists(t.xfer_id)) begin
// The identity space has been violated. The earlier transfer is now
// untrackable: it can never be matched, because its identity belongs
// to something else.
flag("REISSUE",
$sformatf("transfer id 0x%04h was issued again while still outstanding -- the earlier transfer can no longer be matched by anything",
t.xfer_id));
return;
end
outstanding[t.xfer_id] = t;
xfer_seq[t.xfer_id] = iss_seq[t.ep]++;
void'(retired.delete(t.xfer_id)); // recycling an identity is legal
n_issued++;
endfunction
// ---- the completion channel ----
function void write_act(usb_xfer_item t);
if (!outstanding.exists(t.xfer_id)) begin
if (retired.exists(t.xfer_id))
flag("DUPLICATE",
$sformatf("transfer id 0x%04h was delivered a second time", t.xfer_id));
else
flag("UNEXPECTED",
$sformatf("a completion arrived for id 0x%04h, which was never issued",
t.xfer_id));
return;
end
begin
usb_xfer_item e = outstanding[t.xfer_id];
// ---- A RETRY IS NOT A DELIVERY. Nothing is retired. ----
if (t.is_retry) begin
n_retries++;
return;
end
// Identity found the partner. NOW compare the payload.
if (!e.same_payload(t))
flag("MISMATCH",
$sformatf("id 0x%04h returned %0d bytes that do not match the %0d sent",
t.xfer_id, t.payload.size(), e.payload.size()));
else if (xfer_seq[t.xfer_id] != cmp_seq[e.ep])
flag("ORDER",
$sformatf("id 0x%04h completed at position %0d on endpoint %0d, expected %0d -- within one endpoint, order is not optional",
t.xfer_id, xfer_seq[t.xfer_id], e.ep, cmp_seq[e.ep]));
else
n_matched++;
// ---- However it left, it consumed its place in the order. ----
//
// Advancing cmp_seq only on a match leaves a permanent hole after the
// first failure, and every later completion on that endpoint is then
// reported out of order.
cmp_seq[e.ep]++;
retired[t.xfer_id] = 1;
void'(outstanding.delete(t.xfer_id));
end
endfunction
// ---- THE DRAIN. Data the design accepted and never returned. ----
function void check_phase(uvm_phase phase);
foreach (outstanding[id]) begin
usb_xfer_item e = outstanding[id];
n_err["MISSING"]++;
`uvm_error("SB",
$sformatf("transfer id 0x%04h on endpoint %0d was issued and never completed -- the run is over and nothing more is coming",
id, e.ep))
cmp_seq[e.ep]++;
end
endfunction
function void report_phase(uvm_phase phase);
int unsigned total = 0;
foreach (n_err[c]) total += n_err[c];
`uvm_info("SB", $sformatf("issued=%0d matched=%0d retries=%0d errors=%0d %p",
n_issued, n_matched, n_retries, total, n_err), UVM_LOW)
// ---- CONSERVATION. Everything that went in came out, once. ----
if (n_issued != n_matched + n_err["MISMATCH"] + n_err["ORDER"]
+ n_err["MISSING"])
`uvm_error("CONSERVATION",
$sformatf("%0d transfers were issued but only %0d are accounted for -- something left the table without being reported",
n_issued, n_matched + n_err["MISMATCH"] + n_err["ORDER"]
+ n_err["MISSING"]))
// A scoreboard that matched nothing is not a passing scoreboard.
if (n_matched == 0)
`uvm_error("COVERAGE",
"nothing was ever matched -- the scoreboard was either not connected or the test drove nothing, and either way its silence means nothing")
endfunction
endclass18. Common Misconceptions
"Comparing payloads is what a scoreboard does." Comparing payloads is the second half. Finding the right partner is the first, and doing both with one comparison does neither.
"Identical payloads are a corner case." Zero-length reports, keepalives, status polls and cleared buffers make them the common case.
"A queue is fine if the design completes in order." Across endpoints it does not, and a queue flags every legal interleaving.
"Order does not matter — the host will check." Within an endpoint it will not; the stream is simply corrupt.
"One valid bit per entry is enough." Then a duplicate and a phantom completion are the same report, and the investigation goes to the wrong place.
"A retry is a second delivery." It is the protocol working, and flagging it makes the scoreboard unusable on a busy bus.
"Advance the sequence number on success." One loss then makes every later completion on that endpoint look out of order.
"An identity must be unique for the whole run." Only among transfers that can be in flight together — which is what makes a finite tag space work.
"Errors going down means things are improving." For a value matcher it can mean two bugs are cancelling.
19. Exercises
1. Construct the smallest traffic pattern for which a value-matching scoreboard reports zero errors while two transfers are genuinely wrong. Then show it cannot be fixed by adding a third payload.
2. S2 scores 21 800 and misses nothing at all. Explain what it does report, why the suite catches it, and what a suite that only counted errors would conclude.
3. S1 scores 563 in all three languages with the random phase removed, and 3425 / 2986 / 563 with it. Work out what property of a mutation makes its directed score reproducible and its random score not.
4. Revert §5 — advance cmp_seq only on a match — and derive the number of order violations produced by one missing transfer followed by N good ones.
5. The drain returns RETIRED tags to EMPTY. Show the false positive that appears if it does not, and the missed duplicate that appears if retired is cleared too eagerly.
6. Write the conservation check for the UVM scoreboard in §17 including the retry path, and say why n_retries must not appear in it.
20. Summary
| Idea | Why it matters |
|---|---|
| Match on identity, then check value | identity finds the partner; value checks it |
| Identical payloads are common | keepalives, status polls, cleared buffers |
| A duplicate and a loss cancel | a value matcher gets quieter as things get worse |
| It cannot be a FIFO | cross-endpoint reordering is legal and constant |
| ...but in-endpoint order is a bug | a corrupt stream the host will not check |
| A retired tag is not an empty tag | or a duplicate is reported as a surprise |
| A retry is not a delivery | or every flow-controlled transfer is flagged |
| Every departure consumes its order slot | or one loss becomes N order violations |
| Identities are reusable | unique among transfers in flight, not for the run |
| Both channels read the table as it was | a same-tag collision is the model bug random finds |
| Conservation catches the unanticipated | things in must equal things out |
| Decompose scores into directed + random | before calling a cross-language spread a finding |
| 48/48 and 9/9, 402 204 checks | 7 mutations, all killed in 3 languages |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o sb_v.out sb_v.v sb_v_tb.v && ./sb_v.out |
| SystemVerilog | iverilog -g2012 -o sb_sv.out sb_sv.sv sb_sv_tb.sv && ./sb_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a sb_vhdl.vhd sb_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_sb_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_sb_vhdl |
| One mutation | iverilog -g2005 -DMUT_S1 -o mm sb_v_mut.v sb_v_tb.v && ./mm |
All three implementations pass with 0 errors: every tag state crossed with every input combination, every table occupancy reached by real issues, and the conservation of transfers into and out of the table checked at the end of the run.
Chapter 24.4 — USB Functional Coverage asks what all of this has actually covered. The trap there is a reporting one: an unreachable bin and an untested bin look identical in a coverage report — both show 0% — and the two demand opposite responses. One needs a test written; the other needs the bin deleted, because chasing it wastes the effort that should have gone into the first.
Continue learning
Related tutorials
- Related topic
USB Protocol Checkers
Every ordering rule says what must happen next, and none of them fires when nothing happens at all — the timeout is a checker's only liveness tool, and a checker with false positives gets switched off.
- Related topic
USB Assertions
“Eventually” has no failing case, so it cannot be checked in a finite run — every real liveness check is bounded, a window has two edges, and an obligation still outstanding at end of test is a failure, not an unknown.
- Related topic
USB Functional Coverage
An unreachable bin and an untested bin both read 0% and demand opposite responses — and an exclusion is a claim about the design, so a bin that is excluded and then hit must fail.
- Related topic
USB VIP Usage
A VIP is a second implementation of the specification, so where it and the design disagree one of them is wrong — and the adapter's job is to make that visible, never to resolve it in a shim.
Standards & specifications
- Governing standard
- USB-IF (Universal Serial Bus Specification)(opens USB Implementers Forum (USB-IF) in a new tab)
Defines the USB bus — its electrical signalling, connectors, packet and transaction model, device framework and the descriptors a device must expose — together with the device-class specifications layered on it. It does not define host-controller register interfaces (xHCI and EHCI are separate documents) nor any operating system's driver architecture.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the USB curriculum.
