USB · Module 21
Controller FSMs
A bus reset arrives in any state and always returns to Default — and returning to Default is not enough, because every endpoint's toggle, halt, buffer and pointer holds session state that must be flushed with it.
Four chapters have built four blocks. 21.1 holds a toggle and a halt per endpoint. 21.2 holds buffered packets. 21.3 answers requests. 21.4 holds two pointers into a buffer.
This chapter is the state machine above all of them — and the reason it exists is that one signal has to reach down into every one.
1. The Six States
| State | Meaning | What works |
|---|---|---|
| ATTACHED | plugged in, no VBUS | nothing |
| POWERED | VBUS present, not yet reset | nothing — the device waits |
| DEFAULT | a bus reset has happened | address 0, endpoint 0 only |
| ADDRESS | SET_ADDRESS gave it a unique address | still endpoint 0 only |
| CONFIGURED | SET_CONFIGURATION selected one | now the other endpoints exist |
| SUSPENDED | the bus has been idle for over 3 ms | nothing, until resume |
Enumeration walks the first five in order, and the walk is short: power up, get reset, get an address, get configured. A device that reaches CONFIGURED is a working device.
2. A Bus Reset Arrives in Any State
Here is the rule that shapes the block:
A BUS RESET CAN ARRIVE IN ANY STATE,
AND ALWAYS RETURNS TO DEFAULT.Not "usually". Not "from the states where it makes sense". The host may reset the bus at any moment — because a driver was reloaded, because a user opened a laptop lid, because something further up the tree went wrong and the hub reset its whole downstream port. A CONFIGURED device happily moving data becomes a DEFAULT device at address 0, with no warning and no negotiation.
3. Returning to Default Is Not Enough
Now the part that gets missed, and the reason this chapter sits at the end of the module rather than the beginning.
The device's state is not just the six values above. It also includes, spread across the four blocks this module has built:
| Held where | What | From |
|---|---|---|
| every endpoint | the DATA toggle | 21.1 |
| every endpoint | the HALT condition | 21.1 |
| every endpoint | buffered packets and their lengths | 21.2 |
| every endpoint | the committed and provisional pointers | 21.4 |
A bus reset must flush all of it.
So the design has one output, ep_flush, that goes to every endpoint and clears everything listed above.
4. SET_CONFIGURATION Flushes Too
Same reasoning, narrower scope. Selecting a configuration re-creates the endpoints: a different configuration may have a different set of them, with different packet sizes and different types. So their toggles restart at DATA0 and their halts are cleared.
A host that reconfigures a device without re-enumerating it depends on this, and the failure looks identical to the reset case — one packet lost at the start of the new configuration.
One signal, four blocks
5. Suspend Is Orthogonal, and Must Remember
The last structural decision. Suspend is not a seventh place in the sequence — it is something that happens to a state.
A configured device whose bus goes idle suspends. When the bus wakes up, the device is configured again. It does not re-enumerate; it does not lose its address; the host expects to carry on exactly where it left off.
CONFIGURED --suspend--> SUSPENDED --resume--> CONFIGURED
ADDRESS --suspend--> SUSPENDED --resume--> ADDRESS
DEFAULT --suspend--> SUSPENDED --resume--> DEFAULTSo the machine has to save the state it suspended from and return to it. A resume that always lands in DEFAULT forces a full re-enumeration after every idle period — which a laptop produces constantly. The device works; it just takes a second to wake up, every single time, and the bug is reported as "slow" rather than as "broken". Mutation J2, at 143 000 failures.
The device state machine
6. What We Are Building
usb_device_fsm
inputs outputs
------ -------
vbus state 6 states
bus_reset dev_address [6:0]
suspend_req dev_config [3:0]
resume_event ep_flush -> EVERY endpoint
set_address / address_val endpoints_usable
set_config / config_val suspended
saved_state what resume returns to
n_resets / n_flushes / n_suspends / n_resumes / n_configuredThe priority order in the next-state logic is itself part of the specification:
| Priority | Condition | Why it outranks what follows |
|---|---|---|
| 1 | !vbus | a device with no power has no state to preserve |
| 2 | bus_reset | the host has stopped believing whatever the device thought it was |
| 3 | in ATTACHED | VBUS just arrived: become POWERED |
| 4 | in SUSPENDED | only a resume (or 1 and 2 above) leaves |
| 5 | suspend_req | an idle bus suspends from wherever we are |
| 6 | SET_ADDRESS | only legal in DEFAULT or ADDRESS |
| 7 | SET_CONFIGURATION | only legal in ADDRESS or CONFIGURED |
7. Verilog-2005 Implementation
// usb_device_fsm -- the state machine above everything else in this module,
// and the reset rule that has to reach down through all of it.
//
// THE SIX STATES
//
// ATTACHED plugged in, no VBUS. Nothing is powered.
// POWERED VBUS present. The device is alive and waiting to be reset.
// DEFAULT a bus reset has happened. The device answers at ADDRESS 0
// and only on endpoint 0.
// ADDRESS SET_ADDRESS gave it a unique address. Still only endpoint 0.
// CONFIGURED SET_CONFIGURATION selected a configuration. NOW the other
// endpoints exist.
// SUSPENDED the bus went idle for more than 3 ms.
//
// THE RULE THAT SHAPES THE BLOCK
//
// A BUS RESET CAN ARRIVE IN ANY STATE, AND ALWAYS RETURNS TO DEFAULT.
//
// Not "usually". Not "from the states where it makes sense". The host may
// reset the bus at any moment -- because a driver was reloaded, because the
// user opened a lid, because something further up the tree went wrong -- and
// every device below it must return to DEFAULT with address 0.
//
// AND RETURNING TO DEFAULT IS NOT ENOUGH
//
// This is the part that gets missed. The device's state is not just the six
// values above. It also includes, in every endpoint:
//
// * the DATA toggle (chapter 21.1)
// * the HALT condition (chapter 21.1)
// * whatever is buffered (chapters 21.2 and 21.4)
//
// A bus reset must flush ALL of it. A device that resets its top-level state
// and leaves a toggle at DATA1 re-enumerates perfectly and then drops the
// first packet of the new session, because the host restarts at DATA0 and
// the device is expecting DATA1. One packet. Silently. Only after a reset
// that happened to land while the toggle was odd.
//
// SET_CONFIGURATION FLUSHES TOO
//
// Same reasoning, narrower scope: selecting a configuration re-creates the
// endpoints, so their toggles restart at DATA0 and their halts are cleared.
// A host that reconfigures a device without re-enumerating it depends on
// this, and the failure looks identical to the reset case.
//
// SUSPEND IS ORTHOGONAL, AND MUST REMEMBER
//
// Suspend is not a seventh place in the sequence -- it is something that
// happens TO a state. A configured device that suspends and resumes is
// configured again; it does not have to re-enumerate. So the machine saves
// the state it suspended FROM and returns to it:
//
// CONFIGURED --suspend--> SUSPENDED --resume--> CONFIGURED
// ADDRESS --suspend--> SUSPENDED --resume--> ADDRESS
//
// A resume that always lands in DEFAULT forces a re-enumeration after every
// idle period, which a laptop does constantly. The device works; it just
// takes a second to wake up, every single time.
module usb_device_fsm (
input wire clk,
input wire rst_n,
input wire vbus, // bus power is present
input wire bus_reset, // SE0 for long enough: a bus reset
input wire suspend_req, // the bus has been idle > 3 ms
input wire resume_event, // host resume or remote wakeup
input wire set_address, // a SET_ADDRESS request completed
input wire [6:0] address_val,
input wire set_config, // a SET_CONFIGURATION request completed
input wire [3:0] config_val,
output wire [2:0] state,
output wire [6:0] dev_address,
output wire [3:0] dev_config,
output wire ep_flush, // flush EVERY endpoint's toggle and halt
output wire endpoints_usable,
output wire suspended,
output wire [2:0] saved_state, // what SUSPENDED will return to
output reg [31:0] n_resets,
output reg [31:0] n_flushes,
output reg [31:0] n_suspends,
output reg [31:0] n_resumes,
output reg [31:0] n_configured
);
localparam [2:0] S_ATTACHED = 3'd0,
S_POWERED = 3'd1,
S_DEFAULT = 3'd2,
S_ADDRESS = 3'd3,
S_CONFIGURED = 3'd4,
S_SUSPENDED = 3'd5;
reg [2:0] st_r;
reg [2:0] saved_r;
reg [6:0] addr_r;
reg [3:0] cfg_r;
assign state = st_r;
assign saved_state = saved_r;
assign dev_address = addr_r;
assign dev_config = cfg_r;
assign suspended = (st_r == S_SUSPENDED);
// Only a CONFIGURED device has endpoints beyond endpoint 0. In DEFAULT and
// ADDRESS the device exists and answers, but only on the control endpoint.
assign endpoints_usable = (st_r == S_CONFIGURED);
// A SET_ADDRESS is only meaningful in DEFAULT or ADDRESS; a
// SET_CONFIGURATION only in ADDRESS or CONFIGURED. Anywhere else the
// request should not have reached this block, and acting on it would move
// the machine somewhere the host does not believe it is.
wire addr_ok = set_address && ((st_r == S_DEFAULT) || (st_r == S_ADDRESS));
wire cfg_ok = set_config && ((st_r == S_ADDRESS) || (st_r == S_CONFIGURED));
// ---- THE FLUSH. Everything below this block -- every endpoint's toggle,
// ---- every halt, every buffered byte -- is reset by this one signal.
//
// A bus reset flushes because the whole session restarts. A
// SET_CONFIGURATION flushes because the endpoints themselves are
// re-created. Both are required, and forgetting either costs exactly one
// silently-dropped packet at the start of the next session.
assign ep_flush = vbus && (bus_reset || cfg_ok);
// ---- The next state, as one priority chain ----
//
// Order matters and is the design: losing VBUS outranks everything,
// because a device with no power has no state to preserve. A bus reset
// outranks every request, because the host has stopped believing whatever
// the device thought it was.
reg [2:0] st_n;
reg [2:0] saved_n;
reg [6:0] addr_n;
reg [3:0] cfg_n;
always @* begin
st_n = st_r;
saved_n = saved_r;
addr_n = addr_r;
cfg_n = cfg_r;
if (!vbus) begin
// No power: back to ATTACHED, and nothing is retained.
st_n = S_ATTACHED;
saved_n = S_POWERED;
addr_n = 7'd0;
cfg_n = 4'd0;
end else if (bus_reset) begin
// FROM ANY STATE. Address 0, no configuration, and ep_flush above
// clears every endpoint.
st_n = S_DEFAULT;
saved_n = S_DEFAULT;
addr_n = 7'd0;
cfg_n = 4'd0;
end else if (st_r == S_ATTACHED) begin
// VBUS just arrived. Powered, but not yet reset: no address, no
// configuration, and nothing may be transferred until a reset.
st_n = S_POWERED;
saved_n = S_POWERED;
end else if (st_r == S_SUSPENDED) begin
// Resume returns to the state we suspended FROM, not to DEFAULT.
if (resume_event) st_n = saved_r;
end else if (suspend_req) begin
// Remember where we came from. This is the whole of "suspend is
// orthogonal".
saved_n = st_r;
st_n = S_SUSPENDED;
end else if (addr_ok) begin
// SET_ADDRESS(0) is legal and means "go back to DEFAULT" -- the host
// uses it to de-address a device without resetting the bus.
addr_n = address_val;
st_n = (address_val != 7'd0) ? S_ADDRESS : S_DEFAULT;
end else if (cfg_ok) begin
// SET_CONFIGURATION(0) likewise un-configures without de-addressing.
cfg_n = config_val;
st_n = (config_val != 4'd0) ? S_CONFIGURED : S_ADDRESS;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_r <= S_ATTACHED;
saved_r <= S_POWERED;
addr_r <= 7'd0;
cfg_r <= 4'd0;
n_resets <= 32'd0;
n_flushes <= 32'd0;
n_suspends <= 32'd0;
n_resumes <= 32'd0;
n_configured <= 32'd0;
end else begin
st_r <= st_n;
saved_r <= saved_n;
addr_r <= addr_n;
cfg_r <= cfg_n;
if (vbus && bus_reset) n_resets <= n_resets + 32'd1;
if (ep_flush) n_flushes <= n_flushes + 32'd1;
if ((st_n == S_SUSPENDED) && (st_r != S_SUSPENDED))
n_suspends <= n_suspends + 32'd1;
if ((st_r == S_SUSPENDED) && (st_n != S_SUSPENDED))
n_resumes <= n_resumes + 32'd1;
if ((st_n == S_CONFIGURED) && (st_r != S_CONFIGURED))
n_configured <= n_configured + 32'd1;
end
end
endmoduleSET_ADDRESS(0) and SET_CONFIGURATION(0) are legal requests, not errors. A host uses them to de-address or un-configure a device without resetting the bus, and each moves the machine backwards one step. A design that treats a zero argument as "no change" leaves the device claiming a state the host has just taken away from it — mutations J3 and J4.
8. SystemVerilog Implementation
// usb_device_fsm -- the state machine above everything else in this module,
// and the reset rule that has to reach down through all of it.
//
// THE SIX STATES
//
// ATTACHED plugged in, no VBUS. Nothing is powered.
// POWERED VBUS present. The device is alive and waiting to be reset.
// DEFAULT a bus reset has happened. The device answers at ADDRESS 0
// and only on endpoint 0.
// ADDRESS SET_ADDRESS gave it a unique address. Still only endpoint 0.
// CONFIGURED SET_CONFIGURATION selected a configuration. NOW the other
// endpoints exist.
// SUSPENDED the bus went idle for more than 3 ms.
//
// THE RULE THAT SHAPES THE BLOCK
//
// A BUS RESET CAN ARRIVE IN ANY STATE, AND ALWAYS RETURNS TO DEFAULT.
//
// Not "usually". Not "from the states where it makes sense". The host may
// reset the bus at any moment -- because a driver was reloaded, because the
// user opened a lid, because something further up the tree went wrong -- and
// every device below it must return to DEFAULT with address 0.
//
// AND RETURNING TO DEFAULT IS NOT ENOUGH
//
// This is the part that gets missed. The device's state is not just the six
// values above. It also includes, in every endpoint:
//
// * the DATA toggle (chapter 21.1)
// * the HALT condition (chapter 21.1)
// * whatever is buffered (chapters 21.2 and 21.4)
//
// A bus reset must flush ALL of it. A device that resets its top-level state
// and leaves a toggle at DATA1 re-enumerates perfectly and then drops the
// first packet of the new session, because the host restarts at DATA0 and
// the device is expecting DATA1. One packet. Silently. Only after a reset
// that happened to land while the toggle was odd.
//
// SET_CONFIGURATION FLUSHES TOO
//
// Same reasoning, narrower scope: selecting a configuration re-creates the
// endpoints, so their toggles restart at DATA0 and their halts are cleared.
// A host that reconfigures a device without re-enumerating it depends on
// this, and the failure looks identical to the reset case.
//
// SUSPEND IS ORTHOGONAL, AND MUST REMEMBER
//
// Suspend is not a seventh place in the sequence -- it is something that
// happens TO a state. A configured device that suspends and resumes is
// configured again; it does not have to re-enumerate. So the machine saves
// the state it suspended FROM and returns to it:
//
// CONFIGURED --suspend--> SUSPENDED --resume--> CONFIGURED
// ADDRESS --suspend--> SUSPENDED --resume--> ADDRESS
//
// A resume that always lands in DEFAULT forces a re-enumeration after every
// idle period, which a laptop does constantly. The device works; it just
// takes a second to wake up, every single time.
package usb_devfsm_pkg;
// The six device states of USB 2.0 section 9.1. They are an enumeration
// rather than an encoding because SUSPENDED is not the seventh step of a
// sequence -- it is something that happens TO one of the others -- and a
// type keeps that visible at every use.
typedef enum logic [2:0] {
S_ATTACHED = 3'd0, // plugged in, no VBUS
S_POWERED = 3'd1, // VBUS present, awaiting a reset
S_DEFAULT = 3'd2, // reset done: address 0, endpoint 0 only
S_ADDRESS = 3'd3, // addressed, endpoint 0 only
S_CONFIGURED = 3'd4, // configured: the other endpoints exist
S_SUSPENDED = 3'd5 // bus idle > 3 ms
} dev_state_e;
endpackage
module usb_device_fsm
import usb_devfsm_pkg::*;
(
input logic clk,
input logic rst_n,
input logic vbus, // bus power is present
input logic bus_reset, // SE0 for long enough: a bus reset
input logic suspend_req, // the bus has been idle > 3 ms
input logic resume_event, // host resume or remote wakeup
input logic set_address, // a SET_ADDRESS request completed
input logic [6:0] address_val,
input logic set_config, // a SET_CONFIGURATION request completed
input logic [3:0] config_val,
output dev_state_e state,
output logic [6:0] dev_address,
output logic [3:0] dev_config,
output logic ep_flush, // flush EVERY endpoint's toggle and halt
output logic endpoints_usable,
output logic suspended,
output dev_state_e saved_state, // what SUSPENDED will return to
output logic [31:0] n_resets,
output logic [31:0] n_flushes,
output logic [31:0] n_suspends,
output logic [31:0] n_resumes,
output logic [31:0] n_configured
);
dev_state_e st_r, saved_r;
logic [6:0] addr_r;
logic [3:0] cfg_r;
assign state = st_r;
assign saved_state = saved_r;
assign dev_address = addr_r;
assign dev_config = cfg_r;
assign suspended = (st_r == S_SUSPENDED);
// Only a CONFIGURED device has endpoints beyond endpoint 0. In DEFAULT and
// ADDRESS the device exists and answers, but only on the control endpoint.
assign endpoints_usable = (st_r == S_CONFIGURED);
// A SET_ADDRESS is only meaningful in DEFAULT or ADDRESS; a
// SET_CONFIGURATION only in ADDRESS or CONFIGURED. Anywhere else the
// request should not have reached this block, and acting on it would move
// the machine somewhere the host does not believe it is.
logic addr_ok, cfg_ok;
assign addr_ok = set_address && ((st_r == S_DEFAULT) || (st_r == S_ADDRESS));
assign cfg_ok = set_config && ((st_r == S_ADDRESS) || (st_r == S_CONFIGURED));
// ---- THE FLUSH. Everything below this block -- every endpoint's toggle,
// ---- every halt, every buffered byte -- is reset by this one signal.
//
// A bus reset flushes because the whole session restarts. A
// SET_CONFIGURATION flushes because the endpoints themselves are
// re-created. Both are required, and forgetting either costs exactly one
// silently-dropped packet at the start of the next session.
assign ep_flush = vbus && (bus_reset || cfg_ok);
// ---- The next state, as one priority chain ----
//
// Order matters and is the design: losing VBUS outranks everything,
// because a device with no power has no state to preserve. A bus reset
// outranks every request, because the host has stopped believing whatever
// the device thought it was.
dev_state_e st_n, saved_n;
logic [6:0] addr_n;
logic [3:0] cfg_n;
always_comb begin
st_n = st_r;
saved_n = saved_r;
addr_n = addr_r;
cfg_n = cfg_r;
if (!vbus) begin
// No power: back to ATTACHED, and nothing is retained.
st_n = S_ATTACHED;
saved_n = S_POWERED;
addr_n = 7'd0;
cfg_n = 4'd0;
end else if (bus_reset) begin
// FROM ANY STATE. Address 0, no configuration, and ep_flush above
// clears every endpoint.
st_n = S_DEFAULT;
saved_n = S_DEFAULT;
addr_n = 7'd0;
cfg_n = 4'd0;
end else if (st_r == S_ATTACHED) begin
// VBUS just arrived. Powered, but not yet reset: no address, no
// configuration, and nothing may be transferred until a reset.
st_n = S_POWERED;
saved_n = S_POWERED;
end else if (st_r == S_SUSPENDED) begin
// Resume returns to the state we suspended FROM, not to DEFAULT.
if (resume_event) st_n = saved_r;
end else if (suspend_req) begin
// Remember where we came from. This is the whole of "suspend is
// orthogonal".
saved_n = st_r;
st_n = S_SUSPENDED;
end else if (addr_ok) begin
// SET_ADDRESS(0) is legal and means "go back to DEFAULT" -- the host
// uses it to de-address a device without resetting the bus.
// Written as if/else rather than a ternary: an enum-valued ternary
// needs an explicit cast in Icarus, and the cast would hide which of
// the two states is the zero case.
addr_n = address_val;
if (address_val != 7'd0) st_n = S_ADDRESS;
else st_n = S_DEFAULT;
end else if (cfg_ok) begin
// SET_CONFIGURATION(0) likewise un-configures without de-addressing.
cfg_n = config_val;
if (config_val != 4'd0) st_n = S_CONFIGURED;
else st_n = S_ADDRESS;
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_r <= S_ATTACHED;
saved_r <= S_POWERED;
addr_r <= 7'd0;
cfg_r <= 4'd0;
n_resets <= '0;
n_flushes <= '0;
n_suspends <= '0;
n_resumes <= '0;
n_configured <= '0;
end else begin
st_r <= st_n;
saved_r <= saved_n;
addr_r <= addr_n;
cfg_r <= cfg_n;
if (vbus && bus_reset) n_resets <= n_resets + 1;
if (ep_flush) n_flushes <= n_flushes + 1;
if ((st_n == S_SUSPENDED) && (st_r != S_SUSPENDED))
n_suspends <= n_suspends + 1;
if ((st_r == S_SUSPENDED) && (st_n != S_SUSPENDED))
n_resumes <= n_resumes + 1;
if ((st_n == S_CONFIGURED) && (st_r != S_CONFIGURED))
n_configured <= n_configured + 1;
end
end
endmodule9. VHDL-2008 Implementation
-- usb_device_fsm -- the state machine above everything else in this module,
-- and the reset rule that has to reach down through all of it.
--
-- THE SIX STATES
--
-- ATTACHED plugged in, no VBUS. Nothing is powered.
-- POWERED VBUS present. The device is alive and waiting to be reset.
-- DEFAULT a bus reset has happened. The device answers at ADDRESS 0
-- and only on endpoint 0.
-- ADDRESS SET_ADDRESS gave it a unique address. Still only endpoint 0.
-- CONFIGURED SET_CONFIGURATION selected a configuration. NOW the other
-- endpoints exist.
-- SUSPENDED the bus went idle for more than 3 ms.
--
-- THE RULE THAT SHAPES THE BLOCK
--
-- A BUS RESET CAN ARRIVE IN ANY STATE, AND ALWAYS RETURNS TO DEFAULT.
--
-- Not "usually", and not "from the states where it makes sense". The host
-- may reset the bus at any moment, and every device below it must return to
-- DEFAULT with address 0.
--
-- AND RETURNING TO DEFAULT IS NOT ENOUGH
--
-- The device's state is not just the six values above. It also includes, in
-- every endpoint:
--
-- * the DATA toggle (chapter 21.1)
-- * the HALT condition (chapter 21.1)
-- * whatever is buffered (chapters 21.2 and 21.4)
--
-- A bus reset must flush ALL of it. A device that resets its top-level state
-- and leaves a toggle at DATA1 re-enumerates perfectly and then drops the
-- first packet of the new session, because the host restarts at DATA0 and
-- the device is expecting DATA1. One packet, silently, and only after a
-- reset that happened to land while the toggle was odd.
--
-- SET_CONFIGURATION FLUSHES TOO
--
-- Selecting a configuration re-creates the endpoints, so their toggles
-- restart at DATA0 and their halts are cleared. A host that reconfigures a
-- device without re-enumerating it depends on this.
--
-- SUSPEND IS ORTHOGONAL, AND MUST REMEMBER
--
-- Suspend is not a seventh place in the sequence -- it is something that
-- happens TO a state. A configured device that suspends and resumes is
-- configured again:
--
-- CONFIGURED --suspend--> SUSPENDED --resume--> CONFIGURED
-- ADDRESS --suspend--> SUSPENDED --resume--> ADDRESS
--
-- A resume that always lands in DEFAULT forces a re-enumeration after every
-- idle period, which a laptop does constantly.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package usb_devfsm_pkg is
-- An enumeration rather than an encoding because SUSPENDED is not the
-- seventh step of a sequence -- it is something that happens TO one of the
-- others -- and a type keeps that visible at every use.
type dev_state_t is (
S_ATTACHED, -- plugged in, no VBUS
S_POWERED, -- VBUS present, awaiting a reset
S_DEFAULT, -- reset done: address 0, endpoint 0 only
S_ADDRESS, -- addressed, endpoint 0 only
S_CONFIGURED, -- configured: the other endpoints exist
S_SUSPENDED -- bus idle > 3 ms
);
function st_code(s : dev_state_t) return std_logic_vector;
end package;
package body usb_devfsm_pkg is
function st_code(s : dev_state_t) return std_logic_vector is
begin
return std_logic_vector(to_unsigned(dev_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.usb_devfsm_pkg.all;
entity usb_device_fsm is
port (
clk : in std_logic;
rst_n : in std_logic;
vbus : in std_logic; -- bus power is present
bus_reset : in std_logic; -- SE0 for long enough
suspend_req : in std_logic; -- the bus has been idle > 3 ms
resume_event : in std_logic; -- host resume or remote wakeup
set_address : in std_logic; -- a SET_ADDRESS completed
address_val : in std_logic_vector(6 downto 0);
set_config : in std_logic; -- a SET_CONFIGURATION completed
config_val : in std_logic_vector(3 downto 0);
state : out std_logic_vector(2 downto 0);
dev_address : out std_logic_vector(6 downto 0);
dev_config : out std_logic_vector(3 downto 0);
ep_flush : out std_logic; -- flush EVERY endpoint
endpoints_usable : out std_logic;
suspended : out std_logic;
saved_state : out std_logic_vector(2 downto 0);
n_resets : out std_logic_vector(31 downto 0);
n_flushes : out std_logic_vector(31 downto 0);
n_suspends : out std_logic_vector(31 downto 0);
n_resumes : out std_logic_vector(31 downto 0);
n_configured : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of usb_device_fsm is
signal st_r, saved_r : dev_state_t := S_ATTACHED;
signal addr_r : unsigned(6 downto 0) := (others => '0');
signal cfg_r : unsigned(3 downto 0) := (others => '0');
signal st_n, saved_n : dev_state_t;
signal addr_n : unsigned(6 downto 0);
signal cfg_n : unsigned(3 downto 0);
signal addr_ok, cfg_ok, flush_s : std_logic;
signal rst_c, fl_c, sp_c, rs_c, cf_c : unsigned(31 downto 0)
:= (others => '0');
begin
state <= st_code(st_r);
saved_state <= st_code(saved_r);
dev_address <= std_logic_vector(addr_r);
dev_config <= std_logic_vector(cfg_r);
suspended <= '1' when st_r = S_SUSPENDED else '0';
-- Only a CONFIGURED device has endpoints beyond endpoint 0. In DEFAULT and
-- ADDRESS the device exists and answers, but only on the control endpoint.
endpoints_usable <= '1' when st_r = S_CONFIGURED else '0';
-- A SET_ADDRESS is only meaningful in DEFAULT or ADDRESS; a
-- SET_CONFIGURATION only in ADDRESS or CONFIGURED. Anywhere else the
-- request should not have reached this block.
addr_ok <= '1' when (set_address = '1'
and (st_r = S_DEFAULT or st_r = S_ADDRESS)) else '0';
cfg_ok <= '1' when (set_config = '1'
and (st_r = S_ADDRESS or st_r = S_CONFIGURED)) else '0';
-- ---- THE FLUSH. Everything below this block -- every endpoint's toggle,
-- ---- every halt, every buffered byte -- is reset by this one signal.
flush_s <= '1' when (vbus = '1' and (bus_reset = '1' or cfg_ok = '1'))
else '0';
ep_flush <= flush_s;
-- ---- The next state, as one priority chain ----
--
-- Order matters and is the design: losing VBUS outranks everything,
-- because a device with no power has no state to preserve. A bus reset
-- outranks every request, because the host has stopped believing whatever
-- the device thought it was.
nextstate : process (st_r, saved_r, addr_r, cfg_r, vbus, bus_reset,
suspend_req, resume_event, addr_ok, cfg_ok,
address_val, config_val)
begin
st_n <= st_r;
saved_n <= saved_r;
addr_n <= addr_r;
cfg_n <= cfg_r;
if vbus = '0' then
-- No power: back to ATTACHED, and nothing is retained.
st_n <= S_ATTACHED;
saved_n <= S_POWERED;
addr_n <= (others => '0');
cfg_n <= (others => '0');
elsif bus_reset = '1' then
-- FROM ANY STATE. Address 0, no configuration, and ep_flush above
-- clears every endpoint.
st_n <= S_DEFAULT;
saved_n <= S_DEFAULT;
addr_n <= (others => '0');
cfg_n <= (others => '0');
elsif st_r = S_ATTACHED then
-- VBUS just arrived. Powered, but not yet reset.
st_n <= S_POWERED;
saved_n <= S_POWERED;
elsif st_r = S_SUSPENDED then
-- Resume returns to the state we suspended FROM, not to DEFAULT.
if resume_event = '1' then
st_n <= saved_r;
end if;
elsif suspend_req = '1' then
-- Remember where we came from. This is the whole of "suspend is
-- orthogonal".
saved_n <= st_r;
st_n <= S_SUSPENDED;
elsif addr_ok = '1' then
-- SET_ADDRESS(0) is legal and means "go back to DEFAULT".
addr_n <= unsigned(address_val);
if unsigned(address_val) /= 0 then
st_n <= S_ADDRESS;
else
st_n <= S_DEFAULT;
end if;
elsif cfg_ok = '1' then
-- SET_CONFIGURATION(0) likewise un-configures without de-addressing.
cfg_n <= unsigned(config_val);
if unsigned(config_val) /= 0 then
st_n <= S_CONFIGURED;
else
st_n <= S_ADDRESS;
end if;
end if;
end process;
regs : process (clk, rst_n)
begin
if rst_n = '0' then
st_r <= S_ATTACHED;
saved_r <= S_POWERED;
addr_r <= (others => '0');
cfg_r <= (others => '0');
rst_c <= (others => '0');
fl_c <= (others => '0');
sp_c <= (others => '0');
rs_c <= (others => '0');
cf_c <= (others => '0');
elsif rising_edge(clk) then
st_r <= st_n;
saved_r <= saved_n;
addr_r <= addr_n;
cfg_r <= cfg_n;
if vbus = '1' and bus_reset = '1' then
rst_c <= rst_c + 1;
end if;
if flush_s = '1' then
fl_c <= fl_c + 1;
end if;
if st_n = S_SUSPENDED and st_r /= S_SUSPENDED then
sp_c <= sp_c + 1;
end if;
if st_r = S_SUSPENDED and st_n /= S_SUSPENDED then
rs_c <= rs_c + 1;
end if;
if st_n = S_CONFIGURED and st_r /= S_CONFIGURED then
cf_c <= cf_c + 1;
end if;
end if;
end process;
n_resets <= std_logic_vector(rst_c);
n_flushes <= std_logic_vector(fl_c);
n_suspends <= std_logic_vector(sp_c);
n_resumes <= std_logic_vector(rs_c);
n_configured <= std_logic_vector(cf_c);
end architecture;10. Seeing the Reset Reach Down
A bus reset from CONFIGURED, and the flush that has to go with it
usb_device_fsm — a bus reset from the working state
10 cyclesep_flush pulses three times in ten cycles, and every one of them matters. Cycle 1 and 5 are bus resets; cycle 3 is the SET_CONFIGURATION that re-creates the endpoints.
11. The Testbenches
Each suite sweeps every state against every combination of the eight control inputs:
6 states
x vbus x bus_reset x suspend_req x resume_event
x set_address x (address zero / non-zero)
x set_config x (config zero / non-zero)
= 6 x 256 = 1536 one-step transitionsReaching each starting state uses only legal transitions — goto_state walks the enumeration path rather than forcing registers — and each bench keeps a shadow model of all four pieces of retained state (the state, the saved state, the address and the configuration), compared both before and after every clock edge.
11.1 Verilog testbench
`timescale 1ns/1ps
module tb_df_v;
reg clk=0, rst_n=0;
reg vbus=0, bus_reset=0, suspend_req=0, resume_event=0;
reg set_address=0, set_config=0;
reg [6:0] address_val=0;
reg [3:0] config_val=0;
wire [2:0] state, saved_state;
wire [6:0] dev_address;
wire [3:0] dev_config;
wire ep_flush, endpoints_usable, suspended;
wire [31:0] n_resets, n_flushes, n_suspends, n_resumes, n_configured;
always #5 clk=~clk;
usb_device_fsm dut (
.clk(clk), .rst_n(rst_n), .vbus(vbus), .bus_reset(bus_reset),
.suspend_req(suspend_req), .resume_event(resume_event),
.set_address(set_address), .address_val(address_val),
.set_config(set_config), .config_val(config_val), .state(state),
.dev_address(dev_address), .dev_config(dev_config),
.ep_flush(ep_flush), .endpoints_usable(endpoints_usable),
.suspended(suspended), .saved_state(saved_state), .n_resets(n_resets),
.n_flushes(n_flushes), .n_suspends(n_suspends), .n_resumes(n_resumes),
.n_configured(n_configured));
localparam [2:0] S_ATTACHED=0, S_POWERED=1, S_DEFAULT=2, S_ADDRESS=3,
S_CONFIGURED=4, S_SUSPENDED=5;
// ---- SHADOW MODEL: independent copies of every piece of retained state.
integer s_st, s_saved, s_addr, s_cfg;
integer m_rst, m_flush, m_susp, m_res, m_cfgd;
integer errors=0, i, a, b, c, d, e, f, g, h, st0;
integer n_exh=0;
integer n_vis [0:5];
integer n_flush_from [0:5];
integer n_resume_to [0:5];
task check(input cond, input [639:0] msg);
begin if (!cond) begin errors=errors+1;
if (errors <= 25)
$display(" FAIL: %0s (vbus=%b rst=%b susp=%b res=%b sa=%b a=%0d sc=%b c=%0d | st=%0d saved=%0d addr=%0d cfg=%0d flush=%b || model st=%0d sv=%0d, t=%0t)",
msg, vbus, bus_reset, suspend_req, resume_event, set_address,
address_val, set_config, config_val, state, saved_state,
dev_address, dev_config, ep_flush, s_st, s_saved, $time);
end end
endtask
// The reference model is written as a nested case over the CURRENT state
// where the design uses one flat priority chain -- a different route.
task model(output integer e_st, output integer e_saved,
output integer e_addr, output integer e_cfg, output e_flush);
reg aok, cok;
begin
e_st = s_st; e_saved = s_saved; e_addr = s_addr; e_cfg = s_cfg;
aok = set_address && ((s_st == S_DEFAULT) || (s_st == S_ADDRESS));
cok = set_config && ((s_st == S_ADDRESS) || (s_st == S_CONFIGURED));
e_flush = vbus && (bus_reset || cok);
if (!vbus) begin
e_st = S_ATTACHED; e_saved = S_POWERED; e_addr = 0; e_cfg = 0;
end else if (bus_reset) begin
e_st = S_DEFAULT; e_saved = S_DEFAULT; e_addr = 0; e_cfg = 0;
end else begin
case (s_st)
S_ATTACHED: begin e_st = S_POWERED; e_saved = S_POWERED; end
S_SUSPENDED: if (resume_event) e_st = s_saved;
default: begin
if (suspend_req) begin
e_saved = s_st; e_st = S_SUSPENDED;
end else if (aok) begin
e_addr = address_val;
e_st = (address_val != 0) ? S_ADDRESS : S_DEFAULT;
end else if (cok) begin
e_cfg = config_val;
e_st = (config_val != 0) ? S_CONFIGURED : S_ADDRESS;
end
end
endcase
end
end
endtask
task check_comb;
integer e_st, e_saved, e_addr, e_cfg;
reg e_flush;
begin
model(e_st, e_saved, e_addr, e_cfg, e_flush);
check(state === s_st[2:0], "state matches the shadow model");
check(saved_state === s_saved[2:0], "saved_state matches the model");
check(dev_address === s_addr[6:0], "dev_address matches the model");
check(dev_config === s_cfg[3:0], "dev_config matches the model");
check(ep_flush === e_flush, "ep_flush matches the model");
check(suspended === (s_st == S_SUSPENDED), "suspended matches the state");
check(endpoints_usable === (s_st == S_CONFIGURED),
"endpoints are usable only when CONFIGURED");
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE property. A bus reset with power flushes every endpoint.
// Returning to DEFAULT without flushing leaves a stale toggle, and
// the first packet of the next session is silently dropped.
if (vbus && bus_reset)
check(ep_flush,
"a bus reset did not flush the endpoints -- stale toggles survive into the new session");
// 2. A SET_CONFIGURATION flushes too: the endpoints are re-created.
if (vbus && set_config
&& ((state === S_ADDRESS) || (state === S_CONFIGURED)))
check(ep_flush,
"a SET_CONFIGURATION did not flush the endpoints it re-created");
// 3. Endpoints beyond endpoint 0 exist only when CONFIGURED.
if (endpoints_usable)
check(state === S_CONFIGURED,
"endpoints were usable outside the CONFIGURED state");
// 4. A device with no configuration is never CONFIGURED.
if (state === S_CONFIGURED)
check(dev_config !== 4'd0,
"the device is CONFIGURED with configuration 0");
// 5. DEFAULT means address zero, by definition.
if (state === S_DEFAULT)
check(dev_address === 7'd0,
"the device is in DEFAULT with a non-zero address");
// 6. Nothing is retained without power.
if (state === S_ATTACHED)
check((dev_address === 7'd0) && (dev_config === 4'd0),
"an unpowered device retained an address or a configuration");
// 7. SUSPENDED always remembers somewhere real to go back to.
if (state === S_SUSPENDED)
check(saved_state !== S_SUSPENDED,
"SUSPENDED remembers SUSPENDED -- resume would never leave");
// 8. A flush never happens without power.
if (!vbus) check(!ep_flush, "an unpowered device flushed its endpoints");
if (s_st >= 0 && s_st <= 5) n_vis[s_st] = n_vis[s_st] + 1;
if (e_flush && s_st >= 0 && s_st <= 5)
n_flush_from[s_st] = n_flush_from[s_st] + 1;
if ((s_st == S_SUSPENDED) && resume_event && vbus && !bus_reset
&& s_saved >= 0 && s_saved <= 5)
n_resume_to[s_saved] = n_resume_to[s_saved] + 1;
end
endtask
task model_step;
integer e_st, e_saved, e_addr, e_cfg;
reg e_flush;
begin
model(e_st, e_saved, e_addr, e_cfg, e_flush);
if (vbus && bus_reset) m_rst = m_rst + 1;
if (e_flush) m_flush = m_flush + 1;
if ((e_st == S_SUSPENDED) && (s_st != S_SUSPENDED)) m_susp = m_susp + 1;
if ((s_st == S_SUSPENDED) && (e_st != S_SUSPENDED)) m_res = m_res + 1;
if ((e_st == S_CONFIGURED) && (s_st != S_CONFIGURED)) m_cfgd = m_cfgd + 1;
s_st = e_st; s_saved = e_saved; s_addr = e_addr; s_cfg = e_cfg;
end
endtask
task step;
begin
#1;
check_comb;
model_step;
@(posedge clk); #1;
check(state === s_st[2:0], "state tracked the model");
check(saved_state === s_saved[2:0], "saved_state tracked the model");
check(dev_address === s_addr[6:0], "dev_address tracked the model");
check(dev_config === s_cfg[3:0], "dev_config tracked the model");
check(n_resets === m_rst[31:0], "n_resets matches the model");
check(n_flushes === m_flush[31:0], "n_flushes matches the model");
check(n_suspends === m_susp[31:0], "n_suspends matches the model");
check(n_resumes === m_res[31:0], "n_resumes matches the model");
check(n_configured === m_cfgd[31:0], "n_configured matches the model");
end
endtask
task idle;
begin
bus_reset=0; suspend_req=0; resume_event=0;
set_address=0; set_config=0;
end
endtask
task hard_reset;
begin
rst_n=0; vbus=0; idle; address_val=0; config_val=0;
@(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
s_st=S_ATTACHED; s_saved=S_POWERED; s_addr=0; s_cfg=0;
m_rst=0; m_flush=0; m_susp=0; m_res=0; m_cfgd=0;
end
endtask
// Walk the machine into a chosen state, using only legal transitions.
task goto_state(input integer want);
begin
hard_reset;
if (want == S_ATTACHED) begin
vbus=0; idle; step;
end else begin
vbus=1; idle; step; // ATTACHED -> POWERED
if (want != S_POWERED) begin
bus_reset=1; step; idle; // -> DEFAULT
if (want == S_ADDRESS || want == S_CONFIGURED
|| want == S_SUSPENDED) begin
set_address=1; address_val=7'd5; step; idle; // -> ADDRESS
end
if (want == S_CONFIGURED || want == S_SUSPENDED) begin
set_config=1; config_val=4'd1; step; idle; // -> CONFIGURED
end
if (want == S_SUSPENDED) begin
suspend_req=1; step; idle; // -> SUSPENDED
end
end
end
#1;
check(state === want[2:0], "goto_state reached the requested state");
end
endtask
initial begin
for (i=0;i<6;i=i+1) begin
n_vis[i]=0; n_flush_from[i]=0; n_resume_to[i]=0;
end
hard_reset;
check(state === S_ATTACHED, "reset leaves the device ATTACHED");
check(!endpoints_usable, "with no usable endpoints");
// ===== A. EXHAUSTIVE one-step transition sweep =====
// 6 states x vbus x bus_reset x suspend_req x resume_event
// x set_address x address non-zero x set_config x config non-zero
// = 6 x 256 = 1536 transitions: every state against every combination
// of the eight control inputs.
for (st0=0; st0<6; st0=st0+1)
for (a=0;a<2;a=a+1)
for (b=0;b<2;b=b+1)
for (c=0;c<2;c=c+1)
for (d=0;d<2;d=d+1)
for (e=0;e<2;e=e+1)
for (f=0;f<2;f=f+1)
for (g=0;g<2;g=g+1)
for (h=0;h<2;h=h+1) begin
goto_state(st0);
vbus=a[0]; bus_reset=b[0]; suspend_req=c[0];
resume_event=d[0]; set_address=e[0];
address_val = f[0] ? 7'd9 : 7'd0;
set_config=g[0];
config_val = h[0] ? 4'd1 : 4'd0;
step;
n_exh = n_exh + 1;
idle;
end
$display(" exhaustive device-FSM sweep: %0d of %0d transitions verified",
n_exh, 6*256);
// ===== B. directed: the enumeration walk, and the reset that undoes it
hard_reset;
// 1. Plug in.
vbus=1; step; idle;
check(state === S_POWERED, "VBUS moves ATTACHED to POWERED");
check(dev_address === 7'd0, "with no address");
check(!endpoints_usable, "and no usable endpoints");
// 2. The host resets the bus. This is the FIRST thing it does.
bus_reset=1; step; idle;
check(state === S_DEFAULT, "a bus reset moves POWERED to DEFAULT");
check(n_flushes === 32'd1, "and flushes every endpoint");
// 3. SET_ADDRESS.
set_address=1; address_val=7'd7; step; idle;
check(state === S_ADDRESS, "SET_ADDRESS(7) moves DEFAULT to ADDRESS");
check(dev_address === 7'd7, "with the address applied");
check(!endpoints_usable, "but still only endpoint 0");
// 4. SET_CONFIGURATION.
set_config=1; config_val=4'd1; #1;
check(ep_flush,
"SET_CONFIGURATION flushes: it re-creates the endpoints");
step; idle;
check(state === S_CONFIGURED, "and moves ADDRESS to CONFIGURED");
check(endpoints_usable, "NOW the other endpoints exist");
check(n_flushes === 32'd2, "that is the second flush");
// 5. THE case. A bus reset from CONFIGURED -- the furthest state --
// returns all the way to DEFAULT and flushes everything.
bus_reset=1; #1;
check(ep_flush, "a bus reset from CONFIGURED flushes the endpoints");
step; idle;
check(state === S_DEFAULT, "and returns to DEFAULT from ANY state");
check(dev_address === 7'd0, "the address is gone");
check(dev_config === 4'd0, "the configuration is gone");
check(!endpoints_usable, "and the endpoints are not usable");
// 6. Suspend from CONFIGURED and resume BACK to CONFIGURED.
goto_state(S_CONFIGURED);
check(state === S_CONFIGURED, "configured again");
suspend_req=1; step; idle;
check(state === S_SUSPENDED, "an idle bus suspends the device");
check(saved_state === S_CONFIGURED, "remembering where it came from");
check(!endpoints_usable, "a suspended device transfers nothing");
resume_event=1; step; idle;
check(state === S_CONFIGURED,
"and resume returns to CONFIGURED -- NOT to DEFAULT");
check(endpoints_usable, "so no re-enumeration is needed");
check(n_resumes === 32'd1, "one resume");
// 7. Suspend from ADDRESS resumes to ADDRESS, not to CONFIGURED.
goto_state(S_ADDRESS);
suspend_req=1; step; idle;
check(saved_state === S_ADDRESS, "suspended from ADDRESS");
resume_event=1; step; idle;
check(state === S_ADDRESS, "and resumed to ADDRESS");
// 8. SET_ADDRESS(0) de-addresses without a bus reset.
goto_state(S_ADDRESS);
set_address=1; address_val=7'd0; step; idle;
check(state === S_DEFAULT, "SET_ADDRESS(0) returns ADDRESS to DEFAULT");
check(dev_address === 7'd0, "with address zero");
// 9. SET_CONFIGURATION(0) un-configures without de-addressing.
goto_state(S_CONFIGURED);
set_config=1; config_val=4'd0; step; idle;
check(state === S_ADDRESS,
"SET_CONFIGURATION(0) returns CONFIGURED to ADDRESS");
check(dev_address === 7'd5, "keeping the address");
check(!endpoints_usable, "and losing the endpoints");
// 10. Losing VBUS discards everything.
goto_state(S_CONFIGURED);
vbus=0; step; idle;
check(state === S_ATTACHED, "losing VBUS returns to ATTACHED");
check(dev_address === 7'd0, "the address is gone");
check(dev_config === 4'd0, "and the configuration with it");
check(!ep_flush, "and an unpowered device does not flush anything");
// ===== C. randomised =====
hard_reset; vbus=1;
for (i=0;i<40000;i=i+1) begin
vbus = ({$random}%64)!=0;
bus_reset = ({$random}%24)==0;
suspend_req = ({$random}%12)==0;
resume_event = ({$random}%4)==0;
set_address = ({$random}%6)==0;
address_val = {$random}%128;
set_config = ({$random}%6)==0;
config_val = {$random}%16;
step;
end
for (i=0;i<6;i=i+1)
check(n_vis[i] > 100, "every device state was visited many times");
for (i=2;i<=4;i=i+1)
check(n_flush_from[i] > 50,
"endpoint flushes were issued from every configurable state");
for (i=1;i<=4;i=i+1)
check(n_resume_to[i] > 10,
"resumes returned to every state that can be suspended from");
$display("");
$display(" REACH: transitions=%0d | visits: attached=%0d powered=%0d default=%0d address=%0d configured=%0d suspended=%0d",
n_exh, n_vis[0], n_vis[1], n_vis[2], n_vis[3], n_vis[4], n_vis[5]);
$display(" FLUSHES from: default=%0d address=%0d configured=%0d suspended=%0d | RESUMES to: powered=%0d default=%0d address=%0d configured=%0d",
n_flush_from[2], n_flush_from[3], n_flush_from[4],
n_flush_from[5], n_resume_to[1], n_resume_to[2],
n_resume_to[3], n_resume_to[4]);
$display(" COUNTERS: resets=%0d flushes=%0d suspends=%0d resumes=%0d configured=%0d",
n_resets, n_flushes, n_suspends, n_resumes, n_configured);
$display(" [Verilog] usb_device_fsm: %0d errors", errors);
$display(" [Verilog] %0s", errors==0 ? "PASS" : "FAIL");
$display("");
$finish;
end
endmodule11.2 SystemVerilog testbench
`timescale 1ns/1ps
module tb_df_sv;
import usb_devfsm_pkg::*;
logic clk=0, rst_n=0;
logic vbus=0, bus_reset=0, suspend_req=0, resume_event=0;
logic set_address=0, set_config=0;
logic [6:0] address_val=0;
logic [3:0] config_val=0;
dev_state_e state, saved_state;
logic [6:0] dev_address;
logic [3:0] dev_config;
logic ep_flush, endpoints_usable, suspended;
logic [31:0] n_resets, n_flushes, n_suspends, n_resumes, n_configured;
// 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 = 21505;
always #5 clk=~clk;
usb_device_fsm dut (
.clk, .rst_n, .vbus, .bus_reset, .suspend_req, .resume_event,
.set_address, .address_val, .set_config, .config_val, .state,
.dev_address, .dev_config, .ep_flush, .endpoints_usable, .suspended,
.saved_state, .n_resets, .n_flushes, .n_suspends, .n_resumes,
.n_configured);
// ---- SHADOW MODEL: independent copies of every piece of retained state.
int s_st, s_saved, s_addr, s_cfg;
int m_rst, m_flush, m_susp, m_res, m_cfgd;
int errors=0, i, a, b, c, d, e, f, g, h, st0;
int n_exh=0;
int n_vis [6];
int n_flush_from [6];
int n_resume_to [6];
task automatic check(input bit cond, input string msg);
// Icarus will not call .name() on a net, so the enum outputs are copied
// into variables of the same type before being printed.
dev_state_e st_v, sv_v;
if (!cond) begin
errors++;
st_v = state; sv_v = saved_state;
if (errors <= 25)
$display(" FAIL: %0s (vbus=%b rst=%b susp=%b res=%b sa=%b a=%0d sc=%b c=%0d | st=%s saved=%s addr=%0d cfg=%0d flush=%b || model st=%0d sv=%0d, t=%0t)",
msg, vbus, bus_reset, suspend_req, resume_event, set_address,
address_val, set_config, config_val, st_v.name(),
sv_v.name(), dev_address, dev_config, ep_flush, s_st,
s_saved, $time);
end
endtask
// The reference model is written as a nested case over the CURRENT state
// where the design uses one flat priority chain -- a different route.
task automatic model(output int e_st, output int e_saved,
output int e_addr, output int e_cfg,
output bit e_flush);
bit aok, cok;
begin
e_st = s_st; e_saved = s_saved; e_addr = s_addr; e_cfg = s_cfg;
aok = set_address && ((s_st == S_DEFAULT) || (s_st == S_ADDRESS));
cok = set_config && ((s_st == S_ADDRESS) || (s_st == S_CONFIGURED));
e_flush = vbus && (bus_reset || cok);
if (!vbus) begin
e_st = S_ATTACHED; e_saved = S_POWERED; e_addr = 0; e_cfg = 0;
end else if (bus_reset) begin
e_st = S_DEFAULT; e_saved = S_DEFAULT; e_addr = 0; e_cfg = 0;
end else begin
case (s_st)
S_ATTACHED: begin e_st = S_POWERED; e_saved = S_POWERED; end
S_SUSPENDED: if (resume_event) e_st = s_saved;
default: begin
if (suspend_req) begin
e_saved = s_st; e_st = S_SUSPENDED;
end else if (aok) begin
e_addr = address_val;
e_st = (address_val != 0) ? S_ADDRESS : S_DEFAULT;
end else if (cok) begin
e_cfg = config_val;
e_st = (config_val != 0) ? S_CONFIGURED : S_ADDRESS;
end
end
endcase
end
end
endtask
task automatic check_comb;
int e_st, e_saved, e_addr, e_cfg;
bit e_flush;
begin
model(e_st, e_saved, e_addr, e_cfg, e_flush);
check(state === dev_state_e'(s_st), "state matches the shadow model");
check(saved_state === dev_state_e'(s_saved), "saved_state matches the model");
check(dev_address === 7'(s_addr), "dev_address matches the model");
check(dev_config === 4'(s_cfg), "dev_config matches the model");
check(ep_flush === e_flush, "ep_flush matches the model");
check(suspended === (s_st == S_SUSPENDED), "suspended matches the state");
check(endpoints_usable === (s_st == S_CONFIGURED),
"endpoints are usable only when CONFIGURED");
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE property. A bus reset with power flushes every endpoint.
// Returning to DEFAULT without flushing leaves a stale toggle, and
// the first packet of the next session is silently dropped.
if (vbus && bus_reset)
check(ep_flush,
"a bus reset did not flush the endpoints -- stale toggles survive into the new session");
// 2. A SET_CONFIGURATION flushes too: the endpoints are re-created.
if (vbus && set_config
&& ((state === S_ADDRESS) || (state === S_CONFIGURED)))
check(ep_flush,
"a SET_CONFIGURATION did not flush the endpoints it re-created");
// 3. Endpoints beyond endpoint 0 exist only when CONFIGURED.
if (endpoints_usable)
check(state === S_CONFIGURED,
"endpoints were usable outside the CONFIGURED state");
// 4. A device with no configuration is never CONFIGURED.
if (state === S_CONFIGURED)
check(dev_config !== 4'd0,
"the device is CONFIGURED with configuration 0");
// 5. DEFAULT means address zero, by definition.
if (state === S_DEFAULT)
check(dev_address === 7'd0,
"the device is in DEFAULT with a non-zero address");
// 6. Nothing is retained without power.
if (state === S_ATTACHED)
check((dev_address === 7'd0) && (dev_config === 4'd0),
"an unpowered device retained an address or a configuration");
// 7. SUSPENDED always remembers somewhere real to go back to.
if (state === S_SUSPENDED)
check(saved_state !== S_SUSPENDED,
"SUSPENDED remembers SUSPENDED -- resume would never leave");
// 8. A flush never happens without power.
if (!vbus) check(!ep_flush, "an unpowered device flushed its endpoints");
n_vis[s_st] = n_vis[s_st] + 1;
if (e_flush) n_flush_from[s_st] = n_flush_from[s_st] + 1;
if ((s_st == S_SUSPENDED) && resume_event && vbus && !bus_reset)
n_resume_to[s_saved] = n_resume_to[s_saved] + 1;
end
endtask
task automatic model_step;
int e_st, e_saved, e_addr, e_cfg;
bit e_flush;
begin
model(e_st, e_saved, e_addr, e_cfg, e_flush);
if (vbus && bus_reset) m_rst = m_rst + 1;
if (e_flush) m_flush = m_flush + 1;
if ((e_st == S_SUSPENDED) && (s_st != S_SUSPENDED)) m_susp = m_susp + 1;
if ((s_st == S_SUSPENDED) && (e_st != S_SUSPENDED)) m_res = m_res + 1;
if ((e_st == S_CONFIGURED) && (s_st != S_CONFIGURED)) m_cfgd = m_cfgd + 1;
s_st = e_st; s_saved = e_saved; s_addr = e_addr; s_cfg = e_cfg;
end
endtask
task automatic step;
begin
#1;
check_comb;
model_step;
@(posedge clk); #1;
check(state === dev_state_e'(s_st), "state tracked the model");
check(saved_state === dev_state_e'(s_saved), "saved_state tracked the model");
check(dev_address === 7'(s_addr), "dev_address tracked the model");
check(dev_config === 4'(s_cfg), "dev_config tracked the model");
check(n_resets === 32'(m_rst), "n_resets matches the model");
check(n_flushes === 32'(m_flush), "n_flushes matches the model");
check(n_suspends === 32'(m_susp), "n_suspends matches the model");
check(n_resumes === 32'(m_res), "n_resumes matches the model");
check(n_configured === 32'(m_cfgd), "n_configured matches the model");
end
endtask
task automatic idle;
begin
bus_reset=0; suspend_req=0; resume_event=0;
set_address=0; set_config=0;
end
endtask
task automatic hard_reset;
begin
rst_n=0; vbus=0; idle; address_val=0; config_val=0;
@(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
s_st=S_ATTACHED; s_saved=S_POWERED; s_addr=0; s_cfg=0;
m_rst=0; m_flush=0; m_susp=0; m_res=0; m_cfgd=0;
end
endtask
// Walk the machine into a chosen state, using only legal transitions.
task automatic goto_state(input int want);
begin
hard_reset;
if (want == S_ATTACHED) begin
vbus=0; idle; step;
end else begin
vbus=1; idle; step; // ATTACHED -> POWERED
if (want != S_POWERED) begin
bus_reset=1; step; idle; // -> DEFAULT
if (want == S_ADDRESS || want == S_CONFIGURED
|| want == S_SUSPENDED) begin
set_address=1; address_val=7'd5; step; idle; // -> ADDRESS
end
if (want == S_CONFIGURED || want == S_SUSPENDED) begin
set_config=1; config_val=4'd1; step; idle; // -> CONFIGURED
end
if (want == S_SUSPENDED) begin
suspend_req=1; step; idle; // -> SUSPENDED
end
end
end
#1;
check(state === dev_state_e'(want), "goto_state reached the requested state");
end
endtask
initial begin
void'($urandom(urandom_seed));
for (i=0;i<6;i++) begin
n_vis[i]=0; n_flush_from[i]=0; n_resume_to[i]=0;
end
hard_reset;
check(state === S_ATTACHED, "reset leaves the device ATTACHED");
check(!endpoints_usable, "with no usable endpoints");
// ===== A. EXHAUSTIVE one-step transition sweep =====
// 6 states x vbus x bus_reset x suspend_req x resume_event
// x set_address x address non-zero x set_config x config non-zero
// = 6 x 256 = 1536 transitions: every state against every combination
// of the eight control inputs.
for (st0=0; st0<6; st0=st0+1)
for (a=0;a<2;a=a+1)
for (b=0;b<2;b=b+1)
for (c=0;c<2;c=c+1)
for (d=0;d<2;d=d+1)
for (e=0;e<2;e=e+1)
for (f=0;f<2;f=f+1)
for (g=0;g<2;g=g+1)
for (h=0;h<2;h=h+1) begin
goto_state(st0);
vbus=a[0]; bus_reset=b[0]; suspend_req=c[0];
resume_event=d[0]; set_address=e[0];
address_val = f[0] ? 7'd9 : 7'd0;
set_config=g[0];
config_val = h[0] ? 4'd1 : 4'd0;
step;
n_exh = n_exh + 1;
idle;
end
$display(" exhaustive device-FSM sweep: %0d of %0d transitions verified",
n_exh, 6*256);
// ===== B. directed: the enumeration walk, and the reset that undoes it
hard_reset;
// 1. Plug in.
vbus=1; step; idle;
check(state === S_POWERED, "VBUS moves ATTACHED to POWERED");
check(dev_address === 7'd0, "with no address");
check(!endpoints_usable, "and no usable endpoints");
// 2. The host resets the bus. This is the FIRST thing it does.
bus_reset=1; step; idle;
check(state === S_DEFAULT, "a bus reset moves POWERED to DEFAULT");
check(n_flushes === 32'd1, "and flushes every endpoint");
// 3. SET_ADDRESS.
set_address=1; address_val=7'd7; step; idle;
check(state === S_ADDRESS, "SET_ADDRESS(7) moves DEFAULT to ADDRESS");
check(dev_address === 7'd7, "with the address applied");
check(!endpoints_usable, "but still only endpoint 0");
// 4. SET_CONFIGURATION.
set_config=1; config_val=4'd1; #1;
check(ep_flush,
"SET_CONFIGURATION flushes: it re-creates the endpoints");
step; idle;
check(state === S_CONFIGURED, "and moves ADDRESS to CONFIGURED");
check(endpoints_usable, "NOW the other endpoints exist");
check(n_flushes === 32'd2, "that is the second flush");
// 5. THE case. A bus reset from CONFIGURED -- the furthest state --
// returns all the way to DEFAULT and flushes everything.
bus_reset=1; #1;
check(ep_flush, "a bus reset from CONFIGURED flushes the endpoints");
step; idle;
check(state === S_DEFAULT, "and returns to DEFAULT from ANY state");
check(dev_address === 7'd0, "the address is gone");
check(dev_config === 4'd0, "the configuration is gone");
check(!endpoints_usable, "and the endpoints are not usable");
// 6. Suspend from CONFIGURED and resume BACK to CONFIGURED.
goto_state(S_CONFIGURED);
check(state === S_CONFIGURED, "configured again");
suspend_req=1; step; idle;
check(state === S_SUSPENDED, "an idle bus suspends the device");
check(saved_state === S_CONFIGURED, "remembering where it came from");
check(!endpoints_usable, "a suspended device transfers nothing");
resume_event=1; step; idle;
check(state === S_CONFIGURED,
"and resume returns to CONFIGURED -- NOT to DEFAULT");
check(endpoints_usable, "so no re-enumeration is needed");
check(n_resumes === 32'd1, "one resume");
// 7. Suspend from ADDRESS resumes to ADDRESS, not to CONFIGURED.
goto_state(S_ADDRESS);
suspend_req=1; step; idle;
check(saved_state === S_ADDRESS, "suspended from ADDRESS");
resume_event=1; step; idle;
check(state === S_ADDRESS, "and resumed to ADDRESS");
// 8. SET_ADDRESS(0) de-addresses without a bus reset.
goto_state(S_ADDRESS);
set_address=1; address_val=7'd0; step; idle;
check(state === S_DEFAULT, "SET_ADDRESS(0) returns ADDRESS to DEFAULT");
check(dev_address === 7'd0, "with address zero");
// 9. SET_CONFIGURATION(0) un-configures without de-addressing.
goto_state(S_CONFIGURED);
set_config=1; config_val=4'd0; step; idle;
check(state === S_ADDRESS,
"SET_CONFIGURATION(0) returns CONFIGURED to ADDRESS");
check(dev_address === 7'd5, "keeping the address");
check(!endpoints_usable, "and losing the endpoints");
// 10. Losing VBUS discards everything.
goto_state(S_CONFIGURED);
vbus=0; step; idle;
check(state === S_ATTACHED, "losing VBUS returns to ATTACHED");
check(dev_address === 7'd0, "the address is gone");
check(dev_config === 4'd0, "and the configuration with it");
check(!ep_flush, "and an unpowered device does not flush anything");
// ===== C. randomised =====
hard_reset; vbus=1;
for (i=0;i<40000;i=i+1) begin
vbus = ($urandom%64)!=0;
bus_reset = ($urandom%24)==0;
suspend_req = ($urandom%12)==0;
resume_event = ($urandom%4)==0;
set_address = ($urandom%6)==0;
address_val = $urandom%128;
set_config = ($urandom%6)==0;
config_val = $urandom%16;
step;
end
for (i=0;i<6;i=i+1)
check(n_vis[i] > 100, "every device state was visited many times");
for (i=2;i<=4;i=i+1)
check(n_flush_from[i] > 50,
"endpoint flushes were issued from every configurable state");
for (i=1;i<=4;i=i+1)
check(n_resume_to[i] > 10,
"resumes returned to every state that can be suspended from");
$display("");
$display(" REACH: transitions=%0d | visits: attached=%0d powered=%0d default=%0d address=%0d configured=%0d suspended=%0d",
n_exh, n_vis[0], n_vis[1], n_vis[2], n_vis[3], n_vis[4], n_vis[5]);
$display(" FLUSHES from: default=%0d address=%0d configured=%0d suspended=%0d | RESUMES to: powered=%0d default=%0d address=%0d configured=%0d",
n_flush_from[2], n_flush_from[3], n_flush_from[4],
n_flush_from[5], n_resume_to[1], n_resume_to[2],
n_resume_to[3], n_resume_to[4]);
$display(" COUNTERS: resets=%0d flushes=%0d suspends=%0d resumes=%0d configured=%0d",
n_resets, n_flushes, n_suspends, n_resumes, n_configured);
$display(" [SystemVerilog] usb_device_fsm: %0d errors", errors);
$display(" [SystemVerilog] %0s", errors==0 ? "PASS" : "FAIL");
$display("");
$finish;
end
endmodule11.3 VHDL testbench
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
use work.usb_devfsm_pkg.all;
entity tb_df_vhdl is
end entity;
architecture sim of tb_df_vhdl is
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal vbus, bus_reset, suspend_req, resume_event : std_logic := '0';
signal set_address, set_config : std_logic := '0';
signal address_val : std_logic_vector(6 downto 0) := (others => '0');
signal config_val : std_logic_vector(3 downto 0) := (others => '0');
signal state, saved_state : std_logic_vector(2 downto 0);
signal dev_address : std_logic_vector(6 downto 0);
signal dev_config : std_logic_vector(3 downto 0);
signal ep_flush, endpoints_usable, suspended : std_logic;
signal n_resets, n_flushes, n_suspends, n_resumes, n_configured
: std_logic_vector(31 downto 0);
signal running : boolean := true;
type cnt6_t is array (0 to 5) of integer;
begin
clk <= not clk after 5 ns when running else '0';
dut : entity work.usb_device_fsm
port map (clk => clk, rst_n => rst_n, vbus => vbus,
bus_reset => bus_reset, suspend_req => suspend_req,
resume_event => resume_event, set_address => set_address,
address_val => address_val, set_config => set_config,
config_val => config_val, state => state,
dev_address => dev_address, dev_config => dev_config,
ep_flush => ep_flush, endpoints_usable => endpoints_usable,
suspended => suspended, saved_state => saved_state,
n_resets => n_resets, n_flushes => n_flushes,
n_suspends => n_suspends, n_resumes => n_resumes,
n_configured => n_configured);
stim : process
variable seed1 : positive := 9137;
variable seed2 : positive := 4421;
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: independent copies of every piece of retained state.
variable s_st, s_saved : dev_state_t := S_ATTACHED;
variable s_addr, s_cfg : integer := 0;
variable m_rst, m_flush, m_susp, m_res, m_cfgd : integer := 0;
variable n_exh : integer := 0;
variable n_vis, n_flush_from, n_resume_to : cnt6_t := (others => 0);
procedure check(cond : boolean; msg : string) is
begin
if not cond then
errors := errors + 1;
if errors <= 25 then
report " FAIL: " & msg
& " (vbus=" & std_logic'image(vbus)(2)
& " rst=" & std_logic'image(bus_reset)(2)
& " susp=" & std_logic'image(suspend_req)(2)
& " res=" & std_logic'image(resume_event)(2)
& " sa=" & std_logic'image(set_address)(2)
& " a=" & integer'image(to_integer(unsigned(address_val)))
& " sc=" & std_logic'image(set_config)(2)
& " c=" & integer'image(to_integer(unsigned(config_val)))
& " | st=" & integer'image(to_integer(unsigned(state)))
& " saved=" & integer'image(to_integer(unsigned(saved_state)))
& " addr=" & integer'image(to_integer(unsigned(dev_address)))
& " cfg=" & integer'image(to_integer(unsigned(dev_config)))
& " flush=" & std_logic'image(ep_flush)(2)
& " || model st=" & integer'image(dev_state_t'pos(s_st))
& " sv=" & integer'image(dev_state_t'pos(s_saved))
& ")" 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 reference model is written as a nested case over the CURRENT state
-- where the design uses one flat priority chain -- a different route.
procedure model(variable e_st, e_saved : out dev_state_t;
variable e_addr, e_cfg : out integer;
variable e_flush : out boolean) is
variable aok, cok : boolean;
variable av, cv : integer;
begin
e_st := s_st; e_saved := s_saved; e_addr := s_addr; e_cfg := s_cfg;
av := to_integer(unsigned(address_val));
cv := to_integer(unsigned(config_val));
aok := set_address = '1' and (s_st = S_DEFAULT or s_st = S_ADDRESS);
cok := set_config = '1' and (s_st = S_ADDRESS or s_st = S_CONFIGURED);
e_flush := vbus = '1' and (bus_reset = '1' or cok);
if vbus = '0' then
e_st := S_ATTACHED; e_saved := S_POWERED; e_addr := 0; e_cfg := 0;
elsif bus_reset = '1' then
e_st := S_DEFAULT; e_saved := S_DEFAULT; e_addr := 0; e_cfg := 0;
else
case s_st is
when S_ATTACHED =>
e_st := S_POWERED; e_saved := S_POWERED;
when S_SUSPENDED =>
if resume_event = '1' then e_st := s_saved; end if;
when others =>
if suspend_req = '1' then
e_saved := s_st; e_st := S_SUSPENDED;
elsif aok then
e_addr := av;
if av /= 0 then e_st := S_ADDRESS; else e_st := S_DEFAULT; end if;
elsif cok then
e_cfg := cv;
if cv /= 0 then e_st := S_CONFIGURED; else e_st := S_ADDRESS; end if;
end if;
end case;
end if;
end procedure;
procedure check_comb is
variable e_st, e_saved : dev_state_t;
variable e_addr, e_cfg : integer;
variable e_flush : boolean;
begin
model(e_st, e_saved, e_addr, e_cfg, e_flush);
check(state = st_code(s_st), "state matches the shadow model");
check(saved_state = st_code(s_saved), "saved_state matches the model");
check(to_integer(unsigned(dev_address)) = s_addr,
"dev_address matches the model");
check(to_integer(unsigned(dev_config)) = s_cfg,
"dev_config matches the model");
check((ep_flush = '1') = e_flush, "ep_flush matches the model");
check((suspended = '1') = (s_st = S_SUSPENDED),
"suspended matches the state");
check((endpoints_usable = '1') = (s_st = S_CONFIGURED),
"endpoints are usable only when CONFIGURED");
-- ---- SAFETY PROPERTIES, independent of the model ----
-- 1. THE property. A bus reset with power flushes every endpoint.
if vbus = '1' and bus_reset = '1' then
check(ep_flush = '1',
"a bus reset did not flush the endpoints -- stale toggles survive into the new session");
end if;
-- 2. A SET_CONFIGURATION flushes too: the endpoints are re-created.
if vbus = '1' and set_config = '1'
and (state = st_code(S_ADDRESS) or state = st_code(S_CONFIGURED)) then
check(ep_flush = '1',
"a SET_CONFIGURATION did not flush the endpoints it re-created");
end if;
-- 3. Endpoints beyond endpoint 0 exist only when CONFIGURED.
if endpoints_usable = '1' then
check(state = st_code(S_CONFIGURED),
"endpoints were usable outside the CONFIGURED state");
end if;
-- 4. A device with no configuration is never CONFIGURED.
if state = st_code(S_CONFIGURED) then
check(to_integer(unsigned(dev_config)) /= 0,
"the device is CONFIGURED with configuration 0");
end if;
-- 5. DEFAULT means address zero, by definition.
if state = st_code(S_DEFAULT) then
check(to_integer(unsigned(dev_address)) = 0,
"the device is in DEFAULT with a non-zero address");
end if;
-- 6. Nothing is retained without power.
if state = st_code(S_ATTACHED) then
check(to_integer(unsigned(dev_address)) = 0
and to_integer(unsigned(dev_config)) = 0,
"an unpowered device retained an address or a configuration");
end if;
-- 7. SUSPENDED always remembers somewhere real to go back to.
if state = st_code(S_SUSPENDED) then
check(saved_state /= st_code(S_SUSPENDED),
"SUSPENDED remembers SUSPENDED -- resume would never leave");
end if;
-- 8. A flush never happens without power.
if vbus = '0' then
check(ep_flush = '0', "an unpowered device flushed its endpoints");
end if;
n_vis(dev_state_t'pos(s_st)) := n_vis(dev_state_t'pos(s_st)) + 1;
if e_flush then
n_flush_from(dev_state_t'pos(s_st))
:= n_flush_from(dev_state_t'pos(s_st)) + 1;
end if;
if s_st = S_SUSPENDED and resume_event = '1' and vbus = '1'
and bus_reset = '0' then
n_resume_to(dev_state_t'pos(s_saved))
:= n_resume_to(dev_state_t'pos(s_saved)) + 1;
end if;
end procedure;
procedure model_step is
variable e_st, e_saved : dev_state_t;
variable e_addr, e_cfg : integer;
variable e_flush : boolean;
begin
model(e_st, e_saved, e_addr, e_cfg, e_flush);
if vbus = '1' and bus_reset = '1' then m_rst := m_rst + 1; end if;
if e_flush then m_flush := m_flush + 1; end if;
if e_st = S_SUSPENDED and s_st /= S_SUSPENDED then
m_susp := m_susp + 1;
end if;
if s_st = S_SUSPENDED and e_st /= S_SUSPENDED then
m_res := m_res + 1;
end if;
if e_st = S_CONFIGURED and s_st /= S_CONFIGURED then
m_cfgd := m_cfgd + 1;
end if;
s_st := e_st; s_saved := e_saved; s_addr := e_addr; s_cfg := e_cfg;
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(saved_state = st_code(s_saved), "saved_state tracked the model");
check(to_integer(unsigned(dev_address)) = s_addr,
"dev_address tracked the model");
check(to_integer(unsigned(dev_config)) = s_cfg,
"dev_config tracked the model");
check(n_resets = std_logic_vector(to_unsigned(m_rst, 32)),
"n_resets matches the model");
check(n_flushes = std_logic_vector(to_unsigned(m_flush, 32)),
"n_flushes matches the model");
check(n_suspends = std_logic_vector(to_unsigned(m_susp, 32)),
"n_suspends matches the model");
check(n_resumes = std_logic_vector(to_unsigned(m_res, 32)),
"n_resumes matches the model");
check(n_configured = std_logic_vector(to_unsigned(m_cfgd, 32)),
"n_configured matches the model");
end procedure;
procedure idle is
begin
bus_reset <= '0'; suspend_req <= '0'; resume_event <= '0';
set_address <= '0'; set_config <= '0';
end procedure;
procedure hard_reset is
begin
rst_n <= '0'; vbus <= '0'; idle;
address_val <= (others => '0'); config_val <= (others => '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 := S_ATTACHED; s_saved := S_POWERED; s_addr := 0; s_cfg := 0;
m_rst := 0; m_flush := 0; m_susp := 0; m_res := 0; m_cfgd := 0;
end procedure;
-- Walk the machine into a chosen state, using only legal transitions.
procedure goto_state(want : dev_state_t) is
begin
hard_reset;
if want = S_ATTACHED then
vbus <= '0'; idle; step;
else
vbus <= '1'; idle; step; -- ATTACHED -> POWERED
if want /= S_POWERED then
bus_reset <= '1'; step; idle; -- -> DEFAULT
if want = S_ADDRESS or want = S_CONFIGURED
or want = S_SUSPENDED then
set_address <= '1';
address_val <= std_logic_vector(to_unsigned(5, 7));
step; idle; -- -> ADDRESS
end if;
if want = S_CONFIGURED or want = S_SUSPENDED then
set_config <= '1';
config_val <= std_logic_vector(to_unsigned(1, 4));
step; idle; -- -> CONFIGURED
end if;
if want = S_SUSPENDED then
suspend_req <= '1'; step; idle; -- -> SUSPENDED
end if;
end if;
end if;
wait for 1 ns;
check(state = st_code(want), "goto_state reached the requested state");
end procedure;
variable iv : integer;
begin
hard_reset;
check(state = st_code(S_ATTACHED), "reset leaves the device ATTACHED");
check(endpoints_usable = '0', "with no usable endpoints");
-- ===== A. EXHAUSTIVE one-step transition sweep =====
-- 6 states x vbus x bus_reset x suspend_req x resume_event
-- x set_address x address non-zero x set_config x config non-zero
-- = 6 x 256 = 1536 transitions: every state against every combination
-- of the eight control inputs.
for st0 in 0 to 5 loop
for a in 0 to 1 loop
for b in 0 to 1 loop
for c in 0 to 1 loop
for d in 0 to 1 loop
for e in 0 to 1 loop
for f in 0 to 1 loop
for g in 0 to 1 loop
for h in 0 to 1 loop
goto_state(dev_state_t'val(st0));
if a = 1 then vbus <= '1'; else vbus <= '0'; end if;
if b = 1 then bus_reset <= '1'; else bus_reset <= '0'; end if;
if c = 1 then suspend_req <= '1'; else suspend_req <= '0'; end if;
if d = 1 then resume_event <= '1'; else resume_event <= '0'; end if;
if e = 1 then set_address <= '1'; else set_address <= '0'; end if;
if f = 1 then
address_val <= std_logic_vector(to_unsigned(9, 7));
else
address_val <= (others => '0');
end if;
if g = 1 then set_config <= '1'; else set_config <= '0'; end if;
if h = 1 then
config_val <= std_logic_vector(to_unsigned(1, 4));
else
config_val <= (others => '0');
end if;
step;
n_exh := n_exh + 1;
idle;
end loop;
end loop;
end loop;
end loop;
end loop;
end loop;
end loop;
end loop;
end loop;
report " exhaustive device-FSM sweep: " & integer'image(n_exh)
& " of 1536 transitions verified" severity note;
-- ===== B. directed: the enumeration walk, and the reset that undoes it
hard_reset;
-- 1. Plug in.
vbus <= '1'; step; idle;
check(state = st_code(S_POWERED), "VBUS moves ATTACHED to POWERED");
check(to_integer(unsigned(dev_address)) = 0, "with no address");
check(endpoints_usable = '0', "and no usable endpoints");
-- 2. The host resets the bus. This is the FIRST thing it does.
bus_reset <= '1'; step; idle;
check(state = st_code(S_DEFAULT), "a bus reset moves POWERED to DEFAULT");
check(n_flushes = std_logic_vector(to_unsigned(1, 32)),
"and flushes every endpoint");
-- 3. SET_ADDRESS.
set_address <= '1'; address_val <= std_logic_vector(to_unsigned(7, 7));
step; idle;
check(state = st_code(S_ADDRESS),
"SET_ADDRESS(7) moves DEFAULT to ADDRESS");
check(to_integer(unsigned(dev_address)) = 7, "with the address applied");
check(endpoints_usable = '0', "but still only endpoint 0");
-- 4. SET_CONFIGURATION.
set_config <= '1'; config_val <= std_logic_vector(to_unsigned(1, 4));
wait for 1 ns;
check(ep_flush = '1',
"SET_CONFIGURATION flushes: it re-creates the endpoints");
step; idle;
check(state = st_code(S_CONFIGURED),
"and moves ADDRESS to CONFIGURED");
check(endpoints_usable = '1', "NOW the other endpoints exist");
check(n_flushes = std_logic_vector(to_unsigned(2, 32)),
"that is the second flush");
-- 5. THE case. A bus reset from CONFIGURED -- the furthest state --
-- returns all the way to DEFAULT and flushes everything.
bus_reset <= '1'; wait for 1 ns;
check(ep_flush = '1',
"a bus reset from CONFIGURED flushes the endpoints");
step; idle;
check(state = st_code(S_DEFAULT),
"and returns to DEFAULT from ANY state");
check(to_integer(unsigned(dev_address)) = 0, "the address is gone");
check(to_integer(unsigned(dev_config)) = 0, "the configuration is gone");
check(endpoints_usable = '0', "and the endpoints are not usable");
-- 6. Suspend from CONFIGURED and resume BACK to CONFIGURED.
goto_state(S_CONFIGURED);
check(state = st_code(S_CONFIGURED), "configured again");
suspend_req <= '1'; step; idle;
check(state = st_code(S_SUSPENDED), "an idle bus suspends the device");
check(saved_state = st_code(S_CONFIGURED),
"remembering where it came from");
check(endpoints_usable = '0', "a suspended device transfers nothing");
resume_event <= '1'; step; idle;
check(state = st_code(S_CONFIGURED),
"and resume returns to CONFIGURED -- NOT to DEFAULT");
check(endpoints_usable = '1', "so no re-enumeration is needed");
check(n_resumes = std_logic_vector(to_unsigned(1, 32)), "one resume");
-- 7. Suspend from ADDRESS resumes to ADDRESS, not to CONFIGURED.
goto_state(S_ADDRESS);
suspend_req <= '1'; step; idle;
check(saved_state = st_code(S_ADDRESS), "suspended from ADDRESS");
resume_event <= '1'; step; idle;
check(state = st_code(S_ADDRESS), "and resumed to ADDRESS");
-- 8. SET_ADDRESS(0) de-addresses without a bus reset.
goto_state(S_ADDRESS);
set_address <= '1'; address_val <= (others => '0'); step; idle;
check(state = st_code(S_DEFAULT),
"SET_ADDRESS(0) returns ADDRESS to DEFAULT");
check(to_integer(unsigned(dev_address)) = 0, "with address zero");
-- 9. SET_CONFIGURATION(0) un-configures without de-addressing.
goto_state(S_CONFIGURED);
set_config <= '1'; config_val <= (others => '0'); step; idle;
check(state = st_code(S_ADDRESS),
"SET_CONFIGURATION(0) returns CONFIGURED to ADDRESS");
check(to_integer(unsigned(dev_address)) = 5, "keeping the address");
check(endpoints_usable = '0', "and losing the endpoints");
-- 10. Losing VBUS discards everything.
goto_state(S_CONFIGURED);
vbus <= '0'; step; idle;
check(state = st_code(S_ATTACHED), "losing VBUS returns to ATTACHED");
check(to_integer(unsigned(dev_address)) = 0, "the address is gone");
check(to_integer(unsigned(dev_config)) = 0,
"and the configuration with it");
check(ep_flush = '0',
"and an unpowered device does not flush anything");
-- ===== C. randomised =====
-- ieee.math_real.uniform is a genuinely different generator from either
-- Verilog builtin, which is what makes this column independent evidence.
hard_reset; vbus <= '1';
for i in 0 to 39999 loop
rnd(iv, 64); if iv /= 0 then vbus <= '1'; else vbus <= '0'; end if;
rnd(iv, 24); if iv = 0 then bus_reset <= '1'; else bus_reset <= '0'; end if;
rnd(iv, 12); if iv = 0 then suspend_req <= '1'; else suspend_req <= '0'; end if;
rnd(iv, 4); if iv = 0 then resume_event <= '1'; else resume_event <= '0'; end if;
rnd(iv, 6); if iv = 0 then set_address <= '1'; else set_address <= '0'; end if;
rnd(iv, 128); address_val <= std_logic_vector(to_unsigned(iv, 7));
rnd(iv, 6); if iv = 0 then set_config <= '1'; else set_config <= '0'; end if;
rnd(iv, 16); config_val <= std_logic_vector(to_unsigned(iv, 4));
step;
end loop;
for i in 0 to 5 loop
check(n_vis(i) > 100, "every device state was visited many times");
end loop;
for i in 2 to 4 loop
check(n_flush_from(i) > 50,
"endpoint flushes were issued from every configurable state");
end loop;
for i in 1 to 4 loop
check(n_resume_to(i) > 10,
"resumes returned to every state that can be suspended from");
end loop;
report " REACH: transitions=" & integer'image(n_exh)
& " | visits: attached=" & integer'image(n_vis(0))
& " powered=" & integer'image(n_vis(1))
& " default=" & integer'image(n_vis(2))
& " address=" & integer'image(n_vis(3))
& " configured=" & integer'image(n_vis(4))
& " suspended=" & integer'image(n_vis(5)) severity note;
report " FLUSHES from: default=" & integer'image(n_flush_from(2))
& " address=" & integer'image(n_flush_from(3))
& " configured=" & integer'image(n_flush_from(4))
& " suspended=" & integer'image(n_flush_from(5))
& " | RESUMES to: powered=" & integer'image(n_resume_to(1))
& " default=" & integer'image(n_resume_to(2))
& " address=" & integer'image(n_resume_to(3))
& " configured=" & integer'image(n_resume_to(4)) severity note;
report " COUNTERS: resets="
& integer'image(to_integer(unsigned(n_resets)))
& " flushes=" & integer'image(to_integer(unsigned(n_flushes)))
& " suspends=" & integer'image(to_integer(unsigned(n_suspends)))
& " resumes=" & integer'image(to_integer(unsigned(n_resumes)))
& " configured=" & integer'image(to_integer(unsigned(n_configured)))
severity note;
report " [VHDL] usb_device_fsm: " & integer'image(errors) & " errors"
severity note;
if errors = 0 then
report " [VHDL] PASS" severity note;
else
report " [VHDL] FAIL" severity failure;
end if;
running <= false;
wait;
end process;
end architecture;12. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| Exhaustive transitions | 1536 / 1536 | 1536 / 1536 | 1536 / 1536 |
| ATTACHED visits | 2412 | 2420 | 2452 |
| POWERED visits | 9634 | 9795 | 10010 |
| DEFAULT visits | 8679 | 8348 | 8684 |
| ADDRESS visits | 6921 | 7273 | 7004 |
| CONFIGURED visits | 9340 | 9431 | 8527 |
| SUSPENDED visits | 8676 | 8395 | 8985 |
| flushes from DEFAULT | 391 | 348 | 356 |
| flushes from ADDRESS | 1909 | 1900 | 1795 |
| flushes from CONFIGURED | 1843 | 1910 | 1630 |
| resumes to POWERED / DEFAULT / ADDRESS / CONFIGURED | 538 / 482 / 381 / 603 | 538 / 440 / 407 / 583 | 537 / 502 / 392 / 554 |
| bus resets | 1652 | 1612 | 1578 |
| endpoint flushes | 4056 | 4080 | 3708 |
| Result | PASS | PASS | PASS |
Every state is visited thousands of times, and the testbenches assert it. The resumes-to row is the one that makes §5's claim evidence rather than assertion: resume returned to four different states, several hundred times each. A suite in which every resume happened to come from CONFIGURED would pass identically against a design that always returns to CONFIGURED.
13. Mutation Testing, and a Lesson About Reading the Columns
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| J1 | a bus reset does not flush the endpoints | 49546 | 49448 | 49391 |
| J2 | resume always returns to DEFAULT | 144498 | 142700 | 142360 |
| J3 | SET_ADDRESS(0) does not return to DEFAULT | 70250 | 61466 | 77778 |
| J4 | SET_CONFIGURATION(0) does not return to ADDRESS | 73006 | 71098 | 42040 |
| J5 | a bus reset is ignored in CONFIGURED | 212909 | 213347 | 202525 |
| J6 | SET_CONFIGURATION does not flush | 47266 | 47391 | 46704 |
| J7 | endpoints are usable in ADDRESS too | 13844 | 14548 | 14010 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages. But look at J4: 73006 / 71098 / 42040. The VHDL column is 42% below the other two, which by the rule this series has used since Module 18 means the mutation does not mean the same thing in that language — and that rule has been right six times.
It is wrong here, and finding out took one experiment.
J5 is the largest at ~213 000, and it is the bug from §2: a CONFIGURED device that ignores a bus reset. Every cycle after the ignored reset is a mismatch, because the host and the device now disagree about the device's address permanently.
J1 and J6 are the two flush mutations, at ~49 500 and ~47 300 — almost identical, which is the right answer. They remove the two sources of ep_flush, and the suite is equally sensitive to both. A design that flushed on reset but not on SET_CONFIGURATION would be more plausible to ship, and that is precisely why it should not be easier to miss.
J7 is the smallest at ~14 000. It changes one output in one state, and it is worth noting that this is the mutation whose real-world consequence is the least dramatic — firmware is told it may use endpoints that do not exist yet — and the count reflects exactly that narrowness.
14. Debugging Walkthrough: The Device That Loses Its First Packet
The report. A USB data-logger occasionally loses the first record of a session. Not corrupted — absent. It happens perhaps one session in two, and only on sessions that begin after the host software is restarted.
Step 1 — one record, or a burst? Exactly one, always the first. A single missing packet is not a bandwidth or a buffering problem — those lose runs. One packet points at a per-packet mechanism.
Step 2 — what is special about a software restart? The driver reloads, and reloading a USB driver resets the bus. Sessions that begin without a restart do not lose anything. So the correlation is with bus resets, not with the data.
Step 3 — is the reset handled? Read the device state after the reset: DEFAULT, address 0. Correct. Re-enumeration proceeds normally and the device reaches CONFIGURED. The top-level FSM is doing exactly the right thing.
Step 4 — what else holds state? Everything in §3. Read the endpoint's expected toggle immediately after the reset. It is DATA1. The host, per the specification, restarts at DATA0.
Step 5 — what the device does with that. The first packet arrives as DATA0, the endpoint expects DATA1, and Chapter 21.1's rule applies exactly as written: a mismatched toggle is a retransmission, so the endpoint ACKs it and stores nothing. The host sees an ACK and moves on. No error is reported by anything, anywhere.
Step 6 — why one session in two. The toggle is DATA1 after an odd number of packets in the previous session. Half the time it is DATA0 and the reset is harmless.
Step 7 — the fix. ep_flush was not wired to the endpoint's toggle register. Mutation J1, in production, and the whole failure is one missing connection between two correct blocks.
15. UVM: Resetting From Everywhere
15.1 The transaction
class usb_devstate_item extends uvm_sequence_item;
`uvm_object_utils(usb_devstate_item)
rand bit vbus;
rand bit bus_reset;
rand bit suspend_req;
rand bit resume_event;
rand bit set_address;
rand bit [6:0] address_val;
rand bit set_config;
rand bit [3:0] config_val;
// A healthy bus: power is on, resets are occasional, requests are rare.
constraint c_realistic {
vbus dist {1 := 63, 0 := 1};
bus_reset dist {0 := 23, 1 := 1};
suspend_req dist {0 := 11, 1 := 1};
resume_event dist {0 := 3, 1 := 1};
set_address dist {0 := 5, 1 := 1};
set_config dist {0 := 5, 1 := 1};
}
// The ZERO arguments are legal requests, not errors -- SET_ADDRESS(0) and
// SET_CONFIGURATION(0) each move the machine BACKWARDS one step, and a
// uniform draw would present them a fraction of the time.
constraint c_zero_args_matter {
address_val dist { 0 := 30, [1:127] := 70 };
config_val dist { 0 := 30, [1:15] := 70 };
}
// Two control requests cannot complete in the same cycle on real hardware.
// Soft, so the adversarial sequence can check the priority anyway.
constraint c_one_request { soft !(set_address && set_config); }
function new(string name = "usb_devstate_item"); super.new(name); endfunction
function string convert2string();
return $sformatf("vbus=%0b rst=%0b susp=%0b res=%0b sa=%0b/%0d sc=%0b/%0d",
vbus, bus_reset, suspend_req, resume_event,
set_address, address_val, set_config, config_val);
endfunction
endclass15.2 Sequences
// THE sequence for this chapter. It walks the device all the way to
// CONFIGURED and THEN resets the bus -- the transition a design written
// around the enumeration walk is most likely to have missed, because it is
// the one that goes backwards from the state everything else is aiming at.
class reset_from_configured_seq extends uvm_sequence #(usb_devstate_item);
`uvm_object_utils(reset_from_configured_seq)
function new(string name = "reset_from_configured_seq"); super.new(name); endfunction
task body();
repeat (300) begin
usb_devstate_item it;
// walk: reset -> address -> configure
for (int stage = 0; stage < 3; stage++) begin
it = usb_devstate_item::type_id::create("it");
start_item(it);
it.c_realistic.constraint_mode(0);
it.c_zero_args_matter.constraint_mode(0);
if (!it.randomize() with {
vbus == 1; suspend_req == 0; resume_event == 0;
bus_reset == (stage == 0);
set_address == (stage == 1);
set_config == (stage == 2);
address_val != 0;
config_val != 0; })
`uvm_error("RAND", "walk randomize failed")
finish_item(it);
end
// ...and now reset from CONFIGURED.
it = usb_devstate_item::type_id::create("it");
start_item(it);
it.c_realistic.constraint_mode(0);
if (!it.randomize() with { vbus == 1; bus_reset == 1;
suspend_req == 0; resume_event == 0; })
`uvm_error("RAND", "reset randomize failed")
finish_item(it);
end
endtask
endclass
// Suspend and resume from EVERY state that can be suspended from. A suite in
// which every resume happens to come from CONFIGURED passes identically
// against a design that always returns to CONFIGURED.
class suspend_resume_everywhere_seq extends uvm_sequence #(usb_devstate_item);
`uvm_object_utils(suspend_resume_everywhere_seq)
function new(string name = "suspend_resume_everywhere_seq"); super.new(name); endfunction
rand int unsigned depth; // 0 = DEFAULT, 1 = ADDRESS, 2 = CONFIGURED
constraint c_depth { depth inside {[0:2]}; }
task body();
repeat (400) begin
usb_devstate_item it;
int unsigned d = $urandom_range(0, 2);
// walk to the chosen depth
for (int stage = 0; stage <= d; stage++) begin
it = usb_devstate_item::type_id::create("it");
start_item(it);
it.c_realistic.constraint_mode(0);
it.c_zero_args_matter.constraint_mode(0);
if (!it.randomize() with {
vbus == 1; suspend_req == 0; resume_event == 0;
bus_reset == (stage == 0);
set_address == (stage == 1);
set_config == (stage == 2);
address_val != 0; config_val != 0; })
`uvm_error("RAND", "depth randomize failed")
finish_item(it);
end
// suspend, then resume -- and the scoreboard checks we came BACK here
it = usb_devstate_item::type_id::create("it");
start_item(it);
it.c_realistic.constraint_mode(0);
if (!it.randomize() with { vbus == 1; bus_reset == 0;
suspend_req == 1; resume_event == 0; })
`uvm_error("RAND", "suspend randomize failed")
finish_item(it);
it = usb_devstate_item::type_id::create("it");
start_item(it);
it.c_realistic.constraint_mode(0);
if (!it.randomize() with { vbus == 1; bus_reset == 0;
suspend_req == 0; resume_event == 1; })
`uvm_error("RAND", "resume randomize failed")
finish_item(it);
end
endtask
endclass
// The backward requests: SET_ADDRESS(0) and SET_CONFIGURATION(0), which move
// the machine one step back without a bus reset.
class backward_requests_seq extends uvm_sequence #(usb_devstate_item);
`uvm_object_utils(backward_requests_seq)
function new(string name = "backward_requests_seq"); super.new(name); endfunction
task body();
repeat (400) begin
usb_devstate_item it = usb_devstate_item::type_id::create("it");
start_item(it);
it.c_zero_args_matter.constraint_mode(0);
if (!it.randomize() with { vbus == 1; bus_reset == 0;
suspend_req == 0; resume_event == 0;
(set_address || set_config) == 1;
address_val == 0; config_val == 0; })
`uvm_error("RAND", "backward randomize failed")
finish_item(it);
end
endtask
endclass15.3 The scoreboard
class usb_devstate_scoreboard extends uvm_scoreboard;
`uvm_component_utils(usb_devstate_scoreboard)
uvm_analysis_imp #(usb_devstate_mon_item, usb_devstate_scoreboard) ap;
// The scoreboard keeps its OWN copy of every piece of retained state.
dev_state_e sb_state, sb_saved;
bit [6:0] sb_addr;
bit [3:0] sb_cfg;
int unsigned n_reset_from[6]; // resets seen from each state
int unsigned n_resume_to[6]; // resumes landing in each state
int unsigned n_flush, n_missing_flush;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
sb_state = S_ATTACHED;
sb_saved = S_POWERED;
endfunction
function void write(usb_devstate_mon_item t);
dev_state_e prev = sb_state;
// ---- THE property. A bus reset with power MUST flush. ----
if (t.vbus && t.bus_reset) begin
if (!t.ep_flush)
`uvm_error("STALE_STATE",
"a bus reset did not assert ep_flush -- every endpoint toggle, halt, buffer and pointer survives into the new session, and the first packet of that session is silently dropped")
n_reset_from[int'(prev)]++;
end
// ---- SET_CONFIGURATION re-creates the endpoints, so it flushes too ----
if (t.vbus && t.set_config
&& (prev inside {S_ADDRESS, S_CONFIGURED})) begin
if (!t.ep_flush)
`uvm_error("STALE_STATE",
"a SET_CONFIGURATION did not flush the endpoints it re-created")
end
// ---- ...and nothing ELSE flushes. A spurious flush loses a packet ----
if (t.ep_flush) begin
bit legitimate = t.vbus
&& (t.bus_reset
|| (t.set_config
&& (prev inside {S_ADDRESS, S_CONFIGURED})));
if (!legitimate)
`uvm_error("SPURIOUS_FLUSH",
"the endpoints were flushed with no reset and no SET_CONFIGURATION")
n_flush++;
end
// ---- Advance the scoreboard's own state, by the spec's rules ----
if (!t.vbus) begin
sb_state = S_ATTACHED; sb_saved = S_POWERED;
sb_addr = 0; sb_cfg = 0;
end else if (t.bus_reset) begin
sb_state = S_DEFAULT; sb_saved = S_DEFAULT;
sb_addr = 0; sb_cfg = 0;
end else if (prev == S_ATTACHED) begin
sb_state = S_POWERED; sb_saved = S_POWERED;
end else if (prev == S_SUSPENDED) begin
if (t.resume_event) begin
// THE resume property: we return to where we suspended FROM.
sb_state = sb_saved;
n_resume_to[int'(sb_saved)]++;
end
end else if (t.suspend_req) begin
sb_saved = prev; sb_state = S_SUSPENDED;
end else if (t.set_address && (prev inside {S_DEFAULT, S_ADDRESS})) begin
sb_addr = t.address_val;
sb_state = (t.address_val != 0) ? S_ADDRESS : S_DEFAULT;
end else if (t.set_config && (prev inside {S_ADDRESS, S_CONFIGURED})) begin
sb_cfg = t.config_val;
sb_state = (t.config_val != 0) ? S_CONFIGURED : S_ADDRESS;
end
// ---- Compare, from independently maintained state ----
if (t.state_after !== sb_state)
`uvm_error("STATE",
$sformatf("state=%s, scoreboard=%s", t.state_after.name(),
sb_state.name()))
if (t.addr_after !== sb_addr)
`uvm_error("ADDR", $sformatf("address=%0d, scoreboard=%0d",
t.addr_after, sb_addr))
if (t.cfg_after !== sb_cfg)
`uvm_error("CFG", $sformatf("config=%0d, scoreboard=%0d",
t.cfg_after, sb_cfg))
// ---- Structural invariants ----
if (t.endpoints_usable && (t.state_after != S_CONFIGURED))
`uvm_error("ENDPOINTS",
"endpoints reported usable outside CONFIGURED -- firmware may transfer on endpoints that do not exist")
if ((t.state_after == S_DEFAULT) && (t.addr_after != 0))
`uvm_error("ADDR", "DEFAULT with a non-zero address")
if ((t.state_after == S_SUSPENDED) && (t.saved_after == S_SUSPENDED))
`uvm_error("SUSPEND", "SUSPENDED remembers SUSPENDED -- resume never leaves")
endfunction
function void report_phase(uvm_phase phase);
`uvm_info("SB", $sformatf("flushes=%0d", n_flush), UVM_LOW)
// A reset must have been presented from EVERY state, including the one
// it is most likely to have been forgotten from.
foreach (n_reset_from[i])
if (n_reset_from[i] == 0)
`uvm_error("COVERAGE",
$sformatf("no bus reset was ever presented in state %0d", i))
if (n_reset_from[int'(S_CONFIGURED)] == 0)
`uvm_error("COVERAGE",
"no bus reset from CONFIGURED -- the transition most likely to be missing is untested")
// And resume must have returned to more than one state.
begin
int distinct = 0;
foreach (n_resume_to[i]) if (n_resume_to[i] > 0) distinct++;
if (distinct < 2)
`uvm_error("COVERAGE",
"every resume returned to the same state -- a design that always resumes to one state would pass this run")
end
endfunction
endclassThe last check in report_phase is the one worth stealing. "Resume returns to the state it suspended from" is untestable if every suspend in the run came from the same state — the run passes identically against a design that ignores saved_state entirely. Counting how many distinct states resume landed in turns that from an assumption into a measurement.
15.4 Functional coverage
covergroup dev_fsm_cg with function sample(
dev_state_e st, dev_state_e saved, bit vbus, bit rst, bit susp,
bit res, bit sa, bit [6:0] av, bit sc, bit [3:0] cv, bit flush);
cp_state : coverpoint st {
bins all[] = {S_ATTACHED, S_POWERED, S_DEFAULT, S_ADDRESS,
S_CONFIGURED, S_SUSPENDED};
}
cp_reset : coverpoint rst { bins none = {0}; bins reset = {1}; }
// THE cross. A bus reset from EVERY state, and the bin that matters is
// (CONFIGURED, reset) -- the transition a design built around the
// enumeration walk is most likely to be missing.
x_reset_from_anywhere : cross cp_state, cp_reset;
// Where a resume LANDS. Four reachable values, and a design that ignores
// saved_state closes only one of them.
cp_resume_to : coverpoint saved iff (st == S_SUSPENDED && res) {
bins to_powered = {S_POWERED};
bins to_default = {S_DEFAULT};
bins to_address = {S_ADDRESS};
bins to_configured = {S_CONFIGURED};
}
// The zero arguments, which are legal requests that move BACKWARDS.
cp_addr_zero : coverpoint (av == 0) iff (sa) {
bins de_address = {1};
bins address = {0};
}
cp_cfg_zero : coverpoint (cv == 0) iff (sc) {
bins un_configure = {1};
bins configure = {0};
}
// Both backward requests, from both states in which each is legal.
x_backward : cross cp_state, cp_addr_zero;
x_unconfig : cross cp_state, cp_cfg_zero;
// Every source of a flush, from every state.
cp_flush : coverpoint flush { bins flushing = {1}; bins quiet = {0}; }
x_flush_state : cross cp_flush, cp_state;
endgroup16. SystemVerilog Assertions
module usb_device_fsm_sva
import usb_devfsm_pkg::*;
(
input logic clk,
input logic rst_n,
input logic vbus,
input logic bus_reset,
input logic suspend_req,
input logic resume_event,
input logic set_address,
input logic [6:0] address_val,
input logic set_config,
input logic [3:0] config_val,
input dev_state_e state,
input logic [6:0] dev_address,
input logic [3:0] dev_config,
input logic ep_flush,
input logic endpoints_usable,
input logic suspended,
input dev_state_e saved_state
);
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// ---- 1. THE property. A bus reset with power flushes every endpoint. ----
property p_reset_flushes;
(vbus && bus_reset) |-> ep_flush;
endproperty
a_reset_flushes : assert property (p_reset_flushes)
else $error("a bus reset did not flush -- stale toggles and halts survive into the new session");
// ---- 2. ...and returns to DEFAULT, from ANY state. ----
property p_reset_returns_to_default;
(vbus && bus_reset) |=> ((state == S_DEFAULT) && (dev_address == 7'd0)
&& (dev_config == 4'd0));
endproperty
a_reset_returns_to_default : assert property (p_reset_returns_to_default)
else $error("a bus reset did not return the device to DEFAULT from %s",
$past(state.name()));
// ---- 3. SET_CONFIGURATION flushes too: it re-creates the endpoints. ----
property p_setconfig_flushes;
(vbus && set_config && (state inside {S_ADDRESS, S_CONFIGURED}))
|-> ep_flush;
endproperty
a_setconfig_flushes : assert property (p_setconfig_flushes);
// ---- 4. Nothing ELSE flushes. A spurious flush loses a packet just as
// ---- surely as a missing one.
property p_no_spurious_flush;
ep_flush |-> (vbus && (bus_reset
|| (set_config
&& (state inside {S_ADDRESS, S_CONFIGURED}))));
endproperty
a_no_spurious_flush : assert property (p_no_spurious_flush)
else $error("the endpoints were flushed with no reset and no SET_CONFIGURATION");
// ---- 5. THE resume property. Resume returns to where we suspended. ----
property p_resume_returns_to_saved;
((state == S_SUSPENDED) && resume_event && vbus && !bus_reset)
|=> (state == $past(saved_state));
endproperty
a_resume_returns_to_saved : assert property (p_resume_returns_to_saved)
else $error("resume landed in %s, expected %s -- the device will re-enumerate after every idle period",
state.name(), $past(saved_state.name()));
// ---- 6. Endpoints beyond endpoint 0 exist only when CONFIGURED. ----
property p_endpoints_only_when_configured;
endpoints_usable == (state == S_CONFIGURED);
endproperty
a_endpoints_only_when_configured :
assert property (p_endpoints_only_when_configured);
// ---- 7. DEFAULT means address zero, by definition. ----
property p_default_is_address_zero;
(state == S_DEFAULT) |-> (dev_address == 7'd0);
endproperty
a_default_is_address_zero : assert property (p_default_is_address_zero);
// ---- 8. CONFIGURED means a non-zero configuration. ----
property p_configured_has_a_config;
(state == S_CONFIGURED) |-> (dev_config != 4'd0);
endproperty
a_configured_has_a_config : assert property (p_configured_has_a_config);
// ---- 9. Nothing is retained without power. ----
property p_unpowered_retains_nothing;
!vbus |=> ((state == S_ATTACHED) && (dev_address == 7'd0)
&& (dev_config == 4'd0));
endproperty
a_unpowered_retains_nothing : assert property (p_unpowered_retains_nothing);
// ---- 10. SUSPENDED always has somewhere real to return to. ----
property p_saved_is_never_suspended;
(state == S_SUSPENDED) |-> (saved_state != S_SUSPENDED);
endproperty
a_saved_is_never_suspended : assert property (p_saved_is_never_suspended)
else $error("SUSPENDED remembers SUSPENDED -- resume would never leave");
// ---- 11. A flush never happens with no power. ----
property p_no_flush_unpowered;
!vbus |-> !ep_flush;
endproperty
a_no_flush_unpowered : assert property (p_no_flush_unpowered);
// ---- Cover: a reset from EVERY state, and resume to more than one. ----
c_reset_from_configured :
cover property ((vbus && bus_reset && (state == S_CONFIGURED)));
c_reset_from_suspended :
cover property ((vbus && bus_reset && (state == S_SUSPENDED)));
c_resume_to_configured :
cover property (((state == S_SUSPENDED) && resume_event
&& (saved_state == S_CONFIGURED)));
c_resume_to_address :
cover property (((state == S_SUSPENDED) && resume_event
&& (saved_state == S_ADDRESS)));
c_de_address : cover property ((set_address && (address_val == 7'd0)));
c_un_config : cover property ((set_config && (config_val == 4'd0)));
endmodule
bind usb_device_fsm usb_device_fsm_sva u_sva (.*);17. Common Misconceptions
"A bus reset only happens at plug-in." It happens whenever the host decides — driver reload, suspend/resume at the hub, error recovery upstream. A working, configured, actively-transferring device can be reset without warning.
"Resetting the state machine is resetting the device." The state machine is the easiest part. Toggles, halts, buffers and pointers all hold session state, and all of them have to go.
"Suspend is another state in the sequence." It is orthogonal: something that happens to a state. Resume returns to whichever state it left, or the device re-enumerates after every idle period.
"SET_ADDRESS(0) is an invalid request." It is how a host de-addresses a device without a bus reset. Same for SET_CONFIGURATION(0). Both move the machine one step backwards.
"Endpoints exist as soon as the device is addressed." Only endpoint 0 does. The rest are created by SET_CONFIGURATION, which is why it flushes them.
"A SET_CONFIGURATION to the configuration already selected is a no-op." It still re-creates the endpoints and still flushes. The specification is explicit, and firmware that reconfigures to the same value relies on the reset of the toggles.
"An extra flush is harmless — it just resets some toggles." It loses a packet, exactly as a missing flush does. See SVA property 4.
18. Exercises
1. Remove bus_reset from ep_flush (mutation J1) and predict which safety property fails first. Then explain why the count is ~49 500 rather than the ~1600 bus resets the suite generates.
2. J4's columns differ by 42% and it means nothing. Identify which of the other six mutations are also history-dependent, predict their seed variance, and measure one to check.
3. Add a SET_INTERFACE request, which resets the toggles of the endpoints in one interface but not the others. What does ep_flush become, and which of the eleven SVA properties need changing?
4. Properties 1 and 4 pin ep_flush from both sides. Find the pair of properties that pin endpoints_usable the same way, and show that property 6 alone already does the job — then explain what is different about ep_flush that needs two.
5. The scoreboard counts how many distinct states resume landed in. Write the equivalent guard for bus resets, and say what it would have caught in a suite that only ever reset from POWERED.
6. Wire this block to Chapter 21.1's usb_endpoint_ctrl through ep_flush → ep_reset and run 21.1's exhaustive sweep with resets injected from this FSM. Which of 21.1's seven mutations become easier to kill, and which become harder?
19. Summary
| Idea | Why it matters |
|---|---|
| Six states, and enumeration walks five of them | ATTACHED → POWERED → DEFAULT → ADDRESS → CONFIGURED |
| A bus reset arrives in any state | including CONFIGURED, which is the one that gets missed |
| ...and always returns to DEFAULT | address 0, no configuration |
| Returning to DEFAULT is not enough | toggles, halts, buffers and pointers all hold session state |
One ep_flush signal reaching every endpoint | a traceable net, not a convention |
SET_CONFIGURATION flushes too | it re-creates the endpoints |
| Nothing else may flush | a spurious flush loses a packet too |
| Suspend is orthogonal and must remember | resume returns to where it suspended from |
SET_ADDRESS(0) / SET_CONFIGURATION(0) are legal | they move the machine backwards |
| Endpoints beyond 0 exist only in CONFIGURED | |
| 1536-transition exhaustive verification | every state × every input combination |
| 7 mutations, all killed in 3 languages | and one apparent outlier that was pure seed variance |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o df_v.out df_v.v df_v_tb.v && ./df_v.out |
| SystemVerilog | iverilog -g2012 -o df_sv.out df_sv.sv df_sv_tb.sv && ./df_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a df_vhdl.vhd df_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_df_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_df_vhdl |
| One mutation | iverilog -g2005 -DMUT_J1 -o mm df_v_mut.v df_v_tb.v && ./mm |
All three implementations pass with 0 errors: 1536 of 1536 exhaustive transitions, 40 000 randomised cycles, every state visited and asserted visited.
20. Module 21 Complete
Five chapters, five synthesisable blocks, fifteen implementations, thirty-five mutations — all killed in all three languages.
| Chapter | Block | The structural idea |
|---|---|---|
| 21.1 | usb_endpoint_ctrl | one bit distinguishes a retransmission from new data |
| 21.2 | usb_ep_pingpong | a FIFO of packets, because a ZLP is a packet |
| 21.3 | usb_descriptor_engine | the host says how much it will take |
| 21.4 | usb_packet_engine | write provisionally; the CRC arrives last |
| 21.5 | usb_device_fsm | a reset must reach every register that holds session state |
Read together, they are five answers to the same question: what does a device do when it does not yet know something it needs?
The endpoint does not know whether a packet is new — so it keeps one bit that makes the question answerable. The FIFO does not know how many bytes a transfer contains — so it stores boundaries rather than bytes. The descriptor engine does not know how much the host wants — so it asks the request instead of assuming. The packet engine does not know whether the payload is good — so it writes provisionally and commits at the end. And the device FSM does not know when the host will give up on it — so it is ready to be reset at any instant, all the way down.
None of the five is a performance optimisation. Every one of them exists because information arrives later than the decision that needs it, and the difference between a device that works and one that mostly works is which of those five gaps you noticed.
Continue learning
Related tutorials
- Related topic
The Enumeration Question
Attach to configured, with the one step almost everybody gets backwards — SET_ADDRESS takes effect after the status stage, and a device that switches early is invisible to the host.
- Related topic
Endpoint Logic
A lost ACK and a lost data packet look identical to the host, so it resends the same bytes — and the data toggle is the only thing that tells a device a retransmission from new data.
- Related topic
FIFO Architecture
An endpoint FIFO stores packets, not bytes — a zero-length packet carries nothing and must still occupy a buffer, because it is the only thing that terminates a transfer ending on a packet boundary.
- Related topic
Descriptor Engine
wLength is the size of the host's buffer, not a preference — and whether a zero-length packet must follow depends on comparing what was sent against what was asked for, not against what exists.
Standards & specifications
- Governing standard
- USB-IF (Universal Serial Bus Specification)(opens USB Implementers Forum (USB-IF) in a new tab)
Defines the USB bus — its electrical signalling, connectors, packet and transaction model, device framework and the descriptors a device must expose — together with the device-class specifications layered on it. It does not define host-controller register interfaces (xHCI and EHCI are separate documents) nor any operating system's driver architecture.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the USB curriculum.
