USB · Module 27
What Is USB?
The opening interview question answered with one load-bearing idea instead of a list — USB is host-scheduled, and polling, NAK, the frame and the missing interrupt line are all consequences of it.
This is the first question in almost every USB interview, it is always asked, and it is almost never the question it appears to be.
1. What Is Actually Being Asked
"What is USB?" is not a request for the expansion of the acronym. The interviewer already knows. What they are measuring is whether you can compress a large protocol into its one load-bearing idea — because that is the skill the job needs, and ninety seconds of talking is a good way to find out whether you have it.
There are two failure modes and they are both common.
The list. "USB is a serial bus, it's hot-pluggable, it has four transfer types, it uses differential signalling, there's USB 1.1, 2.0, 3.x, USB-C…" This is all true, and it demonstrates that you have read a summary. Nothing in it is organising; any item could be removed without the rest changing.
The layer diagram. Starting at the physical layer and working up. This is how specifications are written and it is the wrong order for a person: it spends the first sixty seconds on NRZI and bit stuffing and never arrives at what the bus is for.
2. The Ninety-Second Answer
USB is a host-scheduled bus. There is exactly one host, it is the only thing on the bus that may start a transfer, and every device is strictly a responder: it transmits only in the window immediately after the host has addressed it, and when addressed it must answer.
That single asymmetry produces everything else. Because a device cannot initiate, there is no interrupt line from device to host — a device with urgent news must wait to be asked, so the host polls, and "interrupt transfers" in USB are a polling guarantee rather than an interrupt. Because the device must answer even when it has nothing, there is a handshake that means "ask me again", which is NAK. Because the host has to fit all its polling into a repeating budget, there is a frame: 1 ms in USB 2.0 full-speed, 125 µs microframes at high speed. And because the host alone decides who talks and when, collisions are not arbitrated — they are impossible by construction, and a bus with two talkers is not a busy bus, it is a broken one.
Everything else — the four transfer types, the endpoint model, enumeration, the tree of hubs — is the machinery that makes a single-initiator bus usable for keyboards and cameras and disks at the same time.
That is the answer. It is roughly seventy seconds spoken, it contains one idea, and it has already pre-answered the four most likely follow-ups.
3. Why "Host-Scheduled" Is the Right Centre
The test of an organising idea is whether the surrounding facts become consequences rather than additions. Here they do:
| Fact about USB | Consequence of what |
|---|---|
| No device-to-host interrupt line | a device cannot initiate |
| Interrupt transfers are polled | same |
| NAK exists at all | the device must answer even when empty |
| Frames / microframes | the host must budget its polling |
| No collision detection | one initiator means no collisions |
| Devices have addresses; the host does not | only the host sends tokens |
| Enumeration is host-driven | the device cannot announce itself |
| A hub is a repeater, not a switch | the tree has one root |
| Isochronous has no retry | a retry needs a slot the schedule did not reserve |
4. What We Are Building
An answer you can only say is worth less than one you can build. So the rest of this chapter is the sentence above turned into hardware: the smallest module that makes the defining property true rather than merely stated.
usb_token_gate sits between what a device wants to transmit and what it is permitted to transmit. Those are two different things, and the entire module is the difference.
What the device wants, and what it is allowed
The two edges out of the buffer are the design. One of them requires a token; the other is what happens the rest of the time, which is nothing.
A token, an answer, and silence
Note data_avail in that waveform. It is high the entire time. The device has something to send in every single cycle and sends it in exactly one of them.
NAK is a loop, not a refusal
A single NAK proves nothing — a design that NAKs permanently produces exactly the same first cycle. The property is that NAK is neither sticky nor terminal, and it takes repeated polling to test. Section 13 shows what happens to a mutation score when you do not.
5. Seven Properties
Everything the module is claimed to do, written down before the code, so that "it works" has a definition:
| # | Property |
|---|---|
| 1 | The device transmits only in the cycle after a token addressed to it. There is no other path. |
| 2 | A token for another address produces silence, and the silence is counted. |
| 3 | SOF is answered by nobody. It is a broadcast timestamp addressed to no device. |
| 4 | An IN token gets exactly one of three answers: DATA, NAK, or STALL — never nothing. |
| 5 | STALL takes priority over DATA: a halted endpoint is halted even with a full buffer. |
| 6 | A SETUP token is never stalled, halted or not — it is how software clears a halt. |
| 7 | A token superseded by another token is never answered. |
Property 7 is the one nobody writes on a whiteboard and it is a real failure: if the host asks, changes its mind, and asks something else, a device that answers the first question is transmitting into a slot that now belongs to another device.
6. Verilog-2005 RTL
// =====================================================================
// usb_token_gate -- "What is USB?" answered in hardware.
//
// Ninety seconds is enough for one idea, and there is only one that
// explains the rest of the protocol:
//
// A USB device may never initiate a transfer.
//
// It speaks only when the host has just addressed it, and it must
// say SOMETHING when it does. Every other feature of USB falls out
// of that: the polling, the NAK handshake that means "ask me again",
// the frame that bounds how often you are asked, and the complete
// absence of an interrupt line from device to host.
//
// This module is the gate that enforces it. It sits between what a
// device WANTS to transmit and what it is ALLOWED to transmit, and
// the whole design is the difference between those two things.
// =====================================================================
module usb_token_gate #(
parameter [6:0] DEV_ADDR = 7'h2A,
parameter N_EP = 4
) (
input wire clk,
input wire rst_n,
// ---- the bus, as the device sees it ----
input wire tok_valid,
input wire [1:0] tok_pid, // T_OUT / T_IN / T_SOF / T_SETUP
input wire [6:0] tok_addr,
input wire [1:0] tok_ep,
// ---- what the device would LIKE to do, every cycle ----
//
// Note that this is an INPUT and it is completely ignored except in
// the one cycle after a token. A device with a full transmit buffer
// and an urgent interrupt to report has exactly the same rights as
// an idle one: none.
input wire [N_EP-1:0] data_avail,
input wire [N_EP-1:0] halted,
// ---- what it is ALLOWED to do ----
output wire tx_valid,
output wire [2:0] tx_pid, // R_NONE / R_DATA / R_NAK / R_STALL / R_ACK
output wire [1:0] tx_ep,
// ---- observability: the counters ARE the proof ----
output wire [31:0] n_resp,
output wire [31:0] n_ignored,
output wire [31:0] n_nak,
output wire [31:0] n_stall,
output wire [31:0] n_superseded
);
localparam [1:0] T_OUT = 2'd0,
T_IN = 2'd1,
T_SOF = 2'd2,
T_SETUP = 2'd3;
localparam [2:0] R_NONE = 3'd0,
R_DATA = 3'd1,
R_NAK = 3'd2,
R_STALL = 3'd3,
R_ACK = 3'd4;
// ---- the entire state of the gate ----
//
// `armed` is the permission slip. It is set ONLY by a token that was
// addressed to this device, it lasts exactly one cycle, and nothing
// else in the design can set it.
reg armed;
reg [1:0] arm_ep;
reg [1:0] arm_pid;
reg tx_valid_r;
reg [2:0] tx_pid_r;
reg [1:0] tx_ep_r;
reg [31:0] resp_r, ign_r, nak_r, stall_r, sup_r;
assign tx_valid = tx_valid_r;
assign tx_pid = tx_pid_r;
assign tx_ep = tx_ep_r;
assign n_resp = resp_r;
assign n_ignored = ign_r;
assign n_nak = nak_r;
assign n_stall = stall_r;
assign n_superseded = sup_r;
// ---- is this token ours, and does it want an answer? ----
//
// Three separate questions, deliberately not collapsed into one
// expression, because each is a different way to get this wrong:
//
// for_us -- the address check. Dropping it makes the device
// answer for every other device on the bus.
// wants_rsp -- SOF is a broadcast timestamp. It is addressed to
// nobody and answered by nobody.
// in_dir -- an IN token asks for data; OUT/SETUP deliver it and
// get a handshake instead.
wire for_us = tok_valid && (tok_addr == DEV_ADDR);
wire wants_rsp = (tok_pid != T_SOF);
wire in_dir = (tok_pid == T_IN);
wire accept = for_us && wants_rsp;
integer i;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
armed <= 1'b0;
arm_ep <= 2'd0;
arm_pid <= T_OUT;
tx_valid_r <= 1'b0;
tx_pid_r <= R_NONE;
tx_ep_r <= 2'd0;
resp_r <= 32'd0;
ign_r <= 32'd0;
nak_r <= 32'd0;
stall_r <= 32'd0;
sup_r <= 32'd0;
end else begin
// ---- the response, driven from `armed` and NOTHING else ----
//
// Written first so that the arming logic below can overwrite it
// in the same cycle a new token arrives. The ordering matters:
// a token arriving while armed SUPERSEDES the old one, and the
// old response is never sent. A device that answers a token the
// host has already moved on from is transmitting into somebody
// else's slot.
tx_valid_r <= 1'b0;
tx_pid_r <= R_NONE;
tx_ep_r <= arm_ep;
if (armed) begin
tx_valid_r <= 1'b1;
tx_ep_r <= arm_ep;
resp_r <= resp_r + 32'd1;
if (arm_pid == T_IN) begin
// ---- an IN token: three legal answers and no fourth ----
//
// STALL is checked FIRST. A halted endpoint is halted whether
// or not it happens to have data sitting in its buffer, and
// sending that data would resume an endpoint that software
// has deliberately stopped.
if (halted[arm_ep]) begin
tx_pid_r <= R_STALL;
stall_r <= stall_r + 32'd1;
end else if (data_avail[arm_ep]) begin
tx_pid_r <= R_DATA;
end else begin
// NAK is not an error. It is the device saying "ask me
// again" -- which is the only way a device that cannot
// initiate is able to express not-ready at all.
tx_pid_r <= R_NAK;
nak_r <= nak_r + 32'd1;
end
end else begin
// OUT / SETUP: the host supplied the data, the device
// acknowledges. A halted endpoint still stalls.
if (halted[arm_ep] && (arm_pid != T_SETUP)) begin
tx_pid_r <= R_STALL;
stall_r <= stall_r + 32'd1;
end else begin
tx_pid_r <= R_ACK;
end
end
end
// ---- arming: the ONLY path to permission ----
if (tok_valid) begin
if (accept) begin
if (armed) sup_r <= sup_r + 32'd1;
armed <= 1'b1;
arm_ep <= tok_ep;
arm_pid <= tok_pid;
end else begin
// A token for another address, or an SOF. The device stays
// silent, and the silence is counted so a run can prove it
// saw traffic it correctly declined to answer.
if (armed) sup_r <= sup_r + 32'd1;
armed <= 1'b0;
ign_r <= ign_r + 32'd1;
end
end else begin
// No token this cycle. Permission expires. There is no path
// through this module by which `armed` becomes 1 here, and
// that absence is the entire point of the design.
armed <= 1'b0;
end
end
end
endmodule7. SystemVerilog RTL
// =====================================================================
// usb_token_gate -- SystemVerilog.
//
// Same design, same property, and two things the Verilog cannot say:
// the PIDs are named types rather than magic numbers, and the one
// rule the whole module exists for is written down as a function of
// the token rather than as a comment above it.
// =====================================================================
package tg_pkg;
typedef enum logic [1:0] { T_OUT = 2'd0, T_IN = 2'd1,
T_SOF = 2'd2, T_SETUP = 2'd3 } tok_e;
typedef enum logic [2:0] { R_NONE = 3'd0, R_DATA = 3'd1, R_NAK = 3'd2,
R_STALL = 3'd3, R_ACK = 3'd4 } rsp_e;
endpackage
module usb_token_gate
import tg_pkg::*;
#(
parameter logic [6:0] DEV_ADDR = 7'h2A,
parameter int N_EP = 4
) (
input logic clk,
input logic rst_n,
input logic tok_valid,
input logic [1:0] tok_pid,
input logic [6:0] tok_addr,
input logic [1:0] tok_ep,
// What the device would LIKE to do, every cycle -- and which is
// ignored except in the one cycle after a token addressed to it.
input logic [N_EP-1:0] data_avail,
input logic [N_EP-1:0] halted,
output logic tx_valid,
output logic [2:0] tx_pid,
output logic [1:0] tx_ep,
output logic [31:0] n_resp,
output logic [31:0] n_ignored,
output logic [31:0] n_nak,
output logic [31:0] n_stall,
output logic [31:0] n_superseded
);
logic armed;
logic [1:0] arm_ep, arm_pid;
logic [31:0] resp_r, ign_r, nak_r, stall_r, sup_r;
assign n_resp = resp_r;
assign n_ignored = ign_r;
assign n_nak = nak_r;
assign n_stall = stall_r;
assign n_superseded = sup_r;
// ---- the rule, as a function ----
//
// A token grants the right to transmit if and only if it names this
// device and is not the broadcast frame marker. Written as a function
// so that it has exactly one definition and one place to be wrong.
function automatic logic grants(logic valid, logic [6:0] a, logic [1:0] p);
return valid && (a == DEV_ADDR) && (p != T_SOF);
endfunction
wire for_us = tok_valid && (tok_addr == DEV_ADDR);
wire wants_rsp = (tok_pid != T_SOF);
wire accept = for_us && wants_rsp;
// The response for an armed IN token. A function rather than a
// nested ternary because Icarus will not take an enum-valued
// conditional, and because the priority -- STALL before DATA -- is
// a decision worth seeing on its own line.
function automatic logic [2:0] in_response(logic h, logic d);
if (h) return R_STALL;
if (d) return R_DATA;
return R_NAK;
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
armed <= 1'b0;
arm_ep <= 2'd0;
arm_pid <= T_OUT;
tx_valid <= 1'b0;
tx_pid <= R_NONE;
tx_ep <= 2'd0;
resp_r <= '0;
ign_r <= '0;
nak_r <= '0;
stall_r <= '0;
sup_r <= '0;
end else begin
tx_valid <= 1'b0;
tx_pid <= R_NONE;
tx_ep <= arm_ep;
if (armed) begin
tx_valid <= 1'b1;
tx_ep <= arm_ep;
resp_r <= resp_r + 32'd1;
if (arm_pid == T_IN) begin
tx_pid <= in_response(halted[arm_ep], data_avail[arm_ep]);
if (halted[arm_ep]) stall_r <= stall_r + 32'd1;
else if (!data_avail[arm_ep]) nak_r <= nak_r + 32'd1;
end else begin
// A SETUP is never stalled. The control endpoint is how
// software clears a halt in the first place, so a device
// that stalls SETUP has locked itself out permanently.
if (halted[arm_ep] && (arm_pid != T_SETUP)) begin
tx_pid <= R_STALL;
stall_r <= stall_r + 32'd1;
end else begin
tx_pid <= R_ACK;
end
end
end
if (tok_valid) begin
if (accept) begin
if (armed) sup_r <= sup_r + 32'd1;
armed <= 1'b1;
arm_ep <= tok_ep;
arm_pid <= tok_pid;
end else begin
if (armed) sup_r <= sup_r + 32'd1;
armed <= 1'b0;
ign_r <= ign_r + 32'd1;
end
end else begin
armed <= 1'b0;
end
end
end
endmodule8. VHDL-2008 RTL
-- =====================================================================
-- usb_token_gate -- VHDL-2008.
--
-- The third language is not decoration. Porting a design forces every
-- implicit assumption in it to be written down, and a property that
-- survives three independent expressions is a property rather than an
-- artefact of one compiler's idea of what you meant.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package tg_pkg is
-- Deliberately NOT named T_OUT / R_NONE: VHDL identifiers are case
-- insensitive, so a package constant and a port that differ only in
-- case are the same name, and the port silently wins.
constant TOK_OUT : std_logic_vector(1 downto 0) := "00";
constant TOK_IN : std_logic_vector(1 downto 0) := "01";
constant TOK_SOF : std_logic_vector(1 downto 0) := "10";
constant TOK_SETUP : std_logic_vector(1 downto 0) := "11";
constant RSP_NONE : std_logic_vector(2 downto 0) := "000";
constant RSP_DATA : std_logic_vector(2 downto 0) := "001";
constant RSP_NAK : std_logic_vector(2 downto 0) := "010";
constant RSP_STALL : std_logic_vector(2 downto 0) := "011";
constant RSP_ACK : std_logic_vector(2 downto 0) := "100";
end package;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.tg_pkg.all;
entity usb_token_gate is
generic (
DEV_ADDR : std_logic_vector(6 downto 0) := "0101010";
N_EP : natural := 4
);
port (
clk : in std_logic;
rst_n : in std_logic;
tok_valid : in std_logic;
tok_pid : in std_logic_vector(1 downto 0);
tok_addr : in std_logic_vector(6 downto 0);
tok_ep : in std_logic_vector(1 downto 0);
data_avail : in std_logic_vector(N_EP-1 downto 0);
halted : in std_logic_vector(N_EP-1 downto 0);
tx_valid : out std_logic;
tx_pid : out std_logic_vector(2 downto 0);
tx_ep : out std_logic_vector(1 downto 0);
n_resp : out std_logic_vector(31 downto 0);
n_ignored : 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_superseded : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of usb_token_gate is
signal armed : std_logic := '0';
signal arm_ep : std_logic_vector(1 downto 0) := "00";
signal arm_pid : std_logic_vector(1 downto 0) := TOK_OUT;
signal resp_r, ign_r, nak_r, stall_r, sup_r : unsigned(31 downto 0)
:= (others => '0');
signal for_us, wants_rsp, accept_tok : std_logic;
begin
for_us <= '1' when (tok_valid = '1' and tok_addr = DEV_ADDR) else '0';
wants_rsp <= '1' when (tok_pid /= TOK_SOF) else '0';
accept_tok <= for_us and wants_rsp;
n_resp <= std_logic_vector(resp_r);
n_ignored <= std_logic_vector(ign_r);
n_nak <= std_logic_vector(nak_r);
n_stall <= std_logic_vector(stall_r);
n_superseded <= std_logic_vector(sup_r);
process(clk, rst_n)
variable ep_i : natural;
begin
if rst_n = '0' then
armed <= '0';
arm_ep <= "00";
arm_pid <= TOK_OUT;
tx_valid <= '0';
tx_pid <= RSP_NONE;
tx_ep <= "00";
resp_r <= (others => '0');
ign_r <= (others => '0');
nak_r <= (others => '0');
stall_r <= (others => '0');
sup_r <= (others => '0');
elsif rising_edge(clk) then
tx_valid <= '0';
tx_pid <= RSP_NONE;
tx_ep <= arm_ep;
if armed = '1' then
ep_i := to_integer(unsigned(arm_ep));
tx_valid <= '1';
tx_ep <= arm_ep;
resp_r <= resp_r + 1;
if arm_pid = TOK_IN then
-- STALL is tested first: a halted endpoint stays halted even
-- when its buffer happens to be full.
if halted(ep_i) = '1' then
tx_pid <= RSP_STALL;
stall_r <= stall_r + 1;
elsif data_avail(ep_i) = '1' then
tx_pid <= RSP_DATA;
else
tx_pid <= RSP_NAK;
nak_r <= nak_r + 1;
end if;
else
if halted(ep_i) = '1' and arm_pid /= TOK_SETUP then
tx_pid <= RSP_STALL;
stall_r <= stall_r + 1;
else
tx_pid <= RSP_ACK;
end if;
end if;
end if;
if tok_valid = '1' then
if accept_tok = '1' then
if armed = '1' then sup_r <= sup_r + 1; end if;
armed <= '1';
arm_ep <= tok_ep;
arm_pid <= tok_pid;
else
if armed = '1' then sup_r <= sup_r + 1; end if;
armed <= '0';
ign_r <= ign_r + 1;
end if;
else
-- No token. Permission expires, and there is no other path by
-- which it is granted.
armed <= '0';
end if;
end if;
end process;
end architecture;9. How the Testbench Knows the Answer
The shadow model is deliberately not a second copy of the design's state machine. The design asks "am I armed?". The shadow asks "was the token in the previous cycle addressed to me and not an SOF?" — the same property expressed as a statement about history rather than about state.
That distinction is the whole value of the model. A bug in a state machine can hide perfectly inside an identical state machine; it cannot hide inside a different formulation of the same claim.
design: tx_valid <= armed (a state question)
shadow: expect = p_acc (a history question)
where p_acc is recomputed each cycle as
"the previous token named me and was not SOF"
Same property. Different machine. Either one being
wrong shows up as a disagreement.On top of that, the bench keeps a counter called n_unsolicited: every cycle in which the device transmitted without a preceding accepted token. It is asserted against zero on every step and printed at the end, because "it never happened" is a claim that needs a number behind it — and because a run in which the situation never arose would otherwise report PASS while proving nothing.
Verilog-2005 testbench
// =====================================================================
// Testbench for usb_token_gate.
//
// The shadow model is deliberately NOT a copy of the design's FSM.
// The design asks "am I armed?"; the shadow asks "was the token in
// the PREVIOUS cycle addressed to me and not an SOF?" -- the same
// property expressed as a statement about history rather than about
// state, so that a defect in the state machine cannot hide inside an
// identical state machine.
// =====================================================================
`timescale 1ns/1ps
module tb_tg_v;
localparam [6:0] DEV_ADDR = 7'h2A;
localparam N_EP = 4;
localparam [1:0] T_OUT = 2'd0, T_IN = 2'd1, T_SOF = 2'd2, T_SETUP = 2'd3;
localparam [2:0] R_NONE = 3'd0, R_DATA = 3'd1, R_NAK = 3'd2,
R_STALL = 3'd3, R_ACK = 3'd4;
reg clk = 1'b0, rst_n = 1'b0;
reg tok_valid = 1'b0;
reg [1:0] tok_pid = T_OUT;
reg [6:0] tok_addr = 7'd0;
reg [1:0] tok_ep = 2'd0;
reg [N_EP-1:0] data_avail = {N_EP{1'b0}};
reg [N_EP-1:0] halted = {N_EP{1'b0}};
wire tx_valid;
wire [2:0] tx_pid;
wire [1:0] tx_ep;
wire [31:0] n_resp, n_ignored, n_nak, n_stall, n_superseded;
usb_token_gate #(.DEV_ADDR(DEV_ADDR), .N_EP(N_EP)) dut (
.clk(clk), .rst_n(rst_n),
.tok_valid(tok_valid), .tok_pid(tok_pid),
.tok_addr(tok_addr), .tok_ep(tok_ep),
.data_avail(data_avail), .halted(halted),
.tx_valid(tx_valid), .tx_pid(tx_pid), .tx_ep(tx_ep),
.n_resp(n_resp), .n_ignored(n_ignored), .n_nak(n_nak),
.n_stall(n_stall), .n_superseded(n_superseded)
);
always #5 clk = ~clk;
integer errors = 0, checks = 0, steps = 0;
// ---- the shadow, as history ----
reg p_acc; // the previous cycle's token was for us
reg [1:0] p_ep, p_pid;
reg [31:0] s_resp, s_ign, s_nak, s_stall, s_sup;
// ---- the headline counter ----
//
// Every cycle in which the device transmitted WITHOUT a preceding
// accepted token. It is checked against zero on every single step
// and reported at the end, because "it never happened" is a claim
// that needs a number behind it.
integer n_unsolicited = 0;
// ---- exhaustive reach over (pid, ours, ep, avail, halt) ----
reg reach [0:127];
integer ri, n_reach;
task ck(input cond, input [255:0] what);
begin
checks = checks + 1;
if (!cond) begin
errors = errors + 1;
if (errors <= 20)
$display(" ERROR @%0t step=%0d: %0s", $time, steps, what);
end
end
endtask
// ---------------------------------------------------------------
// One bus cycle: drive, clock, check, then advance the shadow.
// ---------------------------------------------------------------
task step(input tv, input [1:0] tp, input [6:0] ta, input [1:0] te,
input [N_EP-1:0] da, input [N_EP-1:0] hl);
reg e_valid;
reg [2:0] e_pid;
reg [1:0] e_ep;
reg acc;
begin
tok_valid = tv; tok_pid = tp; tok_addr = ta; tok_ep = te;
data_avail = da; halted = hl;
// ---- what the device is allowed to do on THIS edge ----
//
// Entirely a function of the PREVIOUS cycle's token plus this
// cycle's device state. Nothing about the current token can
// create a right to transmit now.
e_valid = p_acc;
e_ep = p_ep;
e_pid = R_NONE;
if (p_acc) begin
if (p_pid == T_IN) begin
if (hl[p_ep]) e_pid = R_STALL;
else if (da[p_ep]) e_pid = R_DATA;
else e_pid = R_NAK;
end else begin
if (hl[p_ep] && (p_pid != T_SETUP)) e_pid = R_STALL;
else e_pid = R_ACK;
end
end
// shadow counters, advanced before the edge so they can be
// compared against the design's on the same cycle
if (p_acc) begin
s_resp = s_resp + 1;
if (e_pid == R_NAK) s_nak = s_nak + 1;
if (e_pid == R_STALL) s_stall = s_stall + 1;
end
acc = tv && (ta == DEV_ADDR) && (tp != T_SOF);
if (tv) begin
if (p_acc) s_sup = s_sup + 1;
if (!acc) s_ign = s_ign + 1;
end
@(posedge clk);
#1;
steps = steps + 1;
// ---- PROPERTY 1: no transmission without permission ----
if (tx_valid && !e_valid) n_unsolicited = n_unsolicited + 1;
ck(tx_valid === e_valid, "tx_valid disagrees with the token history");
// ---- PROPERTY 2: the right answer, on the right endpoint ----
if (e_valid) begin
ck(tx_pid === e_pid, "wrong response PID");
ck(tx_ep === e_ep, "response on the wrong endpoint");
end else begin
ck(tx_pid === R_NONE, "a PID was driven while silent");
end
// ---- PROPERTY 3: the counters agree with an independent tally ----
ck(n_resp === s_resp, "response count disagrees");
ck(n_ignored === s_ign, "ignored-token count disagrees");
ck(n_nak === s_nak, "NAK count disagrees");
ck(n_stall === s_stall, "STALL count disagrees");
ck(n_superseded === s_sup, "superseded count disagrees");
// ---- PROPERTY 4: the headline, asserted every cycle ----
ck(n_unsolicited == 0, "the device transmitted unsolicited");
// advance the history
p_acc = acc;
if (acc) begin p_ep = te; p_pid = tp; end
end
endtask
task reset_dut;
begin
rst_n = 1'b0;
tok_valid = 1'b0; data_avail = {N_EP{1'b0}}; halted = {N_EP{1'b0}};
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
p_acc = 1'b0; p_ep = 2'd0; p_pid = T_OUT;
s_resp = 0; s_ign = 0; s_nak = 0; s_stall = 0; s_sup = 0;
@(posedge clk); #1;
end
endtask
integer pi, oi, ei, ai, hi, k, seed;
reg [N_EP-1:0] dv, hv;
reg [6:0] addr;
initial begin
for (ri = 0; ri < 128; ri = ri + 1) reach[ri] = 1'b0;
seed = 32'd27001;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- every token against every
// device state. 4 PIDs x ours/not x 4 endpoints x avail x halt.
// =============================================================
reset_dut;
for (pi = 0; pi < 4; pi = pi + 1)
for (oi = 0; oi < 2; oi = oi + 1)
for (ei = 0; ei < 4; ei = ei + 1)
for (ai = 0; ai < 2; ai = ai + 1)
for (hi = 0; hi < 2; hi = hi + 1) begin
dv = ai ? (1 << ei) : {N_EP{1'b0}};
hv = hi ? (1 << ei) : {N_EP{1'b0}};
addr = oi ? DEV_ADDR : 7'h55;
// the token ...
step(1'b1, pi[1:0], addr, ei[1:0], dv, hv);
// ... and the cycle in which the answer is due
step(1'b0, T_OUT, 7'd0, 2'd0, dv, hv);
// ... and one more, in which there must be silence again
step(1'b0, T_OUT, 7'd0, 2'd0, dv, hv);
ri = (pi << 5) | (oi << 4) | (ei << 2) | (ai << 1) | hi;
reach[ri] = 1'b1;
end
// =============================================================
// PHASE 2 (DIRECTED) -- the device wants to talk and may not.
//
// Every data_avail pattern, with no token on the bus at all.
// This is the 90-second answer as an experiment: a device with
// sixteen different kinds of urgent news and no way to deliver
// any of it.
// =============================================================
reset_dut;
for (k = 0; k < 16; k = k + 1) begin
dv = k[3:0];
step(1'b0, T_OUT, 7'd0, 2'd0, dv, {N_EP{1'b0}});
step(1'b0, T_OUT, 7'd0, 2'd0, dv, {N_EP{1'b0}});
step(1'b0, T_IN, 7'd0, 2'd0, dv, {N_EP{1'b0}}); // pid set, no valid
end
// =============================================================
// PHASE 2b (DIRECTED) -- NAK is a loop, not an answer.
//
// An empty endpoint must NAK EVERY time it is polled, not once,
// and must switch to DATA the instant it has something. That is
// the entire mechanism by which a device which cannot initiate
// is nonetheless able to say "not yet" -- and testing it once
// per endpoint tests nothing, because a single NAK is also what
// a design that NAKs permanently would produce.
// =============================================================
reset_dut;
for (ei = 0; ei < 4; ei = ei + 1) begin
for (k = 0; k < 6; k = k + 1) begin
step(1'b1, T_IN, DEV_ADDR, ei[1:0], 4'h0, 4'h0);
step(1'b0, T_OUT, 7'd0, 2'd0, 4'h0, 4'h0);
end
step(1'b1, T_IN, DEV_ADDR, ei[1:0], (4'h1 << ei), 4'h0);
step(1'b0, T_OUT, 7'd0, 2'd0, (4'h1 << ei), 4'h0);
for (k = 0; k < 3; k = k + 1) begin
step(1'b1, T_IN, DEV_ADDR, ei[1:0], 4'h0, 4'h0);
step(1'b0, T_OUT, 7'd0, 2'd0, 4'h0, 4'h0);
end
end
// =============================================================
// PHASE 3 (DIRECTED) -- a superseded token is never answered.
//
// Back-to-back tokens: the host asked, changed its mind, and
// asked something else. A device that answers the first is
// transmitting into the slot the host gave to somebody else.
// Exhaustive over (first ep x second ep x second is-ours).
// =============================================================
reset_dut;
for (ei = 0; ei < 4; ei = ei + 1)
for (k = 0; k < 4; k = k + 1)
for (oi = 0; oi < 2; oi = oi + 1) begin
step(1'b1, T_IN, DEV_ADDR, ei[1:0], 4'hF, 4'h0);
step(1'b1, T_IN, oi ? DEV_ADDR : 7'h55, k[1:0], 4'hF, 4'h0);
step(1'b0, T_OUT, 7'd0, 2'd0, 4'hF, 4'h0);
step(1'b0, T_OUT, 7'd0, 2'd0, 4'hF, 4'h0);
end
// =============================================================
// PHASE 4 (RANDOM) -- a busy bus with four other devices.
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut;
for (k = 0; k < 40000; k = k + 1) begin
// A token on most cycles, because a real bus is busy; a quarter
// of them ours, the rest addressed to the other devices whose
// traffic this device must sit through in silence.
if (($random(seed) % 100) < 70)
step(1'b1,
($random(seed) % 4),
(($random(seed) % 4) == 0) ? DEV_ADDR : (($random(seed) & 7'h7F) | 7'h01),
($random(seed) % 4),
($random(seed) & 4'hF),
(($random(seed) % 8) == 0) ? ($random(seed) & 4'hF) : 4'h0);
else
step(1'b0, T_OUT, 7'd0, 2'd0, ($random(seed) & 4'hF), 4'h0);
end
`endif
n_reach = 0;
for (ri = 0; ri < 128; ri = ri + 1) if (reach[ri]) n_reach = n_reach + 1;
$display("steps=%0d checks=%0d reach=%0d/128 errors=%0d",
steps, checks, n_reach, errors);
$display("[bus] responses=%0d ignored=%0d nak=%0d stall=%0d superseded=%0d",
n_resp, n_ignored, n_nak, n_stall, n_superseded);
$display("[the whole point] unsolicited transmissions = %0d", n_unsolicited);
if (n_reach != 128) begin
$display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
end
if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
else $display("FAIL: %0d errors in %0d checks", errors, checks);
$finish;
end
endmoduleSystemVerilog testbench
// =====================================================================
// Testbench for usb_token_gate.
//
// The shadow model is deliberately NOT a copy of the design's FSM.
// The design asks "am I armed?"; the shadow asks "was the token in
// the PREVIOUS cycle addressed to me and not an SOF?" -- the same
// property expressed as a statement about history rather than about
// state, so that a defect in the state machine cannot hide inside an
// identical state machine.
// =====================================================================
`timescale 1ns/1ps
module tb_tg_sv;
import tg_pkg::*;
localparam [6:0] DEV_ADDR = 7'h2A;
localparam N_EP = 4;
logic clk = 1'b0, rst_n = 1'b0;
logic tok_valid = 1'b0;
logic[1:0] tok_pid = T_OUT;
logic[6:0] tok_addr = 7'd0;
logic[1:0] tok_ep = 2'd0;
logic[N_EP-1:0] data_avail = {N_EP{1'b0}};
logic[N_EP-1:0] halted = {N_EP{1'b0}};
logic tx_valid;
logic [2:0] tx_pid;
logic [1:0] tx_ep;
logic [31:0] n_resp, n_ignored, n_nak, n_stall, n_superseded;
usb_token_gate #(.DEV_ADDR(DEV_ADDR), .N_EP(N_EP)) dut (
.clk(clk), .rst_n(rst_n),
.tok_valid(tok_valid), .tok_pid(tok_pid),
.tok_addr(tok_addr), .tok_ep(tok_ep),
.data_avail(data_avail), .halted(halted),
.tx_valid(tx_valid), .tx_pid(tx_pid), .tx_ep(tx_ep),
.n_resp(n_resp), .n_ignored(n_ignored), .n_nak(n_nak),
.n_stall(n_stall), .n_superseded(n_superseded)
);
always #5 clk = ~clk;
integer errors = 0, checks = 0, steps = 0;
// ---- the shadow, as history ----
logic p_acc; // the previous cycle's token was for us
logic [1:0] p_ep, p_pid;
logic [31:0] s_resp, s_ign, s_nak, s_stall, s_sup;
// ---- the headline counter ----
//
// Every cycle in which the device transmitted WITHOUT a preceding
// accepted token. It is checked against zero on every single step
// and reported at the end, because "it never happened" is a claim
// that needs a number behind it.
integer n_unsolicited = 0;
// ---- exhaustive reach over (pid, ours, ep, avail, halt) ----
logic reach [0:127];
integer ri, n_reach;
task ck(input cond, input logic [255:0] what);
begin
checks = checks + 1;
if (!cond) begin
errors = errors + 1;
if (errors <= 20)
$display(" ERROR @%0t step=%0d: %0s", $time, steps, what);
end
end
endtask
// ---------------------------------------------------------------
// One bus cycle: drive, clock, check, then advance the shadow.
// ---------------------------------------------------------------
task step(input tv, input [1:0] tp, input [6:0] ta, input [1:0] te,
input [N_EP-1:0] da, input [N_EP-1:0] hl);
logic e_valid;
logic [2:0] e_pid;
logic [1:0] e_ep;
logic acc;
begin
tok_valid = tv; tok_pid = tp; tok_addr = ta; tok_ep = te;
data_avail = da; halted = hl;
// ---- what the device is allowed to do on THIS edge ----
//
// Entirely a function of the PREVIOUS cycle's token plus this
// cycle's device state. Nothing about the current token can
// create a right to transmit now.
e_valid = p_acc;
e_ep = p_ep;
e_pid = R_NONE;
if (p_acc) begin
if (p_pid == T_IN) begin
if (hl[p_ep]) e_pid = R_STALL;
else if (da[p_ep]) e_pid = R_DATA;
else e_pid = R_NAK;
end else begin
if (hl[p_ep] && (p_pid != T_SETUP)) e_pid = R_STALL;
else e_pid = R_ACK;
end
end
// shadow counters, advanced before the edge so they can be
// compared against the design's on the same cycle
if (p_acc) begin
s_resp = s_resp + 1;
if (e_pid == R_NAK) s_nak = s_nak + 1;
if (e_pid == R_STALL) s_stall = s_stall + 1;
end
acc = tv && (ta == DEV_ADDR) && (tp != T_SOF);
if (tv) begin
if (p_acc) s_sup = s_sup + 1;
if (!acc) s_ign = s_ign + 1;
end
@(posedge clk);
#1;
steps = steps + 1;
// ---- PROPERTY 1: no transmission without permission ----
if (tx_valid && !e_valid) n_unsolicited = n_unsolicited + 1;
ck(tx_valid === e_valid, "tx_valid disagrees with the token history");
// ---- PROPERTY 2: the right answer, on the right endpoint ----
if (e_valid) begin
ck(tx_pid === e_pid, "wrong response PID");
ck(tx_ep === e_ep, "response on the wrong endpoint");
end else begin
ck(tx_pid === R_NONE, "a PID was driven while silent");
end
// ---- PROPERTY 3: the counters agree with an independent tally ----
ck(n_resp === s_resp, "response count disagrees");
ck(n_ignored === s_ign, "ignored-token count disagrees");
ck(n_nak === s_nak, "NAK count disagrees");
ck(n_stall === s_stall, "STALL count disagrees");
ck(n_superseded === s_sup, "superseded count disagrees");
// ---- PROPERTY 4: the headline, asserted every cycle ----
ck(n_unsolicited == 0, "the device transmitted unsolicited");
// advance the history
p_acc = acc;
if (acc) begin p_ep = te; p_pid = tp; end
end
endtask
task reset_dut;
begin
rst_n = 1'b0;
tok_valid = 1'b0; data_avail = {N_EP{1'b0}}; halted = {N_EP{1'b0}};
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
p_acc = 1'b0; p_ep = 2'd0; p_pid = T_OUT;
s_resp = 0; s_ign = 0; s_nak = 0; s_stall = 0; s_sup = 0;
@(posedge clk); #1;
end
endtask
integer pi, oi, ei, ai, hi, k, seed;
logic [N_EP-1:0] dv, hv;
logic [6:0] addr;
initial begin
for (ri = 0; ri < 128; ri = ri + 1) reach[ri] = 1'b0;
seed = 32'd27001;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- every token against every
// device state. 4 PIDs x ours/not x 4 endpoints x avail x halt.
// =============================================================
reset_dut;
for (pi = 0; pi < 4; pi = pi + 1)
for (oi = 0; oi < 2; oi = oi + 1)
for (ei = 0; ei < 4; ei = ei + 1)
for (ai = 0; ai < 2; ai = ai + 1)
for (hi = 0; hi < 2; hi = hi + 1) begin
dv = ai ? (1 << ei) : {N_EP{1'b0}};
hv = hi ? (1 << ei) : {N_EP{1'b0}};
addr = oi ? DEV_ADDR : 7'h55;
// the token ...
step(1'b1, pi[1:0], addr, ei[1:0], dv, hv);
// ... and the cycle in which the answer is due
step(1'b0, T_OUT, 7'd0, 2'd0, dv, hv);
// ... and one more, in which there must be silence again
step(1'b0, T_OUT, 7'd0, 2'd0, dv, hv);
ri = (pi << 5) | (oi << 4) | (ei << 2) | (ai << 1) | hi;
reach[ri] = 1'b1;
end
// =============================================================
// PHASE 2 (DIRECTED) -- the device wants to talk and may not.
//
// Every data_avail pattern, with no token on the bus at all.
// This is the 90-second answer as an experiment: a device with
// sixteen different kinds of urgent news and no way to deliver
// any of it.
// =============================================================
reset_dut;
for (k = 0; k < 16; k = k + 1) begin
dv = k[3:0];
step(1'b0, T_OUT, 7'd0, 2'd0, dv, {N_EP{1'b0}});
step(1'b0, T_OUT, 7'd0, 2'd0, dv, {N_EP{1'b0}});
step(1'b0, T_IN, 7'd0, 2'd0, dv, {N_EP{1'b0}}); // pid set, no valid
end
// =============================================================
// PHASE 2b (DIRECTED) -- NAK is a loop, not an answer.
//
// An empty endpoint must NAK EVERY time it is polled, not once,
// and must switch to DATA the instant it has something. That is
// the entire mechanism by which a device which cannot initiate
// is nonetheless able to say "not yet" -- and testing it once
// per endpoint tests nothing, because a single NAK is also what
// a design that NAKs permanently would produce.
// =============================================================
reset_dut;
for (ei = 0; ei < 4; ei = ei + 1) begin
for (k = 0; k < 6; k = k + 1) begin
step(1'b1, T_IN, DEV_ADDR, ei[1:0], 4'h0, 4'h0);
step(1'b0, T_OUT, 7'd0, 2'd0, 4'h0, 4'h0);
end
step(1'b1, T_IN, DEV_ADDR, ei[1:0], (4'h1 << ei), 4'h0);
step(1'b0, T_OUT, 7'd0, 2'd0, (4'h1 << ei), 4'h0);
for (k = 0; k < 3; k = k + 1) begin
step(1'b1, T_IN, DEV_ADDR, ei[1:0], 4'h0, 4'h0);
step(1'b0, T_OUT, 7'd0, 2'd0, 4'h0, 4'h0);
end
end
// =============================================================
// PHASE 3 (DIRECTED) -- a superseded token is never answered.
//
// Back-to-back tokens: the host asked, changed its mind, and
// asked something else. A device that answers the first is
// transmitting into the slot the host gave to somebody else.
// Exhaustive over (first ep x second ep x second is-ours).
// =============================================================
reset_dut;
for (ei = 0; ei < 4; ei = ei + 1)
for (k = 0; k < 4; k = k + 1)
for (oi = 0; oi < 2; oi = oi + 1) begin
step(1'b1, T_IN, DEV_ADDR, ei[1:0], 4'hF, 4'h0);
step(1'b1, T_IN, oi ? DEV_ADDR : 7'h55, k[1:0], 4'hF, 4'h0);
step(1'b0, T_OUT, 7'd0, 2'd0, 4'hF, 4'h0);
step(1'b0, T_OUT, 7'd0, 2'd0, 4'hF, 4'h0);
end
// =============================================================
// PHASE 4 (RANDOM) -- a busy bus with four other devices.
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut;
for (k = 0; k < 40000; k = k + 1) begin
// A token on most cycles, because a real bus is busy; a quarter
// of them ours, the rest addressed to the other devices whose
// traffic this device must sit through in silence.
if (($random(seed) % 100) < 70)
step(1'b1,
($random(seed) % 4),
(($random(seed) % 4) == 0) ? DEV_ADDR : (($random(seed) & 7'h7F) | 7'h01),
($random(seed) % 4),
($random(seed) & 4'hF),
(($random(seed) % 8) == 0) ? ($random(seed) & 4'hF) : 4'h0);
else
step(1'b0, T_OUT, 7'd0, 2'd0, ($random(seed) & 4'hF), 4'h0);
end
`endif
n_reach = 0;
for (ri = 0; ri < 128; ri = ri + 1) if (reach[ri]) n_reach = n_reach + 1;
$display("steps=%0d checks=%0d reach=%0d/128 errors=%0d",
steps, checks, n_reach, errors);
$display("[bus] responses=%0d ignored=%0d nak=%0d stall=%0d superseded=%0d",
n_resp, n_ignored, n_nak, n_stall, n_superseded);
$display("[the whole point] unsolicited transmissions = %0d", n_unsolicited);
if (n_reach != 128) begin
$display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
end
if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
else $display("FAIL: %0d errors in %0d checks", errors, checks);
$finish;
end
endmoduleVHDL-2008 testbench
-- =====================================================================
-- Testbench for usb_token_gate (VHDL-2008).
--
-- Same shadow model as the Verilog bench and the same phases, with an
-- independent pseudo-random source -- so the random columns of the
-- mutation table are genuinely a second opinion rather than the same
-- stimulus compiled twice.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.tg_pkg.all;
entity tb_tg_vhdl is
-- VHDL has no preprocessor, so the directed/random split is an
-- elaboration-time generic: nvc -e -gDIRECTED_ONLY=true
generic (DIRECTED_ONLY : boolean := false);
end entity;
architecture sim of tb_tg_vhdl is
constant DEV_ADDR : std_logic_vector(6 downto 0) := "0101010";
constant N_EP : natural := 4;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal tok_valid : std_logic := '0';
signal tok_pid : std_logic_vector(1 downto 0) := TOK_OUT;
signal tok_addr : std_logic_vector(6 downto 0) := (others => '0');
signal tok_ep : std_logic_vector(1 downto 0) := "00";
signal data_avail : std_logic_vector(N_EP-1 downto 0) := (others => '0');
signal halted : std_logic_vector(N_EP-1 downto 0) := (others => '0');
signal tx_valid : std_logic;
signal tx_pid : std_logic_vector(2 downto 0);
signal tx_ep : std_logic_vector(1 downto 0);
signal n_resp, n_ignored, n_nak, n_stall, n_superseded
: std_logic_vector(31 downto 0);
signal done : boolean := false;
begin
dut : entity work.usb_token_gate
generic map (DEV_ADDR => DEV_ADDR, N_EP => N_EP)
port map (
clk => clk, rst_n => rst_n,
tok_valid => tok_valid, tok_pid => tok_pid,
tok_addr => tok_addr, tok_ep => tok_ep,
data_avail => data_avail, halted => halted,
tx_valid => tx_valid, tx_pid => tx_pid, tx_ep => tx_ep,
n_resp => n_resp, n_ignored => n_ignored, n_nak => n_nak,
n_stall => n_stall, n_superseded => n_superseded);
clk <= not clk after 5 ns when not done else '0';
stim : process
variable errors : natural := 0;
variable checks : natural := 0;
variable steps : natural := 0;
variable unsol : natural := 0;
variable p_acc : std_logic := '0';
variable p_ep : std_logic_vector(1 downto 0) := "00";
variable p_pid : std_logic_vector(1 downto 0) := TOK_OUT;
variable s_resp, s_ign, s_nak, s_stall, s_sup : natural := 0;
variable reach : std_logic_vector(0 to 127) := (others => '0');
variable n_reach : natural := 0;
variable rnd : unsigned(31 downto 0) := x"0006975B";
variable ln : line;
-- VHDL wants every subprogram declared before it is used, so the
-- order of these three is load-bearing.
procedure ck(cond : boolean; what : string) is
begin
checks := checks + 1;
if not cond then
errors := errors + 1;
if errors <= 20 then
write(ln, string'(" ERROR step=") & integer'image(steps)
& string'(": ") & what);
writeline(output, ln);
end if;
end if;
end procedure;
impure function nxt return natural is
begin
-- a 32-bit xorshift: unrelated to Icarus's generator, which is
-- the entire reason the VHDL random column means something
rnd := rnd xor (rnd sll 13);
rnd := rnd xor (rnd srl 17);
rnd := rnd xor (rnd sll 5);
return to_integer(rnd(14 downto 0));
end function;
procedure step(tv : std_logic;
tp : std_logic_vector(1 downto 0);
ta : std_logic_vector(6 downto 0);
te : std_logic_vector(1 downto 0);
da : std_logic_vector(N_EP-1 downto 0);
hl : std_logic_vector(N_EP-1 downto 0)) is
variable e_valid : std_logic;
variable e_pid : std_logic_vector(2 downto 0);
variable e_ep : std_logic_vector(1 downto 0);
variable acc : std_logic;
variable pe : natural;
begin
tok_valid <= tv; tok_pid <= tp; tok_addr <= ta; tok_ep <= te;
data_avail <= da; halted <= hl;
e_valid := p_acc;
e_ep := p_ep;
e_pid := RSP_NONE;
pe := to_integer(unsigned(p_ep));
if p_acc = '1' then
if p_pid = TOK_IN then
if hl(pe) = '1' then e_pid := RSP_STALL;
elsif da(pe) = '1' then e_pid := RSP_DATA;
else e_pid := RSP_NAK;
end if;
else
if hl(pe) = '1' and p_pid /= TOK_SETUP then e_pid := RSP_STALL;
else e_pid := RSP_ACK;
end if;
end if;
s_resp := s_resp + 1;
if e_pid = RSP_NAK then s_nak := s_nak + 1; end if;
if e_pid = RSP_STALL then s_stall := s_stall + 1; end if;
end if;
acc := '0';
if tv = '1' and ta = DEV_ADDR and tp /= TOK_SOF then acc := '1'; end if;
if tv = '1' then
if p_acc = '1' then s_sup := s_sup + 1; end if;
if acc = '0' then s_ign := s_ign + 1; end if;
end if;
wait until rising_edge(clk);
wait for 1 ns;
steps := steps + 1;
if tx_valid = '1' and e_valid = '0' then unsol := unsol + 1; end if;
ck(tx_valid = e_valid, "tx_valid disagrees with the token history");
if e_valid = '1' then
ck(tx_pid = e_pid, "wrong response PID");
ck(tx_ep = e_ep, "response on the wrong endpoint");
else
ck(tx_pid = RSP_NONE, "a PID was driven while silent");
end if;
ck(to_integer(unsigned(n_resp)) = s_resp, "response count disagrees");
ck(to_integer(unsigned(n_ignored)) = s_ign, "ignored-token count disagrees");
ck(to_integer(unsigned(n_nak)) = s_nak, "NAK count disagrees");
ck(to_integer(unsigned(n_stall)) = s_stall, "STALL count disagrees");
ck(to_integer(unsigned(n_superseded)) = s_sup, "superseded count disagrees");
ck(unsol = 0, "the device transmitted unsolicited");
p_acc := acc;
if acc = '1' then p_ep := te; p_pid := tp; end if;
end procedure;
procedure reset_dut is
begin
rst_n <= '0';
tok_valid <= '0';
data_avail <= (others => '0');
halted <= (others => '0');
wait until rising_edge(clk);
wait until rising_edge(clk);
rst_n <= '1';
p_acc := '0'; p_ep := "00"; p_pid := TOK_OUT;
s_resp := 0; s_ign := 0; s_nak := 0; s_stall := 0; s_sup := 0;
wait until rising_edge(clk);
wait for 1 ns;
end procedure;
variable dv, hv : std_logic_vector(N_EP-1 downto 0);
variable addr : std_logic_vector(6 downto 0);
variable ri : natural;
begin
-- PHASE 1 (DIRECTED, EXHAUSTIVE)
reset_dut;
for pi in 0 to 3 loop
for oi in 0 to 1 loop
for ei in 0 to 3 loop
for ai in 0 to 1 loop
for hi in 0 to 1 loop
dv := (others => '0');
hv := (others => '0');
if ai = 1 then dv(ei) := '1'; end if;
if hi = 1 then hv(ei) := '1'; end if;
if oi = 1 then addr := DEV_ADDR; else addr := "1010101"; end if;
step('1', std_logic_vector(to_unsigned(pi, 2)), addr,
std_logic_vector(to_unsigned(ei, 2)), dv, hv);
step('0', TOK_OUT, "0000000", "00", dv, hv);
step('0', TOK_OUT, "0000000", "00", dv, hv);
ri := pi*32 + oi*16 + ei*4 + ai*2 + hi;
reach(ri) := '1';
end loop;
end loop;
end loop;
end loop;
end loop;
-- PHASE 2 (DIRECTED) -- the device wants to talk and may not
reset_dut;
for k in 0 to 15 loop
dv := std_logic_vector(to_unsigned(k, N_EP));
step('0', TOK_OUT, "0000000", "00", dv, (others => '0'));
step('0', TOK_OUT, "0000000", "00", dv, (others => '0'));
step('0', TOK_IN, "0000000", "00", dv, (others => '0'));
end loop;
-- PHASE 2b (DIRECTED) -- NAK is a loop, not an answer
reset_dut;
for ei in 0 to 3 loop
for k in 0 to 5 loop
step('1', TOK_IN, DEV_ADDR, std_logic_vector(to_unsigned(ei, 2)),
"0000", "0000");
step('0', TOK_OUT, "0000000", "00", "0000", "0000");
end loop;
dv := (others => '0');
dv(ei) := '1';
step('1', TOK_IN, DEV_ADDR, std_logic_vector(to_unsigned(ei, 2)),
dv, "0000");
step('0', TOK_OUT, "0000000", "00", dv, "0000");
for k in 0 to 2 loop
step('1', TOK_IN, DEV_ADDR, std_logic_vector(to_unsigned(ei, 2)),
"0000", "0000");
step('0', TOK_OUT, "0000000", "00", "0000", "0000");
end loop;
end loop;
-- PHASE 3 (DIRECTED) -- a superseded token is never answered
reset_dut;
for ei in 0 to 3 loop
for k in 0 to 3 loop
for oi in 0 to 1 loop
step('1', TOK_IN, DEV_ADDR, std_logic_vector(to_unsigned(ei, 2)),
"1111", "0000");
if oi = 1 then addr := DEV_ADDR; else addr := "1010101"; end if;
step('1', TOK_IN, addr, std_logic_vector(to_unsigned(k, 2)),
"1111", "0000");
step('0', TOK_OUT, "0000000", "00", "1111", "0000");
step('0', TOK_OUT, "0000000", "00", "1111", "0000");
end loop;
end loop;
end loop;
-- PHASE 4 (RANDOM)
if not DIRECTED_ONLY then
reset_dut;
for k in 0 to 39999 loop
if (nxt mod 100) < 70 then
if (nxt mod 4) = 0 then addr := DEV_ADDR;
else addr := std_logic_vector(to_unsigned((nxt mod 127) + 1, 7));
end if;
dv := std_logic_vector(to_unsigned(nxt mod 16, N_EP));
if (nxt mod 8) = 0 then
hv := std_logic_vector(to_unsigned(nxt mod 16, N_EP));
else
hv := (others => '0');
end if;
step('1', std_logic_vector(to_unsigned(nxt mod 4, 2)), addr,
std_logic_vector(to_unsigned(nxt mod 4, 2)), dv, hv);
else
dv := std_logic_vector(to_unsigned(nxt mod 16, N_EP));
step('0', TOK_OUT, "0000000", "00", dv, "0000");
end if;
end loop;
end if;
n_reach := 0;
for i in 0 to 127 loop
if reach(i) = '1' then n_reach := n_reach + 1; end if;
end loop;
write(ln, string'("steps=") & integer'image(steps)
& string'(" checks=") & integer'image(checks)
& string'(" reach=") & integer'image(n_reach) & string'("/128")
& string'(" errors=") & integer'image(errors));
writeline(output, ln);
write(ln, string'("[bus] responses=") & integer'image(s_resp)
& string'(" ignored=") & integer'image(s_ign)
& string'(" nak=") & integer'image(s_nak)
& string'(" stall=") & integer'image(s_stall)
& string'(" superseded=") & integer'image(s_sup));
writeline(output, ln);
write(ln, string'("[the whole point] unsolicited transmissions = ")
& integer'image(unsol));
writeline(output, ln);
if n_reach /= 128 then
write(ln, string'("FAIL: exhaustive sweep incomplete"));
writeline(output, ln);
errors := errors + 1;
end if;
if errors = 0 then
write(ln, string'("PASS: 0 errors in ") & integer'image(checks)
& string'(" checks"));
else
write(ln, string'("FAIL: ") & integer'image(errors)
& string'(" errors in ") & integer'image(checks) & string'(" checks"));
end if;
writeline(output, ln);
done <= true;
wait;
end process;
end architecture;10. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| (PID × ours × endpoint × data × halted) reached | 128 / 128 | 128 / 128 | 128 / 128 |
| NAK-loop polls swept | 40 / 40 | 40 / 40 | 40 / 40 |
| supersede scenarios swept | 32 / 32 | 32 / 32 | 32 / 32 |
| Steps | 40640 | 40640 | 40640 |
| Checks executed | 331561 | 331561 | 330578 |
| responses issued | 6305 | 6305 | 5322 |
| tokens correctly ignored | 27781 | 27781 | 22688 |
| NAKs | 1053 | 1053 | 862 |
| STALLs | 221 | 221 | 156 |
| superseded tokens | 5343 | 5343 | 3715 |
| unsolicited transmissions | 0 | 0 | 0 |
| Result | PASS | PASS | PASS |
The exhaustive sweep is every combination of what the host asked and what the device wanted: 4 token types × addressed-to-us or not × 4 endpoints × data available or not × halted or not. All 128, each followed by the cycle in which an answer is due and one more in which there must be silence again.
11. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| A1 | the address check is dropped — the device answers for everybody | 280793 | 280793 | 273647 |
| A4 | a token for another device leaves our permission standing | 228331 | 228331 | 214051 |
| A2 | SOF is answered | 205094 | 205094 | 204305 |
| A3 | the device transmits when it has data — no token needed | 145085 | 145085 | 146685 |
| A6 | a halted endpoint answers normally instead of stalling | 79414 | 79414 | 79190 |
| A7 | SETUP is stalled when the endpoint is halted | 39060 | 39060 | 38145 |
| A5 | an empty endpoint sends DATA instead of NAK | 1093 | 1093 | 902 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages.
A3 is the mutation this chapter exists for — it is the ninety-second answer being false. Nine lines that let the device transmit because it felt like it. It dies 145,085 times, and every one of those is the bench noticing a byte on the bus that the host never asked for.
Directed against random
| # | All phases | Directed only | Random |
|---|---|---|---|
| A1 | 280793 | 2454 | 278339 |
| A2 | 205094 | 858 | 204236 |
| A3 | 145085 | 1847 | 143238 |
| A4 | 228331 | 284 | 228047 |
| A5 | 1093 | 40 | 1053 |
| A6 | 79414 | 480 | 78934 |
| A7 | 39060 | 52 | 39008 |
Every one is killed by directed stimulus alone. The random phase dominates the totals — it is 40,000 steps against roughly 640 directed ones — but no mutation depends on it.
12. Two Findings the Mutations Produced
A5's directed score was 4, and 4 is luck
The first run of this matrix gave A5 a directed score of four. Four is not a result. It is the number of times a narrow situation happens to line up with a check, and a different seed would have given 2 or 7.
The cause was arithmetic rather than mysterious. For A5 to be observable the bench needs an IN token, addressed to us, on a non-halted endpoint with no data — and the exhaustive sweep contains exactly four such cells, one per endpoint. Each produced exactly one failing check, because A5 leaves the NAK counter correct and only the PID is wrong, so nothing diverges cumulatively.
A5 directed = 4 = the number of scenarios
x 1 check each
A score equal to the scenario count means every
scenario is checked ONCE. That is cornered, not killed.The cure was not more random cycles. It was to ask what property was actually being claimed and then drive it over its domain. The claim is not "an empty endpoint NAKs"; it is "NAK is neither sticky nor terminal" — an empty endpoint NAKs every poll, and switches to DATA the instant it has something. That takes a loop, not a single token:
six polls while empty, one poll with data, three more while empty — per endpoint, over all four. A5 went from 4 to 40, and the suite gained a property it genuinely did not have before.
A6's SystemVerilog column read 102 against 79414
The first three-language run put A6 at 79,414 in Verilog, 79,190 in VHDL, and 102 in SystemVerilog. A 780× spread in one language.
The rule for this shape is that an out-of-line column is usually the mutant, not the testbench — and it was again. The Verilog mutation disables the halted test in a block that controls both the response PID and the STALL counter:
Verilog A6: if (1'b0) begin <- was halted[ep]
tx_pid_r <= R_STALL;
stall_r <= stall_r + 1; <- ALSO disabled
end else if (data_avail) ...
SystemVerilog A6 (wrong): in_response(1'b0, avail)
stall_r logic UNTOUCHED
Same label. Different mutation. The SV version left the
counter correct, so only the PID check could ever fire.The SystemVerilog design had factored the halted test into a function and a separate counter statement, so mutating the function touched one of the two reads. Fixing it meant mutating the whole three-line region so that both reads are neutralised, exactly as the Verilog does. A6 became 79,414 in SystemVerilog too — identical to Verilog, as it should be, since Icarus gives both the same stimulus.
13. The Follow-Ups, and What They Are Testing
Answering section 2 well guarantees a follow-up. These are the five that actually come, in roughly the order they come:
"So how does a device signal that something happened?" It does not. It waits to be polled and then says so. Interrupt transfers are the host promising to ask at least every N frames. The name is the single most misleading word in the specification.
"What if the device isn't ready?" NAK — and NAK is not an error, it is flow control. The host retries in a later slot. The follow-up to the follow-up is "what if it NAKs forever?", and the answer is that nothing in the protocol stops it: the driver times out, which is why a device that NAKs permanently presents as a hang rather than as a failure.
"How does the host know a device is there?" It does not, until the hub reports a port status change — and the hub knows because of an electrical event, not a message. That is the only place in USB where information travels up the tree without being asked for, and it is deliberately outside the data protocol. Chapter 27.3 is this question.
"Can two devices talk at once?" No, and there is no mechanism to handle it if they do. The host's schedule makes it impossible; a hub that sees two talkers reports a collision rather than arbitrating, because arbitrating would destroy the evidence that the schedule was violated. Chapter 27.2 is this question.
"Why is isochronous unreliable?" Because a retry needs a slot, and the schedule already allocated every slot. Guaranteed bandwidth and guaranteed delivery are the same resource spent two different ways. Chapter 27.5 is this question.
14. UVM: Asserting the Property Instead of Describing It
// The defining property of USB is a NEGATIVE claim -- "the device never
// transmits unsolicited" -- and negative claims are where testbenches are
// weakest: an environment can run for a billion cycles without ever being
// in a position to observe one.
//
// So this monitor does two things that a data scoreboard does not:
// 1. it checks the negative claim on EVERY cycle, not on transactions;
// 2. it fails the run if the claim was never PUT AT RISK.
class tok_item extends uvm_sequence_item;
`uvm_object_utils(tok_item)
rand bit valid;
rand bit [1:0] pid; // 0=OUT 1=IN 2=SOF 3=SETUP
rand bit [6:0] addr;
rand bit [1:0] ep;
rand bit [3:0] data_avail;
rand bit [3:0] halted;
function new(string name = "tok_item"); super.new(name); endfunction
// A bus on which every token is ours is not a bus, it is a point-to-point
// link -- and it cannot distinguish this device from one with no address
// comparator at all. Most traffic must belong to somebody else.
constraint c_mostly_foreign { addr dist { 7'h2A := 25, [1:127] := 75 }; }
constraint c_busy { valid dist { 1 := 70, 0 := 30 }; }
endclass
class tok_gate_monitor extends uvm_component;
`uvm_component_utils(tok_gate_monitor)
virtual tg_if vif;
// ---- the negative claim ----
int unsigned n_unsolicited;
// ---- and the evidence that it was at risk ----
//
// Each of these counts a situation in which a BROKEN design would have
// transmitted. If they are all zero the run proved nothing, however many
// cycles it lasted, and report_phase says so as an error.
int unsigned n_wanted_no_token; // data ready, no token at all
int unsigned n_foreign_token; // a token for another device
int unsigned n_sof; // a broadcast frame marker
int unsigned n_superseded; // a token replaced before its answer
int unsigned n_resp, n_nak, n_stall;
bit p_acc;
bit [1:0] p_ep, p_pid;
function new(string name, uvm_component parent); super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
forever begin
@(posedge vif.clk);
if (!vif.rst_n) begin p_acc = 0; continue; end
// ---- PROPERTY 1, every cycle: no permission, no transmission ----
if (vif.tx_valid && !p_acc) begin
n_unsolicited++;
`uvm_error("USB/UNSOLICITED",
$sformatf("device drove tx_pid=%0d with no token in the previous cycle",
vif.tx_pid))
end
// ---- PROPERTY 2: addressed means it MUST answer ----
//
// The positive half, and it is just as important. A device that
// goes silent when polled is not safe, it is broken: the host
// waits out the turnaround timeout, retries, and eventually
// reports the endpoint as absent.
if (p_acc && !vif.tx_valid)
`uvm_error("USB/SILENT",
"device was addressed and did not answer: silence is not a legal response")
if (p_acc) begin
n_resp++;
if (vif.tx_pid == 3'd2) n_nak++;
if (vif.tx_pid == 3'd3) n_stall++;
end
// ---- evidence gathering: was the claim ever at risk? ----
if (!vif.tok_valid && |vif.data_avail) n_wanted_no_token++;
if (vif.tok_valid && vif.tok_addr != 7'h2A) n_foreign_token++;
if (vif.tok_valid && vif.tok_pid == 2'd2) n_sof++;
if (vif.tok_valid && p_acc) n_superseded++;
p_acc = vif.tok_valid && (vif.tok_addr == 7'h2A) && (vif.tok_pid != 2'd2);
if (p_acc) begin p_ep = vif.tok_ep; p_pid = vif.tok_pid; end
end
endtask
function void report_phase(uvm_phase phase);
super.report_phase(phase);
`uvm_info("USB",
$sformatf("%0d responses (%0d NAK, %0d STALL) | %0d unsolicited",
n_resp, n_nak, n_stall, n_unsolicited), UVM_LOW)
// A negative property needs its stimulus audited, not assumed. Each of
// these is a run in which the design was never given the OPPORTUNITY to
// break the rule -- and reporting PASS on such a run is the failure mode
// this whole block exists to prevent.
if (n_wanted_no_token == 0)
`uvm_error("USB/COV",
"the device never had data pending with no token on the bus: the central property was never at risk")
if (n_foreign_token == 0)
`uvm_error("USB/COV",
"every token in this run was addressed to us: the address check was never exercised")
if (n_sof == 0)
`uvm_error("USB/COV",
"no SOF was ever seen: the broadcast-token rule was never exercised")
if (n_superseded == 0)
`uvm_error("USB/COV",
"no token was ever superseded before its answer was due")
`uvm_info("USB/COV",
$sformatf("at-risk cycles: %0d wanted-no-token, %0d foreign, %0d SOF, %0d superseded",
n_wanted_no_token, n_foreign_token, n_sof, n_superseded), UVM_LOW)
endfunction
endclass15. Common Misconceptions
"USB devices can interrupt the host." No device on a USB bus can initiate anything. "Interrupt transfer" is a polling guarantee with an unfortunate name.
"NAK is an error." It is flow control, and a completely normal part of every bulk transfer. What is not normal is NAK forever — which the protocol permits and the driver has to time out.
"The device is idle, so the bus is idle." The bus is full of tokens for other devices. This design sits through four times more traffic than it answers.
"A full transmit buffer means the device will send." It means nothing at all until a token arrives. data_avail is high in every cycle of the first waveform and used in one.
"SOF is a token like any other." It is addressed to nobody. A device that answers it collides with every other device on the bus simultaneously.
"STALL means the transfer failed." It means the endpoint is halted and will stay halted until software clears it — which it does over the control endpoint, which is why SETUP can never be stalled.
"The host guarantees it will not supersede a token." It reorders and abandons constantly. Property 7 exists because the device must not answer a question the host has moved on from.
"A device with no address check only affects itself." It corrupts replies for every other device on the bus. A1 is the highest-scoring mutation here and the hardest to diagnose in the field.
"A low mutation score means a weak check." A5 scored 1093 and is a perfectly real defect. It scored 4 directed because the domain has four cells — and that was worth fixing.
16. Exercises
1. Give the ninety-second answer in your own words, then list five facts about USB and show each is a consequence of it. If any fact is not a consequence, your centre is in the wrong place.
2. A5's directed score equalled the number of scenarios in its domain. Derive that number from the sweep in section 10, and construct a second property of NAK that the loop phase still does not test.
3. Property 6 says SETUP is never stalled. Trace what happens to a device that stalls SETUP while halted, and show that it can never be recovered without a bus reset.
4. A1 makes a device answer for others. Design a bus-level monitor that identifies which device has the broken comparator, given only the traffic.
5. The bench counts n_unsolicited and asserts it is zero every cycle. Argue why asserting it once at the end would be insufficient, and give a design defect that only the per-cycle check catches.
6. Property 7 drops a superseded token. Argue the opposite — queue both and answer in order — and give the failure it causes on a real bus.
7. Extend the design to high speed, where a device may respond with NYET as well. Which of the seven properties change and which new one is needed?
17. Summary
| Idea | Why it matters |
|---|---|
| USB is host-scheduled | one sentence from which everything else follows |
| A device may never initiate | there is no device-to-host interrupt line |
| Interrupt transfers are polled | the name is the worst in the specification |
| NAK means ask me again | the only way a responder expresses not-ready |
| NAK is a loop, not an answer | one NAK and permanent NAK look identical |
| Frames exist because polling needs a budget | 1 ms full-speed, 125 µs microframes |
| Collisions are impossible, not arbitrated | one initiator |
| SOF is answered by nobody | a broadcast timestamp |
| STALL beats DATA | a halted endpoint is halted with a full buffer |
| SETUP is never stalled | it is how software clears a halt |
| A superseded token is never answered | the host reorders and abandons |
| Audit the stimulus for a negative property | a run that never risked it proves nothing |
| An out-of-line column is usually the mutant | A6 was 780× off in one language |
| A directed score equal to the scenario count | is cornered, not killed |
| 128 states, 7 mutations, 3 languages | 0 unsolicited transmissions in 331,561 checks |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o tg_v.out tg_v.v tg_v_tb.v && ./tg_v.out |
| SystemVerilog | iverilog -g2012 -o tg_sv.out tg_sv.sv tg_sv_tb.sv && ./tg_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a tg_vhdl.vhd tg_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_tg_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_tg_vhdl |
| One mutation | iverilog -g2005 -DMUT_A3 -o mm tg_v_mut.v tg_v_tb.v && ./mm |
| Directed only (Verilog) | iverilog -g2005 -DDIRECTED_ONLY -o mm tg_v_mut.v tg_v_tb.v && ./mm |
| Directed only (VHDL) | nvc --std=2008 -e -gDIRECTED_ONLY=true tb_tg_vhdl |
All three implementations pass with 0 errors: all 128 combinations of token type, addressing, endpoint, data availability and halt state; 6305 responses issued against 27,781 tokens correctly ignored; zero unsolicited transmissions in 331,561 checks; and all seven mutations killed by directed stimulus alone.
Chapter 27.2 — Host / Device / Hub Identification is the follow-up you have just invited: if only the host may initiate, what exactly is a hub? Almost every candidate answers "a switch", and a hub is not a switch — it is a repeater with one upstream port, and the difference is a design in which no downstream port can reach another one at all.
Continue learning
Related tutorials
- 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
Downstream Device Discovery
A hub cannot interrupt the host, so every port event waits to be asked for — and the window between the poll and the acknowledgement is where devices are silently lost.
- Related topic
Endpoint Logic
A lost ACK and a lost data packet look identical to the host, so it resends the same bytes — and the data toggle is the only thing that tells a device a retransmission from new data.
- Related topic
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.
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.
