USB · Module 21
Descriptor Engine
wLength is the size of the host's buffer, not a preference — and whether a zero-length packet must follow depends on comparing what was sent against what was asked for, not against what exists.
Chapter 21.2 established that a short packet terminates a transfer and that a zero-length packet is the terminator when the data ends on a boundary. This chapter is where that rule meets a number the host chose.
1. wLength Is a Promise About a Buffer
Every GET_DESCRIPTOR request carries a wLength field. It is easy to read it as "how much of the descriptor I want", which makes it sound like a preference. It is not a preference.
wLength is the size of the buffer the host has allocated. Sending more than that is a buffer overflow in the host's driver — in kernel memory, on the machine your device is plugged into.
And the host genuinely does ask for less than the whole thing, routinely, as part of ordinary enumeration:
GET_DESCRIPTOR(CONFIGURATION, wLength = 9)
the host does not yet know how long a configuration
descriptor is. It asks for the 9-byte header.
... reads bLength and wTotalLength out of those 9 bytes ...
GET_DESCRIPTOR(CONFIGURATION, wLength = 64)
now it knows, and asks for all of it.That first request is a 9-byte buffer. A device that ignores wLength and always sends its full 32-byte configuration descriptor writes 32 bytes into it.
So the first half of this block is one line:
send_len = min(wLength, descriptor length)2. Knowing When to Stop
Now the harder half. A control read's data stage ends when either of two things happens:
| Condition | Who observes it | |
|---|---|---|
| 1 | the device has sent exactly wLength bytes | both ends, by counting |
| 2 | the device sends a short packet — fewer than wMaxPacketSize, and zero counts | the host, in-band |
Either one terminates the transfer. The device must make sure at least one of them occurs, and that is not automatic.
Consider: the host asks for 64 bytes. The descriptor is 32 bytes. The endpoint's wMaxPacketSize is 32.
- The device sends 32 bytes. That is one full packet — not short, so condition 2 does not fire.
- 32 is not 64, so condition 1 does not fire either.
- Nothing has terminated the transfer. The host is still waiting.
The device has to append a zero-length packet, which is short by definition, and the transfer ends.
zlp_required = (send_len < wLength) && (send_len % mps == 0)Both terms are load-bearing, and dropping either produces a real, shipped bug:
| Drop this term | What happens |
|---|---|
send_len < wLength | a ZLP after a transfer that already ended by count — an extra packet the host did not ask for, which it sees at the start of the next transfer |
send_len % mps == 0 | a ZLP after a short packet, which already terminated the transfer — same symptom |
| both | the multiple-of-mps truncated read hangs, exactly as in §2's example |
Four requests for the same 32-byte descriptor, and the four different endings
3. An Unknown Descriptor Is Not an Empty One
The third structural decision. A host asks for descriptors the device may not have — a DEVICE_QUALIFIER on a full-speed-only device, a BOS descriptor on a USB 2 device, string index 7 when there are three strings.
That is not an error. It is how a host discovers what a device supports.
The correct answer is a STALL, which the host reads as "this device does not have that". The wrong answer — and it is a tempting one, because it makes the RTL simpler — is to return a descriptor of length zero:
| STALL | length-0 descriptor | |
|---|---|---|
| what the host concludes | "not supported" | "supported, and empty" |
| what the host does next | moves on | may retry, or mis-configure |
| distinguishable from a real empty descriptor? | yes | no |
A device that answers every request with an empty descriptor enumerates successfully and then behaves oddly in ways that depend entirely on which host is asking. Mutation G4 in §12.
4. What We Are Building
usb_descriptor_engine
request answer
------- ------
req_valid found we have it
desc_type [2:0] stall_req we do not
w_length [7:0] desc_len how long it really is
mps_sel [1:0] send_len how much we will send
truncated send_len < desc_len
zlp_required
n_packets including the ZLP
mps
terminator NONE / COUNT / SHORT / ZLP
n_requests / n_stalls / n_truncated / n_zlpterminator is worth a note. It names which of the three endings applies — and, crucially, it has a value for none of them:
terminator | meaning |
|---|---|
END_BY_COUNT | the byte count reached wLength |
END_BY_SHORT | the last packet was shorter than mps |
END_BY_ZLP | an empty packet was appended |
END_NONE | nothing ends this transfer — the host will hang |
END_NONE is the whole reason the output exists. A hang is the absence of an event, and absences do not show up on a waveform. Giving the absence a name and an encoding turns "the transfer never ended" from something you infer after three hours into something the cursor tells you.
5. Verilog-2005 Implementation
// usb_descriptor_engine -- serving a descriptor against a length the HOST
// chose, and the one-line rule that decides whether a zero-length packet has
// to follow it.
//
// THE HOST SAYS HOW MUCH IT WANTS
//
// Every GET_DESCRIPTOR request carries a wLength field: "send me at most this
// many bytes". It is not a request for the whole descriptor. It is a promise
// about the size of the buffer the host has allocated, and exceeding it is a
// buffer overflow in the host's driver.
//
// Enumeration depends on this. A host does not know how long a configuration
// descriptor is until it has read one, so it asks TWICE:
//
// GET_DESCRIPTOR(CONFIG, wLength = 9) <- just the header, please
// ... reads bLength and wTotalLength from the 9 bytes ...
// GET_DESCRIPTOR(CONFIG, wLength = 64) <- now the whole thing
//
// So a device that ignores wLength and always sends the full descriptor does
// not merely waste bandwidth. On the first of those two requests it sends 64
// bytes into a 9-byte buffer.
//
// send_len = min(wLength, descriptor length)
//
// THE HARD PART IS KNOWING WHEN TO STOP
//
// A control read's data stage ends when EITHER of two things happens:
//
// 1. the device has sent exactly wLength bytes, or
// 2. the device sends a SHORT packet -- fewer than wMaxPacketSize bytes,
// and a zero-length packet counts (chapter 21.2)
//
// Condition 1 is a count both ends can do. Condition 2 is the in-band marker.
// Either one terminates the transfer, and the device must make sure at least
// one of them occurs -- otherwise the host waits for data that will never
// come, which is the enumeration hang every USB developer meets once.
//
// So a ZLP is required exactly when the device sent LESS than the host asked
// for AND the amount it sent was a whole number of packets:
//
// zlp_required = (send_len < wLength) && (send_len % mps == 0)
//
// Both terms are load-bearing, and dropping either produces a real bug:
//
// drop (send_len < wLength) -> a ZLP after a transfer that already ended
// by count. The host sees an extra packet it
// did not ask for, on the next transfer.
//
// drop (send_len % mps == 0) -> a ZLP after a SHORT packet, which already
// terminated the transfer. Same symptom.
//
// drop the whole thing -> the multiple-of-mps truncated read hangs.
//
// Note the boundary that catches people: send_len = 0 with wLength > 0 is
// aligned (0 % mps == 0) and less than wLength, so it DOES need a ZLP -- the
// device sends a single empty packet. But send_len = 0 with wLength = 0 needs
// nothing at all, because there is no data stage to terminate.
module usb_descriptor_engine (
input wire clk,
input wire rst_n,
input wire req_valid, // a GET_DESCRIPTOR arrived
input wire [2:0] desc_type, // which descriptor
input wire [7:0] w_length, // how much the host is willing to take
input wire [1:0] mps_sel, // 0..3 -> wMaxPacketSize 8 / 16 / 32 / 64
output wire found, // we have that descriptor
output wire stall_req, // we do not: the request must be STALLed
output wire [7:0] desc_len, // how long it actually is
output wire [7:0] send_len, // how much we will send
output wire truncated, // send_len < desc_len
output wire zlp_required,
output wire [7:0] n_packets, // including the ZLP if there is one
output wire [7:0] mps,
output wire [1:0] terminator, // WHICH of the three ends this transfer
output reg [31:0] n_requests,
output reg [31:0] n_stalls,
output reg [31:0] n_truncated,
output reg [31:0] n_zlp
);
// A tiny descriptor ROM. Types 0 and 6 are not descriptors this device
// has, which is a THIRD outcome distinct from "found and empty".
// Lengths chosen so that several land on exact packet boundaries.
function [7:0] len_of;
input [2:0] t;
begin
case (t)
3'd1: len_of = 8'd18; // DEVICE
3'd2: len_of = 8'd32; // CONFIGURATION (exactly 32)
3'd3: len_of = 8'd4; // STRING 0 (language IDs)
3'd4: len_of = 8'd16; // STRING 1 (exactly 16)
3'd5: len_of = 8'd10; // DEVICE_QUALIFIER
3'd7: len_of = 8'd64; // BOS (exactly 64)
default: len_of = 8'd0; // not a descriptor we have
endcase
end
endfunction
function have;
input [2:0] t;
begin
have = (t != 3'd0) && (t != 3'd6);
end
endfunction
assign found = req_valid && have(desc_type);
// An unknown descriptor is NOT "a descriptor of length zero". It is a
// request the device cannot answer, and the protocol's way of saying so is
// a STALL -- which the host uses to discover optional features.
assign stall_req = req_valid && !have(desc_type);
assign desc_len = found ? len_of(desc_type) : 8'd0;
// wMaxPacketSize is always a power of two, so the alignment test is a mask
// and the packet count is a shift. Real controllers rely on this; a design
// that needs a divider here has chosen the wrong representation.
assign mps = 8'd8 << mps_sel;
wire [7:0] mps_mask = mps - 8'd1;
// THE truncation. Send the smaller of what was asked for and what exists.
assign send_len = !found ? 8'd0
: (w_length < desc_len) ? w_length
: desc_len;
assign truncated = found && (send_len < desc_len);
// Was the amount we sent a whole number of packets? A zero-length send is
// aligned, which is exactly why a truncated-to-nothing read still needs a
// ZLP to terminate it.
wire aligned = ((send_len & mps_mask) == 8'd0);
// THE RULE. Both terms matter -- see the header comment.
assign zlp_required = found && (send_len < w_length) && aligned;
// Whole packets, plus a partial one if the send did not end on a boundary,
// plus the terminating ZLP if one is required.
wire [7:0] whole_packets = send_len >> (3 + mps_sel);
assign n_packets = whole_packets
+ (aligned ? 8'd0 : 8'd1)
+ (zlp_required ? 8'd1 : 8'd0);
// WHICH of the three terminators applies. Exactly one must, whenever a
// descriptor was found and anything is going to be sent -- and naming the
// "none of them applies" case is what turns a silent host hang into
// something a waveform shows you.
localparam [1:0] END_NONE = 2'd0, // no data stage at all
END_BY_COUNT = 2'd1, // wLength bytes were sent
END_BY_SHORT = 2'd2, // the last packet was short
END_BY_ZLP = 2'd3; // a zero-length packet was appended
assign terminator = !found ? END_NONE
: zlp_required ? END_BY_ZLP
: !aligned ? END_BY_SHORT
: (send_len == w_length) ? END_BY_COUNT
: END_NONE;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_requests <= 32'd0;
n_stalls <= 32'd0;
n_truncated <= 32'd0;
n_zlp <= 32'd0;
end else if (req_valid) begin
n_requests <= n_requests + 32'd1;
if (stall_req) n_stalls <= n_stalls + 32'd1;
if (truncated) n_truncated <= n_truncated + 32'd1;
if (zlp_required) n_zlp <= n_zlp + 32'd1;
end
end
endmodule6. SystemVerilog Implementation
// usb_descriptor_engine -- serving a descriptor against a length the HOST
// chose, and the one-line rule that decides whether a zero-length packet has
// to follow it.
//
// The SystemVerilog build names the three ways a data stage can end. See the
// comment above `terminator` near the bottom: making END_NONE a value the
// design can produce is what turns "nothing terminates this transfer" from a
// silent hang into something a waveform shows you.
//
// THE HOST SAYS HOW MUCH IT WANTS
//
// Every GET_DESCRIPTOR request carries a wLength field: "send me at most this
// many bytes". It is not a request for the whole descriptor. It is a promise
// about the size of the buffer the host has allocated, and exceeding it is a
// buffer overflow in the host's driver.
//
// Enumeration depends on this. A host does not know how long a configuration
// descriptor is until it has read one, so it asks TWICE:
//
// GET_DESCRIPTOR(CONFIG, wLength = 9) <- just the header, please
// ... reads bLength and wTotalLength from the 9 bytes ...
// GET_DESCRIPTOR(CONFIG, wLength = 64) <- now the whole thing
//
// So a device that ignores wLength and always sends the full descriptor does
// not merely waste bandwidth. On the first of those two requests it sends 64
// bytes into a 9-byte buffer.
//
// send_len = min(wLength, descriptor length)
//
// THE HARD PART IS KNOWING WHEN TO STOP
//
// A control read's data stage ends when EITHER of two things happens:
//
// 1. the device has sent exactly wLength bytes, or
// 2. the device sends a SHORT packet -- fewer than wMaxPacketSize bytes,
// and a zero-length packet counts (chapter 21.2)
//
// Condition 1 is a count both ends can do. Condition 2 is the in-band marker.
// Either one terminates the transfer, and the device must make sure at least
// one of them occurs -- otherwise the host waits for data that will never
// come, which is the enumeration hang every USB developer meets once.
//
// So a ZLP is required exactly when the device sent LESS than the host asked
// for AND the amount it sent was a whole number of packets:
//
// zlp_required = (send_len < wLength) && (send_len % mps == 0)
//
// Both terms are load-bearing, and dropping either produces a real bug:
//
// drop (send_len < wLength) -> a ZLP after a transfer that already ended
// by count. The host sees an extra packet it
// did not ask for, on the next transfer.
//
// drop (send_len % mps == 0) -> a ZLP after a SHORT packet, which already
// terminated the transfer. Same symptom.
//
// drop the whole thing -> the multiple-of-mps truncated read hangs.
//
// Note the boundary that catches people: send_len = 0 with wLength > 0 is
// aligned (0 % mps == 0) and less than wLength, so it DOES need a ZLP -- the
// device sends a single empty packet. But send_len = 0 with wLength = 0 needs
// nothing at all, because there is no data stage to terminate.
package usb_desc_pkg;
// The descriptor types this device serves. DESC_NONE and DESC_RSVD are not
// gaps in the table -- they are requests the device answers with a STALL,
// which is a THIRD outcome distinct from "found, and empty".
typedef enum logic [2:0] {
DESC_NONE = 3'd0, // not a descriptor: STALL
DESC_DEVICE = 3'd1,
DESC_CONFIG = 3'd2,
DESC_STR0 = 3'd3,
DESC_STR1 = 3'd4,
DESC_QUAL = 3'd5,
DESC_RSVD = 3'd6, // not a descriptor: STALL
DESC_BOS = 3'd7
} desc_type_e;
// How a control read's data stage ENDS. Naming these is the point of the
// chapter: exactly one of them must apply, or the host hangs.
typedef enum logic [1:0] {
END_NONE = 2'd0, // no data stage at all
END_BY_COUNT = 2'd1, // wLength bytes were sent
END_BY_SHORT = 2'd2, // the last packet was shorter than mps
END_BY_ZLP = 2'd3 // a zero-length packet was appended
} term_e;
endpackage
module usb_descriptor_engine
import usb_desc_pkg::*;
(
input logic clk,
input logic rst_n,
input logic req_valid, // a GET_DESCRIPTOR arrived
input desc_type_e desc_type, // which descriptor
input logic [7:0] w_length, // how much the host is willing to take
input logic [1:0] mps_sel, // 0..3 -> wMaxPacketSize 8 / 16 / 32 / 64
output logic found, // we have that descriptor
output logic stall_req, // we do not: the request must be STALLed
output logic [7:0] desc_len, // how long it actually is
output logic [7:0] send_len, // how much we will send
output logic truncated, // send_len < desc_len
output logic zlp_required,
output logic [7:0] n_packets, // including the ZLP if there is one
output logic [7:0] mps,
output term_e terminator, // WHICH of the three ends this transfer
output logic [31:0] n_requests,
output logic [31:0] n_stalls,
output logic [31:0] n_truncated,
output logic [31:0] n_zlp
);
// A tiny descriptor ROM. Types 0 and 6 are not descriptors this device
// has, which is a THIRD outcome distinct from "found and empty".
// Lengths chosen so that several land on exact packet boundaries.
function automatic logic [7:0] len_of(desc_type_e t);
case (t)
DESC_DEVICE: return 8'd18;
DESC_CONFIG: return 8'd32; // exactly 32
DESC_STR0: return 8'd4;
DESC_STR1: return 8'd16; // exactly 16
DESC_QUAL: return 8'd10;
DESC_BOS: return 8'd64; // exactly 64
default: return 8'd0; // not a descriptor we have
endcase
endfunction
function automatic bit have(desc_type_e t);
return (t != DESC_NONE) && (t != DESC_RSVD);
endfunction
assign found = req_valid && have(desc_type);
// An unknown descriptor is NOT "a descriptor of length zero". It is a
// request the device cannot answer, and the protocol's way of saying so is
// a STALL -- which the host uses to discover optional features.
assign stall_req = req_valid && !have(desc_type);
assign desc_len = found ? len_of(desc_type) : 8'd0;
// wMaxPacketSize is always a power of two, so the alignment test is a mask
// and the packet count is a shift. Real controllers rely on this; a design
// that needs a divider here has chosen the wrong representation.
assign mps = 8'd8 << mps_sel;
logic [7:0] mps_mask;
assign mps_mask = mps - 8'd1;
// THE truncation. Send the smaller of what was asked for and what exists.
assign send_len = !found ? 8'd0
: (w_length < desc_len) ? w_length
: desc_len;
assign truncated = found && (send_len < desc_len);
// Was the amount we sent a whole number of packets? A zero-length send is
// aligned, which is exactly why a truncated-to-nothing read still needs a
// ZLP to terminate it.
logic aligned;
assign aligned = ((send_len & mps_mask) == 8'd0);
// THE RULE. Both terms matter -- see the header comment.
assign zlp_required = found && (send_len < w_length) && aligned;
// Whole packets, plus a partial one if the send did not end on a boundary,
// plus the terminating ZLP if one is required.
logic [7:0] whole_packets;
assign whole_packets = send_len >> (3 + mps_sel);
assign n_packets = whole_packets
+ (aligned ? 8'd0 : 8'd1)
+ (zlp_required ? 8'd1 : 8'd0);
// WHICH of the three terminators applies. Exactly one must, whenever a
// descriptor was found and anything is going to be sent -- and stating it
// as an enumeration rather than as three booleans is what makes "none of
// them applies" a visible value instead of a silent hang.
always_comb begin
if (!found) terminator = END_NONE;
else if (zlp_required) terminator = END_BY_ZLP;
else if (!aligned) terminator = END_BY_SHORT;
else if (send_len == w_length) terminator = END_BY_COUNT;
else terminator = END_NONE;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_requests <= '0;
n_stalls <= '0;
n_truncated <= '0;
n_zlp <= '0;
end else if (req_valid) begin
n_requests <= n_requests + 1;
if (stall_req) n_stalls <= n_stalls + 1;
if (truncated) n_truncated <= n_truncated + 1;
if (zlp_required) n_zlp <= n_zlp + 1;
end
end
endmodule7. VHDL-2008 Implementation
-- usb_descriptor_engine -- serving a descriptor against a length the HOST
-- chose, and the one-line rule that decides whether a zero-length packet has
-- to follow it.
--
-- THE HOST SAYS HOW MUCH IT WANTS
--
-- Every GET_DESCRIPTOR request carries a wLength field: "send me at most this
-- many bytes". It is not a request for the whole descriptor. It is a promise
-- about the size of the buffer the host has allocated, and exceeding it is a
-- buffer overflow in the host's driver.
--
-- Enumeration depends on this. A host does not know how long a configuration
-- descriptor is until it has read one, so it asks TWICE:
--
-- GET_DESCRIPTOR(CONFIG, wLength = 9) <- just the header, please
-- ... reads bLength and wTotalLength from the 9 bytes ...
-- GET_DESCRIPTOR(CONFIG, wLength = 64) <- now the whole thing
--
-- A device that ignores wLength and always sends the full descriptor does not
-- merely waste bandwidth: on the first of those two requests it sends 64
-- bytes into a 9-byte buffer.
--
-- send_len = min(wLength, descriptor length)
--
-- THE HARD PART IS KNOWING WHEN TO STOP
--
-- A control read's data stage ends when EITHER of two things happens:
--
-- 1. the device has sent exactly wLength bytes, or
-- 2. the device sends a SHORT packet -- fewer than wMaxPacketSize bytes,
-- and a zero-length packet counts (chapter 21.2)
--
-- Either one terminates the transfer, and the device must ensure at least one
-- occurs -- otherwise the host waits for data that never comes, which is the
-- enumeration hang every USB developer meets once.
--
-- zlp_required = (send_len < wLength) and (send_len mod mps = 0)
--
-- Both terms are load-bearing:
--
-- drop (send_len < wLength) -> a ZLP after a transfer that already ended
-- by count: an extra packet the host did
-- not ask for, seen on the NEXT transfer.
--
-- drop (send_len mod mps = 0) -> a ZLP after a SHORT packet, which already
-- terminated the transfer. Same symptom.
--
-- drop the whole thing -> the multiple-of-mps truncated read hangs.
--
-- Note the boundary that catches people: send_len = 0 with wLength > 0 is
-- aligned and less than wLength, so it DOES need a ZLP -- a single empty
-- packet. But send_len = 0 with wLength = 0 needs nothing at all, because
-- there is no data stage to terminate.
--
-- VHDL names both the descriptor set and the three ways a data stage can end,
-- which is what makes END_NONE -- "nothing terminates this transfer" -- a
-- value the design can be caught producing rather than a silent hang.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package usb_desc_pkg is
type desc_type_t is (
DESC_NONE, -- not a descriptor: STALL
DESC_DEVICE,
DESC_CONFIG,
DESC_STR0,
DESC_STR1,
DESC_QUAL,
DESC_RSVD, -- not a descriptor: STALL
DESC_BOS
);
type term_t is (
END_NONE, -- no data stage at all
END_BY_COUNT, -- wLength bytes were sent
END_BY_SHORT, -- the last packet was shorter than mps
END_BY_ZLP -- a zero-length packet was appended
);
function desc_decode(t : std_logic_vector(2 downto 0)) return desc_type_t;
function term_code(t : term_t) return std_logic_vector;
function len_of(t : desc_type_t) return natural;
function have(t : desc_type_t) return boolean;
end package;
package body usb_desc_pkg is
function desc_decode(t : std_logic_vector(2 downto 0)) return desc_type_t is
begin
return desc_type_t'val(to_integer(unsigned(t)));
end function;
function term_code(t : term_t) return std_logic_vector is
begin
return std_logic_vector(to_unsigned(term_t'pos(t), 2));
end function;
-- Lengths chosen so that several land on exact packet boundaries.
function len_of(t : desc_type_t) return natural is
begin
case t is
when DESC_DEVICE => return 18;
when DESC_CONFIG => return 32; -- exactly 32
when DESC_STR0 => return 4;
when DESC_STR1 => return 16; -- exactly 16
when DESC_QUAL => return 10;
when DESC_BOS => return 64; -- exactly 64
when others => return 0; -- not a descriptor we have
end case;
end function;
function have(t : desc_type_t) return boolean is
begin
return (t /= DESC_NONE) and (t /= DESC_RSVD);
end function;
end package body;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_desc_pkg.all;
entity usb_descriptor_engine is
port (
clk : in std_logic;
rst_n : in std_logic;
req_valid : in std_logic; -- a GET_DESCRIPTOR
desc_type : in std_logic_vector(2 downto 0);
w_length : in std_logic_vector(7 downto 0); -- what the host will take
mps_sel : in std_logic_vector(1 downto 0); -- mps = 8 / 16 / 32 / 64
found : out std_logic;
stall_req : out std_logic;
desc_len : out std_logic_vector(7 downto 0);
send_len : out std_logic_vector(7 downto 0);
truncated : out std_logic;
zlp_required : out std_logic;
n_packets : out std_logic_vector(7 downto 0);
mps : out std_logic_vector(7 downto 0);
terminator : out std_logic_vector(1 downto 0);
n_requests : out std_logic_vector(31 downto 0);
n_stalls : out std_logic_vector(31 downto 0);
n_truncated : out std_logic_vector(31 downto 0);
n_zlp : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of usb_descriptor_engine is
signal dt : desc_type_t;
signal fnd : std_logic;
signal stl : std_logic;
signal dlen : natural range 0 to 255 := 0;
signal slen : natural range 0 to 255 := 0;
signal wlen : natural range 0 to 255 := 0;
signal mps_i : natural range 0 to 255 := 8;
signal aligned : boolean := true;
signal zlp : std_logic;
signal trunc : std_logic;
signal npkt : natural range 0 to 255 := 0;
signal term : term_t;
signal req_r, stl_r, tr_r, zlp_r : unsigned(31 downto 0) := (others => '0');
begin
dt <= desc_decode(desc_type);
wlen <= to_integer(unsigned(w_length));
fnd <= '1' when (req_valid = '1' and have(dt)) else '0';
-- An unknown descriptor is NOT "a descriptor of length zero". It is a
-- request the device cannot answer, and the protocol's way of saying so is
-- a STALL -- which is how a host discovers optional features.
stl <= '1' when (req_valid = '1' and not have(dt)) else '0';
dlen <= len_of(dt) when fnd = '1' else 0;
-- wMaxPacketSize is always a power of two, so the alignment test is a mask
-- and the packet count is a shift. A design that needs a divider here has
-- chosen the wrong representation.
mps_i <= 8 * (2 ** to_integer(unsigned(mps_sel)));
-- THE truncation. Send the smaller of what was asked for and what exists.
minimum : process (fnd, wlen, dlen)
begin
if fnd = '0' then
slen <= 0;
elsif wlen < dlen then
slen <= wlen;
else
slen <= dlen;
end if;
end process;
trunc <= '1' when (fnd = '1' and slen < dlen) else '0';
-- Was the amount we sent a whole number of packets? A zero-length send is
-- aligned, which is exactly why a truncated-to-nothing read still needs a
-- ZLP to terminate it.
aligned <= (slen mod mps_i) = 0;
-- THE RULE. Both terms matter -- see the header comment.
zlp <= '1' when (fnd = '1' and slen < wlen and aligned) else '0';
-- Whole packets, plus a partial one if the send did not end on a boundary,
-- plus the terminating ZLP if one is required.
packets : process (slen, mps_i, aligned, zlp)
variable p : natural range 0 to 255;
begin
p := slen / mps_i;
if not aligned then
p := p + 1;
end if;
if zlp = '1' then
p := p + 1;
end if;
npkt <= p;
end process;
-- WHICH of the three terminators applies. Exactly one must, whenever a
-- descriptor was found -- and naming the "none of them applies" case is
-- what turns a silent host hang into something a waveform shows you.
which_end : process (fnd, zlp, aligned, slen, wlen)
begin
if fnd = '0' then
term <= END_NONE;
elsif zlp = '1' then
term <= END_BY_ZLP;
elsif not aligned then
term <= END_BY_SHORT;
elsif slen = wlen then
term <= END_BY_COUNT;
else
term <= END_NONE;
end if;
end process;
found <= fnd;
stall_req <= stl;
desc_len <= std_logic_vector(to_unsigned(dlen, 8));
send_len <= std_logic_vector(to_unsigned(slen, 8));
truncated <= trunc;
zlp_required <= zlp;
n_packets <= std_logic_vector(to_unsigned(npkt, 8));
mps <= std_logic_vector(to_unsigned(mps_i, 8));
terminator <= term_code(term);
regs : process (clk, rst_n)
begin
if rst_n = '0' then
req_r <= (others => '0');
stl_r <= (others => '0');
tr_r <= (others => '0');
zlp_r <= (others => '0');
elsif rising_edge(clk) then
if req_valid = '1' then
req_r <= req_r + 1;
if stl = '1' then
stl_r <= stl_r + 1;
end if;
if trunc = '1' then
tr_r <= tr_r + 1;
end if;
if zlp = '1' then
zlp_r <= zlp_r + 1;
end if;
end if;
end if;
end process;
n_requests <= std_logic_vector(req_r);
n_stalls <= std_logic_vector(stl_r);
n_truncated <= std_logic_vector(tr_r);
n_zlp <= std_logic_vector(zlp_r);
end architecture;8. Seeing the Rule Fire
Four requests, four endings
usb_descriptor_engine — the four ways a data stage ends
10 cycles9. The Testbenches
Each suite sweeps the entire decision surface — 8 descriptor types × 80 wLength values (0…79, past the longest descriptor) × 4 packet sizes = 2560 points — then runs the eight-step enumeration sequence a real host performs, then 40 000 randomised requests against a reference model that counts packets in a loop where the design shifts, and takes the minimum with an if where the design uses a ternary.
9.1 Verilog testbench
`timescale 1ns/1ps
module tb_de_v;
reg clk=0, rst_n=0;
reg req_valid=0;
reg [2:0] desc_type=0;
reg [7:0] w_length=0;
reg [1:0] mps_sel=3;
wire found, stall_req, truncated, zlp_required;
wire [7:0] desc_len, send_len, n_packets, mps;
wire [1:0] terminator;
wire [31:0] n_requests, n_stalls, n_truncated, n_zlp;
always #5 clk=~clk;
usb_descriptor_engine dut (
.clk(clk), .rst_n(rst_n), .req_valid(req_valid), .desc_type(desc_type),
.w_length(w_length), .mps_sel(mps_sel), .found(found),
.stall_req(stall_req), .desc_len(desc_len), .send_len(send_len),
.truncated(truncated), .zlp_required(zlp_required),
.n_packets(n_packets), .mps(mps), .terminator(terminator),
.n_requests(n_requests),
.n_stalls(n_stalls), .n_truncated(n_truncated), .n_zlp(n_zlp));
integer errors=0, i, t, w, m;
integer n_exh=0;
integer n_found=0, n_stall=0, n_trunc=0, n_zlpreq=0, n_exact=0, n_partial=0;
integer n_bytype [0:7];
integer m_req, m_stall, m_trunc, m_zlp;
task check(input cond, input [639:0] msg);
begin if (!cond) begin errors=errors+1;
if (errors <= 25)
$display(" FAIL: %0s (type=%0d wLen=%0d mps=%0d | found=%b dlen=%0d slen=%0d trunc=%b zlp=%b npkt=%0d, t=%0t)",
msg, desc_type, w_length, mps, found, desc_len, send_len,
truncated, zlp_required, n_packets, $time);
end end
endtask
// ---- independent reference model. It computes the packet count by
// ---- COUNTING packets in a loop where the design uses a shift, and takes
// ---- the minimum with an if where the design uses a ternary.
task model(output e_found, output e_stall, output [7:0] e_dlen,
output [7:0] e_slen, output e_trunc, output e_zlp,
output [7:0] e_npkt);
integer real_len, mps_i, sent, pkts;
begin
case (desc_type)
3'd1: real_len = 18;
3'd2: real_len = 32;
3'd3: real_len = 4;
3'd4: real_len = 16;
3'd5: real_len = 10;
3'd7: real_len = 64;
default: real_len = -1; // not a descriptor we have
endcase
mps_i = 8 << mps_sel;
e_found = req_valid && (real_len >= 0);
e_stall = req_valid && (real_len < 0);
e_dlen = e_found ? real_len[7:0] : 8'd0;
if (!e_found) sent = 0;
else if (w_length < real_len) sent = w_length;
else sent = real_len;
e_slen = sent[7:0];
e_trunc = e_found && (sent < real_len);
// The rule, restated: a ZLP is needed when we sent less than was asked
// for and what we sent was a whole number of packets.
e_zlp = e_found && (sent < w_length) && ((sent % mps_i) == 0);
// Count the packets one at a time -- a different route from the
// design's shift-and-add.
pkts = 0;
begin : countloop
integer left;
left = sent;
while (left >= mps_i) begin
pkts = pkts + 1;
left = left - mps_i;
end
if (left > 0) pkts = pkts + 1;
end
if (e_zlp) pkts = pkts + 1;
e_npkt = pkts[7:0];
end
endtask
localparam [1:0] END_NONE=0, END_BY_COUNT=1, END_BY_SHORT=2, END_BY_ZLP=3;
task check_comb;
reg e_found, e_stall, e_trunc, e_zlp;
reg [7:0] e_dlen, e_slen, e_npkt;
reg [1:0] e_term;
integer mps_i;
begin
model(e_found, e_stall, e_dlen, e_slen, e_trunc, e_zlp, e_npkt);
mps_i = 8 << mps_sel;
check(found === e_found, "found matches the model");
check(stall_req === e_stall, "stall_req matches the model");
check(desc_len === e_dlen, "desc_len matches the model");
check(send_len === e_slen, "send_len matches the model");
check(truncated === e_trunc, "truncated matches the model");
check(zlp_required === e_zlp, "zlp_required matches the model");
check(n_packets === e_npkt, "n_packets matches the model");
check(mps === mps_i[7:0], "mps decodes from mps_sel");
// The terminator, computed by the model as a flat if-chain.
if (!e_found) e_term = END_NONE;
else if (e_zlp) e_term = END_BY_ZLP;
else if ((e_slen % mps_i) != 0) e_term = END_BY_SHORT;
else if (e_slen == w_length) e_term = END_BY_COUNT;
else e_term = END_NONE;
check(terminator === e_term, "terminator matches the model");
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE property. Never send the host more than it asked for. This is
// a buffer overflow in the host driver, not a protocol nicety.
check(send_len <= w_length,
"more bytes were offered than the host asked for -- host buffer overflow");
// 2. And never more than the descriptor actually has.
check(send_len <= desc_len,
"more bytes were offered than the descriptor contains");
// 3. found and stall_req are mutually exclusive, and one of them holds
// whenever a request is present.
check(!(found && stall_req), "found and stall_req both asserted");
if (req_valid) check(found || stall_req,
"a request produced neither a descriptor nor a STALL");
if (!req_valid) check(!found && !stall_req,
"an output asserted with no request");
// 4. A transfer must terminate: either we sent exactly what was asked
// for, or the last packet is short, or a ZLP follows. If none of the
// three holds, the host waits for ever.
if (found)
check((send_len == w_length)
|| ((send_len % mps_i) != 0)
|| zlp_required,
"nothing terminates this transfer -- the host will hang");
// 4b. ...restated through the named terminator: a found descriptor
// always has one, and it agrees with the booleans.
if (found)
check(terminator !== END_NONE,
"a descriptor was found with no way to end its data stage");
check((terminator === END_BY_ZLP) === zlp_required,
"terminator disagrees with zlp_required");
// 5. A ZLP is never sent when the byte count already ended the
// transfer: that would be an extra packet the host did not expect.
if (send_len == w_length)
check(!zlp_required,
"a ZLP was added after a transfer that already ended by count");
// 6. Nor after a short packet, which already terminated it.
if (found && ((send_len % mps_i) != 0))
check(!zlp_required,
"a ZLP was added after a short packet -- two terminators");
// 7. An unknown descriptor offers nothing at all.
if (stall_req)
check((send_len == 8'd0) && (n_packets == 8'd0) && !zlp_required,
"a STALLed request still offered data");
// 8. The packet count must be able to carry the bytes.
check(n_packets * mps_i >= send_len,
"n_packets cannot carry send_len bytes");
if (e_found) begin
n_found = n_found + 1;
n_bytype[desc_type] = n_bytype[desc_type] + 1;
if (e_trunc) n_trunc = n_trunc + 1;
if (e_zlp) n_zlpreq = n_zlpreq + 1;
if ((e_slen % mps_i) == 0) n_exact = n_exact + 1;
else n_partial = n_partial + 1;
end
if (e_stall) n_stall = n_stall + 1;
end
endtask
task step;
begin
#1;
check_comb;
if (req_valid) begin
m_req = m_req + 1;
if (stall_req) m_stall = m_stall + 1;
if (truncated) m_trunc = m_trunc + 1;
if (zlp_required) m_zlp = m_zlp + 1;
end
@(posedge clk); #1;
check(n_requests === m_req[31:0], "n_requests matches the model");
check(n_stalls === m_stall[31:0], "n_stalls matches the model");
check(n_truncated === m_trunc[31:0], "n_truncated matches the model");
check(n_zlp === m_zlp[31:0], "n_zlp matches the model");
end
endtask
task hard_reset;
begin
rst_n=0; req_valid=0; desc_type=0; w_length=0; mps_sel=3;
@(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
m_req=0; m_stall=0; m_trunc=0; m_zlp=0;
end
endtask
initial begin
for (i=0;i<8;i=i+1) n_bytype[i]=0;
hard_reset;
check(!found && !stall_req, "no request, no answer");
// ===== A. EXHAUSTIVE sweep of the whole decision =====
// 8 descriptor types x 80 wLength values (0..79, past the longest
// descriptor) x 4 max-packet-sizes = 2560 points. Every truncation
// boundary and every packet-size alignment is visited by construction.
req_valid = 1;
for (t=0; t<8; t=t+1)
for (w=0; w<80; w=w+1)
for (m=0; m<4; m=m+1) begin
desc_type=t[2:0]; w_length=w[7:0]; mps_sel=m[1:0];
step;
n_exh = n_exh + 1;
end
req_valid = 0;
$display(" exhaustive descriptor sweep: %0d of %0d points verified",
n_exh, 8*80*4);
// ===== B. directed: the enumeration sequence a real host performs =====
hard_reset;
// 1. The host asks for the first 9 bytes of the configuration
// descriptor, because it does not yet know how long the whole thing
// is. wLength = 9, descriptor = 32.
req_valid=1; desc_type=3'd2; w_length=8'd9; mps_sel=2'd3; step;
check(found, "the configuration descriptor exists");
check(desc_len === 8'd32, "and it is 32 bytes long");
check(send_len === 8'd9,
"but only 9 bytes are sent -- the host asked for 9");
check(truncated, "so the descriptor was truncated");
check(!zlp_required,
"and no ZLP is needed: 9 bytes is exactly what was asked for");
check(n_packets === 8'd1, "one short packet carries it");
// 2. Now the host knows the length and asks for all of it.
req_valid=1; desc_type=3'd2; w_length=8'd32; mps_sel=2'd3; step;
check(send_len === 8'd32, "all 32 bytes are sent");
check(!truncated, "nothing was truncated");
check(!zlp_required,
"and no ZLP: the byte count reached wLength exactly");
// 3. THE case. The host asks for MORE than exists, and what exists is an
// exact multiple of the packet size.
req_valid=1; desc_type=3'd2; w_length=8'd64; mps_sel=2'd2; step; // mps=32
check(mps === 8'd32, "a 32-byte max packet size");
check(send_len === 8'd32, "32 bytes are sent -- all the descriptor has");
check(!truncated,
"the DESCRIPTOR was not truncated: the host asked for more than exists");
check(zlp_required,
"so a ZLP IS required: 32 bytes is one whole packet and the host wants more");
check(n_packets === 8'd2, "one full packet plus the terminating ZLP");
// 4. The same request with a packet size that does NOT divide it.
req_valid=1; desc_type=3'd1; w_length=8'd64; mps_sel=2'd3; step; // 18, mps 64
check(send_len === 8'd18, "18 bytes are sent");
check(!truncated,
"again nothing was truncated -- 18 is the whole descriptor");
check(!zlp_required,
"but NO ZLP: 18 is a short packet and already terminates the transfer");
check(n_packets === 8'd1, "one short packet");
// 5. The boundary that catches people: nothing to send, but the host
// asked for something.
req_valid=1; desc_type=3'd3; w_length=8'd0; mps_sel=2'd3; step;
check(send_len === 8'd0, "zero bytes sent");
check(!zlp_required,
"and NO ZLP: wLength was 0, so there is no data stage to terminate");
check(n_packets === 8'd0, "no packets at all");
// 6. An unknown descriptor is STALLed, not answered with an empty one.
req_valid=1; desc_type=3'd6; w_length=8'd64; mps_sel=2'd3; step;
check(!found, "type 6 is not a descriptor this device has");
check(stall_req, "so the request is STALLed");
check(send_len === 8'd0, "and nothing is offered");
check(n_packets === 8'd0, "and no packets are counted");
// 7. A descriptor exactly one max-packet-size long, asked for in full.
req_valid=1; desc_type=3'd7; w_length=8'd64; mps_sel=2'd3; step; // 64, mps 64
check(send_len === 8'd64, "all 64 bytes are sent");
check(!zlp_required,
"no ZLP: the count reached wLength, even though it is one whole packet");
check(n_packets === 8'd1, "exactly one full packet");
// 8. ...and the same descriptor when the host asked for more.
req_valid=1; desc_type=3'd7; w_length=8'd80; mps_sel=2'd3; step;
check(send_len === 8'd64, "64 bytes are sent");
check(zlp_required, "and NOW a ZLP is required");
check(n_packets === 8'd2, "one full packet plus the ZLP");
req_valid=0;
// ===== C. randomised =====
hard_reset;
for (i=0;i<40000;i=i+1) begin
req_valid = ({$random}%8)!=0;
desc_type = {$random}%8;
mps_sel = {$random}%4;
// bias toward the descriptor lengths and the boundaries around them
case ({$random}%4)
0: w_length = {$random}%80;
1: w_length = 8'd0;
2: w_length = (8'd8 << (mps_sel));
default: begin
case ({$random}%6)
0: w_length = 8'd18; 1: w_length = 8'd32; 2: w_length = 8'd4;
3: w_length = 8'd16; 4: w_length = 8'd10; default: w_length = 8'd64;
endcase
end
endcase
step;
end
for (i=0;i<8;i=i+1)
if (i != 0 && i != 6)
check(n_bytype[i] > 0, "every real descriptor type was requested");
check(n_stall > 1000, "unknown descriptors were requested often");
check(n_trunc > 1000, "truncation happened often");
check(n_zlpreq > 1000, "the ZLP rule fired often");
check(n_exact > 1000, "aligned sends happened often");
check(n_partial > 1000, "unaligned sends happened often");
$display("");
$display(" REACH: exhaustive=%0d | found=%0d stalled=%0d truncated=%0d zlp-required=%0d",
n_exh, n_found, n_stall, n_trunc, n_zlpreq);
$display(" SHAPE: aligned-sends=%0d partial-sends=%0d | by type: dev=%0d cfg=%0d str0=%0d str1=%0d qual=%0d bos=%0d",
n_exact, n_partial, n_bytype[1], n_bytype[2], n_bytype[3],
n_bytype[4], n_bytype[5], n_bytype[7]);
$display(" COUNTERS: requests=%0d stalls=%0d truncated=%0d zlp=%0d",
n_requests, n_stalls, n_truncated, n_zlp);
$display(" [Verilog] usb_descriptor_engine: %0d errors", errors);
$display(" [Verilog] %0s", errors==0 ? "PASS" : "FAIL");
$display("");
$finish;
end
endmodule9.2 SystemVerilog testbench
`timescale 1ns/1ps
module tb_de_sv;
import usb_desc_pkg::*;
logic clk=0, rst_n=0;
logic req_valid=0;
desc_type_e desc_type = DESC_NONE;
logic [7:0] w_length=0;
logic [1:0] mps_sel=3;
logic found, stall_req, truncated, zlp_required;
logic [7:0] desc_len, send_len, n_packets, mps;
term_e terminator;
logic [31:0] n_requests, n_stalls, n_truncated, n_zlp;
// Icarus seeds $random and $urandom identically, so an unseeded run would
// replay the Verilog suite's stimulus exactly. See chapter 20.5 section 9.2.
int urandom_seed = 21303;
always #5 clk=~clk;
usb_descriptor_engine dut (
.clk, .rst_n, .req_valid, .desc_type, .w_length, .mps_sel, .found,
.stall_req, .desc_len, .send_len, .truncated, .zlp_required,
.n_packets, .mps, .terminator, .n_requests, .n_stalls, .n_truncated,
.n_zlp);
int errors=0, i, t, w, m;
int n_exh=0;
int n_found=0, n_stall=0, n_trunc=0, n_zlpreq=0, n_exact=0, n_partial=0;
int n_bytype [8];
int m_req, m_stall, m_trunc, m_zlp;
task automatic check(input bit cond, input string msg);
// Icarus will not call .name() on a net, so the enum outputs are copied
// into variables of the same type before being printed.
desc_type_e dt_v; term_e tm_v;
if (!cond) begin
errors++;
dt_v = desc_type; tm_v = terminator;
if (errors <= 25)
$display(" FAIL: %0s (type=%s wLen=%0d mps=%0d | found=%b dlen=%0d slen=%0d trunc=%b zlp=%b npkt=%0d term=%s, t=%0t)",
msg, dt_v.name(), w_length, mps, found, desc_len, send_len,
truncated, zlp_required, n_packets, tm_v.name(), $time);
end
endtask
// ---- independent reference model. It computes the packet count by
// ---- COUNTING packets in a loop where the design uses a shift, and takes
// ---- the minimum with an if where the design uses a ternary.
task automatic model(output bit e_found, output bit e_stall,
output logic [7:0] e_dlen, output logic [7:0] e_slen,
output bit e_trunc, output bit e_zlp,
output logic [7:0] e_npkt);
int real_len, mps_i, sent, pkts;
begin
case (desc_type)
DESC_DEVICE: real_len = 18;
DESC_CONFIG: real_len = 32;
DESC_STR0: real_len = 4;
DESC_STR1: real_len = 16;
DESC_QUAL: real_len = 10;
DESC_BOS: real_len = 64;
default: real_len = -1; // not a descriptor we have
endcase
mps_i = 8 << mps_sel;
e_found = req_valid && (real_len >= 0);
e_stall = req_valid && (real_len < 0);
e_dlen = e_found ? real_len[7:0] : 8'd0;
if (!e_found) sent = 0;
else if (w_length < real_len) sent = w_length;
else sent = real_len;
e_slen = sent[7:0];
e_trunc = e_found && (sent < real_len);
// The rule, restated: a ZLP is needed when we sent less than was asked
// for and what we sent was a whole number of packets.
e_zlp = e_found && (sent < w_length) && ((sent % mps_i) == 0);
// Count the packets one at a time -- a different route from the
// design's shift-and-add.
pkts = 0;
begin : countloop
int left;
left = sent;
while (left >= mps_i) begin
pkts = pkts + 1;
left = left - mps_i;
end
if (left > 0) pkts = pkts + 1;
end
if (e_zlp) pkts = pkts + 1;
e_npkt = pkts[7:0];
end
endtask
task automatic check_comb;
bit e_found, e_stall, e_trunc, e_zlp;
logic [7:0] e_dlen, e_slen, e_npkt;
term_e e_term;
int mps_i;
begin
model(e_found, e_stall, e_dlen, e_slen, e_trunc, e_zlp, e_npkt);
mps_i = 8 << mps_sel;
check(found === e_found, "found matches the model");
check(stall_req === e_stall, "stall_req matches the model");
check(desc_len === e_dlen, "desc_len matches the model");
check(send_len === e_slen, "send_len matches the model");
check(truncated === e_trunc, "truncated matches the model");
check(zlp_required === e_zlp, "zlp_required matches the model");
check(n_packets === e_npkt, "n_packets matches the model");
check(mps === mps_i[7:0], "mps decodes from mps_sel");
// The terminator, computed by the model as a flat if-chain.
if (!e_found) e_term = END_NONE;
else if (e_zlp) e_term = END_BY_ZLP;
else if ((e_slen % mps_i) != 0) e_term = END_BY_SHORT;
else if (e_slen == w_length) e_term = END_BY_COUNT;
else e_term = END_NONE;
check(terminator === e_term, "terminator matches the model");
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE property. Never send the host more than it asked for. This is
// a buffer overflow in the host driver, not a protocol nicety.
check(send_len <= w_length,
"more bytes were offered than the host asked for -- host buffer overflow");
// 2. And never more than the descriptor actually has.
check(send_len <= desc_len,
"more bytes were offered than the descriptor contains");
// 3. found and stall_req are mutually exclusive, and one of them holds
// whenever a request is present.
check(!(found && stall_req), "found and stall_req both asserted");
if (req_valid) check(found || stall_req,
"a request produced neither a descriptor nor a STALL");
if (!req_valid) check(!found && !stall_req,
"an output asserted with no request");
// 4. A transfer must terminate: either we sent exactly what was asked
// for, or the last packet is short, or a ZLP follows. If none of the
// three holds, the host waits for ever.
if (found)
check((send_len == w_length)
|| ((send_len % mps_i) != 0)
|| zlp_required,
"nothing terminates this transfer -- the host will hang");
// 4b. ...restated through the named terminator: a found descriptor
// always has one, and it agrees with the booleans.
if (found)
check(terminator !== END_NONE,
"a descriptor was found with no way to end its data stage");
check((terminator === END_BY_ZLP) === zlp_required,
"terminator disagrees with zlp_required");
// 5. A ZLP is never sent when the byte count already ended the
// transfer: that would be an extra packet the host did not expect.
if (send_len == w_length)
check(!zlp_required,
"a ZLP was added after a transfer that already ended by count");
// 6. Nor after a short packet, which already terminated it.
if (found && ((send_len % mps_i) != 0))
check(!zlp_required,
"a ZLP was added after a short packet -- two terminators");
// 7. An unknown descriptor offers nothing at all.
if (stall_req)
check((send_len == 8'd0) && (n_packets == 8'd0) && !zlp_required,
"a STALLed request still offered data");
// 8. The packet count must be able to carry the bytes.
check(n_packets * mps_i >= send_len,
"n_packets cannot carry send_len bytes");
if (e_found) begin
n_found = n_found + 1;
n_bytype[int'(desc_type)] = n_bytype[int'(desc_type)] + 1;
if (e_trunc) n_trunc = n_trunc + 1;
if (e_zlp) n_zlpreq = n_zlpreq + 1;
if ((e_slen % mps_i) == 0) n_exact = n_exact + 1;
else n_partial = n_partial + 1;
end
if (e_stall) n_stall = n_stall + 1;
end
endtask
task automatic step;
begin
#1;
check_comb;
if (req_valid) begin
m_req = m_req + 1;
if (stall_req) m_stall = m_stall + 1;
if (truncated) m_trunc = m_trunc + 1;
if (zlp_required) m_zlp = m_zlp + 1;
end
@(posedge clk); #1;
check(n_requests === 32'(m_req), "n_requests matches the model");
check(n_stalls === 32'(m_stall), "n_stalls matches the model");
check(n_truncated === 32'(m_trunc), "n_truncated matches the model");
check(n_zlp === 32'(m_zlp), "n_zlp matches the model");
end
endtask
task automatic hard_reset;
begin
rst_n=0; req_valid=0; desc_type=DESC_NONE; w_length=0; mps_sel=3;
@(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
m_req=0; m_stall=0; m_trunc=0; m_zlp=0;
end
endtask
initial begin
void'($urandom(urandom_seed));
foreach (n_bytype[i]) n_bytype[i]=0;
hard_reset;
check(!found && !stall_req, "no request, no answer");
// ===== A. EXHAUSTIVE sweep of the whole decision =====
// 8 descriptor types x 80 wLength values (0..79, past the longest
// descriptor) x 4 max-packet-sizes = 2560 points. Every truncation
// boundary and every packet-size alignment is visited by construction.
req_valid = 1;
for (t=0; t<8; t=t+1)
for (w=0; w<80; w=w+1)
for (m=0; m<4; m=m+1) begin
desc_type=desc_type_e'(t[2:0]); w_length=w[7:0]; mps_sel=m[1:0];
step;
n_exh = n_exh + 1;
end
req_valid = 0;
$display(" exhaustive descriptor sweep: %0d of %0d points verified",
n_exh, 8*80*4);
// ===== B. directed: the enumeration sequence a real host performs =====
hard_reset;
// 1. The host asks for the first 9 bytes of the configuration
// descriptor, because it does not yet know how long the whole thing
// is. wLength = 9, descriptor = 32.
req_valid=1; desc_type=DESC_CONFIG; w_length=8'd9; mps_sel=2'd3; step;
check(found, "the configuration descriptor exists");
check(desc_len === 8'd32, "and it is 32 bytes long");
check(send_len === 8'd9,
"but only 9 bytes are sent -- the host asked for 9");
check(truncated, "so the descriptor was truncated");
check(!zlp_required,
"and no ZLP is needed: 9 bytes is exactly what was asked for");
check(n_packets === 8'd1, "one short packet carries it");
// 2. Now the host knows the length and asks for all of it.
req_valid=1; desc_type=DESC_CONFIG; w_length=8'd32; mps_sel=2'd3; step;
check(send_len === 8'd32, "all 32 bytes are sent");
check(!truncated, "nothing was truncated");
check(!zlp_required,
"and no ZLP: the byte count reached wLength exactly");
// 3. THE case. The host asks for MORE than exists, and what exists is an
// exact multiple of the packet size.
req_valid=1; desc_type=DESC_CONFIG; w_length=8'd64; mps_sel=2'd2; step; // mps=32
check(mps === 8'd32, "a 32-byte max packet size");
check(send_len === 8'd32, "32 bytes are sent -- all the descriptor has");
check(!truncated,
"the DESCRIPTOR was not truncated: the host asked for more than exists");
check(zlp_required,
"so a ZLP IS required: 32 bytes is one whole packet and the host wants more");
check(n_packets === 8'd2, "one full packet plus the terminating ZLP");
// 4. The same request with a packet size that does NOT divide it.
req_valid=1; desc_type=DESC_DEVICE; w_length=8'd64; mps_sel=2'd3; step; // 18, mps 64
check(send_len === 8'd18, "18 bytes are sent");
check(!truncated,
"again nothing was truncated -- 18 is the whole descriptor");
check(!zlp_required,
"but NO ZLP: 18 is a short packet and already terminates the transfer");
check(n_packets === 8'd1, "one short packet");
// 5. The boundary that catches people: nothing to send, but the host
// asked for something.
req_valid=1; desc_type=DESC_STR0; w_length=8'd0; mps_sel=2'd3; step;
check(send_len === 8'd0, "zero bytes sent");
check(!zlp_required,
"and NO ZLP: wLength was 0, so there is no data stage to terminate");
check(n_packets === 8'd0, "no packets at all");
// 6. An unknown descriptor is STALLed, not answered with an empty one.
req_valid=1; desc_type=DESC_RSVD; w_length=8'd64; mps_sel=2'd3; step;
check(!found, "type 6 is not a descriptor this device has");
check(stall_req, "so the request is STALLed");
check(send_len === 8'd0, "and nothing is offered");
check(n_packets === 8'd0, "and no packets are counted");
// 7. A descriptor exactly one max-packet-size long, asked for in full.
req_valid=1; desc_type=DESC_BOS; w_length=8'd64; mps_sel=2'd3; step; // 64, mps 64
check(send_len === 8'd64, "all 64 bytes are sent");
check(!zlp_required,
"no ZLP: the count reached wLength, even though it is one whole packet");
check(n_packets === 8'd1, "exactly one full packet");
// 8. ...and the same descriptor when the host asked for more.
req_valid=1; desc_type=DESC_BOS; w_length=8'd80; mps_sel=2'd3; step;
check(send_len === 8'd64, "64 bytes are sent");
check(zlp_required, "and NOW a ZLP is required");
check(n_packets === 8'd2, "one full packet plus the ZLP");
req_valid=0;
// ===== C. randomised =====
hard_reset;
for (i=0;i<40000;i=i+1) begin
req_valid = ($urandom%8)!=0;
desc_type = desc_type_e'($urandom%8);
mps_sel = $urandom%4;
// bias toward the descriptor lengths and the boundaries around them
case ($urandom%4)
0: w_length = $urandom%80;
1: w_length = 8'd0;
2: w_length = (8'd8 << (mps_sel));
default: begin
case ($urandom%6)
0: w_length = 8'd18; 1: w_length = 8'd32; 2: w_length = 8'd4;
3: w_length = 8'd16; 4: w_length = 8'd10; default: w_length = 8'd64;
endcase
end
endcase
step;
end
foreach (n_bytype[i])
if (i != 0 && i != 6)
check(n_bytype[i] > 0, "every real descriptor type was requested");
check(n_stall > 1000, "unknown descriptors were requested often");
check(n_trunc > 1000, "truncation happened often");
check(n_zlpreq > 1000, "the ZLP rule fired often");
check(n_exact > 1000, "aligned sends happened often");
check(n_partial > 1000, "unaligned sends happened often");
$display("");
$display(" REACH: exhaustive=%0d | found=%0d stalled=%0d truncated=%0d zlp-required=%0d",
n_exh, n_found, n_stall, n_trunc, n_zlpreq);
$display(" SHAPE: aligned-sends=%0d partial-sends=%0d | by type: dev=%0d cfg=%0d str0=%0d str1=%0d qual=%0d bos=%0d",
n_exact, n_partial, n_bytype[1], n_bytype[2], n_bytype[3],
n_bytype[4], n_bytype[5], n_bytype[7]);
$display(" COUNTERS: requests=%0d stalls=%0d truncated=%0d zlp=%0d",
n_requests, n_stalls, n_truncated, n_zlp);
$display(" [SystemVerilog] usb_descriptor_engine: %0d errors", errors);
$display(" [SystemVerilog] %0s", errors==0 ? "PASS" : "FAIL");
$display("");
$finish;
end
endmodule9.3 VHDL testbench
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
use work.usb_desc_pkg.all;
entity tb_de_vhdl is
end entity;
architecture sim of tb_de_vhdl is
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal req_valid : std_logic := '0';
signal desc_type : std_logic_vector(2 downto 0) := "000";
signal w_length : std_logic_vector(7 downto 0) := (others => '0');
signal mps_sel : std_logic_vector(1 downto 0) := "11";
signal found, stall_req, truncated, zlp_required : std_logic;
signal desc_len, send_len, n_packets, mps : std_logic_vector(7 downto 0);
signal terminator : std_logic_vector(1 downto 0);
signal n_requests, n_stalls, n_truncated, n_zlp
: std_logic_vector(31 downto 0);
signal running : boolean := true;
type cnt8_t is array (0 to 7) of integer;
begin
clk <= not clk after 5 ns when running else '0';
dut : entity work.usb_descriptor_engine
port map (clk => clk, rst_n => rst_n, req_valid => req_valid,
desc_type => desc_type, w_length => w_length,
mps_sel => mps_sel, found => found, stall_req => stall_req,
desc_len => desc_len, send_len => send_len,
truncated => truncated, zlp_required => zlp_required,
n_packets => n_packets, mps => mps, terminator => terminator,
n_requests => n_requests, n_stalls => n_stalls,
n_truncated => n_truncated, n_zlp => n_zlp);
stim : process
variable seed1 : positive := 6619;
variable seed2 : positive := 1237;
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_exh : integer := 0;
variable n_found, n_stall, n_trunc, n_zlpreq : integer := 0;
variable n_exact, n_partial : integer := 0;
variable n_bytype : cnt8_t := (others => 0);
variable m_req, m_stall, m_trunc, m_zlp : integer := 0;
procedure check(cond : boolean; msg : string) is
begin
if not cond then
errors := errors + 1;
if errors <= 25 then
report " FAIL: " & msg
& " (type=" & integer'image(to_integer(unsigned(desc_type)))
& " wLen=" & integer'image(to_integer(unsigned(w_length)))
& " mps=" & integer'image(to_integer(unsigned(mps)))
& " | found=" & std_logic'image(found)(2)
& " dlen=" & integer'image(to_integer(unsigned(desc_len)))
& " slen=" & integer'image(to_integer(unsigned(send_len)))
& " trunc=" & std_logic'image(truncated)(2)
& " zlp=" & std_logic'image(zlp_required)(2)
& " npkt=" & integer'image(to_integer(unsigned(n_packets)))
& " term=" & integer'image(to_integer(unsigned(terminator)))
& ")" 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 dt : desc_type_t;
variable real_len, mps_iv, sent, pkts, wl, left : integer;
variable e_found, e_stall, e_trunc, e_zlp : boolean;
variable e_term : term_t;
begin
dt := desc_decode(desc_type);
wl := to_integer(unsigned(w_length));
case dt is
when DESC_DEVICE => real_len := 18;
when DESC_CONFIG => real_len := 32;
when DESC_STR0 => real_len := 4;
when DESC_STR1 => real_len := 16;
when DESC_QUAL => real_len := 10;
when DESC_BOS => real_len := 64;
when others => real_len := -1; -- not a descriptor we have
end case;
mps_iv := 8 * (2 ** to_integer(unsigned(mps_sel)));
e_found := req_valid = '1' and real_len >= 0;
e_stall := req_valid = '1' and real_len < 0;
if not e_found then sent := 0;
elsif wl < real_len then sent := wl;
else sent := real_len;
end if;
e_trunc := e_found and sent < real_len;
-- The rule, restated: a ZLP is needed when we sent less than was asked
-- for and what we sent was a whole number of packets.
e_zlp := e_found and sent < wl and (sent mod mps_iv) = 0;
-- Count the packets one at a time -- a different route from the
-- design's divide-and-add.
pkts := 0;
left := sent;
while left >= mps_iv loop
pkts := pkts + 1;
left := left - mps_iv;
end loop;
if left > 0 then pkts := pkts + 1; end if;
if e_zlp then pkts := pkts + 1; end if;
if not e_found then e_term := END_NONE;
elsif e_zlp then e_term := END_BY_ZLP;
elsif (sent mod mps_iv) /= 0 then e_term := END_BY_SHORT;
elsif sent = wl then e_term := END_BY_COUNT;
else e_term := END_NONE;
end if;
check((found = '1') = e_found, "found matches the model");
check((stall_req = '1') = e_stall, "stall_req matches the model");
if e_found then
check(to_integer(unsigned(desc_len)) = real_len,
"desc_len matches the model");
else
check(to_integer(unsigned(desc_len)) = 0,
"desc_len is zero when there is no descriptor");
end if;
check(to_integer(unsigned(send_len)) = sent, "send_len matches the model");
check((truncated = '1') = e_trunc, "truncated matches the model");
check((zlp_required = '1') = e_zlp, "zlp_required matches the model");
check(to_integer(unsigned(n_packets)) = pkts,
"n_packets matches the model");
check(to_integer(unsigned(mps)) = mps_iv, "mps decodes from mps_sel");
check(terminator = term_code(e_term), "terminator matches the model");
-- ---- SAFETY PROPERTIES, independent of the model ----
-- 1. THE property. Never send the host more than it asked for.
check(to_integer(unsigned(send_len)) <= wl,
"more bytes were offered than the host asked for -- host buffer overflow");
-- 2. And never more than the descriptor actually has.
check(to_integer(unsigned(send_len))
<= to_integer(unsigned(desc_len)),
"more bytes were offered than the descriptor contains");
-- 3. found and stall_req are mutually exclusive, and one holds whenever
-- a request is present.
check(not (found = '1' and stall_req = '1'),
"found and stall_req both asserted");
if req_valid = '1' then
check(found = '1' or stall_req = '1',
"a request produced neither a descriptor nor a STALL");
else
check(found = '0' and stall_req = '0',
"an output asserted with no request");
end if;
-- 4. A transfer must terminate: exactly what was asked for, a short
-- last packet, or a ZLP. Otherwise the host waits for ever.
if found = '1' then
check(to_integer(unsigned(send_len)) = wl
or (to_integer(unsigned(send_len)) mod mps_iv) /= 0
or zlp_required = '1',
"nothing terminates this transfer -- the host will hang");
-- 4b. ...restated through the named terminator.
check(terminator /= term_code(END_NONE),
"a descriptor was found with no way to end its data stage");
end if;
check((terminator = term_code(END_BY_ZLP)) = (zlp_required = '1'),
"terminator disagrees with zlp_required");
-- 5. A ZLP is never sent when the byte count already ended the transfer.
if to_integer(unsigned(send_len)) = wl then
check(zlp_required = '0',
"a ZLP was added after a transfer that already ended by count");
end if;
-- 6. Nor after a short packet, which already terminated it.
if found = '1' and (to_integer(unsigned(send_len)) mod mps_iv) /= 0 then
check(zlp_required = '0',
"a ZLP was added after a short packet -- two terminators");
end if;
-- 7. An unknown descriptor offers nothing at all.
if stall_req = '1' then
check(to_integer(unsigned(send_len)) = 0
and to_integer(unsigned(n_packets)) = 0
and zlp_required = '0',
"a STALLed request still offered data");
end if;
-- 8. The packet count must be able to carry the bytes.
check(to_integer(unsigned(n_packets)) * mps_iv
>= to_integer(unsigned(send_len)),
"n_packets cannot carry send_len bytes");
if e_found then
n_found := n_found + 1;
n_bytype(desc_type_t'pos(dt)) := n_bytype(desc_type_t'pos(dt)) + 1;
if e_trunc then n_trunc := n_trunc + 1; end if;
if e_zlp then n_zlpreq := n_zlpreq + 1; end if;
if (sent mod mps_iv) = 0 then n_exact := n_exact + 1;
else n_partial := n_partial + 1; end if;
end if;
if e_stall then n_stall := n_stall + 1; end if;
end procedure;
procedure step is
begin
wait for 1 ns;
check_comb;
if req_valid = '1' then
m_req := m_req + 1;
if stall_req = '1' then m_stall := m_stall + 1; end if;
if truncated = '1' then m_trunc := m_trunc + 1; end if;
if zlp_required = '1' then m_zlp := m_zlp + 1; end if;
end if;
wait until rising_edge(clk);
wait for 1 ns;
check(n_requests = std_logic_vector(to_unsigned(m_req, 32)),
"n_requests matches the model");
check(n_stalls = std_logic_vector(to_unsigned(m_stall, 32)),
"n_stalls matches the model");
check(n_truncated = std_logic_vector(to_unsigned(m_trunc, 32)),
"n_truncated matches the model");
check(n_zlp = std_logic_vector(to_unsigned(m_zlp, 32)),
"n_zlp matches the model");
end procedure;
procedure setreq(t : integer; w : integer; m : integer) is
begin
desc_type <= std_logic_vector(to_unsigned(t, 3));
w_length <= std_logic_vector(to_unsigned(w, 8));
mps_sel <= std_logic_vector(to_unsigned(m, 2));
end procedure;
procedure hard_reset is
begin
rst_n <= '0'; req_valid <= '0'; setreq(0, 0, 3);
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_req := 0; m_stall := 0; m_trunc := 0; m_zlp := 0;
end procedure;
variable iv, msel : integer;
begin
hard_reset;
check(found = '0' and stall_req = '0', "no request, no answer");
-- ===== A. EXHAUSTIVE sweep of the whole decision =====
-- 8 descriptor types x 80 wLength values (0..79, past the longest
-- descriptor) x 4 max-packet-sizes = 2560 points. Every truncation
-- boundary and every packet-size alignment is visited by construction.
req_valid <= '1';
for t in 0 to 7 loop
for w in 0 to 79 loop
for m in 0 to 3 loop
setreq(t, w, m);
step;
n_exh := n_exh + 1;
end loop;
end loop;
end loop;
req_valid <= '0';
report " exhaustive descriptor sweep: " & integer'image(n_exh)
& " of 2560 points verified" severity note;
-- ===== B. directed: the enumeration sequence a real host performs =====
hard_reset;
-- 1. The host asks for the first 9 bytes of the configuration
-- descriptor, because it does not yet know how long the whole thing
-- is. wLength = 9, descriptor = 32.
req_valid <= '1'; setreq(2, 9, 3); step;
check(found = '1', "the configuration descriptor exists");
check(to_integer(unsigned(desc_len)) = 32, "and it is 32 bytes long");
check(to_integer(unsigned(send_len)) = 9,
"but only 9 bytes are sent -- the host asked for 9");
check(truncated = '1', "so the descriptor was truncated");
check(zlp_required = '0',
"and no ZLP is needed: 9 bytes is exactly what was asked for");
check(to_integer(unsigned(n_packets)) = 1,
"one short packet carries it");
-- 2. Now the host knows the length and asks for all of it.
setreq(2, 32, 3); step;
check(to_integer(unsigned(send_len)) = 32, "all 32 bytes are sent");
check(truncated = '0', "nothing was truncated");
check(zlp_required = '0',
"and no ZLP: the byte count reached wLength exactly");
-- 3. THE case. The host asks for MORE than exists, and what exists is an
-- exact multiple of the packet size.
setreq(2, 64, 2); step; -- mps = 32
check(to_integer(unsigned(mps)) = 32, "a 32-byte max packet size");
check(to_integer(unsigned(send_len)) = 32,
"32 bytes are sent -- all the descriptor has");
check(truncated = '0',
"the DESCRIPTOR was not truncated: the host asked for more than exists");
check(zlp_required = '1',
"so a ZLP IS required: 32 bytes is one whole packet and the host wants more");
check(to_integer(unsigned(n_packets)) = 2,
"one full packet plus the terminating ZLP");
-- 4. The same request with a packet size that does NOT divide it.
setreq(1, 64, 3); step; -- 18 bytes, mps 64
check(to_integer(unsigned(send_len)) = 18, "18 bytes are sent");
check(truncated = '0',
"again nothing was truncated -- 18 is the whole descriptor");
check(zlp_required = '0',
"but NO ZLP: 18 is a short packet and already terminates the transfer");
check(to_integer(unsigned(n_packets)) = 1, "one short packet");
-- 5. The boundary that catches people: nothing to send, and the host
-- asked for nothing either.
setreq(3, 0, 3); step;
check(to_integer(unsigned(send_len)) = 0, "zero bytes sent");
check(zlp_required = '0',
"and NO ZLP: wLength was 0, so there is no data stage to terminate");
check(to_integer(unsigned(n_packets)) = 0, "no packets at all");
-- 6. An unknown descriptor is STALLed, not answered with an empty one.
setreq(6, 64, 3); step;
check(found = '0', "type 6 is not a descriptor this device has");
check(stall_req = '1', "so the request is STALLed");
check(to_integer(unsigned(send_len)) = 0, "and nothing is offered");
check(to_integer(unsigned(n_packets)) = 0, "and no packets are counted");
-- 7. A descriptor exactly one max-packet-size long, asked for in full.
setreq(7, 64, 3); step; -- 64 bytes, mps 64
check(to_integer(unsigned(send_len)) = 64, "all 64 bytes are sent");
check(zlp_required = '0',
"no ZLP: the count reached wLength, even though it is one whole packet");
check(to_integer(unsigned(n_packets)) = 1, "exactly one full packet");
-- 8. ...and the same descriptor when the host asked for more.
setreq(7, 80, 3); step;
check(to_integer(unsigned(send_len)) = 64, "64 bytes are sent");
check(zlp_required = '1', "and NOW a ZLP is required");
check(to_integer(unsigned(n_packets)) = 2, "one full packet plus the ZLP");
req_valid <= '0';
-- ===== C. randomised =====
-- ieee.math_real.uniform is a genuinely different generator from either
-- Verilog builtin, which is what makes this column independent evidence.
hard_reset;
for i in 0 to 39999 loop
rnd(iv, 8); if iv /= 0 then req_valid <= '1'; else req_valid <= '0'; end if;
rnd(iv, 8); desc_type <= std_logic_vector(to_unsigned(iv, 3));
-- The packet-size selector is kept in a VARIABLE as well as driven onto
-- the signal, because the wLength bias below needs the size chosen THIS
-- iteration. Reading mps_sel back would read the previous value: a
-- signal assignment does not take effect until the next wait.
rnd(msel, 4); mps_sel <= std_logic_vector(to_unsigned(msel, 2));
-- bias toward the descriptor lengths and the boundaries around them
rnd(iv, 4);
case iv is
when 0 => rnd(iv, 80);
w_length <= std_logic_vector(to_unsigned(iv, 8));
when 1 => w_length <= (others => '0');
when 2 => w_length <= std_logic_vector(
to_unsigned(8 * (2 ** msel), 8));
when others =>
rnd(iv, 6);
case iv is
when 0 => w_length <= std_logic_vector(to_unsigned(18, 8));
when 1 => w_length <= std_logic_vector(to_unsigned(32, 8));
when 2 => w_length <= std_logic_vector(to_unsigned(4, 8));
when 3 => w_length <= std_logic_vector(to_unsigned(16, 8));
when 4 => w_length <= std_logic_vector(to_unsigned(10, 8));
when others => w_length <= std_logic_vector(to_unsigned(64, 8));
end case;
end case;
step;
end loop;
for i in 0 to 7 loop
if i /= 0 and i /= 6 then
check(n_bytype(i) > 0, "every real descriptor type was requested");
end if;
end loop;
check(n_stall > 1000, "unknown descriptors were requested often");
check(n_trunc > 1000, "truncation happened often");
check(n_zlpreq > 1000, "the ZLP rule fired often");
check(n_exact > 1000, "aligned sends happened often");
check(n_partial > 1000, "unaligned sends happened often");
report " REACH: exhaustive=" & integer'image(n_exh)
& " | found=" & integer'image(n_found)
& " stalled=" & integer'image(n_stall)
& " truncated=" & integer'image(n_trunc)
& " zlp-required=" & integer'image(n_zlpreq) severity note;
report " SHAPE: aligned-sends=" & integer'image(n_exact)
& " partial-sends=" & integer'image(n_partial)
& " | by type: dev=" & integer'image(n_bytype(1))
& " cfg=" & integer'image(n_bytype(2))
& " str0=" & integer'image(n_bytype(3))
& " str1=" & integer'image(n_bytype(4))
& " qual=" & integer'image(n_bytype(5))
& " bos=" & integer'image(n_bytype(7)) severity note;
report " COUNTERS: requests="
& integer'image(to_integer(unsigned(n_requests)))
& " stalls=" & integer'image(to_integer(unsigned(n_stalls)))
& " truncated=" & integer'image(to_integer(unsigned(n_truncated)))
& " zlp=" & integer'image(to_integer(unsigned(n_zlp)))
severity note;
report " [VHDL] usb_descriptor_engine: " & 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;10. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| Exhaustive points | 2560 / 2560 | 2560 / 2560 | 2560 / 2560 |
| descriptors found | 28115 | 28269 | 28149 |
| requests STALLed | 9430 | 9368 | 9412 |
| descriptors truncated | 14215 | 14251 | 14445 |
| ZLP required | 1812 | 1854 | 1861 |
| aligned sends | 12641 | 12839 | 12797 |
| partial sends | 15474 | 15430 | 15352 |
| DEVICE requests | 4830 | 4774 | 4678 |
| CONFIGURATION requests | 4733 | 4765 | 4707 |
| STRING 0 / STRING 1 | 4608 / 4631 | 4789 / 4688 | 4663 / 4671 |
| DEVICE_QUALIFIER / BOS | 4730 / 4583 | 4631 / 4622 | 4595 / 4835 |
| Result | PASS | PASS | PASS |
Every real descriptor type was requested thousands of times, and the testbenches assert that. The row worth reading is ZLP required ≈ 1850 — the condition is a conjunction of two narrow terms, so it fires on about 6.5% of found requests, and the biased randomiser is what lifts it from "occasionally" to "1850 times".
11. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| G1 | wLength ignored — the full descriptor is always sent | 81229 | 81487 | 82768 |
| G2 | the ZLP rule drops send_len < wLength | 43320 | 43944 | 43748 |
| G3 | the ZLP rule drops the alignment term | 40314 | 40498 | 39358 |
| G4 | an unknown descriptor is answered, not STALLed | 42585 | 42489 | 42619 |
| G5 | alignment tested against a fixed 64, not mps | 12094 | 12417 | 12232 |
| G6 | the terminating ZLP is not counted in n_packets | 1814 | 1856 | 1863 |
| G7 | alignment tested on desc_len, not send_len | 18710 | 18812 | 19219 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages, all counts distinct, all columns within 3%.
G1 is the host-buffer-overflow bug, and at ~81 000 it is the largest because send_len feeds truncated, zlp_required, n_packets and terminator — one wrong line corrupting four outputs plus two counters.
G2 and G3 are the two halves of the ZLP rule, and they score almost identically (43 000 and 40 000) while being opposite errors. That symmetry is informative: it says the suite is equally sensitive to a ZLP that should not be there and one that should. A suite that caught only one of them would have a blind spot shaped exactly like the bug that hangs transfers.
G6 is the smallest at ~1850, and it is the most interesting. It affects only n_packets, and only on the ~1850 requests where a ZLP is required — so its count is almost exactly the number of times the ZLP rule fires. A mutation whose failure count equals a reach counter is a mutation on a path that nothing else observes, which is precisely what a single-output error looks like when the suite is tight.
G5 and G7 are the two "right idea, wrong operand" mutations, and the gap between them (12 000 vs 18 700) says something useful: testing alignment against a fixed packet size (G5) is wrong only when mps ≠ 64, which is three quarters of the sweep; testing it on the descriptor length instead of the sent length (G7) is wrong whenever the two differ, which is more often.
12. Debugging Walkthrough: The Device That Enumerates on Linux and Not on Windows
The report. A device enumerates perfectly on one operating system and fails on another, at the same point every time: the host reads the device descriptor, then hangs.
Step 1 — what is different about the two hosts? Capture both. The working host asks GET_DESCRIPTOR(DEVICE, wLength = 18). The failing host asks GET_DESCRIPTOR(DEVICE, wLength = 64) — a common pattern, because 64 is the control endpoint's maximum packet size and the host will take whatever arrives.
Step 2 — what does the device send? 18 bytes in both cases. Correct: the descriptor is 18 bytes long.
Step 3 — so why does one hang? On the working host, 18 == wLength, so the transfer ended by count. On the failing host, 18 < 64 — but 18 is also not a multiple of 64, so the packet is short and the transfer ends anyway. Both should work.
Step 4 — look again at the endpoint. bMaxPacketSize0 is 8, not 64. This is a low-speed device. And 18 bytes on an 8-byte endpoint is two full packets and a 2-byte remainder — still short, still fine.
Step 5 — the actual failing request. Further up the capture: GET_DESCRIPTOR(CONFIGURATION, wLength = 64). The configuration descriptor is 32 bytes. On an 8-byte endpoint that is four full packets and nothing left over, and 32 < 64. Nothing terminates the transfer. The host waits.
Step 6 — why the other host was fine. It asked for wLength = 9 first, then wLength = 32 — exact, so the count terminated it. The failing host asked for 64 in one shot. Both hosts are correct; only one exercises the ZLP path.
Step 7 — the fix and the diagnostic. zlp_required was implemented as send_len < desc_len — the truncation test rather than the under-request test. The two coincide on most requests and differ on exactly this one. The terminator output would have read END_NONE on that transaction, and END_NONE is not a value a correct design ever produces.
13. A Test That Was Wrong About a Correct Design
While this chapter was being written, two directed checks failed against an implementation that was right. They are worth showing, because the mistake is the one §1's callout warns about.
// The host asks for MORE than exists: 64 bytes of a 32-byte descriptor.
req_valid=1; desc_type=3'd2; w_length=8'd64; mps_sel=2'd2; step;
check(send_len === 8'd32, "32 bytes are sent -- all the descriptor has");
check(truncated, "which is less than the 64 asked for"); // <-- WRONGtruncated is defined as send_len < desc_len — the descriptor was cut short. Here send_len is 32 and desc_len is 32: nothing was truncated. The host asked for more than exists, which is a different fact, and the one that drives the ZLP rule.
The corrected check:
check(!truncated,
"the DESCRIPTOR was not truncated: the host asked for more than exists");This is the sixth time in this curriculum that a directed test has encoded a wrong expectation against a correct design, and the pattern is always the same: a name that reads like plain English gets used with its plain-English meaning rather than its defined one. "Truncated" sounds like it should cover both cases. It does not, and the design is right to distinguish them — the whole ZLP rule depends on the distinction.
The defence is the one this series keeps arriving at: when a directed check fails, work out which of the two is wrong before changing either. A test that is "fixed" by loosening it has removed a check; a design that is "fixed" to satisfy a wrong test has acquired a bug.
14. UVM: Driving the Host's Two-Phase Descriptor Read
14.1 The transaction
class usb_desc_item extends uvm_sequence_item;
`uvm_object_utils(usb_desc_item)
rand bit req_valid;
rand desc_type_e desc_type;
rand bit [7:0] w_length;
rand bit [1:0] mps_sel;
// A real host mostly asks for descriptors that exist. The unknown-type
// path is exercised by its own sequence rather than by flooding here.
constraint c_mostly_real {
desc_type dist { DESC_NONE := 5, DESC_RSVD := 5,
DESC_DEVICE := 18, DESC_CONFIG := 18,
DESC_STR0 := 18, DESC_STR1 := 18,
DESC_QUAL := 9, DESC_BOS := 9 };
}
// THE bias. The interesting wLength values are the descriptor lengths
// themselves and the packet-size boundaries -- a uniform draw over 0..79
// lands on one about 15% of the time and on the ZLP condition far less.
constraint c_boundary_heavy {
w_length dist { 0 := 10, // no data stage at all
[1:7] := 10,
18 := 10, 32 := 10, 4 := 10, // exact descriptor lengths
16 := 10, 10 := 10, 64 := 10,
[65:79] := 15, // more than anything exists
[8:63] := 15 };
}
function new(string name = "usb_desc_item"); super.new(name); endfunction
function string convert2string();
return $sformatf("%s wLength=%0d mps=%0d", desc_type.name(), w_length,
8 << mps_sel);
endfunction
endclass14.2 Sequences
// THE sequence for this chapter. It reproduces the two-phase read a real
// host performs during enumeration: a short probe to learn the length, then
// a full read. The probe is the request that overflows a host buffer if
// wLength is ignored, and it is not a request random stimulus emphasises.
class two_phase_config_read_seq extends uvm_sequence #(usb_desc_item);
`uvm_object_utils(two_phase_config_read_seq)
function new(string name = "two_phase_config_read_seq"); super.new(name); endfunction
task body();
repeat (300) begin
usb_desc_item it;
// Phase 1: the 9-byte header probe.
it = usb_desc_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { req_valid == 1; desc_type == DESC_CONFIG;
w_length == 9; })
`uvm_error("RAND", "probe randomize failed")
finish_item(it);
// Phase 2: the full read, now that the host knows the length.
it = usb_desc_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { req_valid == 1; desc_type == DESC_CONFIG;
w_length == 32; })
`uvm_error("RAND", "full-read randomize failed")
finish_item(it);
end
endtask
endclass
// The ZLP population: requests where the host asks for MORE than exists and
// what exists is a whole number of packets. Constructed rather than sampled,
// because the conjunction is narrow enough that random traffic under-visits
// it by an order of magnitude.
class zlp_required_seq extends uvm_sequence #(usb_desc_item);
`uvm_object_utils(zlp_required_seq)
function new(string name = "zlp_required_seq"); super.new(name); endfunction
task body();
// (descriptor length, mps_sel) pairs where the length divides the packet
// size exactly: CONFIG/32 on mps 32, STR1/16 on mps 16 and 8, BOS/64 on
// every size.
desc_type_e types[] = '{DESC_CONFIG, DESC_STR1, DESC_BOS, DESC_BOS};
bit [1:0] sels[] = '{2'd2, 2'd1, 2'd3, 2'd2};
foreach (types[i]) begin
repeat (150) begin
usb_desc_item it = usb_desc_item::type_id::create("it");
start_item(it);
it.c_boundary_heavy.constraint_mode(0);
// wLength strictly greater than the descriptor: this is what makes a
// ZLP necessary rather than merely possible.
if (!it.randomize() with { req_valid == 1;
desc_type == types[i];
mps_sel == sels[i];
w_length > 64; })
`uvm_error("RAND", "zlp randomize failed")
finish_item(it);
end
end
endtask
endclass
// Feature discovery: a host probing for descriptors the device may not have.
// The property under test is that each one is STALLed rather than answered
// with an empty descriptor.
class feature_probe_seq extends uvm_sequence #(usb_desc_item);
`uvm_object_utils(feature_probe_seq)
function new(string name = "feature_probe_seq"); super.new(name); endfunction
task body();
repeat (400) begin
usb_desc_item it = usb_desc_item::type_id::create("it");
start_item(it);
it.c_mostly_real.constraint_mode(0);
if (!it.randomize() with { req_valid == 1;
desc_type inside {DESC_NONE, DESC_RSVD}; })
`uvm_error("RAND", "probe randomize failed")
finish_item(it);
end
endtask
endclass14.3 The scoreboard
class usb_desc_scoreboard extends uvm_scoreboard;
`uvm_component_utils(usb_desc_scoreboard)
uvm_analysis_imp #(usb_desc_mon_item, usb_desc_scoreboard) ap;
int unsigned n_zlp, n_stall, n_truncated, n_overrun_attempts;
// The scoreboard's own descriptor table. Sharing a table with the DUT
// would make a wrong length agree with itself.
function automatic int len_of(desc_type_e t);
case (t)
DESC_DEVICE: return 18;
DESC_CONFIG: return 32;
DESC_STR0: return 4;
DESC_STR1: return 16;
DESC_QUAL: return 10;
DESC_BOS: return 64;
default: return -1; // not a descriptor this device has
endcase
endfunction
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void write(usb_desc_mon_item t);
int dlen = len_of(t.desc_type);
int mps_i = 8 << t.mps_sel;
int sent;
if (!t.req_valid) return;
// ---- An unknown descriptor is STALLed, never answered ----
if (dlen < 0) begin
if (!t.stall_req)
`uvm_error("DISCOVERY",
"an unsupported descriptor was not STALLed -- the host cannot tell it is absent")
if (t.found || (t.send_len != 0))
`uvm_error("DISCOVERY",
"an unsupported descriptor was answered with data")
n_stall++;
return;
end
sent = (t.w_length < dlen) ? t.w_length : dlen;
// ---- THE property. Never more than the host's buffer holds. ----
if (t.send_len > t.w_length)
`uvm_error("OVERFLOW",
$sformatf("offered %0d bytes into a %0d-byte host buffer",
t.send_len, t.w_length))
if (t.send_len != sent)
`uvm_error("LENGTH",
$sformatf("send_len=%0d, expected min(%0d, %0d)=%0d",
t.send_len, t.w_length, dlen, sent))
if (t.w_length < dlen) n_overrun_attempts++;
// ---- The transfer must end. This is the hang, stated positively. ----
begin
bit ends_by_count = (sent == t.w_length);
bit ends_by_short = ((sent % mps_i) != 0);
bit ends_by_zlp = t.zlp_required;
if (!(ends_by_count || ends_by_short || ends_by_zlp))
`uvm_error("HANG",
$sformatf("nothing terminates this transfer: sent=%0d wLength=%0d mps=%0d",
sent, t.w_length, mps_i))
// ...and exactly the right one of the three.
if (t.zlp_required != ((sent < t.w_length) && ((sent % mps_i) == 0)))
`uvm_error("ZLP",
$sformatf("zlp_required=%0b but sent=%0d wLength=%0d mps=%0d",
t.zlp_required, sent, t.w_length, mps_i))
// A ZLP after a transfer that already ended is an EXTRA packet, and the
// host sees it at the start of the next transfer.
if (t.zlp_required && ends_by_count)
`uvm_error("EXTRA", "a ZLP was appended to a transfer that ended by count")
if (t.zlp_required && ends_by_short)
`uvm_error("EXTRA", "a ZLP was appended after a short packet")
if (t.zlp_required) n_zlp++;
end
// ---- terminator must never read END_NONE on a descriptor we have ----
if (t.terminator == END_NONE)
`uvm_error("HANG",
"terminator reads END_NONE for a descriptor that was found -- this transfer cannot end")
if (t.truncated) n_truncated++;
endfunction
function void report_phase(uvm_phase phase);
`uvm_info("SB", $sformatf(
"zlps=%0d stalls=%0d truncations=%0d over-requests=%0d",
n_zlp, n_stall, n_truncated, n_overrun_attempts), UVM_LOW)
if (n_zlp == 0) `uvm_error("COVERAGE",
"the ZLP rule never fired -- the condition this block exists for is untested")
if (n_stall == 0) `uvm_error("COVERAGE",
"no unsupported descriptor was ever requested")
if (n_truncated == 0) `uvm_error("COVERAGE",
"no descriptor was ever truncated -- the host-buffer property is untested")
endfunction
endclass14.4 Functional coverage
covergroup desc_engine_cg with function sample(
desc_type_e dt, bit [7:0] wlen, bit [1:0] msel, bit [7:0] slen,
bit zlp, term_e term);
cp_type : coverpoint dt {
bins real_descs[] = {DESC_DEVICE, DESC_CONFIG, DESC_STR0,
DESC_STR1, DESC_QUAL, DESC_BOS};
bins unsupported = {DESC_NONE, DESC_RSVD};
}
cp_mps : coverpoint msel { bins sizes[] = {0, 1, 2, 3}; }
// THE terminator coverpoint. END_NONE gets a bin on purpose: it should be
// impossible for a found descriptor, and a bin that CANNOT be hit is a
// statement, not an oversight. An illegal_bins would abort the run; a
// normal bin lets the coverage report show it empty, which is the evidence.
cp_term : coverpoint term {
bins by_count = {END_BY_COUNT};
bins by_short = {END_BY_SHORT};
bins by_zlp = {END_BY_ZLP};
bins none = {END_NONE};
}
// The request expressed RELATIVE to what exists -- an absolute coverpoint
// on wLength closes while never once asking for exactly the descriptor
// length of the descriptor actually requested.
cp_ask : coverpoint (wlen == 0 ? 0 : 1) {
bins zero = {0};
bins some = {1};
}
// Was what we sent a whole number of packets? This, crossed with whether
// the host asked for more, IS the ZLP rule.
cp_aligned : coverpoint ((slen & ((8 << msel) - 1)) == 0) {
bins aligned = {1};
bins partial = {0};
}
cp_under : coverpoint (slen < wlen) {
bins sent_less = {1};
bins sent_all = {0};
}
// THE cross. Four bins, and exactly one of them requires a ZLP. Closing it
// is the statement that both halves of the rule were exercised in both
// directions -- which is what separates a suite that would catch G2 from
// one that would catch G3.
x_zlp_rule : cross cp_aligned, cp_under;
// Every terminator at every packet size: a ZLP on an 8-byte endpoint and
// on a 64-byte one are the same rule, and a design that special-cases the
// largest size should be caught.
x_term_mps : cross cp_term, cp_mps;
endgroupx_zlp_rule is a four-bin cross that is worth more than its size suggests. The two coverpoints are exactly the two terms of the rule, so the cross enumerates every combination of the two conditions:
cp_aligned | cp_under | ZLP? | which mutation lives here |
|---|---|---|---|
| aligned | sent less | yes | dropping the rule entirely — the hang |
| aligned | sent all | no | G2 — a ZLP after an ended transfer |
| partial | sent less | no | G3 — a ZLP after a short packet |
| partial | sent all | no | — |
A regression that closes three of those four bins has, by construction, not tested one of G2 or G3.
15. SystemVerilog Assertions
module usb_descriptor_engine_sva
import usb_desc_pkg::*;
(
input logic clk,
input logic rst_n,
input logic req_valid,
input desc_type_e desc_type,
input logic [7:0] w_length,
input logic [1:0] mps_sel,
input logic found,
input logic stall_req,
input logic [7:0] desc_len,
input logic [7:0] send_len,
input logic truncated,
input logic zlp_required,
input logic [7:0] n_packets,
input logic [7:0] mps,
input term_e terminator
);
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// ---- 1. THE property. Never offer the host more than it asked for. ----
property p_never_exceed_wlength;
send_len <= w_length;
endproperty
a_never_exceed_wlength : assert property (p_never_exceed_wlength)
else $error("offered %0d bytes into a %0d-byte host buffer",
send_len, w_length);
// ---- 2. Nor more than the descriptor contains. ----
property p_never_exceed_descriptor;
send_len <= desc_len;
endproperty
a_never_exceed_descriptor : assert property (p_never_exceed_descriptor);
// ---- 3. THE other property. Every transfer must have an ending. ----
property p_transfer_always_terminates;
found |-> ((send_len == w_length)
|| ((send_len & (mps - 8'd1)) != 8'd0)
|| zlp_required);
endproperty
a_transfer_always_terminates :
assert property (p_transfer_always_terminates)
else $error("nothing terminates this transfer: sent=%0d wLength=%0d mps=%0d",
send_len, w_length, mps);
// ---- 4. ...restated through the named terminator. ----
property p_terminator_never_none_when_found;
found |-> (terminator != END_NONE);
endproperty
a_terminator_never_none_when_found :
assert property (p_terminator_never_none_when_found)
else $error("terminator is END_NONE for a descriptor that was found");
// ---- 5. A ZLP is never appended to a transfer that already ended. ----
property p_no_zlp_after_count;
(send_len == w_length) |-> !zlp_required;
endproperty
a_no_zlp_after_count : assert property (p_no_zlp_after_count)
else $error("a ZLP followed a transfer that ended by count -- the host will see it next transfer");
// ---- 6. Nor after a short packet. ----
property p_no_zlp_after_short;
(found && ((send_len & (mps - 8'd1)) != 8'd0)) |-> !zlp_required;
endproperty
a_no_zlp_after_short : assert property (p_no_zlp_after_short);
// ---- 7. found and stall_req partition the request. ----
property p_found_xor_stall;
req_valid |-> (found ^ stall_req);
endproperty
a_found_xor_stall : assert property (p_found_xor_stall)
else $error("a request produced both or neither of found and stall_req");
// ---- 8. A STALLed request offers nothing at all. ----
property p_stall_offers_nothing;
stall_req |-> ((send_len == 8'd0) && (n_packets == 8'd0)
&& !zlp_required);
endproperty
a_stall_offers_nothing : assert property (p_stall_offers_nothing);
// ---- 9. The packet count can carry the bytes. ----
property p_packets_carry_bytes;
(n_packets * mps) >= send_len;
endproperty
a_packets_carry_bytes : assert property (p_packets_carry_bytes);
// ---- 10. A required ZLP is counted as a packet. ----
property p_zlp_is_counted;
zlp_required |-> (n_packets == (send_len >> (3 + mps_sel)) + 8'd1);
endproperty
a_zlp_is_counted : assert property (p_zlp_is_counted)
else $error("a required ZLP was not counted in n_packets");
// ---- Cover: the narrow conditions were actually reached. ----
c_zlp : cover property ((zlp_required));
c_stall : cover property ((stall_req));
c_truncated : cover property ((truncated));
c_zero_ask : cover property ((req_valid && (w_length == 8'd0)));
c_zlp_small : cover property ((zlp_required && (mps_sel == 2'd0)));
endmodule
bind usb_descriptor_engine usb_descriptor_engine_sva u_sva (.*);16. Common Misconceptions
"wLength is how much the host wants, so sending more is generous." It is the size of the buffer the host allocated. Sending more is a kernel buffer overflow.
"A short packet and a ZLP are different mechanisms." A ZLP is a short packet — the limiting case. The rule is one rule.
"A ZLP is needed whenever the descriptor is truncated." No: a ZLP is needed when the device sent less than the host asked for and the amount sent was a whole number of packets. Truncation is send_len < desc_len, which is a different comparison, and using it is the bug in §12.
"If there is nothing to send, there is nothing to do." send_len = 0 with wLength > 0 requires a ZLP. send_len = 0 with wLength = 0 requires nothing. Same send_len, opposite answers.
"An unknown descriptor should return an empty one — it is the polite answer." It tells the host the feature exists and is empty, which is a different fact from "absent". STALL is how a device says "I do not have that", and hosts rely on it for feature discovery.
"The alignment test can use 64 — it is the biggest packet size." Only if every endpoint is 64 bytes. A low-speed control endpoint is 8. Mutation G5.
"n_packets is a diagnostic, so an off-by-one is harmless." It is off by one exactly when a ZLP is required, which is exactly when a scheduler using it would under-allocate bus time for the packet that terminates the transfer.
17. Exercises
1. Implement zlp_required as truncated && aligned — the bug from §12 — and predict which of the eight safety properties fires first. Then run it and explain why the count is not the same as G2's or G3's.
2. Extend w_length to its real 16 bits and add a descriptor longer than 255 bytes. Which parts of the design need to change, and which do not? Re-run the exhaustive sweep at its new size and confirm all seven mutations still die.
3. Property 10 checks that a required ZLP is counted. Write the complementary property — that a ZLP is not counted when it is not required — and find the mutation that only the complement catches.
4. The x_zlp_rule cross has four bins and one of them requires a ZLP. Build a regression that closes all four, then remove the zlp_required_seq sequence and measure which bin empties. Use the result to argue whether constructed sequences or biased randomisation is the better way to reach a narrow conjunction.
5. cp_term in §14.4 gives END_NONE an ordinary bin rather than an illegal_bins. Argue both sides, then decide which you would ship — considering what each choice does to a nightly regression that hits the condition once.
6. The VHDL testbench trap in §9 came from reading a signal back in the same process iteration that drove it. Construct a second, subtler instance of the same class of bug somewhere in these testbenches, and say what kind of evidence would reveal it.
18. Summary
| Idea | Why it matters |
|---|---|
wLength is the host's buffer size | exceeding it is a kernel buffer overflow |
send_len = min(wLength, desc_len) | and the host really does ask for less |
| A data stage ends by count or by short packet | there is no length field in the protocol |
zlp_required = (send_len < wLength) && aligned | both terms load-bearing |
| drop the first term | a ZLP after a transfer that already ended |
| drop the second | a ZLP after a short packet |
| drop both | the multiple-of-mps read hangs |
send_len = 0, wLength > 0 → ZLP | but wLength = 0 → nothing |
| An unknown descriptor → STALL | not a length-0 descriptor |
terminator names the ending, including END_NONE | a hang is an absence; give the absence a name |
mps is a power of two | so alignment is a mask and division is a shift |
| 2560-point exhaustive verification | 8 types × 80 lengths × 4 packet sizes |
| 7 mutations, all killed in 3 languages | after a stimulus mismatch skewed four columns |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o de_v.out de_v.v de_v_tb.v && ./de_v.out |
| SystemVerilog | iverilog -g2012 -o de_sv.out de_sv.sv de_sv_tb.sv && ./de_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a de_vhdl.vhd de_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_de_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_de_vhdl |
| One mutation | iverilog -g2005 -DMUT_G1 -o mm de_v_mut.v de_v_tb.v && ./mm |
All three implementations pass with 0 errors: 2560 of 2560 exhaustive points, 40 000 randomised requests, every descriptor type reached and asserted reached.
Chapter 21.4 — Protocol Engine is the block that actually moves the bytes, and it has a constraint none of the previous three had: the handshake is due before the CRC is known. A packet's CRC covers the whole packet, so it is only verified at the end — yet the device must already have been writing the payload somewhere. The answer is to write it provisionally and commit or discard on the CRC result, which makes the endpoint FIFO's write pointer something that can move backwards.
Continue learning
Related tutorials
- Related topic
Endpoint Logic
A lost ACK and a lost data packet look identical to the host, so it resends the same bytes — and the data toggle is the only thing that tells a device a retransmission from new data.
- Related topic
FIFO Architecture
An endpoint FIFO stores packets, not bytes — a zero-length packet carries nothing and must still occupy a buffer, because it is the only thing that terminates a transfer ending on a packet boundary.
- Related topic
Downstream Device Discovery
A hub cannot interrupt the host, so every port event waits to be asked for — and the window between the poll and the acknowledgement is where devices are silently lost.
- Related topic
Bus Power
A device's current allowance changes exactly once during enumeration — and bMaxPower is counted in 2 mA units, not milliamps.
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.
