USB · Module 21
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.
Module 20 finished the protocol. This module builds the device.
Five chapters, five synthesisable blocks, and by the end they compose into a working USB device controller: the per-endpoint state machine, the endpoint FIFO, the descriptor engine, the packet engine, and the top-level device state machine that a bus reset can interrupt at any moment.
This chapter is the first of those blocks, and it turns on a single bit.
1. The Handshake That Never Arrived
A USB OUT transaction is three packets and takes about a microsecond:
host -> [ OUT token ]
host -> [ DATA0 | payload | CRC16 ]
device -> [ ACK ]The device wrote the payload into its endpoint FIFO and answered ACK. Done.
Now suppose that ACK is corrupted on the way back. A single bit flip in a three-byte handshake packet, one cable-length away from the host.
From the host's point of view, here is what it knows: no valid handshake arrived. That is all. And it is exactly what the host would have observed if the DATA packet had never reached the device at all.
A lost ACK and a lost DATA packet look the same from the host
2. Nothing in the Payload Says "Again"
The retransmitted packet is byte-for-byte identical to the one the device already accepted. Same token, same payload, same CRC. There is no sequence number in a USB data packet, no timestamp, no retry flag.
So the device has to answer an impossible-looking question — have I seen this exact packet before? — using information that is not in the packet's contents.
The answer is in the PID. Every DATA packet is either DATA0 or DATA1, and the two ends alternate strictly. The device keeps one bit of state, the expected toggle, and compares:
| Received PID | vs expected | Meaning |
|---|---|---|
| matches | — | new data |
| does not match | — | a retransmission of the packet I already took |
That is the whole mechanism, and it costs one flip-flop per endpoint.
3. The Response Is Three Rules, and All Three Are Counter-Intuitive
When a retransmission is recognised, the correct behaviour is:
1. ACK IT AGAIN
2. DO NOT write it to the FIFO
3. DO NOT advance the toggleEach of the three is easy to get wrong, and each wrong answer produces a different bug:
| If you... | What happens | How it looks in the field |
|---|---|---|
| NAK the retransmission | the host never gets the handshake it is waiting for, and retries for ever | the endpoint wedges; the transfer never completes |
| write it to the FIFO | the payload is delivered twice | silent data duplication — no error anywhere |
| advance the toggle | the next genuine packet now mismatches and is treated as a duplicate | silent data loss — one packet vanishes |
Rule 1 is the one people argue about. Why ACK something you are throwing away? Because the ACK is not about the data — it is about the host's retry loop. The host is stuck waiting for a handshake. Until it gets one it will keep resending, and the endpoint makes no progress. The ACK's job here is to say "stop retrying, we are in sync", and that is true even though nothing was stored.
Rules 2 and 3 are the same idea from two sides: the device's state is already correct, so nothing about it should change.
4. Three More Rules That Are Not Negotiable
A SETUP packet can never be NAKed or STALLed. Control transfers are how a host recovers a confused device — clearing a halt, resetting a configuration, reading a descriptor to work out what went wrong. The one transaction type that must always get through is the one carrying the recovery command. A SETUP also clears any halt and forces the toggle to DATA1 for the data stage that follows.
A halted endpoint answers STALL to everything except SETUP, and the halt is sticky: it survives until something explicitly clears it.
CLEAR_FEATURE(ENDPOINT_HALT) resets the toggle to DATA0, not just the halt. Host and device have to agree on where to restart, and DATA0 is the agreed restart point. Clearing the halt without resetting the toggle leaves the two ends disagreeing by one bit, which means the very first packet after recovery is discarded as a duplicate.
A DATA packet whose CRC failed gets no handshake at all — not a NAK. This one is worth dwelling on:
The decision, in priority order
5. The State Machine Is Four States
Two bits of state — the toggle and the halt — give four states, and every one of them is reachable and behaves differently:
The endpoint's four states
Note what is not on that diagram: there is no transition out of a running state for a retransmission, a CRC failure or a full FIFO. All three leave the state exactly where it was. That is the point — and it is also why this block is so easy to verify wrongly, as section 12 shows.
6. What We Are Building
usb_endpoint_ctrl
inputs outputs
------ -------
ep_is_in handshake NONE / ACK / NAK / STALL / DATA
token NONE/OUT/IN/SETUP fifo_write_en
rx_toggle fifo_read_en
rx_crc_ok tx_toggle
fifo_ready data_dup
in_acked expected_toggle
halt_set halted
halt_clear
ep_reset n_ack / n_nak / n_stall
n_silent / n_dup / n_writtenThe decision is combinational — a device has roughly a bit-time to answer a transaction, so there is no room for a pipeline stage — and only the toggle, the halt and six diagnostic counters are registered.
The full decision surface is small enough to enumerate:
4 internal states (toggle x halted)
x 2 (ep_is_in) x 4 (token) x 2 (rx_toggle) x 2 (rx_crc_ok)
x 2 (fifo_ready) x 2 (in_acked) x 2 (halt_set) x 2 (halt_clear)
= 4 x 512 = 2048 one-step transitions, ALL of them reachable7. Verilog-2005 Implementation
// usb_endpoint_ctrl -- the per-endpoint state machine, and the one bit that
// stands between a correct device and silent data duplication.
//
// THE PROBLEM THE TOGGLE SOLVES
//
// A USB OUT transaction is three packets: the host sends a token, then a DATA
// packet, and the device answers with a handshake.
//
// host: [OUT token][DATA0 ....] device: [ACK]
//
// That ACK travels back up the cable and it can be corrupted in transit. From
// the host's point of view a corrupted ACK and a lost DATA packet look
// IDENTICAL -- in both cases no valid handshake arrived -- so the host does
// the only safe thing and sends the DATA packet again.
//
// The device has already accepted that data and written it to the endpoint
// FIFO. The retransmission arriving now is byte-for-byte identical to the one
// it already took. Nothing in the payload says "you have seen me before".
//
// THE ONLY DISCRIMINATOR IS THE PID TOGGLE
//
// Every DATA packet is either DATA0 or DATA1, and the two ends alternate. The
// device keeps an EXPECTED toggle. A packet arriving with the expected toggle
// is new. A packet arriving with the OTHER toggle is a retransmission of one
// already accepted -- and the correct response is subtle:
//
// ACK IT AGAIN, but DO NOT write it to the FIFO,
// and DO NOT advance the toggle.
//
// The ACK is required because the host is still waiting for one and will keep
// retrying until it gets it. The write must not happen because the data is
// already in the FIFO. The toggle must not advance because it is already
// correct -- advancing it would reject the NEXT genuine packet.
//
// Getting any one of those three wrong produces a different bug:
// NAK the retransmission -> the host retries for ever; endpoint wedges
// write it again -> silent data duplication, no error anywhere
// advance the toggle -> the next genuine packet is treated as a dup
// and dropped; silent data LOSS
//
// THREE MORE RULES THAT ARE NOT NEGOTIABLE
//
// A SETUP packet can never be NAKed or STALLed. Control transfers are how
// a host recovers a confused device, so the one transaction type that must
// always get through is the one that carries the recovery command. A SETUP
// also clears a halt and forces the toggle to DATA1 for the data stage.
//
// A halted endpoint answers STALL to everything except SETUP. The halt is
// sticky: it survives until explicitly cleared.
//
// CLEAR_FEATURE(ENDPOINT_HALT) resets the toggle to DATA0 as well as
// clearing the halt. Host and device must agree on the toggle after a
// halt, and DATA0 is the agreed restart point.
//
// A DATA packet whose CRC failed gets NO HANDSHAKE AT ALL -- not a NAK.
// Silence. A NAK would tell the host "I received you and I am busy", which
// is a lie: the device does not know what it received. Silence lets the
// host's timeout fire and the retry happen for the right reason.
module usb_endpoint_ctrl (
input wire clk,
input wire rst_n,
input wire ep_is_in, // this endpoint's direction (EP0 aside)
input wire [1:0] token, // NONE / OUT / IN / SETUP
input wire rx_toggle, // the PID toggle of the DATA packet received
input wire rx_crc_ok, // that packet's CRC-16 checked out
input wire fifo_ready, // OUT: room to write. IN: data to send.
input wire in_acked, // the host ACKed the data we sent
input wire halt_set, // SET_FEATURE(ENDPOINT_HALT)
input wire halt_clear, // CLEAR_FEATURE(ENDPOINT_HALT)
input wire ep_reset, // bus reset
output wire [2:0] handshake,
output wire fifo_write_en,
output wire fifo_read_en,
output wire tx_toggle,
output wire data_dup, // a retransmission was absorbed
output wire expected_toggle,
output wire halted,
output reg [31:0] n_ack,
output reg [31:0] n_nak,
output reg [31:0] n_stall,
output reg [31:0] n_silent,
output reg [31:0] n_dup,
output reg [31:0] n_written
);
localparam [1:0] T_NONE = 2'd0, T_OUT = 2'd1, T_IN = 2'd2, T_SETUP = 2'd3;
localparam [2:0] H_NONE = 3'd0, // no response leaves the device at all
H_ACK = 3'd1,
H_NAK = 3'd2,
H_STALL = 3'd3,
H_DATA = 3'd4; // an IN: the device sends a DATA packet
reg toggle_r;
reg halted_r;
assign expected_toggle = toggle_r;
assign halted = halted_r;
assign tx_toggle = toggle_r;
// A token is ours only if it matches our direction. A SETUP is addressed to
// a control endpoint, which is an OUT-capable one.
wire is_setup = (token == T_SETUP) && !ep_is_in;
wire is_out = (token == T_OUT) && !ep_is_in;
wire is_in = (token == T_IN) && ep_is_in;
wire mismatch = (token != T_NONE) && !is_setup && !is_out && !is_in;
// A retransmission: a well-formed DATA packet carrying the toggle we are
// NOT expecting. The CRC must be good before the toggle can be believed --
// the toggle is part of the packet the CRC protects.
wire out_dup = is_out && !halted_r && rx_crc_ok && (rx_toggle != toggle_r);
wire out_new = is_out && !halted_r && rx_crc_ok && (rx_toggle == toggle_r);
// THE DECISION. Order is the design: SETUP outranks the halt, the halt
// outranks everything else, and a bad CRC produces silence rather than any
// handshake at all.
assign handshake = (token == T_NONE) ? H_NONE
: mismatch ? H_NONE // not addressed to us
: is_setup ? H_ACK // never NAK, never STALL
: halted_r ? H_STALL
: is_out ? ( !rx_crc_ok ? H_NONE // silence, not NAK
: out_dup ? H_ACK // re-ACK, take nothing
: !fifo_ready ? H_NAK
: H_ACK )
: ( !fifo_ready ? H_NAK : H_DATA );
// The write happens ONLY for genuinely new data with room to put it.
assign fifo_write_en = out_new && fifo_ready;
assign fifo_read_en = is_in && !halted_r && fifo_ready;
assign data_dup = out_dup;
// ---- Toggle and halt, as unambiguous priority chains ----
//
// Written as single next-state expressions rather than as a sequence of
// assignments inside the clocked block. A process that assigns the same
// signal twice leaves the answer to the language's last-assignment rule,
// which differs between Verilog and VHDL and hides bugs in exactly the
// place this design cannot afford one.
wire out_advance = fifo_write_en; // OUT: on accept
wire in_advance = is_in && !halted_r && in_acked; // IN: on host ACK
wire toggle_next = ep_reset ? 1'b0 // bus reset: restart at DATA0
: is_setup ? 1'b1 // the data stage begins at DATA1
: halt_clear ? 1'b0 // CLEAR_FEATURE resets the toggle
: (out_advance || in_advance) ? ~toggle_r
: toggle_r;
wire halted_next = ep_reset ? 1'b0
: is_setup ? 1'b0 // a SETUP clears a halt
: halt_clear ? 1'b0
: halt_set ? 1'b1
: halted_r;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
toggle_r <= 1'b0;
halted_r <= 1'b0;
n_ack <= 32'd0;
n_nak <= 32'd0;
n_stall <= 32'd0;
n_silent <= 32'd0;
n_dup <= 32'd0;
n_written <= 32'd0;
end else begin
toggle_r <= toggle_next;
halted_r <= halted_next;
if (handshake == H_ACK) n_ack <= n_ack + 32'd1;
if (handshake == H_NAK) n_nak <= n_nak + 32'd1;
if (handshake == H_STALL) n_stall <= n_stall + 32'd1;
// "Silent" counts only a transaction that WAS addressed to us and got
// no answer -- a CRC failure. An idle cycle is not silence.
if ((is_out || is_in) && (handshake == H_NONE))
n_silent <= n_silent + 32'd1;
if (data_dup) n_dup <= n_dup + 32'd1;
if (fifo_write_en) n_written <= n_written + 32'd1;
end
end
endmoduleTwo details deserve emphasis.
out_dup requires rx_crc_ok before it looks at the toggle. The toggle bit lives in the PID, inside the packet the CRC protects. Reading it from a packet the CRC rejected is the same mistake Chapter 20.4 is about, one layer down — and it has the same consequence: a decision made on bits known to be wrong.
The toggle and halt are written as single next-state expressions, not as a sequence of assignments inside the clocked block:
// What NOT to do -- correct in Verilog, DIFFERENT in VHDL, unreadable in both
always @(posedge clk) begin
if (halt_clear) toggle <= 1'b0;
if (out_advance) toggle <= ~toggle; // silently overrides the line above
end
// What this design does instead: one expression, priority explicit
wire toggle_next = ep_reset ? 1'b0
: is_setup ? 1'b1
: halt_clear ? 1'b0
: (out_advance || in_advance) ? ~toggle_r
: toggle_r;The first form leaves the answer to the language's last-assignment rule. Verilog and VHDL resolve it the same way in this particular case, but the reader cannot see that without knowing the rule, and a series earlier in this curriculum lost an afternoon to exactly that ambiguity hiding a mutation. One expression, one priority order, visible.
8. SystemVerilog Implementation
// usb_endpoint_ctrl -- the per-endpoint state machine, and the one bit that
// stands between a correct device and silent data duplication.
//
// THE PROBLEM THE TOGGLE SOLVES
//
// A USB OUT transaction is three packets: the host sends a token, then a DATA
// packet, and the device answers with a handshake.
//
// host: [OUT token][DATA0 ....] device: [ACK]
//
// That ACK can be corrupted on the way back. From the host's point of view a
// corrupted ACK and a lost DATA packet are INDISTINGUISHABLE -- in both cases
// no valid handshake arrived -- so the host resends the DATA packet.
//
// The device already accepted that data. The retransmission is byte-for-byte
// identical to the packet it took. Nothing in the payload says "again".
//
// THE ONLY DISCRIMINATOR IS THE PID TOGGLE, and the correct response to a
// retransmission is threefold and easy to get wrong:
//
// ACK IT AGAIN, but DO NOT write it to the FIFO,
// and DO NOT advance the toggle.
//
// NAK the retransmission -> the host retries for ever; endpoint wedges
// write it again -> silent data duplication, no error anywhere
// advance the toggle -> the next genuine packet looks like a dup and
// is dropped; silent data LOSS
//
// The SystemVerilog build names the handshake outcomes, which matters here
// because H_NONE and H_NAK are the two that get confused: one means "I could
// not read you", the other means "I read you and I am busy". A waveform that
// says NONE rather than 0 makes that distinction visible at the cursor.
package usb_ep_pkg;
typedef enum logic [1:0] {
T_NONE = 2'd0,
T_OUT = 2'd1,
T_IN = 2'd2,
T_SETUP = 2'd3
} token_e;
typedef enum logic [2:0] {
H_NONE = 3'd0, // no response leaves the device at all
H_ACK = 3'd1,
H_NAK = 3'd2,
H_STALL = 3'd3,
H_DATA = 3'd4 // an IN: the device sends a DATA packet
} handshake_e;
endpackage
// THREE MORE RULES THAT ARE NOT NEGOTIABLE
//
// A SETUP can never be NAKed or STALLed. Control transfers are how a host
// recovers a confused device, so the one transaction that must always get
// through is the one carrying the recovery command. A SETUP also clears a
// halt and forces the toggle to DATA1 for the data stage.
//
// A halted endpoint answers STALL to everything except SETUP, and the halt
// is sticky until explicitly cleared.
//
// CLEAR_FEATURE(ENDPOINT_HALT) resets the toggle to DATA0 as well as
// clearing the halt, because host and device must agree on where to
// restart.
//
// A DATA packet whose CRC failed gets NO HANDSHAKE AT ALL -- not a NAK.
// A NAK would claim "I received you and I am busy", which is a lie: the
// device does not know what it received. Silence lets the host's timeout
// fire and the retry happen for the right reason.
module usb_endpoint_ctrl
import usb_ep_pkg::*;
(
input logic clk,
input logic rst_n,
input logic ep_is_in,
input token_e token,
input logic rx_toggle,
input logic rx_crc_ok,
input logic fifo_ready,
input logic in_acked,
input logic halt_set,
input logic halt_clear,
input logic ep_reset,
output handshake_e handshake,
output logic fifo_write_en,
output logic fifo_read_en,
output logic tx_toggle,
output logic data_dup,
output logic expected_toggle,
output logic halted,
output logic [31:0] n_ack,
output logic [31:0] n_nak,
output logic [31:0] n_stall,
output logic [31:0] n_silent,
output logic [31:0] n_dup,
output logic [31:0] n_written
);
logic toggle_r, halted_r;
handshake_e handshake_c;
assign expected_toggle = toggle_r;
assign halted = halted_r;
assign tx_toggle = toggle_r;
// A token is ours only if it matches our direction. A SETUP is addressed to
// a control endpoint, which is an OUT-capable one.
logic is_setup, is_out, is_in, mismatch;
assign is_setup = (token == T_SETUP) && !ep_is_in;
assign is_out = (token == T_OUT) && !ep_is_in;
assign is_in = (token == T_IN) && ep_is_in;
assign mismatch = (token != T_NONE) && !is_setup && !is_out && !is_in;
// A retransmission: a well-formed DATA packet carrying the toggle we are
// NOT expecting. The CRC must pass before the toggle can be believed --
// the toggle is inside the packet the CRC protects.
logic out_dup, out_new;
assign out_dup = is_out && !halted_r && rx_crc_ok && (rx_toggle != toggle_r);
assign out_new = is_out && !halted_r && rx_crc_ok && (rx_toggle == toggle_r);
// THE DECISION. Order is the design: SETUP outranks the halt, the halt
// outranks everything else, and a bad CRC produces silence rather than any
// handshake at all.
always_comb begin
if (token == T_NONE) handshake_c = H_NONE;
else if (mismatch) handshake_c = H_NONE; // not addressed to us
else if (is_setup) handshake_c = H_ACK; // never NAK, never STALL
else if (halted_r) handshake_c = H_STALL;
else if (is_out) begin
if (!rx_crc_ok) handshake_c = H_NONE; // silence, not NAK
else if (out_dup) handshake_c = H_ACK; // re-ACK, take nothing
else if (!fifo_ready) handshake_c = H_NAK;
else handshake_c = H_ACK;
end else begin // is_in
if (!fifo_ready) handshake_c = H_NAK;
else handshake_c = H_DATA;
end
end
assign handshake = handshake_c;
// The write happens ONLY for genuinely new data with room to put it.
assign fifo_write_en = out_new && fifo_ready;
assign fifo_read_en = is_in && !halted_r && fifo_ready;
assign data_dup = out_dup;
// ---- Toggle and halt, as unambiguous priority chains ----
//
// Written as single next-state expressions rather than as a sequence of
// assignments inside the clocked block. A process that assigns the same
// signal twice leaves the answer to the language's last-assignment rule,
// which differs between Verilog and VHDL and hides bugs in exactly the
// place this design cannot afford one.
logic out_advance, in_advance, toggle_next, halted_next;
assign out_advance = fifo_write_en; // OUT: on accept
assign in_advance = is_in && !halted_r && in_acked; // IN: on host ACK
assign toggle_next = ep_reset ? 1'b0 // bus reset: restart at DATA0
: is_setup ? 1'b1 // the data stage begins at DATA1
: halt_clear ? 1'b0 // CLEAR_FEATURE resets the toggle
: (out_advance || in_advance) ? ~toggle_r
: toggle_r;
assign halted_next = ep_reset ? 1'b0
: is_setup ? 1'b0 // a SETUP clears a halt
: halt_clear ? 1'b0
: halt_set ? 1'b1
: halted_r;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
toggle_r <= 1'b0;
halted_r <= 1'b0;
n_ack <= '0;
n_nak <= '0;
n_stall <= '0;
n_silent <= '0;
n_dup <= '0;
n_written <= '0;
end else begin
toggle_r <= toggle_next;
halted_r <= halted_next;
if (handshake_c == H_ACK) n_ack <= n_ack + 1;
if (handshake_c == H_NAK) n_nak <= n_nak + 1;
if (handshake_c == H_STALL) n_stall <= n_stall + 1;
// "Silent" counts only a transaction that WAS addressed to us and got
// no answer -- a CRC failure. An idle cycle is not silence.
if ((is_out || is_in) && (handshake_c == H_NONE))
n_silent <= n_silent + 1;
if (data_dup) n_dup <= n_dup + 1;
if (fifo_write_en) n_written <= n_written + 1;
end
end
endmodule9. VHDL-2008 Implementation
-- usb_endpoint_ctrl -- the per-endpoint state machine, and the one bit that
-- stands between a correct device and silent data duplication.
--
-- THE PROBLEM THE TOGGLE SOLVES
--
-- A USB OUT transaction is three packets: the host sends a token, then a DATA
-- packet, and the device answers with a handshake.
--
-- host: [OUT token][DATA0 ....] device: [ACK]
--
-- That ACK can be corrupted on the way back. From the host's point of view a
-- corrupted ACK and a lost DATA packet are INDISTINGUISHABLE -- in both cases
-- no valid handshake arrived -- so the host resends the DATA packet.
--
-- The device already accepted that data. The retransmission is byte-for-byte
-- identical to the packet it took. Nothing in the payload says "again".
--
-- THE ONLY DISCRIMINATOR IS THE PID TOGGLE, and the correct response to a
-- retransmission is threefold and easy to get wrong:
--
-- ACK IT AGAIN, but DO NOT write it to the FIFO,
-- and DO NOT advance the toggle.
--
-- NAK the retransmission -> the host retries for ever; endpoint wedges
-- write it again -> silent data duplication, no error anywhere
-- advance the toggle -> the next genuine packet looks like a dup and
-- is dropped; silent data LOSS
--
-- VHDL's enumerated types make the two silences distinguishable by TYPE:
-- H_NONE and H_NAK are different values of handshake_t, and a case statement
-- over it must be exhaustive, so a receiver cannot quietly fold "I could not
-- read you" into "I am busy".
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package usb_ep_pkg is
type token_t is (T_NONE, T_OUT, T_IN, T_SETUP);
type handshake_t is (H_NONE, H_ACK, H_NAK, H_STALL, H_DATA);
function token_decode(t : std_logic_vector(1 downto 0)) return token_t;
function hs_code(h : handshake_t) return std_logic_vector;
end package;
package body usb_ep_pkg is
function token_decode(t : std_logic_vector(1 downto 0)) return token_t is
begin
case t is
when "00" => return T_NONE;
when "01" => return T_OUT;
when "10" => return T_IN;
when others => return T_SETUP;
end case;
end function;
function hs_code(h : handshake_t) return std_logic_vector is
begin
return std_logic_vector(to_unsigned(handshake_t'pos(h), 3));
end function;
end package body;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_ep_pkg.all;
-- THREE MORE RULES THAT ARE NOT NEGOTIABLE
--
-- A SETUP can never be NAKed or STALLed. Control transfers are how a host
-- recovers a confused device, so the one transaction that must always get
-- through is the one carrying the recovery command. A SETUP also clears a
-- halt and forces the toggle to DATA1 for the data stage.
--
-- A halted endpoint answers STALL to everything except SETUP, and the halt
-- is sticky until explicitly cleared.
--
-- CLEAR_FEATURE(ENDPOINT_HALT) resets the toggle to DATA0 as well as
-- clearing the halt, because host and device must agree on where to restart.
--
-- A DATA packet whose CRC failed gets NO HANDSHAKE AT ALL -- not a NAK.
-- A NAK would claim "I received you and I am busy", which is a lie: the
-- device does not know what it received. Silence lets the host's timeout
-- fire and the retry happen for the right reason.
entity usb_endpoint_ctrl is
port (
clk : in std_logic;
rst_n : in std_logic;
ep_is_in : in std_logic;
token : in std_logic_vector(1 downto 0);
rx_toggle : in std_logic;
rx_crc_ok : in std_logic;
fifo_ready : in std_logic;
in_acked : in std_logic;
halt_set : in std_logic;
halt_clear : in std_logic;
ep_reset : in std_logic;
handshake : out std_logic_vector(2 downto 0);
fifo_write_en : out std_logic;
fifo_read_en : out std_logic;
tx_toggle : out std_logic;
data_dup : out std_logic;
expected_toggle : out std_logic;
halted : out std_logic;
n_ack : out std_logic_vector(31 downto 0);
n_nak : out std_logic_vector(31 downto 0);
n_stall : out std_logic_vector(31 downto 0);
n_silent : out std_logic_vector(31 downto 0);
n_dup : out std_logic_vector(31 downto 0);
n_written : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of usb_endpoint_ctrl is
signal tok : token_t;
signal toggle_r : std_logic := '0';
signal halted_r : std_logic := '0';
signal is_setup, is_out, is_in, mismatch : std_logic;
signal out_dup, out_new : std_logic;
signal hs : handshake_t;
signal wr_en, rd_en : std_logic;
signal toggle_next, halted_next : std_logic;
signal ack_r, nak_r, stall_r, sil_r, dup_r, wr_r
: unsigned(31 downto 0) := (others => '0');
begin
tok <= token_decode(token);
expected_toggle <= toggle_r;
halted <= halted_r;
tx_toggle <= toggle_r;
-- A token is ours only if it matches our direction. A SETUP is addressed to
-- a control endpoint, which is an OUT-capable one.
is_setup <= '1' when (tok = T_SETUP and ep_is_in = '0') else '0';
is_out <= '1' when (tok = T_OUT and ep_is_in = '0') else '0';
is_in <= '1' when (tok = T_IN and ep_is_in = '1') else '0';
mismatch <= '1' when (tok /= T_NONE and is_setup = '0'
and is_out = '0' and is_in = '0') else '0';
-- A retransmission: a well-formed DATA packet carrying the toggle we are
-- NOT expecting. The CRC must pass before the toggle can be believed --
-- the toggle is inside the packet the CRC protects.
out_dup <= '1' when (is_out = '1' and halted_r = '0' and rx_crc_ok = '1'
and rx_toggle /= toggle_r) else '0';
out_new <= '1' when (is_out = '1' and halted_r = '0' and rx_crc_ok = '1'
and rx_toggle = toggle_r) else '0';
-- THE DECISION. Order is the design: SETUP outranks the halt, the halt
-- outranks everything else, and a bad CRC produces silence rather than any
-- handshake at all.
decide : process (tok, mismatch, is_setup, is_out, halted_r, rx_crc_ok,
out_dup, fifo_ready)
begin
if tok = T_NONE then
hs <= H_NONE;
elsif mismatch = '1' then
hs <= H_NONE; -- not addressed to us
elsif is_setup = '1' then
hs <= H_ACK; -- never NAK, never STALL
elsif halted_r = '1' then
hs <= H_STALL;
elsif is_out = '1' then
if rx_crc_ok = '0' then
hs <= H_NONE; -- silence, not NAK
elsif out_dup = '1' then
hs <= H_ACK; -- re-ACK, take nothing
elsif fifo_ready = '0' then
hs <= H_NAK;
else
hs <= H_ACK;
end if;
else -- is_in
if fifo_ready = '0' then
hs <= H_NAK;
else
hs <= H_DATA;
end if;
end if;
end process;
handshake <= hs_code(hs);
-- The write happens ONLY for genuinely new data with room to put it.
wr_en <= '1' when (out_new = '1' and fifo_ready = '1') else '0';
rd_en <= '1' when (is_in = '1' and halted_r = '0' and fifo_ready = '1')
else '0';
fifo_write_en <= wr_en;
fifo_read_en <= rd_en;
data_dup <= out_dup;
-- ---- Toggle and halt, as unambiguous priority chains ----
--
-- Written as single next-state expressions rather than as a sequence of
-- assignments inside the clocked process. A process that assigns the same
-- signal twice leaves the answer to VHDL's last-assignment-wins rule, which
-- differs from what a Verilog reader expects and hides bugs in exactly the
-- place this design cannot afford one.
toggle_next <= '0' when ep_reset = '1' -- restart at DATA0
else '1' when is_setup = '1' -- data stage: DATA1
else '0' when halt_clear = '1' -- CLEAR_FEATURE
else not toggle_r when (wr_en = '1' -- OUT: on accept
or (is_in = '1' and halted_r = '0'
and in_acked = '1')) -- IN: on ACK
else toggle_r;
halted_next <= '0' when ep_reset = '1'
else '0' when is_setup = '1' -- a SETUP un-halts
else '0' when halt_clear = '1'
else '1' when halt_set = '1'
else halted_r;
regs : process (clk, rst_n)
begin
if rst_n = '0' then
toggle_r <= '0';
halted_r <= '0';
ack_r <= (others => '0');
nak_r <= (others => '0');
stall_r <= (others => '0');
sil_r <= (others => '0');
dup_r <= (others => '0');
wr_r <= (others => '0');
elsif rising_edge(clk) then
toggle_r <= toggle_next;
halted_r <= halted_next;
if hs = H_ACK then
ack_r <= ack_r + 1;
end if;
if hs = H_NAK then
nak_r <= nak_r + 1;
end if;
if hs = H_STALL then
stall_r <= stall_r + 1;
end if;
-- "Silent" counts only a transaction that WAS addressed to us and got
-- no answer -- a CRC failure. An idle cycle is not silence.
if (is_out = '1' or is_in = '1') and hs = H_NONE then
sil_r <= sil_r + 1;
end if;
if out_dup = '1' then
dup_r <= dup_r + 1;
end if;
if wr_en = '1' then
wr_r <= wr_r + 1;
end if;
end if;
end process;
n_ack <= std_logic_vector(ack_r);
n_nak <= std_logic_vector(nak_r);
n_stall <= std_logic_vector(stall_r);
n_silent <= std_logic_vector(sil_r);
n_dup <= std_logic_vector(dup_r);
n_written <= std_logic_vector(wr_r);
end architecture;10. Seeing the Retransmission
A lost ACK, the retransmission, and the packet that follows
usb_endpoint_ctrl — absorbing a retransmission
10 cyclesRead n_written across the bottom. Three packets arrived. Two were stored. And expected_toggle does not move at cycle 3 — it was already right.
11. The Testbenches
Each suite sweeps all 2048 one-step transitions, runs ten directed scenarios that tell the retransmission story end to end, and then 40 000 randomised cycles against a reference model deliberately built by a different route — a case (1'b1) priority tree where the design uses a ternary chain, and a flat if-chain over booleans in VHDL where the design uses signals and an enumerated decision.
11.1 Verilog testbench
`timescale 1ns/1ps
module tb_ep_v;
reg clk=0, rst_n=0;
reg ep_is_in=0, rx_toggle=0, rx_crc_ok=1, fifo_ready=1;
reg in_acked=0, halt_set=0, halt_clear=0, ep_reset=0;
reg [1:0] token=0;
wire [2:0] handshake;
wire fifo_write_en, fifo_read_en, tx_toggle, data_dup;
wire expected_toggle, halted;
wire [31:0] n_ack, n_nak, n_stall, n_silent, n_dup, n_written;
always #5 clk=~clk;
usb_endpoint_ctrl dut (
.clk(clk), .rst_n(rst_n), .ep_is_in(ep_is_in), .token(token),
.rx_toggle(rx_toggle), .rx_crc_ok(rx_crc_ok), .fifo_ready(fifo_ready),
.in_acked(in_acked), .halt_set(halt_set), .halt_clear(halt_clear),
.ep_reset(ep_reset), .handshake(handshake),
.fifo_write_en(fifo_write_en), .fifo_read_en(fifo_read_en),
.tx_toggle(tx_toggle), .data_dup(data_dup),
.expected_toggle(expected_toggle), .halted(halted),
.n_ack(n_ack), .n_nak(n_nak), .n_stall(n_stall), .n_silent(n_silent),
.n_dup(n_dup), .n_written(n_written));
localparam [1:0] T_NONE=0, T_OUT=1, T_IN=2, T_SETUP=3;
localparam [2:0] H_NONE=0, H_ACK=1, H_NAK=2, H_STALL=3, H_DATA=4;
integer errors=0, i, s, a, b, c, d, e, f, g, h;
integer n_trans=0;
integer n_hs [0:4];
integer n_st [0:3];
integer n_wr=0, n_du=0, n_si=0;
integer m_ack, m_nak, m_stall, m_sil, m_dup, m_wr;
// The model's prediction of the NEXT state, stashed before the clock edge
// so it can be compared after it. Checking only the combinational outputs
// and the counters leaves every state-only bug invisible.
reg g_tog_n, g_hlt_n;
task check(input cond, input [639:0] msg);
begin if (!cond) begin errors=errors+1;
if (errors <= 25)
$display(" FAIL: %0s (in=%b tok=%0d rxT=%b crc=%b rdy=%b iack=%b hs=%b hc=%b rst=%b | tog=%b hlt=%b -> hsk=%0d wr=%b dup=%b, t=%0t)",
msg, ep_is_in, token, rx_toggle, rx_crc_ok, fifo_ready,
in_acked, halt_set, halt_clear, ep_reset, expected_toggle,
halted, handshake, fifo_write_en, data_dup, $time);
end end
endtask
// ---- independent reference model, built as a case tree where the design
// ---- uses a ternary chain ----
task model(output [2:0] e_hs, output e_wr, output e_rd, output e_dup,
output e_tog_n, output e_hlt_n);
reg setup_, out_, in_, mis_, dup_, new_;
begin
setup_ = (token == T_SETUP) && !ep_is_in;
out_ = (token == T_OUT) && !ep_is_in;
in_ = (token == T_IN) && ep_is_in;
mis_ = (token != T_NONE) && !setup_ && !out_ && !in_;
dup_ = out_ && !halted && rx_crc_ok && (rx_toggle != expected_toggle);
new_ = out_ && !halted && rx_crc_ok && (rx_toggle == expected_toggle);
case (1'b1)
(token == T_NONE): e_hs = H_NONE;
mis_: e_hs = H_NONE;
setup_: e_hs = H_ACK;
halted: e_hs = H_STALL;
out_: begin
if (!rx_crc_ok) e_hs = H_NONE;
else if (dup_) e_hs = H_ACK;
else if (!fifo_ready) e_hs = H_NAK;
else e_hs = H_ACK;
end
default: begin // in_
if (!fifo_ready) e_hs = H_NAK; else e_hs = H_DATA;
end
endcase
e_wr = new_ && fifo_ready;
e_rd = in_ && !halted && fifo_ready;
e_dup = dup_;
if (ep_reset) e_tog_n = 1'b0;
else if (setup_) e_tog_n = 1'b1;
else if (halt_clear) e_tog_n = 1'b0;
else if (e_wr || (in_ && !halted && in_acked)) e_tog_n = ~expected_toggle;
else e_tog_n = expected_toggle;
if (ep_reset) e_hlt_n = 1'b0;
else if (setup_) e_hlt_n = 1'b0;
else if (halt_clear) e_hlt_n = 1'b0;
else if (halt_set) e_hlt_n = 1'b1;
else e_hlt_n = halted;
end
endtask
task check_comb;
reg [2:0] e_hs; reg e_wr, e_rd, e_dup, e_tn, e_hn;
begin
model(e_hs, e_wr, e_rd, e_dup, e_tn, e_hn);
g_tog_n = e_tn;
g_hlt_n = e_hn;
check(handshake === e_hs, "handshake matches the model");
check(fifo_write_en === e_wr, "fifo_write_en matches the model");
check(fifo_read_en === e_rd, "fifo_read_en matches the model");
check(data_dup === e_dup, "data_dup matches the model");
check(tx_toggle === expected_toggle,
"the transmitted toggle is the expected toggle");
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE property. A retransmission is ACKed and NOT written. Writing
// it duplicates data silently; NAKing it wedges the endpoint.
if (data_dup) begin
check(handshake === H_ACK,
"a retransmission was not ACKed -- the host will retry for ever");
check(!fifo_write_en,
"a retransmission was WRITTEN -- silent data duplication");
end
// 2. A SETUP is never refused. It is how a host recovers a device.
if ((token == T_SETUP) && !ep_is_in)
check(handshake === H_ACK,
"a SETUP was NAKed or STALLed -- recovery is now impossible");
// 3. Nothing is ever written on a packet whose CRC failed.
if (!rx_crc_ok)
check(!fifo_write_en,
"data with a failed CRC was written to the endpoint FIFO");
// 4. A failed CRC produces SILENCE, never a NAK: a NAK would claim
// knowledge of a packet the device could not read.
if ((token == T_OUT) && !ep_is_in && !rx_crc_ok && !halted)
check(handshake === H_NONE,
"a corrupt packet was answered -- the host was told a lie");
// 5. A halted endpoint STALLs everything except a SETUP.
if (halted && (token != T_NONE) && !((token == T_SETUP) && !ep_is_in))
check(handshake === H_STALL || handshake === H_NONE,
"a halted endpoint answered something other than STALL");
// 6. Never write and report a duplicate in the same cycle.
check(!(fifo_write_en && data_dup),
"a packet was both accepted as new and absorbed as a duplicate");
// 7. A token for the other direction is never answered.
if ((token == T_OUT) && ep_is_in)
check(handshake === H_NONE, "an IN endpoint answered an OUT token");
if ((token == T_IN) && !ep_is_in)
check(handshake === H_NONE, "an OUT endpoint answered an IN token");
if (e_hs <= 4) n_hs[e_hs] = n_hs[e_hs] + 1;
n_st[{expected_toggle, halted}] = n_st[{expected_toggle, halted}] + 1;
if (e_wr) n_wr = n_wr + 1;
if (e_dup) n_du = n_du + 1;
if (((token==T_OUT)&&!ep_is_in) || ((token==T_IN)&&ep_is_in))
if (e_hs == H_NONE) n_si = n_si + 1;
end
endtask
task step;
begin
#1;
check_comb;
if (handshake === H_ACK) m_ack = m_ack + 1;
if (handshake === H_NAK) m_nak = m_nak + 1;
if (handshake === H_STALL) m_stall = m_stall + 1;
if ((((token==T_OUT)&&!ep_is_in) || ((token==T_IN)&&ep_is_in))
&& (handshake === H_NONE)) m_sil = m_sil + 1;
if (data_dup) m_dup = m_dup + 1;
if (fifo_write_en) m_wr = m_wr + 1;
@(posedge clk); #1;
// THE state check. Without it a mutation that corrupts only the toggle
// or only the halt is nearly invisible: every combinational output and
// every counter can still be correct in the cycle it happens.
check(expected_toggle === g_tog_n,
"the toggle took the next value the model predicted");
check(halted === g_hlt_n,
"the halt took the next value the model predicted");
check(n_ack === m_ack[31:0], "n_ack matches the model");
check(n_nak === m_nak[31:0], "n_nak matches the model");
check(n_stall === m_stall[31:0], "n_stall matches the model");
check(n_silent === m_sil[31:0], "n_silent matches the model");
check(n_dup === m_dup[31:0], "n_dup matches the model");
check(n_written === m_wr[31:0], "n_written matches the model");
end
endtask
task idle;
begin
token=T_NONE; halt_set=0; halt_clear=0; ep_reset=0; in_acked=0;
rx_crc_ok=1; fifo_ready=1;
end
endtask
task hard_reset;
begin
rst_n=0; ep_is_in=0; idle;
@(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
m_ack=0; m_nak=0; m_stall=0; m_sil=0; m_dup=0; m_wr=0;
end
endtask
// Force the endpoint into a chosen (toggle, halted) state without relying
// on the very logic under test to have got there by itself.
task goto_state(input want_tog, input want_hlt);
begin
idle; ep_reset=1; @(posedge clk); #1; // -> (0,0)
idle;
if (want_tog) begin
// A SETUP is the only legitimate way to reach DATA1, and it is a real
// transaction: it produces an ACK, so the model's counter must be
// told about it or the counter checks drift by one per setup.
ep_is_in=0; token=T_SETUP; @(posedge clk); #1; idle; // -> (1,0)
m_ack = m_ack + 1;
end
if (want_hlt) begin
halt_set=1; @(posedge clk); #1; idle;
end
#1;
check(expected_toggle === want_tog, "goto_state reached the toggle");
check(halted === want_hlt, "goto_state reached the halt state");
end
endtask
initial begin
for (i=0;i<5;i=i+1) n_hs[i]=0;
for (i=0;i<4;i=i+1) n_st[i]=0;
hard_reset;
check(expected_toggle === 1'b0, "reset starts at DATA0");
check(!halted, "and not halted");
// ===== A. EXHAUSTIVE one-step transition sweep =====
// 4 internal states (toggle x halted) x 512 input combinations
// = 2048 transitions: every reachable (state, input) pair exactly once.
for (s=0; s<4; s=s+1)
for (a=0;a<2;a=a+1) // ep_is_in
for (b=0;b<4;b=b+1) // token
for (c=0;c<2;c=c+1) // rx_toggle
for (d=0;d<2;d=d+1) // rx_crc_ok
for (e=0;e<2;e=e+1) // fifo_ready
for (f=0;f<2;f=f+1) // in_acked
for (g=0;g<2;g=g+1) // halt_set
for (h=0;h<2;h=h+1) begin // halt_clear
goto_state(s[1], s[0]);
ep_is_in=a[0]; token=b[1:0]; rx_toggle=c[0]; rx_crc_ok=d[0];
fifo_ready=e[0]; in_acked=f[0]; halt_set=g[0]; halt_clear=h[0];
ep_reset=0;
step;
n_trans = n_trans + 1;
idle;
end
$display(" exhaustive endpoint transition sweep: %0d of %0d verified",
n_trans, 4*2*4*2*2*2*2*2*2);
// ===== B. directed: the retransmission story, end to end =====
hard_reset; ep_is_in=0;
// 1. A normal OUT: DATA0 arrives when DATA0 is expected.
token=T_OUT; rx_toggle=0; rx_crc_ok=1; fifo_ready=1; step; idle;
check(n_written === 32'd1, "the first packet was written");
#1; check(expected_toggle === 1'b1, "and the toggle advanced to DATA1");
// 2. THE case. The host never saw our ACK and sends DATA0 again.
token=T_OUT; rx_toggle=0; rx_crc_ok=1; fifo_ready=1; #1;
check(data_dup, "the repeated DATA0 is recognised as a retransmission");
check(handshake === H_ACK, "it is ACKed again, because the host is waiting");
check(!fifo_write_en, "but NOT written -- that data is already in the FIFO");
step; idle;
check(n_written === 32'd1, "still exactly one packet written");
check(n_dup === 32'd1, "and one duplicate absorbed");
#1; check(expected_toggle === 1'b1,
"and the toggle did NOT advance -- it was already correct");
// 3. The next genuine packet is DATA1 and is accepted normally.
token=T_OUT; rx_toggle=1; rx_crc_ok=1; fifo_ready=1; step; idle;
check(n_written === 32'd2, "the next genuine packet was written");
#1; check(expected_toggle === 1'b0, "and the toggle advanced back to DATA0");
// 4. A corrupt packet: silence, no write, no toggle movement.
token=T_OUT; rx_toggle=0; rx_crc_ok=0; fifo_ready=1; #1;
check(handshake === H_NONE,
"a failed CRC is answered with SILENCE, not a NAK");
check(!fifo_write_en, "and nothing is written");
step; idle;
#1; check(expected_toggle === 1'b0, "and the toggle is unchanged");
// 5. Full FIFO: NAK, no write, no toggle movement.
token=T_OUT; rx_toggle=0; rx_crc_ok=1; fifo_ready=0; #1;
check(handshake === H_NAK, "a full FIFO produces a NAK");
check(!fifo_write_en, "with no write");
step; idle;
#1; check(expected_toggle === 1'b0, "and no toggle movement");
// 6. Halt the endpoint: everything STALLs.
halt_set=1; step; idle;
#1; check(halted, "the endpoint is halted");
token=T_OUT; rx_toggle=0; #1;
check(handshake === H_STALL, "and an OUT now STALLs");
step; idle;
// 7. But a SETUP still gets through, and clears the halt.
token=T_SETUP; #1;
check(handshake === H_ACK, "a SETUP is ACKed even on a halted endpoint");
step; idle;
#1; check(!halted, "the SETUP cleared the halt");
check(expected_toggle === 1'b1,
"and set the toggle to DATA1 for the data stage");
// 8. CLEAR_FEATURE resets the toggle to DATA0 as well as clearing halt.
halt_set=1; step; idle; #1; check(halted, "halted again");
halt_clear=1; step; idle;
#1; check(!halted, "CLEAR_FEATURE cleared the halt");
check(expected_toggle === 1'b0,
"and reset the toggle to DATA0 -- host and device must agree");
// 9. An IN endpoint: the toggle advances on the host's ACK, not on send.
hard_reset; ep_is_in=1;
token=T_IN; fifo_ready=1; in_acked=0; #1;
check(handshake === H_DATA, "an IN with data available sends DATA");
check(fifo_read_en, "and reads the FIFO");
step; idle;
#1; check(expected_toggle === 1'b0,
"the toggle has NOT advanced -- the host has not ACKed yet");
token=T_IN; fifo_ready=1; in_acked=1; step; idle;
#1; check(expected_toggle === 1'b1, "now the host ACKed, the toggle moved");
// 10. An IN with no data NAKs.
token=T_IN; fifo_ready=0; #1;
check(handshake === H_NAK, "an IN with an empty FIFO NAKs");
step; idle;
// ===== C. randomised =====
hard_reset;
for (i=0;i<40000;i=i+1) begin
ep_is_in = ({$random}%4)==0;
token = {$random}%4;
rx_toggle= {$random}%2;
rx_crc_ok= ({$random}%16)!=0;
fifo_ready=({$random}%4)!=0;
in_acked = ({$random}%2);
halt_set = ({$random}%32)==0;
halt_clear=({$random}%32)==0;
ep_reset = ({$random}%64)==0;
step;
end
for (i=0;i<5;i=i+1)
check(n_hs[i] > 0, "every handshake outcome was reached");
for (i=0;i<4;i=i+1)
check(n_st[i] > 0, "every (toggle, halted) state was reached");
check(n_du > 500, "retransmissions were absorbed many times");
check(n_si > 100, "corrupt packets produced silence many times");
$display("");
$display(" REACH: transitions=%0d | handshakes: none=%0d ack=%0d nak=%0d stall=%0d data=%0d",
n_trans, n_hs[0], n_hs[1], n_hs[2], n_hs[3], n_hs[4]);
$display(" STATES: (tog0,run)=%0d (tog0,halt)=%0d (tog1,run)=%0d (tog1,halt)=%0d | written=%0d dups=%0d silent=%0d",
n_st[0], n_st[1], n_st[2], n_st[3], n_wr, n_du, n_si);
$display(" COUNTERS: ack=%0d nak=%0d stall=%0d silent=%0d dup=%0d written=%0d",
n_ack, n_nak, n_stall, n_silent, n_dup, n_written);
$display(" [Verilog] usb_endpoint_ctrl: %0d errors", errors);
$display(" [Verilog] %0s", errors==0 ? "PASS" : "FAIL");
$display("");
$finish;
end
endmodule11.2 SystemVerilog testbench
`timescale 1ns/1ps
module tb_ep_sv;
import usb_ep_pkg::*;
logic clk=0, rst_n=0;
logic ep_is_in=0, rx_toggle=0, rx_crc_ok=1, fifo_ready=1;
logic in_acked=0, halt_set=0, halt_clear=0, ep_reset=0;
token_e token = T_NONE;
handshake_e handshake;
logic fifo_write_en, fifo_read_en, tx_toggle, data_dup;
logic expected_toggle, halted;
logic [31:0] n_ack, n_nak, n_stall, n_silent, n_dup, n_written;
always #5 clk=~clk;
usb_endpoint_ctrl dut (
.clk, .rst_n, .ep_is_in, .token, .rx_toggle, .rx_crc_ok, .fifo_ready,
.in_acked, .halt_set, .halt_clear, .ep_reset, .handshake,
.fifo_write_en, .fifo_read_en, .tx_toggle, .data_dup,
.expected_toggle, .halted,
.n_ack, .n_nak, .n_stall, .n_silent, .n_dup, .n_written);
int errors=0, i, s, a, b, c, d, e, f, g, h;
int n_trans=0;
int n_hs [5];
int n_st [4];
int n_wr=0, n_du=0, n_si=0;
int m_ack, m_nak, m_stall, m_sil, m_dup, m_wr;
// The model's prediction of the NEXT state, stashed before the clock edge
// so it can be compared after it. Checking only the combinational outputs
// and the counters leaves every state-only bug invisible.
bit g_tog_n, g_hlt_n;
// Icarus seeds $random and $urandom identically, so an unseeded run would
// replay the Verilog suite's stimulus exactly and the two columns would
// stop being independent evidence. See chapter 20.5 section 9.2.
int urandom_seed = 21101;
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.
token_e tk_v; handshake_e hs_v;
if (!cond) begin
errors++;
tk_v = token; hs_v = handshake;
if (errors <= 25)
$display(" FAIL: %0s (in=%b tok=%s rxT=%b crc=%b rdy=%b iack=%b hs=%b hc=%b rst=%b | tog=%b hlt=%b -> hsk=%s wr=%b dup=%b, t=%0t)",
msg, ep_is_in, tk_v.name(), rx_toggle, rx_crc_ok, fifo_ready,
in_acked, halt_set, halt_clear, ep_reset, expected_toggle,
halted, hs_v.name(), fifo_write_en, data_dup, $time);
end
endtask
// ---- independent reference model, built as a case tree where the design
// ---- uses a ternary chain ----
task automatic model(output handshake_e e_hs, output bit e_wr,
output bit e_rd, output bit e_dup,
output bit e_tog_n, output bit e_hlt_n);
bit setup_, out_, in_, mis_, dup_, new_;
begin
setup_ = (token == T_SETUP) && !ep_is_in;
out_ = (token == T_OUT) && !ep_is_in;
in_ = (token == T_IN) && ep_is_in;
mis_ = (token != T_NONE) && !setup_ && !out_ && !in_;
dup_ = out_ && !halted && rx_crc_ok && (rx_toggle != expected_toggle);
new_ = out_ && !halted && rx_crc_ok && (rx_toggle == expected_toggle);
case (1'b1)
(token == T_NONE): e_hs = H_NONE;
mis_: e_hs = H_NONE;
setup_: e_hs = H_ACK;
halted: e_hs = H_STALL;
out_: begin
if (!rx_crc_ok) e_hs = H_NONE;
else if (dup_) e_hs = H_ACK;
else if (!fifo_ready) e_hs = H_NAK;
else e_hs = H_ACK;
end
default: begin // in_
if (!fifo_ready) e_hs = H_NAK; else e_hs = H_DATA;
end
endcase
e_wr = new_ && fifo_ready;
e_rd = in_ && !halted && fifo_ready;
e_dup = dup_;
if (ep_reset) e_tog_n = 1'b0;
else if (setup_) e_tog_n = 1'b1;
else if (halt_clear) e_tog_n = 1'b0;
else if (e_wr || (in_ && !halted && in_acked)) e_tog_n = ~expected_toggle;
else e_tog_n = expected_toggle;
if (ep_reset) e_hlt_n = 1'b0;
else if (setup_) e_hlt_n = 1'b0;
else if (halt_clear) e_hlt_n = 1'b0;
else if (halt_set) e_hlt_n = 1'b1;
else e_hlt_n = halted;
end
endtask
task automatic check_comb;
handshake_e e_hs; bit e_wr, e_rd, e_dup, e_tn, e_hn;
begin
model(e_hs, e_wr, e_rd, e_dup, e_tn, e_hn);
g_tog_n = e_tn;
g_hlt_n = e_hn;
check(handshake === e_hs, "handshake matches the model");
check(fifo_write_en === e_wr, "fifo_write_en matches the model");
check(fifo_read_en === e_rd, "fifo_read_en matches the model");
check(data_dup === e_dup, "data_dup matches the model");
check(tx_toggle === expected_toggle,
"the transmitted toggle is the expected toggle");
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE property. A retransmission is ACKed and NOT written. Writing
// it duplicates data silently; NAKing it wedges the endpoint.
if (data_dup) begin
check(handshake === H_ACK,
"a retransmission was not ACKed -- the host will retry for ever");
check(!fifo_write_en,
"a retransmission was WRITTEN -- silent data duplication");
end
// 2. A SETUP is never refused. It is how a host recovers a device.
if ((token == T_SETUP) && !ep_is_in)
check(handshake === H_ACK,
"a SETUP was NAKed or STALLed -- recovery is now impossible");
// 3. Nothing is ever written on a packet whose CRC failed.
if (!rx_crc_ok)
check(!fifo_write_en,
"data with a failed CRC was written to the endpoint FIFO");
// 4. A failed CRC produces SILENCE, never a NAK: a NAK would claim
// knowledge of a packet the device could not read.
if ((token == T_OUT) && !ep_is_in && !rx_crc_ok && !halted)
check(handshake === H_NONE,
"a corrupt packet was answered -- the host was told a lie");
// 5. A halted endpoint STALLs everything except a SETUP.
if (halted && (token != T_NONE) && !((token == T_SETUP) && !ep_is_in))
check(handshake === H_STALL || handshake === H_NONE,
"a halted endpoint answered something other than STALL");
// 6. Never write and report a duplicate in the same cycle.
check(!(fifo_write_en && data_dup),
"a packet was both accepted as new and absorbed as a duplicate");
// 7. A token for the other direction is never answered.
if ((token == T_OUT) && ep_is_in)
check(handshake === H_NONE, "an IN endpoint answered an OUT token");
if ((token == T_IN) && !ep_is_in)
check(handshake === H_NONE, "an OUT endpoint answered an IN token");
n_hs[int'(e_hs)] = n_hs[int'(e_hs)] + 1;
n_st[int'({expected_toggle, halted})] = n_st[int'({expected_toggle, halted})] + 1;
if (e_wr) n_wr = n_wr + 1;
if (e_dup) n_du = n_du + 1;
if (((token==T_OUT)&&!ep_is_in) || ((token==T_IN)&&ep_is_in))
if (e_hs == H_NONE) n_si = n_si + 1;
end
endtask
task automatic step;
begin
#1;
check_comb;
if (handshake === H_ACK) m_ack = m_ack + 1;
if (handshake === H_NAK) m_nak = m_nak + 1;
if (handshake === H_STALL) m_stall = m_stall + 1;
if ((((token==T_OUT)&&!ep_is_in) || ((token==T_IN)&&ep_is_in))
&& (handshake === H_NONE)) m_sil = m_sil + 1;
if (data_dup) m_dup = m_dup + 1;
if (fifo_write_en) m_wr = m_wr + 1;
@(posedge clk); #1;
// THE state check. Without it a mutation that corrupts only the toggle
// or only the halt is nearly invisible: every combinational output and
// every counter can still be correct in the cycle it happens.
check(expected_toggle === g_tog_n,
"the toggle took the next value the model predicted");
check(halted === g_hlt_n,
"the halt took the next value the model predicted");
check(n_ack === 32'(m_ack), "n_ack matches the model");
check(n_nak === 32'(m_nak), "n_nak matches the model");
check(n_stall === 32'(m_stall), "n_stall matches the model");
check(n_silent === 32'(m_sil), "n_silent matches the model");
check(n_dup === 32'(m_dup), "n_dup matches the model");
check(n_written === 32'(m_wr), "n_written matches the model");
end
endtask
task automatic idle;
begin
token=T_NONE; halt_set=0; halt_clear=0; ep_reset=0; in_acked=0;
rx_crc_ok=1; fifo_ready=1;
end
endtask
task automatic hard_reset;
begin
rst_n=0; ep_is_in=0; idle;
@(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
m_ack=0; m_nak=0; m_stall=0; m_sil=0; m_dup=0; m_wr=0;
end
endtask
// Force the endpoint into a chosen (toggle, halted) state without relying
// on the very logic under test to have got there by itself.
task automatic goto_state(input bit want_tog, input bit want_hlt);
begin
idle; ep_reset=1; @(posedge clk); #1; // -> (0,0)
idle;
if (want_tog) begin
// A SETUP is the only legitimate way to reach DATA1, and it is a real
// transaction: it produces an ACK, so the model's counter must be
// told about it or the counter checks drift by one per setup.
ep_is_in=0; token=T_SETUP; @(posedge clk); #1; idle; // -> (1,0)
m_ack = m_ack + 1;
end
if (want_hlt) begin
halt_set=1; @(posedge clk); #1; idle;
end
#1;
check(expected_toggle === want_tog, "goto_state reached the toggle");
check(halted === want_hlt, "goto_state reached the halt state");
end
endtask
initial begin
void'($urandom(urandom_seed));
foreach (n_hs[i]) n_hs[i]=0;
foreach (n_st[i]) n_st[i]=0;
hard_reset;
check(expected_toggle === 1'b0, "reset starts at DATA0");
check(!halted, "and not halted");
// ===== A. EXHAUSTIVE one-step transition sweep =====
// 4 internal states (toggle x halted) x 512 input combinations
// = 2048 transitions: every reachable (state, input) pair exactly once.
for (s=0; s<4; s=s+1)
for (a=0;a<2;a=a+1) // ep_is_in
for (b=0;b<4;b=b+1) // token
for (c=0;c<2;c=c+1) // rx_toggle
for (d=0;d<2;d=d+1) // rx_crc_ok
for (e=0;e<2;e=e+1) // fifo_ready
for (f=0;f<2;f=f+1) // in_acked
for (g=0;g<2;g=g+1) // halt_set
for (h=0;h<2;h=h+1) begin // halt_clear
goto_state(s[1], s[0]);
ep_is_in=a[0]; token=token_e'(b[1:0]); rx_toggle=c[0]; rx_crc_ok=d[0];
fifo_ready=e[0]; in_acked=f[0]; halt_set=g[0]; halt_clear=h[0];
ep_reset=0;
step;
n_trans = n_trans + 1;
idle;
end
$display(" exhaustive endpoint transition sweep: %0d of %0d verified",
n_trans, 4*2*4*2*2*2*2*2*2);
// ===== B. directed: the retransmission story, end to end =====
hard_reset; ep_is_in=0;
// 1. A normal OUT: DATA0 arrives when DATA0 is expected.
token=T_OUT; rx_toggle=0; rx_crc_ok=1; fifo_ready=1; step; idle;
check(n_written === 32'd1, "the first packet was written");
#1; check(expected_toggle === 1'b1, "and the toggle advanced to DATA1");
// 2. THE case. The host never saw our ACK and sends DATA0 again.
token=T_OUT; rx_toggle=0; rx_crc_ok=1; fifo_ready=1; #1;
check(data_dup, "the repeated DATA0 is recognised as a retransmission");
check(handshake === H_ACK, "it is ACKed again, because the host is waiting");
check(!fifo_write_en, "but NOT written -- that data is already in the FIFO");
step; idle;
check(n_written === 32'd1, "still exactly one packet written");
check(n_dup === 32'd1, "and one duplicate absorbed");
#1; check(expected_toggle === 1'b1,
"and the toggle did NOT advance -- it was already correct");
// 3. The next genuine packet is DATA1 and is accepted normally.
token=T_OUT; rx_toggle=1; rx_crc_ok=1; fifo_ready=1; step; idle;
check(n_written === 32'd2, "the next genuine packet was written");
#1; check(expected_toggle === 1'b0, "and the toggle advanced back to DATA0");
// 4. A corrupt packet: silence, no write, no toggle movement.
token=T_OUT; rx_toggle=0; rx_crc_ok=0; fifo_ready=1; #1;
check(handshake === H_NONE,
"a failed CRC is answered with SILENCE, not a NAK");
check(!fifo_write_en, "and nothing is written");
step; idle;
#1; check(expected_toggle === 1'b0, "and the toggle is unchanged");
// 5. Full FIFO: NAK, no write, no toggle movement.
token=T_OUT; rx_toggle=0; rx_crc_ok=1; fifo_ready=0; #1;
check(handshake === H_NAK, "a full FIFO produces a NAK");
check(!fifo_write_en, "with no write");
step; idle;
#1; check(expected_toggle === 1'b0, "and no toggle movement");
// 6. Halt the endpoint: everything STALLs.
halt_set=1; step; idle;
#1; check(halted, "the endpoint is halted");
token=T_OUT; rx_toggle=0; #1;
check(handshake === H_STALL, "and an OUT now STALLs");
step; idle;
// 7. But a SETUP still gets through, and clears the halt.
token=T_SETUP; #1;
check(handshake === H_ACK, "a SETUP is ACKed even on a halted endpoint");
step; idle;
#1; check(!halted, "the SETUP cleared the halt");
check(expected_toggle === 1'b1,
"and set the toggle to DATA1 for the data stage");
// 8. CLEAR_FEATURE resets the toggle to DATA0 as well as clearing halt.
halt_set=1; step; idle; #1; check(halted, "halted again");
halt_clear=1; step; idle;
#1; check(!halted, "CLEAR_FEATURE cleared the halt");
check(expected_toggle === 1'b0,
"and reset the toggle to DATA0 -- host and device must agree");
// 9. An IN endpoint: the toggle advances on the host's ACK, not on send.
hard_reset; ep_is_in=1;
token=T_IN; fifo_ready=1; in_acked=0; #1;
check(handshake === H_DATA, "an IN with data available sends DATA");
check(fifo_read_en, "and reads the FIFO");
step; idle;
#1; check(expected_toggle === 1'b0,
"the toggle has NOT advanced -- the host has not ACKed yet");
token=T_IN; fifo_ready=1; in_acked=1; step; idle;
#1; check(expected_toggle === 1'b1, "now the host ACKed, the toggle moved");
// 10. An IN with no data NAKs.
token=T_IN; fifo_ready=0; #1;
check(handshake === H_NAK, "an IN with an empty FIFO NAKs");
step; idle;
// ===== C. randomised =====
hard_reset;
for (i=0;i<40000;i=i+1) begin
ep_is_in = ($urandom%4)==0;
token = token_e'($urandom%4);
rx_toggle= $urandom%2;
rx_crc_ok= ($urandom%16)!=0;
fifo_ready=($urandom%4)!=0;
in_acked = $urandom%2;
halt_set = ($urandom%32)==0;
halt_clear=($urandom%32)==0;
ep_reset = ($urandom%64)==0;
step;
end
foreach (n_hs[i]) check(n_hs[i] > 0, "every handshake outcome was reached");
foreach (n_st[i]) check(n_st[i] > 0, "every (toggle, halted) state was reached");
check(n_du > 500, "retransmissions were absorbed many times");
check(n_si > 100, "corrupt packets produced silence many times");
$display("");
$display(" REACH: transitions=%0d | handshakes: none=%0d ack=%0d nak=%0d stall=%0d data=%0d",
n_trans, n_hs[0], n_hs[1], n_hs[2], n_hs[3], n_hs[4]);
$display(" STATES: (tog0,run)=%0d (tog0,halt)=%0d (tog1,run)=%0d (tog1,halt)=%0d | written=%0d dups=%0d silent=%0d",
n_st[0], n_st[1], n_st[2], n_st[3], n_wr, n_du, n_si);
$display(" COUNTERS: ack=%0d nak=%0d stall=%0d silent=%0d dup=%0d written=%0d",
n_ack, n_nak, n_stall, n_silent, n_dup, n_written);
$display(" [SystemVerilog] usb_endpoint_ctrl: %0d errors", errors);
$display(" [SystemVerilog] %0s", errors==0 ? "PASS" : "FAIL");
$display("");
$finish;
end
endmodule11.3 VHDL testbench
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
use work.usb_ep_pkg.all;
entity tb_ep_vhdl is
end entity;
architecture sim of tb_ep_vhdl is
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal ep_is_in, rx_toggle, in_acked : std_logic := '0';
signal rx_crc_ok, fifo_ready : std_logic := '1';
signal halt_set, halt_clear, ep_reset : std_logic := '0';
signal token : std_logic_vector(1 downto 0) := "00";
signal handshake : std_logic_vector(2 downto 0);
signal fifo_write_en, fifo_read_en, tx_toggle, data_dup : std_logic;
signal expected_toggle, halted : std_logic;
signal n_ack, n_nak, n_stall, n_silent, n_dup, n_written
: std_logic_vector(31 downto 0);
signal running : boolean := true;
type cnt5_t is array (0 to 4) of integer;
type cnt4_t is array (0 to 3) of integer;
begin
clk <= not clk after 5 ns when running else '0';
dut : entity work.usb_endpoint_ctrl
port map (clk => clk, rst_n => rst_n, ep_is_in => ep_is_in, token => token,
rx_toggle => rx_toggle, rx_crc_ok => rx_crc_ok,
fifo_ready => fifo_ready, in_acked => in_acked,
halt_set => halt_set, halt_clear => halt_clear,
ep_reset => ep_reset, handshake => handshake,
fifo_write_en => fifo_write_en, fifo_read_en => fifo_read_en,
tx_toggle => tx_toggle, data_dup => data_dup,
expected_toggle => expected_toggle, halted => halted,
n_ack => n_ack, n_nak => n_nak, n_stall => n_stall,
n_silent => n_silent, n_dup => n_dup, n_written => n_written);
stim : process
variable seed1 : positive := 5531;
variable seed2 : positive := 9967;
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_trans : integer := 0;
variable n_hs : cnt5_t := (others => 0);
variable n_st : cnt4_t := (others => 0);
variable n_wr, n_du, n_si : integer := 0;
variable m_ack, m_nak, m_stall, m_sil, m_dup, m_wr : integer := 0;
-- The model's prediction of the NEXT state, stashed before the clock edge
-- so it can be compared after it. Checking only the combinational outputs
-- and the counters leaves every state-only bug invisible.
variable g_tog_n, g_hlt_n : std_logic;
procedure check(cond : boolean; msg : string) is
begin
if not cond then
errors := errors + 1;
if errors <= 25 then
report " FAIL: " & msg
& " (in=" & std_logic'image(ep_is_in)(2)
& " tok=" & integer'image(to_integer(unsigned(token)))
& " rxT=" & std_logic'image(rx_toggle)(2)
& " crc=" & std_logic'image(rx_crc_ok)(2)
& " rdy=" & std_logic'image(fifo_ready)(2)
& " iack=" & std_logic'image(in_acked)(2)
& " hs=" & std_logic'image(halt_set)(2)
& " hc=" & std_logic'image(halt_clear)(2)
& " rst=" & std_logic'image(ep_reset)(2)
& " | tog=" & std_logic'image(expected_toggle)(2)
& " hlt=" & std_logic'image(halted)(2)
& " -> hsk=" & integer'image(to_integer(unsigned(handshake)))
& " wr=" & std_logic'image(fifo_write_en)(2)
& " dup=" & std_logic'image(data_dup)(2)
& ")" 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 tk : token_t;
-- "out", "in" and "new" are VHDL reserved words and an identifier
-- may not end in an underscore, so these carry a v_ prefix.
variable v_setup, v_out, v_in, v_mis, v_dup, v_new : boolean;
variable e_hs : handshake_t;
variable e_wr, e_rd, e_dup : boolean;
begin
tk := token_decode(token);
v_setup := (tk = T_SETUP) and ep_is_in = '0';
v_out := (tk = T_OUT) and ep_is_in = '0';
v_in := (tk = T_IN) and ep_is_in = '1';
v_mis := (tk /= T_NONE) and not v_setup and not v_out and not v_in;
v_dup := v_out and halted = '0' and rx_crc_ok = '1'
and rx_toggle /= expected_toggle;
v_new := v_out and halted = '0' and rx_crc_ok = '1'
and rx_toggle = expected_toggle;
-- The reference model is a flat if-chain over booleans where the design
-- uses signals and an enumerated decision -- a different route to the
-- same answer.
if tk = T_NONE then e_hs := H_NONE;
elsif v_mis then e_hs := H_NONE;
elsif v_setup then e_hs := H_ACK;
elsif halted = '1' then e_hs := H_STALL;
elsif v_out then
if rx_crc_ok = '0' then e_hs := H_NONE;
elsif v_dup then e_hs := H_ACK;
elsif fifo_ready = '0' then e_hs := H_NAK;
else e_hs := H_ACK;
end if;
else
if fifo_ready = '0' then e_hs := H_NAK;
else e_hs := H_DATA;
end if;
end if;
e_wr := v_new and fifo_ready = '1';
e_rd := v_in and halted = '0' and fifo_ready = '1';
e_dup := v_dup;
check(handshake = hs_code(e_hs), "handshake matches the model");
check((fifo_write_en = '1') = e_wr, "fifo_write_en matches the model");
check((fifo_read_en = '1') = e_rd, "fifo_read_en matches the model");
check((data_dup = '1') = e_dup, "data_dup matches the model");
check(tx_toggle = expected_toggle,
"the transmitted toggle is the expected toggle");
-- ---- SAFETY PROPERTIES, independent of the model ----
-- 1. THE property. A retransmission is ACKed and NOT written.
if data_dup = '1' then
check(handshake = hs_code(H_ACK),
"a retransmission was not ACKed -- the host will retry for ever");
check(fifo_write_en = '0',
"a retransmission was WRITTEN -- silent data duplication");
end if;
-- 2. A SETUP is never refused. It is how a host recovers a device.
if tk = T_SETUP and ep_is_in = '0' then
check(handshake = hs_code(H_ACK),
"a SETUP was NAKed or STALLed -- recovery is now impossible");
end if;
-- 3. Nothing is ever written on a packet whose CRC failed.
if rx_crc_ok = '0' then
check(fifo_write_en = '0',
"data with a failed CRC was written to the endpoint FIFO");
end if;
-- 4. A failed CRC produces SILENCE, never a NAK.
if tk = T_OUT and ep_is_in = '0' and rx_crc_ok = '0'
and halted = '0' then
check(handshake = hs_code(H_NONE),
"a corrupt packet was answered -- the host was told a lie");
end if;
-- 5. A halted endpoint STALLs everything except a SETUP.
if halted = '1' and tk /= T_NONE
and not (tk = T_SETUP and ep_is_in = '0') then
check(handshake = hs_code(H_STALL) or handshake = hs_code(H_NONE),
"a halted endpoint answered something other than STALL");
end if;
-- 6. Never write and report a duplicate in the same cycle.
check(not (fifo_write_en = '1' and data_dup = '1'),
"a packet was both accepted as new and absorbed as a duplicate");
-- 7. A token for the other direction is never answered.
if tk = T_OUT and ep_is_in = '1' then
check(handshake = hs_code(H_NONE),
"an IN endpoint answered an OUT token");
end if;
if tk = T_IN and ep_is_in = '0' then
check(handshake = hs_code(H_NONE),
"an OUT endpoint answered an IN token");
end if;
-- predicted next state, same priority chain as the design but written
-- as a flat if-chain over the model's own booleans
if ep_reset = '1' then g_tog_n := '0';
elsif v_setup then g_tog_n := '1';
elsif halt_clear = '1' then g_tog_n := '0';
elsif e_wr or (v_in and halted = '0' and in_acked = '1') then
g_tog_n := not expected_toggle;
else g_tog_n := expected_toggle;
end if;
if ep_reset = '1' then g_hlt_n := '0';
elsif v_setup then g_hlt_n := '0';
elsif halt_clear = '1' then g_hlt_n := '0';
elsif halt_set = '1' then g_hlt_n := '1';
else g_hlt_n := halted;
end if;
n_hs(handshake_t'pos(e_hs)) := n_hs(handshake_t'pos(e_hs)) + 1;
if expected_toggle = '1' then
if halted = '1' then n_st(3) := n_st(3) + 1;
else n_st(2) := n_st(2) + 1; end if;
else
if halted = '1' then n_st(1) := n_st(1) + 1;
else n_st(0) := n_st(0) + 1; end if;
end if;
if e_wr then n_wr := n_wr + 1; end if;
if e_dup then n_du := n_du + 1; end if;
if (v_out or v_in) and e_hs = H_NONE then n_si := n_si + 1; end if;
end procedure;
procedure step is
variable tk : token_t;
begin
wait for 1 ns;
check_comb;
tk := token_decode(token);
if handshake = hs_code(H_ACK) then m_ack := m_ack + 1; end if;
if handshake = hs_code(H_NAK) then m_nak := m_nak + 1; end if;
if handshake = hs_code(H_STALL) then m_stall := m_stall + 1; end if;
if (((tk = T_OUT) and ep_is_in = '0')
or ((tk = T_IN) and ep_is_in = '1'))
and handshake = hs_code(H_NONE) then
m_sil := m_sil + 1;
end if;
if data_dup = '1' then m_dup := m_dup + 1; end if;
if fifo_write_en = '1' then m_wr := m_wr + 1; end if;
wait until rising_edge(clk);
wait for 1 ns;
-- THE state check. Without it a mutation that corrupts only the toggle
-- or only the halt is nearly invisible: every combinational output and
-- every counter can still be correct in the cycle it happens.
check(expected_toggle = g_tog_n,
"the toggle took the next value the model predicted");
check(halted = g_hlt_n,
"the halt took the next value the model predicted");
check(n_ack = std_logic_vector(to_unsigned(m_ack, 32)),
"n_ack matches the model");
check(n_nak = std_logic_vector(to_unsigned(m_nak, 32)),
"n_nak matches the model");
check(n_stall = std_logic_vector(to_unsigned(m_stall, 32)),
"n_stall matches the model");
check(n_silent = std_logic_vector(to_unsigned(m_sil, 32)),
"n_silent matches the model");
check(n_dup = std_logic_vector(to_unsigned(m_dup, 32)),
"n_dup matches the model");
check(n_written = std_logic_vector(to_unsigned(m_wr, 32)),
"n_written matches the model");
end procedure;
procedure idle is
begin
token <= "00"; halt_set <= '0'; halt_clear <= '0'; ep_reset <= '0';
in_acked <= '0'; rx_crc_ok <= '1'; fifo_ready <= '1';
end procedure;
procedure hard_reset is
begin
rst_n <= '0'; ep_is_in <= '0'; idle;
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_ack := 0; m_nak := 0; m_stall := 0; m_sil := 0; m_dup := 0; m_wr := 0;
end procedure;
-- Force the endpoint into a chosen (toggle, halted) state without relying
-- on the very logic under test to have got there by itself.
procedure goto_state(want_tog : std_logic; want_hlt : std_logic) is
begin
idle; ep_reset <= '1';
wait until rising_edge(clk); wait for 1 ns; -- -> (0,0)
idle;
if want_tog = '1' then
-- A SETUP is the only legitimate way to reach DATA1, and it is a real
-- transaction: it produces an ACK, so the model's counter must be
-- told about it or the counter checks drift by one per setup.
ep_is_in <= '0'; token <= "11";
wait until rising_edge(clk); wait for 1 ns; idle;
m_ack := m_ack + 1;
end if;
if want_hlt = '1' then
halt_set <= '1';
wait until rising_edge(clk); wait for 1 ns; idle;
end if;
wait for 1 ns;
check(expected_toggle = want_tog, "goto_state reached the toggle");
check(halted = want_hlt, "goto_state reached the halt state");
end procedure;
variable iv : integer;
variable tg, hl : std_logic;
begin
hard_reset;
check(expected_toggle = '0', "reset starts at DATA0");
check(halted = '0', "and not halted");
-- ===== A. EXHAUSTIVE one-step transition sweep =====
-- 4 internal states (toggle x halted) x 512 input combinations
-- = 2048 transitions: every reachable (state, input) pair exactly once.
for s in 0 to 3 loop
if s >= 2 then tg := '1'; else tg := '0'; end if;
if (s mod 2) = 1 then hl := '1'; else hl := '0'; end if;
for a in 0 to 1 loop
for b in 0 to 3 loop
for c in 0 to 1 loop
for d in 0 to 1 loop
for e in 0 to 1 loop
for f in 0 to 1 loop
for g in 0 to 1 loop
for h in 0 to 1 loop
goto_state(tg, hl);
if a = 1 then ep_is_in <= '1'; else ep_is_in <= '0'; end if;
token <= std_logic_vector(to_unsigned(b, 2));
if c = 1 then rx_toggle <= '1'; else rx_toggle <= '0'; end if;
if d = 1 then rx_crc_ok <= '1'; else rx_crc_ok <= '0'; end if;
if e = 1 then fifo_ready <= '1'; else fifo_ready <= '0'; end if;
if f = 1 then in_acked <= '1'; else in_acked <= '0'; end if;
if g = 1 then halt_set <= '1'; else halt_set <= '0'; end if;
if h = 1 then halt_clear <= '1'; else halt_clear <= '0'; end if;
ep_reset <= '0';
step;
n_trans := n_trans + 1;
idle;
end loop;
end loop;
end loop;
end loop;
end loop;
end loop;
end loop;
end loop;
end loop;
report " exhaustive endpoint transition sweep: " & integer'image(n_trans)
& " of 2048 verified" severity note;
-- ===== B. directed: the retransmission story, end to end =====
hard_reset; ep_is_in <= '0';
-- 1. A normal OUT: DATA0 arrives when DATA0 is expected.
token <= "01"; rx_toggle <= '0'; rx_crc_ok <= '1'; fifo_ready <= '1';
step; idle;
check(n_written = std_logic_vector(to_unsigned(1, 32)),
"the first packet was written");
wait for 1 ns;
check(expected_toggle = '1', "and the toggle advanced to DATA1");
-- 2. THE case. The host never saw our ACK and sends DATA0 again.
token <= "01"; rx_toggle <= '0'; rx_crc_ok <= '1'; fifo_ready <= '1';
wait for 1 ns;
check(data_dup = '1',
"the repeated DATA0 is recognised as a retransmission");
check(handshake = hs_code(H_ACK),
"it is ACKed again, because the host is waiting");
check(fifo_write_en = '0',
"but NOT written -- that data is already in the FIFO");
step; idle;
check(n_written = std_logic_vector(to_unsigned(1, 32)),
"still exactly one packet written");
check(n_dup = std_logic_vector(to_unsigned(1, 32)),
"and one duplicate absorbed");
wait for 1 ns;
check(expected_toggle = '1',
"and the toggle did NOT advance -- it was already correct");
-- 3. The next genuine packet is DATA1 and is accepted normally.
token <= "01"; rx_toggle <= '1'; rx_crc_ok <= '1'; fifo_ready <= '1';
step; idle;
check(n_written = std_logic_vector(to_unsigned(2, 32)),
"the next genuine packet was written");
wait for 1 ns;
check(expected_toggle = '0', "and the toggle advanced back to DATA0");
-- 4. A corrupt packet: silence, no write, no toggle movement.
token <= "01"; rx_toggle <= '0'; rx_crc_ok <= '0'; fifo_ready <= '1';
wait for 1 ns;
check(handshake = hs_code(H_NONE),
"a failed CRC is answered with SILENCE, not a NAK");
check(fifo_write_en = '0', "and nothing is written");
step; idle;
wait for 1 ns;
check(expected_toggle = '0', "and the toggle is unchanged");
-- 5. Full FIFO: NAK, no write, no toggle movement.
token <= "01"; rx_toggle <= '0'; rx_crc_ok <= '1'; fifo_ready <= '0';
wait for 1 ns;
check(handshake = hs_code(H_NAK), "a full FIFO produces a NAK");
check(fifo_write_en = '0', "with no write");
step; idle;
wait for 1 ns;
check(expected_toggle = '0', "and no toggle movement");
-- 6. Halt the endpoint: everything STALLs.
halt_set <= '1'; step; idle;
wait for 1 ns; check(halted = '1', "the endpoint is halted");
token <= "01"; rx_toggle <= '0'; wait for 1 ns;
check(handshake = hs_code(H_STALL), "and an OUT now STALLs");
step; idle;
-- 7. But a SETUP still gets through, and clears the halt.
token <= "11"; wait for 1 ns;
check(handshake = hs_code(H_ACK),
"a SETUP is ACKed even on a halted endpoint");
step; idle;
wait for 1 ns;
check(halted = '0', "the SETUP cleared the halt");
check(expected_toggle = '1',
"and set the toggle to DATA1 for the data stage");
-- 8. CLEAR_FEATURE resets the toggle to DATA0 as well as clearing halt.
halt_set <= '1'; step; idle;
wait for 1 ns; check(halted = '1', "halted again");
halt_clear <= '1'; step; idle;
wait for 1 ns;
check(halted = '0', "CLEAR_FEATURE cleared the halt");
check(expected_toggle = '0',
"and reset the toggle to DATA0 -- host and device must agree");
-- 9. An IN endpoint: the toggle advances on the host's ACK, not on send.
hard_reset; ep_is_in <= '1';
token <= "10"; fifo_ready <= '1'; in_acked <= '0'; wait for 1 ns;
check(handshake = hs_code(H_DATA), "an IN with data available sends DATA");
check(fifo_read_en = '1', "and reads the FIFO");
step; idle;
wait for 1 ns;
check(expected_toggle = '0',
"the toggle has NOT advanced -- the host has not ACKed yet");
token <= "10"; fifo_ready <= '1'; in_acked <= '1'; step; idle;
wait for 1 ns;
check(expected_toggle = '1', "now the host ACKed, the toggle moved");
-- 10. An IN with no data NAKs.
token <= "10"; fifo_ready <= '0'; wait for 1 ns;
check(handshake = hs_code(H_NAK), "an IN with an empty FIFO NAKs");
step; idle;
-- ===== 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, 4); if iv = 0 then ep_is_in <= '1'; else ep_is_in <= '0'; end if;
rnd(iv, 4); token <= std_logic_vector(to_unsigned(iv, 2));
rnd(iv, 2); if iv = 1 then rx_toggle <= '1'; else rx_toggle <= '0'; end if;
rnd(iv, 16); if iv /= 0 then rx_crc_ok <= '1'; else rx_crc_ok <= '0'; end if;
rnd(iv, 4); if iv /= 0 then fifo_ready <= '1'; else fifo_ready <= '0'; end if;
rnd(iv, 2); if iv = 1 then in_acked <= '1'; else in_acked <= '0'; end if;
rnd(iv, 32); if iv = 0 then halt_set <= '1'; else halt_set <= '0'; end if;
rnd(iv, 32); if iv = 0 then halt_clear <= '1'; else halt_clear <= '0'; end if;
rnd(iv, 64); if iv = 0 then ep_reset <= '1'; else ep_reset <= '0'; end if;
step;
end loop;
for i in 0 to 4 loop
check(n_hs(i) > 0, "every handshake outcome was reached");
end loop;
for i in 0 to 3 loop
check(n_st(i) > 0, "every (toggle, halted) state was reached");
end loop;
check(n_du > 500, "retransmissions were absorbed many times");
check(n_si > 100, "corrupt packets produced silence many times");
report " REACH: transitions=" & integer'image(n_trans)
& " | handshakes: none=" & integer'image(n_hs(0))
& " ack=" & integer'image(n_hs(1))
& " nak=" & integer'image(n_hs(2))
& " stall=" & integer'image(n_hs(3))
& " data=" & integer'image(n_hs(4)) severity note;
report " STATES: (tog0,run)=" & integer'image(n_st(0))
& " (tog0,halt)=" & integer'image(n_st(1))
& " (tog1,run)=" & integer'image(n_st(2))
& " (tog1,halt)=" & integer'image(n_st(3))
& " | written=" & integer'image(n_wr)
& " dups=" & integer'image(n_du)
& " silent=" & integer'image(n_si) severity note;
report " COUNTERS: ack=" & integer'image(to_integer(unsigned(n_ack)))
& " nak=" & integer'image(to_integer(unsigned(n_nak)))
& " stall=" & integer'image(to_integer(unsigned(n_stall)))
& " silent=" & integer'image(to_integer(unsigned(n_silent)))
& " dup=" & integer'image(to_integer(unsigned(n_dup)))
& " written=" & integer'image(to_integer(unsigned(n_written)))
severity note;
report " [VHDL] usb_endpoint_ctrl: " & 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;12. Exhaustive Verification, and a Gap the Mutants Found
All three implementations pass:
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| Exhaustive transitions | 2048 / 2048 | 2048 / 2048 | 2048 / 2048 |
H_NONE reached | 24211 | 24380 | 24449 |
H_ACK reached | 13521 | 13282 | 13299 |
H_NAK reached | 1457 | 1508 | 1390 |
H_STALL reached | 1129 | 1187 | 1177 |
H_DATA reached | 1743 | 1704 | 1746 |
| state (DATA0, running) | 11827 | 11493 | 11648 |
| state (DATA0, halted) | 1599 | 1881 | 2058 |
| state (DATA1, running) | 25599 | 25664 | 25622 |
| state (DATA1, halted) | 3036 | 3023 | 2733 |
| retransmissions absorbed | 3248 | 3151 | 3163 |
| corrupt packets silenced | 471 | 475 | 510 |
| packets written | 2497 | 2417 | 2416 |
| Result | PASS | PASS | PASS |
All five handshake outcomes and all four internal states are reached, and the testbenches assert that rather than merely reporting it.
But the first version of this suite was wrong, and it took the mutation matrix to show it.
@(posedge clk); #1;
// THE state check. Without it a mutation that corrupts only the toggle
// or only the halt is nearly invisible: every combinational output and
// every counter can still be correct in the cycle it happens.
check(expected_toggle === g_tog_n,
"the toggle took the next value the model predicted");
check(halted === g_hlt_n,
"the halt took the next value the model predicted");The general lesson: a suite that checks a sequential block only through its combinational outputs is testing a function, not a machine. If the design has state, the bench needs its own copy of that state and must compare it every cycle. Chapter 21.2 takes the idea further and keeps a complete shadow model of the block.
13. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| E1 | a retransmission is written to the FIFO | 9649 | 9541 | 9477 |
| E2 | a retransmission is NAKed instead of ACKed | 6497 | 6303 | 6327 |
| E3 | the toggle advances on a retransmission | 3087 | 3014 | 3006 |
| E4 | the halt is tested before the SETUP | 1673 | 1719 | 1757 |
| E5 | a failed CRC is answered with NAK, not silence | 943 | 951 | 1021 |
| E6 | CLEAR_FEATURE does not reset the toggle | 1124 | 1094 | 1131 |
| E7 | the IN toggle advances on send, not on ACK | 1096 | 1069 | 1120 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages, the columns agree to within 4%, and no two counts coincide.
E1 is the mutation this chapter exists for. It is one deleted term — the toggle comparison drops out of fifo_write_en — and in silicon it produces duplicated payload bytes with no error reported anywhere. No CRC fails. No counter increments. No handshake is out of place. The data is simply there twice, and only on transfers where an ACK happened to be corrupted, which on a short cable is approximately never and on a marginal one is constant.
E2 and E3 are the other two thirds of §3's rule, and their counts tell you something about relative detectability: NAKing a retransmission (E2, 6497) is caught twice as often as advancing the toggle (E3, 3087), because the wrong handshake is visible immediately while the wrong toggle only shows up on the next transaction.
E5 is the smallest at ~950, and that is the honest number. It fires only on an OUT to a non-halted endpoint with a failed CRC — a four-way conjunction that the randomised phase produces about 470 times. A count this low always deserves the question "is the suite weak, or is the condition narrow?" Here it is narrow, and all three languages agree to within 8%.
14. Debugging Walkthrough: One Corrupted File in Ten Thousand
The report. A USB mass-storage device occasionally writes a file with a few bytes duplicated in the middle. Roughly one file in ten thousand, only on one customer's site, never on the bench.
Step 1 — is the data corrupted or duplicated? Duplicated. The inserted bytes are an exact copy of the preceding 64 bytes. That is not noise; that is a whole packet appearing twice, which means it is a transaction-level problem, not a signal-integrity one.
Step 2 — is the bus healthy? Check the host controller's error counters. Non-zero: a few CRC errors and a handful of transaction timeouts per hour. The instinct is "bad cable, replace it" — and the instinct is wrong for the same reason it was wrong in Chapter 20.4 §15: errors being detected is the system working. The question is what happened next.
Step 3 — correlate. The duplicated-block events line up with the transaction timeouts, not with the CRC errors. A transaction timeout is the host saying "I sent a packet and got no handshake" — which is exactly the lost-ACK case from §1.
Step 4 — the decisive measurement. Instrument n_written and n_dup on the endpoint. In a correct device, a retry storm makes n_dup climb while n_written does not. Here n_dup is zero and n_written climbs on every retry. The endpoint is not recognising retransmissions at all.
Step 5 — the cause. fifo_write_en was gated on rx_crc_ok && fifo_ready and not on the toggle comparison. Mutation E1, in production.
Step 6 — why only one site. The failure requires a corrupted ACK, which requires a marginal physical layer. The customer's cable run went past a welding bay. The bug scales with electrical noise, which is why it looks like a cable problem and why replacing the cable appears to fix it — for a while.
15. UVM: Verifying That a Duplicate Changes Nothing
The interesting stimulus here is a packet the device has already seen, which is not something ordinary constrained-random traffic produces — a random toggle bit is a duplicate only half the time, and only by accident. The environment has to model the host's retry behaviour deliberately.
15.1 The transaction
class usb_ep_item extends uvm_sequence_item;
`uvm_object_utils(usb_ep_item)
rand token_e token;
rand bit ep_is_in;
rand bit rx_toggle;
rand bit rx_crc_ok;
rand bit fifo_ready;
rand bit in_acked;
rand bit halt_set;
rand bit halt_clear;
rand bit ep_reset;
// Errors are rare on a healthy bus. The directed sequences below override
// this when they want the error path specifically.
constraint c_mostly_clean {
rx_crc_ok dist {1 := 95, 0 := 5};
fifo_ready dist {1 := 80, 0 := 20};
}
// SET and CLEAR_FEATURE cannot arrive in the same cycle on real hardware:
// they are two different control transfers. Soft, so the adversarial
// sequence can turn it off and check the design still resolves them.
constraint c_no_simultaneous_feature {
soft !(halt_set && halt_clear);
}
// A bus reset is rare and overrides everything.
constraint c_reset_rare { ep_reset dist {0 := 99, 1 := 1}; }
function new(string name = "usb_ep_item"); super.new(name); endfunction
function string convert2string();
return $sformatf("tok=%s in=%0b tog=%0b crc=%0b rdy=%0b iack=%0b",
token.name(), ep_is_in, rx_toggle, rx_crc_ok,
fifo_ready, in_acked);
endfunction
endclass15.2 The sequence that models a retrying host
// THE sequence for this chapter. It does what a real host does when a
// handshake goes missing: sends the SAME packet again, with the SAME toggle,
// one or more times, and only then moves on. Random stimulus produces a
// duplicate by accident; this produces the retry PATTERN, which is what the
// endpoint actually has to survive.
class lost_ack_retry_seq extends uvm_sequence #(usb_ep_item);
`uvm_object_utils(lost_ack_retry_seq)
function new(string name = "lost_ack_retry_seq"); super.new(name); endfunction
rand int unsigned n_bursts;
constraint c_bursts { n_bursts inside {[200:400]}; }
task body();
bit tog = 0;
repeat (n_bursts) begin
int unsigned retries;
usb_ep_item it;
// How many times the host had to resend before a handshake got back.
// 0 is the ordinary case; the tail is what this sequence is for.
retries = $urandom_range(0, 3);
// The original packet plus `retries` identical copies. Every copy
// carries the SAME toggle, because the host has not advanced either.
repeat (retries + 1) begin
it = usb_ep_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { token == T_OUT;
ep_is_in == 0;
rx_toggle == tog;
rx_crc_ok == 1;
fifo_ready == 1;
halt_set == 0;
halt_clear == 0;
ep_reset == 0; })
`uvm_error("RAND", "retry randomize failed")
finish_item(it);
end
// Only after the endpoint has ACKed does the host advance.
tog = ~tog;
end
endtask
endclass
// A SETUP arriving at a halted endpoint -- the recovery path. If a SETUP can
// ever be STALLed, a halted endpoint is unrecoverable and the device is
// bricked until it is physically unplugged.
class setup_recovery_seq extends uvm_sequence #(usb_ep_item);
`uvm_object_utils(setup_recovery_seq)
function new(string name = "setup_recovery_seq"); super.new(name); endfunction
task body();
repeat (300) begin
usb_ep_item it = usb_ep_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { token == T_SETUP; ep_is_in == 0;
halt_set == 1; // halted THIS cycle
ep_reset == 0; })
`uvm_error("RAND", "setup randomize failed")
finish_item(it);
end
endtask
endclass
// A noisy physical layer: CRC failures on a third of packets. The property
// under test is that every one of them produces SILENCE and no write.
class noisy_bus_seq extends uvm_sequence #(usb_ep_item);
`uvm_object_utils(noisy_bus_seq)
function new(string name = "noisy_bus_seq"); super.new(name); endfunction
task body();
repeat (600) begin
usb_ep_item it = usb_ep_item::type_id::create("it");
start_item(it);
it.c_mostly_clean.constraint_mode(0);
if (!it.randomize() with { token inside {T_OUT, T_IN};
rx_crc_ok dist {0 := 33, 1 := 67};
ep_reset == 0; })
`uvm_error("RAND", "noisy randomize failed")
finish_item(it);
end
endtask
endclass15.3 The scoreboard
class usb_ep_scoreboard extends uvm_scoreboard;
`uvm_component_utils(usb_ep_scoreboard)
uvm_analysis_imp #(usb_ep_mon_item, usb_ep_scoreboard) ap;
// The scoreboard keeps its OWN copy of the toggle. Comparing against the
// DUT's own expected_toggle would make the check circular -- chapter 21.1
// section 12 is what happens when the bench does not track state itself.
bit sb_toggle;
bit sb_halted;
int unsigned n_dup, n_written, n_silent, n_setup_refused;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void write(usb_ep_mon_item t);
bit is_out, is_setup, dup;
is_setup = (t.token == T_SETUP) && !t.ep_is_in;
is_out = (t.token == T_OUT) && !t.ep_is_in;
dup = is_out && !sb_halted && t.rx_crc_ok
&& (t.rx_toggle != sb_toggle);
// ---- THE check. A duplicate is ACKed and changes NOTHING. ----
if (dup) begin
if (t.handshake != H_ACK)
`uvm_error("RETRY",
$sformatf("a retransmission was answered %s -- the host will retry for ever",
t.handshake.name()))
if (t.fifo_write_en)
`uvm_error("DUPLICATE",
"a retransmission was WRITTEN -- the payload is now in the FIFO twice")
n_dup++;
end
// ---- A SETUP is never refused: it is the recovery path. ----
if (is_setup) begin
if (t.handshake != H_ACK)
`uvm_error("RECOVERY",
$sformatf("a SETUP was answered %s -- a halted endpoint can never recover",
t.handshake.name()))
n_setup_refused += (t.handshake != H_ACK);
end
// ---- A failed CRC produces silence, and never a write. ----
if (is_out && !t.rx_crc_ok && !sb_halted) begin
if (t.handshake != H_NONE)
`uvm_error("HONESTY",
"a packet whose CRC failed was answered -- the device claimed knowledge it lacks")
if (t.fifo_write_en)
`uvm_error("HONESTY", "a packet whose CRC failed was written")
n_silent++;
end
if (t.fifo_write_en) n_written++;
// ---- Advance the scoreboard's own state, by the spec's rules ----
if (t.ep_reset) begin sb_toggle = 0; sb_halted = 0; end
else if (is_setup) begin sb_toggle = 1; sb_halted = 0; end
else if (t.halt_clear) begin sb_toggle = 0; sb_halted = 0; end
else begin
if (t.halt_set) sb_halted = 1;
if (t.fifo_write_en) sb_toggle = ~sb_toggle;
if ((t.token == T_IN) && t.ep_is_in && !sb_halted && t.in_acked)
sb_toggle = ~sb_toggle;
end
// And check the DUT agrees -- from an independently maintained copy.
if (t.expected_toggle !== sb_toggle)
`uvm_error("TOGGLE_DRIFT",
$sformatf("toggle drift: DUT=%0b scoreboard=%0b", t.expected_toggle,
sb_toggle))
endfunction
function void report_phase(uvm_phase phase);
`uvm_info("SB", $sformatf("dups=%0d written=%0d silences=%0d",
n_dup, n_written, n_silent), UVM_LOW)
// A run that never made the endpoint absorb a duplicate proves nothing
// about the one mechanism this block exists to implement.
if (n_dup == 0) `uvm_error("COVERAGE",
"no retransmission was ever presented -- the toggle mechanism is untested")
if (n_silent == 0) `uvm_error("COVERAGE", "no CRC failure was ever presented")
if (n_written == 0) `uvm_error("COVERAGE", "no packet was ever accepted")
endfunction
endclass15.4 Functional coverage
covergroup ep_ctrl_cg with function sample(
token_e tok, bit ep_in, bit rx_tog, bit exp_tog, bit crc, bit rdy,
bit halted, handshake_e hs);
cp_token : coverpoint tok { bins all[] = {T_NONE, T_OUT, T_IN, T_SETUP}; }
cp_hs : coverpoint hs { bins all[] = {H_NONE, H_ACK, H_NAK,
H_STALL, H_DATA}; }
cp_dir : coverpoint ep_in { bins out_ep = {0}; bins in_ep = {1}; }
cp_crc : coverpoint crc { bins ok = {1}; bins bad = {0}; }
cp_rdy : coverpoint rdy { bins ready = {1}; bins full = {0}; }
cp_halt : coverpoint halted { bins running = {0}; bins halted = {1}; }
// THE coverpoint. Does the received toggle match what we expect?
// The "mismatch" bin IS the retransmission case.
cp_match : coverpoint (rx_tog == exp_tog) {
bins new_data = {1};
bins retransmission = {0};
}
// THE cross. Every handshake, under both toggle outcomes. Closing it means
// the retransmission path was exercised against every response the design
// can produce -- not merely that duplicates happened somewhere.
x_match_hs : cross cp_match, cp_hs, cp_token {
ignore_bins not_an_out = binsof(cp_token) intersect {T_NONE, T_IN, T_SETUP};
}
// A SETUP under both halt states: the recovery path is only interesting
// when there is something to recover from.
x_setup_halt : cross cp_token, cp_halt {
ignore_bins not_setup = binsof(cp_token) intersect {T_NONE, T_OUT, T_IN};
}
// The four-way conjunction mutation E5 lives in.
x_crc_dir_halt : cross cp_crc, cp_dir, cp_halt;
endgroupcp_match is the coverpoint that corresponds to the design's whole reason for existing, and it is worth noting how easy it is to omit. A coverage model built from the port list gets rx_toggle and expected_toggle as two separate coverpoints, each dutifully hitting both values, and reports 100% — while never recording whether they ever differed. The interesting event is the relationship, not either signal.
16. SystemVerilog Assertions
module usb_endpoint_ctrl_sva
import usb_ep_pkg::*;
(
input logic clk,
input logic rst_n,
input logic ep_is_in,
input token_e token,
input logic rx_toggle,
input logic rx_crc_ok,
input logic fifo_ready,
input logic in_acked,
input logic halt_set,
input logic halt_clear,
input logic ep_reset,
input handshake_e handshake,
input logic fifo_write_en,
input logic fifo_read_en,
input logic data_dup,
input logic expected_toggle,
input logic halted
);
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// ---- 1. THE property. A retransmission is ACKed and stored nowhere. ----
property p_dup_acked_not_written;
data_dup |-> (handshake == H_ACK) && !fifo_write_en;
endproperty
a_dup_acked_not_written : assert property (p_dup_acked_not_written)
else $error("a retransmission was mishandled: handshake=%s write=%0b",
handshake.name(), fifo_write_en);
// ---- 2. ...and it does not move the toggle. ----
property p_dup_does_not_move_toggle;
data_dup |=> $stable(expected_toggle);
endproperty
a_dup_does_not_move_toggle : assert property (p_dup_does_not_move_toggle)
else $error("a retransmission advanced the toggle -- the next genuine packet will be dropped");
// ---- 3. A SETUP is never refused. ----
property p_setup_never_refused;
((token == T_SETUP) && !ep_is_in) |-> (handshake == H_ACK);
endproperty
a_setup_never_refused : assert property (p_setup_never_refused)
else $error("a SETUP was refused -- the endpoint can no longer be recovered");
// ---- 4. A SETUP always clears the halt and arms DATA1. ----
property p_setup_clears_halt;
((token == T_SETUP) && !ep_is_in && !ep_reset)
|=> (!halted && expected_toggle);
endproperty
a_setup_clears_halt : assert property (p_setup_clears_halt);
// ---- 5. Nothing is written on a failed CRC, ever. ----
property p_no_write_without_crc;
!rx_crc_ok |-> !fifo_write_en;
endproperty
a_no_write_without_crc : assert property (p_no_write_without_crc);
// ---- 6. A failed CRC on an addressed OUT produces SILENCE. ----
property p_bad_crc_is_silent;
((token == T_OUT) && !ep_is_in && !rx_crc_ok && !halted)
|-> (handshake == H_NONE);
endproperty
a_bad_crc_is_silent : assert property (p_bad_crc_is_silent)
else $error("a corrupt packet was answered %s", handshake.name());
// ---- 7. CLEAR_FEATURE restarts at DATA0. ----
property p_clear_feature_resets_toggle;
(halt_clear && !ep_reset && !((token == T_SETUP) && !ep_is_in))
|=> (!halted && !expected_toggle);
endproperty
a_clear_feature_resets_toggle :
assert property (p_clear_feature_resets_toggle)
else $error("CLEAR_FEATURE left the toggle at DATA1 -- host and device now disagree");
// ---- 8. The toggle moves ONLY on an accepted packet or a reset event. ----
property p_toggle_moves_only_for_a_reason;
(!$stable(expected_toggle))
|-> $past(ep_reset || ((token == T_SETUP) && !ep_is_in) || halt_clear
|| fifo_write_en
|| ((token == T_IN) && ep_is_in && !halted && in_acked));
endproperty
a_toggle_moves_only_for_a_reason :
assert property (p_toggle_moves_only_for_a_reason)
else $error("the toggle moved with no accepted packet and no reset event");
// ---- 9. A write and a duplicate are mutually exclusive. ----
property p_write_xor_dup;
not (fifo_write_en && data_dup);
endproperty
a_write_xor_dup : assert property (p_write_xor_dup);
// ---- Cover: the interesting states were actually reached. ----
c_dup : cover property ((data_dup));
c_silence : cover property (((token == T_OUT) && !ep_is_in && !rx_crc_ok));
c_stall : cover property ((handshake == H_STALL));
c_setup_on_halt : cover property (((token == T_SETUP) && !ep_is_in && halted));
endmodule
bind usb_endpoint_ctrl usb_endpoint_ctrl_sva u_sva (.*);Icarus Verilog does not support concurrent assertions, so these are checked as procedural conditions inside the testbenches (marked "SAFETY PROPERTIES") and given here in SVA form for a commercial simulator or a formal tool.
17. Common Misconceptions
"The toggle is for ordering, like a sequence number." It is not. It has exactly two values and cannot order more than two things. Its only job is to answer "is this the packet I am expecting, or the one I just took?" — a one-bit question.
"A duplicate should be NAKed, since we are not accepting it." A NAK means "I cannot take this now, ask again", which restarts the very retry loop the ACK exists to end. The device is in sync; the ACK says so.
"ACKing a packet you discard is dishonest." The ACK does not mean "I stored these bytes". It means "this transaction is complete and you may move on", and that is exactly true: the data is in the FIFO, from the first copy.
"A CRC failure should be NAKed — the host needs to know." The host already knows: no handshake arrived. A NAK would additionally claim the device read the packet and chose not to take it, which is false. See §4.
"A SETUP is just another transaction." It is the only one that cannot be refused, because it is the one that carries the command to un-refuse everything else. An endpoint that can STALL a SETUP is a device that can be bricked by a single control transfer.
"CLEAR_FEATURE only needs to clear the halt — the name says so." It also resets the toggle to DATA0, and leaving that out costs exactly one packet after every halt recovery, silently.
"The IN toggle advances when we send the data." It advances when the host ACKs it. If the IN data packet is lost the host will ask again, and the device must send the same packet with the same toggle. Advancing on send desynchronises permanently (mutation E7).
18. Exercises
1. Delete the (rx_toggle != toggle_r) term from out_dup and predict, before running it, which of the seven model checks and which of the seven safety properties fail. Then run it and explain why the count is 9649 rather than the ~3200 duplicates the sweep generates.
2. The testbench gap in §12 was found by two mutations scoring 5 and 1. Write a third state-only mutation — one that corrupts halted_next and nothing else — and confirm it also scored near-zero before the fix and dies properly after.
3. Property 8 constrains when the toggle may move. Write its counterpart for halted, then find the mutation that property 8's counterpart catches and property 8 does not.
4. A real endpoint has a third recovery input: the host may issue SET_INTERFACE, which resets the toggle on every endpoint in the interface. Add it, extend the exhaustive sweep to 4096 transitions, and confirm all seven existing mutations still die with similar counts. Which one changes the most, and why?
5. cp_match in §15.4 covers whether the toggles differed. Write the coverage assertion that fails a regression in which cp_match.retransmission has zero hits, and explain why this is a better guard than a simple "coverage must exceed 90%" gate.
6. The design answers a mismatched-toggle SETUP the same way as any other SETUP. Is that correct? Find the answer in the USB 2.0 specification's description of the SETUP stage, then write the assertion that pins it.
19. Summary
| Idea | Why it matters |
|---|---|
| A lost ACK and a lost DATA look identical to the host | so the host resends, and the device sees the same packet twice |
| Nothing in the payload marks a retransmission | the discriminator has to be outside the data |
| The data toggle is that discriminator | one flip-flop per endpoint |
| A duplicate is ACKed | to end the host's retry loop |
| ...and not written | writing it duplicates the payload silently |
| ...and does not move the toggle | moving it drops the next genuine packet |
| A SETUP can never be NAKed or STALLed | it is the recovery path |
| A halted endpoint STALLs everything else | and the halt is sticky |
CLEAR_FEATURE resets the toggle to DATA0 | host and device must agree where to restart |
| A failed CRC gets silence, not a NAK | a NAK claims knowledge the device does not have |
| The IN toggle advances on the host's ACK | not when the data is sent |
| 2048-transition exhaustive verification | every state, every input combination |
| 7 mutations, all killed in 3 languages | after a testbench gap that hid two of them |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o ep_v.out ep_v.v ep_v_tb.v && ./ep_v.out |
| SystemVerilog | iverilog -g2012 -o ep_sv.out ep_sv.sv ep_sv_tb.sv && ./ep_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a ep_vhdl.vhd ep_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_ep_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_ep_vhdl |
| One mutation | iverilog -g2005 -DMUT_E1 -o mm ep_v_mut.v ep_v_tb.v && ./mm |
All three implementations pass with 0 errors: 2048 of 2048 exhaustive transitions, 40 000 randomised cycles, every handshake outcome and every internal state reached and asserted reached.
Chapter 21.2 — FIFO Architecture builds the thing this chapter's fifo_write_en writes into, and it turns on a distinction just as sharp: an endpoint FIFO stores packets, not bytes. A zero-length packet carries no data and is still a packet that must occupy a buffer — and a FIFO that cannot represent it hangs every transfer whose length happens to be an exact multiple of the packet size.
Continue learning
Related tutorials
- Related topic
USB in Embedded Devices
On a microcontroller the controller is a peripheral, and the protocol can be perfectly correct while the device goes deaf. Everything turns on one question — who owns this buffer right now — answered by one bit per buffer and exhausted over 132 transitions in three languages.
- Related topic
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.
- Related topic
Endpoint Problems
A NAK is not an error and a STALL is not a NAK — one is flow control working, one is firmware refusing permanently, and a monitor that treats them alike either floods the log or misses the endpoint that has stopped.
- Related topic
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.
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.
