USB · Module 20
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.
Chapter 20.3 got the link trained. Symbols are locked, the clock is recovered, both directions agree. The link can now carry packets.
This chapter is about what those packets are — and about one structural decision inside them that almost every first-draft receiver gets wrong.
1. Four Packet Types, and Only One Carries Data
SuperSpeed defines exactly four kinds of packet. The list is short and worth memorising, because every SuperSpeed trace you will ever read is made of these and nothing else.
| Type | Name | Carries data? | Consumed by |
|---|---|---|---|
0x00 | LMP — Link Management Packet | no | the link layer at the other end of this link |
0x04 | TP — Transaction Packet | no | the protocol layer — an endpoint |
0x08 | DP — Data Packet | yes | the protocol layer — an endpoint |
0x0C | ITP — Isochronous Timestamp Packet | no (a timestamp) | every device, broadcast |
Three of the four carry no payload at all. ACK, NRDY, ERDY, STALL, the credit updates from Chapter 20.1 — all of those are Transaction Packets, distinguished by a subtype field inside the header rather than by being different kinds of packet.
2. Every Packet Begins With a 14-Byte Header
Whatever the type, the packet opens the same way:
[ HEADER : 14 bytes ][ CRC-16 ][ PAYLOAD ][ CRC-32 ]
\_______ one CRC domain ______/ \__ a second CRC domain __/
type, route, sequence only a Data Packet
number, subtype, flags has this part at allFourteen bytes of header, then two bytes of CRC-16 covering those fourteen bytes and nothing else. A Data Packet then appends its payload and a separate CRC-32 covering the payload and nothing else.
A SuperSpeed packet, and its two independent CRC domains
3. Two CRCs Is Not Redundancy
The obvious reading of two CRCs is "belt and braces" — extra protection for the same data. It is not that at all. The two CRCs cover disjoint byte ranges, and a failure in each one means something categorically different.
| Header CRC fails | Payload CRC fails | |
|---|---|---|
| What is damaged | the packet's identity | the packet's contents |
| Do we know the type? | no | yes |
| Do we know the endpoint? | no | yes |
| Do we know the sequence number? | no | yes |
| Who can act on it | only the link layer | the protocol layer |
| Recovery | resend the whole packet | resend this transfer's data |
| Retry is likely to help | yes — transient bit error | yes |
Read the "no" column again. When the header CRC fails, there is nothing to report upward. You cannot tell the protocol layer "endpoint 3's data was corrupt", because the field that said endpoint 3 is inside the bytes the CRC just declared untrustworthy. The receiver's honest statement is "a packet arrived and I cannot say what it was."
That is a link-layer event, and the link layer's answer is LBAD — tell the far end the packet was bad and let it resend the whole thing. Nothing above the link layer ever hears about it.
When only the payload CRC fails, the situation is completely different and much better. The header was intact, so the receiver knows the type, the endpoint, the direction and the sequence number. Exactly one transfer's data is bad, and the protocol layer can ask that one endpoint for that one packet again.
4. The Consequence: The CRC Is Checked Before the Type Is Read
Here is the decision the whole chapter turns on.
A receiver has the header bytes and the CRC result available at the same moment. It can write its decode either way round:
// WRONG -- decode first, validate afterwards
always_comb begin
case (hdr_type) // reads the type field unconditionally
T_LMP: pkt_type = PKT_LMP;
T_TP: pkt_type = PKT_TP;
...
endcase
if (!hdr_crc_ok) flag_an_error(); // ... and *then* notices it was garbage
endversus:
// RIGHT -- the CRC gates the decode
always_comb begin
if (!pkt_valid) pkt_type = PKT_NONE;
else if (!hdr_crc_ok) pkt_type = PKT_UNKNOWN; // stop here. Read nothing.
else case (hdr_type)
...
endcase
endThe first version works perfectly until the first corrupted header, and then routes a packet on the strength of bits that are known to be wrong.
This is not a theoretical worry. The header's routing field is what selects a device behind a hub, and its endpoint field selects a buffer inside that device. A single flipped bit in a corrupted header can name a different, innocent endpoint — one whose data stream now receives a fragment belonging to somebody else. The CRC caught the corruption. The decode order threw the catch away.
So the design in this chapter reports a distinct value, PKT_UNKNOWN, meaning "a packet arrived; its type is not knowable." That value is not a diagnostic convenience. It is the only truthful answer, and making it a first-class output is what stops the rest of the receiver from inventing one.
5. Reserved Types Are a Third Case, Not a Fifth Type
The header's Type field is five bits — 32 encodings, of which four are defined. The other 28 are reserved, and a packet carrying one of them is a third distinct situation:
| header CRC | type field | what to do | |
|---|---|---|---|
| Known type | good | one of the four | decode and route it |
| Reserved type | good | one of the other 28 | discard, no link retry |
| Corrupt header | bad | unreadable | discard, link retry |
The middle row is the one that gets conflated with the bottom row, and the difference matters: a reserved type must not trigger a link retry. The CRC passed, so the bytes arrived exactly as the transmitter sent them. Asking for them again returns the same reserved encoding, and the link spends its error budget on a packet that was never damaged. What a reserved type actually means is either a newer specification revision or a broken transmitter — and in both cases the correct behaviour is to drop the packet silently and count it.
This is forward compatibility working as designed: a device built to an older revision meets a packet type it has never heard of, ignores it, and keeps the link up.
The decode, in the only order that is safe
6. Where Each Packet Goes
Putting sections 1, 3 and 5 together gives the complete routing table the decoder implements:
| Decoded as | Route | Acknowledged? | Notes |
|---|---|---|---|
PKT_NONE | none | — | no packet this cycle |
PKT_LMP | link | link-level | never reaches an endpoint |
PKT_TP | protocol | yes | ACK, NRDY, ERDY, credits |
PKT_DP | protocol | yes | routed even if its payload CRC failed |
PKT_ITP | protocol | no | broadcast timestamp |
PKT_RSVD | discard | — | no link retry |
PKT_UNKNOWN | discard | — | link retry |
Two rows there are easy to get wrong.
PKT_DP is routed even when its payload CRC failed. The instinct is to discard a packet with a bad CRC, but discarding it destroys the only copy of the information the protocol layer needs to recover: which endpoint and which sequence number to ask again. The header was good. Use it. The packet is delivered with payload_error asserted alongside it — delivered and flagged, not dropped.
PKT_ITP is never acknowledged. An Isochronous Timestamp Packet is broadcast to everything on the bus and carries the host's notion of time. Acknowledging it would be pointless: by the time an ACK travelled back, the timestamp it confirmed would already be stale, and with many devices on the bus the ACKs would flood the very bus whose timing the ITP exists to distribute. It is fire-and-forget by design.
7. What We Are Building
usb3_packet_decode
inputs outputs
------ -------
pkt_valid pkt_type (7 outcomes)
hdr_type [4:0] route (4 outcomes)
hdr_crc_ok hdr_error
payload_present payload_error
payload_crc_ok link_retry_req
protocol_nak_req
ack_expected
hdr_errors [31:0]
payload_errors [31:0]
reserved_types [31:0]
delivered [31:0]The whole decode is combinational; only the four diagnostic counters are registered. That is deliberate — a receiver that needs a pipeline stage to decide where a packet goes has to buffer the packet while it decides, and the header is designed to be classifiable in the cycle it arrives.
The decision surface is small enough to enumerate completely:
2 (pkt_valid) x 32 (hdr_type) x 2 (hdr_crc_ok)
x 2 (payload_present) x 2 (payload_crc_ok)
= 512 points, ALL of them reachable512 points is the entire input space, including all 28 reserved type encodings individually rather than one representative sample of them. The testbenches sweep every point on all three implementations.
8. Verilog-2005 Implementation
// usb3_packet_decode -- four packet types, two independent CRCs, and why a
// corrupt header is a different kind of problem from corrupt data.
//
// A SuperSpeed packet begins with a 14-byte HEADER carrying its type and its
// routing, followed by a 2-byte CRC over that header alone. A Data Packet
// then carries a PAYLOAD with its own, separate CRC.
//
// [ header : 14 bytes ][ header CRC-16 ][ payload ][ payload CRC-32 ]
// \______ protected together ______/ \___ protected separately __/
//
// THE STRUCTURAL DECISION
//
// Those two CRCs are not redundancy. They protect two things that fail in
// different ways and are recovered by different layers:
//
// HEADER CRC BAD The type and routing are UNKNOWN. The link layer
// cannot even tell what kind of packet this was, let
// alone which endpoint it belonged to. Nothing above
// the link layer can be told about it, because there is
// nothing to tell them. Recovery is a LINK-level retry.
//
// PAYLOAD CRC BAD The header was good, so the type, the endpoint and
// the sequence number are all known and trustworthy.
// Exactly one endpoint's data is bad, and the protocol
// layer can ask for that one packet again.
//
// THE CONSEQUENCE, AND IT IS THE POINT OF THE MODULE
//
// When the header CRC fails, the type field MUST NOT BE DECODED. The bits
// are there and they will parse into something -- but they are bits a CRC
// has just declared untrustworthy, and acting on them routes a packet
// somewhere on the strength of data known to be corrupt.
//
// A design that decodes the type first and checks the CRC afterwards works
// perfectly until the first corrupted header, and then delivers garbage to
// whichever endpoint the damaged bits happened to name.
//
// RESERVED TYPES ARE NOT A FIFTH KIND
//
// The 5-bit type field has 32 encodings and four are defined. The other 28
// are reserved, and a packet carrying one has a VALID CRC and an
// UNINTERPRETABLE type -- which is a third case, distinct from both a CRC
// failure and a known type, and it is discarded without a link retry
// because retrying will produce the same reserved value again.
module usb3_packet_decode (
input wire clk,
input wire rst_n,
input wire pkt_valid,
input wire [4:0] hdr_type, // the header's Type field
input wire hdr_crc_ok, // the 16-bit header CRC checked out
input wire payload_present, // a Data Packet carries one
input wire payload_crc_ok, // the 32-bit payload CRC checked out
output wire [2:0] pkt_type,
output wire [1:0] route,
output wire hdr_error,
output wire payload_error,
output wire link_retry_req, // LINK layer: resend the whole packet
output wire protocol_nak_req,// PROTOCOL layer: resend this data
output wire ack_expected, // ITP is never acknowledged
output reg [31:0] hdr_errors,
output reg [31:0] payload_errors,
output reg [31:0] reserved_types,
output reg [31:0] delivered
);
// The four defined Type encodings. Everything else is reserved.
localparam [4:0] T_LMP = 5'h00, // Link Management -- stays at the link
T_TP = 5'h04, // Transaction Packet
T_DP = 5'h08, // Data Packet -- the only one with a payload
T_ITP = 5'h0C; // Isochronous Timestamp -- broadcast
localparam [2:0] P_NONE = 3'd0, // no packet this cycle
P_LMP = 3'd1,
P_TP = 3'd2,
P_DP = 3'd3,
P_ITP = 3'd4,
P_RSVD = 3'd5, // valid CRC, undefined type
P_UNKNOWN = 3'd6;// the CRC failed: the type is NOT known
localparam [1:0] R_NONE = 2'd0,
R_LINK = 2'd1, // the link layer consumes it
R_PROTOCOL = 2'd2, // it goes up to the endpoint
R_DISCARD = 2'd3;
// THE DECODE, AND ITS ORDER. The CRC is consulted BEFORE the type field,
// not after. With a bad header CRC the type is reported as P_UNKNOWN --
// not as whatever the corrupt bits happen to spell.
assign pkt_type = !pkt_valid ? P_NONE
: !hdr_crc_ok ? P_UNKNOWN
: (hdr_type == T_LMP) ? P_LMP
: (hdr_type == T_TP) ? P_TP
: (hdr_type == T_DP) ? P_DP
: (hdr_type == T_ITP) ? P_ITP
: P_RSVD;
// A Data Packet whose payload failed its own CRC still has a GOOD header,
// so it is still routed: the protocol layer needs to know which endpoint
// to ask again, and that information is in the header it can trust.
assign route = (pkt_type == P_NONE) ? R_NONE
: (pkt_type == P_UNKNOWN) ? R_DISCARD
: (pkt_type == P_RSVD) ? R_DISCARD
: (pkt_type == P_LMP) ? R_LINK
: R_PROTOCOL;
assign hdr_error = pkt_valid && !hdr_crc_ok;
// A payload error is only meaningful on a Data Packet with a good header.
// Reporting one after a header failure would be claiming to know that the
// payload was bad -- which requires knowing there WAS a payload, which
// requires the header.
assign payload_error = pkt_valid && hdr_crc_ok
&& (pkt_type == P_DP) && payload_present
&& !payload_crc_ok;
// TWO DIFFERENT RECOVERIES, and they are mutually exclusive by
// construction: one of them needs a header the other one does not have.
assign link_retry_req = hdr_error;
assign protocol_nak_req = payload_error;
// An Isochronous Timestamp Packet is broadcast and never acknowledged --
// an ACK would be pointless for a timestamp, which is stale the moment it
// is questioned. LMPs are link-level and acknowledged differently.
assign ack_expected = (pkt_type == P_TP) || (pkt_type == P_DP);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
hdr_errors <= 32'd0;
payload_errors <= 32'd0;
reserved_types <= 32'd0;
delivered <= 32'd0;
end else if (pkt_valid) begin
if (hdr_error) hdr_errors <= hdr_errors + 32'd1;
if (payload_error) payload_errors <= payload_errors + 32'd1;
if (pkt_type == P_RSVD) reserved_types <= reserved_types + 32'd1;
if (route != R_DISCARD && route != R_NONE)
delivered <= delivered + 32'd1;
end
end
endmoduleThree things in that listing are worth pausing on.
The order of the ternary chain is the design. !hdr_crc_ok ? P_UNKNOWN sits above every comparison against hdr_type, so the type field is not read at all on a CRC failure. Swap those two lines and the module still compiles, still passes every directed test that uses good headers, and is wrong.
route is computed from pkt_type, not from the raw inputs. That is not a stylistic choice. Deriving the route from hdr_type directly would reintroduce exactly the bug the decode order was written to avoid — there would be a second, independent reader of the untrusted field. By making pkt_type the single interpretation of the header, every consumer inherits the CRC gate for free.
payload_error carries its own hdr_crc_ok term even though pkt_type == P_DP already implies the CRC passed. The redundancy is intentional: it makes the property "a payload error is never claimed without a trustworthy header" true locally, readable in the one expression, rather than true only as a consequence of how pkt_type happens to be computed three assignments earlier.
9. SystemVerilog Implementation
The SystemVerilog build names the outcomes with enumerations. That is not cosmetic here: PKT_UNKNOWN becomes a value a downstream case statement must handle, and a receiver stage that forgets the corrupt-header case gets a lint warning instead of silently falling through a default.
// usb3_packet_decode -- four packet types, two independent CRCs, and why a
// corrupt header is a different kind of problem from corrupt data.
//
// A SuperSpeed packet begins with a 14-byte HEADER carrying its type and its
// routing, followed by a 2-byte CRC over that header alone. A Data Packet
// then carries a PAYLOAD with its own, separate CRC.
//
// [ header : 14 bytes ][ header CRC-16 ][ payload ][ payload CRC-32 ]
// \______ protected together ______/ \___ protected separately __/
//
// The SystemVerilog build names the two classifications with enums, which is
// what makes the central rule visible in the waveform viewer: when the header
// CRC fails the type reads PKT_UNKNOWN, not "the type the corrupt bits spell".
package usb3_packet_pkg;
// The four defined Type encodings. Everything else is reserved.
typedef enum logic [4:0] {
T_LMP = 5'h00, // Link Management -- stays at the link layer
T_TP = 5'h04, // Transaction Packet
T_DP = 5'h08, // Data Packet -- the only one with a payload
T_ITP = 5'h0C // Isochronous Timestamp -- broadcast, never acknowledged
} hdr_type_e;
typedef enum logic [2:0] {
PKT_NONE = 3'd0, // no packet this cycle
PKT_LMP = 3'd1,
PKT_TP = 3'd2,
PKT_DP = 3'd3,
PKT_ITP = 3'd4,
PKT_RSVD = 3'd5, // valid CRC, undefined type encoding
PKT_UNKNOWN = 3'd6 // the header CRC FAILED: the type is not known
} pkt_type_e;
typedef enum logic [1:0] {
ROUTE_NONE = 2'd0,
ROUTE_LINK = 2'd1, // the link layer consumes it
ROUTE_PROTOCOL = 2'd2, // it goes up to the endpoint
ROUTE_DISCARD = 2'd3
} route_e;
endpackage
// THE STRUCTURAL DECISION
//
// The two CRCs are not redundancy. They protect two things that fail in
// different ways and are recovered by different layers:
//
// HEADER CRC BAD The type and routing are UNKNOWN. The link layer cannot
// tell what kind of packet this was, let alone which
// endpoint it belonged to. Nothing above the link layer
// can be told about it, because there is nothing to tell
// them. Recovery is a LINK-level retry.
//
// PAYLOAD CRC BAD The header was good, so the type, the endpoint and the
// sequence number are all known and trustworthy. Exactly
// one endpoint's data is bad and the protocol layer can
// ask for that one packet again.
//
// THE CONSEQUENCE, AND IT IS THE POINT OF THE MODULE
//
// When the header CRC fails, the type field MUST NOT BE DECODED. The bits are
// there and they will parse into something -- but they are bits a CRC has
// just declared untrustworthy, and acting on them routes a packet somewhere
// on the strength of data known to be corrupt.
//
// RESERVED TYPES ARE NOT A FIFTH KIND
//
// The 5-bit type field has 32 encodings and four are defined. The other 28 are
// reserved, and a packet carrying one has a VALID CRC and an UNINTERPRETABLE
// type -- a third case, distinct from both a CRC failure and a known type. It
// is discarded WITHOUT a link retry, because retrying will produce the same
// reserved value again.
module usb3_packet_decode
import usb3_packet_pkg::*;
(
input logic clk,
input logic rst_n,
input logic pkt_valid,
input logic [4:0] hdr_type, // the header's Type field
input logic hdr_crc_ok, // the 16-bit header CRC checked out
input logic payload_present, // a Data Packet carries one
input logic payload_crc_ok, // the 32-bit payload CRC checked out
output pkt_type_e pkt_type,
output route_e route,
output logic hdr_error,
output logic payload_error,
output logic link_retry_req, // LINK layer: resend the whole packet
output logic protocol_nak_req,// PROTOCOL layer: resend this data
output logic ack_expected, // ITP is never acknowledged
output logic [31:0] hdr_errors,
output logic [31:0] payload_errors,
output logic [31:0] reserved_types,
output logic [31:0] delivered
);
pkt_type_e pkt_type_c;
route_e route_c;
// THE DECODE, AND ITS ORDER. The CRC is consulted BEFORE the type field,
// never after. With a bad header CRC the type is PKT_UNKNOWN.
always_comb begin
if (!pkt_valid) pkt_type_c = PKT_NONE;
else if (!hdr_crc_ok) pkt_type_c = PKT_UNKNOWN;
else begin
case (hdr_type)
T_LMP: pkt_type_c = PKT_LMP;
T_TP: pkt_type_c = PKT_TP;
T_DP: pkt_type_c = PKT_DP;
T_ITP: pkt_type_c = PKT_ITP;
default: pkt_type_c = PKT_RSVD;
endcase
end
end
// A Data Packet whose payload failed its own CRC still has a GOOD header,
// so it is still routed: the protocol layer needs to know which endpoint to
// ask again, and that information is in the header it can trust.
always_comb begin
case (pkt_type_c)
PKT_NONE: route_c = ROUTE_NONE;
PKT_UNKNOWN, PKT_RSVD: route_c = ROUTE_DISCARD;
PKT_LMP: route_c = ROUTE_LINK;
default: route_c = ROUTE_PROTOCOL;
endcase
end
assign pkt_type = pkt_type_c;
assign route = route_c;
assign hdr_error = pkt_valid && !hdr_crc_ok;
// A payload error is only meaningful on a Data Packet with a good header.
// Reporting one after a header failure would be claiming to know that the
// payload was bad -- which requires knowing there WAS a payload, which
// requires the header.
assign payload_error = pkt_valid && hdr_crc_ok
&& (pkt_type_c == PKT_DP) && payload_present
&& !payload_crc_ok;
// TWO DIFFERENT RECOVERIES, mutually exclusive by construction: one of them
// needs a header the other one does not have.
assign link_retry_req = hdr_error;
assign protocol_nak_req = payload_error;
// An ITP is broadcast and never acknowledged -- an ACK for a timestamp is
// pointless, since it is stale the moment it is questioned. LMPs are
// link-level and acknowledged differently.
assign ack_expected = (pkt_type_c == PKT_TP) || (pkt_type_c == PKT_DP);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
hdr_errors <= '0;
payload_errors <= '0;
reserved_types <= '0;
delivered <= '0;
end else if (pkt_valid) begin
if (hdr_error) hdr_errors <= hdr_errors + 1;
if (payload_error) payload_errors <= payload_errors + 1;
if (pkt_type_c == PKT_RSVD) reserved_types <= reserved_types + 1;
if (route_c != ROUTE_DISCARD && route_c != ROUTE_NONE)
delivered <= delivered + 1;
end
end
endmodule10. VHDL-2008 Implementation
VHDL takes the enumeration idea furthest. pkt_type_t is a genuine enumerated type, not an encoding, so PKT_UNKNOWN cannot be confused with an integer and a case over it must be exhaustive or the design will not analyse.
-- usb3_packet_decode -- four packet types, two independent CRCs, and why a
-- corrupt header is a different kind of problem from corrupt data.
--
-- A SuperSpeed packet begins with a 14-byte HEADER carrying its type and its
-- routing, followed by a 2-byte CRC over that header alone. A Data Packet
-- then carries a PAYLOAD with its own, separate CRC.
--
-- [ header : 14 bytes ][ header CRC-16 ][ payload ][ payload CRC-32 ]
-- \______ protected together ______/ \___ protected separately __/
--
-- VHDL's strong enumeration types make the central rule structural rather
-- than conventional: PKT_UNKNOWN is a distinct value of pkt_type_t, so a
-- downstream case statement that forgets to handle "the header was corrupt"
-- fails to elaborate rather than falling through to a default.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package usb3_packet_pkg is
-- Seven outcomes, and the last two are the interesting ones: PKT_RSVD is a
-- GOOD header carrying an undefined encoding, PKT_UNKNOWN is a header whose
-- CRC failed so no encoding can be read from it at all.
type pkt_type_t is (PKT_NONE, PKT_LMP, PKT_TP, PKT_DP, PKT_ITP,
PKT_RSVD, PKT_UNKNOWN);
type route_t is (ROUTE_NONE, ROUTE_LINK, ROUTE_PROTOCOL, ROUTE_DISCARD);
function pkt_code (p : pkt_type_t) return std_logic_vector;
function route_code(r : route_t) return std_logic_vector;
end package;
package body usb3_packet_pkg is
function pkt_code (p : pkt_type_t) return std_logic_vector is
begin
return std_logic_vector(to_unsigned(pkt_type_t'pos(p), 3));
end function;
function route_code (r : route_t) return std_logic_vector is
begin
return std_logic_vector(to_unsigned(route_t'pos(r), 2));
end function;
end package body;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb3_packet_pkg.all;
-- THE STRUCTURAL DECISION
--
-- The two CRCs are not redundancy. They protect two things that fail in
-- different ways and are recovered by different layers:
--
-- HEADER CRC BAD The type and routing are UNKNOWN. The link layer cannot
-- tell what kind of packet this was, let alone which
-- endpoint it belonged to. Recovery is a LINK-level retry.
--
-- PAYLOAD CRC BAD The header was good, so the type, the endpoint and the
-- sequence number are all trustworthy. Exactly one
-- endpoint's data is bad and the protocol layer can ask
-- for that one packet again.
--
-- THE CONSEQUENCE: when the header CRC fails, the type field MUST NOT BE
-- DECODED. The bits are there and they will parse into something -- but they
-- are bits a CRC has just declared untrustworthy.
--
-- RESERVED TYPES ARE NOT A FIFTH KIND. Four of the 32 encodings are defined;
-- a packet carrying one of the other 28 has a VALID CRC and an
-- UNINTERPRETABLE type, and is discarded WITHOUT a link retry, because
-- retrying will produce the same reserved value again.
entity usb3_packet_decode is
port (
clk : in std_logic;
rst_n : in std_logic;
pkt_valid : in std_logic;
hdr_type : in std_logic_vector(4 downto 0);
hdr_crc_ok : in std_logic;
payload_present : in std_logic;
payload_crc_ok : in std_logic;
pkt_type : out std_logic_vector(2 downto 0);
route : out std_logic_vector(1 downto 0);
hdr_error : out std_logic;
payload_error : out std_logic;
link_retry_req : out std_logic;
protocol_nak_req : out std_logic;
ack_expected : out std_logic;
hdr_errors : out std_logic_vector(31 downto 0);
payload_errors : out std_logic_vector(31 downto 0);
reserved_types : out std_logic_vector(31 downto 0);
delivered : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of usb3_packet_decode is
-- The four defined Type encodings. Everything else is reserved.
constant T_LMP : std_logic_vector(4 downto 0) := "00000"; -- 0x00
constant T_TP : std_logic_vector(4 downto 0) := "00100"; -- 0x04
constant T_DP : std_logic_vector(4 downto 0) := "01000"; -- 0x08
constant T_ITP : std_logic_vector(4 downto 0) := "01100"; -- 0x0C
signal ptype : pkt_type_t;
signal rt : route_t;
signal he : std_logic;
signal pe : std_logic;
signal he_r, pe_r, rs_r, dl_r : unsigned(31 downto 0) := (others => '0');
begin
-- THE DECODE, AND ITS ORDER. The CRC is consulted BEFORE the type field,
-- never after. With a bad header CRC the type is PKT_UNKNOWN.
decode : process (pkt_valid, hdr_crc_ok, hdr_type)
begin
if pkt_valid = '0' then
ptype <= PKT_NONE;
elsif hdr_crc_ok = '0' then
ptype <= PKT_UNKNOWN;
else
case hdr_type is
when T_LMP => ptype <= PKT_LMP;
when T_TP => ptype <= PKT_TP;
when T_DP => ptype <= PKT_DP;
when T_ITP => ptype <= PKT_ITP;
when others => ptype <= PKT_RSVD;
end case;
end if;
end process;
-- A Data Packet whose payload failed its own CRC still has a GOOD header,
-- so it is still routed: the protocol layer needs to know which endpoint to
-- ask again, and that is in the header it can trust.
routing : process (ptype)
begin
case ptype is
when PKT_NONE => rt <= ROUTE_NONE;
when PKT_UNKNOWN | PKT_RSVD => rt <= ROUTE_DISCARD;
when PKT_LMP => rt <= ROUTE_LINK;
when others => rt <= ROUTE_PROTOCOL;
end case;
end process;
he <= '1' when (pkt_valid = '1' and hdr_crc_ok = '0') else '0';
-- A payload error is only meaningful on a Data Packet with a good header.
-- Claiming one after a header failure would assert knowledge that there WAS
-- a payload -- which requires the header.
pe <= '1' when (pkt_valid = '1' and hdr_crc_ok = '1' and ptype = PKT_DP
and payload_present = '1' and payload_crc_ok = '0')
else '0';
pkt_type <= pkt_code(ptype);
route <= route_code(rt);
hdr_error <= he;
payload_error <= pe;
-- TWO DIFFERENT RECOVERIES, mutually exclusive by construction: one needs a
-- header the other one does not have.
link_retry_req <= he;
protocol_nak_req <= pe;
-- An ITP is broadcast and never acknowledged -- an ACK for a timestamp is
-- pointless, since it is stale the moment it is questioned.
ack_expected <= '1' when (ptype = PKT_TP or ptype = PKT_DP) else '0';
counters : process (clk, rst_n)
begin
if rst_n = '0' then
he_r <= (others => '0');
pe_r <= (others => '0');
rs_r <= (others => '0');
dl_r <= (others => '0');
elsif rising_edge(clk) then
if pkt_valid = '1' then
if he = '1' then
he_r <= he_r + 1;
end if;
if pe = '1' then
pe_r <= pe_r + 1;
end if;
if ptype = PKT_RSVD then
rs_r <= rs_r + 1;
end if;
if rt /= ROUTE_DISCARD and rt /= ROUTE_NONE then
dl_r <= dl_r + 1;
end if;
end if;
end if;
end process;
hdr_errors <= std_logic_vector(he_r);
payload_errors <= std_logic_vector(pe_r);
reserved_types <= std_logic_vector(rs_r);
delivered <= std_logic_vector(dl_r);
end architecture;The pkt_code and route_code functions exist only to present the enumerated values on std_logic_vector ports, so that the three implementations have identical port maps and can be compared observable-for-observable. Inside the architecture everything is strongly typed.
11. Seeing It Happen
Two packets, two different failures, two different recoveries — in adjacent cycles:
A header CRC failure and a payload CRC failure, side by side
usb3_packet_decode — two failures, two recoveries
10 cyclesNote cycle 5 carefully. route is PROTO, not DISCARD — the packet with the bad payload is delivered. That is the row from section 6 that looks like a bug and is not.
12. The Testbenches
Each testbench does three things: sweeps the complete 512-point input space, checks a handful of named directed scenarios, and then runs 40 000 randomised cycles against an independent reference model. The reference model is deliberately written in a different shape from the design — where the design uses a ternary chain the model uses a case, and vice versa — so that a single misunderstanding cannot be baked into both.
12.1 Verilog testbench
`timescale 1ns/1ps
module tb_pk_v;
reg clk=0, rst_n=0;
reg pv=0, hco=0, pp=0, pco=0;
reg [4:0] ht=0;
wire [2:0] pkt_type;
wire [1:0] route;
wire hdr_error, payload_error, link_retry_req, protocol_nak_req;
wire ack_expected;
wire [31:0] hdr_errors, payload_errors, reserved_types, delivered;
always #5 clk=~clk;
usb3_packet_decode dut (
.clk(clk), .rst_n(rst_n), .pkt_valid(pv), .hdr_type(ht),
.hdr_crc_ok(hco), .payload_present(pp), .payload_crc_ok(pco),
.pkt_type(pkt_type), .route(route), .hdr_error(hdr_error),
.payload_error(payload_error), .link_retry_req(link_retry_req),
.protocol_nak_req(protocol_nak_req), .ack_expected(ack_expected),
.hdr_errors(hdr_errors), .payload_errors(payload_errors),
.reserved_types(reserved_types), .delivered(delivered));
localparam [2:0] P_NONE=0, P_LMP=1, P_TP=2, P_DP=3, P_ITP=4,
P_RSVD=5, P_UNK=6;
localparam [1:0] R_NONE=0, R_LINK=1, R_PROTO=2, R_DISCARD=3;
integer errors=0, i, t, c, q, r, v;
integer n_exh=0;
integer n_pt [0:6];
integer n_rt [0:3];
integer m_he, m_pe, m_rs, m_dl;
task check(input cond, input [639:0] msg);
begin if (!cond) begin errors=errors+1;
if (errors <= 25)
$display(" FAIL: %0s (pv=%b ht=%0h crc=%b pp=%b pcrc=%b | type=%0d route=%0d he=%b pe=%b, t=%0t)",
msg, pv, ht, hco, pp, pco, pkt_type, route, hdr_error,
payload_error, $time);
end end
endtask
task check_comb;
integer e_type, e_route;
reg e_he, e_pe, e_ack;
begin
// The model decides the type with a case over the four defined
// encodings rather than a chain of ternaries -- a different route.
if (!pv) e_type = P_NONE;
else if (!hco) e_type = P_UNK;
else case (ht)
5'h00: e_type = P_LMP;
5'h04: e_type = P_TP;
5'h08: e_type = P_DP;
5'h0C: e_type = P_ITP;
default: e_type = P_RSVD;
endcase
if (e_type == P_NONE) e_route = R_NONE;
else if (e_type == P_UNK || e_type == P_RSVD) e_route = R_DISCARD;
else if (e_type == P_LMP) e_route = R_LINK;
else e_route = R_PROTO;
e_he = pv && !hco;
e_pe = pv && hco && (e_type == P_DP) && pp && !pco;
e_ack = (e_type == P_TP) || (e_type == P_DP);
check(pkt_type === e_type[2:0], "pkt_type matches the model");
check(route === e_route[1:0], "route matches the model");
check(hdr_error === e_he, "hdr_error matches the model");
check(payload_error === e_pe, "payload_error matches the model");
check(link_retry_req === e_he, "link retry follows the header");
check(protocol_nak_req === e_pe, "protocol NAK follows the payload");
check(ack_expected === e_ack, "only TP and DP are acknowledged");
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE one that matters: with a bad header CRC the type is NOT
// decoded. Acting on the type field of a header a CRC has just
// declared corrupt routes a packet on untrustworthy bits.
if (pv && !hco)
check(pkt_type === P_UNK,
"a corrupt header was decoded into a packet type anyway");
// 2. And such a packet reaches nobody.
if (pv && !hco)
check(route === R_DISCARD,
"a packet with a corrupt header was routed somewhere");
// 3. The two recoveries are mutually exclusive: one needs a header
// the other does not have.
check(!(link_retry_req && protocol_nak_req),
"a link retry and a protocol NAK requested for one packet");
// 4. A payload error is never claimed without a good header, because
// knowing there WAS a payload requires the header.
check(!payload_error || hco,
"a payload error claimed with no trustworthy header");
// 5. Nothing is routed when no packet is present.
if (!pv) check(route === R_NONE, "a route asserted with no packet");
if (e_type >= 0 && e_type <= 6) n_pt[e_type] = n_pt[e_type] + 1;
if (e_route >= 0 && e_route <= 3) n_rt[e_route] = n_rt[e_route] + 1;
end
endtask
task step;
begin
#1;
check_comb;
if (pv) begin
if (hdr_error) m_he = m_he + 1;
if (payload_error) m_pe = m_pe + 1;
if (pkt_type === P_RSVD) m_rs = m_rs + 1;
if (route !== R_DISCARD && route !== R_NONE) m_dl = m_dl + 1;
end
@(posedge clk); #1;
check(hdr_errors === m_he[31:0], "hdr_errors matches the model");
check(payload_errors === m_pe[31:0], "payload_errors matches the model");
check(reserved_types === m_rs[31:0], "reserved_types matches the model");
check(delivered === m_dl[31:0], "delivered matches the model");
end
endtask
task hard_reset;
begin
rst_n=0; pv=0; ht=0; hco=0; pp=0; pco=0;
@(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
m_he=0; m_pe=0; m_rs=0; m_dl=0;
end
endtask
initial begin
for (i=0;i<7;i=i+1) n_pt[i]=0;
for (i=0;i<4;i=i+1) n_rt[i]=0;
hard_reset;
check(pkt_type === P_NONE, "no packet, no type");
check(route === R_NONE, "and no route");
// ===== A. EXHAUSTIVE over the whole decode =====
// 32 header-type encodings x header CRC ok/bad x payload present/absent
// x payload CRC ok/bad x packet valid/not = 512 points, which is the
// ENTIRE decision surface including all 28 reserved type encodings.
for (v=0; v<2; v=v+1)
for (t=0; t<32; t=t+1)
for (c=0; c<2; c=c+1)
for (q=0; q<2; q=q+1)
for (r=0; r<2; r=r+1) begin
pv=v[0]; ht=t[4:0]; hco=c[0]; pp=q[0]; pco=r[0];
step;
n_exh = n_exh + 1;
end
$display(" exhaustive packet-decode sweep: %0d of %0d points verified",
n_exh, 2*32*2*2*2);
// ===== B. directed: the four types and the two failures =====
hard_reset;
pv=1; ht=5'h00; hco=1; pp=0; pco=1; step;
check(pkt_type === P_LMP, "type 0x00 is a Link Management Packet");
check(route === R_LINK, "and it stays at the LINK layer");
check(!ack_expected, "LMPs are not acknowledged this way");
pv=1; ht=5'h04; hco=1; pp=0; pco=1; step;
check(pkt_type === P_TP, "type 0x04 is a Transaction Packet");
check(route === R_PROTO, "which goes up to the protocol layer");
check(ack_expected, "and is acknowledged");
pv=1; ht=5'h0C; hco=1; pp=0; pco=1; step;
check(pkt_type === P_ITP, "type 0x0C is an Isochronous Timestamp Packet");
check(route === R_PROTO, "which is delivered");
check(!ack_expected,
"but NEVER acknowledged: a timestamp is stale once questioned");
// a Data Packet with a bad payload still routes
pv=1; ht=5'h08; hco=1; pp=1; pco=0; step;
check(pkt_type === P_DP, "type 0x08 is a Data Packet");
check(payload_error, "its payload CRC failed");
check(route === R_PROTO,
"and it is STILL routed: the header says which endpoint to re-ask");
check(protocol_nak_req, "with a PROTOCOL-level NAK");
check(!link_retry_req, "and NOT a link retry");
// a bad header is a different problem entirely
pv=1; ht=5'h08; hco=0; pp=1; pco=1; step;
check(pkt_type === P_UNK,
"a corrupt header yields UNKNOWN, not the type its bits spell");
check(route === R_DISCARD, "and the packet reaches nobody");
check(link_retry_req, "recovery is a LINK retry");
check(!protocol_nak_req, "and not a protocol NAK");
check(!payload_error,
"and no payload error is claimed: there is no trustworthy header");
// a reserved type has a GOOD CRC and an uninterpretable type
pv=1; ht=5'h11; hco=1; pp=0; pco=1; step;
check(pkt_type === P_RSVD, "an undefined encoding is RESERVED");
check(route === R_DISCARD, "and is discarded");
check(!link_retry_req,
"without a link retry: retrying returns the same reserved value");
check(!hdr_error, "and it is not a header error -- the CRC was fine");
// ===== C. randomised =====
for (i=0;i<40000;i=i+1) begin
pv=({$random}%8)!=0;
// bias toward the four defined encodings so the reserved path is
// exercised without swamping everything else
if (({$random}%3)==0) ht = {$random}%32;
else ht = (({$random}%4)*4);
hco=({$random}%8)!=0; pp=({$random}%2); pco=({$random}%4)!=0;
step;
end
for (i=0;i<7;i=i+1)
check(n_pt[i] > 0, "every packet-type outcome was reached");
for (i=0;i<4;i=i+1)
check(n_rt[i] > 0, "every routing outcome was reached");
$display("");
$display(" REACH: exhaustive=%0d | types: none=%0d lmp=%0d tp=%0d dp=%0d itp=%0d rsvd=%0d unknown=%0d",
n_exh, n_pt[0], n_pt[1], n_pt[2], n_pt[3], n_pt[4], n_pt[5],
n_pt[6]);
$display(" ROUTES: none=%0d link=%0d protocol=%0d discard=%0d | hdr-err=%0d payload-err=%0d reserved=%0d delivered=%0d",
n_rt[0], n_rt[1], n_rt[2], n_rt[3], hdr_errors, payload_errors,
reserved_types, delivered);
$display(" [Verilog] usb3_packet_decode: %0d errors", errors);
$display(" [Verilog] %0s", errors==0 ? "PASS" : "FAIL");
$display("");
$finish;
end
endmodule12.2 SystemVerilog testbench
`timescale 1ns/1ps
module tb_pk_sv;
import usb3_packet_pkg::*;
logic clk=0, rst_n=0;
logic pv=0, hco=0, pp=0, pco=0;
logic [4:0] ht=0;
pkt_type_e pkt_type;
route_e route;
logic hdr_error, payload_error, link_retry_req, protocol_nak_req;
logic ack_expected;
logic [31:0] hdr_errors, payload_errors, reserved_types, delivered;
always #5 clk=~clk;
usb3_packet_decode dut (
.clk, .rst_n, .pkt_valid(pv), .hdr_type(ht), .hdr_crc_ok(hco),
.payload_present(pp), .payload_crc_ok(pco), .pkt_type, .route,
.hdr_error, .payload_error, .link_retry_req, .protocol_nak_req,
.ack_expected, .hdr_errors, .payload_errors, .reserved_types, .delivered);
int errors=0, i, t, c, q, r, v;
int n_exh=0;
int n_pt [7];
int n_rt [4];
int m_he, m_pe, m_rs, m_dl;
// Icarus will not call .name() on a net, so the enum outputs are copied
// into variables of the same type before being printed.
task automatic check(input bit cond, input string msg);
pkt_type_e pt_v;
route_e rt_v;
if (!cond) begin
errors++;
pt_v = pkt_type;
rt_v = route;
if (errors <= 25)
$display(" FAIL: %0s (pv=%b ht=%0h crc=%b pp=%b pcrc=%b | type=%s route=%s, t=%0t)",
msg, pv, ht, hco, pp, pco, pt_v.name(), rt_v.name(), $time);
end
endtask
task automatic check_comb;
pkt_type_e e_type;
route_e e_route;
bit e_he, e_pe, e_ack;
begin
// The model reaches the same answer by a different route: a priority
// chain of comparisons rather than the design's case statement.
if (!pv) e_type = PKT_NONE;
else if (!hco) e_type = PKT_UNKNOWN;
else if (ht == 5'h00) e_type = PKT_LMP;
else if (ht == 5'h04) e_type = PKT_TP;
else if (ht == 5'h08) e_type = PKT_DP;
else if (ht == 5'h0C) e_type = PKT_ITP;
else e_type = PKT_RSVD;
if (e_type == PKT_NONE) e_route = ROUTE_NONE;
else if (e_type == PKT_UNKNOWN || e_type == PKT_RSVD) e_route = ROUTE_DISCARD;
else if (e_type == PKT_LMP) e_route = ROUTE_LINK;
else e_route = ROUTE_PROTOCOL;
e_he = pv && !hco;
e_pe = pv && hco && (e_type == PKT_DP) && pp && !pco;
e_ack = (e_type == PKT_TP) || (e_type == PKT_DP);
check(pkt_type === e_type, "pkt_type matches the model");
check(route === e_route, "route matches the model");
check(hdr_error === e_he, "hdr_error matches the model");
check(payload_error === e_pe, "payload_error matches the model");
check(link_retry_req === e_he, "link retry follows the header");
check(protocol_nak_req === e_pe, "protocol NAK follows the payload");
check(ack_expected === e_ack, "only TP and DP are acknowledged");
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE one that matters: with a bad header CRC the type is NOT
// decoded. Acting on the type field of a header a CRC has just
// declared corrupt routes a packet on untrustworthy bits.
if (pv && !hco)
check(pkt_type === PKT_UNKNOWN,
"a corrupt header was decoded into a packet type anyway");
// 2. And such a packet reaches nobody.
if (pv && !hco)
check(route === ROUTE_DISCARD,
"a packet with a corrupt header was routed somewhere");
// 3. The two recoveries are mutually exclusive: one needs a header the
// other does not have.
check(!(link_retry_req && protocol_nak_req),
"a link retry and a protocol NAK requested for one packet");
// 4. A payload error is never claimed without a good header.
check(!payload_error || hco,
"a payload error claimed with no trustworthy header");
// 5. Nothing is routed when no packet is present.
if (!pv) check(route === ROUTE_NONE, "a route asserted with no packet");
n_pt[int'(e_type)] = n_pt[int'(e_type)] + 1;
n_rt[int'(e_route)] = n_rt[int'(e_route)] + 1;
end
endtask
task automatic step;
begin
#1;
check_comb;
if (pv) begin
if (hdr_error) m_he++;
if (payload_error) m_pe++;
if (pkt_type === PKT_RSVD) m_rs++;
if (route !== ROUTE_DISCARD && route !== ROUTE_NONE) m_dl++;
end
@(posedge clk); #1;
check(hdr_errors === 32'(m_he), "hdr_errors matches the model");
check(payload_errors === 32'(m_pe), "payload_errors matches the model");
check(reserved_types === 32'(m_rs), "reserved_types matches the model");
check(delivered === 32'(m_dl), "delivered matches the model");
end
endtask
task automatic hard_reset;
begin
rst_n=0; pv=0; ht=0; hco=0; pp=0; pco=0;
@(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
m_he=0; m_pe=0; m_rs=0; m_dl=0;
end
endtask
initial begin
foreach (n_pt[i]) n_pt[i]=0;
foreach (n_rt[i]) n_rt[i]=0;
hard_reset;
check(pkt_type === PKT_NONE, "no packet, no type");
check(route === ROUTE_NONE, "and no route");
// ===== A. EXHAUSTIVE over the whole decode =====
// 32 header-type encodings x header CRC ok/bad x payload present/absent
// x payload CRC ok/bad x packet valid/not = 512 points -- the ENTIRE
// decision surface, including all 28 reserved type encodings.
for (v=0; v<2; v++)
for (t=0; t<32; t++)
for (c=0; c<2; c++)
for (q=0; q<2; q++)
for (r=0; r<2; r++) begin
pv=v[0]; ht=t[4:0]; hco=c[0]; pp=q[0]; pco=r[0];
step;
n_exh++;
end
$display(" exhaustive packet-decode sweep: %0d of %0d points verified",
n_exh, 2*32*2*2*2);
// ===== B. directed: the four types and the two failures =====
hard_reset;
pv=1; ht=5'h00; hco=1; pp=0; pco=1; step;
check(pkt_type === PKT_LMP, "type 0x00 is a Link Management Packet");
check(route === ROUTE_LINK, "and it stays at the LINK layer");
check(!ack_expected, "LMPs are not acknowledged this way");
pv=1; ht=5'h04; hco=1; pp=0; pco=1; step;
check(pkt_type === PKT_TP, "type 0x04 is a Transaction Packet");
check(route === ROUTE_PROTOCOL, "which goes up to the protocol layer");
check(ack_expected, "and is acknowledged");
pv=1; ht=5'h0C; hco=1; pp=0; pco=1; step;
check(pkt_type === PKT_ITP, "type 0x0C is an Isochronous Timestamp Packet");
check(route === ROUTE_PROTOCOL, "which is delivered");
check(!ack_expected,
"but NEVER acknowledged: a timestamp is stale once questioned");
// a Data Packet with a bad payload still routes
pv=1; ht=5'h08; hco=1; pp=1; pco=0; step;
check(pkt_type === PKT_DP, "type 0x08 is a Data Packet");
check(payload_error, "its payload CRC failed");
check(route === ROUTE_PROTOCOL,
"and it is STILL routed: the header says which endpoint to re-ask");
check(protocol_nak_req, "with a PROTOCOL-level NAK");
check(!link_retry_req, "and NOT a link retry");
// a bad header is a different problem entirely
pv=1; ht=5'h08; hco=0; pp=1; pco=1; step;
check(pkt_type === PKT_UNKNOWN,
"a corrupt header yields UNKNOWN, not the type its bits spell");
check(route === ROUTE_DISCARD, "and the packet reaches nobody");
check(link_retry_req, "recovery is a LINK retry");
check(!protocol_nak_req, "and not a protocol NAK");
check(!payload_error,
"and no payload error is claimed: there is no trustworthy header");
// a reserved type has a GOOD CRC and an uninterpretable type
pv=1; ht=5'h11; hco=1; pp=0; pco=1; step;
check(pkt_type === PKT_RSVD, "an undefined encoding is RESERVED");
check(route === ROUTE_DISCARD, "and is discarded");
check(!link_retry_req,
"without a link retry: retrying returns the same reserved value");
check(!hdr_error, "and it is not a header error -- the CRC was fine");
// ===== C. randomised =====
for (i=0;i<40000;i++) begin
pv=($urandom%8)!=0;
if (($urandom%3)==0) ht = $urandom%32;
else ht = 5'(($urandom%4)*4);
hco=($urandom%8)!=0; pp=$urandom%2; pco=($urandom%4)!=0;
step;
end
foreach (n_pt[i]) check(n_pt[i] > 0, "every packet-type outcome was reached");
foreach (n_rt[i]) check(n_rt[i] > 0, "every routing outcome was reached");
$display("");
$display(" REACH: exhaustive=%0d | types: none=%0d lmp=%0d tp=%0d dp=%0d itp=%0d rsvd=%0d unknown=%0d",
n_exh, n_pt[0], n_pt[1], n_pt[2], n_pt[3], n_pt[4], n_pt[5], n_pt[6]);
$display(" ROUTES: none=%0d link=%0d protocol=%0d discard=%0d | hdr-err=%0d payload-err=%0d reserved=%0d delivered=%0d",
n_rt[0], n_rt[1], n_rt[2], n_rt[3], hdr_errors, payload_errors,
reserved_types, delivered);
$display(" [SystemVerilog] usb3_packet_decode: %0d errors", errors);
$display(" [SystemVerilog] %0s", errors==0 ? "PASS" : "FAIL");
$display("");
$finish;
end
endmodule12.3 VHDL testbench
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
use work.usb3_packet_pkg.all;
entity tb_pk_vhdl is
end entity;
architecture sim of tb_pk_vhdl is
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal pv, hco, pp, pco : std_logic := '0';
signal ht : std_logic_vector(4 downto 0) := (others => '0');
signal pkt_type : std_logic_vector(2 downto 0);
signal route : std_logic_vector(1 downto 0);
signal hdr_error, payload_error, link_retry_req : std_logic;
signal protocol_nak_req, ack_expected : std_logic;
signal hdr_errors, payload_errors, reserved_types, delivered
: std_logic_vector(31 downto 0);
signal running : boolean := true;
type cnt7_t is array (0 to 6) of integer;
type cnt4_t is array (0 to 3) of integer;
begin
clk <= not clk after 5 ns when running else '0';
dut : entity work.usb3_packet_decode
port map (clk => clk, rst_n => rst_n, pkt_valid => pv, hdr_type => ht,
hdr_crc_ok => hco, payload_present => pp, payload_crc_ok => pco,
pkt_type => pkt_type, route => route, hdr_error => hdr_error,
payload_error => payload_error, link_retry_req => link_retry_req,
protocol_nak_req => protocol_nak_req,
ack_expected => ack_expected, hdr_errors => hdr_errors,
payload_errors => payload_errors,
reserved_types => reserved_types, delivered => delivered);
stim : process
variable seed1 : positive := 8867;
variable seed2 : positive := 3391;
variable r1 : real;
-- VHDL-2008 requires a shared variable to have a protected type, so the
-- bookkeeping lives inside the single stimulus process instead.
variable errors : integer := 0;
variable n_pt : cnt7_t := (others => 0);
variable n_rt : cnt4_t := (others => 0);
variable n_exh : integer := 0;
variable m_he, m_pe, m_rs, m_dl : integer := 0;
procedure check(cond : boolean; msg : string) is
begin
if not cond then
errors := errors + 1;
if errors <= 25 then
report " FAIL: " & msg
& " (pv=" & std_logic'image(pv)(2)
& " ht=" & integer'image(to_integer(unsigned(ht)))
& " crc=" & std_logic'image(hco)(2)
& " pp=" & std_logic'image(pp)(2)
& " pcrc=" & std_logic'image(pco)(2)
& " | type=" & integer'image(to_integer(unsigned(pkt_type)))
& " route=" & integer'image(to_integer(unsigned(route)))
& ")" severity note;
end if;
end if;
end procedure;
procedure rnd(variable v : out integer; m : integer) is
begin
uniform(seed1, seed2, r1);
v := integer(floor(r1 * real(m)));
end procedure;
procedure check_comb is
variable e_type : pkt_type_t;
variable e_route : route_t;
variable e_he, e_pe, e_ack : std_logic;
begin
-- The model reaches the same answer by a different route: a chain of
-- comparisons rather than the design's case statement.
if pv = '0' then
e_type := PKT_NONE;
elsif hco = '0' then
e_type := PKT_UNKNOWN;
elsif ht = "00000" then e_type := PKT_LMP;
elsif ht = "00100" then e_type := PKT_TP;
elsif ht = "01000" then e_type := PKT_DP;
elsif ht = "01100" then e_type := PKT_ITP;
else e_type := PKT_RSVD;
end if;
if e_type = PKT_NONE then
e_route := ROUTE_NONE;
elsif e_type = PKT_UNKNOWN or e_type = PKT_RSVD then
e_route := ROUTE_DISCARD;
elsif e_type = PKT_LMP then
e_route := ROUTE_LINK;
else
e_route := ROUTE_PROTOCOL;
end if;
if pv = '1' and hco = '0' then e_he := '1'; else e_he := '0'; end if;
if pv = '1' and hco = '1' and e_type = PKT_DP and pp = '1'
and pco = '0' then
e_pe := '1';
else
e_pe := '0';
end if;
if e_type = PKT_TP or e_type = PKT_DP then
e_ack := '1';
else
e_ack := '0';
end if;
check(pkt_type = pkt_code(e_type), "pkt_type matches the model");
check(route = route_code(e_route), "route matches the model");
check(hdr_error = e_he, "hdr_error matches the model");
check(payload_error = e_pe, "payload_error matches the model");
check(link_retry_req = e_he, "link retry follows the header");
check(protocol_nak_req = e_pe, "protocol NAK follows the payload");
check(ack_expected = e_ack, "only TP and DP are acknowledged");
-- ---- SAFETY PROPERTIES, independent of the model ----
-- 1. THE one that matters: with a bad header CRC the type is NOT
-- decoded. Acting on the type field of a header a CRC has just
-- declared corrupt routes a packet on untrustworthy bits.
if pv = '1' and hco = '0' then
check(pkt_type = pkt_code(PKT_UNKNOWN),
"a corrupt header was decoded into a packet type anyway");
-- 2. And such a packet reaches nobody.
check(route = route_code(ROUTE_DISCARD),
"a packet with a corrupt header was routed somewhere");
end if;
-- 3. The two recoveries are mutually exclusive.
check(not (link_retry_req = '1' and protocol_nak_req = '1'),
"a link retry and a protocol NAK requested for one packet");
-- 4. A payload error is never claimed without a good header.
check(payload_error = '0' or hco = '1',
"a payload error claimed with no trustworthy header");
-- 5. Nothing is routed when no packet is present.
if pv = '0' then
check(route = route_code(ROUTE_NONE),
"a route asserted with no packet");
end if;
n_pt(pkt_type_t'pos(e_type)) := n_pt(pkt_type_t'pos(e_type)) + 1;
n_rt(route_t'pos(e_route)) := n_rt(route_t'pos(e_route)) + 1;
end procedure;
procedure step is
begin
wait for 1 ns;
check_comb;
if pv = '1' then
if hdr_error = '1' then m_he := m_he + 1; end if;
if payload_error = '1' then m_pe := m_pe + 1; end if;
if pkt_type = pkt_code(PKT_RSVD) then m_rs := m_rs + 1; end if;
if route /= route_code(ROUTE_DISCARD)
and route /= route_code(ROUTE_NONE) then
m_dl := m_dl + 1;
end if;
end if;
wait until rising_edge(clk);
wait for 1 ns;
check(hdr_errors = std_logic_vector(to_unsigned(m_he, 32)),
"hdr_errors matches the model");
check(payload_errors = std_logic_vector(to_unsigned(m_pe, 32)),
"payload_errors matches the model");
check(reserved_types = std_logic_vector(to_unsigned(m_rs, 32)),
"reserved_types matches the model");
check(delivered = std_logic_vector(to_unsigned(m_dl, 32)),
"delivered matches the model");
end procedure;
procedure hard_reset is
begin
rst_n <= '0'; pv <= '0'; ht <= (others => '0');
hco <= '0'; pp <= '0'; pco <= '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_he := 0; m_pe := 0; m_rs := 0; m_dl := 0;
end procedure;
variable iv : integer;
begin
hard_reset;
check(pkt_type = pkt_code(PKT_NONE), "no packet, no type");
check(route = route_code(ROUTE_NONE), "and no route");
-- ===== A. EXHAUSTIVE over the whole decode =====
-- 32 header-type encodings x header CRC ok/bad x payload present/absent
-- x payload CRC ok/bad x packet valid/not = 512 points -- the ENTIRE
-- decision surface, including all 28 reserved type encodings.
for v in 0 to 1 loop
for t in 0 to 31 loop
for c in 0 to 1 loop
for q in 0 to 1 loop
for r in 0 to 1 loop
if v = 1 then pv <= '1'; else pv <= '0'; end if;
ht <= std_logic_vector(to_unsigned(t, 5));
if c = 1 then hco <= '1'; else hco <= '0'; end if;
if q = 1 then pp <= '1'; else pp <= '0'; end if;
if r = 1 then pco <= '1'; else pco <= '0'; end if;
step;
n_exh := n_exh + 1;
end loop;
end loop;
end loop;
end loop;
end loop;
report " exhaustive packet-decode sweep: " & integer'image(n_exh)
& " of 512 points verified" severity note;
-- ===== B. directed: the four types and the two failures =====
hard_reset;
pv <= '1'; ht <= "00000"; hco <= '1'; pp <= '0'; pco <= '1'; step;
check(pkt_type = pkt_code(PKT_LMP),
"type 0x00 is a Link Management Packet");
check(route = route_code(ROUTE_LINK), "and it stays at the LINK layer");
check(ack_expected = '0', "LMPs are not acknowledged this way");
pv <= '1'; ht <= "00100"; hco <= '1'; pp <= '0'; pco <= '1'; step;
check(pkt_type = pkt_code(PKT_TP), "type 0x04 is a Transaction Packet");
check(route = route_code(ROUTE_PROTOCOL),
"which goes up to the protocol layer");
check(ack_expected = '1', "and is acknowledged");
pv <= '1'; ht <= "01100"; hco <= '1'; pp <= '0'; pco <= '1'; step;
check(pkt_type = pkt_code(PKT_ITP),
"type 0x0C is an Isochronous Timestamp Packet");
check(route = route_code(ROUTE_PROTOCOL), "which is delivered");
check(ack_expected = '0',
"but NEVER acknowledged: a timestamp is stale once questioned");
-- a Data Packet with a bad payload still routes
pv <= '1'; ht <= "01000"; hco <= '1'; pp <= '1'; pco <= '0'; step;
check(pkt_type = pkt_code(PKT_DP), "type 0x08 is a Data Packet");
check(payload_error = '1', "its payload CRC failed");
check(route = route_code(ROUTE_PROTOCOL),
"and it is STILL routed: the header says which endpoint to re-ask");
check(protocol_nak_req = '1', "with a PROTOCOL-level NAK");
check(link_retry_req = '0', "and NOT a link retry");
-- a bad header is a different problem entirely
pv <= '1'; ht <= "01000"; hco <= '0'; pp <= '1'; pco <= '1'; step;
check(pkt_type = pkt_code(PKT_UNKNOWN),
"a corrupt header yields UNKNOWN, not the type its bits spell");
check(route = route_code(ROUTE_DISCARD), "and the packet reaches nobody");
check(link_retry_req = '1', "recovery is a LINK retry");
check(protocol_nak_req = '0', "and not a protocol NAK");
check(payload_error = '0',
"and no payload error is claimed: there is no trustworthy header");
-- a reserved type has a GOOD CRC and an uninterpretable type
pv <= '1'; ht <= "10001"; hco <= '1'; pp <= '0'; pco <= '1'; step;
check(pkt_type = pkt_code(PKT_RSVD), "an undefined encoding is RESERVED");
check(route = route_code(ROUTE_DISCARD), "and is discarded");
check(link_retry_req = '0',
"without a link retry: retrying returns the same reserved value");
check(hdr_error = '0',
"and it is not a header error -- the CRC was fine");
-- ===== C. randomised =====
for i in 0 to 39999 loop
rnd(iv, 8); if iv /= 0 then pv <= '1'; else pv <= '0'; end if;
rnd(iv, 3);
if iv = 0 then
rnd(iv, 32);
ht <= std_logic_vector(to_unsigned(iv, 5));
else
rnd(iv, 4);
ht <= std_logic_vector(to_unsigned(iv * 4, 5));
end if;
rnd(iv, 8); if iv /= 0 then hco <= '1'; else hco <= '0'; end if;
rnd(iv, 2); if iv = 1 then pp <= '1'; else pp <= '0'; end if;
rnd(iv, 4); if iv /= 0 then pco <= '1'; else pco <= '0'; end if;
step;
end loop;
for i in 0 to 6 loop
check(n_pt(i) > 0, "every packet-type outcome was reached");
end loop;
for i in 0 to 3 loop
check(n_rt(i) > 0, "every routing outcome was reached");
end loop;
report " REACH: exhaustive=" & integer'image(n_exh)
& " | types: none=" & integer'image(n_pt(0))
& " lmp=" & integer'image(n_pt(1))
& " tp=" & integer'image(n_pt(2))
& " dp=" & integer'image(n_pt(3))
& " itp=" & integer'image(n_pt(4))
& " rsvd=" & integer'image(n_pt(5))
& " unknown=" & integer'image(n_pt(6)) severity note;
report " ROUTES: none=" & integer'image(n_rt(0))
& " link=" & integer'image(n_rt(1))
& " protocol=" & integer'image(n_rt(2))
& " discard=" & integer'image(n_rt(3))
& " | hdr-err=" & integer'image(to_integer(unsigned(hdr_errors)))
& " payload-err=" & integer'image(to_integer(unsigned(payload_errors)))
& " reserved=" & integer'image(to_integer(unsigned(reserved_types)))
& " delivered=" & integer'image(to_integer(unsigned(delivered)))
severity note;
report " [VHDL] usb3_packet_decode: " & integer'image(errors) & " errors"
severity note;
if errors = 0 then
report " [VHDL] PASS" severity note;
else
report " [VHDL] FAIL" severity failure;
end if;
running <= false;
wait;
end process;
end architecture;13. Exhaustive Verification, and What "Exhaustive" Bought
All three implementations pass, and the reach figures confirm the sweep actually landed everywhere it claimed to:
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| Exhaustive points | 512 / 512 | 512 / 512 | 512 / 512 |
PKT_NONE reached | 5219 | 5219 | 5185 |
PKT_LMP reached | 5587 | 5504 | 5369 |
PKT_TP reached | 5354 | 5410 | 5404 |
PKT_DP reached | 5448 | 5395 | 5503 |
PKT_ITP reached | 5408 | 5411 | 5452 |
PKT_RSVD reached | 9022 | 9099 | 9063 |
PKT_UNKNOWN reached | 4480 | 4480 | 4542 |
ROUTE_LINK | 5587 | 5504 | 5369 |
ROUTE_PROTOCOL | 16210 | 16216 | 16359 |
ROUTE_DISCARD | 13502 | 13579 | 13605 |
| Header errors counted | 4352 | 4352 | 4414 |
| Payload errors counted | 695 | 677 | 694 |
| Packets delivered | 21781 | 21704 | 21712 |
| Result | PASS | PASS | PASS |
Every one of the seven pkt_type outcomes and all four route outcomes were reached, which the testbenches assert rather than merely report. PKT_RSVD is the most-reached outcome because 28 of the 32 encodings produce it — a reminder that in the real input space, "a type I do not recognise" is the common case, not the exotic one.
14. Mutation Testing
A passing testbench proves nothing about the testbench. Seven single-change mutants were injected into each implementation and the suites re-run; the numbers are the failure counts each mutant produced.
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| K1 | Decode the type before checking the CRC | 16740 | 16640 | 16931 |
| K2 | A reserved type also reports hdr_error | 18046 | 18200 | 18128 |
| K3 | A Data Packet with a bad payload is discarded | 697 | 679 | 703 |
| K4 | ITP is acknowledged like TP and DP | 5409 | 5412 | 5453 |
| K5 | payload_error no longer requires a payload | 1388 | 1302 | 1340 |
| K6 | LMP routes to the protocol layer | 5588 | 5505 | 5370 |
| K7 | delivered counts discarded packets too | 40258 | 40258 | 40258 |
| — | unmutated baseline | 0 | 0 | 0 |
Every mutant dies, in every language, and no two mutants produce the same count — so no two of them are being caught by the same single check.
K1 is the mutation this chapter exists for. It is the "decode first, validate after" error from section 4, injected verbatim, and it produces roughly 16 700 failures. Worth noticing why that number is so large: the mutation does not merely mis-report one signal. Removing PKT_UNKNOWN changes pkt_type, which changes route, which changes ack_expected, which changes delivered — one wrong line propagating into four observables. In real silicon it would propagate into four behaviours, which is why a bug of this shape is so hard to localise from a symptom.
K3 is the smallest killer at ~690 failures, and that is expected. It fires only on a Data Packet that has a good header, a payload present, and a bad payload CRC — a four-way conjunction. The randomised phase generates about 690 of those in 40 000 cycles, and the exhaustive sweep contributes exactly one point. A count that low is a warning to check why it is low: here it is a genuinely narrow input condition, and all three languages agree on it to within 3%.
K2 and K6 are the two that a careless reviewer would call harmless. K2 asks for a link retry on a reserved type — "surely that is just a wasted retry?" It is a wasted retry that recurs every time the same transmitter sends the same packet, which on a device from a newer specification revision is a permanent error storm on a link that has nothing wrong with it. K6 routes LMPs to the protocol layer, where they arrive at an endpoint that has no idea what to do with them; the link-layer state machine that was waiting for them, meanwhile, times out.
15. Debugging Walkthrough: Rare Corruption on an Idle Endpoint
This is the K1 bug as it presents in the field, and the sequence of deductions that finds it.
The report. A storage device occasionally returns a block containing a few bytes of what is obviously somebody else's data — a fragment of an audio stream, in one memorable case. It happens perhaps once an hour, only with certain cables, and never on the bench.
Step 1 — is the link healthy? Read the link error counters. They are non-zero: a few LBAD events per hour. That looks like it explains the problem — a marginal cable, occasional bit errors — and the tempting conclusion is "replace the cable." Resist it. A marginal cable causes retries, and retries are supposed to be invisible. Errors being detected is the system working. The question is what happens next.
Step 2 — correlate. Timestamp the corruption events against the LBAD events. They coincide. So corruption happens when an error is detected, not when one is missed. That inverts the whole investigation: the CRC is doing its job, and something downstream of the CRC is not.
Step 3 — which CRC? Add separate counters for header-CRC and payload-CRC failures — the hdr_errors and payload_errors outputs of the design in this chapter exist for exactly this reason. The corruption correlates with header failures only. Payload failures are recovered cleanly.
Step 4 — the decisive trace. Capture pkt_type and route on the cycles where hdr_crc_ok is low. In a correct receiver they read UNKNOWN and DISCARD. Here they read a plausible type and a valid route — and the route is not always the same one.
That is the whole bug, visible in one cursor position. The receiver is reading the type and routing fields out of a header the CRC has just rejected, and a flipped bit in the routing field occasionally addresses a different endpoint.
Step 5 — why it never reproduced on the bench. The bug requires a physical-layer bit error and for that error to land in a routing or type field rather than anywhere else in the fourteen bytes. Short bench cables produce almost no bit errors, so the trigger is absent. The bug scales with cable quality, which is exactly the property that makes it look like a cable problem.
16. UVM: Verifying the Untrustworthy-Header Path
The interesting part of this design is only reachable by injecting errors, which makes it a natural fit for a constrained-random UVM environment where corruption is a first-class stimulus rather than an afterthought.
16.1 The transaction
class usb3_pkt_item extends uvm_sequence_item;
`uvm_object_utils(usb3_pkt_item)
rand bit pkt_valid;
rand bit [4:0] hdr_type;
rand bit hdr_crc_ok;
rand bit payload_present;
rand bit payload_crc_ok;
// The four defined encodings, so a "legal" packet can be asked for
// without the sequence having to know the numbers.
constraint c_defined_type {
soft hdr_type inside {5'h00, 5'h04, 5'h08, 5'h0C};
}
// Errors are rare in the field. Making them rare here too means the
// ordinary paths get exercised; the directed sequences below override
// this when they want the error path specifically.
constraint c_errors_rare {
hdr_crc_ok dist {1 := 95, 0 := 5};
payload_crc_ok dist {1 := 95, 0 := 5};
}
// Only a Data Packet carries a payload.
constraint c_payload_only_dp {
(hdr_type != 5'h08) -> payload_present == 0;
}
function new(string name = "usb3_pkt_item");
super.new(name);
endfunction
function string convert2string();
return $sformatf("valid=%0b type=0x%02h hcrc=%0b pp=%0b pcrc=%0b",
pkt_valid, hdr_type, hdr_crc_ok, payload_present,
payload_crc_ok);
endfunction
endclassThe c_payload_only_dp constraint deserves comment. It models the protocol faithfully — only Data Packets have payloads — but it also hides a bug class, because with it in force payload_present is never 1 on a non-DP packet and mutation K5 becomes harder to kill. That is why the error-injection sequence below deliberately relaxes it: a receiver must behave correctly against a broken transmitter, and a constraint that only generates legal traffic cannot prove that.
16.2 Sequences
// Ordinary traffic: legal types, errors at field-realistic rates.
class nominal_traffic_seq extends uvm_sequence #(usb3_pkt_item);
`uvm_object_utils(nominal_traffic_seq)
function new(string name = "nominal_traffic_seq"); super.new(name); endfunction
task body();
repeat (2000) begin
usb3_pkt_item it = usb3_pkt_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { pkt_valid dist {1 := 90, 0 := 10}; })
`uvm_error("RAND", "nominal randomize failed")
finish_item(it);
end
endtask
endclass
// THE sequence for this chapter: every packet has a corrupt header, and the
// type field is randomised over all 32 encodings. A receiver that decodes
// before validating will route these somewhere, and the scoreboard will see
// it. This is the K1 mutation's natural predator.
class corrupt_header_seq extends uvm_sequence #(usb3_pkt_item);
`uvm_object_utils(corrupt_header_seq)
function new(string name = "corrupt_header_seq"); super.new(name); endfunction
task body();
repeat (500) begin
usb3_pkt_item it = usb3_pkt_item::type_id::create("it");
start_item(it);
// c_defined_type is soft, so this override is legal and the full
// 32-encoding space is explored under a failing CRC.
if (!it.randomize() with { pkt_valid == 1;
hdr_crc_ok == 0;
hdr_type inside {[0:31]}; })
`uvm_error("RAND", "corrupt-header randomize failed")
finish_item(it);
end
endtask
endclass
// Reserved encodings under a GOOD CRC -- the third case from section 5.
// The property under test is that these are discarded WITHOUT a retry.
class reserved_type_seq extends uvm_sequence #(usb3_pkt_item);
`uvm_object_utils(reserved_type_seq)
function new(string name = "reserved_type_seq"); super.new(name); endfunction
task body();
repeat (500) begin
usb3_pkt_item it = usb3_pkt_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { pkt_valid == 1;
hdr_crc_ok == 1;
!(hdr_type inside {5'h00, 5'h04,
5'h08, 5'h0C}); })
`uvm_error("RAND", "reserved-type randomize failed")
finish_item(it);
end
endtask
endclass
// A malformed transmitter: payloads on packets that should not have them,
// and missing payloads on Data Packets. Relaxes c_payload_only_dp on
// purpose -- see the note above section 16.2.
class malformed_tx_seq extends uvm_sequence #(usb3_pkt_item);
`uvm_object_utils(malformed_tx_seq)
function new(string name = "malformed_tx_seq"); super.new(name); endfunction
task body();
repeat (500) begin
usb3_pkt_item it = usb3_pkt_item::type_id::create("it");
start_item(it);
it.c_payload_only_dp.constraint_mode(0);
if (!it.randomize() with { pkt_valid == 1; hdr_crc_ok == 1; })
`uvm_error("RAND", "malformed randomize failed")
finish_item(it);
end
endtask
endclass16.3 The scoreboard, and the one check that matters
class usb3_pkt_scoreboard extends uvm_scoreboard;
`uvm_component_utils(usb3_pkt_scoreboard)
uvm_analysis_imp #(usb3_pkt_mon_item, usb3_pkt_scoreboard) ap;
int unsigned n_unknown, n_reserved, n_delivered, n_retry, n_nak;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void write(usb3_pkt_mon_item t);
// ---- THE check. A header the CRC rejected must not have been read. ----
if (t.pkt_valid && !t.hdr_crc_ok) begin
if (t.pkt_type !== PKT_UNKNOWN)
`uvm_error("HDR_TRUST",
$sformatf("corrupt header decoded as %s -- the type field was read",
t.pkt_type.name()))
if (t.route !== ROUTE_DISCARD)
`uvm_error("HDR_TRUST",
$sformatf("corrupt header routed to %s", t.route.name()))
if (!t.link_retry_req)
`uvm_error("RECOVERY", "corrupt header did not request a link retry")
if (t.protocol_nak_req)
`uvm_error("RECOVERY",
"corrupt header requested a PROTOCOL nak -- with what endpoint?")
n_unknown++;
end
// ---- A reserved type is NOT a header error and must NOT be retried ----
if (t.pkt_type === PKT_RSVD) begin
if (t.link_retry_req)
`uvm_error("RETRY_STORM",
"reserved type requested a link retry; the retry will return it again")
if (t.hdr_error)
`uvm_error("CLASSIFY", "reserved type reported as a header error")
n_reserved++;
end
// ---- A bad payload is still DELIVERED, flagged, never dropped ----
if (t.pkt_type === PKT_DP && t.payload_present && !t.payload_crc_ok) begin
if (t.route !== ROUTE_PROTOCOL)
`uvm_error("DP_DROP",
"a Data Packet with a bad payload was dropped -- the protocol layer now cannot say what to re-request")
if (!t.protocol_nak_req)
`uvm_error("RECOVERY", "bad payload did not request a protocol NAK")
n_nak++;
end
// ---- The two recoveries can never both be right ----
if (t.link_retry_req && t.protocol_nak_req)
`uvm_error("EXCLUSIVE",
"both a link retry and a protocol NAK for one packet")
if (t.link_retry_req) n_retry++;
if (t.route === ROUTE_LINK || t.route === ROUTE_PROTOCOL) n_delivered++;
endfunction
function void report_phase(uvm_phase phase);
`uvm_info("SB", $sformatf(
"unknown=%0d reserved=%0d delivered=%0d link-retries=%0d protocol-naks=%0d",
n_unknown, n_reserved, n_delivered, n_retry, n_nak), UVM_LOW)
// A run in which the error paths were never reached proves nothing.
if (n_unknown == 0) `uvm_error("COVERAGE", "no corrupt header was ever seen")
if (n_reserved == 0) `uvm_error("COVERAGE", "no reserved type was ever seen")
if (n_nak == 0) `uvm_error("COVERAGE", "no payload error was ever seen")
endfunction
endclassThose three report_phase checks are the habit worth stealing. A UVM run that reports zero errors because it never reached the interesting states is worse than a failing run, because it is indistinguishable from a good one in a regression summary. Asserting that the error paths were exercised turns "nothing broke" into "the thing that could break was tried."
16.4 Functional coverage
covergroup pkt_decode_cg with function sample(
bit valid, bit [4:0] ht, bit hcrc, bit pp, bit pcrc,
pkt_type_e pt, route_e rt);
cp_type : coverpoint pt {
bins defined[] = {PKT_LMP, PKT_TP, PKT_DP, PKT_ITP};
bins reserved = {PKT_RSVD};
bins unknown = {PKT_UNKNOWN}; // the one that matters
bins idle = {PKT_NONE};
}
cp_route : coverpoint rt { bins all[] = {ROUTE_NONE, ROUTE_LINK,
ROUTE_PROTOCOL, ROUTE_DISCARD}; }
// All 32 encodings individually, not four plus "other".
cp_encoding : coverpoint ht { bins enc[32] = {[0:31]}; }
cp_hcrc : coverpoint hcrc { bins ok = {1}; bins bad = {0}; }
cp_pp : coverpoint pp { bins present = {1}; bins absent = {0}; }
cp_pcrc : coverpoint pcrc { bins ok = {1}; bins bad = {0}; }
// THE cross: every type encoding seen under BOTH CRC outcomes. This is
// what proves the decode was gated rather than merely correct on good
// headers -- and it is 64 bins, all of them reachable. A coverpoint must
// be declared before a cross names it.
x_enc_crc : cross cp_encoding, cp_hcrc;
// A Data Packet under every combination of payload presence and payload
// CRC -- the four-way conjunction that mutation K3 lives in.
x_dp_payload : cross cp_type, cp_pp, cp_pcrc {
ignore_bins non_dp = binsof(cp_type) intersect {PKT_LMP, PKT_TP,
PKT_ITP, PKT_NONE};
}
endgroupx_enc_crc is the coverage goal that corresponds to the design's central rule. Closing it means every one of the 32 type encodings was presented both with a passing header CRC and with a failing one — which is precisely the evidence that the CRC gate was tested, not just the decode.
17. SystemVerilog Assertions
The properties are all single-cycle, because the decode is combinational. That makes them cheap enough to leave enabled in every regression.
// bind this module to the DUT
module usb3_packet_decode_sva
import usb3_packet_pkg::*;
(
input logic clk,
input logic rst_n,
input logic pkt_valid,
input logic [4:0] hdr_type,
input logic hdr_crc_ok,
input logic payload_present,
input logic payload_crc_ok,
input pkt_type_e pkt_type,
input route_e route,
input logic hdr_error,
input logic payload_error,
input logic link_retry_req,
input logic protocol_nak_req,
input logic ack_expected
);
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// ---- 1. THE property. A rejected header is not interpreted. ----
property p_corrupt_header_not_decoded;
(pkt_valid && !hdr_crc_ok) |-> (pkt_type == PKT_UNKNOWN);
endproperty
a_corrupt_header_not_decoded : assert property (p_corrupt_header_not_decoded)
else $error("a header the CRC rejected was decoded as %s", pkt_type.name());
// ---- 2. ...and it reaches nobody. ----
property p_corrupt_header_discarded;
(pkt_valid && !hdr_crc_ok) |-> (route == ROUTE_DISCARD);
endproperty
a_corrupt_header_discarded : assert property (p_corrupt_header_discarded);
// ---- 3. The two recoveries are mutually exclusive. ----
property p_recoveries_exclusive;
not (link_retry_req && protocol_nak_req);
endproperty
a_recoveries_exclusive : assert property (p_recoveries_exclusive)
else $error("a link retry and a protocol NAK for the same packet");
// ---- 4. A payload error implies a trustworthy header. ----
property p_payload_error_needs_header;
payload_error |-> hdr_crc_ok;
endproperty
a_payload_error_needs_header : assert property (p_payload_error_needs_header);
// ---- 5. A reserved type never provokes a retry. ----
property p_reserved_no_retry;
(pkt_type == PKT_RSVD) |-> !link_retry_req;
endproperty
a_reserved_no_retry : assert property (p_reserved_no_retry)
else $error("a retry was requested for a packet that arrived intact");
// ---- 6. A bad payload is delivered, not dropped. ----
property p_bad_payload_still_routed;
(pkt_valid && hdr_crc_ok && pkt_type == PKT_DP
&& payload_present && !payload_crc_ok) |-> (route == ROUTE_PROTOCOL);
endproperty
a_bad_payload_still_routed : assert property (p_bad_payload_still_routed)
else $error("a Data Packet with a recoverable payload error was dropped");
// ---- 7. The timestamp is never acknowledged. ----
property p_itp_never_acked;
(pkt_type == PKT_ITP) |-> !ack_expected;
endproperty
a_itp_never_acked : assert property (p_itp_never_acked);
// ---- 8. LMPs never leave the link layer. ----
property p_lmp_stays_at_link;
(pkt_type == PKT_LMP) |-> (route == ROUTE_LINK);
endproperty
a_lmp_stays_at_link : assert property (p_lmp_stays_at_link);
// ---- Cover: the error paths were actually exercised. ----
c_unknown : cover property ((pkt_valid && !hdr_crc_ok));
c_reserved : cover property ((pkt_type == PKT_RSVD));
c_nak : cover property ((protocol_nak_req));
endmodule
bind usb3_packet_decode usb3_packet_decode_sva u_sva (.*);Icarus Verilog does not support concurrent assertions, so these were checked as procedural conditions inside the testbenches (sections 12.1–12.3, marked "SAFETY PROPERTIES") and are given here in SVA form for use with a commercial simulator or a formal tool. All eight are single-cycle implications with no temporal depth, which makes them excellent formal targets — a property checker will prove or disprove each of them in seconds, over the full 512-point space, without any stimulus at all.
18. Common Misconceptions
"Two CRCs means the header is double-protected." No — the two CRCs cover disjoint byte ranges. The header is covered by exactly one CRC and the payload by exactly one. What is doubled is not the protection but the classification: the receiver learns not just that something was corrupt but which part, and that is what lets two different layers respond.
"A CRC failure means drop the packet." For the header, yes. For the payload, no — dropping it destroys the routing information the protocol layer needs to recover. The packet is delivered with payload_error asserted. This is the single most common error in a first draft of this block.
"A reserved type is an error." It is an unknown, not an error. The bytes arrived exactly as sent. Treating it as an error produces a permanent retry storm against a device that is merely newer than you are. Count it, drop the packet, keep the link up.
"PKT_UNKNOWN is just a debug value." It is the only truthful output for a rejected header, and making it a distinct value is what prevents the rest of the receiver from fabricating a type. In the VHDL build it is a member of an enumerated type, so a downstream case cannot silently ignore it.
"ITP needs an ACK like everything else." An ACK for a timestamp is worthless — by the time it returns, the time it confirmed has passed — and with many devices on the bus the ACK traffic would congest the bus whose timing the ITP exists to distribute. It is broadcast, fire-and-forget.
"Decode order is a style question." It is the difference between a receiver that discards corrupt packets and one that delivers them to arbitrary endpoints. Mutation K1 measures the gap: 16 700 failing checks from moving two lines.
"The header CRC makes the payload CRC unnecessary for short packets." The two cover different bytes. A payload of one byte is still entirely outside the header CRC's domain, and a bit error in it is invisible to the header check.
19. Exercises
1. Swap the order of the !hdr_crc_ok test and the hdr_type comparisons in the Verilog design, and run the Verilog testbench. Before you run it, predict which of the seven model checks and five safety properties will fail. (Answer: the count is 16 740 — see if you can explain the magnitude, not just the sign.)
2. Widen hdr_type to six bits, making 64 encodings of which four are still defined. Update the exhaustive sweep and confirm it still enumerates the complete space. Which reach counter changes the most, and why is that the expected answer for a forward-compatible receiver?
3. Add an output retry_count that saturates at 3 and forces route to ROUTE_DISCARD with no further retries once saturated — a real receiver must not retry forever. Then write the safety property that a saturated receiver never asserts link_retry_req, and a mutation that breaks it.
4. Mutation K7 saturates at 40 258 in all three languages. Restructure the delivered check so that the counter is compared differentially (did it change by the right amount this cycle?) rather than absolutely, re-run K7, and explain why the new number is both smaller and more informative.
5. The c_payload_only_dp UVM constraint models legal traffic and thereby hides mutation K5. Write a coverage assertion that fails if a regression ever runs without payload_present being 1 on a non-DP packet at least once — i.e. make the hole in the constraint visible in the coverage report rather than silent.
6. Take the eight SVA properties to a formal tool and prove them over the unconstrained input space. Then remove the hdr_crc_ok term from payload_error (the deliberate local redundancy from section 8) and see which property still catches it. Does the design remain correct? Does it remain locally readable?
20. Summary
| Idea | Why it matters |
|---|---|
| Four packet types: LMP, TP, DP, ITP | only DP carries data; LMP never leaves the link layer |
| 14-byte header, own CRC-16 | the packet's identity is protected separately from its contents |
| Payload, own CRC-32 | a data error and an identity error are different events |
Header CRC bad → PKT_UNKNOWN | the type field must not be read; nothing can be reported upward |
| Header CRC bad → link retry | only the link layer has enough information to act |
| Payload CRC bad → protocol NAK | the good header names the endpoint to re-ask |
| The two recoveries are exclusive | by construction: one needs a header the other lacks |
| A bad payload is delivered, flagged | dropping it destroys the recovery information |
| 28 reserved encodings → discard, no retry | the bytes were intact; a retry returns them unchanged |
| ITP is never acknowledged | a confirmed timestamp is already stale |
| 512-point exhaustive verification | the complete decision surface, every reserved encoding |
| 7 mutations, all killed in 3 languages | including the decode-order bug at ~16 700 failures |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o pk_v.out pk_v.v pk_v_tb.v && ./pk_v.out |
| SystemVerilog | iverilog -g2012 -o pk_sv.out pk_sv.sv pk_sv_tb.sv && ./pk_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a pk_vhdl.vhd pk_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_pk_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_pk_vhdl |
| One mutation | iverilog -g2005 -DMUT_K1 -o mm pk_v_mut.v pk_v_tb.v && ./mm |
All three implementations pass with 0 errors: 512 of 512 exhaustive points, 40 000 randomised cycles, and every one of the seven pkt_type outcomes and four route outcomes reached and asserted reached.
Chapter 20.5 — USB 3.x vs USB 2.0 Differences closes the module by putting the two buses side by side. Not as a feature table — as a question a real driver has to answer: given a device operating at some speed, which mechanism applies? Polling or credits. NAK or ERDY. A 100 mA unit load or a 150 mA one. The answer is not "the newer one", because a SuperSpeed-capable device connected through a USB 2 hub is a USB 2 device, and a receiver that assumes otherwise breaks in exactly the way Chapter 20.2 warned about.
Continue learning
Related tutorials
- Related topic
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.
- 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
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 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.
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.
