USB · Module 22
Host Resource Management
Software cannot edit a context hardware owns, so xHCI has two of them — and inside a Configure Endpoint command the Drop flags are applied before the Add flags, making drop+add an atomic re-initialisation.
22.2 solved ring ownership with one bit. This chapter is the same problem one level up: who owns the description of an endpoint, and how it changes hands.
1. Software Does Not Own This State
Every endpoint on every device has a context: a structure in host memory holding its type, its max packet size, its ring pointer and its current state.
The controller reads it — and writes it. The state field in particular is hardware's: updated as transfers complete, as endpoints stall, as errors halt them.
So software cannot simply edit it. A context hardware owns may be written by hardware at any moment, and a read-modify-write from software would silently discard whatever hardware wrote in between. There is no lock, no compare-and-swap, and no way to make one across a PCIe link.
2. Add and Drop Flags
The Input Context begins with a control section holding two 32-bit masks:
Drop Context flags D31 .. D1 D0 endpoints to TEAR DOWN
Add Context flags A31 .. A1 A0 endpoints to SET UP
bit 1 = endpoint 0 (the control endpoint)
bit 2 = the next endpoint, and so on
bit 0 = the SLOT context, not an endpointAnd the rule that makes the pair useful:
DROP FLAGS ARE APPLIED BEFORE ADD FLAGS.Setting both bits for the same endpoint in one command is therefore not a contradiction. It is a re-initialisation: tear the endpoint down and build it again, atomically, in one command, with no window in which the endpoint does not exist.
Software uses it to change an endpoint's max packet size or its ring pointer — things that cannot be edited in place, because hardware owns the context — without a teardown that another thread could observe.
3. D0 Is Not a Legal Drop
Bit 0 of the Drop mask would mean "drop the slot context" — and a Configure Endpoint command may not do that. The slot is torn down by Disable Slot, not by a configuration change.
So D0 is ignored. A controller that honours it destroys the device's addressing during a routine reconfiguration: the slot context holds the device address, the route string and the hub topology, and without it the device is unreachable until it is re-enumerated.
The design reports it as illegal_drop rather than merely ignoring it, because a driver setting D0 has a bug, and a bug that is silently absorbed is a bug that ships.
4. A Doorbell Is a Hint, Not a Command
Ringing an endpoint's doorbell means "there may be new work on your ring." It is not an instruction to start.
A doorbell for an endpoint that is DISABLED, or HALTED and not yet reset, is perfectly legal and does nothing. Software is allowed to be optimistic — it rings after enqueueing, without first reading back a state that hardware owns — and hardware re-checks rather than assuming.
Four states, and the fact that three of them are 'not transferring'
A controller that treats the doorbell as a command starts a halted endpoint without the Reset Endpoint that was supposed to clear the error — and the first thing it does is fail again, in exactly the same way, for ever. Mutation P3.
5. What We Are Building
xhci_ep_context -- ONE endpoint's context
commands state
-------- -----
cfg_valid state DISABLED / STOPPED /
my_drop (this context's D) RUNNING / HALTED
my_add (this context's A) usable
is_slot_ctx (index 0) reinitialised dropped AND added
illegal_drop D0 was set
db_valid / db_is_mine db_ignored a hint that did nothing
ep_error
reset_ep
n_configured / n_disabled / n_reinit
n_db_ignored / n_illegal_drop / n_haltsThe priority chain is itself the specification:
| Priority | Input | Why it outranks what follows |
|---|---|---|
| 1 | cfg_valid | a command says whether the endpoint exists; a doorbell is a hint about its ring |
| 2 | ep_error | the endpoint has already failed; a doorbell cannot undo that |
| 3 | reset_ep | clears a halt, and only a halt |
| 4 | db_valid | the hint, applied last and only if nothing else happened |
6. Verilog-2005 Implementation
// xhci_ep_context -- the per-endpoint context an xHCI host controller owns,
// and the ordering rule inside a Configure Endpoint command that makes
// "drop it and add it" mean something rather than nothing.
//
// SOFTWARE DOES NOT OWN THIS STATE
//
// Every endpoint on every device has a CONTEXT: a structure in host memory
// holding its type, its packet size, its ring pointer and its current state.
// The controller reads and WRITES it -- the state field in particular is
// hardware's, updated as transfers complete, stall, or error out.
//
// So software cannot simply edit it. A context hardware owns may be written
// by hardware at any moment, and a read-modify-write from software would
// silently discard whatever hardware wrote in between.
//
// xHCI solves this by having TWO of them. The OUTPUT context is hardware's
// and software only reads it. The INPUT context is software's scratch copy,
// and software hands it over with a command:
//
// software fills an INPUT context
// software issues Configure Endpoint, pointing at it
// hardware reads it, applies it, and updates the OUTPUT context
//
// No shared mutable structure, no read-modify-write race, and one clearly
// identified moment of transfer. That is the same shape as the cycle bit in
// chapter 22.2: ownership expressed structurally rather than by convention.
//
// THE INPUT CONTROL CONTEXT: ADD AND DROP FLAGS
//
// The Input Context begins with a control section holding two 32-bit masks:
//
// Drop Context flags D31..D0 endpoints to TEAR DOWN
// Add Context flags A31..A0 endpoints to SET UP
//
// Bit 1 is endpoint 0, bit 2 the next, and so on. Bit 0 refers to the SLOT
// context rather than to an endpoint.
//
// And the rule that makes the pair useful:
//
// DROP FLAGS ARE APPLIED BEFORE ADD FLAGS.
//
// Setting both bits for the same endpoint in one command is therefore not a
// contradiction. It is a RE-INITIALISATION: tear the endpoint down and build
// it again, atomically, in one command, with no window in which the endpoint
// does not exist. Software uses it to change an endpoint's packet size or
// ring pointer without a teardown that another thread could observe.
//
// Apply Add first and the same command means the exact opposite -- set it up,
// then tear it down -- and the endpoint ends up DISABLED. Same bits, same
// command, and the device stops working.
//
// D0 IS NOT A LEGAL DROP
//
// Bit 0 of the DROP mask would mean "drop the slot context", and a Configure
// Endpoint command may not do that -- the slot is torn down by Disable Slot,
// not by a configuration change. So D0 is ignored, and a controller that
// honours it destroys the device's addressing on a routine reconfiguration.
//
// A DOORBELL IS A HINT, NOT A COMMAND
//
// Ringing an endpoint's doorbell means "there may be new work on your ring".
// It is not an instruction to start. A doorbell for an endpoint that is
// DISABLED, or HALTED and not yet reset, is perfectly legal and does
// NOTHING -- software is allowed to be optimistic, and hardware re-checks
// rather than assuming.
//
// A controller that treats the doorbell as a command starts a halted
// endpoint without the reset that was supposed to clear the error, and the
// first thing it does is fail again in the same way.
module xhci_ep_context (
input wire clk,
input wire rst_n,
input wire cfg_valid, // a Configure Endpoint command is executing
input wire my_drop, // ...and its Drop mask names me
input wire my_add, // ...and its Add mask names me
input wire is_slot_ctx, // this context is index 0: the SLOT
input wire db_valid, // a doorbell was rung
input wire db_is_mine, // ...for this endpoint
input wire ep_error, // a transfer errored: halt
input wire reset_ep, // a Reset Endpoint command
output wire [2:0] state,
output wire usable, // transfers may run
output wire reinitialised,// dropped AND added in one command
output wire illegal_drop, // D0: the slot context cannot be dropped
output wire db_ignored, // a doorbell that correctly did nothing
output reg [31:0] n_configured,
output reg [31:0] n_disabled,
output reg [31:0] n_reinit,
output reg [31:0] n_db_ignored,
output reg [31:0] n_illegal_drop,
output reg [31:0] n_halts
);
localparam [2:0] EP_DISABLED = 3'd0, // not configured; nothing may run
EP_STOPPED = 3'd1, // configured, idle, ready to start
EP_RUNNING = 3'd2, // transfers in progress
EP_HALTED = 3'd3; // errored; needs a Reset Endpoint
reg [2:0] st_r;
assign state = st_r;
assign usable = (st_r == EP_RUNNING);
// ---- The Configure Endpoint decode ----
//
// D0 would mean "drop the slot context", which a Configure Endpoint
// command may not do. It is ignored, and reported so that a driver bug is
// visible rather than silently destroying the device's addressing.
assign illegal_drop = cfg_valid && is_slot_ctx && my_drop;
wire drop_applies = cfg_valid && my_drop && !is_slot_ctx;
wire add_applies = cfg_valid && my_add;
// THE ordering. Both flags set is a RE-INITIALISATION, not a
// contradiction: tear down, then build again, atomically.
assign reinitialised = drop_applies && add_applies;
// A doorbell that correctly did nothing. Legal, expected, and worth
// counting -- a high ignored-doorbell count is software ringing for
// endpoints it has not configured.
wire db_for_me = db_valid && db_is_mine;
assign db_ignored = db_for_me && (st_r != EP_STOPPED);
// ---- Next state, as ONE priority chain ----
//
// A command outranks a doorbell: the doorbell is a hint about a ring, and
// the command is a statement about whether the endpoint exists at all.
// An error outranks a doorbell for the same reason.
reg [2:0] st_n;
always @* begin
st_n = st_r;
if (cfg_valid) begin
// Drop first, then Add -- and because Add is second, setting both
// leaves the endpoint STOPPED rather than DISABLED.
if (drop_applies) st_n = EP_DISABLED;
if (add_applies) st_n = EP_STOPPED;
end else if (ep_error) begin
// Only a configured endpoint can error. A disabled one is not running
// anything that could fail.
if (st_r != EP_DISABLED) st_n = EP_HALTED;
end else if (reset_ep) begin
// Reset Endpoint clears a halt and ONLY a halt. It is not a general
// "put this endpoint back to idle" command.
if (st_r == EP_HALTED) st_n = EP_STOPPED;
end else if (db_for_me) begin
// A HINT. It starts an endpoint that is ready to start, and does
// nothing at all otherwise.
if (st_r == EP_STOPPED) st_n = EP_RUNNING;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_r <= EP_DISABLED;
n_configured <= 32'd0;
n_disabled <= 32'd0;
n_reinit <= 32'd0;
n_db_ignored <= 32'd0;
n_illegal_drop <= 32'd0;
n_halts <= 32'd0;
end else begin
st_r <= st_n;
if ((st_n != EP_DISABLED) && (st_r == EP_DISABLED))
n_configured <= n_configured + 32'd1;
if ((st_n == EP_DISABLED) && (st_r != EP_DISABLED))
n_disabled <= n_disabled + 32'd1;
if (reinitialised) n_reinit <= n_reinit + 32'd1;
if (db_ignored) n_db_ignored <= n_db_ignored + 32'd1;
if (illegal_drop) n_illegal_drop <= n_illegal_drop + 32'd1;
if ((st_n == EP_HALTED) && (st_r != EP_HALTED))
n_halts <= n_halts + 32'd1;
end
end
endmoduleNote that the drop/add ordering is written as two successive assignments inside one always block — if (drop_applies) st_n = EP_DISABLED; if (add_applies) st_n = EP_STOPPED; — which is exactly the pattern Chapter 21.1 warned against for mutually exclusive updates.
It is correct here, and the difference is worth naming: these updates are not mutually exclusive, and the later one winning is the specification. "Drop, then add" is literally what the command means, and writing it as two ordered assignments is the clearest possible statement of it. The danger in 21.1 was that two assignments looked independent while the order silently decided the answer; here the order is the answer, and it is the first thing the comment says.
7. SystemVerilog Implementation
// xhci_ep_context -- the per-endpoint context an xHCI host controller owns,
// and the ordering rule inside a Configure Endpoint command that makes
// "drop it and add it" mean something rather than nothing.
//
// SOFTWARE DOES NOT OWN THIS STATE
//
// Every endpoint on every device has a CONTEXT: a structure in host memory
// holding its type, its packet size, its ring pointer and its current state.
// The controller reads and WRITES it -- the state field in particular is
// hardware's, updated as transfers complete, stall, or error out.
//
// So software cannot simply edit it. A context hardware owns may be written
// by hardware at any moment, and a read-modify-write from software would
// silently discard whatever hardware wrote in between.
//
// xHCI solves this by having TWO of them. The OUTPUT context is hardware's
// and software only reads it. The INPUT context is software's scratch copy,
// and software hands it over with a command:
//
// software fills an INPUT context
// software issues Configure Endpoint, pointing at it
// hardware reads it, applies it, and updates the OUTPUT context
//
// No shared mutable structure, no read-modify-write race, and one clearly
// identified moment of transfer. That is the same shape as the cycle bit in
// chapter 22.2: ownership expressed structurally rather than by convention.
//
// THE INPUT CONTROL CONTEXT: ADD AND DROP FLAGS
//
// The Input Context begins with a control section holding two 32-bit masks:
//
// Drop Context flags D31..D0 endpoints to TEAR DOWN
// Add Context flags A31..A0 endpoints to SET UP
//
// Bit 1 is endpoint 0, bit 2 the next, and so on. Bit 0 refers to the SLOT
// context rather than to an endpoint.
//
// And the rule that makes the pair useful:
//
// DROP FLAGS ARE APPLIED BEFORE ADD FLAGS.
//
// Setting both bits for the same endpoint in one command is therefore not a
// contradiction. It is a RE-INITIALISATION: tear the endpoint down and build
// it again, atomically, in one command, with no window in which the endpoint
// does not exist. Software uses it to change an endpoint's packet size or
// ring pointer without a teardown that another thread could observe.
//
// Apply Add first and the same command means the exact opposite -- set it up,
// then tear it down -- and the endpoint ends up DISABLED. Same bits, same
// command, and the device stops working.
//
// D0 IS NOT A LEGAL DROP
//
// Bit 0 of the DROP mask would mean "drop the slot context", and a Configure
// Endpoint command may not do that -- the slot is torn down by Disable Slot,
// not by a configuration change. So D0 is ignored, and a controller that
// honours it destroys the device's addressing on a routine reconfiguration.
//
// A DOORBELL IS A HINT, NOT A COMMAND
//
// Ringing an endpoint's doorbell means "there may be new work on your ring".
// It is not an instruction to start. A doorbell for an endpoint that is
// DISABLED, or HALTED and not yet reset, is perfectly legal and does
// NOTHING -- software is allowed to be optimistic, and hardware re-checks
// rather than assuming.
//
// A controller that treats the doorbell as a command starts a halted
// endpoint without the reset that was supposed to clear the error, and the
// first thing it does is fail again in the same way.
package xhci_ctx_pkg;
// The endpoint states an xHCI context can hold. They are an enumeration
// rather than an encoding because THREE of them are "not transferring" and
// they need completely different actions from software: DISABLED needs a
// Configure Endpoint, STOPPED needs a doorbell, HALTED needs a Reset
// Endpoint, and ringing the wrong one of those does nothing at all.
typedef enum logic [2:0] {
EP_DISABLED = 3'd0, // not configured; nothing may run
EP_STOPPED = 3'd1, // configured, idle, ready to start
EP_RUNNING = 3'd2, // transfers in progress
EP_HALTED = 3'd3 // errored; needs a Reset Endpoint
} ep_state_e;
endpackage
module xhci_ep_context
import xhci_ctx_pkg::*;
(
input logic clk,
input logic rst_n,
input logic cfg_valid, // a Configure Endpoint command executing
input logic my_drop, // ...and its Drop mask names me
input logic my_add, // ...and its Add mask names me
input logic is_slot_ctx, // this context is index 0: the SLOT
input logic db_valid, // a doorbell was rung
input logic db_is_mine, // ...for this endpoint
input logic ep_error, // a transfer errored: halt
input logic reset_ep, // a Reset Endpoint command
output ep_state_e state,
output logic usable, // transfers may run
output logic reinitialised, // dropped AND added in one command
output logic illegal_drop, // D0: the slot cannot be dropped
output logic db_ignored, // a doorbell that correctly did nothing
output logic [31:0] n_configured,
output logic [31:0] n_disabled,
output logic [31:0] n_reinit,
output logic [31:0] n_db_ignored,
output logic [31:0] n_illegal_drop,
output logic [31:0] n_halts
);
ep_state_e st_r;
assign state = st_r;
assign usable = (st_r == EP_RUNNING);
// ---- The Configure Endpoint decode ----
//
// D0 would mean "drop the slot context", which a Configure Endpoint
// command may not do. It is ignored, and reported so that a driver bug is
// visible rather than silently destroying the device's addressing.
assign illegal_drop = cfg_valid && is_slot_ctx && my_drop;
logic drop_applies, add_applies;
assign drop_applies = cfg_valid && my_drop && !is_slot_ctx;
assign add_applies = cfg_valid && my_add;
// THE ordering. Both flags set is a RE-INITIALISATION, not a
// contradiction: tear down, then build again, atomically.
assign reinitialised = drop_applies && add_applies;
// A doorbell that correctly did nothing. Legal, expected, and worth
// counting -- a high ignored-doorbell count is software ringing for
// endpoints it has not configured.
logic db_for_me;
assign db_for_me = db_valid && db_is_mine;
assign db_ignored = db_for_me && (st_r != EP_STOPPED);
// ---- Next state, as ONE priority chain ----
//
// A command outranks a doorbell: the doorbell is a hint about a ring, and
// the command is a statement about whether the endpoint exists at all.
// An error outranks a doorbell for the same reason.
ep_state_e st_n;
always_comb begin
st_n = st_r;
if (cfg_valid) begin
// Drop first, then Add -- and because Add is second, setting both
// leaves the endpoint STOPPED rather than DISABLED.
if (drop_applies) st_n = EP_DISABLED;
if (add_applies) st_n = EP_STOPPED;
end else if (ep_error) begin
// Only a configured endpoint can error. A disabled one is not running
// anything that could fail.
if (st_r != EP_DISABLED) st_n = EP_HALTED;
end else if (reset_ep) begin
// Reset Endpoint clears a halt and ONLY a halt. It is not a general
// "put this endpoint back to idle" command.
if (st_r == EP_HALTED) st_n = EP_STOPPED;
end else if (db_for_me) begin
// A HINT. It starts an endpoint that is ready to start, and does
// nothing at all otherwise.
if (st_r == EP_STOPPED) st_n = EP_RUNNING;
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_r <= EP_DISABLED;
n_configured <= '0;
n_disabled <= '0;
n_reinit <= '0;
n_db_ignored <= '0;
n_illegal_drop <= '0;
n_halts <= '0;
end else begin
st_r <= st_n;
if ((st_n != EP_DISABLED) && (st_r == EP_DISABLED))
n_configured <= n_configured + 1;
if ((st_n == EP_DISABLED) && (st_r != EP_DISABLED))
n_disabled <= n_disabled + 1;
if (reinitialised) n_reinit <= n_reinit + 1;
if (db_ignored) n_db_ignored <= n_db_ignored + 1;
if (illegal_drop) n_illegal_drop <= n_illegal_drop + 1;
if ((st_n == EP_HALTED) && (st_r != EP_HALTED))
n_halts <= n_halts + 1;
end
end
endmodule8. VHDL-2008 Implementation
-- xhci_ep_context -- the per-endpoint context an xHCI host controller owns,
-- and the ordering rule inside a Configure Endpoint command that makes
-- "drop it and add it" mean something rather than nothing.
--
-- SOFTWARE DOES NOT OWN THIS STATE
--
-- Every endpoint on every device has a CONTEXT: a structure in host memory
-- holding its type, its packet size, its ring pointer and its current state.
-- The controller reads and WRITES it -- the state field in particular is
-- hardware's, updated as transfers complete, stall, or error out.
--
-- So software cannot simply edit it. A context hardware owns may be written
-- by hardware at any moment, and a read-modify-write from software would
-- silently discard whatever hardware wrote in between.
--
-- xHCI solves this by having TWO of them. The OUTPUT context is hardware's
-- and software only reads it. The INPUT context is software's scratch copy,
-- handed over with a command:
--
-- software fills an INPUT context
-- software issues Configure Endpoint, pointing at it
-- hardware reads it, applies it, and updates the OUTPUT context
--
-- No shared mutable structure, no read-modify-write race, and one clearly
-- identified moment of transfer. That is the same shape as the cycle bit in
-- chapter 22.2: ownership expressed structurally rather than by convention.
--
-- THE INPUT CONTROL CONTEXT: ADD AND DROP FLAGS
--
-- The Input Context begins with a control section holding two 32-bit masks:
--
-- Drop Context flags D31..D0 endpoints to TEAR DOWN
-- Add Context flags A31..A0 endpoints to SET UP
--
-- Bit 1 is endpoint 0, bit 2 the next, and so on. Bit 0 refers to the SLOT
-- context rather than to an endpoint.
--
-- And the rule that makes the pair useful:
--
-- DROP FLAGS ARE APPLIED BEFORE ADD FLAGS.
--
-- Setting both bits for the same endpoint in one command is therefore not a
-- contradiction. It is a RE-INITIALISATION: tear the endpoint down and build
-- it again, atomically, in one command, with no window in which the endpoint
-- does not exist. Software uses it to change an endpoint's packet size or
-- ring pointer without a teardown another thread could observe.
--
-- Apply Add first and the same command means the exact opposite -- set it up,
-- then tear it down -- and the endpoint ends up DISABLED.
--
-- D0 IS NOT A LEGAL DROP
--
-- Bit 0 of the DROP mask would mean "drop the slot context", and a Configure
-- Endpoint command may not do that -- the slot is torn down by Disable Slot,
-- not by a configuration change. So D0 is ignored, and a controller that
-- honours it destroys the device's addressing on a routine reconfiguration.
--
-- A DOORBELL IS A HINT, NOT A COMMAND
--
-- Ringing an endpoint's doorbell means "there may be new work on your ring".
-- It is not an instruction to start. A doorbell for an endpoint that is
-- DISABLED, or HALTED and not yet reset, is perfectly legal and does
-- NOTHING -- software is allowed to be optimistic, and hardware re-checks
-- rather than assuming.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package xhci_ctx_pkg is
-- The endpoint states an xHCI context can hold. An enumeration rather than
-- an encoding because THREE of them are "not transferring" and they need
-- completely different actions from software: EP_DISABLED needs a
-- Configure Endpoint, EP_STOPPED needs a doorbell, EP_HALTED needs a Reset
-- Endpoint, and doing the wrong one of those does nothing at all.
type ep_state_t is (
EP_DISABLED, -- not configured; nothing may run
EP_STOPPED, -- configured, idle, ready to start
EP_RUNNING, -- transfers in progress
EP_HALTED -- errored; needs a Reset Endpoint
);
function st_code(s : ep_state_t) return std_logic_vector;
end package;
package body xhci_ctx_pkg is
function st_code(s : ep_state_t) return std_logic_vector is
begin
return std_logic_vector(to_unsigned(ep_state_t'pos(s), 3));
end function;
end package body;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.xhci_ctx_pkg.all;
entity xhci_ep_context is
port (
clk : in std_logic;
rst_n : in std_logic;
cfg_valid : in std_logic; -- a Configure Endpoint is executing
my_drop : in std_logic; -- ...and its Drop mask names me
my_add : in std_logic; -- ...and its Add mask names me
is_slot_ctx : in std_logic; -- this context is index 0: the SLOT
db_valid : in std_logic; -- a doorbell was rung
db_is_mine : in std_logic; -- ...for this endpoint
ep_error : in std_logic; -- a transfer errored: halt
reset_ep : in std_logic; -- a Reset Endpoint command
state : out std_logic_vector(2 downto 0);
usable : out std_logic; -- transfers may run
reinitialised : out std_logic; -- dropped AND added in one command
illegal_drop : out std_logic; -- D0: the slot cannot be dropped
db_ignored : out std_logic; -- a doorbell that correctly did nothing
n_configured : out std_logic_vector(31 downto 0);
n_disabled : out std_logic_vector(31 downto 0);
n_reinit : out std_logic_vector(31 downto 0);
n_db_ignored : out std_logic_vector(31 downto 0);
n_illegal_drop : out std_logic_vector(31 downto 0);
n_halts : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of xhci_ep_context is
signal st_r, st_n : ep_state_t := EP_DISABLED;
signal drop_applies, add_applies, db_for_me : std_logic;
signal reinit_s, illegal_s, dbig_s : std_logic;
signal cfg_c, dis_c, re_c, dbi_c, ill_c, hal_c : unsigned(31 downto 0)
:= (others => '0');
begin
state <= st_code(st_r);
usable <= '1' when st_r = EP_RUNNING else '0';
-- ---- The Configure Endpoint decode ----
--
-- D0 would mean "drop the slot context", which a Configure Endpoint
-- command may not do. It is ignored, and reported so that a driver bug is
-- visible rather than silently destroying the device's addressing.
illegal_s <= '1' when (cfg_valid = '1' and is_slot_ctx = '1'
and my_drop = '1') else '0';
drop_applies <= '1' when (cfg_valid = '1' and my_drop = '1'
and is_slot_ctx = '0') else '0';
add_applies <= cfg_valid and my_add;
-- THE ordering. Both flags set is a RE-INITIALISATION, not a
-- contradiction: tear down, then build again, atomically.
reinit_s <= drop_applies and add_applies;
-- A doorbell that correctly did nothing. Legal, expected, and worth
-- counting -- a high ignored-doorbell count is software ringing for
-- endpoints it has not configured.
db_for_me <= db_valid and db_is_mine;
dbig_s <= '1' when (db_for_me = '1' and st_r /= EP_STOPPED) else '0';
reinitialised <= reinit_s;
illegal_drop <= illegal_s;
db_ignored <= dbig_s;
-- ---- Next state, as ONE priority chain ----
--
-- A command outranks a doorbell: the doorbell is a hint about a ring, and
-- the command is a statement about whether the endpoint exists at all.
-- An error outranks a doorbell for the same reason.
nextstate : process (st_r, cfg_valid, drop_applies, add_applies,
ep_error, reset_ep, db_for_me)
begin
st_n <= st_r;
if cfg_valid = '1' then
-- Drop first, then Add -- and because Add is second, setting both
-- leaves the endpoint STOPPED rather than DISABLED.
if drop_applies = '1' then
st_n <= EP_DISABLED;
end if;
if add_applies = '1' then
st_n <= EP_STOPPED;
end if;
elsif ep_error = '1' then
-- Only a configured endpoint can error. A disabled one is not running
-- anything that could fail.
if st_r /= EP_DISABLED then
st_n <= EP_HALTED;
end if;
elsif reset_ep = '1' then
-- Reset Endpoint clears a halt and ONLY a halt. It is not a general
-- "put this endpoint back to idle" command.
if st_r = EP_HALTED then
st_n <= EP_STOPPED;
end if;
elsif db_for_me = '1' then
-- A HINT. It starts an endpoint that is ready to start, and does
-- nothing at all otherwise.
if st_r = EP_STOPPED then
st_n <= EP_RUNNING;
end if;
end if;
end process;
regs : process (clk, rst_n)
begin
if rst_n = '0' then
st_r <= EP_DISABLED;
cfg_c <= (others => '0');
dis_c <= (others => '0');
re_c <= (others => '0');
dbi_c <= (others => '0');
ill_c <= (others => '0');
hal_c <= (others => '0');
elsif rising_edge(clk) then
st_r <= st_n;
if st_n /= EP_DISABLED and st_r = EP_DISABLED then
cfg_c <= cfg_c + 1;
end if;
if st_n = EP_DISABLED and st_r /= EP_DISABLED then
dis_c <= dis_c + 1;
end if;
if reinit_s = '1' then
re_c <= re_c + 1;
end if;
if dbig_s = '1' then
dbi_c <= dbi_c + 1;
end if;
if illegal_s = '1' then
ill_c <= ill_c + 1;
end if;
if st_n = EP_HALTED and st_r /= EP_HALTED then
hal_c <= hal_c + 1;
end if;
end if;
end process;
n_configured <= std_logic_vector(cfg_c);
n_disabled <= std_logic_vector(dis_c);
n_reinit <= std_logic_vector(re_c);
n_db_ignored <= std_logic_vector(dbi_c);
n_illegal_drop <= std_logic_vector(ill_c);
n_halts <= std_logic_vector(hal_c);
end architecture;9. Seeing the Re-initialisation
Configure, start, error, an ignored doorbell, reset — and an atomic re-initialisation
xhci_ep_context — the life of an endpoint, and drop+add
10 cycles10. The Testbenches
Each suite sweeps every state against every combination of the eight control inputs:
4 endpoint states
x cfg_valid x my_drop x my_add x is_slot_ctx
x db_valid x db_is_mine x ep_error x reset_ep
= 4 x 256 = 1024 one-step transitionsEvery starting state is reached through legal transitions only — a Configure Endpoint to leave DISABLED, a doorbell to start, an error to halt — and the reference model states the drop/add outcome as four explicit cases where the design applies two successive assignments, so the model cannot share an ordering bug with the design.
10.1 Verilog testbench
`timescale 1ns/1ps
module tb_ec_v;
reg clk=0, rst_n=0;
reg cfg_valid=0, my_drop=0, my_add=0, is_slot_ctx=0;
reg db_valid=0, db_is_mine=0, ep_error=0, reset_ep=0;
wire [2:0] state;
wire usable, reinitialised, illegal_drop, db_ignored;
wire [31:0] n_configured, n_disabled, n_reinit, n_db_ignored;
wire [31:0] n_illegal_drop, n_halts;
always #5 clk=~clk;
xhci_ep_context dut (
.clk(clk), .rst_n(rst_n), .cfg_valid(cfg_valid), .my_drop(my_drop),
.my_add(my_add), .is_slot_ctx(is_slot_ctx), .db_valid(db_valid),
.db_is_mine(db_is_mine), .ep_error(ep_error), .reset_ep(reset_ep),
.state(state), .usable(usable), .reinitialised(reinitialised),
.illegal_drop(illegal_drop), .db_ignored(db_ignored),
.n_configured(n_configured), .n_disabled(n_disabled),
.n_reinit(n_reinit), .n_db_ignored(n_db_ignored),
.n_illegal_drop(n_illegal_drop), .n_halts(n_halts));
localparam [2:0] EP_DISABLED=0, EP_STOPPED=1, EP_RUNNING=2, EP_HALTED=3;
// ---- SHADOW MODEL of the endpoint state ----
integer s_st;
integer m_cfg, m_dis, m_re, m_dbi, m_ill, m_hal;
integer errors=0, i, st0, a, b, c, d, e, f, g;
integer n_exh=0;
integer n_vis [0:3];
integer n_reinit_seen=0, n_dbig=0, n_ill=0, n_halt=0;
task check(input cond, input [639:0] msg);
begin if (!cond) begin errors=errors+1;
if (errors <= 25)
$display(" FAIL: %0s (cfg=%b drop=%b add=%b slot=%b db=%b mine=%b err=%b rst=%b | state=%0d usable=%b reinit=%b illegal=%b dbig=%b || model=%0d, t=%0t)",
msg, cfg_valid, my_drop, my_add, is_slot_ctx, db_valid,
db_is_mine, ep_error, reset_ep, state, usable,
reinitialised, illegal_drop, db_ignored, s_st, $time);
end end
endtask
// The model is a flat case over the CURRENT state where the design applies
// drop and add as two successive assignments -- a different route, and one
// that cannot share an ordering bug with it.
task model(output integer e_st, output e_reinit, output e_ill,
output e_dbig);
reg dropa, adda, dbme;
begin
e_ill = cfg_valid && is_slot_ctx && my_drop;
dropa = cfg_valid && my_drop && !is_slot_ctx;
adda = cfg_valid && my_add;
dbme = db_valid && db_is_mine;
e_reinit = dropa && adda;
e_dbig = dbme && (s_st != EP_STOPPED);
e_st = s_st;
if (cfg_valid) begin
// stated as the four cases rather than as two assignments
if (dropa && adda) e_st = EP_STOPPED; // re-initialised
else if (dropa) e_st = EP_DISABLED;
else if (adda) e_st = EP_STOPPED;
end else if (ep_error) begin
case (s_st)
EP_DISABLED: e_st = EP_DISABLED;
default: e_st = EP_HALTED;
endcase
end else if (reset_ep) begin
if (s_st == EP_HALTED) e_st = EP_STOPPED;
end else if (dbme) begin
if (s_st == EP_STOPPED) e_st = EP_RUNNING;
end
end
endtask
task check_comb;
integer e_st;
reg e_reinit, e_ill, e_dbig;
begin
model(e_st, e_reinit, e_ill, e_dbig);
check(state === s_st[2:0], "state matches the shadow model");
check(usable === (s_st == EP_RUNNING), "usable matches the state");
check(reinitialised === e_reinit, "reinitialised matches the model");
check(illegal_drop === e_ill, "illegal_drop matches the model");
check(db_ignored === e_dbig, "db_ignored matches the model");
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE property. Drop before Add: an endpoint named by BOTH masks
// is re-initialised, not torn down.
if (cfg_valid && my_drop && my_add && !is_slot_ctx)
check(reinitialised,
"drop+add in one command was not reported as a re-initialisation");
// 2. A doorbell is a HINT. It never starts an endpoint that is not
// ready to start.
if (db_valid && db_is_mine && !cfg_valid && !ep_error && !reset_ep) begin
if (state === EP_DISABLED)
check(db_ignored,
"a doorbell on a DISABLED endpoint was not ignored");
if (state === EP_HALTED)
check(db_ignored,
"a doorbell on a HALTED endpoint was not ignored -- the reset that clears the error has not happened");
end
// 3. D0 is not a legal drop: the slot context cannot be dropped by a
// Configure Endpoint command.
if (cfg_valid && is_slot_ctx && my_drop)
check(illegal_drop,
"a drop of the SLOT context was not flagged as illegal");
// 4. Only a RUNNING endpoint is usable.
check(usable === (state === EP_RUNNING),
"usable disagrees with the state");
// 5. A disabled endpoint runs nothing.
if (state === EP_DISABLED)
check(!usable, "a DISABLED endpoint reported itself usable");
// 6. A halted endpoint runs nothing either.
if (state === EP_HALTED)
check(!usable, "a HALTED endpoint reported itself usable");
// 7. The state is always one of the four that exist.
check(state <= EP_HALTED, "the endpoint reached an undefined state");
// 8. A re-initialisation is always also a drop and an add.
if (reinitialised)
check(cfg_valid && my_drop && my_add && !is_slot_ctx,
"a re-initialisation was reported with no drop+add pair");
if (s_st <= 3) n_vis[s_st] = n_vis[s_st] + 1;
if (e_reinit) n_reinit_seen = n_reinit_seen + 1;
if (e_dbig) n_dbig = n_dbig + 1;
if (e_ill) n_ill = n_ill + 1;
if ((e_st == EP_HALTED) && (s_st != EP_HALTED)) n_halt = n_halt + 1;
end
endtask
task model_step;
integer e_st;
reg e_reinit, e_ill, e_dbig;
begin
model(e_st, e_reinit, e_ill, e_dbig);
if ((e_st != EP_DISABLED) && (s_st == EP_DISABLED)) m_cfg = m_cfg + 1;
if ((e_st == EP_DISABLED) && (s_st != EP_DISABLED)) m_dis = m_dis + 1;
if (e_reinit) m_re = m_re + 1;
if (e_dbig) m_dbi = m_dbi + 1;
if (e_ill) m_ill = m_ill + 1;
if ((e_st == EP_HALTED) && (s_st != EP_HALTED)) m_hal = m_hal + 1;
s_st = e_st;
end
endtask
task step;
begin
#1;
check_comb;
model_step;
@(posedge clk); #1;
check(state === s_st[2:0], "state tracked the model");
check(n_configured === m_cfg[31:0], "n_configured matches the model");
check(n_disabled === m_dis[31:0], "n_disabled matches the model");
check(n_reinit === m_re[31:0], "n_reinit matches the model");
check(n_db_ignored === m_dbi[31:0], "n_db_ignored matches the model");
check(n_illegal_drop === m_ill[31:0], "n_illegal_drop matches the model");
check(n_halts === m_hal[31:0], "n_halts matches the model");
end
endtask
task idle_in;
begin
cfg_valid=0; my_drop=0; my_add=0; db_valid=0; db_is_mine=0;
ep_error=0; reset_ep=0;
end
endtask
task hard_reset;
begin
rst_n=0; idle_in; is_slot_ctx=0;
@(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
s_st=EP_DISABLED;
m_cfg=0; m_dis=0; m_re=0; m_dbi=0; m_ill=0; m_hal=0;
end
endtask
// Walk to a chosen state using only legal transitions: Configure Endpoint
// to leave DISABLED, a doorbell to start, an error to halt.
task goto_state(input integer want);
begin
hard_reset;
is_slot_ctx=0;
if (want != EP_DISABLED) begin
cfg_valid=1; my_add=1; step; idle_in; // -> STOPPED
if (want == EP_RUNNING) begin
db_valid=1; db_is_mine=1; step; idle_in; // -> RUNNING
end else if (want == EP_HALTED) begin
ep_error=1; step; idle_in; // -> HALTED
end
end
#1;
check(state === want[2:0], "goto_state reached the requested state");
end
endtask
initial begin
for (i=0;i<4;i=i+1) n_vis[i]=0;
hard_reset;
check(state === EP_DISABLED, "reset leaves the endpoint DISABLED");
check(!usable, "and unusable");
// ===== A. EXHAUSTIVE sweep =====
// 4 endpoint states x cfg_valid x my_drop x my_add x is_slot_ctx
// x db_valid x db_is_mine x ep_error x reset_ep
// = 4 x 256 = 1024 one-step transitions: every state against every
// combination of the eight control inputs.
for (st0=0; st0<4; st0=st0+1)
for (a=0;a<2;a=a+1) // cfg_valid
for (b=0;b<2;b=b+1) // my_drop
for (c=0;c<2;c=c+1) // my_add
for (d=0;d<2;d=d+1) // is_slot_ctx
for (e=0;e<2;e=e+1) // db_valid
for (f=0;f<2;f=f+1) // db_is_mine
for (g=0;g<2;g=g+1) // ep_error
for (i=0;i<2;i=i+1) begin // reset_ep
goto_state(st0);
cfg_valid=a[0]; my_drop=b[0]; my_add=c[0]; is_slot_ctx=d[0];
db_valid=e[0]; db_is_mine=f[0]; ep_error=g[0]; reset_ep=i[0];
step;
n_exh = n_exh + 1;
idle_in;
end
$display(" exhaustive endpoint-context sweep: %0d of %0d transitions verified",
n_exh, 4*256);
// ===== B. directed: the life of an endpoint =====
hard_reset;
// 1. Configure Endpoint with only the Add flag: the endpoint exists.
cfg_valid=1; my_add=1; step; idle_in;
check(state === EP_STOPPED, "an Add flag configures the endpoint");
check(!usable, "but it is not running yet -- nothing has rung its doorbell");
check(n_configured === 32'd1, "one configuration");
// 2. A doorbell starts it.
db_valid=1; db_is_mine=1; step; idle_in;
check(state === EP_RUNNING, "a doorbell starts a STOPPED endpoint");
check(usable, "and now transfers may run");
check(n_db_ignored === 32'd0, "that doorbell was not ignored");
// 3. A doorbell for someone ELSE does nothing at all.
db_valid=1; db_is_mine=0; step; idle_in;
check(state === EP_RUNNING, "a doorbell for another endpoint changes nothing");
check(n_db_ignored === 32'd0, "and is not even counted as ignored -- it was never ours");
// 4. A transfer errors: the endpoint halts.
ep_error=1; step; idle_in;
check(state === EP_HALTED, "an error halts the endpoint");
check(!usable, "which stops transfers");
check(n_halts === 32'd1, "one halt");
// 5. THE doorbell rule. Software rings it anyway -- which is legal, and
// must do nothing, because the error has not been cleared.
db_valid=1; db_is_mine=1; #1;
check(db_ignored,
"a doorbell on a HALTED endpoint is IGNORED -- it is a hint, not a command");
step; idle_in;
check(state === EP_HALTED, "the endpoint is still halted");
check(n_db_ignored === 32'd1, "and the ignored doorbell was counted");
// 6. Reset Endpoint clears the halt.
reset_ep=1; step; idle_in;
check(state === EP_STOPPED, "Reset Endpoint clears the halt");
check(!usable, "leaving it stopped, not running");
// 7. Reset Endpoint from a state that is NOT halted does nothing. It is
// not a general "go back to idle" command.
db_valid=1; db_is_mine=1; step; idle_in;
check(state === EP_RUNNING, "started again");
reset_ep=1; step; idle_in;
check(state === EP_RUNNING,
"Reset Endpoint on a RUNNING endpoint does nothing");
// ===== C. directed: drop, add, and the order between them =====
hard_reset;
cfg_valid=1; my_add=1; step; idle_in;
db_valid=1; db_is_mine=1; step; idle_in;
check(state === EP_RUNNING, "a configured, running endpoint");
// 8. Drop alone tears it down.
cfg_valid=1; my_drop=1; step; idle_in;
check(state === EP_DISABLED, "a Drop flag tears the endpoint down");
check(n_disabled === 32'd1, "one teardown");
// 9. THE case. Drop AND Add in the same command is a RE-INITIALISATION,
// because drops are applied first. The endpoint ends up configured.
cfg_valid=1; my_add=1; step; idle_in;
db_valid=1; db_is_mine=1; step; idle_in;
check(state === EP_RUNNING, "running again");
cfg_valid=1; my_drop=1; my_add=1; #1;
check(reinitialised, "drop and add together is a re-initialisation");
step; idle_in;
check(state === EP_STOPPED,
"so the endpoint ends up CONFIGURED, not disabled -- drops go first");
check(n_reinit === 32'd1, "one re-initialisation");
check(!usable, "freshly re-initialised, so stopped rather than running");
// 10. D0 is not a legal drop. The slot context cannot be dropped by a
// Configure Endpoint command.
hard_reset;
is_slot_ctx=1;
cfg_valid=1; my_add=1; step; idle_in;
check(state === EP_STOPPED, "the slot context can be ADDED");
cfg_valid=1; my_drop=1; #1;
check(illegal_drop, "but a DROP of the slot context is illegal");
step; idle_in;
check(state === EP_STOPPED,
"and is ignored -- the slot survives a routine reconfiguration");
check(n_illegal_drop === 32'd1, "and the driver bug was counted");
// 11. A disabled endpoint cannot error -- it is not running anything.
hard_reset;
is_slot_ctx=0;
ep_error=1; step; idle_in;
check(state === EP_DISABLED, "a DISABLED endpoint does not halt on error");
check(n_halts === 32'd0, "and no halt was counted");
// 12. A command outranks a doorbell in the same cycle.
hard_reset;
cfg_valid=1; my_add=1; step; idle_in;
cfg_valid=1; my_drop=1; db_valid=1; db_is_mine=1; step; idle_in;
check(state === EP_DISABLED,
"a Configure Endpoint command outranks a doorbell in the same cycle");
// ===== D. randomised =====
hard_reset;
for (i=0;i<40000;i=i+1) begin
cfg_valid = ({$random}%4)==0;
my_drop = ({$random}%2);
my_add = ({$random}%2);
is_slot_ctx = ({$random}%8)==0;
db_valid = ({$random}%2);
db_is_mine = ({$random}%2);
ep_error = ({$random}%12)==0;
reset_ep = ({$random}%10)==0;
step;
end
for (i=0;i<4;i=i+1)
check(n_vis[i] > 500, "every endpoint state was visited many times");
check(n_reinit_seen > 500, "re-initialisations happened often");
check(n_dbig > 500, "doorbells were correctly ignored often");
check(n_ill > 200, "illegal slot drops were presented often");
check(n_halt > 200, "halts happened often");
$display("");
$display(" REACH: transitions=%0d | states: disabled=%0d stopped=%0d running=%0d halted=%0d",
n_exh, n_vis[0], n_vis[1], n_vis[2], n_vis[3]);
$display(" CASES: re-initialisations=%0d ignored-doorbells=%0d illegal-slot-drops=%0d halts=%0d",
n_reinit_seen, n_dbig, n_ill, n_halt);
$display(" COUNTERS: configured=%0d disabled=%0d reinit=%0d db-ignored=%0d illegal=%0d halts=%0d",
n_configured, n_disabled, n_reinit, n_db_ignored,
n_illegal_drop, n_halts);
$display(" [Verilog] xhci_ep_context: %0d errors", errors);
$display(" [Verilog] %0s", errors==0 ? "PASS" : "FAIL");
$display("");
$finish;
end
endmodule10.2 SystemVerilog testbench
`timescale 1ns/1ps
module tb_ec_sv;
import xhci_ctx_pkg::*;
logic clk=0, rst_n=0;
logic cfg_valid=0, my_drop=0, my_add=0, is_slot_ctx=0;
logic db_valid=0, db_is_mine=0, ep_error=0, reset_ep=0;
ep_state_e state;
logic usable, reinitialised, illegal_drop, db_ignored;
// Icarus seeds $random and $urandom identically, so an unseeded run would
// replay the Verilog suite's stimulus exactly. See chapter 20.5 section 9.2.
int urandom_seed = 22404;
wire [31:0] n_configured, n_disabled, n_reinit, n_db_ignored;
wire [31:0] n_illegal_drop, n_halts;
always #5 clk=~clk;
xhci_ep_context dut (
.clk, .rst_n, .cfg_valid, .my_drop, .my_add, .is_slot_ctx, .db_valid,
.db_is_mine, .ep_error, .reset_ep, .state, .usable, .reinitialised,
.illegal_drop, .db_ignored, .n_configured, .n_disabled, .n_reinit,
.n_db_ignored, .n_illegal_drop, .n_halts);
// ---- SHADOW MODEL of the endpoint state ----
int s_st;
int m_cfg, m_dis, m_re, m_dbi, m_ill, m_hal;
int errors=0, i, st0, a, b, c, d, e, f, g;
int n_exh=0;
int n_vis [4];
int n_reinit_seen=0, n_dbig=0, n_ill=0, n_halt=0;
task automatic check(input bit cond, input string msg);
// Icarus will not call .name() on a net, so the enum output is copied
// into a variable of the same type before being printed.
ep_state_e st_v;
if (!cond) begin
errors++;
st_v = state;
if (errors <= 25)
$display(" FAIL: %0s (cfg=%b drop=%b add=%b slot=%b db=%b mine=%b err=%b rst=%b | state=%s usable=%b reinit=%b illegal=%b dbig=%b || model=%0d, t=%0t)",
msg, cfg_valid, my_drop, my_add, is_slot_ctx, db_valid,
db_is_mine, ep_error, reset_ep, st_v.name(), usable,
reinitialised, illegal_drop, db_ignored, s_st, $time);
end
endtask
// The model is a flat case over the CURRENT state where the design applies
// drop and add as two successive assignments -- a different route, and one
// that cannot share an ordering bug with it.
task automatic model(output int e_st, output bit e_reinit,
output bit e_ill, output bit e_dbig);
bit dropa, adda, dbme;
begin
e_ill = cfg_valid && is_slot_ctx && my_drop;
dropa = cfg_valid && my_drop && !is_slot_ctx;
adda = cfg_valid && my_add;
dbme = db_valid && db_is_mine;
e_reinit = dropa && adda;
e_dbig = dbme && (s_st != EP_STOPPED);
e_st = s_st;
if (cfg_valid) begin
// stated as the four cases rather than as two assignments
if (dropa && adda) e_st = EP_STOPPED; // re-initialised
else if (dropa) e_st = EP_DISABLED;
else if (adda) e_st = EP_STOPPED;
end else if (ep_error) begin
case (s_st)
EP_DISABLED: e_st = EP_DISABLED;
default: e_st = EP_HALTED;
endcase
end else if (reset_ep) begin
if (s_st == EP_HALTED) e_st = EP_STOPPED;
end else if (dbme) begin
if (s_st == EP_STOPPED) e_st = EP_RUNNING;
end
end
endtask
task automatic check_comb;
int e_st;
bit e_reinit, e_ill, e_dbig;
begin
model(e_st, e_reinit, e_ill, e_dbig);
check(state === ep_state_e'(s_st), "state matches the shadow model");
check(usable === (s_st == EP_RUNNING), "usable matches the state");
check(reinitialised === e_reinit, "reinitialised matches the model");
check(illegal_drop === e_ill, "illegal_drop matches the model");
check(db_ignored === e_dbig, "db_ignored matches the model");
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE property. Drop before Add: an endpoint named by BOTH masks
// is re-initialised, not torn down.
if (cfg_valid && my_drop && my_add && !is_slot_ctx)
check(reinitialised,
"drop+add in one command was not reported as a re-initialisation");
// 2. A doorbell is a HINT. It never starts an endpoint that is not
// ready to start.
if (db_valid && db_is_mine && !cfg_valid && !ep_error && !reset_ep) begin
if (state === EP_DISABLED)
check(db_ignored,
"a doorbell on a DISABLED endpoint was not ignored");
if (state === EP_HALTED)
check(db_ignored,
"a doorbell on a HALTED endpoint was not ignored -- the reset that clears the error has not happened");
end
// 3. D0 is not a legal drop: the slot context cannot be dropped by a
// Configure Endpoint command.
if (cfg_valid && is_slot_ctx && my_drop)
check(illegal_drop,
"a drop of the SLOT context was not flagged as illegal");
// 4. Only a RUNNING endpoint is usable.
check(usable === (state === EP_RUNNING),
"usable disagrees with the state");
// 5. A disabled endpoint runs nothing.
if (state === EP_DISABLED)
check(!usable, "a DISABLED endpoint reported itself usable");
// 6. A halted endpoint runs nothing either.
if (state === EP_HALTED)
check(!usable, "a HALTED endpoint reported itself usable");
// 7. The state is always one of the four that exist.
check(state <= EP_HALTED, "the endpoint reached an undefined state");
// 8. A re-initialisation is always also a drop and an add.
if (reinitialised)
check(cfg_valid && my_drop && my_add && !is_slot_ctx,
"a re-initialisation was reported with no drop+add pair");
n_vis[s_st] = n_vis[s_st] + 1;
if (e_reinit) n_reinit_seen = n_reinit_seen + 1;
if (e_dbig) n_dbig = n_dbig + 1;
if (e_ill) n_ill = n_ill + 1;
if ((e_st == EP_HALTED) && (s_st != EP_HALTED)) n_halt = n_halt + 1;
end
endtask
task automatic model_step;
int e_st;
bit e_reinit, e_ill, e_dbig;
begin
model(e_st, e_reinit, e_ill, e_dbig);
if ((e_st != EP_DISABLED) && (s_st == EP_DISABLED)) m_cfg = m_cfg + 1;
if ((e_st == EP_DISABLED) && (s_st != EP_DISABLED)) m_dis = m_dis + 1;
if (e_reinit) m_re = m_re + 1;
if (e_dbig) m_dbi = m_dbi + 1;
if (e_ill) m_ill = m_ill + 1;
if ((e_st == EP_HALTED) && (s_st != EP_HALTED)) m_hal = m_hal + 1;
s_st = e_st;
end
endtask
task automatic step;
begin
#1;
check_comb;
model_step;
@(posedge clk); #1;
check(state === ep_state_e'(s_st), "state tracked the model");
check(n_configured === 32'(m_cfg), "n_configured matches the model");
check(n_disabled === 32'(m_dis), "n_disabled matches the model");
check(n_reinit === 32'(m_re), "n_reinit matches the model");
check(n_db_ignored === 32'(m_dbi), "n_db_ignored matches the model");
check(n_illegal_drop === 32'(m_ill), "n_illegal_drop matches the model");
check(n_halts === 32'(m_hal), "n_halts matches the model");
end
endtask
task automatic idle_in;
begin
cfg_valid=0; my_drop=0; my_add=0; db_valid=0; db_is_mine=0;
ep_error=0; reset_ep=0;
end
endtask
task automatic hard_reset;
begin
rst_n=0; idle_in; is_slot_ctx=0;
@(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
s_st=EP_DISABLED;
m_cfg=0; m_dis=0; m_re=0; m_dbi=0; m_ill=0; m_hal=0;
end
endtask
// Walk to a chosen state using only legal transitions: Configure Endpoint
// to leave DISABLED, a doorbell to start, an error to halt.
task automatic goto_state(input int want);
begin
hard_reset;
is_slot_ctx=0;
if (want != EP_DISABLED) begin
cfg_valid=1; my_add=1; step; idle_in; // -> STOPPED
if (want == EP_RUNNING) begin
db_valid=1; db_is_mine=1; step; idle_in; // -> RUNNING
end else if (want == EP_HALTED) begin
ep_error=1; step; idle_in; // -> HALTED
end
end
#1;
check(state === ep_state_e'(want), "goto_state reached the requested state");
end
endtask
initial begin
void'($urandom(urandom_seed));
foreach (n_vis[i]) n_vis[i]=0;
hard_reset;
check(state === EP_DISABLED, "reset leaves the endpoint DISABLED");
check(!usable, "and unusable");
// ===== A. EXHAUSTIVE sweep =====
// 4 endpoint states x cfg_valid x my_drop x my_add x is_slot_ctx
// x db_valid x db_is_mine x ep_error x reset_ep
// = 4 x 256 = 1024 one-step transitions: every state against every
// combination of the eight control inputs.
for (st0=0; st0<4; st0=st0+1)
for (a=0;a<2;a=a+1) // cfg_valid
for (b=0;b<2;b=b+1) // my_drop
for (c=0;c<2;c=c+1) // my_add
for (d=0;d<2;d=d+1) // is_slot_ctx
for (e=0;e<2;e=e+1) // db_valid
for (f=0;f<2;f=f+1) // db_is_mine
for (g=0;g<2;g=g+1) // ep_error
for (i=0;i<2;i=i+1) begin // reset_ep
goto_state(st0);
cfg_valid=a[0]; my_drop=b[0]; my_add=c[0]; is_slot_ctx=d[0];
db_valid=e[0]; db_is_mine=f[0]; ep_error=g[0]; reset_ep=i[0];
step;
n_exh = n_exh + 1;
idle_in;
end
$display(" exhaustive endpoint-context sweep: %0d of %0d transitions verified",
n_exh, 4*256);
// ===== B. directed: the life of an endpoint =====
hard_reset;
// 1. Configure Endpoint with only the Add flag: the endpoint exists.
cfg_valid=1; my_add=1; step; idle_in;
check(state === EP_STOPPED, "an Add flag configures the endpoint");
check(!usable, "but it is not running yet -- nothing has rung its doorbell");
check(n_configured === 32'd1, "one configuration");
// 2. A doorbell starts it.
db_valid=1; db_is_mine=1; step; idle_in;
check(state === EP_RUNNING, "a doorbell starts a STOPPED endpoint");
check(usable, "and now transfers may run");
check(n_db_ignored === 32'd0, "that doorbell was not ignored");
// 3. A doorbell for someone ELSE does nothing at all.
db_valid=1; db_is_mine=0; step; idle_in;
check(state === EP_RUNNING, "a doorbell for another endpoint changes nothing");
check(n_db_ignored === 32'd0, "and is not even counted as ignored -- it was never ours");
// 4. A transfer errors: the endpoint halts.
ep_error=1; step; idle_in;
check(state === EP_HALTED, "an error halts the endpoint");
check(!usable, "which stops transfers");
check(n_halts === 32'd1, "one halt");
// 5. THE doorbell rule. Software rings it anyway -- which is legal, and
// must do nothing, because the error has not been cleared.
db_valid=1; db_is_mine=1; #1;
check(db_ignored,
"a doorbell on a HALTED endpoint is IGNORED -- it is a hint, not a command");
step; idle_in;
check(state === EP_HALTED, "the endpoint is still halted");
check(n_db_ignored === 32'd1, "and the ignored doorbell was counted");
// 6. Reset Endpoint clears the halt.
reset_ep=1; step; idle_in;
check(state === EP_STOPPED, "Reset Endpoint clears the halt");
check(!usable, "leaving it stopped, not running");
// 7. Reset Endpoint from a state that is NOT halted does nothing. It is
// not a general "go back to idle" command.
db_valid=1; db_is_mine=1; step; idle_in;
check(state === EP_RUNNING, "started again");
reset_ep=1; step; idle_in;
check(state === EP_RUNNING,
"Reset Endpoint on a RUNNING endpoint does nothing");
// ===== C. directed: drop, add, and the order between them =====
hard_reset;
cfg_valid=1; my_add=1; step; idle_in;
db_valid=1; db_is_mine=1; step; idle_in;
check(state === EP_RUNNING, "a configured, running endpoint");
// 8. Drop alone tears it down.
cfg_valid=1; my_drop=1; step; idle_in;
check(state === EP_DISABLED, "a Drop flag tears the endpoint down");
check(n_disabled === 32'd1, "one teardown");
// 9. THE case. Drop AND Add in the same command is a RE-INITIALISATION,
// because drops are applied first. The endpoint ends up configured.
cfg_valid=1; my_add=1; step; idle_in;
db_valid=1; db_is_mine=1; step; idle_in;
check(state === EP_RUNNING, "running again");
cfg_valid=1; my_drop=1; my_add=1; #1;
check(reinitialised, "drop and add together is a re-initialisation");
step; idle_in;
check(state === EP_STOPPED,
"so the endpoint ends up CONFIGURED, not disabled -- drops go first");
check(n_reinit === 32'd1, "one re-initialisation");
check(!usable, "freshly re-initialised, so stopped rather than running");
// 10. D0 is not a legal drop. The slot context cannot be dropped by a
// Configure Endpoint command.
hard_reset;
is_slot_ctx=1;
cfg_valid=1; my_add=1; step; idle_in;
check(state === EP_STOPPED, "the slot context can be ADDED");
cfg_valid=1; my_drop=1; #1;
check(illegal_drop, "but a DROP of the slot context is illegal");
step; idle_in;
check(state === EP_STOPPED,
"and is ignored -- the slot survives a routine reconfiguration");
check(n_illegal_drop === 32'd1, "and the driver bug was counted");
// 11. A disabled endpoint cannot error -- it is not running anything.
hard_reset;
is_slot_ctx=0;
ep_error=1; step; idle_in;
check(state === EP_DISABLED, "a DISABLED endpoint does not halt on error");
check(n_halts === 32'd0, "and no halt was counted");
// 12. A command outranks a doorbell in the same cycle.
hard_reset;
cfg_valid=1; my_add=1; step; idle_in;
cfg_valid=1; my_drop=1; db_valid=1; db_is_mine=1; step; idle_in;
check(state === EP_DISABLED,
"a Configure Endpoint command outranks a doorbell in the same cycle");
// ===== D. randomised =====
hard_reset;
for (i=0;i<40000;i=i+1) begin
cfg_valid = ($urandom%4)==0;
my_drop = $urandom%2;
my_add = $urandom%2;
is_slot_ctx = ($urandom%8)==0;
db_valid = $urandom%2;
db_is_mine = $urandom%2;
ep_error = ($urandom%12)==0;
reset_ep = ($urandom%10)==0;
step;
end
foreach (n_vis[i])
check(n_vis[i] > 500, "every endpoint state was visited many times");
check(n_reinit_seen > 500, "re-initialisations happened often");
check(n_dbig > 500, "doorbells were correctly ignored often");
check(n_ill > 200, "illegal slot drops were presented often");
check(n_halt > 200, "halts happened often");
$display("");
$display(" REACH: transitions=%0d | states: disabled=%0d stopped=%0d running=%0d halted=%0d",
n_exh, n_vis[0], n_vis[1], n_vis[2], n_vis[3]);
$display(" CASES: re-initialisations=%0d ignored-doorbells=%0d illegal-slot-drops=%0d halts=%0d",
n_reinit_seen, n_dbig, n_ill, n_halt);
$display(" COUNTERS: configured=%0d disabled=%0d reinit=%0d db-ignored=%0d illegal=%0d halts=%0d",
n_configured, n_disabled, n_reinit, n_db_ignored,
n_illegal_drop, n_halts);
$display(" [SystemVerilog] xhci_ep_context: %0d errors", errors);
$display(" [SystemVerilog] %0s", errors==0 ? "PASS" : "FAIL");
$display("");
$finish;
end
endmodule10.3 VHDL testbench
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
use work.xhci_ctx_pkg.all;
entity tb_ec_vhdl is
end entity;
architecture sim of tb_ec_vhdl is
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal cfg_valid, my_drop, my_add, is_slot_ctx : std_logic := '0';
signal db_valid, db_is_mine, ep_error, reset_ep : std_logic := '0';
signal state : std_logic_vector(2 downto 0);
signal usable, reinitialised, illegal_drop, db_ignored : std_logic;
signal n_configured, n_disabled, n_reinit, n_db_ignored,
n_illegal_drop, n_halts : std_logic_vector(31 downto 0);
signal running : boolean := true;
type cnt4_t is array (0 to 3) of integer;
begin
clk <= not clk after 5 ns when running else '0';
dut : entity work.xhci_ep_context
port map (clk => clk, rst_n => rst_n, cfg_valid => cfg_valid,
my_drop => my_drop, my_add => my_add,
is_slot_ctx => is_slot_ctx, db_valid => db_valid,
db_is_mine => db_is_mine, ep_error => ep_error,
reset_ep => reset_ep, state => state, usable => usable,
reinitialised => reinitialised, illegal_drop => illegal_drop,
db_ignored => db_ignored, n_configured => n_configured,
n_disabled => n_disabled, n_reinit => n_reinit,
n_db_ignored => n_db_ignored,
n_illegal_drop => n_illegal_drop, n_halts => n_halts);
stim : process
variable seed1 : positive := 5119;
variable seed2 : positive := 7727;
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;
-- SHADOW MODEL of the endpoint state.
variable s_st : ep_state_t := EP_DISABLED;
variable m_cfg, m_dis, m_re, m_dbi, m_ill, m_hal : integer := 0;
variable n_exh : integer := 0;
variable n_vis : cnt4_t := (others => 0);
variable n_reinit_seen, n_dbig, n_ill, n_halt : integer := 0;
procedure check(cond : boolean; msg : string) is
begin
if not cond then
errors := errors + 1;
if errors <= 25 then
report " FAIL: " & msg
& " (cfg=" & std_logic'image(cfg_valid)(2)
& " drop=" & std_logic'image(my_drop)(2)
& " add=" & std_logic'image(my_add)(2)
& " slot=" & std_logic'image(is_slot_ctx)(2)
& " db=" & std_logic'image(db_valid)(2)
& " mine=" & std_logic'image(db_is_mine)(2)
& " err=" & std_logic'image(ep_error)(2)
& " rst=" & std_logic'image(reset_ep)(2)
& " | state=" & integer'image(to_integer(unsigned(state)))
& " usable=" & std_logic'image(usable)(2)
& " reinit=" & std_logic'image(reinitialised)(2)
& " illegal=" & std_logic'image(illegal_drop)(2)
& " dbig=" & std_logic'image(db_ignored)(2)
& " || model=" & integer'image(ep_state_t'pos(s_st))
& ")" 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;
-- The model is a flat case over the CURRENT state where the design
-- applies drop and add as two successive assignments -- a different
-- route, and one that cannot share an ordering bug with it.
procedure model(variable e_st : out ep_state_t;
variable e_reinit, e_ill, e_dbig : out boolean) is
variable dropa, adda, dbme : boolean;
begin
e_ill := cfg_valid = '1' and is_slot_ctx = '1' and my_drop = '1';
dropa := cfg_valid = '1' and my_drop = '1' and is_slot_ctx = '0';
adda := cfg_valid = '1' and my_add = '1';
dbme := db_valid = '1' and db_is_mine = '1';
e_reinit := dropa and adda;
e_dbig := dbme and s_st /= EP_STOPPED;
e_st := s_st;
if cfg_valid = '1' then
-- stated as the four cases rather than as two assignments
if dropa and adda then e_st := EP_STOPPED; -- re-initialised
elsif dropa then e_st := EP_DISABLED;
elsif adda then e_st := EP_STOPPED;
end if;
elsif ep_error = '1' then
case s_st is
when EP_DISABLED => e_st := EP_DISABLED;
when others => e_st := EP_HALTED;
end case;
elsif reset_ep = '1' then
if s_st = EP_HALTED then e_st := EP_STOPPED; end if;
elsif dbme then
if s_st = EP_STOPPED then e_st := EP_RUNNING; end if;
end if;
end procedure;
procedure check_comb is
variable e_st : ep_state_t;
variable e_reinit, e_ill, e_dbig : boolean;
begin
model(e_st, e_reinit, e_ill, e_dbig);
check(state = st_code(s_st), "state matches the shadow model");
check((usable = '1') = (s_st = EP_RUNNING), "usable matches the state");
check((reinitialised = '1') = e_reinit,
"reinitialised matches the model");
check((illegal_drop = '1') = e_ill, "illegal_drop matches the model");
check((db_ignored = '1') = e_dbig, "db_ignored matches the model");
-- ---- SAFETY PROPERTIES, independent of the model ----
-- 1. THE property. Drop before Add: an endpoint named by BOTH masks is
-- re-initialised, not torn down.
if cfg_valid = '1' and my_drop = '1' and my_add = '1'
and is_slot_ctx = '0' then
check(reinitialised = '1',
"drop+add in one command was not reported as a re-initialisation");
end if;
-- 2. A doorbell is a HINT.
if db_valid = '1' and db_is_mine = '1' and cfg_valid = '0'
and ep_error = '0' and reset_ep = '0' then
if state = st_code(EP_DISABLED) then
check(db_ignored = '1',
"a doorbell on a DISABLED endpoint was not ignored");
end if;
if state = st_code(EP_HALTED) then
check(db_ignored = '1',
"a doorbell on a HALTED endpoint was not ignored -- the reset that clears the error has not happened");
end if;
end if;
-- 3. D0 is not a legal drop.
if cfg_valid = '1' and is_slot_ctx = '1' and my_drop = '1' then
check(illegal_drop = '1',
"a drop of the SLOT context was not flagged as illegal");
end if;
-- 4. Only a RUNNING endpoint is usable.
check((usable = '1') = (state = st_code(EP_RUNNING)),
"usable disagrees with the state");
-- 5. A disabled endpoint runs nothing.
if state = st_code(EP_DISABLED) then
check(usable = '0', "a DISABLED endpoint reported itself usable");
end if;
-- 6. A halted endpoint runs nothing either.
if state = st_code(EP_HALTED) then
check(usable = '0', "a HALTED endpoint reported itself usable");
end if;
-- 7. The state is always one of the four that exist.
check(to_integer(unsigned(state)) <= 3,
"the endpoint reached an undefined state");
-- 8. A re-initialisation is always also a drop and an add.
if reinitialised = '1' then
check(cfg_valid = '1' and my_drop = '1' and my_add = '1'
and is_slot_ctx = '0',
"a re-initialisation was reported with no drop+add pair");
end if;
n_vis(ep_state_t'pos(s_st)) := n_vis(ep_state_t'pos(s_st)) + 1;
if e_reinit then n_reinit_seen := n_reinit_seen + 1; end if;
if e_dbig then n_dbig := n_dbig + 1; end if;
if e_ill then n_ill := n_ill + 1; end if;
if e_st = EP_HALTED and s_st /= EP_HALTED then
n_halt := n_halt + 1;
end if;
end procedure;
procedure model_step is
variable e_st : ep_state_t;
variable e_reinit, e_ill, e_dbig : boolean;
begin
model(e_st, e_reinit, e_ill, e_dbig);
if e_st /= EP_DISABLED and s_st = EP_DISABLED then
m_cfg := m_cfg + 1;
end if;
if e_st = EP_DISABLED and s_st /= EP_DISABLED then
m_dis := m_dis + 1;
end if;
if e_reinit then m_re := m_re + 1; end if;
if e_dbig then m_dbi := m_dbi + 1; end if;
if e_ill then m_ill := m_ill + 1; end if;
if e_st = EP_HALTED and s_st /= EP_HALTED then
m_hal := m_hal + 1;
end if;
s_st := e_st;
end procedure;
procedure step is
begin
wait for 1 ns;
check_comb;
model_step;
wait until rising_edge(clk);
wait for 1 ns;
check(state = st_code(s_st), "state tracked the model");
check(n_configured = std_logic_vector(to_unsigned(m_cfg, 32)),
"n_configured matches the model");
check(n_disabled = std_logic_vector(to_unsigned(m_dis, 32)),
"n_disabled matches the model");
check(n_reinit = std_logic_vector(to_unsigned(m_re, 32)),
"n_reinit matches the model");
check(n_db_ignored = std_logic_vector(to_unsigned(m_dbi, 32)),
"n_db_ignored matches the model");
check(n_illegal_drop = std_logic_vector(to_unsigned(m_ill, 32)),
"n_illegal_drop matches the model");
check(n_halts = std_logic_vector(to_unsigned(m_hal, 32)),
"n_halts matches the model");
end procedure;
procedure idle_in is
begin
cfg_valid <= '0'; my_drop <= '0'; my_add <= '0'; db_valid <= '0';
db_is_mine <= '0'; ep_error <= '0'; reset_ep <= '0';
end procedure;
procedure hard_reset is
begin
rst_n <= '0'; idle_in; is_slot_ctx <= '0';
wait until rising_edge(clk); wait for 1 ns;
wait until rising_edge(clk); wait for 1 ns;
rst_n <= '1'; wait for 1 ns;
s_st := EP_DISABLED;
m_cfg := 0; m_dis := 0; m_re := 0; m_dbi := 0; m_ill := 0; m_hal := 0;
end procedure;
-- Walk to a chosen state using only legal transitions: Configure
-- Endpoint to leave DISABLED, a doorbell to start, an error to halt.
procedure goto_state(want : ep_state_t) is
begin
hard_reset;
is_slot_ctx <= '0';
if want /= EP_DISABLED then
cfg_valid <= '1'; my_add <= '1'; step; idle_in; -- -> STOPPED
if want = EP_RUNNING then
db_valid <= '1'; db_is_mine <= '1'; step; idle_in; -- -> RUNNING
elsif want = EP_HALTED then
ep_error <= '1'; step; idle_in; -- -> HALTED
end if;
end if;
wait for 1 ns;
check(state = st_code(want), "goto_state reached the requested state");
end procedure;
variable iv : integer;
variable ba, bb, bc, bd, be, bf, bg, bh : std_logic;
begin
hard_reset;
check(state = st_code(EP_DISABLED), "reset leaves the endpoint DISABLED");
check(usable = '0', "and unusable");
-- ===== A. EXHAUSTIVE sweep =====
-- 4 endpoint states x cfg_valid x my_drop x my_add x is_slot_ctx
-- x db_valid x db_is_mine x ep_error x reset_ep
-- = 4 x 256 = 1024 one-step transitions.
for st0 in 0 to 3 loop
for a in 0 to 1 loop
if a = 1 then ba := '1'; else ba := '0'; end if;
for b in 0 to 1 loop
if b = 1 then bb := '1'; else bb := '0'; end if;
for c in 0 to 1 loop
if c = 1 then bc := '1'; else bc := '0'; end if;
for d in 0 to 1 loop
if d = 1 then bd := '1'; else bd := '0'; end if;
for e in 0 to 1 loop
if e = 1 then be := '1'; else be := '0'; end if;
for f in 0 to 1 loop
if f = 1 then bf := '1'; else bf := '0'; end if;
for g in 0 to 1 loop
if g = 1 then bg := '1'; else bg := '0'; end if;
for h in 0 to 1 loop
if h = 1 then bh := '1'; else bh := '0'; end if;
goto_state(ep_state_t'val(st0));
cfg_valid <= ba; my_drop <= bb; my_add <= bc;
is_slot_ctx <= bd; db_valid <= be; db_is_mine <= bf;
ep_error <= bg; reset_ep <= bh;
step;
n_exh := n_exh + 1;
idle_in;
end loop;
end loop;
end loop;
end loop;
end loop;
end loop;
end loop;
end loop;
end loop;
report " exhaustive endpoint-context sweep: " & integer'image(n_exh)
& " of 1024 transitions verified" severity note;
-- ===== B. directed: the life of an endpoint =====
hard_reset;
-- 1. Configure Endpoint with only the Add flag.
cfg_valid <= '1'; my_add <= '1'; step; idle_in;
check(state = st_code(EP_STOPPED), "an Add flag configures the endpoint");
check(usable = '0',
"but it is not running yet -- nothing has rung its doorbell");
check(n_configured = std_logic_vector(to_unsigned(1, 32)),
"one configuration");
-- 2. A doorbell starts it.
db_valid <= '1'; db_is_mine <= '1'; step; idle_in;
check(state = st_code(EP_RUNNING), "a doorbell starts a STOPPED endpoint");
check(usable = '1', "and now transfers may run");
check(n_db_ignored = std_logic_vector(to_unsigned(0, 32)),
"that doorbell was not ignored");
-- 3. A doorbell for someone ELSE does nothing.
db_valid <= '1'; db_is_mine <= '0'; step; idle_in;
check(state = st_code(EP_RUNNING),
"a doorbell for another endpoint changes nothing");
check(n_db_ignored = std_logic_vector(to_unsigned(0, 32)),
"and is not even counted as ignored -- it was never ours");
-- 4. A transfer errors: the endpoint halts.
ep_error <= '1'; step; idle_in;
check(state = st_code(EP_HALTED), "an error halts the endpoint");
check(usable = '0', "which stops transfers");
check(n_halts = std_logic_vector(to_unsigned(1, 32)), "one halt");
-- 5. THE doorbell rule.
db_valid <= '1'; db_is_mine <= '1'; wait for 1 ns;
check(db_ignored = '1',
"a doorbell on a HALTED endpoint is IGNORED -- it is a hint, not a command");
step; idle_in;
check(state = st_code(EP_HALTED), "the endpoint is still halted");
check(n_db_ignored = std_logic_vector(to_unsigned(1, 32)),
"and the ignored doorbell was counted");
-- 6. Reset Endpoint clears the halt.
reset_ep <= '1'; step; idle_in;
check(state = st_code(EP_STOPPED), "Reset Endpoint clears the halt");
check(usable = '0', "leaving it stopped, not running");
-- 7. Reset Endpoint from a non-halted state does nothing.
db_valid <= '1'; db_is_mine <= '1'; step; idle_in;
check(state = st_code(EP_RUNNING), "started again");
reset_ep <= '1'; step; idle_in;
check(state = st_code(EP_RUNNING),
"Reset Endpoint on a RUNNING endpoint does nothing");
-- ===== C. directed: drop, add, and the order between them =====
hard_reset;
cfg_valid <= '1'; my_add <= '1'; step; idle_in;
db_valid <= '1'; db_is_mine <= '1'; step; idle_in;
check(state = st_code(EP_RUNNING), "a configured, running endpoint");
-- 8. Drop alone tears it down.
cfg_valid <= '1'; my_drop <= '1'; step; idle_in;
check(state = st_code(EP_DISABLED),
"a Drop flag tears the endpoint down");
check(n_disabled = std_logic_vector(to_unsigned(1, 32)), "one teardown");
-- 9. THE case. Drop AND Add together is a RE-INITIALISATION.
cfg_valid <= '1'; my_add <= '1'; step; idle_in;
db_valid <= '1'; db_is_mine <= '1'; step; idle_in;
check(state = st_code(EP_RUNNING), "running again");
cfg_valid <= '1'; my_drop <= '1'; my_add <= '1'; wait for 1 ns;
check(reinitialised = '1',
"drop and add together is a re-initialisation");
step; idle_in;
check(state = st_code(EP_STOPPED),
"so the endpoint ends up CONFIGURED, not disabled -- drops go first");
check(n_reinit = std_logic_vector(to_unsigned(1, 32)),
"one re-initialisation");
check(usable = '0',
"freshly re-initialised, so stopped rather than running");
-- 10. D0 is not a legal drop.
hard_reset;
is_slot_ctx <= '1';
cfg_valid <= '1'; my_add <= '1'; step; idle_in;
check(state = st_code(EP_STOPPED), "the slot context can be ADDED");
cfg_valid <= '1'; my_drop <= '1'; wait for 1 ns;
check(illegal_drop = '1',
"but a DROP of the slot context is illegal");
step; idle_in;
check(state = st_code(EP_STOPPED),
"and is ignored -- the slot survives a routine reconfiguration");
check(n_illegal_drop = std_logic_vector(to_unsigned(1, 32)),
"and the driver bug was counted");
-- 11. A disabled endpoint cannot error.
hard_reset;
is_slot_ctx <= '0';
ep_error <= '1'; step; idle_in;
check(state = st_code(EP_DISABLED),
"a DISABLED endpoint does not halt on error");
check(n_halts = std_logic_vector(to_unsigned(0, 32)),
"and no halt was counted");
-- 12. A command outranks a doorbell in the same cycle.
hard_reset;
cfg_valid <= '1'; my_add <= '1'; step; idle_in;
cfg_valid <= '1'; my_drop <= '1'; db_valid <= '1'; db_is_mine <= '1';
step; idle_in;
check(state = st_code(EP_DISABLED),
"a Configure Endpoint command outranks a doorbell in the same cycle");
-- ===== D. 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 cfg_valid <= '1';
else cfg_valid <= '0'; end if;
rnd(iv, 2); if iv = 1 then my_drop <= '1'; else my_drop <= '0'; end if;
rnd(iv, 2); if iv = 1 then my_add <= '1'; else my_add <= '0'; end if;
rnd(iv, 8); if iv = 0 then is_slot_ctx <= '1';
else is_slot_ctx <= '0'; end if;
rnd(iv, 2); if iv = 1 then db_valid <= '1';
else db_valid <= '0'; end if;
rnd(iv, 2); if iv = 1 then db_is_mine <= '1';
else db_is_mine <= '0'; end if;
rnd(iv, 12); if iv = 0 then ep_error <= '1';
else ep_error <= '0'; end if;
rnd(iv, 10); if iv = 0 then reset_ep <= '1';
else reset_ep <= '0'; end if;
step;
end loop;
for i in 0 to 3 loop
check(n_vis(i) > 500, "every endpoint state was visited many times");
end loop;
check(n_reinit_seen > 500, "re-initialisations happened often");
check(n_dbig > 500, "doorbells were correctly ignored often");
check(n_ill > 200, "illegal slot drops were presented often");
check(n_halt > 200, "halts happened often");
report " REACH: transitions=" & integer'image(n_exh)
& " | states: disabled=" & integer'image(n_vis(0))
& " stopped=" & integer'image(n_vis(1))
& " running=" & integer'image(n_vis(2))
& " halted=" & integer'image(n_vis(3)) severity note;
report " CASES: re-initialisations=" & integer'image(n_reinit_seen)
& " ignored-doorbells=" & integer'image(n_dbig)
& " illegal-slot-drops=" & integer'image(n_ill)
& " halts=" & integer'image(n_halt) severity note;
report " COUNTERS: configured="
& integer'image(to_integer(unsigned(n_configured)))
& " disabled=" & integer'image(to_integer(unsigned(n_disabled)))
& " reinit=" & integer'image(to_integer(unsigned(n_reinit)))
& " db-ignored=" & integer'image(to_integer(unsigned(n_db_ignored)))
& " illegal=" & integer'image(to_integer(unsigned(n_illegal_drop)))
& " halts=" & integer'image(to_integer(unsigned(n_halts)))
severity note;
report " [VHDL] xhci_ep_context: " & 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;11. Exhaustive Verification and Mutation Testing
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| Exhaustive transitions | 1024 / 1024 | 1024 / 1024 | 1024 / 1024 |
EP_DISABLED visits | 12705 | 13365 | 13320 |
EP_STOPPED visits | 14531 | 14203 | 14120 |
EP_RUNNING visits | 8932 | 8985 | 9227 |
EP_HALTED visits | 6155 | 5770 | 5656 |
| re-initialisations | 2258 | 2268 | 2211 |
| doorbells correctly ignored | 6634 | 6896 | 6914 |
| illegal slot drops presented | 734 | 741 | 721 |
| halts | 1861 | 1779 | 1764 |
| Result | PASS | PASS | PASS |
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| P1 | Add is applied before Drop | 190380 | 190427 | 188861 |
| P2 | a doorbell starts a DISABLED endpoint | 173842 | 174255 | 175720 |
| P3 | a doorbell starts a HALTED endpoint | 86196 | 85116 | 85212 |
| P4 | D0 is honoured — the slot context is dropped | 202237 | 202246 | 202329 |
| P5 | an error halts a DISABLED endpoint | 165824 | 165069 | 165616 |
| P6 | Reset Endpoint works from any state | 44236 | 44146 | 44109 |
| P7 | the doorbell outranks the command | 170761 | 170664 | 170183 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages, all counts distinct, columns within 1.5%.
P1 is the mutation this chapter is about, at ~190 000, and it is two lines swapped. Note that it scores less than P4 (~202 000): honouring D0 is wrong on a larger fraction of the input space, because is_slot_ctx and my_drop are independent, whereas P1 only differs when both masks name the same endpoint.
P6 is the smallest at ~44 000, and it is the one worth dwelling on. Reset Endpoint working from any state sounds harmless — even helpful. What it actually does is silently stop a running endpoint whenever software issues a reset for an endpoint it believed was halted and which has since recovered. The transfer in progress is abandoned with no completion event. It scores low because it only differs when a reset arrives at a non-halted endpoint, which is a narrow condition and exactly the racy one.
P2 and P3 are the two halves of "a doorbell is a hint", and their counts differ by a factor of two (174 000 against 86 000) for a structural reason: DISABLED is visited about twice as often as HALTED in the sweep, because it is the reset state and the destination of every drop. A mutation's count is the size of the population it is wrong on, not the severity of being wrong there — and P3, the smaller of the two, is the more dangerous, because it defeats an error-recovery mechanism rather than merely starting something that does not exist.
12. Debugging Walkthrough: The Endpoint That Vanishes on Reconfiguration
The report. A USB audio device works perfectly until the user changes the sample rate. After that, the device is present, enumerated and completely silent. Unplugging and replugging fixes it.
Step 1 — what does a sample-rate change do? It changes an isochronous endpoint's max packet size, which is in the endpoint context, which hardware owns. Software cannot edit it in place, so the driver issues a Configure Endpoint command with the endpoint in both masks — drop it and add it back with the new packet size, atomically.
Step 2 — what state is the endpoint in afterwards? Read the Output Context. EP_DISABLED. The endpoint does not exist.
Step 3 — is the driver's command right? Check the Input Context it built: D and A both set for that endpoint, which is exactly the documented way to re-initialise one. The driver is correct.
Step 4 — so what did the controller do with it? The specification says drops are applied first, then adds, so the endpoint should end STOPPED. Ending DISABLED means the adds were applied first and the drop overwrote them.
Step 5 — why replugging fixes it. A fresh enumeration configures the endpoint with a command that has only the Add flag set — no drop — so the ordering never matters. The bug requires both flags in one command, which happens only on reconfiguration, which happens only when the user changes something.
Step 6 — the cause. Two assignments in the wrong order. Mutation P1, in production.
13. UVM and Assertions
13.1 The transaction and the sequence that matters
class xhci_ctx_item extends uvm_sequence_item;
`uvm_object_utils(xhci_ctx_item)
rand bit cfg_valid;
rand bit my_drop;
rand bit my_add;
rand bit is_slot_ctx;
rand bit db_valid;
rand bit db_is_mine;
rand bit ep_error;
rand bit reset_ep;
// Commands are rare; doorbells are constant. That is what the traffic on
// a real controller looks like, and it is why the interesting command
// cases need their own sequences rather than waiting for the randomiser.
constraint c_realistic {
cfg_valid dist {0 := 3, 1 := 1};
db_valid dist {1 := 1, 0 := 1};
ep_error dist {0 := 11, 1 := 1};
reset_ep dist {0 := 9, 1 := 1};
is_slot_ctx dist {0 := 7, 1 := 1};
}
function new(string name = "xhci_ctx_item"); super.new(name); endfunction
function string convert2string();
return $sformatf("cfg=%0b D=%0b A=%0b slot=%0b db=%0b/%0b err=%0b rst=%0b",
cfg_valid, my_drop, my_add, is_slot_ctx, db_valid,
db_is_mine, ep_error, reset_ep);
endfunction
endclass
// THE sequence for this chapter. It reproduces what a driver does when it
// changes an endpoint's packet size: configure, start, run, and then issue
// ONE command with both the Drop and Add flags set. A correct controller
// leaves the endpoint configured; a controller that applies Add first leaves
// it disabled, and the device goes silent.
class reconfigure_endpoint_seq extends uvm_sequence #(xhci_ctx_item);
`uvm_object_utils(reconfigure_endpoint_seq)
function new(string name = "reconfigure_endpoint_seq"); super.new(name); endfunction
task body();
repeat (300) begin
xhci_ctx_item it;
// configure it
it = xhci_ctx_item::type_id::create("it");
start_item(it);
it.c_realistic.constraint_mode(0);
if (!it.randomize() with { cfg_valid == 1; my_add == 1; my_drop == 0;
is_slot_ctx == 0; db_valid == 0;
ep_error == 0; reset_ep == 0; })
`uvm_error("RAND", "configure randomize failed")
finish_item(it);
// start it
it = xhci_ctx_item::type_id::create("it");
start_item(it);
it.c_realistic.constraint_mode(0);
if (!it.randomize() with { cfg_valid == 0; db_valid == 1;
db_is_mine == 1; ep_error == 0;
reset_ep == 0; })
`uvm_error("RAND", "doorbell randomize failed")
finish_item(it);
// ...and re-initialise it in ONE command
it = xhci_ctx_item::type_id::create("it");
start_item(it);
it.c_realistic.constraint_mode(0);
if (!it.randomize() with { cfg_valid == 1; my_drop == 1; my_add == 1;
is_slot_ctx == 0; db_valid == 0;
ep_error == 0; reset_ep == 0; })
`uvm_error("RAND", "reinit randomize failed")
finish_item(it);
end
endtask
endclass
// An optimistic driver: it rings doorbells without reading back a state that
// hardware owns. Every one of these on a DISABLED or HALTED endpoint must do
// nothing -- which is the doorbell-is-a-hint rule, and the two mutations
// that break it.
class optimistic_doorbell_seq extends uvm_sequence #(xhci_ctx_item);
`uvm_object_utils(optimistic_doorbell_seq)
function new(string name = "optimistic_doorbell_seq"); super.new(name); endfunction
task body();
repeat (500) begin
xhci_ctx_item it = xhci_ctx_item::type_id::create("it");
start_item(it);
it.c_realistic.constraint_mode(0);
if (!it.randomize() with { cfg_valid == 0; db_valid == 1;
db_is_mine == 1; ep_error == 0;
reset_ep == 0; })
`uvm_error("RAND", "optimistic randomize failed")
finish_item(it);
end
endtask
endclass13.2 The scoreboard's three checks
class xhci_ctx_scoreboard extends uvm_scoreboard;
`uvm_component_utils(xhci_ctx_scoreboard)
uvm_analysis_imp #(xhci_ctx_mon_item, xhci_ctx_scoreboard) ap;
ep_state_e sb_state;
int unsigned n_reinit, n_db_ignored, n_illegal, n_halts;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
sb_state = EP_DISABLED;
endfunction
function void write(xhci_ctx_mon_item t);
ep_state_e prev = sb_state;
bit dropa = t.cfg_valid && t.my_drop && !t.is_slot_ctx;
bit adda = t.cfg_valid && t.my_add;
bit dbme = t.db_valid && t.db_is_mine;
// ---- THE property. Drop before Add: both flags is a RE-INIT. ----
if (dropa && adda) begin
if (!t.reinitialised)
`uvm_error("REINIT",
"drop+add was not reported as a re-initialisation")
n_reinit++;
end
// ---- A doorbell is a HINT, not a command ----
if (dbme && !t.cfg_valid && !t.ep_error && !t.reset_ep) begin
if ((prev == EP_DISABLED) || (prev == EP_HALTED)) begin
if (!t.db_ignored)
`uvm_error("DOORBELL",
$sformatf("a doorbell at a %s endpoint was not ignored -- software is allowed to be optimistic and hardware must re-check",
prev.name()))
n_db_ignored++;
end
end
// ---- D0 is never honoured ----
if (t.cfg_valid && t.is_slot_ctx && t.my_drop) begin
if (!t.illegal_drop)
`uvm_error("SLOT",
"a drop of the SLOT context was not flagged -- honouring it destroys the device's addressing")
n_illegal++;
end
// ---- Advance the scoreboard's own state, by the spec's rules ----
if (t.cfg_valid) begin
// drop FIRST, then add -- stated as the four cases, so the scoreboard
// cannot share an ordering bug with the design
if (dropa && adda) sb_state = EP_STOPPED;
else if (dropa) sb_state = EP_DISABLED;
else if (adda) sb_state = EP_STOPPED;
end else if (t.ep_error) begin
if (prev != EP_DISABLED) begin sb_state = EP_HALTED; n_halts++; end
end else if (t.reset_ep) begin
if (prev == EP_HALTED) sb_state = EP_STOPPED;
end else if (dbme) begin
if (prev == EP_STOPPED) sb_state = EP_RUNNING;
end
if (t.state !== sb_state)
`uvm_error("STATE", $sformatf("state=%s, scoreboard=%s",
t.state.name(), sb_state.name()))
if (t.usable !== (sb_state == EP_RUNNING))
`uvm_error("USABLE", "usable disagrees with the state");
endfunction
function void report_phase(uvm_phase phase);
`uvm_info("SB", $sformatf(
"reinits=%0d ignored-doorbells=%0d illegal-drops=%0d halts=%0d",
n_reinit, n_db_ignored, n_illegal, n_halts), UVM_LOW)
if (n_reinit == 0) `uvm_error("COVERAGE",
"no drop+add command was ever issued -- the ordering rule this chapter is about is untested")
if (n_db_ignored == 0) `uvm_error("COVERAGE",
"no doorbell was ever rung at a DISABLED or HALTED endpoint")
if (n_illegal == 0) `uvm_error("COVERAGE",
"D0 was never presented")
endfunction
endclass13.3 SystemVerilog assertions
module xhci_ep_context_sva
import xhci_ctx_pkg::*;
(
input logic clk,
input logic rst_n,
input logic cfg_valid,
input logic my_drop,
input logic my_add,
input logic is_slot_ctx,
input logic db_valid,
input logic db_is_mine,
input logic ep_error,
input logic reset_ep,
input ep_state_e state,
input logic usable,
input logic reinitialised,
input logic illegal_drop,
input logic db_ignored
);
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// ---- 1. THE property. Drop before Add: an endpoint named by BOTH masks
// ---- ends up CONFIGURED, not torn down.
property p_drop_before_add;
(cfg_valid && my_drop && my_add && !is_slot_ctx)
|=> (state == EP_STOPPED);
endproperty
a_drop_before_add : assert property (p_drop_before_add)
else $error("drop+add left the endpoint %s -- the adds were applied first, and a routine reconfiguration just disabled the endpoint",
state.name());
// ---- 2. A doorbell NEVER starts an endpoint that is not STOPPED. ----
property p_doorbell_is_a_hint;
(db_valid && db_is_mine && !cfg_valid && !ep_error && !reset_ep
&& (state != EP_STOPPED)) |=> $stable(state);
endproperty
a_doorbell_is_a_hint : assert property (p_doorbell_is_a_hint)
else $error("a doorbell changed the state of a %s endpoint -- it is a hint, not a command",
$past(state.name()));
// ---- 3. ...and it is reported as ignored, so the distinction between
// ---- "nothing to do" and "wrong question" is visible.
property p_ignored_is_reported;
db_ignored == (db_valid && db_is_mine && (state != EP_STOPPED));
endproperty
a_ignored_is_reported : assert property (p_ignored_is_reported);
// ---- 4. D0 is never honoured. ----
property p_slot_never_dropped;
(cfg_valid && is_slot_ctx && my_drop && !my_add) |=> $stable(state);
endproperty
a_slot_never_dropped : assert property (p_slot_never_dropped)
else $error("the SLOT context was dropped by a Configure Endpoint command -- the device's addressing is gone");
property p_illegal_drop_reported;
illegal_drop == (cfg_valid && is_slot_ctx && my_drop);
endproperty
a_illegal_drop_reported : assert property (p_illegal_drop_reported);
// ---- 5. Only a RUNNING endpoint is usable. ----
property p_usable_iff_running;
usable == (state == EP_RUNNING);
endproperty
a_usable_iff_running : assert property (p_usable_iff_running);
// ---- 6. A DISABLED endpoint cannot halt: it is running nothing. ----
property p_disabled_cannot_halt;
((state == EP_DISABLED) && ep_error && !cfg_valid) |=> $stable(state);
endproperty
a_disabled_cannot_halt : assert property (p_disabled_cannot_halt);
// ---- 7. Reset Endpoint clears a halt and ONLY a halt. ----
property p_reset_only_clears_a_halt;
(reset_ep && !cfg_valid && !ep_error && (state != EP_HALTED))
|=> $stable(state);
endproperty
a_reset_only_clears_a_halt :
assert property (p_reset_only_clears_a_halt)
else $error("Reset Endpoint changed a %s endpoint -- a transfer in progress was abandoned with no completion event",
$past(state.name()));
// ---- 8. A command outranks a doorbell in the same cycle. ----
property p_command_outranks_doorbell;
(cfg_valid && my_drop && !my_add && !is_slot_ctx && db_valid
&& db_is_mine) |=> (state == EP_DISABLED);
endproperty
a_command_outranks_doorbell :
assert property (p_command_outranks_doorbell);
// ---- Cover: the interesting populations were reached. ----
c_reinit : cover property ((reinitialised));
c_db_disabled : cover property ((db_valid && db_is_mine
&& (state == EP_DISABLED)));
c_db_halted : cover property ((db_valid && db_is_mine
&& (state == EP_HALTED)));
c_slot_drop : cover property ((illegal_drop));
endmodule
bind xhci_ep_context xhci_ep_context_sva u_sva (.*);14. Common Misconceptions
"Software can edit an endpoint context." Hardware owns it and writes to it. That is why there are two contexts and a command to transfer between them.
"Setting Drop and Add for the same endpoint is contradictory." It is a re-initialisation, and it is the documented way to change an endpoint's packet size or ring pointer atomically.
"The order of Drop and Add does not matter — they are different endpoints usually." Usually. The one case where it matters is the one a driver reaches on every reconfiguration.
"D0 drops the slot." D0 in a Configure Endpoint command is illegal and ignored. The slot is torn down by Disable Slot.
"A doorbell starts an endpoint." It says there may be work. It starts a STOPPED endpoint and does nothing at all to a DISABLED or HALTED one.
"Ringing a doorbell at a halted endpoint is a driver bug." It is legal and expected — software rings after enqueueing without reading back state it does not own. Hardware re-checks.
"Reset Endpoint puts an endpoint back to idle." It clears a halt, and only a halt. Applying it to a running endpoint abandons a transfer with no completion event.
"A disabled endpoint can error." It is not running anything that could fail.
15. Exercises
1. Swap the two assignments (mutation P1) and predict which of the eight safety properties fires first. Then explain why P1 scores less than P4 despite being the more subtle bug.
2. P3 (a doorbell starts a HALTED endpoint) scores half of P2 (a doorbell starts a DISABLED one). Show that the ratio is the ratio of state visits, and construct a stimulus that inverts it.
3. Extend the block to a full 32-context slot, with drop_flags[31:0] and add_flags[31:0]. What is the exhaustive state space now, and which of the eight SVA properties still hold unchanged?
4. Property 4 uses !my_add in its antecedent. Remove that term, run it against the correct design, and explain the failure. What does that tell you about writing a property for an input combination the specification says is illegal?
5. The design reports db_ignored as an output. Argue for and against exposing it, given that a correct system rings ignored doorbells constantly and the counter will therefore always be large.
6. reinitialised is an output that no other logic consumes. Is it dead? Apply the dead-versus-unreachable test from this curriculum's mutation discipline, and say what would change if it were removed.
16. Summary
| Idea | Why it matters |
|---|---|
| Hardware owns the endpoint context | software cannot read-modify-write it |
| Two contexts: Input and Output | one identified moment of transfer |
| Drop flags are applied before Add flags | so drop+add is an atomic re-initialisation |
| Reverse them and the same command disables the endpoint | two lines, and a device that goes silent |
| D0 is not a legal drop | the slot holds the address; Disable Slot tears it down |
| A doorbell is a hint | it starts a STOPPED endpoint and nothing else |
| Three states mean "not transferring" | and each needs a different command |
| Reset Endpoint clears a halt, and only a halt | otherwise it abandons a live transfer |
| A command outranks a doorbell | the command says whether the endpoint exists |
| 1024-transition exhaustive verification | every state × every input combination |
| 7 mutations, all killed in 3 languages | P1 is two lines swapped |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o ec_v.out ec_v.v ec_v_tb.v && ./ec_v.out |
| SystemVerilog | iverilog -g2012 -o ec_sv.out ec_sv.sv ec_sv_tb.sv && ./ec_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a ec_vhdl.vhd ec_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_ec_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_ec_vhdl |
| One mutation | iverilog -g2005 -DMUT_P1 -o mm ec_v_mut.v ec_v_tb.v && ./mm |
All three implementations pass with 0 errors: 1024 of 1024 exhaustive transitions, 40 000 randomised cycles, every endpoint state reached and asserted reached.
17. Module 22 Complete
Four chapters, four synthesisable blocks, twelve implementations, twenty-eight mutations — all killed in all three languages.
| Chapter | Block | The structural idea |
|---|---|---|
| 22.1 | ehci_async_walker | a ring with no end; termination is a lap with no work |
| 22.2 | xhci_trb_ring | one bit per entry replaces every shared pointer |
| 22.3 | usb_frame_scheduler | you cannot start what you cannot finish |
| 22.4 | xhci_ep_context | ownership transfers at a command, and drops go first |
Read together, these four are about a single problem that the device side never has: the host controller and the driver are two agents editing the same memory at the same time, and neither can stop the other.
Every mechanism in the module is an answer to it. EHCI makes its list circular so that an edit is never observed half-done, and pays for it by needing the Reclamation flag to know when to stop. xHCI replaces the shared pointers with a cycle bit, so that nothing is shared but the array itself. The scheduler commits to a transaction before it starts, because it cannot take it back. And the context manager transfers ownership at a single command boundary, because there is no lock that spans a PCIe link.
None of these is a synchronisation primitive. There is no mutex, no atomic, no barrier anywhere in a USB host controller. What there is instead, four times over, is a structure in which the dangerous interleavings cannot be expressed — which is the only kind of synchronisation available when the other agent is a piece of silicon that will not wait for you.
Continue learning
Related tutorials
- Related topic
xHCI Overview
xHCI shares a ring between software and hardware using one Cycle bit per TRB and no pointer exchange at all — and an unowned entry holds the previous lap's complete, plausible descriptor.
- Related topic
EHCI Overview
EHCI has no command queue — it is a DMA engine walking a linked list software edits underneath it, around a ring with no end, where a link pointer is a packed word and the terminate bit must be read first.
- Related topic
Host-Side Scheduling
A USB transaction cannot be stopped once its token goes out, so the scheduler must ask whether it will finish before it starts — and the periodic reserve exists to protect bulk traffic, not to limit isochronous.
- Related topic
USB Controllers on SoC
DWC2, MUSB and xHCI differ in a hundred mechanical ways that do not matter and one architectural way that does — whether endpoints own their packet buffers or share a pool.
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.
