USB · Module 25
Transfer Errors
Retries mask the error rate — a link losing a third of its traffic reports a perfect transfer success rate, because the denominator everybody uses is the wrong one.
Chapter 25.3 looked at one endpoint answering one token. This chapter moves up one level, to the transfer built out of those transactions — and to the reason a transfer-level error count is close to useless.
1. A Transfer Is Not One Event on the Wire
A USB transfer is a sequence of attempts. The host issues a transaction; if it fails — CRC, timeout, bad PID — the host simply issues it again, up to a fixed budget. If any attempt succeeds, the transfer succeeded.
Three attempts, one transfer, one success
2. Retries Mask the Error Rate
Count that transfer the way every tool counts it and you get:
transfers attempted 1
transfers failed 0
transfer error rate 0.0%
...on a link that destroyed two out of three transactions.The testbench in section 13 measures this rather than asserting it. Phase 2 runs 200 transfers, each of which fails once and then succeeds:
PHASE2 transfer-level failures=0 attempt-level failures=200/400Zero, against fifty percent.
3. The Denominator Is Attempts, Not Transfers
So the fix is a different denominator, and it is the whole of the chapter in one line:
WRONG failed transfers
------------------
total transfers
RIGHT failed attempts
------------------
total attemptsThe two differ by exactly the retry factor, and the retry factor is the thing you were trying to measure in the first place. The design exposes both counts — n_xfer as well as n_attempt — deliberately, because the useful diagnostic is not either number but their ratio.
In the Verilog run's random phase:
attempts 39010
transfers 27774 ratio 1.40
failed attempts 12012
failed transfers 776 ratio 15.5
15 out of every 16 failures never reached the transfer layer.4. A Retry That Succeeded Is Still Evidence
"It worked" is not "it was fine". A transfer that needed two attempts consumed three times the bus bandwidth, three times the power, and had one attempt left before it became a visible failure.
5. The Class Names the Layer
The second question a triage block has to answer is whose bug is it, and the error class answers it:
| Class | Layer | What it means |
|---|---|---|
| CRC16, bit-stuff | PHY | the wire said something other than what was sent |
| timeout, bad PID | PROTOCOL | nobody answered, or answered wrongly |
| STALL, babble | FIRMWARE | the device's software did this on purpose, or lost track |
The useful output is not the total; it is the distribution. A hundred errors that are all CRC is a cable. A hundred split evenly across all six is a power-supply problem that looks like everything at once.
6. And the Layer Total Is Not the Largest Class
This one costs real time, and the arithmetic is the entire argument:
CRC 40 PHY
timeout 30 PROTOCOL
bad PID 30 PROTOCOL
largest CLASS: CRC, 40 -> "it's the cable"
largest LAYER: PROTOCOL, 60 -> it's the controller
Same data. Different team. Different week.Ranking classes instead of summing layers picks the wrong team about as often as not, and it does it in exactly the case where the evidence is ambiguous enough that nobody notices the method was wrong. The testbench drives this case directly in phase 5:
PHASE5 largest class = CRC (40), dominant layer = 1 (phy=40 proto=60 fw=0)7. Two Events in One Cycle Need Two Pulses
The block reports two independent things, and there is a cycle in which both happen:
- the attempt that exhausts the retry budget is also
- the attempt that takes a layer over its alarm threshold
Fold both into one err_code field and the last assignment wins. One of the two events is silently deleted, and which one depends on the order the branches happen to be written in.
8. What We Are Building
usb_error_triage — two denominators, seven classes, three layers
9. Verilog-2005 Implementation
// usb_error_triage -- the two questions a transfer error log has to answer,
// and the reason the obvious way of asking both gives the wrong answer.
//
// QUESTION ONE: HOW BAD IS IT? AND RETRIES HIDE THE ANSWER
//
// A USB transfer is not one event on the wire. It is a sequence of
// ATTEMPTS: the host issues a transaction, and if it fails -- CRC, timeout,
// bad PID -- the host simply issues it again. Up to MAX_RETRY times. If any
// attempt succeeds, the TRANSFER succeeded, and nothing above the host
// controller ever hears that anything went wrong.
//
// attempts: FAIL FAIL OK
// transfer: SUCCESS
// application sees: a normal, successful transfer
//
// So the error rate measured at the transfer layer is ~0 on a link that is
// losing one transaction in three. That is not a rounding error; it is a
// metric reporting the opposite of the truth, and it is why "the device
// works fine" and "the link is nearly dead" are routinely both true.
//
// THE DENOMINATOR IS ATTEMPTS, NOT TRANSFERS.
//
// errors/transfers understates the damage by exactly the retry factor, and
// the retry factor is the thing you are trying to measure.
//
// A RETRY THAT SUCCEEDED IS STILL EVIDENCE
//
// "It worked" is not "it was fine". A transfer that needed two attempts
// consumed three times the bus bandwidth, three times the power, and had
// one attempt left before it became a visible failure. It is the single
// best leading indicator there is, and it is invisible in every
// success/failure metric.
//
// So a completed transfer that took any retries at all is REPORTED -- once
// per transfer, not once per attempt, which is chapter 25.3's rule.
//
// QUESTION TWO: WHOSE BUG IS IT? AND THE CLASS NAMES THE LAYER
//
// Six error classes, three layers:
//
// PHY CRC16, bit-stuff the wire, the connector, the cable
// PROTOCOL timeout, bad PID the controller, the schedule
// FIRMWARE STALL, babble the device's software
//
// The useful output is not the total. It is the DISTRIBUTION, because the
// distribution names the layer, and the layer names the team. A hundred
// errors that are all CRC is a cable. A hundred errors split evenly across
// all six is a power-supply problem that looks like everything at once.
//
// AND THE LAYER TOTAL IS NOT THE LARGEST CLASS
//
// Forty CRC errors, thirty timeouts and thirty bad PIDs is SIXTY protocol
// errors against forty PHY errors -- the protocol layer, by a clear margin,
// even though the single largest class belongs to the PHY. Ranking classes
// instead of summing layers picks the wrong team about as often as not.
module usb_error_triage #(
parameter integer MAX_RETRY = 3, // attempts before the host gives up
parameter integer LAYER_ALARM = 32 // errors in one layer before alarming
) (
input wire clk,
input wire rst_n,
input wire attempt_valid, // one transaction attempt completed
input wire [2:0] err_class, // 0 = it worked; see the localparams
input wire eot,
output wire [7:0] retry_depth, // retries used by the transfer in flight
output wire [7:0] worst_retry,
output wire [1:0] dominant_layer,
output wire err_pulse,
output wire [1:0] err_code,
// ---- The layer alarm gets its OWN pulse, not a code on the shared one.
//
// A layer crossing its threshold and a transfer running out of retries
// are independent events that happen in the same cycle all the time --
// the attempt that exhausts the budget is also an attempt that counts
// towards a layer. Folding both into one code field means the last
// assignment wins and one of the two events is silently deleted.
//
// Two events, two pulses. It is one extra wire and it removes an entire
// class of "we definitely reported that" arguments.
output wire alarm_pulse,
output wire [1:0] alarm_layer,
// ---- the two denominators, both exposed, deliberately ----
output reg [31:0] n_attempt, // the RIGHT denominator
output reg [31:0] n_xfer, // the WRONG one, kept so it can be
// compared against the right one
output reg [31:0] n_fail, // failed ATTEMPTS
output reg [31:0] n_xfer_fail, // transfers that ran out of retries
output reg [31:0] n_masked, // transfers that succeeded ON A RETRY
output reg [31:0] n_alarm, // layer thresholds crossed (max 3)
output reg [31:0] n_crc,
output reg [31:0] n_stuff,
output reg [31:0] n_timeout,
output reg [31:0] n_pid,
output reg [31:0] n_stall,
output reg [31:0] n_babble,
output reg [31:0] n_badclass, // an encoding that is not an error class
output reg [31:0] n_phy,
output reg [31:0] n_proto,
output reg [31:0] n_fw
);
localparam [2:0] E_NONE = 3'd0, // the attempt succeeded
E_CRC = 3'd1, // PHY
E_STUFF = 3'd2, // PHY
E_TMO = 3'd3, // PROTOCOL
E_PID = 3'd4, // PROTOCOL
E_STALL = 3'd5, // FIRMWARE
E_BABBL = 3'd6, // FIRMWARE
E_RSVD = 3'd7; // not an error class at all
localparam [1:0] L_PHY = 2'd0, L_PROTO = 2'd1, L_FW = 2'd2, L_NONE = 2'd3;
localparam [1:0] T_NONE = 2'd0,
T_MASKED = 2'd1, // a transfer succeeded ON A RETRY
T_XFER_FAIL = 2'd2; // retries exhausted
// ---- The retry budget is compared at FULL WIDTH. ----
//
// The obvious way to write this is to keep the depth in two bits -- a
// budget of 3 needs no more -- and compare against `MAX_RETRY[1:0]`. That
// works, and it makes the module parameterised in name only: with
// MAX_RETRY = 5 the part-select is 1, and so is `localparam [1:0] MAXR =
// MAX_RETRY`. BOTH forms truncate, silently, and the block gives up after
// one failed attempt while its parameter says five.
//
// Narrowing a parameter to the width its DEFAULT value happens to need is
// the bug. The counter and the comparison are eight bits wide because the
// parameter is an integer, and the testbench instantiates a second copy
// with MAX_RETRY = 5 specifically to prove it.
localparam [7:0] MAXR = MAX_RETRY;
// ---- The class-to-layer map, written as a function. ----
//
// A function rather than a table of constants, so the reason each class
// belongs where it does sits next to the assignment. E_RSVD maps to
// L_NONE on purpose: an encoding that is not an error class must not be
// attributed to a layer, because attributing it puts a bug on somebody's
// desk on the strength of a value nobody defined.
function [1:0] layer_of;
input [2:0] c;
begin
case (c)
E_CRC, E_STUFF: layer_of = L_PHY; // the wire said the wrong thing
E_TMO, E_PID: layer_of = L_PROTO; // nobody answered, or wrongly
E_STALL, E_BABBL: layer_of = L_FW; // the device's software did
default: layer_of = L_NONE; // E_NONE and E_RSVD
endcase
end
endfunction
reg [7:0] dep_r;
reg [7:0] worst_r;
reg [1:0] ec_r;
reg er_r;
reg [2:0] alarmed_r; // one bit per layer: report the CROSSING, once
reg al_r;
reg [1:0] alay_r;
assign alarm_pulse = al_r;
assign alarm_layer = alay_r;
assign retry_depth = dep_r;
assign worst_retry = worst_r;
assign err_pulse = er_r;
assign err_code = ec_r;
// ---- The dominant layer is the largest LAYER TOTAL. ----
//
// Not the layer owning the largest single class. Forty CRC against thirty
// timeouts and thirty bad PIDs is a PROTOCOL problem, and ranking classes
// reports the PHY.
assign dominant_layer = ((n_phy >= n_proto) && (n_phy >= n_fw)) ? L_PHY :
((n_proto >= n_fw)) ? L_PROTO :
L_FW;
reg [7:0] dep_n;
reg [7:0] worst_n;
reg [1:0] ec_n;
reg er_n;
reg [2:0] alarmed_n;
reg al_n;
reg [1:0] alay_n;
reg [1:0] lay_n;
reg fail_n, ok_n, xfer_n, xfail_n, mask_n;
reg [31:0] phy_n, proto_n, fw_n;
always @* begin
dep_n = dep_r;
worst_n = worst_r;
ec_n = T_NONE;
er_n = 1'b0;
alarmed_n = alarmed_r;
al_n = 1'b0;
alay_n = L_NONE;
lay_n = layer_of(err_class);
fail_n = 1'b0; ok_n = 1'b0; xfer_n = 1'b0;
xfail_n = 1'b0; mask_n = 1'b0;
phy_n = n_phy; proto_n = n_proto; fw_n = n_fw;
if (eot) begin
// Nothing: the counters are the report.
end else if (attempt_valid) begin
if (err_class == E_NONE) begin
// ---- The attempt succeeded, so the TRANSFER succeeded. ----
ok_n = 1'b1;
xfer_n = 1'b1;
if (dep_r != 8'd0) begin
// ---- ...but it only succeeded because of a retry. ----
//
// The transfer layer will record a success and the application
// will never hear about this. It is the best leading indicator
// on the bus and it is invisible in every success/failure metric.
er_n = 1'b1; ec_n = T_MASKED;
mask_n = 1'b1;
end
dep_n = 8'd0;
end else begin
// ---- A failed ATTEMPT. Counted here, in the right denominator. ----
fail_n = 1'b1;
if (lay_n == L_PHY) phy_n = n_phy + 32'd1;
else if (lay_n == L_PROTO) proto_n = n_proto + 32'd1;
else if (lay_n == L_FW) fw_n = n_fw + 32'd1;
// L_NONE: E_RSVD. Counted as a failed attempt, because it was one,
// and attributed to nobody, because nothing defines it.
if (dep_r + 8'd1 >= MAXR) begin
// ---- Retries exhausted. NOW the transfer has failed. ----
//
// This is the only error the layers above the host controller
// ever see, and by the time it arrives MAX_RETRY-1 attempts have
// already been thrown away unlogged.
er_n = 1'b1; ec_n = T_XFER_FAIL;
xfail_n = 1'b1;
xfer_n = 1'b1;
dep_n = 8'd0;
end else begin
dep_n = dep_r + 8'd1;
if (dep_n > worst_r) worst_n = dep_n;
end
// ---- The layer alarm: reported on the CROSSING, once. ----
//
// A layer that is over threshold stays over threshold, so alarming
// on the state rather than the transition reproduces chapter 25.3's
// flood exactly.
if ((lay_n == L_PHY) && (phy_n >= LAYER_ALARM) && !alarmed_r[0]) begin
al_n = 1'b1; alay_n = L_PHY; alarmed_n[0] = 1'b1;
end else if ((lay_n == L_PROTO) && (proto_n >= LAYER_ALARM)
&& !alarmed_r[1]) begin
al_n = 1'b1; alay_n = L_PROTO; alarmed_n[1] = 1'b1;
end else if ((lay_n == L_FW) && (fw_n >= LAYER_ALARM)
&& !alarmed_r[2]) begin
al_n = 1'b1; alay_n = L_FW; alarmed_n[2] = 1'b1;
end
end
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
dep_r <= 8'd0;
worst_r <= 8'd0;
ec_r <= T_NONE;
er_r <= 1'b0;
alarmed_r <= 3'd0;
al_r <= 1'b0;
alay_r <= L_NONE;
n_attempt <= 32'd0; n_xfer <= 32'd0; n_fail <= 32'd0;
n_xfer_fail <= 32'd0; n_masked <= 32'd0; n_alarm <= 32'd0;
n_crc <= 32'd0; n_stuff <= 32'd0; n_timeout <= 32'd0;
n_pid <= 32'd0; n_stall <= 32'd0; n_babble <= 32'd0;
n_badclass<= 32'd0;
n_phy <= 32'd0; n_proto <= 32'd0; n_fw <= 32'd0;
end else begin
dep_r <= dep_n;
worst_r <= worst_n;
ec_r <= ec_n;
er_r <= er_n;
alarmed_r <= alarmed_n;
al_r <= al_n;
alay_r <= alay_n;
if (attempt_valid && !eot) n_attempt <= n_attempt + 32'd1;
if (xfer_n) n_xfer <= n_xfer + 32'd1;
if (fail_n) n_fail <= n_fail + 32'd1;
if (xfail_n) n_xfer_fail <= n_xfer_fail + 32'd1;
if (mask_n) n_masked <= n_masked + 32'd1;
if (al_n) n_alarm <= n_alarm + 32'd1;
n_phy <= phy_n;
n_proto <= proto_n;
n_fw <= fw_n;
if (fail_n) begin
case (err_class)
E_CRC: n_crc <= n_crc + 32'd1;
E_STUFF: n_stuff <= n_stuff + 32'd1;
E_TMO: n_timeout <= n_timeout + 32'd1;
E_PID: n_pid <= n_pid + 32'd1;
E_STALL: n_stall <= n_stall + 32'd1;
E_BABBL: n_babble <= n_babble + 32'd1;
default: n_badclass<= n_badclass+ 32'd1;
endcase
end
end
end
endmodule10. SystemVerilog Implementation
// usb_error_triage -- the two questions a transfer error log has to answer,
// and the reason the obvious way of asking both gives the wrong answer.
//
// QUESTION ONE: HOW BAD IS IT? AND RETRIES HIDE THE ANSWER
//
// A USB transfer is not one event on the wire. It is a sequence of
// ATTEMPTS: the host issues a transaction, and if it fails -- CRC, timeout,
// bad PID -- the host simply issues it again. Up to MAX_RETRY times. If any
// attempt succeeds, the TRANSFER succeeded, and nothing above the host
// controller ever hears that anything went wrong.
//
// attempts: FAIL FAIL OK
// transfer: SUCCESS
// application sees: a normal, successful transfer
//
// So the error rate measured at the transfer layer is ~0 on a link that is
// losing one transaction in three. That is not a rounding error; it is a
// metric reporting the opposite of the truth, and it is why "the device
// works fine" and "the link is nearly dead" are routinely both true.
//
// THE DENOMINATOR IS ATTEMPTS, NOT TRANSFERS.
//
// errors/transfers understates the damage by exactly the retry factor, and
// the retry factor is the thing you are trying to measure.
//
// A RETRY THAT SUCCEEDED IS STILL EVIDENCE
//
// "It worked" is not "it was fine". A transfer that needed two attempts
// consumed three times the bus bandwidth, three times the power, and had
// one attempt left before it became a visible failure. It is the single
// best leading indicator there is, and it is invisible in every
// success/failure metric.
//
// So a completed transfer that took any retries at all is REPORTED -- once
// per transfer, not once per attempt, which is chapter 25.3's rule.
//
// QUESTION TWO: WHOSE BUG IS IT? AND THE CLASS NAMES THE LAYER
//
// Six error classes, three layers:
//
// PHY CRC16, bit-stuff the wire, the connector, the cable
// PROTOCOL timeout, bad PID the controller, the schedule
// FIRMWARE STALL, babble the device's software
//
// The useful output is not the total. It is the DISTRIBUTION, because the
// distribution names the layer, and the layer names the team. A hundred
// errors that are all CRC is a cable. A hundred errors split evenly across
// all six is a power-supply problem that looks like everything at once.
//
// AND THE LAYER TOTAL IS NOT THE LARGEST CLASS
//
// Forty CRC errors, thirty timeouts and thirty bad PIDs is SIXTY protocol
// errors against forty PHY errors -- the protocol layer, by a clear margin,
// even though the single largest class belongs to the PHY. Ranking classes
// instead of summing layers picks the wrong team about as often as not.
package usb_triage_pkg;
typedef enum logic [2:0] {
E_NONE = 3'd0, // the attempt succeeded
E_CRC = 3'd1, // PHY
E_STUFF = 3'd2, // PHY
E_TMO = 3'd3, // PROTOCOL
E_PID = 3'd4, // PROTOCOL
E_STALL = 3'd5, // FIRMWARE
E_BABBL = 3'd6, // FIRMWARE
E_RSVD = 3'd7 // not an error class at all
} err_class_e;
typedef enum logic [1:0] {
L_PHY = 2'd0,
L_PROTO = 2'd1,
L_FW = 2'd2,
L_NONE = 2'd3 // attributed to NOBODY, deliberately
} layer_e;
typedef enum logic [1:0] {
T_NONE = 2'd0,
T_MASKED = 2'd1, // a transfer succeeded ON A RETRY
T_XFER_FAIL = 2'd2 // retries exhausted
} triage_e;
endpackage
module usb_error_triage
import usb_triage_pkg::*;
#(
parameter int MAX_RETRY = 3, // attempts before the host gives up
parameter int LAYER_ALARM = 32 // errors in one layer before alarming
) (
input logic clk,
input logic rst_n,
input logic attempt_valid, // one transaction attempt completed
input err_class_e err_class, // E_NONE means it worked
input logic eot,
output logic [7:0] retry_depth, // retries used by the transfer in flight
output logic [7:0] worst_retry,
output layer_e dominant_layer,
output logic err_pulse,
output triage_e err_code,
// ---- The layer alarm gets its OWN pulse, not a code on the shared one.
//
// A layer crossing its threshold and a transfer running out of retries
// are independent events that happen in the same cycle all the time --
// the attempt that exhausts the budget is also an attempt that counts
// towards a layer. Folding both into one code field means the last
// assignment wins and one of the two events is silently deleted.
//
// Two events, two pulses. It is one extra wire and it removes an entire
// class of "we definitely reported that" arguments.
output logic alarm_pulse,
output layer_e alarm_layer,
// ---- the two denominators, both exposed, deliberately ----
output logic [31:0] n_attempt, // the RIGHT denominator
output logic [31:0] n_xfer, // the WRONG one, kept so it can be
// compared against the right one
output logic [31:0] n_fail, // failed ATTEMPTS
output logic [31:0] n_xfer_fail, // transfers that ran out of retries
output logic [31:0] n_masked, // transfers that succeeded ON A RETRY
output logic [31:0] n_alarm, // layer thresholds crossed (max 3)
output logic [31:0] n_crc,
output logic [31:0] n_stuff,
output logic [31:0] n_timeout,
output logic [31:0] n_pid,
output logic [31:0] n_stall,
output logic [31:0] n_babble,
output logic [31:0] n_badclass, // an encoding that is not an error class
output logic [31:0] n_phy,
output logic [31:0] n_proto,
output logic [31:0] n_fw
);
// ---- The retry budget is compared at FULL WIDTH. ----
//
// The obvious way to write this is to keep the depth in two bits -- a
// budget of 3 needs no more -- and compare against `MAX_RETRY[1:0]`. That
// works, and it makes the module parameterised in name only: with
// MAX_RETRY = 5 the part-select is 1, and so is `localparam [1:0] MAXR =
// MAX_RETRY`. BOTH forms truncate, silently, and the block gives up after
// one failed attempt while its parameter says five.
//
// Narrowing a parameter to the width its DEFAULT value happens to need is
// the bug. The counter and the comparison are eight bits wide because the
// parameter is an int, and the testbench instantiates a second copy with
// MAX_RETRY = 5 specifically to prove it.
localparam logic [7:0] MAXR = 8'(MAX_RETRY);
// ---- The class-to-layer map, written as a function. ----
//
// A function rather than a table of constants, so the reason each class
// belongs where it does sits next to the assignment. E_RSVD maps to
// L_NONE on purpose: an encoding that is not an error class must not be
// attributed to a layer, because attributing it puts a bug on somebody's
// desk on the strength of a value nobody defined.
function automatic layer_e layer_of(input err_class_e c);
begin
case (c)
E_CRC, E_STUFF: layer_of = L_PHY; // the wire said the wrong thing
E_TMO, E_PID: layer_of = L_PROTO; // nobody answered, or wrongly
E_STALL, E_BABBL: layer_of = L_FW; // the device's software did
default: layer_of = L_NONE; // E_NONE and E_RSVD
endcase
end
endfunction
logic [7:0] dep_r;
logic [7:0] worst_r;
triage_e ec_r;
logic er_r;
logic [2:0] alarmed_r; // one bit per layer: report the CROSSING, once
logic al_r;
layer_e alay_r;
assign alarm_pulse = al_r;
assign alarm_layer = alay_r;
assign retry_depth = dep_r;
assign worst_retry = worst_r;
assign err_pulse = er_r;
assign err_code = ec_r;
// ---- The dominant layer is the largest LAYER TOTAL. ----
//
// Not the layer owning the largest single class. Forty CRC against thirty
// timeouts and thirty bad PIDs is a PROTOCOL problem, and ranking classes
// reports the PHY.
//
// Written as a function rather than a chain of ternaries: a conditional
// expression whose arms are enumeration literals needs an explicit cast
// in SystemVerilog, and Icarus rejects it outright. if/else over the same
// conditions needs no cast and reads better besides.
function automatic layer_e dominant(input logic [31:0] p,
input logic [31:0] r,
input logic [31:0] f);
begin
if ((p >= r) && (p >= f)) dominant = L_PHY;
else if (r >= f) dominant = L_PROTO;
else dominant = L_FW;
end
endfunction
assign dominant_layer = dominant(n_phy, n_proto, n_fw);
logic [7:0] dep_n;
logic [7:0] worst_n;
triage_e ec_n;
logic er_n;
logic [2:0] alarmed_n;
logic al_n;
layer_e alay_n;
layer_e lay_n;
logic fail_n, ok_n, xfer_n, xfail_n, mask_n;
logic [31:0] phy_n, proto_n, fw_n;
always_comb begin
dep_n = dep_r;
worst_n = worst_r;
ec_n = T_NONE;
er_n = 1'b0;
alarmed_n = alarmed_r;
al_n = 1'b0;
alay_n = L_NONE;
lay_n = layer_of(err_class);
fail_n = 1'b0; ok_n = 1'b0; xfer_n = 1'b0;
xfail_n = 1'b0; mask_n = 1'b0;
phy_n = n_phy; proto_n = n_proto; fw_n = n_fw;
if (eot) begin
// Nothing: the counters are the report.
end else if (attempt_valid) begin
if (err_class == E_NONE) begin
// ---- The attempt succeeded, so the TRANSFER succeeded. ----
ok_n = 1'b1;
xfer_n = 1'b1;
if (dep_r != 8'd0) begin
// ---- ...but it only succeeded because of a retry. ----
//
// The transfer layer will record a success and the application
// will never hear about this. It is the best leading indicator
// on the bus and it is invisible in every success/failure metric.
er_n = 1'b1; ec_n = T_MASKED;
mask_n = 1'b1;
end
dep_n = 8'd0;
end else begin
// ---- A failed ATTEMPT. Counted here, in the right denominator. ----
fail_n = 1'b1;
if (lay_n == L_PHY) phy_n = n_phy + 32'd1;
else if (lay_n == L_PROTO) proto_n = n_proto + 32'd1;
else if (lay_n == L_FW) fw_n = n_fw + 32'd1;
// L_NONE: E_RSVD. Counted as a failed attempt, because it was one,
// and attributed to nobody, because nothing defines it.
if ((dep_r + 8'd1) >= MAXR) begin
// ---- Retries exhausted. NOW the transfer has failed. ----
//
// This is the only error the layers above the host controller
// ever see, and by the time it arrives MAX_RETRY-1 attempts have
// already been thrown away unlogged.
er_n = 1'b1; ec_n = T_XFER_FAIL;
xfail_n = 1'b1;
xfer_n = 1'b1;
dep_n = 8'd0;
end else begin
dep_n = dep_r + 8'd1;
if (dep_n > worst_r) worst_n = dep_n;
end
// ---- The layer alarm: reported on the CROSSING, once. ----
//
// A layer that is over threshold stays over threshold, so alarming
// on the state rather than the transition reproduces chapter 25.3's
// flood exactly.
if ((lay_n == L_PHY) && (phy_n >= LAYER_ALARM) && !alarmed_r[0]) begin
al_n = 1'b1; alay_n = L_PHY; alarmed_n[0] = 1'b1;
end else if ((lay_n == L_PROTO) && (proto_n >= LAYER_ALARM)
&& !alarmed_r[1]) begin
al_n = 1'b1; alay_n = L_PROTO; alarmed_n[1] = 1'b1;
end else if ((lay_n == L_FW) && (fw_n >= LAYER_ALARM)
&& !alarmed_r[2]) begin
al_n = 1'b1; alay_n = L_FW; alarmed_n[2] = 1'b1;
end
end
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
dep_r <= 8'd0;
worst_r <= 8'd0;
ec_r <= T_NONE;
er_r <= 1'b0;
alarmed_r <= 3'd0;
al_r <= 1'b0;
alay_r <= L_NONE;
n_attempt <= 32'd0; n_xfer <= 32'd0; n_fail <= 32'd0;
n_xfer_fail <= 32'd0; n_masked <= 32'd0; n_alarm <= 32'd0;
n_crc <= 32'd0; n_stuff <= 32'd0; n_timeout <= 32'd0;
n_pid <= 32'd0; n_stall <= 32'd0; n_babble <= 32'd0;
n_badclass<= 32'd0;
n_phy <= 32'd0; n_proto <= 32'd0; n_fw <= 32'd0;
end else begin
dep_r <= dep_n;
worst_r <= worst_n;
ec_r <= ec_n;
er_r <= er_n;
alarmed_r <= alarmed_n;
al_r <= al_n;
alay_r <= alay_n;
if (attempt_valid && !eot) n_attempt <= n_attempt + 32'd1;
if (xfer_n) n_xfer <= n_xfer + 32'd1;
if (fail_n) n_fail <= n_fail + 32'd1;
if (xfail_n) n_xfer_fail <= n_xfer_fail + 32'd1;
if (mask_n) n_masked <= n_masked + 32'd1;
if (al_n) n_alarm <= n_alarm + 32'd1;
n_phy <= phy_n;
n_proto <= proto_n;
n_fw <= fw_n;
if (fail_n) begin
case (err_class)
E_CRC: n_crc <= n_crc + 32'd1;
E_STUFF: n_stuff <= n_stuff + 32'd1;
E_TMO: n_timeout <= n_timeout + 32'd1;
E_PID: n_pid <= n_pid + 32'd1;
E_STALL: n_stall <= n_stall + 32'd1;
E_BABBL: n_babble <= n_babble + 32'd1;
default: n_badclass<= n_badclass+ 32'd1;
endcase
end
end
end
endmodule11. VHDL-2008 Implementation
-- usb_error_triage -- the two questions a transfer error log has to answer,
-- and the reason the obvious way of asking both gives the wrong answer.
--
-- QUESTION ONE: HOW BAD IS IT? AND RETRIES HIDE THE ANSWER
--
-- A USB transfer is not one event on the wire. It is a sequence of
-- ATTEMPTS: the host issues a transaction, and if it fails -- CRC, timeout,
-- bad PID -- the host simply issues it again, up to MAX_RETRY times. If any
-- attempt succeeds, the TRANSFER succeeded, and nothing above the host
-- controller ever hears that anything went wrong.
--
-- attempts: FAIL FAIL OK
-- transfer: SUCCESS
-- application sees: a normal, successful transfer
--
-- So the error rate measured at the transfer layer is ~0 on a link losing
-- one transaction in three. That is not a rounding error; it is a metric
-- reporting the opposite of the truth, and it is why "the device works
-- fine" and "the link is nearly dead" are routinely both true.
--
-- THE DENOMINATOR IS ATTEMPTS, NOT TRANSFERS.
--
-- A RETRY THAT SUCCEEDED IS STILL EVIDENCE
--
-- A transfer that needed two attempts consumed three times the bus
-- bandwidth and had one attempt left before it became a visible failure. It
-- is the best leading indicator there is and it is invisible in every
-- success/failure metric, so it is reported -- once per transfer, not once
-- per attempt, which is chapter 25.3's rule.
--
-- QUESTION TWO: WHOSE BUG IS IT? AND THE CLASS NAMES THE LAYER
--
-- PHY CRC16, bit-stuff the wire, the connector, the cable
-- PROTOCOL timeout, bad PID the controller, the schedule
-- FIRMWARE STALL, babble the device's software
--
-- The useful output is the DISTRIBUTION, because the distribution names the
-- layer and the layer names the team.
--
-- AND THE LAYER TOTAL IS NOT THE LARGEST CLASS
--
-- Forty CRC errors, thirty timeouts and thirty bad PIDs is SIXTY protocol
-- errors against forty PHY errors -- the protocol layer by a clear margin,
-- even though the single largest class belongs to the PHY.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package usb_triage_pkg is
constant E_NONE : std_logic_vector(2 downto 0) := "000"; -- it worked
constant E_CRC : std_logic_vector(2 downto 0) := "001"; -- PHY
constant E_STUFF : std_logic_vector(2 downto 0) := "010"; -- PHY
constant E_TMO : std_logic_vector(2 downto 0) := "011"; -- PROTOCOL
constant E_PID : std_logic_vector(2 downto 0) := "100"; -- PROTOCOL
constant E_STALL : std_logic_vector(2 downto 0) := "101"; -- FIRMWARE
constant E_BABBL : std_logic_vector(2 downto 0) := "110"; -- FIRMWARE
constant E_RSVD : std_logic_vector(2 downto 0) := "111"; -- not a class
constant L_PHY : std_logic_vector(1 downto 0) := "00";
constant L_PROTO : std_logic_vector(1 downto 0) := "01";
constant L_FW : std_logic_vector(1 downto 0) := "10";
constant L_NONE : std_logic_vector(1 downto 0) := "11"; -- nobody
constant T_NONE : std_logic_vector(1 downto 0) := "00";
constant T_MASKED : std_logic_vector(1 downto 0) := "01";
constant T_XFER_FAIL : std_logic_vector(1 downto 0) := "10";
-- The class-to-layer map, as a function, so the reason each class belongs
-- where it does sits next to the assignment. E_RSVD maps to L_NONE on
-- purpose: an encoding nobody defined must not put a bug on a team's desk.
function layer_of (c : std_logic_vector(2 downto 0))
return std_logic_vector;
end package;
package body usb_triage_pkg is
function layer_of (c : std_logic_vector(2 downto 0))
return std_logic_vector is
begin
case c is
when E_CRC | E_STUFF => return L_PHY; -- the wire said wrong
when E_TMO | E_PID => return L_PROTO; -- nobody answered, or wrongly
when E_STALL | E_BABBL => return L_FW; -- the device's software did
when others => return L_NONE; -- E_NONE and E_RSVD
end case;
end function;
end package body;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_triage_pkg.all;
entity usb_error_triage is
generic (
MAX_RETRY : integer := 3;
LAYER_ALARM : integer := 32
);
port (
clk : in std_logic;
rst_n : in std_logic;
attempt_valid : in std_logic;
err_class : in std_logic_vector(2 downto 0);
eot : in std_logic;
retry_depth : out std_logic_vector(7 downto 0);
worst_retry : out std_logic_vector(7 downto 0);
dominant_layer : out std_logic_vector(1 downto 0);
err_pulse : out std_logic;
err_code : out std_logic_vector(1 downto 0);
-- The layer alarm gets its OWN pulse, not a code on the shared one. A
-- layer crossing its threshold and a transfer running out of retries are
-- independent events that happen in the same cycle all the time, and
-- folding both into one code field means the last assignment wins and
-- one of the two is silently deleted.
alarm_pulse : out std_logic;
alarm_layer : out std_logic_vector(1 downto 0);
n_attempt : out unsigned(31 downto 0); -- the RIGHT denominator
n_xfer : out unsigned(31 downto 0); -- the WRONG one, exposed so it
-- can be compared to the right
n_fail : out unsigned(31 downto 0);
n_xfer_fail : out unsigned(31 downto 0);
n_masked : out unsigned(31 downto 0);
n_alarm : out unsigned(31 downto 0);
n_crc : out unsigned(31 downto 0);
n_stuff : out unsigned(31 downto 0);
n_timeout : out unsigned(31 downto 0);
n_pid : out unsigned(31 downto 0);
n_stall : out unsigned(31 downto 0);
n_babble : out unsigned(31 downto 0);
n_badclass : out unsigned(31 downto 0);
n_phy : out unsigned(31 downto 0);
n_proto : out unsigned(31 downto 0);
n_fw : out unsigned(31 downto 0)
);
end entity;
architecture rtl of usb_error_triage is
-- ---- The retry budget is compared at FULL WIDTH. ----
--
-- The obvious way to write this is to keep the depth in two bits -- a
-- budget of 3 needs no more -- and compare against a two-bit conversion of
-- the generic. That works, and it makes the entity parameterised in name
-- only: to_unsigned(5, 2) is 1, so the block would give up after one
-- failed attempt while its generic says five.
--
-- Narrowing a generic to the width its DEFAULT value happens to need is
-- the bug. The testbench instantiates a second copy with MAX_RETRY = 5
-- specifically to prove this one is not narrowed.
signal dep_r : unsigned(7 downto 0);
signal worst_r : unsigned(7 downto 0);
signal ec_r : std_logic_vector(1 downto 0);
signal er_r : std_logic;
signal al_r : std_logic;
signal alay_r : std_logic_vector(1 downto 0);
signal alarmed_r : std_logic_vector(2 downto 0);
-- Initialised at declaration. The dominant_layer assignment below is
-- concurrent, so it evaluates once at time 0 before any reset has been
-- applied; uninitialised counters make numeric_std's ">=" report a
-- metavalue and return FALSE, which is a warning on every run that
-- trains people to ignore warnings.
signal att_c, xfer_c, fail_c, xfail_c, mask_c, alarm_c :
unsigned(31 downto 0) := (others => '0');
signal crc_c, stuff_c, tmo_c, pid_c, stall_c, babbl_c :
unsigned(31 downto 0) := (others => '0');
signal bad_c : unsigned(31 downto 0) := (others => '0');
signal phy_c, proto_c, fw_c :
unsigned(31 downto 0) := (others => '0');
begin
retry_depth <= std_logic_vector(dep_r);
worst_retry <= std_logic_vector(worst_r);
err_pulse <= er_r;
err_code <= ec_r;
alarm_pulse <= al_r;
alarm_layer <= alay_r;
n_attempt <= att_c;
n_xfer <= xfer_c;
n_fail <= fail_c;
n_xfer_fail <= xfail_c;
n_masked <= mask_c;
n_alarm <= alarm_c;
n_crc <= crc_c;
n_stuff <= stuff_c;
n_timeout <= tmo_c;
n_pid <= pid_c;
n_stall <= stall_c;
n_babble <= babbl_c;
n_badclass <= bad_c;
n_phy <= phy_c;
n_proto <= proto_c;
n_fw <= fw_c;
-- ---- The dominant layer is the largest LAYER TOTAL. ----
--
-- Not the layer owning the largest single class. Forty CRC against thirty
-- timeouts and thirty bad PIDs is a PROTOCOL problem, and ranking classes
-- reports the PHY.
dominant_layer <= L_PHY when (phy_c >= proto_c) and (phy_c >= fw_c) else
L_PROTO when (proto_c >= fw_c) else
L_FW;
process (clk, rst_n)
variable l : std_logic_vector(1 downto 0);
variable dep_v : unsigned(7 downto 0);
variable wrst_v : unsigned(7 downto 0);
variable ec_v : std_logic_vector(1 downto 0);
variable er_v : std_logic;
variable al_v : std_logic;
variable alay_v : std_logic_vector(1 downto 0);
variable alm_v : std_logic_vector(2 downto 0);
variable phy_v, proto_v, fw_v : unsigned(31 downto 0);
begin
if rst_n = '0' then
dep_r <= (others => '0');
worst_r <= (others => '0');
ec_r <= T_NONE;
er_r <= '0';
al_r <= '0';
alay_r <= L_NONE;
alarmed_r <= (others => '0');
att_c <= (others => '0');
xfer_c <= (others => '0');
fail_c <= (others => '0');
xfail_c <= (others => '0');
mask_c <= (others => '0');
alarm_c <= (others => '0');
crc_c <= (others => '0');
stuff_c <= (others => '0');
tmo_c <= (others => '0');
pid_c <= (others => '0');
stall_c <= (others => '0');
babbl_c <= (others => '0');
bad_c <= (others => '0');
phy_c <= (others => '0');
proto_c <= (others => '0');
fw_c <= (others => '0');
elsif rising_edge(clk) then
l := layer_of(err_class);
dep_v := dep_r;
wrst_v := worst_r;
ec_v := T_NONE;
er_v := '0';
al_v := '0';
alay_v := L_NONE;
alm_v := alarmed_r;
phy_v := phy_c;
proto_v:= proto_c;
fw_v := fw_c;
if eot = '1' then
null;
elsif attempt_valid = '1' then
att_c <= att_c + 1;
if err_class = E_NONE then
-- ---- The attempt succeeded, so the TRANSFER succeeded. ----
xfer_c <= xfer_c + 1;
if dep_v /= x"00" then
-- ...but only because of a retry. The transfer layer records a
-- success and the application never hears about this. It is the
-- only trace of the damage anywhere in the system.
er_v := '1';
ec_v := T_MASKED;
mask_c <= mask_c + 1;
end if;
dep_v := x"00";
else
-- ---- A failed ATTEMPT. Counted in the RIGHT denominator. ----
fail_c <= fail_c + 1;
case err_class is
when E_CRC => crc_c <= crc_c + 1;
when E_STUFF => stuff_c <= stuff_c + 1;
when E_TMO => tmo_c <= tmo_c + 1;
when E_PID => pid_c <= pid_c + 1;
when E_STALL => stall_c <= stall_c + 1;
when E_BABBL => babbl_c <= babbl_c + 1;
when others => bad_c <= bad_c + 1;
end case;
if l = L_PHY then
phy_v := phy_c + 1;
elsif l = L_PROTO then
proto_v := proto_c + 1;
elsif l = L_FW then
fw_v := fw_c + 1;
end if;
-- L_NONE: E_RSVD. Counted as a failed attempt, because it was
-- one, and attributed to nobody, because nothing defines it.
if (dep_v + 1) >= to_unsigned(MAX_RETRY, 8) then
-- ---- Retries exhausted. NOW the transfer has failed. ----
--
-- This is the only error the layers above the host controller
-- ever see, and by the time it arrives MAX_RETRY-1 attempts have
-- been thrown away unlogged.
er_v := '1';
ec_v := T_XFER_FAIL;
xfail_c <= xfail_c + 1;
xfer_c <= xfer_c + 1;
dep_v := x"00";
else
dep_v := dep_v + 1;
if dep_v > wrst_v then
wrst_v := dep_v;
end if;
end if;
-- ---- The layer alarm: reported on the CROSSING, once. ----
--
-- A layer over threshold stays over threshold, so alarming on the
-- state rather than the transition reproduces chapter 25.3's flood
-- exactly.
if (l = L_PHY) and (phy_v >= to_unsigned(LAYER_ALARM, 32))
and alm_v(0) = '0' then
al_v := '1'; alay_v := L_PHY; alm_v(0) := '1';
alarm_c <= alarm_c + 1;
elsif (l = L_PROTO) and (proto_v >= to_unsigned(LAYER_ALARM, 32))
and alm_v(1) = '0' then
al_v := '1'; alay_v := L_PROTO; alm_v(1) := '1';
alarm_c <= alarm_c + 1;
elsif (l = L_FW) and (fw_v >= to_unsigned(LAYER_ALARM, 32))
and alm_v(2) = '0' then
al_v := '1'; alay_v := L_FW; alm_v(2) := '1';
alarm_c <= alarm_c + 1;
end if;
end if;
end if;
phy_c <= phy_v;
proto_c <= proto_v;
fw_c <= fw_v;
dep_r <= dep_v;
worst_r <= wrst_v;
ec_r <= ec_v;
er_r <= er_v;
al_r <= al_v;
alay_r <= alay_v;
alarmed_r <= alm_v;
end if;
end process;
end architecture;12. Seeing a Transfer Survive on Its Last Attempt
Two failures, then a success — reported as MASKED
usb_error_triage — a success that cost three attempts
10 cyclesAnd the coincident case that section 7 is about:
The attempt that exhausts the budget is also the attempt that crosses the alarm
usb_error_triage — two events, two pulses, one cycle
10 cycles13. The Testbenches
The oracle is a shadow model written from sections 2 to 7 rather than from the RTL, re-derived every cycle and compared against every output — twenty-five checks per cycle, including the two structural sums.
The exhaustive claim is the one worth reading carefully, because it is over sequences, not over values:
A retry budget is a property of a SEQUENCE.
The third consecutive failure ends a transfer.
The first does not.
So sweeping the 8 err_class encodings one at a time
proves NOTHING about the budget.
Phase 1 drives all 8^3 = 512 three-attempt sequences,
each preceded by a success so it starts at depth 0.
2048 attempts, every ordering of every class over the
whole budget.Six phases:
| Phase | What it establishes |
|---|---|
| 1 | all 512 three-attempt sequences — every path through the retry logic |
| 2 | 200 transfers that each fail once: 0 transfer failures, 200 of 400 attempts failed |
| 3 | the give-up boundary, one attempt wide, checked on both sides |
| 4 | the layer alarm fires once across 64 crossings of its threshold |
| 5 | 40 CRC against 30+30 protocol: the layer wins, not the largest class |
| 6 | 40000 random attempts with a realistic class mix |
| — | a second instance with MAX_RETRY = 5, driven by the same stimulus |
Verilog-2005 testbench
`timescale 1ns/1ps
// Testbench for usb_error_triage.
//
// The oracle is a shadow model written from the chapter's rules rather than
// from the RTL, re-derived every cycle and compared against every output.
//
// THE EXHAUSTIVE CLAIM IS OVER SEQUENCES, NOT OVER VALUES
//
// A retry budget is a property of a SEQUENCE. An attempt that fails means
// something different depending on what the previous two attempts did --
// the third consecutive failure ends a transfer and the first does not --
// so sweeping the eight err_class encodings one at a time proves nothing
// about the budget at all.
//
// Phase 1 drives all 8^3 = 512 possible three-attempt sequences, each one
// preceded by a success so that it starts from a known retry depth. That is
// every path through the retry logic that MAX_RETRY = 3 admits, and it
// includes every ordering of class and outcome within those paths.
module tb_et2_v;
localparam integer MAX_RETRY = 3;
localparam integer LAYER_ALARM = 32;
localparam [2:0] E_NONE = 3'd0, E_CRC = 3'd1, E_STUFF = 3'd2,
E_TMO = 3'd3, E_PID = 3'd4, E_STALL = 3'd5,
E_BABBL = 3'd6, E_RSVD = 3'd7;
localparam [1:0] L_PHY = 2'd0, L_PROTO = 2'd1, L_FW = 2'd2, L_NONE = 2'd3;
localparam [1:0] T_NONE = 2'd0, T_MASKED = 2'd1, T_XFER_FAIL = 2'd2;
reg clk = 1'b0, rst_n = 1'b0;
reg attempt_valid = 1'b0, eot = 1'b0;
reg [2:0] err_class = 3'd0;
wire [1:0] dominant_layer, err_code, alarm_layer;
wire [7:0] retry_depth;
wire [7:0] worst_retry;
wire err_pulse, alarm_pulse;
wire [31:0] n_attempt, n_xfer, n_fail, n_xfer_fail, n_masked, n_alarm;
wire [31:0] n_crc, n_stuff, n_timeout, n_pid, n_stall, n_babble, n_badclass;
wire [31:0] n_phy, n_proto, n_fw;
usb_error_triage #(.MAX_RETRY(MAX_RETRY), .LAYER_ALARM(LAYER_ALARM)) dut (
.clk(clk), .rst_n(rst_n),
.attempt_valid(attempt_valid), .err_class(err_class), .eot(eot),
.retry_depth(retry_depth), .worst_retry(worst_retry),
.dominant_layer(dominant_layer),
.err_pulse(err_pulse), .err_code(err_code),
.alarm_pulse(alarm_pulse), .alarm_layer(alarm_layer),
.n_attempt(n_attempt), .n_xfer(n_xfer), .n_fail(n_fail),
.n_xfer_fail(n_xfer_fail), .n_masked(n_masked), .n_alarm(n_alarm),
.n_crc(n_crc), .n_stuff(n_stuff), .n_timeout(n_timeout), .n_pid(n_pid),
.n_stall(n_stall), .n_babble(n_babble), .n_badclass(n_badclass),
.n_phy(n_phy), .n_proto(n_proto), .n_fw(n_fw)
);
// ---- A SECOND instance, with a different retry budget. ----
//
// The whole point of a parameter is that changing it changes the
// behaviour. A module whose depth counter is two bits wide accepts
// MAX_RETRY = 5, truncates it to 1, and gives up after one failed attempt
// -- and every test written against the default value of 3 still passes.
//
// This instance is driven by exactly the same stimulus and checked at the
// one place the budget is visible: where it gives up.
wire [31:0] n5_xfer_fail;
wire [7:0] n5_depth;
usb_error_triage #(.MAX_RETRY(5), .LAYER_ALARM(LAYER_ALARM)) dut5 (
.clk(clk), .rst_n(rst_n),
.attempt_valid(attempt_valid), .err_class(err_class), .eot(eot),
.retry_depth(n5_depth), .worst_retry(), .dominant_layer(),
.err_pulse(), .err_code(), .alarm_pulse(), .alarm_layer(),
.n_attempt(), .n_xfer(), .n_fail(), .n_xfer_fail(n5_xfer_fail),
.n_masked(), .n_alarm(),
.n_crc(), .n_stuff(), .n_timeout(), .n_pid(),
.n_stall(), .n_babble(), .n_badclass(),
.n_phy(), .n_proto(), .n_fw()
);
always #5 clk = ~clk;
// ---------------- the shadow model ----------------
reg [7:0] m_dep;
reg [7:0] m_worst;
reg [1:0] m_ec, m_alay;
reg m_er, m_al;
reg [2:0] m_alarm;
reg [31:0] m_att, m_xfer, m_fail, m_xfail, m_mask, m_nal;
reg [31:0] m_crc, m_stuff, m_tmo, m_pid, m_stall, m_babbl, m_bad;
reg [31:0] m_phy, m_proto, m_fw;
// Written from the chapter, not from the design. The map is the claim:
// an encoding that is not an error class is attributed to NOBODY, because
// attributing it puts a bug on somebody's desk on the strength of a value
// nobody defined.
function [1:0] lay;
input [2:0] c;
begin
if (c == E_CRC || c == E_STUFF) lay = L_PHY;
else if (c == E_TMO || c == E_PID) lay = L_PROTO;
else if (c == E_STALL || c == E_BABBL) lay = L_FW;
else lay = L_NONE;
end
endfunction
integer errors = 0, checks = 0, steps = 0;
integer k;
// reach: err_class (8) x the retry depth BEFORE the attempt (0..2)
reg [0:0] reach [0:23];
integer n_reach;
task ck;
input [255:0] nm;
input [31:0] got, exp;
begin
checks = checks + 1;
if (got !== exp) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL t=%0t step=%0d %0s got=%0d exp=%0d",
$time, steps, nm, got, exp);
end
end
endtask
task model_step;
reg [1:0] l;
begin
m_er = 1'b0; m_ec = T_NONE; m_al = 1'b0; m_alay = L_NONE;
l = lay(err_class);
if (eot) begin
// nothing
end else if (attempt_valid) begin
m_att = m_att + 1;
if (err_class == E_NONE) begin
m_xfer = m_xfer + 1;
if (m_dep != 8'd0) begin
// The transfer succeeded, and it only succeeded because of a
// retry. The transfer layer records a success; this is the only
// place the cost is visible.
m_er = 1'b1; m_ec = T_MASKED;
m_mask = m_mask + 1;
end
m_dep = 8'd0;
end else begin
m_fail = m_fail + 1;
case (err_class)
E_CRC: m_crc = m_crc + 1;
E_STUFF: m_stuff = m_stuff + 1;
E_TMO: m_tmo = m_tmo + 1;
E_PID: m_pid = m_pid + 1;
E_STALL: m_stall = m_stall + 1;
E_BABBL: m_babbl = m_babbl + 1;
default: m_bad = m_bad + 1;
endcase
if (l == L_PHY) m_phy = m_phy + 1;
else if (l == L_PROTO) m_proto = m_proto + 1;
else if (l == L_FW) m_fw = m_fw + 1;
if (m_dep + 8'd1 >= MAX_RETRY) begin
m_er = 1'b1; m_ec = T_XFER_FAIL;
m_xfail = m_xfail + 1;
m_xfer = m_xfer + 1;
m_dep = 8'd0;
end else begin
m_dep = m_dep + 8'd1;
if (m_dep > m_worst) m_worst = m_dep;
end
// Its own pulse, so it can coincide with T_XFER_FAIL without
// either event overwriting the other.
if ((l == L_PHY) && (m_phy >= LAYER_ALARM) && !m_alarm[0]) begin
m_al = 1'b1; m_alay = L_PHY; m_alarm[0] = 1'b1; m_nal = m_nal+1;
end else if ((l == L_PROTO) && (m_proto >= LAYER_ALARM)
&& !m_alarm[1]) begin
m_al = 1'b1; m_alay = L_PROTO; m_alarm[1] = 1'b1; m_nal = m_nal+1;
end else if ((l == L_FW) && (m_fw >= LAYER_ALARM)
&& !m_alarm[2]) begin
m_al = 1'b1; m_alay = L_FW; m_alarm[2] = 1'b1; m_nal = m_nal+1;
end
end
end
end
endtask
// The dominant layer, from the model's own totals, with the tie-break the
// design documents: the lowest layer index wins.
// Verilog-2005 requires at least one input port on a function, so the
// model's own totals are passed in rather than read from module scope.
function [1:0] dom;
input dummy;
begin
if ((m_phy >= m_proto) && (m_phy >= m_fw)) dom = L_PHY;
else if (m_proto >= m_fw) dom = L_PROTO;
else dom = L_FW;
end
endfunction
task check_out;
begin
ck("retry_depth", {24'd0, retry_depth}, {24'd0, m_dep});
ck("worst_retry", {24'd0, worst_retry}, {24'd0, m_worst});
ck("err_pulse", {31'd0, err_pulse}, {31'd0, m_er});
ck("err_code", {30'd0, err_code}, {30'd0, m_ec});
ck("alarm_pulse", {31'd0, alarm_pulse}, {31'd0, m_al});
ck("alarm_layer", {30'd0, alarm_layer}, {30'd0, m_alay});
ck("dominant_layer", {30'd0, dominant_layer}, {30'd0, dom(1'b0)});
ck("n_attempt", n_attempt, m_att);
ck("n_xfer", n_xfer, m_xfer);
ck("n_fail", n_fail, m_fail);
ck("n_xfer_fail", n_xfer_fail, m_xfail);
ck("n_masked", n_masked, m_mask);
ck("n_alarm", n_alarm, m_nal);
ck("n_crc", n_crc, m_crc);
ck("n_stuff", n_stuff, m_stuff);
ck("n_timeout", n_timeout, m_tmo);
ck("n_pid", n_pid, m_pid);
ck("n_stall", n_stall, m_stall);
ck("n_babble", n_babble, m_babbl);
ck("n_badclass", n_badclass, m_bad);
ck("n_phy", n_phy, m_phy);
ck("n_proto", n_proto, m_proto);
ck("n_fw", n_fw, m_fw);
// ---- the two structural invariants ----
//
// Every failed attempt is exactly one class; every class belongs to at
// most one layer. A report that cannot be decomposed cannot be trusted
// (chapter 23.4), and E_RSVD is why the LAYER sum excludes the
// unattributed count rather than folding it into one of the three.
ck("class sum",
n_crc + n_stuff + n_timeout + n_pid + n_stall + n_babble + n_badclass,
m_fail);
ck("layer sum", n_phy + n_proto + n_fw, m_fail - m_bad);
end
endtask
task step;
begin
if (attempt_valid && !eot)
reach[{1'b0, err_class} * 3 + {30'd0, m_dep}] = 1'b1;
model_step;
@(posedge clk);
#1;
steps = steps + 1;
check_out;
end
endtask
task att; input [2:0] c;
begin
attempt_valid = 1'b1; eot = 1'b0; err_class = c;
step;
end
endtask
task nop;
begin
attempt_valid = 1'b0; eot = 1'b0;
step;
end
endtask
// Reset the DUT and the model together. Phases 4 and 5 both measure a
// threshold crossing or a ranking, and both are meaningless on top of the
// totals phase 1 leaves behind -- phase 1 drives every class hundreds of
// times, so every layer alarm has already fired before either phase
// starts. A phase that measures an accumulation needs a known accumulator.
task reset_all;
begin
attempt_valid = 1'b0; clear_model; eot = 1'b0;
rst_n = 1'b0;
@(posedge clk); #1;
rst_n = 1'b1;
@(negedge clk);
end
endtask
task clear_model;
begin
m_dep = 8'd0; m_worst = 8'd0; m_ec = T_NONE; m_er = 1'b0;
m_al = 1'b0; m_alay = L_NONE; m_alarm = 3'd0;
m_att=0; m_xfer=0; m_fail=0; m_xfail=0; m_mask=0; m_nal=0;
m_crc=0; m_stuff=0; m_tmo=0; m_pid=0; m_stall=0; m_babbl=0; m_bad=0;
m_phy=0; m_proto=0; m_fw=0;
end
endtask
integer i, a, b, c, w;
integer base_mask, base_xfail, base_att, base_xfer, base_fail, base_al;
initial begin
for (k = 0; k < 24; k = k + 1) reach[k] = 1'b0;
clear_model;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(negedge clk);
// ================= PHASE 1 -- all 512 three-attempt sequences ========
//
// Each sequence is preceded by a success so that it starts at retry
// depth 0. Every ordering of every class over the whole retry budget,
// which is the only way to cover a rule whose meaning depends on
// history.
for (a = 0; a < 8; a = a + 1)
for (b = 0; b < 8; b = b + 1)
for (c = 0; c < 8; c = c + 1) begin
att(E_NONE);
att(a[2:0]);
att(b[2:0]);
att(c[2:0]);
end
// ================= PHASE 2 -- retries MASK the error rate ============
//
// The headline, measured rather than asserted. 200 transfers, each of
// which fails once and then succeeds. Every transfer succeeds, so the
// transfer-level failure rate is EXACTLY ZERO, while one attempt in two
// failed.
att(E_NONE); // start clean
base_att = m_att; base_xfer = m_xfer;
base_fail = m_fail; base_xfail = m_xfail; base_mask = m_mask;
for (i = 0; i < 200; i = i + 1) begin
att(E_CRC);
att(E_NONE);
end
if (m_xfail != base_xfail) begin
errors = errors + 1;
$display("FAIL a transfer failed when none should have");
end
if (m_xfer - base_xfer != 200) begin
errors = errors + 1;
$display("FAIL transfers %0d expected 200", m_xfer - base_xfer);
end
if (m_att - base_att != 400) begin
errors = errors + 1;
$display("FAIL attempts %0d expected 400", m_att - base_att);
end
if (m_fail - base_fail != 200) begin
errors = errors + 1;
$display("FAIL failed attempts %0d expected 200", m_fail - base_fail);
end
// ...and every one of those 200 transfers is reported as MASKED, which
// is the only trace of the damage anywhere in the system.
if (m_mask - base_mask != 200) begin
errors = errors + 1;
$display("FAIL masked %0d expected 200", m_mask - base_mask);
end
$display("PHASE2 transfer-level failures=%0d attempt-level failures=%0d/%0d",
m_xfail - base_xfail, m_fail - base_fail, m_att - base_att);
// ================= PHASE 3 -- the give-up boundary ===================
//
// MAX_RETRY-1 failures then a success is a MASKED transfer and nothing
// else. MAX_RETRY failures is a transfer failure and nothing else. The
// boundary is one attempt wide and both sides of it are checked.
att(E_NONE);
base_xfail = m_xfail; base_mask = m_mask;
for (i = 0; i < MAX_RETRY - 1; i = i + 1) att(E_TMO);
if (m_xfail != base_xfail) begin
errors = errors + 1;
$display("FAIL gave up early");
end
att(E_NONE);
if (m_mask != base_mask + 1) begin
errors = errors + 1;
$display("FAIL MAX_RETRY-1 failures then success was not MASKED");
end
base_xfail = m_xfail; base_mask = m_mask;
for (i = 0; i < MAX_RETRY; i = i + 1) att(E_TMO);
if (m_xfail != base_xfail + 1) begin
errors = errors + 1;
$display("FAIL MAX_RETRY failures did not fail the transfer");
end
if (m_mask != base_mask) begin
errors = errors + 1;
$display("FAIL a failed transfer was also reported as masked");
end
if (m_dep != 8'd0) begin
errors = errors + 1;
$display("FAIL retry depth not reset after giving up");
end
// ---- The parameter has to mean something. ----
//
// Four consecutive failures must NOT exhaust a budget of five, and the
// fifth must. A two-bit depth counter fails the first of these, and no
// test written against the default budget of 3 would ever notice.
att(E_NONE);
base_xfail = n5_xfer_fail;
for (i = 0; i < 4; i = i + 1) att(E_TMO);
if (n5_xfer_fail != base_xfail) begin
errors = errors + 1;
$display("FAIL MAX_RETRY=5 gave up after 4 attempts");
end
if (n5_depth != 8'd4) begin
errors = errors + 1;
$display("FAIL MAX_RETRY=5 depth is %0d after 4 failures, expected 4",
n5_depth);
end
att(E_TMO);
if (n5_xfer_fail != base_xfail + 1) begin
errors = errors + 1;
$display("FAIL MAX_RETRY=5 did not give up after 5 attempts");
end
att(E_NONE);
// ================= PHASE 4 -- the layer alarm fires ONCE =============
//
// Enough firmware-class errors to cross LAYER_ALARM twice over. One
// alarm, on the crossing.
reset_all;
base_al = m_nal;
for (i = 0; i < 2 * LAYER_ALARM; i = i + 1) begin
att(E_STALL);
att(E_NONE);
end
if (m_nal != base_al + 1) begin
errors = errors + 1;
$display("FAIL firmware alarms %0d expected 1", m_nal - base_al);
end
// ================= PHASE 5 -- the layer total, not the largest class ==
//
// The case the chapter names: 40 CRC (PHY) against 30 timeouts and 30
// bad PIDs (both PROTOCOL). The single largest CLASS is CRC. The
// dominant LAYER is protocol, 60 to 40, and an implementation that
// ranks classes reports the wrong team.
//
// Counted from a clean slate by resetting the DUT, because the earlier
// phases have left large totals that would swamp the comparison.
reset_all;
for (i = 0; i < 40; i = i + 1) begin att(E_CRC); att(E_NONE); end
for (i = 0; i < 30; i = i + 1) begin att(E_TMO); att(E_NONE); end
for (i = 0; i < 30; i = i + 1) begin att(E_PID); att(E_NONE); end
if (m_crc != 40 || m_tmo != 30 || m_pid != 30) begin
errors = errors + 1;
$display("FAIL phase 5 class counts %0d %0d %0d", m_crc, m_tmo, m_pid);
end
if (dominant_layer !== L_PROTO) begin
errors = errors + 1;
$display("FAIL dominant layer is %0d, expected PROTO (60 vs 40)",
dominant_layer);
end
$display("PHASE5 largest class = CRC (%0d), dominant layer = %0d (phy=%0d proto=%0d fw=%0d)",
m_crc, dominant_layer, m_phy, m_proto, m_fw);
// The random phase is switchable, because a mutation score is only
// interesting once it is DECOMPOSED. Phase 1 alone reaches all 24
// class x depth situations, so the exhaustiveness proof still holds
// with the random phase compiled out.
`ifndef DIRECTED_ONLY
// ================= PHASE 6 -- random =================================
//
// The class is drawn ONCE into a variable: a chain of ternaries each
// draw a new number, so the branches become independent events rather
// than the nested distribution the code appears to describe.
for (i = 0; i < 40000; i = i + 1) begin
w = $unsigned($random) % 1000;
if (w < 30) begin
nop;
end else if (w < 700) begin
att(E_NONE);
end else if (w < 810) att(E_CRC);
else if (w < 860) att(E_STUFF);
else if (w < 920) att(E_TMO);
else if (w < 960) att(E_PID);
else if (w < 980) att(E_STALL);
else if (w < 995) att(E_BABBL);
else att(E_RSVD);
end
`endif
// ================= the exhaustiveness proof ==========================
n_reach = 0;
for (k = 0; k < 24; k = k + 1) n_reach = n_reach + reach[k];
if (n_reach != 24) begin
errors = errors + 1;
$display("FAIL class x depth reach %0d/24", n_reach);
for (k = 0; k < 24; k = k + 1)
if (!reach[k]) $display(" unreached class=%0d depth=%0d", k/3, k%3);
end
$display("steps=%0d checks=%0d reach=%0d/24 errors=%0d",
steps, checks, n_reach, errors);
$display("attempts=%0d transfers=%0d failed_attempts=%0d xfer_fail=%0d masked=%0d",
n_attempt, n_xfer, n_fail, n_xfer_fail, n_masked);
$display("crc=%0d stuff=%0d timeout=%0d pid=%0d stall=%0d babble=%0d badclass=%0d",
n_crc, n_stuff, n_timeout, n_pid, n_stall, n_babble, n_badclass);
$display("phy=%0d proto=%0d fw=%0d alarms=%0d worst_retry=%0d",
n_phy, n_proto, n_fw, n_alarm, worst_retry);
$display("%0s: %0d errors in %0d checks",
(errors == 0) ? "PASS" : "FAIL", errors, checks);
$finish;
end
endmoduleSystemVerilog testbench
`timescale 1ns/1ps
// Testbench for usb_error_triage.
//
// The oracle is a shadow model written from the chapter's rules rather than
// from the RTL, re-derived every cycle and compared against every output.
//
// THE EXHAUSTIVE CLAIM IS OVER SEQUENCES, NOT OVER VALUES
//
// A retry budget is a property of a SEQUENCE. An attempt that fails means
// something different depending on what the previous two attempts did --
// the third consecutive failure ends a transfer and the first does not --
// so sweeping the eight err_class encodings one at a time proves nothing
// about the budget at all.
//
// Phase 1 drives all 8^3 = 512 possible three-attempt sequences, each one
// preceded by a success so that it starts from a known retry depth. That is
// every path through the retry logic that MAX_RETRY = 3 admits, and it
// includes every ordering of class and outcome within those paths.
module tb_et2_sv;
import usb_triage_pkg::*;
localparam int MAX_RETRY = 3;
localparam int LAYER_ALARM = 32;
logic clk = 1'b0, rst_n = 1'b0;
logic attempt_valid = 1'b0, eot = 1'b0;
err_class_e err_class = E_NONE;
logic [7:0] retry_depth;
layer_e dominant_layer, alarm_layer;
triage_e err_code;
logic [7:0] worst_retry;
logic err_pulse, alarm_pulse;
logic [31:0] n_attempt, n_xfer, n_fail, n_xfer_fail, n_masked, n_alarm;
logic [31:0] n_crc, n_stuff, n_timeout, n_pid, n_stall, n_babble, n_badclass;
logic [31:0] n_phy, n_proto, n_fw;
usb_error_triage #(.MAX_RETRY(MAX_RETRY), .LAYER_ALARM(LAYER_ALARM)) dut (
.clk(clk), .rst_n(rst_n),
.attempt_valid(attempt_valid), .err_class(err_class), .eot(eot),
.retry_depth(retry_depth), .worst_retry(worst_retry),
.dominant_layer(dominant_layer),
.err_pulse(err_pulse), .err_code(err_code),
.alarm_pulse(alarm_pulse), .alarm_layer(alarm_layer),
.n_attempt(n_attempt), .n_xfer(n_xfer), .n_fail(n_fail),
.n_xfer_fail(n_xfer_fail), .n_masked(n_masked), .n_alarm(n_alarm),
.n_crc(n_crc), .n_stuff(n_stuff), .n_timeout(n_timeout), .n_pid(n_pid),
.n_stall(n_stall), .n_babble(n_babble), .n_badclass(n_badclass),
.n_phy(n_phy), .n_proto(n_proto), .n_fw(n_fw)
);
// ---- A SECOND instance, with a different retry budget. ----
//
// The whole point of a parameter is that changing it changes the
// behaviour. A module whose depth counter is two bits wide accepts
// MAX_RETRY = 5, truncates it to 1, and gives up after one failed attempt
// -- and every test written against the default value of 3 still passes.
//
// This instance is driven by exactly the same stimulus and checked at the
// one place the budget is visible: where it gives up.
logic [31:0] n5_xfer_fail;
logic [7:0] n5_depth;
usb_error_triage #(.MAX_RETRY(5), .LAYER_ALARM(LAYER_ALARM)) dut5 (
.clk(clk), .rst_n(rst_n),
.attempt_valid(attempt_valid), .err_class(err_class), .eot(eot),
.retry_depth(n5_depth), .worst_retry(), .dominant_layer(),
.err_pulse(), .err_code(), .alarm_pulse(), .alarm_layer(),
.n_attempt(), .n_xfer(), .n_fail(), .n_xfer_fail(n5_xfer_fail),
.n_masked(), .n_alarm(),
.n_crc(), .n_stuff(), .n_timeout(), .n_pid(),
.n_stall(), .n_babble(), .n_badclass(),
.n_phy(), .n_proto(), .n_fw()
);
always #5 clk = ~clk;
// ---------------- the shadow model ----------------
logic [7:0] m_dep;
logic [7:0] m_worst;
triage_e m_ec;
layer_e m_alay;
logic m_er, m_al;
logic [2:0] m_alarm;
int unsigned m_att, m_xfer, m_fail, m_xfail, m_mask, m_nal;
int unsigned m_crc, m_stuff, m_tmo, m_pid, m_stall, m_babbl, m_bad;
int unsigned m_phy, m_proto, m_fw;
// Written from the chapter, not from the design. The map is the claim:
// an encoding that is not an error class is attributed to NOBODY, because
// attributing it puts a bug on somebody's desk on the strength of a value
// nobody defined.
// if/else rather than a chain of ternaries: a conditional expression whose
// arms are enumeration literals needs an explicit cast in SystemVerilog,
// and Icarus rejects it outright.
function automatic layer_e lay(input err_class_e c);
begin
if (c == E_CRC || c == E_STUFF) lay = L_PHY;
else if (c == E_TMO || c == E_PID) lay = L_PROTO;
else if (c == E_STALL || c == E_BABBL) lay = L_FW;
else lay = L_NONE;
end
endfunction
int errors = 0, checks = 0, steps = 0;
int k;
// reach: err_class (8) x the retry depth BEFORE the attempt (0..2)
bit reach [24];
int n_reach;
task automatic ck(string nm, int unsigned got, int unsigned exp);
checks++;
if (got !== exp) begin
errors++;
if (errors < 25)
$display("FAIL t=%0t step=%0d %0s got=%0d exp=%0d",
$time, steps, nm, got, exp);
end
endtask
task automatic model_step;
layer_e l;
begin
m_er = 1'b0; m_ec = T_NONE; m_al = 1'b0; m_alay = L_NONE;
l = lay(err_class);
if (eot) begin
// nothing
end else if (attempt_valid) begin
m_att = m_att + 1;
if (err_class == E_NONE) begin
m_xfer = m_xfer + 1;
if (m_dep != 8'd0) begin
// The transfer succeeded, and it only succeeded because of a
// retry. The transfer layer records a success; this is the only
// place the cost is visible.
m_er = 1'b1; m_ec = T_MASKED;
m_mask = m_mask + 1;
end
m_dep = 8'd0;
end else begin
m_fail = m_fail + 1;
case (err_class)
E_CRC: m_crc = m_crc + 1;
E_STUFF: m_stuff = m_stuff + 1;
E_TMO: m_tmo = m_tmo + 1;
E_PID: m_pid = m_pid + 1;
E_STALL: m_stall = m_stall + 1;
E_BABBL: m_babbl = m_babbl + 1;
default: m_bad = m_bad + 1;
endcase
if (l == L_PHY) m_phy = m_phy + 1;
else if (l == L_PROTO) m_proto = m_proto + 1;
else if (l == L_FW) m_fw = m_fw + 1;
if ((m_dep + 8'd1) >= 8'(MAX_RETRY)) begin
m_er = 1'b1; m_ec = T_XFER_FAIL;
m_xfail = m_xfail + 1;
m_xfer = m_xfer + 1;
m_dep = 8'd0;
end else begin
m_dep = m_dep + 8'd1;
if (m_dep > m_worst) m_worst = m_dep;
end
// Its own pulse, so it can coincide with T_XFER_FAIL without
// either event overwriting the other.
if ((l == L_PHY) && (m_phy >= LAYER_ALARM) && !m_alarm[0]) begin
m_al = 1'b1; m_alay = L_PHY; m_alarm[0] = 1'b1; m_nal = m_nal+1;
end else if ((l == L_PROTO) && (m_proto >= LAYER_ALARM)
&& !m_alarm[1]) begin
m_al = 1'b1; m_alay = L_PROTO; m_alarm[1] = 1'b1; m_nal = m_nal+1;
end else if ((l == L_FW) && (m_fw >= LAYER_ALARM)
&& !m_alarm[2]) begin
m_al = 1'b1; m_alay = L_FW; m_alarm[2] = 1'b1; m_nal = m_nal+1;
end
end
end
end
endtask
// The dominant layer, from the model's own totals, with the tie-break the
// design documents: the lowest layer index wins.
// Verilog-2005 requires at least one input port on a function, so the
// model's own totals are passed in rather than read from module scope.
function automatic layer_e dom();
begin
if ((m_phy >= m_proto) && (m_phy >= m_fw)) dom = L_PHY;
else if (m_proto >= m_fw) dom = L_PROTO;
else dom = L_FW;
end
endfunction
task automatic check_out;
begin
ck("retry_depth", retry_depth, m_dep);
ck("worst_retry", worst_retry, m_worst);
ck("err_pulse", err_pulse, m_er);
ck("err_code", err_code, m_ec);
ck("alarm_pulse", alarm_pulse, m_al);
ck("alarm_layer", alarm_layer, m_alay);
ck("dominant_layer", dominant_layer, dom());
ck("n_attempt", n_attempt, m_att);
ck("n_xfer", n_xfer, m_xfer);
ck("n_fail", n_fail, m_fail);
ck("n_xfer_fail", n_xfer_fail, m_xfail);
ck("n_masked", n_masked, m_mask);
ck("n_alarm", n_alarm, m_nal);
ck("n_crc", n_crc, m_crc);
ck("n_stuff", n_stuff, m_stuff);
ck("n_timeout", n_timeout, m_tmo);
ck("n_pid", n_pid, m_pid);
ck("n_stall", n_stall, m_stall);
ck("n_babble", n_babble, m_babbl);
ck("n_badclass", n_badclass, m_bad);
ck("n_phy", n_phy, m_phy);
ck("n_proto", n_proto, m_proto);
ck("n_fw", n_fw, m_fw);
// ---- the two structural invariants ----
//
// Every failed attempt is exactly one class; every class belongs to at
// most one layer. A report that cannot be decomposed cannot be trusted
// (chapter 23.4), and E_RSVD is why the LAYER sum excludes the
// unattributed count rather than folding it into one of the three.
ck("class sum",
n_crc + n_stuff + n_timeout + n_pid + n_stall + n_babble + n_badclass,
m_fail);
ck("layer sum", n_phy + n_proto + n_fw, m_fail - m_bad);
end
endtask
task automatic step;
begin
if (attempt_valid && !eot)
reach[int'(err_class) * 3 + int'(m_dep)] = 1'b1;
model_step;
@(posedge clk);
#1;
steps = steps + 1;
check_out;
end
endtask
task automatic att(err_class_e c);
begin
attempt_valid = 1'b1; eot = 1'b0; err_class = c;
step;
end
endtask
task automatic nop;
begin
attempt_valid = 1'b0; eot = 1'b0;
step;
end
endtask
// Reset the DUT and the model together. Phases 4 and 5 both measure a
// threshold crossing or a ranking, and both are meaningless on top of the
// totals phase 1 leaves behind -- phase 1 drives every class hundreds of
// times, so every layer alarm has already fired before either phase
// starts. A phase that measures an accumulation needs a known accumulator.
task automatic reset_all;
begin
attempt_valid = 1'b0; clear_model; eot = 1'b0;
rst_n = 1'b0;
@(posedge clk); #1;
rst_n = 1'b1;
@(negedge clk);
end
endtask
task automatic clear_model;
begin
m_dep = 8'd0; m_worst = 8'd0; m_ec = T_NONE; m_er = 1'b0;
m_al = 1'b0; m_alay = L_NONE; m_alarm = 3'd0;
m_att=0; m_xfer=0; m_fail=0; m_xfail=0; m_mask=0; m_nal=0;
m_crc=0; m_stuff=0; m_tmo=0; m_pid=0; m_stall=0; m_babbl=0; m_bad=0;
m_phy=0; m_proto=0; m_fw=0;
end
endtask
int i, a, b, c, w;
int base_mask, base_xfail, base_att, base_xfer, base_fail, base_al;
initial begin
foreach (reach[q]) reach[q] = 1'b0;
clear_model;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(negedge clk);
// ================= PHASE 1 -- all 512 three-attempt sequences ========
//
// Each sequence is preceded by a success so that it starts at retry
// depth 0. Every ordering of every class over the whole retry budget,
// which is the only way to cover a rule whose meaning depends on
// history.
for (a = 0; a < 8; a = a + 1)
for (b = 0; b < 8; b = b + 1)
for (c = 0; c < 8; c = c + 1) begin
att(E_NONE);
att(err_class_e'(a[2:0]));
att(err_class_e'(b[2:0]));
att(err_class_e'(c[2:0]));
end
// ================= PHASE 2 -- retries MASK the error rate ============
//
// The headline, measured rather than asserted. 200 transfers, each of
// which fails once and then succeeds. Every transfer succeeds, so the
// transfer-level failure rate is EXACTLY ZERO, while one attempt in two
// failed.
att(E_NONE); // start clean
base_att = m_att; base_xfer = m_xfer;
base_fail = m_fail; base_xfail = m_xfail; base_mask = m_mask;
for (i = 0; i < 200; i = i + 1) begin
att(E_CRC);
att(E_NONE);
end
if (m_xfail != base_xfail) begin
errors = errors + 1;
$display("FAIL a transfer failed when none should have");
end
if (m_xfer - base_xfer != 200) begin
errors = errors + 1;
$display("FAIL transfers %0d expected 200", m_xfer - base_xfer);
end
if (m_att - base_att != 400) begin
errors = errors + 1;
$display("FAIL attempts %0d expected 400", m_att - base_att);
end
if (m_fail - base_fail != 200) begin
errors = errors + 1;
$display("FAIL failed attempts %0d expected 200", m_fail - base_fail);
end
// ...and every one of those 200 transfers is reported as MASKED, which
// is the only trace of the damage anywhere in the system.
if (m_mask - base_mask != 200) begin
errors = errors + 1;
$display("FAIL masked %0d expected 200", m_mask - base_mask);
end
$display("PHASE2 transfer-level failures=%0d attempt-level failures=%0d/%0d",
m_xfail - base_xfail, m_fail - base_fail, m_att - base_att);
// ================= PHASE 3 -- the give-up boundary ===================
//
// MAX_RETRY-1 failures then a success is a MASKED transfer and nothing
// else. MAX_RETRY failures is a transfer failure and nothing else. The
// boundary is one attempt wide and both sides of it are checked.
att(E_NONE);
base_xfail = m_xfail; base_mask = m_mask;
for (i = 0; i < MAX_RETRY - 1; i = i + 1) att(E_TMO);
if (m_xfail != base_xfail) begin
errors = errors + 1;
$display("FAIL gave up early");
end
att(E_NONE);
if (m_mask != base_mask + 1) begin
errors = errors + 1;
$display("FAIL MAX_RETRY-1 failures then success was not MASKED");
end
base_xfail = m_xfail; base_mask = m_mask;
for (i = 0; i < MAX_RETRY; i = i + 1) att(E_TMO);
if (m_xfail != base_xfail + 1) begin
errors = errors + 1;
$display("FAIL MAX_RETRY failures did not fail the transfer");
end
if (m_mask != base_mask) begin
errors = errors + 1;
$display("FAIL a failed transfer was also reported as masked");
end
if (m_dep != 8'd0) begin
errors = errors + 1;
$display("FAIL retry depth not reset after giving up");
end
// ---- The parameter has to mean something. ----
//
// Four consecutive failures must NOT exhaust a budget of five, and the
// fifth must. A two-bit depth counter fails the first of these, and no
// test written against the default budget of 3 would ever notice.
att(E_NONE);
base_xfail = n5_xfer_fail;
for (i = 0; i < 4; i++) att(E_TMO);
if (n5_xfer_fail != base_xfail) begin
errors = errors + 1;
$display("FAIL MAX_RETRY=5 gave up after 4 attempts");
end
if (n5_depth != 8'd4) begin
errors = errors + 1;
$display("FAIL MAX_RETRY=5 depth is %0d after 4 failures, expected 4",
n5_depth);
end
att(E_TMO);
if (n5_xfer_fail != base_xfail + 1) begin
errors = errors + 1;
$display("FAIL MAX_RETRY=5 did not give up after 5 attempts");
end
att(E_NONE);
// ================= PHASE 4 -- the layer alarm fires ONCE =============
//
// Enough firmware-class errors to cross LAYER_ALARM twice over. One
// alarm, on the crossing.
reset_all;
base_al = m_nal;
for (i = 0; i < 2 * LAYER_ALARM; i = i + 1) begin
att(E_STALL);
att(E_NONE);
end
if (m_nal != base_al + 1) begin
errors = errors + 1;
$display("FAIL firmware alarms %0d expected 1", m_nal - base_al);
end
// ================= PHASE 5 -- the layer total, not the largest class ==
//
// The case the chapter names: 40 CRC (PHY) against 30 timeouts and 30
// bad PIDs (both PROTOCOL). The single largest CLASS is CRC. The
// dominant LAYER is protocol, 60 to 40, and an implementation that
// ranks classes reports the wrong team.
//
// Counted from a clean slate by resetting the DUT, because the earlier
// phases have left large totals that would swamp the comparison.
reset_all;
for (i = 0; i < 40; i = i + 1) begin att(E_CRC); att(E_NONE); end
for (i = 0; i < 30; i = i + 1) begin att(E_TMO); att(E_NONE); end
for (i = 0; i < 30; i = i + 1) begin att(E_PID); att(E_NONE); end
if (m_crc != 40 || m_tmo != 30 || m_pid != 30) begin
errors = errors + 1;
$display("FAIL phase 5 class counts %0d %0d %0d", m_crc, m_tmo, m_pid);
end
if (dominant_layer !== L_PROTO) begin
errors = errors + 1;
$display("FAIL dominant layer is %0d, expected PROTO (60 vs 40)",
dominant_layer);
end
$display("PHASE5 largest class = CRC (%0d), dominant layer = %0d (phy=%0d proto=%0d fw=%0d)",
m_crc, dominant_layer, m_phy, m_proto, m_fw);
// ================= PHASE 6 -- random =================================
//
// The class is drawn ONCE into a variable: a chain of ternaries each
// draw a new number, so the branches become independent events rather
// than the nested distribution the code appears to describe.
for (i = 0; i < 40000; i = i + 1) begin
w = $unsigned($random) % 1000;
if (w < 30) begin
nop;
end else if (w < 700) begin
att(E_NONE);
end else if (w < 810) att(E_CRC);
else if (w < 860) att(E_STUFF);
else if (w < 920) att(E_TMO);
else if (w < 960) att(E_PID);
else if (w < 980) att(E_STALL);
else if (w < 995) att(E_BABBL);
else att(E_RSVD);
end
// ================= the exhaustiveness proof ==========================
n_reach = 0;
for (k = 0; k < 24; k = k + 1) n_reach = n_reach + reach[k];
if (n_reach != 24) begin
errors = errors + 1;
$display("FAIL class x depth reach %0d/24", n_reach);
for (k = 0; k < 24; k = k + 1)
if (!reach[k]) $display(" unreached class=%0d depth=%0d", k/3, k%3);
end
$display("steps=%0d checks=%0d reach=%0d/24 errors=%0d",
steps, checks, n_reach, errors);
$display("attempts=%0d transfers=%0d failed_attempts=%0d xfer_fail=%0d masked=%0d",
n_attempt, n_xfer, n_fail, n_xfer_fail, n_masked);
$display("crc=%0d stuff=%0d timeout=%0d pid=%0d stall=%0d babble=%0d badclass=%0d",
n_crc, n_stuff, n_timeout, n_pid, n_stall, n_babble, n_badclass);
$display("phy=%0d proto=%0d fw=%0d alarms=%0d worst_retry=%0d",
n_phy, n_proto, n_fw, n_alarm, worst_retry);
$display("%0s: %0d errors in %0d checks",
(errors == 0) ? "PASS" : "FAIL", errors, checks);
$finish;
end
endmoduleVHDL-2008 testbench
-- Testbench for usb_error_triage (VHDL-2008).
--
-- The oracle is a shadow model held in process variables and written from
-- the chapter's rules rather than from the RTL, re-derived every cycle and
-- compared against every output.
--
-- THE EXHAUSTIVE CLAIM IS OVER SEQUENCES, NOT OVER VALUES
--
-- A retry budget is a property of a SEQUENCE: the third consecutive failure
-- ends a transfer and the first does not, so sweeping the eight err_class
-- encodings one at a time proves nothing about the budget at all. Phase 1
-- drives all 8^3 = 512 possible three-attempt sequences, each preceded by a
-- success so that it starts from a known retry depth.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_triage_pkg.all;
entity tb_et2_vhdl is
end entity;
architecture sim of tb_et2_vhdl is
constant MAX_RETRY : integer := 3;
constant LAYER_ALARM : integer := 32;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal attempt_valid : std_logic := '0';
signal err_class : std_logic_vector(2 downto 0) := E_NONE;
signal eot : std_logic := '0';
signal retry_depth : std_logic_vector(7 downto 0);
signal worst_retry : std_logic_vector(7 downto 0);
signal dominant_layer : std_logic_vector(1 downto 0);
signal err_pulse : std_logic;
signal err_code : std_logic_vector(1 downto 0);
signal alarm_pulse : std_logic;
signal alarm_layer : std_logic_vector(1 downto 0);
signal n_attempt, n_xfer, n_fail : unsigned(31 downto 0);
signal n_xfer_fail, n_masked, n_alarm : unsigned(31 downto 0);
signal n_crc, n_stuff, n_timeout, n_pid : unsigned(31 downto 0);
signal n_stall, n_babble, n_badclass : unsigned(31 downto 0);
signal n_phy, n_proto, n_fw : unsigned(31 downto 0);
-- ---- A SECOND instance, with a different retry budget. ----
--
-- The whole point of a generic is that changing it changes the behaviour.
-- An entity whose depth counter is two bits wide accepts MAX_RETRY = 5,
-- truncates it to 1, and gives up after one failed attempt -- and every
-- test written against the default value of 3 still passes.
--
-- This instance is driven by exactly the same stimulus and checked at the
-- one place the budget is visible: where it gives up.
signal n5_xfer_fail : unsigned(31 downto 0);
signal n5_depth : std_logic_vector(7 downto 0);
signal done : boolean := false;
type int_array is array (natural range <>) of integer;
begin
clk <= not clk after 5 ns when not done else '0';
dut : entity work.usb_error_triage
generic map (MAX_RETRY => MAX_RETRY, LAYER_ALARM => LAYER_ALARM)
port map (
clk => clk, rst_n => rst_n,
attempt_valid => attempt_valid, err_class => err_class, eot => eot,
retry_depth => retry_depth, worst_retry => worst_retry,
dominant_layer => dominant_layer,
err_pulse => err_pulse, err_code => err_code,
alarm_pulse => alarm_pulse, alarm_layer => alarm_layer,
n_attempt => n_attempt, n_xfer => n_xfer, n_fail => n_fail,
n_xfer_fail => n_xfer_fail, n_masked => n_masked, n_alarm => n_alarm,
n_crc => n_crc, n_stuff => n_stuff, n_timeout => n_timeout,
n_pid => n_pid, n_stall => n_stall, n_babble => n_babble,
n_badclass => n_badclass,
n_phy => n_phy, n_proto => n_proto, n_fw => n_fw
);
dut5 : entity work.usb_error_triage
generic map (MAX_RETRY => 5, LAYER_ALARM => LAYER_ALARM)
port map (
clk => clk, rst_n => rst_n,
attempt_valid => attempt_valid, err_class => err_class, eot => eot,
retry_depth => n5_depth, worst_retry => open,
dominant_layer => open, err_pulse => open, err_code => open,
alarm_pulse => open, alarm_layer => open,
n_attempt => open, n_xfer => open, n_fail => open,
n_xfer_fail => n5_xfer_fail, n_masked => open, n_alarm => open,
n_crc => open, n_stuff => open, n_timeout => open, n_pid => open,
n_stall => open, n_babble => open, n_badclass => open,
n_phy => open, n_proto => open, n_fw => open
);
stim : process
-- ---------------- the shadow model ----------------
variable m_dep : unsigned(7 downto 0) := (others => '0');
variable m_worst : unsigned(7 downto 0) := (others => '0');
variable m_ec : std_logic_vector(1 downto 0) := T_NONE;
variable m_er : std_logic := '0';
variable m_al : std_logic := '0';
variable m_alay : std_logic_vector(1 downto 0) := L_NONE;
variable m_alarm : std_logic_vector(2 downto 0) := (others => '0');
variable c_att, c_xfer, c_fail, c_xfail, c_mask, c_nal : integer := 0;
variable c_crc, c_stuff, c_tmo, c_pid : integer := 0;
variable c_stall, c_babbl, c_bad : integer := 0;
variable c_phy, c_proto, c_fw : integer := 0;
variable errors, checks, steps : integer := 0;
variable reach : int_array(0 to 23) := (others => 0);
variable n_reach : integer := 0;
variable i_v, w_v : integer := 0;
variable base_mask, base_xfail, base_att : integer := 0;
variable base_xfer, base_fail, base_al : integer := 0;
variable ln : line;
-- A deterministic LFSR, so a rerun reproduces exactly the same traffic.
variable lfsr : unsigned(31 downto 0) := x"A5C3F00D";
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;
procedure ck (nm : string; got, exp : integer) is
begin
checks := checks + 1;
if got /= exp then
errors := errors + 1;
if errors < 25 then
write(ln, string'("FAIL step=") & integer'image(steps) &
" " & nm & " got=" & integer'image(got) &
" exp=" & integer'image(exp));
writeline(output, ln);
end if;
end if;
end procedure;
function sl2i (s : std_logic) return integer is
begin
if s = '1' then return 1; else return 0; end if;
end function;
procedure model_step is
variable l : std_logic_vector(1 downto 0);
begin
m_er := '0'; m_ec := T_NONE; m_al := '0'; m_alay := L_NONE;
l := layer_of(err_class);
if eot = '1' then
null;
elsif attempt_valid = '1' then
c_att := c_att + 1;
if err_class = E_NONE then
c_xfer := c_xfer + 1;
if m_dep /= x"00" then
-- The transfer succeeded, and only because of a retry. The
-- transfer layer records a success; this is the only place the
-- cost is visible.
m_er := '1'; m_ec := T_MASKED;
c_mask := c_mask + 1;
end if;
m_dep := x"00";
else
c_fail := c_fail + 1;
case err_class is
when E_CRC => c_crc := c_crc + 1;
when E_STUFF => c_stuff := c_stuff + 1;
when E_TMO => c_tmo := c_tmo + 1;
when E_PID => c_pid := c_pid + 1;
when E_STALL => c_stall := c_stall + 1;
when E_BABBL => c_babbl := c_babbl + 1;
when others => c_bad := c_bad + 1;
end case;
if l = L_PHY then c_phy := c_phy + 1;
elsif l = L_PROTO then c_proto := c_proto + 1;
elsif l = L_FW then c_fw := c_fw + 1;
end if;
if (m_dep + 1) >= to_unsigned(MAX_RETRY, 8) then
m_er := '1'; m_ec := T_XFER_FAIL;
c_xfail := c_xfail + 1;
c_xfer := c_xfer + 1;
m_dep := x"00";
else
m_dep := m_dep + 1;
if m_dep > m_worst then
m_worst := m_dep;
end if;
end if;
-- Its own pulse, so it can coincide with T_XFER_FAIL without
-- either event overwriting the other.
if (l = L_PHY) and (c_phy >= LAYER_ALARM) and m_alarm(0) = '0' then
m_al := '1'; m_alay := L_PHY; m_alarm(0) := '1';
c_nal := c_nal + 1;
elsif (l = L_PROTO) and (c_proto >= LAYER_ALARM)
and m_alarm(1) = '0' then
m_al := '1'; m_alay := L_PROTO; m_alarm(1) := '1';
c_nal := c_nal + 1;
elsif (l = L_FW) and (c_fw >= LAYER_ALARM)
and m_alarm(2) = '0' then
m_al := '1'; m_alay := L_FW; m_alarm(2) := '1';
c_nal := c_nal + 1;
end if;
end if;
end if;
end procedure;
-- The dominant layer from the model's own totals, with the tie-break the
-- design documents: the lowest layer index wins.
impure function dom return integer is
begin
if (c_phy >= c_proto) and (c_phy >= c_fw) then return 0;
elsif c_proto >= c_fw then return 1;
else return 2;
end if;
end function;
procedure check_out is
begin
ck("retry_depth", to_integer(unsigned(retry_depth)), to_integer(m_dep));
ck("worst_retry", to_integer(unsigned(worst_retry)), to_integer(m_worst));
ck("err_pulse", sl2i(err_pulse), sl2i(m_er));
ck("err_code", to_integer(unsigned(err_code)),
to_integer(unsigned(m_ec)));
ck("alarm_pulse", sl2i(alarm_pulse), sl2i(m_al));
ck("alarm_layer", to_integer(unsigned(alarm_layer)),
to_integer(unsigned(m_alay)));
ck("dominant_layer", to_integer(unsigned(dominant_layer)), dom);
ck("n_attempt", to_integer(n_attempt), c_att);
ck("n_xfer", to_integer(n_xfer), c_xfer);
ck("n_fail", to_integer(n_fail), c_fail);
ck("n_xfer_fail", to_integer(n_xfer_fail), c_xfail);
ck("n_masked", to_integer(n_masked), c_mask);
ck("n_alarm", to_integer(n_alarm), c_nal);
ck("n_crc", to_integer(n_crc), c_crc);
ck("n_stuff", to_integer(n_stuff), c_stuff);
ck("n_timeout", to_integer(n_timeout), c_tmo);
ck("n_pid", to_integer(n_pid), c_pid);
ck("n_stall", to_integer(n_stall), c_stall);
ck("n_babble", to_integer(n_babble), c_babbl);
ck("n_badclass", to_integer(n_badclass), c_bad);
ck("n_phy", to_integer(n_phy), c_phy);
ck("n_proto", to_integer(n_proto), c_proto);
ck("n_fw", to_integer(n_fw), c_fw);
-- ---- the two structural invariants ----
--
-- Every failed attempt is exactly one class; every class belongs to at
-- most one layer. A report that cannot be decomposed cannot be trusted
-- (chapter 23.4), and E_RSVD is why the LAYER sum excludes the
-- unattributed count rather than folding it into one of the three.
ck("class sum",
to_integer(n_crc) + to_integer(n_stuff) + to_integer(n_timeout) +
to_integer(n_pid) + to_integer(n_stall) + to_integer(n_babble) +
to_integer(n_badclass), c_fail);
ck("layer sum",
to_integer(n_phy) + to_integer(n_proto) + to_integer(n_fw),
c_fail - c_bad);
end procedure;
procedure step_one is
variable idx : integer;
begin
if attempt_valid = '1' and eot = '0' then
idx := to_integer(unsigned(err_class)) * 3 + to_integer(m_dep);
reach(idx) := 1;
end if;
model_step;
wait until rising_edge(clk);
wait for 1 ns;
steps := steps + 1;
check_out;
end procedure;
procedure att (c : std_logic_vector(2 downto 0)) is
begin
attempt_valid <= '1'; eot <= '0'; err_class <= c;
wait for 0 ns;
step_one;
end procedure;
procedure nop is
begin
attempt_valid <= '0'; eot <= '0';
wait for 0 ns;
step_one;
end procedure;
procedure clear_model is
begin
m_dep := x"00"; m_worst := (others => '0'); m_ec := T_NONE;
m_er := '0'; m_al := '0'; m_alay := L_NONE;
m_alarm := (others => '0');
c_att := 0; c_xfer := 0; c_fail := 0; c_xfail := 0; c_mask := 0;
c_nal := 0;
c_crc := 0; c_stuff := 0; c_tmo := 0; c_pid := 0;
c_stall := 0; c_babbl := 0; c_bad := 0;
c_phy := 0; c_proto := 0; c_fw := 0;
end procedure;
-- Reset the DUT and the model together. Phases 4 and 5 both measure a
-- threshold crossing or a ranking, and both are meaningless on top of
-- the totals phase 1 leaves behind -- phase 1 drives every class
-- hundreds of times, so every layer alarm has already fired before
-- either phase starts. A phase that measures an accumulation needs a
-- known accumulator.
procedure reset_all is
begin
attempt_valid <= '0'; eot <= '0';
clear_model;
rst_n <= '0';
wait until rising_edge(clk);
wait for 1 ns;
rst_n <= '1';
wait for 1 ns;
end procedure;
begin
wait until rising_edge(clk);
wait until rising_edge(clk);
wait until rising_edge(clk);
rst_n <= '1';
wait for 1 ns;
-- ================= PHASE 1 -- all 512 three-attempt sequences ========
--
-- Each sequence is preceded by a success so that it starts at retry
-- depth 0. Every ordering of every class over the whole retry budget,
-- which is the only way to cover a rule whose meaning depends on
-- history.
for a in 0 to 7 loop
for b in 0 to 7 loop
for c in 0 to 7 loop
att(E_NONE);
att(std_logic_vector(to_unsigned(a, 3)));
att(std_logic_vector(to_unsigned(b, 3)));
att(std_logic_vector(to_unsigned(c, 3)));
end loop;
end loop;
end loop;
-- ================= PHASE 2 -- retries MASK the error rate ============
--
-- The headline, measured rather than asserted. 200 transfers, each of
-- which fails once and then succeeds. Every transfer succeeds, so the
-- transfer-level failure rate is EXACTLY ZERO, while one attempt in two
-- failed.
att(E_NONE);
base_att := c_att; base_xfer := c_xfer;
base_fail := c_fail; base_xfail := c_xfail; base_mask := c_mask;
for i in 0 to 199 loop
att(E_CRC);
att(E_NONE);
end loop;
if c_xfail /= base_xfail then
errors := errors + 1;
write(ln, string'("FAIL a transfer failed when none should have"));
writeline(output, ln);
end if;
if c_xfer - base_xfer /= 200 then
errors := errors + 1;
write(ln, string'("FAIL transfers ") & integer'image(c_xfer - base_xfer)
& " expected 200");
writeline(output, ln);
end if;
if c_att - base_att /= 400 then
errors := errors + 1;
write(ln, string'("FAIL attempts ") & integer'image(c_att - base_att)
& " expected 400");
writeline(output, ln);
end if;
if c_fail - base_fail /= 200 then
errors := errors + 1;
write(ln, string'("FAIL failed attempts ")
& integer'image(c_fail - base_fail) & " expected 200");
writeline(output, ln);
end if;
-- ...and every one of those 200 transfers is reported as MASKED, which
-- is the only trace of the damage anywhere in the system.
if c_mask - base_mask /= 200 then
errors := errors + 1;
write(ln, string'("FAIL masked ") & integer'image(c_mask - base_mask)
& " expected 200");
writeline(output, ln);
end if;
write(ln, string'("PHASE2 transfer-level failures=")
& integer'image(c_xfail - base_xfail)
& " attempt-level failures="
& integer'image(c_fail - base_fail) & "/"
& integer'image(c_att - base_att));
writeline(output, ln);
-- ================= PHASE 3 -- the give-up boundary ===================
--
-- MAX_RETRY-1 failures then a success is a MASKED transfer and nothing
-- else. MAX_RETRY failures is a transfer failure and nothing else. The
-- boundary is one attempt wide and both sides of it are checked.
att(E_NONE);
base_xfail := c_xfail; base_mask := c_mask;
for i in 0 to MAX_RETRY-2 loop att(E_TMO); end loop;
if c_xfail /= base_xfail then
errors := errors + 1;
write(ln, string'("FAIL gave up early")); writeline(output, ln);
end if;
att(E_NONE);
if c_mask /= base_mask + 1 then
errors := errors + 1;
write(ln, string'("FAIL MAX_RETRY-1 failures then success not MASKED"));
writeline(output, ln);
end if;
base_xfail := c_xfail; base_mask := c_mask;
for i in 0 to MAX_RETRY-1 loop att(E_TMO); end loop;
if c_xfail /= base_xfail + 1 then
errors := errors + 1;
write(ln, string'("FAIL MAX_RETRY failures did not fail the transfer"));
writeline(output, ln);
end if;
if c_mask /= base_mask then
errors := errors + 1;
write(ln, string'("FAIL a failed transfer was also reported as masked"));
writeline(output, ln);
end if;
if m_dep /= x"00" then
errors := errors + 1;
write(ln, string'("FAIL retry depth not reset after giving up"));
writeline(output, ln);
end if;
-- ---- The generic has to mean something. ----
--
-- Four consecutive failures must NOT exhaust a budget of five, and the
-- fifth must. A two-bit depth counter fails the first of these, and no
-- test written against the default budget of 3 would ever notice.
att(E_NONE);
base_xfail := to_integer(n5_xfer_fail);
for i in 0 to 3 loop att(E_TMO); end loop;
if to_integer(n5_xfer_fail) /= base_xfail then
errors := errors + 1;
write(ln, string'("FAIL MAX_RETRY=5 gave up after 4 attempts"));
writeline(output, ln);
end if;
if to_integer(unsigned(n5_depth)) /= 4 then
errors := errors + 1;
write(ln, string'("FAIL MAX_RETRY=5 depth is ")
& integer'image(to_integer(unsigned(n5_depth)))
& " after 4 failures, expected 4");
writeline(output, ln);
end if;
att(E_TMO);
if to_integer(n5_xfer_fail) /= base_xfail + 1 then
errors := errors + 1;
write(ln, string'("FAIL MAX_RETRY=5 did not give up after 5 attempts"));
writeline(output, ln);
end if;
att(E_NONE);
-- ================= PHASE 4 -- the layer alarm fires ONCE =============
reset_all;
base_al := c_nal;
for i in 0 to 2*LAYER_ALARM-1 loop
att(E_STALL);
att(E_NONE);
end loop;
if c_nal /= base_al + 1 then
errors := errors + 1;
write(ln, string'("FAIL firmware alarms ")
& integer'image(c_nal - base_al) & " expected 1");
writeline(output, ln);
end if;
-- ================= PHASE 5 -- the layer total, not the largest class ==
--
-- 40 CRC (PHY) against 30 timeouts and 30 bad PIDs (both PROTOCOL). The
-- single largest CLASS is CRC. The dominant LAYER is protocol, 60 to 40,
-- and an implementation that ranks classes reports the wrong team.
reset_all;
for i in 0 to 39 loop att(E_CRC); att(E_NONE); end loop;
for i in 0 to 29 loop att(E_TMO); att(E_NONE); end loop;
for i in 0 to 29 loop att(E_PID); att(E_NONE); end loop;
if c_crc /= 40 or c_tmo /= 30 or c_pid /= 30 then
errors := errors + 1;
write(ln, string'("FAIL phase 5 class counts"));
writeline(output, ln);
end if;
if dominant_layer /= L_PROTO then
errors := errors + 1;
write(ln, string'("FAIL dominant layer is ")
& integer'image(to_integer(unsigned(dominant_layer)))
& ", expected PROTO (60 vs 40)");
writeline(output, ln);
end if;
write(ln, string'("PHASE5 largest class = CRC (") & integer'image(c_crc)
& "), dominant layer = "
& integer'image(to_integer(unsigned(dominant_layer)))
& " (phy=" & integer'image(c_phy)
& " proto=" & integer'image(c_proto)
& " fw=" & integer'image(c_fw) & ")");
writeline(output, ln);
-- ================= PHASE 6 -- random =================================
for i in 0 to 39999 loop
w_v := rnd_nat mod 1000;
if w_v < 30 then nop;
elsif w_v < 700 then att(E_NONE);
elsif w_v < 810 then att(E_CRC);
elsif w_v < 860 then att(E_STUFF);
elsif w_v < 920 then att(E_TMO);
elsif w_v < 960 then att(E_PID);
elsif w_v < 980 then att(E_STALL);
elsif w_v < 995 then att(E_BABBL);
else att(E_RSVD);
end if;
end loop;
-- ================= the exhaustiveness proof ==========================
n_reach := 0;
for k in 0 to 23 loop n_reach := n_reach + reach(k); end loop;
if n_reach /= 24 then
errors := errors + 1;
write(ln, string'("FAIL class x depth reach ") & integer'image(n_reach)
& "/24");
writeline(output, ln);
for k in 0 to 23 loop
if reach(k) = 0 then
write(ln, string'(" unreached class=") & integer'image(k/3)
& " depth=" & integer'image(k mod 3));
writeline(output, ln);
end if;
end loop;
end if;
write(ln, string'("steps=") & integer'image(steps)
& " checks=" & integer'image(checks)
& " reach=" & integer'image(n_reach) & "/24"
& " errors=" & integer'image(errors));
writeline(output, ln);
write(ln, string'("attempts=") & integer'image(to_integer(n_attempt))
& " transfers=" & integer'image(to_integer(n_xfer))
& " failed_attempts=" & integer'image(to_integer(n_fail))
& " xfer_fail=" & integer'image(to_integer(n_xfer_fail))
& " masked=" & integer'image(to_integer(n_masked)));
writeline(output, ln);
write(ln, string'("crc=") & integer'image(to_integer(n_crc))
& " stuff=" & integer'image(to_integer(n_stuff))
& " timeout=" & integer'image(to_integer(n_timeout))
& " pid=" & integer'image(to_integer(n_pid))
& " stall=" & integer'image(to_integer(n_stall))
& " babble=" & integer'image(to_integer(n_babble))
& " badclass=" & integer'image(to_integer(n_badclass)));
writeline(output, ln);
write(ln, string'("phy=") & integer'image(to_integer(n_phy))
& " proto=" & integer'image(to_integer(n_proto))
& " fw=" & integer'image(to_integer(n_fw))
& " alarms=" & integer'image(to_integer(n_alarm))
& " worst_retry="
& integer'image(to_integer(unsigned(worst_retry))));
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;14. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| class × retry depth reached | 24 / 24 | 24 / 24 | 24 / 24 |
| three-attempt sequences | 512 / 512 | 512 / 512 | 512 / 512 |
| Steps | 42791 | 42791 | 42791 |
| Checks executed | 1069775 | 1069775 | 1069775 |
| attempts | 39010 | 39010 | 38956 |
| transfers | 27774 | 27774 | 28636 |
| failed attempts | 12012 | 12012 | 12034 |
| failed transfers | 776 | 776 | 1714 |
| masked transfers | 7837 | 7837 | 5196 |
| CRC / bit-stuff | 4345 / 1900 | 4345 / 1900 | 4407 / 2000 |
| timeout / bad PID | 2434 / 1727 | 2434 / 1727 | 2410 / 1651 |
| STALL / babble | 811 / 588 | 811 / 588 | 779 / 588 |
| undefined encoding | 207 | 207 | 199 |
| PHY / PROTOCOL / FIRMWARE | 6245 / 4161 / 1399 | 6245 / 4161 / 1399 | 6407 / 4061 / 1367 |
| layer alarms | 3 | 3 | 3 |
| Result | PASS | PASS | PASS |
The two rows in bold together are the chapter. 12012 failed attempts, 776 failed transfers: fifteen out of every sixteen failures never reached the layer where anybody was counting.
15. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| R5 | every error attributed to the PHY | 213264 | 213264 | 212898 |
| R3 | the retry depth is not reset by a success | 198997 | 198997 | 200316 |
| R4 | the give-up test is > instead of >= | 174853 | 174853 | 180742 |
| R2 | a transfer that succeeded on a retry is not reported | 59421 | 59421 | 54139 |
| R6 | the alarm fires on the state, not the crossing | 55872 | 55872 | 56196 |
| R1 | failures counted per transfer, not per attempt | 42784 | 42784 | 42784 |
| R7 | the dominant layer ranks the largest class | 670 | 670 | 390 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages.
R1 scores 42784 in all three columns, and that number is not a coincidence. The run is 42791 steps long. From the first failed attempt onward, n_fail is permanently wrong, so it fails its check on every remaining cycle — 42791 minus the seven steps before the first failure. A counter that diverges and never resynchronises is caught on every subsequent step regardless of what the stimulus does, which is why the three columns agree exactly despite two different random streams.
R7 is the low one, at 670, and it deserves its place anyway. dominant_layer differs between the two ranking methods only when the largest single class belongs to a layer that is not the largest in total — an ambiguous case that random traffic produces rarely and that phase 5 constructs deliberately. A mutation with a small score is not a weak mutation; it is a mutation whose opportunity is rare, and the only thing that distinguishes "rare opportunity" from "nearly escaped" is whether the directed phases catch it.
Directed against random
| # | All phases | Directed only | Random |
|---|---|---|---|
| R1 | 42784 | 2784 | 40000 |
| R2 | 59421 | 3947 | 55474 |
| R3 | 198997 | 12907 | 186090 |
| R4 | 174853 | 10399 | 164454 |
| R5 | 213264 | 12938 | 200326 |
| R6 | 55872 | 3462 | 52410 |
| R7 | 670 | 41 | 629 |
Every mutation is killed by directed stimulus alone, R7 included. Phase 1's 512 sequences and phase 5's constructed 40/30/30 do the work; the random phase adds volume and confirms nothing new, which is exactly the relationship those two kinds of stimulus should have.
16. Debugging Walkthrough: The Cable That Was Fine
The report. An industrial data logger drops its USB link roughly once a day. Field engineering has replaced the cable twice and the connector once. The customer's own monitoring shows a transfer success rate of 99.997%.
Step 1 — ask for the attempt-level number. It does not exist; the driver reports completions. Instrument at the host controller instead and count attempts. The attempt-level failure rate is 4.1%.
Step 2 — so the link is losing one transaction in twenty-four, continuously. The 99.997% figure was never wrong. It was answering a different question, and answering it accurately.
Step 3 — which layer? The distribution: 61% timeout, 22% bad PID, 14% CRC, 3% everything else. Ranked by class the largest single entry is timeout, which is a protocol class. Summed by layer: 83% PROTOCOL, 14% PHY. Both methods agree here, which is a relief and not a rule.
Step 4 — so it is not the cable. It was never the cable. A PHY problem shows up as CRC and bit-stuff errors, because that is what a damaged signal does. Timeouts and bad PIDs mean the transaction did not happen or came back malformed at the framing level, which is the controller or the schedule.
Step 5 — why once a day? The retry budget absorbs a 4.1% attempt failure rate almost perfectly: three independent failures in a row happens about once in 14,000 transfers. At the logger's transfer rate that is roughly once every twenty hours.
Step 6 — and why replacing the cable appeared to help. It did not. The failure interval is a tail event with a wide spread; "we changed something and it did not fail for two days" is the null hypothesis, not evidence.
The fix. A scheduling bug in the host controller driver under a specific interrupt load. Four hours to find once the attempt-level distribution existed; nine months and three cables before that.
17. UVM: Triage as an Analysis Component
Triage is not a scoreboard. A scoreboard answers did the right data arrive; triage answers what did it cost and whose fault is it, and those are different components with different lifetimes.
// An ATTEMPT, not a transfer. The distinction is the chapter: a sequence
// item that models a transfer has already thrown away the thing worth
// measuring, and no amount of analysis downstream gets it back.
class usb_attempt_item extends uvm_sequence_item;
`uvm_object_utils(usb_attempt_item)
rand err_class_e err_class;
rand int unsigned xfer_id; // which transfer this attempt belongs to
function new(string name = "usb_attempt_item"); super.new(name); endfunction
// A realistic mix. Note that E_NONE dominates and that E_RSVD is
// generated at all: an encoding that "cannot happen" is exactly the one
// whose handling nobody has tested.
constraint c_mix {
err_class dist {
E_NONE := 670, E_CRC := 110, E_STUFF := 50,
E_TMO := 60, E_PID := 40, E_STALL := 20,
E_BABBL := 15, E_RSVD := 5
};
}
endclass
// ---------------------------------------------------------------------
// The triage subscriber. Per-transfer retry state in an associative array
// keyed by transfer id, so several transfers can be in flight without
// their retry budgets running into each other -- the same structural
// argument as chapter 25.3's per-endpoint counters.
// ---------------------------------------------------------------------
class usb_triage_sub extends uvm_subscriber #(usb_attempt_item);
`uvm_component_utils(usb_triage_sub)
int unsigned MAX_RETRY = 3;
int unsigned LAYER_ALARM = 32;
int unsigned depth [int]; // retry depth, per transfer in flight
// BOTH denominators. The whole point is that they can be divided.
int unsigned n_attempt, n_xfer, n_fail, n_xfer_fail, n_masked;
int unsigned n_class [err_class_e];
int unsigned n_layer [layer_e];
bit alarmed [layer_e];
function new(string name, uvm_component parent); super.new(name, parent);
endfunction
// The map. A function, not a table, so the reason each class belongs
// where it does is next to the assignment -- and E_RSVD lands on L_NONE
// on purpose.
function layer_e layer_of(err_class_e c);
case (c)
E_CRC, E_STUFF: return L_PHY;
E_TMO, E_PID: return L_PROTO;
E_STALL, E_BABBL: return L_FW;
default: return L_NONE; // E_NONE and E_RSVD
endcase
endfunction
function void write(usb_attempt_item t);
layer_e l = layer_of(t.err_class);
n_attempt++;
if (!depth.exists(t.xfer_id)) depth[t.xfer_id] = 0;
if (t.err_class == E_NONE) begin
n_xfer++;
if (depth[t.xfer_id] != 0) begin
// A SUCCESS, reported. This is the only trace anywhere in the
// system that the transfer cost more than one attempt, and it is
// the only signal that moves before the failures start.
`uvm_warning("TRIAGE/MASKED",
$sformatf("xfer %0d succeeded on attempt %0d of %0d",
t.xfer_id, depth[t.xfer_id] + 1, MAX_RETRY))
n_masked++;
end
depth.delete(t.xfer_id);
return;
end
// A failed ATTEMPT. Counted here, in the right denominator.
n_fail++;
n_class[t.err_class]++;
if (l != L_NONE) n_layer[l]++;
// L_NONE is counted as a failure and attributed to NOBODY: putting an
// undefined encoding on a team's desk costs them a week proving a
// negative.
if (depth[t.xfer_id] + 1 >= MAX_RETRY) begin
`uvm_error("TRIAGE/XFER_FAIL",
$sformatf("xfer %0d exhausted %0d attempts", t.xfer_id, MAX_RETRY))
n_xfer_fail++;
n_xfer++;
depth.delete(t.xfer_id);
end else begin
depth[t.xfer_id]++;
end
// A separate report, because the attempt that exhausts a budget is
// often also the attempt that crosses a threshold, and a shared code
// field would deliver exactly one of the two.
if (l != L_NONE && n_layer[l] >= LAYER_ALARM && !alarmed[l]) begin
`uvm_error("TRIAGE/LAYER",
$sformatf("%s layer has passed %0d errors", l.name(), LAYER_ALARM))
alarmed[l] = 1;
end
endfunction
// ---- THE CHECK THAT THE WHOLE COMPONENT EXISTS FOR ----
//
// Not "were there errors". A run with errors may be fine and a run
// without visible failures may be a link on the edge of collapse. The
// question is what the ATTEMPT-level rate was, and whether the retry
// mechanism was doing all the work.
function void report_phase(uvm_phase phase);
real attempt_rate, xfer_rate;
layer_e dom = L_NONE;
int unsigned best = 0;
super.report_phase(phase);
attempt_rate = n_attempt ? (100.0 * n_fail) / n_attempt : 0.0;
xfer_rate = n_xfer ? (100.0 * n_xfer_fail) / n_xfer : 0.0;
// The dominant layer is the largest layer TOTAL. Forty CRC against
// thirty timeouts and thirty bad PIDs is a PROTOCOL problem, and
// ranking classes reports the PHY.
foreach (n_layer[k])
if (k != L_NONE && n_layer[k] > best) begin best = n_layer[k]; dom = k; end
`uvm_info("TRIAGE",
$sformatf("attempt-level %0.3f%% (%0d/%0d) | transfer-level %0.3f%% (%0d/%0d) | masked %0d | dominant %s",
attempt_rate, n_fail, n_attempt,
xfer_rate, n_xfer_fail, n_xfer,
n_masked, dom.name()),
UVM_LOW)
// A link whose transfer-level rate is clean and whose attempt-level
// rate is not has no margin left. It is not a passing result; it is a
// failing result that the retry mechanism is currently concealing.
if (xfer_rate < 0.01 && attempt_rate > 1.0)
`uvm_error("TRIAGE/NO_MARGIN",
$sformatf("transfer layer reports %0.3f%% while %0.2f%% of attempts failed — the budget is absorbing all of it",
xfer_rate, attempt_rate))
endfunction
endclass18. Common Misconceptions
"Our transfer success rate is 99.99%, so the link is healthy." It says nothing about the link. It says the retry budget has not been exceeded.
"Error rate is error rate; the denominator is a detail." The two denominators differ by the retry factor, which is the quantity being measured.
"A retry that worked is not a problem." It is one attempt from being a visible failure, and it is the only warning you get.
"Report every retry." Once per transfer, not once per attempt — or you rebuild 25.3's flood.
"The biggest error class tells you the layer." Forty CRC against thirty timeouts and thirty bad PIDs is a protocol problem, and the biggest class is CRC.
"An undefined encoding should be filed under the closest layer." It should be filed under no layer. Guessing costs a team a week.
"A shared error-code field is fine; the events don't overlap." That is a claim, and the cycle where it is false is usually the cycle you most needed both reports.
"CRC errors mean a bad cable." They mean the signal was corrupted. A bad cable is one cause; so is a power rail, a connector, and crosstalk from something else entirely.
"A mutation with a low score is a weak mutation." R7 scores 670 because its opportunity is rare, not because the check is weak. What matters is whether directed stimulus kills it.
19. Exercises
1. A link has an independent 4% per-attempt failure rate and a retry budget of 3. Compute the transfer-level failure rate, then recompute it for 8% and say what that implies about monitoring thresholds.
2. R1 scores exactly 42784 in all three languages against a 42791-step run. Explain the seven, and say what property of the mutation makes the three columns agree despite two different random streams.
3. The VHDL run has 12034 failed attempts and 1714 failed transfers; the Verilog run has 12012 and 776. Both are correct. Explain the factor of two in terms of the time distribution of failures rather than their rate.
4. Construct an error distribution in which ranking by class and summing by layer disagree, where the disagreement cannot be resolved by adding a fourth class to either layer.
5. Phase 1 drives 512 three-attempt sequences. Show that 512 is exactly right for MAX_RETRY = 3, and give the count for MAX_RETRY = 4.
6. The alarm has its own pulse. Construct the attempt sequence in which a shared err_code field would delete the transfer-failure report, and the one in which it would delete the alarm.
7. The retry depth was originally two bits wide. Write the smallest test that catches that, and explain why every test in phases 1 to 6 passes against it.
8. Add a fourth layer for host software errors. Which existing classes move, which do not, and what does the answer tell you about whether the class-to-layer map belongs in this block at all?
20. Summary
| Idea | Why it matters |
|---|---|
| A transfer is a sequence of attempts | the wire-level event and the API-level event are different |
| Retries mask the error rate | a transfer-level rate reads 0% on a badly broken link |
| The denominator is attempts | errors/transfers understates by exactly the retry factor |
| Expose both denominators | the useful diagnostic is their ratio, not either number |
| A masked transfer is still evidence | it is the only signal that moves before failures start |
| Report it once per transfer | once per attempt rebuilds the flood |
| The class names the layer | and the layer names the team |
| The distribution, not the total | all-CRC is a cable; evenly spread is a power rail |
| The layer total, not the largest class | 40 CRC loses to 30+30 protocol |
| An undefined encoding goes to no layer | guessing costs a team a week proving a negative |
| Two events, two pulses | a shared code field is a mutual-exclusion claim |
| A phase measuring an accumulation needs a known accumulator | phase 4 observed 0 alarms until it reset |
| Narrowing a parameter to its default's width | MAX_RETRY = 5 truncates to 1, and the default still passes |
| A stale mutant produces a complete, plausible table | the generator must abort loudly, and BASE must read 0 |
| 512/512 sequences, 24/24 class × depth | 7 mutations, all killed in 3 languages, all by directed stimulus |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o et2_v.out et2_v.v et2_v_tb.v && ./et2_v.out |
| SystemVerilog | iverilog -g2012 -o et2_sv.out et2_sv.sv et2_sv_tb.sv && ./et2_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a et2_vhdl.vhd et2_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_et2_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_et2_vhdl |
| One mutation | iverilog -g2005 -DMUT_R7 -o mm et2_v_mut.v et2_v_tb.v && ./mm |
| Directed only | iverilog -g2005 -DDIRECTED_ONLY -o mm et2_v_mut.v et2_v_tb.v && ./mm |
All three implementations pass with 0 errors: all 512 three-attempt sequences driven, all 24 class × retry-depth situations reached, 1069775 checks against an independently written shadow model, a second instance with a different retry budget to prove the parameterisation is real, and every one of the seven mutations killed by directed stimulus alone.
Chapter 25.5 — CRC Errors takes the largest of the PHY classes and builds it properly: real CRC5 and CRC16 generators, verified exhaustively over all 2048 token values, and the reason the distribution of CRC failures — not the count — is what names the layer underneath them.
Continue learning
Related tutorials
- Related topic
Enumeration Failures
A failing enumeration retries from the beginning, so the current step is always ATTACH and tells you nothing — the furthest step ever reached is the diagnosis, and a bus reset must not clear it.
- Related topic
Descriptor Issues
A descriptor set is described three times by three different fields, and the host walks it by one while reading it by another — so when they disagree the error lands on a field that is perfectly correct.
- Related topic
Endpoint Problems
A NAK is not an error and a STALL is not a NAK — one is flow control working, one is firmware refusing permanently, and a monitor that treats them alike either floods the log or misses the endpoint that has stopped.
- Related topic
CRC Errors
Real CRC5 and CRC16, verified against every single-bit and every double-bit error on all 2048 token values — and why the distribution of CRC failures names the broken half of the link while the count says nothing.
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.
