USB · Module 20
SuperSpeed Concepts
USB 3 kept the single master and deleted the polling — credit-based flow control, announced readiness, and a sender bounded by the smallest of three limits.
Nineteen modules have rested on one fact: the host asks, and nothing happens until it does. Chapter 2.6 established it; 18.3 built an entire polled reporting mechanism because of it; 19.5 treated the single exception as a large enough concession to need two fences.
USB 3 keeps the single master and deletes the polling. Those turn out to be separable, and separating them is most of what SuperSpeed is.
1. The Cost of Asking
A USB 2 bulk endpoint with nothing to say NAKs, and the host asks again.
The asking costs bus time whether or not there is ever an answer. Every poll is a token packet, a turnaround, and a handshake — a complete transaction that moves no data. On a busy bus with several idle endpoints, a significant fraction of every frame is spent discovering that nothing has changed.
And the host cannot know when to stop, because the only way to find out whether a device is ready is to ask it.
| USB 2 | USB 3 | |
|---|---|---|
| How readiness is learned | by trying — poll, NAK, repeat | by being told — the device sends ERDY |
| Cost of an idle endpoint | a transaction per poll interval | nothing |
| Who tracks it | the host, by failing | the device, by announcing |
USB 3's inversion is not that devices may initiate. They still may not — 19.5's exception is still the only one. It is that a device may decline once and be believed, and say later when that changes.
2. ERDY Is Not a Credit
This is the confusion the chapter exists to prevent.
ERDY — endpoint ready — tells the host resume asking me. It grants no permission to send anything.
The permission is credits, and credits come from an ACK transaction packet, which carries a field called NumP: the number of packets the receiver can still accept.
ERDY "I have data now." → the host resumes scheduling
ACK "I can take N more packets." → the sender may have N outstandingA design that treats ERDY as a credit transmits into a receiver that has room for nothing, which is a receive-buffer overflow rather than a retryable protocol error. Mutation C4 does exactly that and dies 38 457 times — after §12, which is about why it did not at first.
3. Three Limits, and the Smallest Wins
A SuperSpeed sender is bounded by three separate things:
| Limit | Where it comes from | |
|---|---|---|
| 1 | credits | the receiver's NumP, in an ACK |
| 2 | burst size | bMaxBurst + 1, from the endpoint companion descriptor |
| 3 | data | how many packets it actually has |
Conflating any two of them produces a design that works until the third becomes binding.
- min(credits, pending) — ignores the burst limit, and overruns a receiver PHY that cannot absorb packets back to back. Mutation C2, 3765 errors.
- min(credits, burst) — claims it may send packets it does not have. Mutation C3, 41 723 errors.
4. bMaxBurst Is Burst Size Minus One
The SuperSpeed endpoint companion descriptor stores it that way:
/* USB_DT_SS_ENDPOINT_COMP: SuperSpeed Endpoint Companion descriptor */
struct usb_ss_ep_comp_descriptor {
__u8 bLength;
__u8 bDescriptorType;
__u8 bMaxBurst;
__u8 bmAttributes;
__le16 wBytesPerInterval;
} __attribute__ ((packed));bMaxBurst = 0 means a burst of ONE packet. bMaxBurst = 15 means sixteen.
A design that treats the field as the burst size itself forbids bursts entirely on every endpoint that declared 0 — which is most of them, and which is a permanent stall rather than a slowdown, because a burst allowance of zero never permits anything.
Mutation C1 does that and dies 46 038 times. The design saturates at 255 rather than wrapping, for the same reason: bMaxBurst = 255 incrementing to zero would stall the highest-throughput endpoints on the bus.
5. The Flow, Drawn
The dashed ERDY edge is drawn deliberately. It reaches the decision and contributes nothing to it, which is exactly what §2 says and what mutation C4 breaks.
6. The Hardware, Before Any Language
One combinational decision and a small amount of state.
burst_size is bMaxBurst + 1, saturating (§4).
burst_room is what is left of the burst after packets already in flight, floored at zero — a sender that has already used its allowance may send nothing more until an ACK retires it.
may_send is the minimum of three (§3), and binding_limit names which.
An ACK does two things: it sets the credits, and it retires the outstanding count. Those move together — a design that decremented credits without tracking outstanding would permit a whole new burst the instant an ACK arrived, before the previous one had drained. Mutation C6, 20 267 errors.
A link reset discards the credit state entirely. Credits describe room in a receiver that has just been reset; carrying them across would let the sender transmit into a buffer that no longer exists. Mutation C5, 14 794 errors.
And flow_blocked is separate from "nothing to send" — the difference between a link that is idle and one that is stalled, which look identical from outside.
7. Verilog-2005
// usb3_credit_flow -- what USB 3 replaced polling with, and why the bus got
// quiet.
//
// EVERYTHING in Modules 1 to 19 rested on the host asking. A device with no
// data to give NAKs, and the host asks again -- and again -- and the asking
// costs bus time whether or not there is ever an answer. Chapter 12.4
// measured that cost; a bulk endpoint with nothing to say can consume a
// large fraction of a frame doing nothing.
//
// USB 3 keeps the single master and removes the polling, by inverting who
// tracks readiness:
//
// USB 2 the HOST asks, the device answers NAK, repeat.
// Readiness is discovered by trying.
//
// USB 3 the DEVICE says when it is ready (ERDY), and until then the
// host does not ask at all. Readiness is ANNOUNCED.
//
// and by giving the sender a budget instead of a permission slip:
//
// CREDITS the receiver tells the sender how many packets it can still
// accept (NumP, carried in an ACK transaction packet). The
// sender may have that many packets outstanding at once, and
// must stop when the budget is spent.
//
// THREE LIMITS, NOT ONE
//
// A sender is bounded by three separate things and the smallest wins:
//
// 1. CREDITS how many packets the receiver has room for now.
// 2. BURST SIZE bMaxBurst + 1 -- the most it may send back to back.
// 3. DATA how many packets it actually has.
//
// Conflating any two of them produces a design that works until the third
// becomes the binding one.
//
// bMaxBurst IS BURST SIZE MINUS ONE
//
// The SuperSpeed endpoint companion descriptor stores it that way, so a
// bMaxBurst of 0 means a burst of ONE packet, and 15 means 16. A design
// that treats the field as the burst size itself silently halves the
// throughput of every endpoint that declared 1 and forbids bursts entirely
// on every endpoint that declared 0.
//
// /* USB_DT_SS_ENDPOINT_COMP: SuperSpeed Endpoint Companion descriptor */
// struct usb_ss_ep_comp_descriptor {
// __u8 bLength;
// __u8 bDescriptorType;
// __u8 bMaxBurst;
// __u8 bmAttributes;
// __le16 wBytesPerInterval;
// } __attribute__ ((packed));
module usb3_credit_flow #(
parameter integer MAX_CREDITS = 31 // NumP is a bounded field
) (
input wire clk,
input wire rst_n,
// --- from the receiver, in an ACK transaction packet ---
input wire ack_valid,
input wire [7:0] ack_nump, // credits the receiver is advertising
input wire erdy, // "I am ready now" -- replaces polling
// --- the endpoint's own situation ---
input wire [7:0] b_max_burst, // the DESCRIPTOR field: burst MINUS one
input wire [7:0] packets_pending, // how many it actually has to send
input wire send, // one packet leaves the sender
input wire link_reset,
output wire [7:0] burst_size, // bMaxBurst + 1
output reg [7:0] credits, // what the receiver said it can take
output reg [7:0] outstanding, // sent but not yet acknowledged
output wire [7:0] may_send, // how many it may send RIGHT NOW
output wire can_send,
output wire [1:0] binding_limit, // WHICH of the three is binding
output reg ready_announced, // ERDY sent, host has not returned
output reg flow_blocked, // sticky: it wanted to send and could not
output reg [31:0] blocked_cycles,
output reg [31:0] packets_sent,
output reg credit_violation // sticky: a send with no credit
);
// THE OFF-BY-ONE THAT MATTERS. bMaxBurst is burst size minus one.
// Saturating at 255 so a descriptor declaring the maximum does not wrap
// to a burst of zero -- which would stall the endpoint permanently.
assign burst_size = (b_max_burst == 8'hFF) ? 8'hFF : (b_max_burst + 8'd1);
// How much of the burst allowance is left after what is already in flight.
wire [8:0] burst_room_w = {1'b0, burst_size} - {1'b0, outstanding};
wire [7:0] burst_room = (outstanding >= burst_size) ? 8'd0
: burst_room_w[7:0];
// THE MINIMUM OF THREE. Not two: a design that takes min(credits, burst)
// will happily claim it may send packets it does not have, and one that
// takes min(credits, pending) will overrun a burst the receiver's PHY
// cannot absorb back to back.
wire [7:0] min_cb = (credits < burst_room) ? credits : burst_room;
assign may_send = (min_cb < packets_pending) ? min_cb : packets_pending;
assign can_send = (may_send != 8'd0);
// Which limit is actually binding. This is not decoration: "the link is
// slow" has three completely different fixes depending on the answer --
// a bigger receive buffer, a larger bMaxBurst, or more data to send --
// and from outside the three are indistinguishable.
//
// Verilog-2005 has no enumerated type, so the encoding is localparams.
// Chapters 19.2 and 19.3 measured what happens when one language's
// design exposes less than the others, so all three expose this.
localparam [1:0] LIM_NONE = 2'd0, // everything pending can go now
LIM_CREDITS = 2'd1, // the receiver has no room
LIM_BURST = 2'd2, // the burst allowance is spent
LIM_PENDING = 2'd3; // there is simply nothing to send
assign binding_limit =
(packets_pending == 8'd0) ? LIM_PENDING
: (credits <= burst_room &&
credits < packets_pending) ? LIM_CREDITS
: (burst_room < packets_pending) ? LIM_BURST
: LIM_NONE;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
credits <= 8'd0;
outstanding <= 8'd0;
flow_blocked <= 1'b0;
blocked_cycles <= 32'd0;
packets_sent <= 32'd0;
credit_violation <= 1'b0;
ready_announced <= 1'b0;
end else if (link_reset) begin
// A link reset discards the credit state entirely. Credits describe
// room in a receiver that has just been reset; carrying them across
// would let the sender transmit into a buffer that no longer exists.
credits <= 8'd0;
outstanding <= 8'd0;
flow_blocked <= 1'b0;
ready_announced <= 1'b0;
end else begin
// An ERDY is a readiness ANNOUNCEMENT, not a credit grant. It tells
// the host to resume asking; the credits still come from an ACK.
// Treating ERDY as a credit is how a design ends up transmitting
// into a receiver that has room for nothing -- so it sets a flag and
// touches `credits` nowhere.
//
// The flag clears when the host comes back, which is what makes it
// measurable: the interval between the two is the latency USB 3
// trades polling bandwidth for, and a device that keeps re-announcing
// is a device whose ERDYs are not arriving.
if (erdy && !ack_valid) ready_announced <= 1'b1;
else if (ack_valid) ready_announced <= 1'b0;
if (ack_valid) begin
credits <= (ack_nump > MAX_CREDITS[7:0]) ? MAX_CREDITS[7:0] : ack_nump;
// An acknowledgement also retires what it acknowledges.
outstanding <= 8'd0;
end
if (send) begin
if (can_send) begin
// Spend one credit and add one to the outstanding count. The two
// move together: a design that decremented credits without
// tracking outstanding would allow a whole new burst the instant
// an ACK arrived, before the previous one had drained.
if (!ack_valid) begin
credits <= (credits == 8'd0) ? 8'd0 : (credits - 8'd1);
outstanding <= outstanding + 8'd1;
end
packets_sent <= packets_sent + 32'd1;
end else begin
// A send attempt with nothing to spend it on. This is a design
// error in the layer above, and it is RECORDED rather than
// silently dropped -- an unlogged violation is indistinguishable
// from a packet that was never offered.
credit_violation <= 1'b1;
end
end
// Wanting to send and being unable to is the condition worth
// measuring: it is the difference between a link that is idle and a
// link that is stalled, which look identical from outside.
if ((packets_pending != 8'd0) && !can_send) begin
flow_blocked <= 1'b1;
blocked_cycles <= blocked_cycles + 32'd1;
end
end
end
endmodulemin_cb then may_send is two comparisons, not one three-way expression, and the intermediate is named because §3's whole point is that there are three limits rather than two. A single nested ternary would hide which pair was being compared.
8. SystemVerilog
package usb3_flow_pkg;
// WHICH of the three limits is binding. This is not decoration: "the link
// is slow" has three completely different fixes depending on the answer --
// a bigger receive buffer, a larger bMaxBurst, or more data to send -- and
// from outside the three are indistinguishable.
typedef enum logic [1:0] {
LIM_NONE, // everything pending can go now
LIM_CREDITS, // the receiver has no room
LIM_BURST, // the burst allowance is spent
LIM_PENDING // there is simply nothing to send
} flow_limit_e;
endpackage
// usb3_credit_flow_sv -- what USB 3 replaced polling with, and why the bus got
// quiet.
//
// EVERYTHING in Modules 1 to 19 rested on the host asking. A device with no
// data to give NAKs, and the host asks again -- and again -- and the asking
// costs bus time whether or not there is ever an answer. Chapter 12.4
// measured that cost; a bulk endpoint with nothing to say can consume a
// large fraction of a frame doing nothing.
//
// USB 3 keeps the single master and removes the polling, by inverting who
// tracks readiness:
//
// USB 2 the HOST asks, the device answers NAK, repeat.
// Readiness is discovered by trying.
//
// USB 3 the DEVICE says when it is ready (ERDY), and until then the
// host does not ask at all. Readiness is ANNOUNCED.
//
// and by giving the sender a budget instead of a permission slip:
//
// CREDITS the receiver tells the sender how many packets it can still
// accept (NumP, carried in an ACK transaction packet). The
// sender may have that many packets outstanding at once, and
// must stop when the budget is spent.
//
// THREE LIMITS, NOT ONE
//
// A sender is bounded by three separate things and the smallest wins:
//
// 1. CREDITS how many packets the receiver has room for now.
// 2. BURST SIZE bMaxBurst + 1 -- the most it may send back to back.
// 3. DATA how many packets it actually has.
//
// Conflating any two of them produces a design that works until the third
// becomes the binding one.
//
// bMaxBurst IS BURST SIZE MINUS ONE
//
// The SuperSpeed endpoint companion descriptor stores it that way, so a
// bMaxBurst of 0 means a burst of ONE packet, and 15 means 16. A design
// that treats the field as the burst size itself silently halves the
// throughput of every endpoint that declared 1 and forbids bursts entirely
// on every endpoint that declared 0.
//
// /* USB_DT_SS_ENDPOINT_COMP: SuperSpeed Endpoint Companion descriptor */
// struct usb_ss_ep_comp_descriptor {
// __u8 bLength;
// __u8 bDescriptorType;
// __u8 bMaxBurst;
// __u8 bmAttributes;
// __le16 wBytesPerInterval;
// } __attribute__ ((packed));
module usb3_credit_flow_sv
import usb3_flow_pkg::*;
#(
parameter int unsigned MAX_CREDITS = 31 // NumP is a bounded field
) (
input logic clk,
input logic rst_n,
// --- from the receiver, in an ACK transaction packet ---
input logic ack_valid,
input logic [7:0] ack_nump, // credits the receiver is advertising
input logic erdy, // "I am ready now" -- replaces polling
// --- the endpoint's own situation ---
input logic [7:0] b_max_burst, // the DESCRIPTOR field: burst MINUS one
input logic [7:0] packets_pending, // how many it actually has to send
input logic send, // one packet leaves the sender
input logic link_reset,
output logic [7:0] burst_size, // bMaxBurst + 1
output logic [7:0] credits, // what the receiver said it can take
output logic [7:0] outstanding, // sent but not yet acknowledged
output logic [7:0] may_send, // how many it may send RIGHT NOW
output logic can_send,
output flow_limit_e binding_limit, // WHICH of the three is binding
output logic ready_announced, // ERDY sent, host has not returned
output logic flow_blocked, // sticky: it wanted to send and could not
output logic [31:0] blocked_cycles,
output logic [31:0] packets_sent,
output logic credit_violation // sticky: a send with no credit
);
initial begin
if (MAX_CREDITS < 1)
$fatal(1, "MAX_CREDITS=%0d: a link that can never grant a credit can never carry data",
MAX_CREDITS);
if (MAX_CREDITS > 255)
$fatal(1, "MAX_CREDITS=%0d exceeds what the NumP field can express",
MAX_CREDITS);
end
// THE OFF-BY-ONE THAT MATTERS. bMaxBurst is burst size minus one.
// Saturating at 255 so a descriptor declaring the maximum does not wrap
// to a burst of zero -- which would stall the endpoint permanently.
assign burst_size = (b_max_burst == 8'hFF) ? 8'hFF : (b_max_burst + 8'd1);
// How much of the burst allowance is left after what is already in flight.
wire [8:0] burst_room_w = {1'b0, burst_size} - {1'b0, outstanding};
wire [7:0] burst_room = (outstanding >= burst_size) ? 8'd0
: burst_room_w[7:0];
// THE MINIMUM OF THREE. Not two: a design that takes min(credits, burst)
// will happily claim it may send packets it does not have, and one that
// takes min(credits, pending) will overrun a burst the receiver's PHY
// cannot absorb back to back.
wire [7:0] min_cb = (credits < burst_room) ? credits : burst_room;
assign may_send = (min_cb < packets_pending) ? min_cb : packets_pending;
assign can_send = (may_send != 8'd0);
// Which limit is actually binding. This is not decoration: "the link is
// slow" has three completely different fixes depending on the answer --
// a bigger receive buffer, a larger bMaxBurst, or more data to send --
// and from outside the three are indistinguishable.
//
// Written as branches rather than nested ternaries: a conditional
// expression yielding an enum needs an explicit cast in some tools.
always_comb begin
if (packets_pending == 8'd0) binding_limit = LIM_PENDING;
else if (credits <= burst_room
&& credits < packets_pending) binding_limit = LIM_CREDITS;
else if (burst_room < packets_pending) binding_limit = LIM_BURST;
else binding_limit = LIM_NONE;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
credits <= 8'd0;
outstanding <= 8'd0;
flow_blocked <= 1'b0;
blocked_cycles <= 32'd0;
packets_sent <= 32'd0;
credit_violation <= 1'b0;
ready_announced <= 1'b0;
end else if (link_reset) begin
// A link reset discards the credit state entirely. Credits describe
// room in a receiver that has just been reset; carrying them across
// would let the sender transmit into a buffer that no longer exists.
credits <= 8'd0;
outstanding <= 8'd0;
flow_blocked <= 1'b0;
ready_announced <= 1'b0;
end else begin
// An ERDY is a readiness ANNOUNCEMENT, not a credit grant. It tells
// the host to resume asking; the credits still come from an ACK.
// Treating ERDY as a credit is how a design ends up transmitting
// into a receiver that has room for nothing -- so it sets a flag and
// touches `credits` nowhere.
//
// The flag clears when the host comes back, which is what makes it
// measurable: the interval between the two is the latency USB 3
// trades polling bandwidth for, and a device that keeps re-announcing
// is a device whose ERDYs are not arriving.
if (erdy && !ack_valid) ready_announced <= 1'b1;
else if (ack_valid) ready_announced <= 1'b0;
if (ack_valid) begin
credits <= (ack_nump > 8'(MAX_CREDITS)) ? 8'(MAX_CREDITS) : ack_nump;
// An acknowledgement also retires what it acknowledges.
outstanding <= 8'd0;
end
if (send) begin
if (can_send) begin
// Spend one credit and add one to the outstanding count. The two
// move together: a design that decremented credits without
// tracking outstanding would allow a whole new burst the instant
// an ACK arrived, before the previous one had drained.
if (!ack_valid) begin
credits <= (credits == 8'd0) ? 8'd0 : (credits - 8'd1);
outstanding <= outstanding + 8'd1;
end
packets_sent <= packets_sent + 32'd1;
end else begin
// A send attempt with nothing to spend it on. This is a design
// error in the layer above, and it is RECORDED rather than
// silently dropped -- an unlogged violation is indistinguishable
// from a packet that was never offered.
credit_violation <= 1'b1;
end
end
// Wanting to send and being unable to is the condition worth
// measuring: it is the difference between a link that is idle and a
// link that is stalled, which look identical from outside.
if ((packets_pending != 8'd0) && !can_send) begin
flow_blocked <= 1'b1;
blocked_cycles <= blocked_cycles + 32'd1;
end
end
end
endmoduleflow_limit_e has four values because there are four answers, and LIM_PENDING is the one people leave out. An endpoint with nothing to send is not flow-controlled — it is idle and working correctly — and a design that reported LIM_CREDITS for it would send a debugging effort after a receive buffer that is perfectly fine.
9. VHDL-2008
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package usb3_flow_pkg is
-- WHICH of the three limits is binding. This is not decoration: "the link
-- is slow" has three completely different fixes depending on the answer --
-- a bigger receive buffer, a larger bMaxBurst, or more data to send -- and
-- from outside the three are indistinguishable.
type flow_limit_t is (
LIM_NONE, -- everything pending can go now
LIM_CREDITS, -- the receiver has no room
LIM_BURST, -- the burst allowance is spent
LIM_PENDING -- there is simply nothing to send
);
end package;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb3_flow_pkg.all;
-- usb3_credit_flow_vhdl -- what USB 3 replaced polling with, and why the bus
-- got quiet.
--
-- EVERYTHING in Modules 1 to 19 rested on the host asking. A device with no
-- data to give NAKs, and the host asks again -- and again -- and the asking
-- costs bus time whether or not there is ever an answer.
--
-- USB 3 keeps the single master and removes the polling, by inverting who
-- tracks readiness:
--
-- USB 2 the HOST asks, the device answers NAK, repeat.
-- Readiness is discovered by trying.
--
-- USB 3 the DEVICE says when it is ready (ERDY), and until then the
-- host does not ask at all. Readiness is ANNOUNCED.
--
-- and by giving the sender a budget instead of a permission slip:
--
-- CREDITS the receiver tells the sender how many packets it can still
-- accept (NumP, carried in an ACK transaction packet).
--
-- THREE LIMITS, NOT ONE
--
-- A sender is bounded by three separate things and the smallest wins:
--
-- 1. CREDITS how many packets the receiver has room for now.
-- 2. BURST SIZE bMaxBurst + 1 -- the most it may send back to back.
-- 3. DATA how many packets it actually has.
--
-- bMaxBurst IS BURST SIZE MINUS ONE. The SuperSpeed endpoint companion
-- descriptor stores it that way, so a bMaxBurst of 0 means a burst of ONE
-- packet and 15 means 16. A design that treats the field as the burst size
-- itself forbids bursts entirely on every endpoint that declared 0.
entity usb3_credit_flow_vhdl is
generic (
MAX_CREDITS : positive := 31 -- NumP is a bounded field
);
port (
clk : in std_logic;
rst_n : in std_logic;
ack_valid : in std_logic;
ack_nump : in unsigned(7 downto 0);
erdy : in std_logic;
b_max_burst : in unsigned(7 downto 0); -- the DESCRIPTOR field
packets_pending : in unsigned(7 downto 0);
send : in std_logic;
link_reset : in std_logic;
burst_size : out unsigned(7 downto 0); -- bMaxBurst + 1
credits : out unsigned(7 downto 0);
outstanding : out unsigned(7 downto 0);
may_send : out unsigned(7 downto 0);
can_send : out std_logic;
binding_limit : out flow_limit_t;
ready_announced : out std_logic;
flow_blocked : out std_logic;
blocked_cycles : out unsigned(31 downto 0);
packets_sent : out unsigned(31 downto 0);
credit_violation : out std_logic
);
end entity;
architecture rtl of usb3_credit_flow_vhdl is
signal cred_r : unsigned(7 downto 0) := (others => '0');
signal out_r : unsigned(7 downto 0) := (others => '0');
signal rdy_r : std_logic := '0';
signal blk_r : std_logic := '0';
signal blkc_r : unsigned(31 downto 0) := (others => '0');
signal sent_r : unsigned(31 downto 0) := (others => '0');
signal viol_r : std_logic := '0';
signal bsz_i : unsigned(7 downto 0);
signal room_i : unsigned(7 downto 0);
signal may_i : unsigned(7 downto 0);
signal mincb_i : unsigned(7 downto 0);
begin
assert MAX_CREDITS <= 255
report "MAX_CREDITS exceeds what the NumP field can express"
severity failure;
-- THE OFF-BY-ONE THAT MATTERS. bMaxBurst is burst size minus one.
-- Saturating at 255 so a descriptor declaring the maximum does not wrap
-- to a burst of zero -- which would stall the endpoint permanently.
bsz_i <= to_unsigned(255, 8) when b_max_burst = to_unsigned(255, 8)
else b_max_burst + 1;
-- How much of the burst allowance is left after what is already in flight.
room_i <= to_unsigned(0, 8) when out_r >= bsz_i else (bsz_i - out_r);
-- THE MINIMUM OF THREE. Not two: a design that takes min(credits, burst)
-- will happily claim it may send packets it does not have, and one that
-- takes min(credits, pending) will overrun a burst the receiver's PHY
-- cannot absorb back to back.
mincb_i <= cred_r when cred_r < room_i else room_i;
may_i <= mincb_i when mincb_i < packets_pending else packets_pending;
burst_size <= bsz_i;
credits <= cred_r;
outstanding <= out_r;
may_send <= may_i;
can_send <= '1' when may_i /= to_unsigned(0, 8) else '0';
ready_announced <= rdy_r;
flow_blocked <= blk_r;
blocked_cycles <= blkc_r;
packets_sent <= sent_r;
credit_violation <= viol_r;
binding_limit <= LIM_PENDING when packets_pending = to_unsigned(0, 8) else
LIM_CREDITS when (cred_r <= room_i
and cred_r < packets_pending) else
LIM_BURST when room_i < packets_pending else
LIM_NONE;
process (clk, rst_n)
variable nc, no : unsigned(7 downto 0);
begin
if rst_n = '0' then
cred_r <= (others => '0'); out_r <= (others => '0');
rdy_r <= '0'; blk_r <= '0';
blkc_r <= (others => '0'); sent_r <= (others => '0');
viol_r <= '0';
elsif rising_edge(clk) then
if link_reset = '1' then
-- A link reset discards the credit state entirely. Credits describe
-- room in a receiver that has just been reset; carrying them across
-- would let the sender transmit into a buffer that no longer exists.
cred_r <= (others => '0'); out_r <= (others => '0');
blk_r <= '0'; rdy_r <= '0';
else
-- An ERDY is a readiness ANNOUNCEMENT, not a credit grant. It tells
-- the host to resume asking; the credits still come from an ACK.
-- Treating ERDY as a credit is how a design ends up transmitting
-- into a receiver that has room for nothing -- so it sets a flag
-- and touches the credit count nowhere.
if erdy = '1' and ack_valid = '0' then rdy_r <= '1';
elsif ack_valid = '1' then rdy_r <= '0'; end if;
nc := cred_r; no := out_r;
if ack_valid = '1' then
if ack_nump > to_unsigned(MAX_CREDITS, 8) then
nc := to_unsigned(MAX_CREDITS, 8);
else
nc := ack_nump;
end if;
-- An acknowledgement also retires what it acknowledges.
no := (others => '0');
end if;
if send = '1' then
if may_i /= to_unsigned(0, 8) then
-- Spend one credit and add one to the outstanding count. The
-- two move together: a design that decremented credits without
-- tracking outstanding would allow a whole new burst the
-- instant an ACK arrived, before the previous one had drained.
if ack_valid = '0' then
if nc /= to_unsigned(0, 8) then nc := nc - 1; end if;
no := no + 1;
end if;
sent_r <= sent_r + 1;
else
-- A send attempt with nothing to spend it on. Recorded rather
-- than silently dropped: an unlogged violation is
-- indistinguishable from a packet that was never offered.
viol_r <= '1';
end if;
end if;
cred_r <= nc; out_r <= no;
-- Wanting to send and being unable to is the condition worth
-- measuring: it is the difference between a link that is idle and
-- a link that is stalled, which look identical from outside.
if packets_pending /= to_unsigned(0, 8)
and may_i = to_unsigned(0, 8) then
blk_r <= '1';
blkc_r <= blkc_r + 1;
end if;
end if;
end if;
end process;
end architecture;The process computes nc and no as variables and assigns them to the signals once, at the end. That is idiomatic VHDL and it reads clearly — and §12 is about the fact that it also silently neutralised a mutation written against the signals.
10. The Testbench: 4096 Points and 64 Burst Walks
The decision is a pure function of three numbers, so the sweep enumerates them:
// 16 bMaxBurst values x 16 credit grants x 16 pending counts = 4096
// points with nothing outstanding: the whole decision surface.
for (b=0; b<16; b=b+1)
for (c=0; c<16; c=c+1)
for (a=0; a<16; a=a+1) beginAnd a second sweep drives the burst allowance down by actually sending, because burst_room is state rather than an input:
// For each bMaxBurst 0..7, send k packets 0..7 and check the room left,
// with credits held high so the BURST limit is the one under test.Four safety properties are checked against no model, and the first is the one with consequences:
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE one that matters: never claim to be able to send more than
// the receiver said it has room for. Overrunning a credit is a
// receive-buffer overflow, not a retryable protocol error.
check(may_send <= credits,
"may_send exceeded the credits the receiver advertised");
// 2. Nor more than the burst allowance permits.
check(outstanding + may_send <= burst_size || outstanding >= burst_size,
"a burst would exceed bMaxBurst + 1");
// 3. Nor more packets than actually exist.
check(may_send <= pend, "may_send exceeded the packets pending");
// 4. burst_size is never zero: a zero burst stalls the endpoint for
// ever, and bMaxBurst = 0 legitimately means a burst of ONE.
check(burst_size !== 8'd0, "burst_size collapsed to zero");Properties 1, 2 and 3 are the three limits restated as inequalities — which is a different statement from "may_send equals the minimum", and a stronger one: it holds whatever the design computed, including if it computed something the model also got wrong.
And the model takes the minimum by sorting three values where the design nests two comparisons. Same answer, different route.
Measured reach:
exhaustive min-of-three sweep: 4096 of 4096 points verified
exhaustive burst-consumption sweep: 64 of 64 verified
Verilog / SystemVerilog:
REACH: binding: none=18313 credits=22702 burst=1608 pending=1120
[Verilog] usb3_credit_flow: 0 errors — PASS
VHDL:
REACH: binding: none=18206 credits=22528 burst=1596 pending=1457
[VHDL] usb3_credit_flow_vhdl: 0 errors — PASSAll four binding cases are reached thousands of times, which is the sweep's justification: a run that never reaches LIM_BURST has not tested §4's off-by-one at all, however many credits it varied.
10.1 The complete Verilog testbench
The excerpts above are the parts worth arguing about. Here is the whole thing — the sweeps, the reference model, the safety properties and the reach assertions, exactly as simulated against the usb3_credit_flow listing published in this chapter.
`timescale 1ns/1ps
module tb_cf_v;
localparam MAXC = 31;
reg clk=0, rst_n=0, ack_valid=0, erdy=0, send=0, link_reset=0;
reg [7:0] ack_nump=0, bmb=0, pend=0;
wire [7:0] burst_size, credits, outstanding, may_send;
wire can_send, flow_blocked, credit_violation, ready_announced;
wire [1:0] binding_limit;
wire [31:0] blocked_cycles, packets_sent;
always #5 clk=~clk;
usb3_credit_flow #(.MAX_CREDITS(MAXC)) dut (
.clk(clk), .rst_n(rst_n), .ack_valid(ack_valid), .ack_nump(ack_nump),
.erdy(erdy), .b_max_burst(bmb), .packets_pending(pend), .send(send),
.link_reset(link_reset), .burst_size(burst_size), .credits(credits),
.outstanding(outstanding), .may_send(may_send), .can_send(can_send),
.binding_limit(binding_limit), .ready_announced(ready_announced),
.flow_blocked(flow_blocked), .blocked_cycles(blocked_cycles),
.packets_sent(packets_sent), .credit_violation(credit_violation));
localparam [1:0] L_NONE=0, L_CRED=1, L_BURST=2, L_PEND=3;
integer errors=0, i, a, b, c, k;
integer n_exh=0, n_burst_exh=0;
integer n_lim [0:3];
// ---- INDEPENDENT MODEL: its own credits, outstanding and flags ----
integer m_cred, m_out, m_sent;
reg m_ready, m_viol;
task check(input cond, input [639:0] msg);
begin if (!cond) begin errors=errors+1;
if (errors <= 25)
$display(" FAIL: %0s (bmb=%0d cred=%0d out=%0d pend=%0d | bsz=%0d may=%0d lim=%0d, t=%0t)",
msg, bmb, credits, outstanding, pend, burst_size, may_send,
binding_limit, $time);
end end
endtask
// The model computes the minimum by sorting three values rather than by
// two nested comparisons. Same answer, different route.
function [7:0] min3(input [15:0] x, input [15:0] y, input [15:0] z);
integer lo;
begin
lo = x; if (y < lo) lo = y; if (z < lo) lo = z;
min3 = lo[7:0];
end
endfunction
task check_comb;
integer e_bsz, e_room, e_may, e_lim;
begin
e_bsz = (bmb == 255) ? 255 : bmb + 1;
e_room = (m_out >= e_bsz) ? 0 : (e_bsz - m_out);
e_may = min3(m_cred, e_room, pend);
if (pend == 0) e_lim = L_PEND;
else if (m_cred <= e_room && m_cred < pend) e_lim = L_CRED;
else if (e_room < pend) e_lim = L_BURST;
else e_lim = L_NONE;
check(burst_size === e_bsz[7:0], "burst_size is bMaxBurst PLUS ONE");
check(credits === m_cred[7:0], "credits matches the model");
check(outstanding === m_out[7:0], "outstanding matches the model");
check(may_send === e_may, "may_send is the MINIMUM of three");
check(can_send === (e_may != 0), "can_send tracks may_send");
check(binding_limit === e_lim[1:0], "binding_limit names the tightest");
check(ready_announced === m_ready, "ready_announced matches the model");
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE one that matters: never claim to be able to send more than
// the receiver said it has room for. Overrunning a credit is a
// receive-buffer overflow, not a retryable protocol error.
check(may_send <= credits,
"may_send exceeded the credits the receiver advertised");
// 2. Nor more than the burst allowance permits.
check(outstanding + may_send <= burst_size || outstanding >= burst_size,
"a burst would exceed bMaxBurst + 1");
// 3. Nor more packets than actually exist.
check(may_send <= pend, "may_send exceeded the packets pending");
// 4. burst_size is never zero: a zero burst stalls the endpoint for
// ever, and bMaxBurst = 0 legitimately means a burst of ONE.
check(burst_size !== 8'd0, "burst_size collapsed to zero");
if (e_lim >= 0 && e_lim <= 3) n_lim[e_lim] = n_lim[e_lim] + 1;
end
endtask
task step(input av, input [7:0] nump, input er, input sd, input lr);
integer nc, no, e_may, e_bsz, e_room;
begin
ack_valid=av; ack_nump=nump; erdy=er; send=sd; link_reset=lr; #1;
check_comb;
e_bsz = (bmb == 255) ? 255 : bmb + 1;
e_room = (m_out >= e_bsz) ? 0 : (e_bsz - m_out);
e_may = min3(m_cred, e_room, pend);
@(posedge clk); #1;
// advance the model in the design's precedence order
if (lr) begin m_cred=0; m_out=0; m_ready=0; end
else begin
if (er && !av) m_ready = 1; else if (av) m_ready = 0;
nc = m_cred; no = m_out;
if (av) begin nc = (nump > MAXC) ? MAXC : nump; no = 0; end
if (sd) begin
if (e_may != 0) begin
if (!av) begin
nc = (nc == 0) ? 0 : nc - 1;
no = no + 1;
end
m_sent = m_sent + 1;
end else m_viol = 1;
end
m_cred = nc; m_out = no;
end
#1;
check(packets_sent === m_sent[31:0], "packets_sent matches the model");
check(credit_violation === m_viol, "credit_violation matches the model");
end
endtask
task hard_reset;
begin
rst_n=0; ack_valid=0; ack_nump=0; erdy=0; send=0; link_reset=0;
@(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
m_cred=0; m_out=0; m_sent=0; m_ready=0; m_viol=0;
end
endtask
initial begin
for (i=0;i<4;i=i+1) n_lim[i]=0;
hard_reset;
check(credits === 8'd0, "a link starts with no credits");
check(!can_send, "and can send nothing");
// ===== A. EXHAUSTIVE over the min-of-three decision =====
// 16 bMaxBurst values x 16 credit grants x 16 pending counts = 4096
// points with nothing outstanding: the whole decision surface.
for (b=0; b<16; b=b+1)
for (c=0; c<16; c=c+1)
for (a=0; a<16; a=a+1) begin
hard_reset;
bmb = b[7:0]; pend = a[7:0];
step(1'b1, c[7:0], 1'b0, 1'b0, 1'b0); // grant c credits
n_exh = n_exh + 1;
end
$display(" exhaustive min-of-three sweep: %0d of %0d points verified",
n_exh, 16*16*16);
// ===== B. EXHAUSTIVE over the burst allowance being consumed =====
// For each bMaxBurst 0..7, send k packets 0..7 and check the room left,
// with credits held high so the BURST limit is the one under test.
for (b=0; b<8; b=b+1)
for (k=0; k<8; k=k+1) begin
hard_reset;
bmb = b[7:0]; pend = 8'd15;
step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0); // plenty of credits
for (i=0;i<k;i=i+1)
if (can_send) step(1'b0, 8'd0, 1'b0, 1'b1, 1'b0);
else step(1'b0, 8'd0, 1'b0, 1'b0, 1'b0);
n_burst_exh = n_burst_exh + 1;
end
$display(" exhaustive burst-consumption sweep: %0d of %0d verified",
n_burst_exh, 8*8);
// ===== C. directed: the off-by-one, and the three limits =====
hard_reset; bmb = 8'd0; pend = 8'd8;
step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0);
check(burst_size === 8'd1,
"bMaxBurst = 0 means a burst of ONE packet, not zero");
check(may_send === 8'd1, "so exactly one packet may go");
check(binding_limit === L_BURST, "and the BURST is what binds");
hard_reset; bmb = 8'd15; pend = 8'd8;
step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0);
check(burst_size === 8'd16, "bMaxBurst = 15 means a burst of SIXTEEN");
check(may_send === 8'd8, "so all eight pending packets may go");
check(binding_limit === L_NONE, "and nothing binds");
hard_reset; bmb = 8'd15; pend = 8'd8;
step(1'b1, 8'd3, 1'b0, 1'b0, 1'b0);
check(may_send === 8'd3, "three credits caps it at three");
check(binding_limit === L_CRED, "and the CREDITS are what bind");
hard_reset; bmb = 8'd15; pend = 8'd0;
step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0);
check(may_send === 8'd0, "nothing pending, nothing to send");
check(binding_limit === L_PEND, "and that is a DIFFERENT reason");
check(!flow_blocked, "which is not the same as being blocked");
// ERDY is not a credit
hard_reset; bmb = 8'd15; pend = 8'd8;
step(1'b0, 8'd0, 1'b1, 1'b0, 1'b0);
check(credits === 8'd0, "an ERDY grants NO credits");
check(ready_announced, "it announces readiness and nothing else");
check(!can_send, "so nothing may be sent on the strength of it");
step(1'b1, 8'd4, 1'b0, 1'b0, 1'b0);
check(credits === 8'd4, "the credits come from the ACK");
check(!ready_announced, "and the announcement is retired");
// a link reset discards the credit state
hard_reset; bmb = 8'd15; pend = 8'd8;
step(1'b1, 8'd8, 1'b0, 1'b0, 1'b0);
check(credits === 8'd8, "credits granted");
step(1'b0, 8'd0, 1'b0, 1'b0, 1'b1);
check(credits === 8'd0,
"a link reset discards them: the receiver's buffer is gone");
// ===== D. randomised =====
for (i=0;i<40000;i=i+1) begin
if (({$random}%64)==0) begin
hard_reset; bmb={$random}%256; pend={$random}%32;
end else begin
if (({$random}%16)==0) pend = {$random}%32;
step(({$random}%5)==0, {$random}%40, ({$random}%8)==0,
({$random}%3)==0, ({$random}%128)==0);
end
end
check(n_lim[0]>0 && n_lim[1]>0 && n_lim[2]>0 && n_lim[3]>0,
"the run reached ALL FOUR binding-limit cases");
$display("");
$display(" REACH: min3=%0d burst=%0d | binding: none=%0d credits=%0d burst=%0d pending=%0d",
n_exh, n_burst_exh, n_lim[0], n_lim[1], n_lim[2], n_lim[3]);
$display(" [Verilog] usb3_credit_flow: %0d errors", errors);
$display(" [Verilog] %0s", errors==0 ? "PASS" : "FAIL");
$display("");
$finish;
end
endmodule10.2 The complete SystemVerilog testbench
Same structure, with the enumerated types doing the work that localparams do in the Verilog build — which is what makes a failure message name a state instead of printing a number.
`timescale 1ns/1ps
module tb_cf_sv;
import usb3_flow_pkg::*;
localparam MAXC = 31;
reg clk=0, rst_n=0, ack_valid=0, erdy=0, send=0, link_reset=0;
reg [7:0] ack_nump=0, bmb=0, pend=0;
wire [7:0] burst_size, credits, outstanding, may_send;
wire can_send, flow_blocked, credit_violation, ready_announced;
flow_limit_e binding_limit;
wire [31:0] blocked_cycles, packets_sent;
always #5 clk=~clk;
usb3_credit_flow_sv #(.MAX_CREDITS(MAXC)) dut (
.clk(clk), .rst_n(rst_n), .ack_valid(ack_valid), .ack_nump(ack_nump),
.erdy(erdy), .b_max_burst(bmb), .packets_pending(pend), .send(send),
.link_reset(link_reset), .burst_size(burst_size), .credits(credits),
.outstanding(outstanding), .may_send(may_send), .can_send(can_send),
.binding_limit(binding_limit), .ready_announced(ready_announced),
.flow_blocked(flow_blocked), .blocked_cycles(blocked_cycles),
.packets_sent(packets_sent), .credit_violation(credit_violation));
localparam flow_limit_e L_NONE=LIM_NONE, L_CRED=LIM_CREDITS,
L_BURST=LIM_BURST, L_PEND=LIM_PENDING;
integer errors=0, i, a, b, c, k;
integer n_exh=0, n_burst_exh=0;
integer n_lim [0:3];
// ---- INDEPENDENT MODEL: its own credits, outstanding and flags ----
integer m_cred, m_out, m_sent;
reg m_ready, m_viol;
task check(input cond, input [639:0] msg);
begin if (!cond) begin errors=errors+1;
if (errors <= 25)
$display(" FAIL: %0s (bmb=%0d cred=%0d out=%0d pend=%0d | bsz=%0d may=%0d lim=%0d, t=%0t)",
msg, bmb, credits, outstanding, pend, burst_size, may_send,
binding_limit, $time);
end end
endtask
// The model computes the minimum by sorting three values rather than by
// two nested comparisons. Same answer, different route.
function [7:0] min3(input [15:0] x, input [15:0] y, input [15:0] z);
integer lo;
begin
lo = x; if (y < lo) lo = y; if (z < lo) lo = z;
min3 = lo[7:0];
end
endfunction
task check_comb;
integer e_bsz, e_room, e_may, e_lim;
begin
e_bsz = (bmb == 255) ? 255 : bmb + 1;
e_room = (m_out >= e_bsz) ? 0 : (e_bsz - m_out);
e_may = min3(m_cred, e_room, pend);
if (pend == 0) e_lim = L_PEND;
else if (m_cred <= e_room && m_cred < pend) e_lim = L_CRED;
else if (e_room < pend) e_lim = L_BURST;
else e_lim = L_NONE;
check(burst_size === e_bsz[7:0], "burst_size is bMaxBurst PLUS ONE");
check(credits === m_cred[7:0], "credits matches the model");
check(outstanding === m_out[7:0], "outstanding matches the model");
check(may_send === e_may, "may_send is the MINIMUM of three");
check(can_send === (e_may != 0), "can_send tracks may_send");
check(binding_limit === flow_limit_e'(e_lim[1:0]),
"binding_limit names the tightest");
check(ready_announced === m_ready, "ready_announced matches the model");
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE one that matters: never claim to be able to send more than
// the receiver said it has room for. Overrunning a credit is a
// receive-buffer overflow, not a retryable protocol error.
check(may_send <= credits,
"may_send exceeded the credits the receiver advertised");
// 2. Nor more than the burst allowance permits.
check(outstanding + may_send <= burst_size || outstanding >= burst_size,
"a burst would exceed bMaxBurst + 1");
// 3. Nor more packets than actually exist.
check(may_send <= pend, "may_send exceeded the packets pending");
// 4. burst_size is never zero: a zero burst stalls the endpoint for
// ever, and bMaxBurst = 0 legitimately means a burst of ONE.
check(burst_size !== 8'd0, "burst_size collapsed to zero");
if (e_lim >= 0 && e_lim <= 3) n_lim[e_lim] = n_lim[e_lim] + 1;
end
endtask
task step(input av, input [7:0] nump, input er, input sd, input lr);
integer nc, no, e_may, e_bsz, e_room;
begin
ack_valid=av; ack_nump=nump; erdy=er; send=sd; link_reset=lr; #1;
check_comb;
e_bsz = (bmb == 255) ? 255 : bmb + 1;
e_room = (m_out >= e_bsz) ? 0 : (e_bsz - m_out);
e_may = min3(m_cred, e_room, pend);
@(posedge clk); #1;
// advance the model in the design's precedence order
if (lr) begin m_cred=0; m_out=0; m_ready=0; end
else begin
if (er && !av) m_ready = 1; else if (av) m_ready = 0;
nc = m_cred; no = m_out;
if (av) begin nc = (nump > MAXC) ? MAXC : nump; no = 0; end
if (sd) begin
if (e_may != 0) begin
if (!av) begin
nc = (nc == 0) ? 0 : nc - 1;
no = no + 1;
end
m_sent = m_sent + 1;
end else m_viol = 1;
end
m_cred = nc; m_out = no;
end
#1;
check(packets_sent === m_sent[31:0], "packets_sent matches the model");
check(credit_violation === m_viol, "credit_violation matches the model");
end
endtask
task hard_reset;
begin
rst_n=0; ack_valid=0; ack_nump=0; erdy=0; send=0; link_reset=0;
@(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
m_cred=0; m_out=0; m_sent=0; m_ready=0; m_viol=0;
end
endtask
initial begin
for (i=0;i<4;i=i+1) n_lim[i]=0;
hard_reset;
check(credits === 8'd0, "a link starts with no credits");
check(!can_send, "and can send nothing");
// ===== A. EXHAUSTIVE over the min-of-three decision =====
// 16 bMaxBurst values x 16 credit grants x 16 pending counts = 4096
// points with nothing outstanding: the whole decision surface.
for (b=0; b<16; b=b+1)
for (c=0; c<16; c=c+1)
for (a=0; a<16; a=a+1) begin
hard_reset;
bmb = b[7:0]; pend = a[7:0];
step(1'b1, c[7:0], 1'b0, 1'b0, 1'b0); // grant c credits
n_exh = n_exh + 1;
end
$display(" exhaustive min-of-three sweep: %0d of %0d points verified",
n_exh, 16*16*16);
// ===== B. EXHAUSTIVE over the burst allowance being consumed =====
// For each bMaxBurst 0..7, send k packets 0..7 and check the room left,
// with credits held high so the BURST limit is the one under test.
for (b=0; b<8; b=b+1)
for (k=0; k<8; k=k+1) begin
hard_reset;
bmb = b[7:0]; pend = 8'd15;
step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0); // plenty of credits
for (i=0;i<k;i=i+1)
if (can_send) step(1'b0, 8'd0, 1'b0, 1'b1, 1'b0);
else step(1'b0, 8'd0, 1'b0, 1'b0, 1'b0);
n_burst_exh = n_burst_exh + 1;
end
$display(" exhaustive burst-consumption sweep: %0d of %0d verified",
n_burst_exh, 8*8);
// ===== C. directed: the off-by-one, and the three limits =====
hard_reset; bmb = 8'd0; pend = 8'd8;
step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0);
check(burst_size === 8'd1,
"bMaxBurst = 0 means a burst of ONE packet, not zero");
check(may_send === 8'd1, "so exactly one packet may go");
check(binding_limit === L_BURST, "and the BURST is what binds");
hard_reset; bmb = 8'd15; pend = 8'd8;
step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0);
check(burst_size === 8'd16, "bMaxBurst = 15 means a burst of SIXTEEN");
check(may_send === 8'd8, "so all eight pending packets may go");
check(binding_limit === L_NONE, "and nothing binds");
hard_reset; bmb = 8'd15; pend = 8'd8;
step(1'b1, 8'd3, 1'b0, 1'b0, 1'b0);
check(may_send === 8'd3, "three credits caps it at three");
check(binding_limit === L_CRED, "and the CREDITS are what bind");
hard_reset; bmb = 8'd15; pend = 8'd0;
step(1'b1, 8'd31, 1'b0, 1'b0, 1'b0);
check(may_send === 8'd0, "nothing pending, nothing to send");
check(binding_limit === L_PEND, "and that is a DIFFERENT reason");
check(!flow_blocked, "which is not the same as being blocked");
// ERDY is not a credit
hard_reset; bmb = 8'd15; pend = 8'd8;
step(1'b0, 8'd0, 1'b1, 1'b0, 1'b0);
check(credits === 8'd0, "an ERDY grants NO credits");
check(ready_announced, "it announces readiness and nothing else");
check(!can_send, "so nothing may be sent on the strength of it");
step(1'b1, 8'd4, 1'b0, 1'b0, 1'b0);
check(credits === 8'd4, "the credits come from the ACK");
check(!ready_announced, "and the announcement is retired");
// a link reset discards the credit state
hard_reset; bmb = 8'd15; pend = 8'd8;
step(1'b1, 8'd8, 1'b0, 1'b0, 1'b0);
check(credits === 8'd8, "credits granted");
step(1'b0, 8'd0, 1'b0, 1'b0, 1'b1);
check(credits === 8'd0,
"a link reset discards them: the receiver's buffer is gone");
// ===== D. randomised =====
for (i=0;i<40000;i=i+1) begin
if (({$random}%64)==0) begin
hard_reset; bmb={$random}%256; pend={$random}%32;
end else begin
if (({$random}%16)==0) pend = {$random}%32;
step(({$random}%5)==0, {$random}%40, ({$random}%8)==0,
({$random}%3)==0, ({$random}%128)==0);
end
end
check(n_lim[0]>0 && n_lim[1]>0 && n_lim[2]>0 && n_lim[3]>0,
"the run reached ALL FOUR binding-limit cases");
$display("");
$display(" REACH: min3=%0d burst=%0d | binding: none=%0d credits=%0d burst=%0d pending=%0d",
n_exh, n_burst_exh, n_lim[0], n_lim[1], n_lim[2], n_lim[3]);
$display(" [SystemVerilog] usb3_credit_flow_sv: %0d errors", errors);
$display(" [SystemVerilog] %0s", errors==0 ? "PASS" : "FAIL");
$display("");
$finish;
end
endmodule10.3 The complete VHDL testbench
VHDL-2008 requires a shared variable to have a protected type, so all the bookkeeping lives in process variables inside the single stimulus process. The randomisation uses ieee.math_real.uniform, which is a genuinely different generator from either Verilog builtin — see Chapter 20.5 §9.2 for why that distinction turned out to matter across this whole module.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
use work.usb3_flow_pkg.all;
entity tb_cf_vhdl is end entity;
architecture sim of tb_cf_vhdl is
constant MAXC : positive := 31;
signal clk, rst_n : std_logic := '0';
signal ack_valid, erdy, send, link_reset : std_logic := '0';
signal ack_nump, bmb, pend : unsigned(7 downto 0) := (others => '0');
signal burst_size, credits, outstanding, may_send : unsigned(7 downto 0);
signal can_send, flow_blocked, credit_violation, ready_announced
: std_logic;
signal binding_limit : flow_limit_t;
signal blocked_cycles, packets_sent : unsigned(31 downto 0);
signal done : boolean := false;
begin
clk <= not clk after 5 ns when not done else '0';
dut : entity work.usb3_credit_flow_vhdl
generic map (MAX_CREDITS => MAXC)
port map (clk, rst_n, ack_valid, ack_nump, erdy, bmb, pend, send,
link_reset, burst_size, credits, outstanding, may_send,
can_send, binding_limit, ready_announced, flow_blocked,
blocked_cycles, packets_sent, credit_violation);
stim : process
variable errors : natural := 0;
variable n_exh, n_burst_exh : natural := 0;
type lim_count_t is array (flow_limit_t) of natural;
variable n_lim : lim_count_t := (others => 0);
-- INDEPENDENT MODEL: its own credits, outstanding and flags.
variable m_cred, m_out : natural := 0;
variable m_sent : natural := 0;
variable m_ready, m_viol : boolean := false;
variable seed1 : positive := 83; variable seed2 : positive := 29;
variable r1, r2, r3, r4, r5, r6 : real;
function sl (b : boolean) return std_logic is
begin
if b then return '1'; else return '0'; end if;
end function;
procedure chk (c : boolean; m : string) is
begin
if not c then
errors := errors + 1;
if errors <= 25 then report "FAIL: " & m severity warning; end if;
end if;
end procedure;
-- The model computes the minimum by sorting three values rather than by
-- two nested comparisons. Same answer, different route.
function min3 (x, y, z : natural) return natural is
variable lo : natural := x;
begin
if y < lo then lo := y; end if;
if z < lo then lo := z; end if;
return lo;
end function;
procedure check_comb is
variable e_bsz, e_room, e_may : natural;
variable e_lim : flow_limit_t;
begin
if bmb = to_unsigned(255, 8) then e_bsz := 255;
else e_bsz := to_integer(bmb) + 1; end if;
if m_out >= e_bsz then e_room := 0; else e_room := e_bsz - m_out; end if;
e_may := min3(m_cred, e_room, to_integer(pend));
if pend = to_unsigned(0, 8) then e_lim := LIM_PENDING;
elsif m_cred <= e_room and m_cred < to_integer(pend) then
e_lim := LIM_CREDITS;
elsif e_room < to_integer(pend) then e_lim := LIM_BURST;
else e_lim := LIM_NONE; end if;
chk(burst_size = to_unsigned(e_bsz, 8),
"burst_size is bMaxBurst PLUS ONE");
chk(credits = to_unsigned(m_cred, 8), "credits matches the model");
chk(outstanding = to_unsigned(m_out, 8),
"outstanding matches the model");
chk(may_send = to_unsigned(e_may, 8),
"may_send is the MINIMUM of three");
chk((can_send = '1') = (e_may /= 0), "can_send tracks may_send");
chk(binding_limit = e_lim, "binding_limit names the tightest");
chk((ready_announced = '1') = m_ready,
"ready_announced matches the model");
-- ---- SAFETY PROPERTIES, independent of the model ----
-- 1. THE one that matters: never claim to be able to send more than
-- the receiver said it has room for.
chk(may_send <= credits,
"may_send exceeded the credits the receiver advertised");
-- 2. Nor more than the burst allowance permits.
chk(outstanding >= burst_size
or (outstanding + may_send) <= burst_size,
"a burst would exceed bMaxBurst + 1");
-- 3. Nor more packets than actually exist.
chk(may_send <= pend, "may_send exceeded the packets pending");
-- 4. burst_size is never zero.
chk(burst_size /= to_unsigned(0, 8), "burst_size collapsed to zero");
n_lim(e_lim) := n_lim(e_lim) + 1;
end procedure;
procedure step (av : std_logic; nump : natural; er, sd, lr : std_logic) is
variable nc, no, e_bsz, e_room, e_may : natural;
begin
ack_valid <= av; ack_nump <= to_unsigned(nump, 8);
erdy <= er; send <= sd; link_reset <= lr;
wait for 1 ns;
check_comb;
if bmb = to_unsigned(255, 8) then e_bsz := 255;
else e_bsz := to_integer(bmb) + 1; end if;
if m_out >= e_bsz then e_room := 0; else e_room := e_bsz - m_out; end if;
e_may := min3(m_cred, e_room, to_integer(pend));
wait until rising_edge(clk); wait for 1 ns;
if lr = '1' then
m_cred := 0; m_out := 0; m_ready := false;
else
if er = '1' and av = '0' then m_ready := true;
elsif av = '1' then m_ready := false; end if;
nc := m_cred; no := m_out;
if av = '1' then
if nump > MAXC then nc := MAXC; else nc := nump; end if;
no := 0;
end if;
if sd = '1' then
if e_may /= 0 then
if av = '0' then
if nc /= 0 then nc := nc - 1; end if;
no := no + 1;
end if;
m_sent := m_sent + 1;
else
m_viol := true;
end if;
end if;
m_cred := nc; m_out := no;
end if;
wait for 1 ns;
chk(packets_sent = to_unsigned(m_sent, 32),
"packets_sent matches the model");
chk((credit_violation = '1') = m_viol,
"credit_violation matches the model");
end procedure;
procedure hard_reset is
begin
rst_n <= '0'; ack_valid <= '0'; ack_nump <= (others => '0');
erdy <= '0'; send <= '0'; link_reset <= '0';
wait until rising_edge(clk); wait for 1 ns;
wait until rising_edge(clk); wait for 1 ns;
rst_n <= '1'; wait for 1 ns;
m_cred := 0; m_out := 0; m_sent := 0;
m_ready := false; m_viol := false;
end procedure;
begin
hard_reset;
chk(credits = to_unsigned(0, 8), "a link starts with no credits");
chk(can_send = '0', "and can send nothing");
-- ===== A. EXHAUSTIVE over the min-of-three decision =====
-- 16 bMaxBurst values x 16 credit grants x 16 pending counts = 4096.
for b in 0 to 15 loop
for c in 0 to 15 loop
for a in 0 to 15 loop
hard_reset;
bmb <= to_unsigned(b, 8); pend <= to_unsigned(a, 8);
wait for 1 ns;
step('1', c, '0', '0', '0');
n_exh := n_exh + 1;
end loop;
end loop;
end loop;
report "exhaustive min-of-three sweep: " & integer'image(n_exh)
& " of 4096 points verified";
-- ===== B. EXHAUSTIVE over the burst allowance being consumed =====
for b in 0 to 7 loop
for k in 0 to 7 loop
hard_reset;
bmb <= to_unsigned(b, 8); pend <= to_unsigned(15, 8);
wait for 1 ns;
step('1', 31, '0', '0', '0');
for i in 1 to k loop
if can_send = '1' then step('0', 0, '0', '1', '0');
else step('0', 0, '0', '0', '0'); end if;
end loop;
n_burst_exh := n_burst_exh + 1;
end loop;
end loop;
report "exhaustive burst-consumption sweep: "
& integer'image(n_burst_exh) & " of 64 verified";
-- ===== C. directed: the off-by-one, and the three limits =====
hard_reset; bmb <= to_unsigned(0, 8); pend <= to_unsigned(8, 8);
wait for 1 ns; step('1', 31, '0', '0', '0');
chk(burst_size = to_unsigned(1, 8),
"bMaxBurst = 0 means a burst of ONE packet, not zero");
chk(may_send = to_unsigned(1, 8), "so exactly one packet may go");
chk(binding_limit = LIM_BURST, "and the BURST is what binds");
hard_reset; bmb <= to_unsigned(15, 8); pend <= to_unsigned(8, 8);
wait for 1 ns; step('1', 31, '0', '0', '0');
chk(burst_size = to_unsigned(16, 8),
"bMaxBurst = 15 means a burst of SIXTEEN");
chk(may_send = to_unsigned(8, 8),
"so all eight pending packets may go");
chk(binding_limit = LIM_NONE, "and nothing binds");
hard_reset; bmb <= to_unsigned(15, 8); pend <= to_unsigned(8, 8);
wait for 1 ns; step('1', 3, '0', '0', '0');
chk(may_send = to_unsigned(3, 8), "three credits caps it at three");
chk(binding_limit = LIM_CREDITS, "and the CREDITS are what bind");
hard_reset; bmb <= to_unsigned(15, 8); pend <= to_unsigned(0, 8);
wait for 1 ns; step('1', 31, '0', '0', '0');
chk(may_send = to_unsigned(0, 8), "nothing pending, nothing to send");
chk(binding_limit = LIM_PENDING, "and that is a DIFFERENT reason");
chk(flow_blocked = '0', "which is not the same as being blocked");
hard_reset; bmb <= to_unsigned(15, 8); pend <= to_unsigned(8, 8);
wait for 1 ns; step('0', 0, '1', '0', '0');
chk(credits = to_unsigned(0, 8), "an ERDY grants NO credits");
chk(ready_announced = '1', "it announces readiness and nothing else");
chk(can_send = '0', "so nothing may be sent on the strength of it");
step('1', 4, '0', '0', '0');
chk(credits = to_unsigned(4, 8), "the credits come from the ACK");
chk(ready_announced = '0', "and the announcement is retired");
hard_reset; bmb <= to_unsigned(15, 8); pend <= to_unsigned(8, 8);
wait for 1 ns; step('1', 8, '0', '0', '0');
chk(credits = to_unsigned(8, 8), "credits granted");
step('0', 0, '0', '0', '1');
chk(credits = to_unsigned(0, 8),
"a link reset discards them: the receiver's buffer is gone");
-- ===== D. randomised =====
for i in 1 to 40000 loop
uniform(seed1, seed2, r1);
if r1 < 0.015625 then
hard_reset;
uniform(seed1, seed2, r2); uniform(seed1, seed2, r3);
bmb <= to_unsigned(integer(floor(r2*256.0)), 8);
pend <= to_unsigned(integer(floor(r3*32.0)), 8);
wait for 1 ns;
else
uniform(seed1, seed2, r2); uniform(seed1, seed2, r3);
uniform(seed1, seed2, r4); uniform(seed1, seed2, r5);
uniform(seed1, seed2, r6);
if r6 < 0.0625 then
pend <= to_unsigned(integer(floor(r6*32.0*16.0)) mod 32, 8);
wait for 1 ns;
end if;
step(sl(r2 < 0.2), integer(floor(r3*40.0)), sl(r4 < 0.125),
sl(r5 < 0.3333), sl(r6 < 0.0078125));
end if;
end loop;
chk(n_lim(LIM_NONE) > 0 and n_lim(LIM_CREDITS) > 0
and n_lim(LIM_BURST) > 0 and n_lim(LIM_PENDING) > 0,
"the run reached ALL FOUR binding-limit cases");
report "REACH: min3=" & integer'image(n_exh)
& " burst=" & integer'image(n_burst_exh)
& " | binding: none=" & integer'image(n_lim(LIM_NONE))
& " credits=" & integer'image(n_lim(LIM_CREDITS))
& " burst=" & integer'image(n_lim(LIM_BURST))
& " pending=" & integer'image(n_lim(LIM_PENDING));
report "[VHDL] usb3_credit_flow_vhdl: " & integer'image(errors)
& " errors";
if errors = 0 then report "[VHDL] PASS";
else report "[VHDL] FAIL" severity error; end if;
done <= true;
wait;
end process;
end architecture;Run it with:
nvc --std=2008 -a cf_vhdl.vhd cf_vhdl_tb.vhd
nvc --std=2008 -e tb_cf_vhdl
nvc --std=2008 -r tb_cf_vhdl11. Mutation Testing — Across All Three Languages
| Mutation | Verilog | SystemVerilog | VHDL | |
|---|---|---|---|---|
| C1 | bMaxBurst treated as the burst size (no +1) | 46038 | 46038 | 47050 |
| C2 | the burst limit ignored — min(credits, pending) | 3765 | 3765 | 4158 |
| C3 | the data limit ignored — min(credits, burst) | 41723 | 41723 | 41377 |
| C4 | ERDY grants credits | 38457 | 38457 | 43987 |
| C5 | a link reset preserves the credit state | 14794 | 14794 | 13177 |
| C6 | sending does not add to the outstanding count | 20267 | 20267 | 20625 |
| C7 | the binding-limit priority is swapped | 2657 | 2657 | 2783 |
C2 scores lowest of the three min-of-three mutations at 3765, and the reason is reachability rather than importance: it differs from the correct design only when the burst is the tightest limit, which §10 measured at 1608 of the exhaustive points. C3 scores 41 723 because "more data than credits" is the common case.
C7 changes no transfer at all — may_send is identical — and still dies 2657 times, because binding_limit is checked. A mutation that only affects a diagnostic output is only detectable if the diagnostic is checked, which is the argument for §3's output existing.
12. The Mutation VHDL's Semantics Made Equivalent
The first run of this matrix had C4 surviving outright in VHDL:
| Mutation | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| C4 — ERDY grants credits | 38457 | 38457 | 0 |
Zero errors, against a design the other two languages proved the mutation breaks.
The VHDL mutation had been written against the signal:
if erdy = '1' and ack_valid = '0' then
rdy_r <= '1'; cred_r <= to_unsigned(MAX_CREDITS, 8); -- MUT C4
elsif ack_valid = '1' then rdy_r <= '0'; end if;— and the process assigns cred_r again, unconditionally, forty lines later:
cred_r <= nc; out_r <= no;In VHDL the last signal assignment in a process wins. The mutation's write was overwritten before the process ended, so C4 was not a weak mutation — it was genuinely equivalent, and no amount of extra stimulus would have killed it.
The fix was to retarget the mutation at the variable that actually reaches the signal:
# C4 must target the VARIABLE nc, not the signal cred_r: the process
# assigns `cred_r <= nc` unconditionally at its end, and VHDL's
# last-assignment-wins would silently override a mutation written against
# the signal -- making it equivalent rather than merely different.C4 in VHDL went from 0 to 43 987.
13. A UVM Environment for Flow Control
Credit flow is a protocol between two agents, which is what makes it a natural UVM target rather than a directed-test one.
// The receiver is an agent, not a stimulus file. Its credit advertisements
// are a policy, and different policies expose different bugs: a receiver
// that always advertises its maximum never exercises the credit limit at
// all, which is section 10's LIM_CREDITS bin reading zero.
class receiver_agent extends uvm_agent;
`uvm_component_utils(receiver_agent)
rand int unsigned buffer_depth;
rand int unsigned drain_rate;
constraint c_realistic {
buffer_depth inside {[1:31]};
drain_rate inside {[0:4]};
}
endclass
class credit_item extends uvm_sequence_item;
`uvm_object_utils(credit_item)
rand bit [7:0] nump;
rand bit ack, erdy, send, link_reset;
// A receiver advertising zero is the interesting case and the one a
// naive generator produces least often.
constraint c_starve { nump dist { 0 := 3, [1:3] := 5, [4:31] := 4 }; }
constraint c_rare { link_reset dist { 1 := 1, 0 := 200 }; }
endclass
// The adversarial sequence: offer a send in every state, including the
// states where there is nothing to spend it on. A well-behaved layer above
// would never do this -- which is exactly why the DUT must not rely on it.
class overrun_attack_seq extends uvm_sequence #(credit_item);
`uvm_object_utils(overrun_attack_seq)
task body();
credit_item it;
`uvm_do_with(it, { ack == 1; nump == 0; }) // no room at all
repeat (8)
`uvm_do_with(it, { send == 1; ack == 0; }) // try anyway
endtask
endclass
class flow_scoreboard extends uvm_scoreboard;
`uvm_component_utils(flow_scoreboard)
local int unsigned m_credits, m_outstanding;
function void write(flow_txn t);
int unsigned burst_size = t.b_max_burst + 1; // SECTION 4
int unsigned room = (m_outstanding >= burst_size)
? 0 : burst_size - m_outstanding;
// THE property. Not "may_send matched" -- an inequality, because its
// violation is a receive-buffer overflow in real silicon and there is
// no expected value that makes overrunning a credit acceptable.
if (t.may_send > m_credits)
`uvm_fatal("FLOW/OVERRUN", $sformatf(
"claimed %0d packets against %0d credits", t.may_send, m_credits))
if (t.may_send > room)
`uvm_error("FLOW/BURST", "a burst would exceed bMaxBurst + 1")
if (t.may_send > t.packets_pending)
`uvm_error("FLOW/PHANTOM", "claimed more packets than exist")
// ERDY grants nothing. Section 2, and mutation C4.
if (t.erdy && !t.ack && t.credits != m_credits)
`uvm_error("FLOW/ERDY", "an ERDY changed the credit count")
if (t.ack) begin m_credits = t.nump; m_outstanding = 0; end
else if (t.send && t.may_send > 0) begin
m_credits--; m_outstanding++;
end
endfunction
endclass
covergroup flow_cg with function sample(
flow_limit_e lim, int unsigned burst_size, bit erdy_no_ack);
// ALL FOUR must be reached. A run missing LIM_BURST has not tested
// section 4's off-by-one however many credits it varied.
cp_limit : coverpoint lim {
bins none = {LIM_NONE}; bins credits = {LIM_CREDITS};
bins burst = {LIM_BURST}; bins pending = {LIM_PENDING};
}
// bMaxBurst = 0 is the value the off-by-one destroys, and it is the
// most common value in real descriptors.
cp_burst1 : coverpoint burst_size { bins single = {1}; bins multi = {[2:$]}; }
// An ERDY with no ACK behind it -- the case mutation C4 corrupts.
cp_erdy : coverpoint erdy_no_ack { bins announced = {1}; }
x_limit_burst : cross cp_limit, cp_burst1;
endgroup14. Assertions
// THE property: never claim more than the receiver has room for.
property p_within_credits;
@(posedge clk) disable iff (!rst_n)
may_send <= credits;
endproperty
a_within_credits : assert property (p_within_credits)
else $fatal(1, "may_send exceeded the advertised credits");
// Nor more than the burst allowance. Mutation C2.
property p_within_burst;
@(posedge clk) disable iff (!rst_n)
(outstanding < burst_size) |-> (outstanding + may_send <= burst_size);
endproperty
a_within_burst : assert property (p_within_burst);
// Nor more packets than exist. Mutation C3.
property p_within_pending;
@(posedge clk) disable iff (!rst_n)
may_send <= packets_pending;
endproperty
a_within_pending : assert property (p_within_pending);
// An ERDY with no ACK changes no credits. Mutation C4, stated as the
// absence of an effect rather than the presence of one.
property p_erdy_grants_nothing;
@(posedge clk) disable iff (!rst_n)
(erdy && !ack_valid && !link_reset) |=> $stable(credits) || $past(send);
endproperty
a_erdy_grants_nothing : assert property (p_erdy_grants_nothing)
else $error("an ERDY changed the credit count");
// burst_size is never zero. Mutation C1's consequence.
property p_burst_never_zero;
@(posedge clk) disable iff (!rst_n) burst_size != '0;
endproperty
a_burst_never_zero : assert property (p_burst_never_zero);The first three are the three limits as three separate assertions, deliberately not combined into one. A single may_send == min(a,b,c) would pass a design that got the minimum right by accident from two wrong inputs; three inequalities each fail independently.
These were written but not simulated; Icarus supports no concurrent assertions.
15. Debugging: the Link That Runs at a Third of Its Rate
The report: a SuperSpeed device transfers at roughly a third of the rate the same silicon achieves on a reference host. No errors, no retries, no resets — it is simply slower.
The procedure:
1. Establish which limit is binding, and recognise that this is the question. Throughput alone cannot distinguish a small receive buffer from a small burst from an endpoint with nothing to say — and those have no fixes in common.
2. Read bMaxBurst from the endpoint companion descriptor, and add one (§4). A device declaring 0 gets bursts of one packet, which on a 5 Gb/s link with per-packet overhead is roughly the throughput being reported.
3. Check whether the host is advertising credits at all. A receiver that always returns NumP = 1 serialises the link no matter how large the burst allowance is — the sender may never have more than one packet outstanding.
4. Distinguish "blocked" from "idle". An endpoint reporting LIM_PENDING is working correctly and has nothing to send; the bottleneck is upstream of USB entirely. This is the case most often misdiagnosed, because the throughput graph looks identical.
5. Look for ERDY storms. A device that repeatedly announces readiness and is not scheduled is telling you its ERDYs are not arriving, or that the host is not acting on them. ready_announced staying high is that condition, and it is invisible from a bandwidth measurement.
16. Common Misconceptions
"USB 3 devices can initiate transfers." They cannot (§1) — 19.5's wakeup remains the only exception. They may announce readiness, which is different.
"ERDY means the device is sending." It means resume scheduling me (§2). The permission comes from credits in an ACK. Mutation C4, 38 457 errors.
"bMaxBurst is the burst size." It is the burst size minus one (§4) — 0 means one packet. Mutation C1, 46 038 errors.
"Credits are the only limit." Three limits, smallest wins (§3). Mutations C2 and C3.
"Credits survive a link reset." They describe a buffer that has just been reset (§6). Mutation C5, 14 794 errors.
"Knowing the throughput tells you what to fix." It tells you nothing — the three limits produce identical throughput graphs and have no fixes in common (§3, §15).
"A mutation that changes no data transfer is not worth having." C7 changes may_send not at all and dies 2657 times, because the diagnostic it corrupts is checked (§11).
17. Exercises
1. §12 found a mutation VHDL's last-assignment-wins made equivalent. Audit the remaining VHDL designs in Modules 18–20 for signals assigned more than once in one process, and say which mutations against them would be neutralised.
2. An endpoint declares bMaxBurst = 15 and the receiver advertises NumP = 2. Compute the maximum sustained throughput as a fraction of the burst-limited rate, and say which limit binding_limit reports.
3. Write the SVA property that catches C6 — sending without incrementing outstanding — without referring to outstanding.
4. §3 argues the three limits must not be conflated. Construct a workload for which min(credits, pending) and the correct min-of-three agree on every cycle, and say what that implies about C2's count.
5. usb_ss_max_streams reads five bits of bmAttributes and returns 1 << n. Determine the largest number of streams an endpoint can declare, and what happens to this design if streams are added.
6. A device sends ERDY and the host never returns. Determine what the design reports, what a USB 2 device would have done in the same situation, and which is easier to diagnose.
18. Summary
USB 3 kept the single master and deleted the polling (§1). A USB 2 endpoint with nothing to say costs a transaction per poll; a USB 3 endpoint costs nothing until it announces readiness.
ERDY is not a credit (§2). It says resume asking me; the permission to transmit is NumP, carried in an ACK. Mutation C4, 38 457 errors.
Three limits bound a sender and the smallest wins (§3): credits, burst size, and data on hand. Conflating any two works until the third becomes binding — C2 at 3765 and C3 at 41 723 — and which limit binds is the more useful output, because the three have no fixes in common.
bMaxBurst is burst size minus one (§4). Zero means one packet, and a design reading it literally stalls every endpoint that declared zero. Mutation C1, 46 038 errors.
All three HDL implementations were simulated (§19) and seven mutations died in all three (§11), with the decision verified exhaustively over 4096 points plus 64 burst-consumption walks (§10), reaching all four binding cases thousands of times each.
And C4 survived outright in VHDL on the first run (§12) — zero errors against 38 457 elsewhere. The mutation had been written against a signal that the process assigns again, unconditionally, later; VHDL's last-assignment-wins overwrote it, making it genuinely equivalent rather than weak. Retargeting it at the variable that actually reaches the signal took it to 43 987.
That is the fourth distinct way in three modules that a mutation has failed to mean the same thing in all three languages — after a duplicate, a ternary with identical branches, and a duplicated design condition. The detector has been the same every time: a column badly out of line with the other two.
19. Tooling, Honestly
| Language | Design | Testbench | Analysed / compiled | Simulated | Mutations |
|---|---|---|---|---|---|
| Verilog-2005 | usb3_credit_flow | cf_v_tb.v | ✅ Icarus -g2005 | ✅ 0 errors, 4096 + 64 | ✅ all seven |
| SystemVerilog | usb3_credit_flow_sv | cf_sv_tb.sv | ✅ Icarus -g2012 | ✅ 0 errors, 4096 + 64 | ✅ all seven |
| VHDL-2008 | usb3_credit_flow_vhdl | cf_vhdl_tb.vhd | ✅ nvc 1.23.0 | ✅ 0 errors, 4096 + 64 | ✅ all seven |
| UVM (§13) | — | — | ❌ no UVM-capable simulator here | ❌ | — |
| SVA (§14) | — | — | ❌ unsupported by Icarus | ❌ | — |
binding_limit was added to all three designs at once, not to SystemVerilog and VHDL first. Chapters 19.2 §11 and 19.3 §13 each cost a round of re-measurement to discover that one design exposing less makes the comparison measure the benches instead.
VHDL's randomised tail differs (pending reached 1457 times against 1120) because the three benches draw from different generators. The 4096 exhaustive points and 64 burst walks are identical by construction.
20. What Comes Next
This chapter described a link without saying anything about the wires it runs on — and those are not the wires Modules 1–19 have been about.
Chapter 20.2 — Dual-Bus Architecture is about what is physically in a USB 3 cable, which is two complete buses: the USB 2 D+/D− pair, unchanged and still present, and two additional differential pairs carrying SuperSpeed in each direction.
They coexist physically and barely interact logically, and the rule that connects them is sharper than it looks: a device operates at SuperSpeed or at USB 2, never both — so the interesting hardware is not either bus, it is the decision between them, and what happens when SuperSpeed training fails.
Browse the full path on the USB tutorials index.
Continue learning
Related tutorials
- Related topic
Dual-Bus Architecture
A USB 3 cable carries two complete buses — physically parallel, logically exclusive — and the presence pull-up deliberately sits outside that exclusion.
- Related topic
USB 3.x vs USB 2.0 Differences
A SuperSpeed-capable device behind a USB 2 hub is a USB 2 device: capability is a property of the link that trained, never of the descriptor the device published.
- Related topic
Link Training
A SuperSpeed link must train itself before carrying anything — and cannot use the link to do it, so the earliest signalling runs with the high-speed transmitter off.
- Related topic
USB 3.x Packets
Every SuperSpeed packet carries two independent CRCs, and that is not redundancy: a corrupt header is a link-layer problem while corrupt data is a protocol-layer one, so the header CRC must gate the type decode.
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.
