USB · Module 27
The Endpoints Question
Endpoint 1 IN and endpoint 1 OUT are two different endpoints — separate buffers, toggles, halt states and packet sizes — and a table indexed by number alone is a device where halting a read kills its writes.
Chapter 27.3 ended with SET_CONFIGURATION and endpoints becoming usable. This is the question that follows, and it has a stock answer that is correct and incomplete in a way that matters.
1. The Question
"What is an endpoint?"
The stock answer: a logical channel on a device, identified by a number 0 to 15, with a direction and a maximum packet size, and a transfer type set by its descriptor. Every word of that is true. It will get you through a screening call.
It will not get you through a design interview, because it hides the thing the question is really testing.
2. Why This Is the Question
Because it is where the bugs are. A device controller that indexes its endpoint table by number alone compiles, enumerates, passes casual testing, and then:
- halting endpoint 1 IN silently halts endpoint 1 OUT, so a driver that stalls a broken read direction kills its writes as well;
- the two directions share a data toggle, so traffic in one direction desynchronises the other and packets are silently dropped as duplicates;
- an
INtransfer uses theOUTdirection's maximum packet size, which is usually the same number and occasionally is not.
None of those produce an error. All of them produce a device that works until it does not.
Indexed by NUMBER: Indexed by (NUMBER, DIRECTION):
table[4] table[4][2]
halt(1) -> kills both halt(1, IN) -> kills IN only
toggle(1) shared toggle(1, IN) independent
maxp(1) shared maxp(1, IN) independent
One of these is a device. The other is a device that
works until somebody halts an endpoint.3. Endpoint 0 Is the Exception That Proves the Rule
Endpoint 0 is genuinely bidirectional, and for a specific reason: a control transfer is one conversation that runs both ways. The SETUP goes out, the data may go either way, the status stage comes back — and all of it is one logical exchange, not two pipes that happen to share a number.
So EP0's two halves share a packet size, share a configuration, and exist from the moment the device powers up. They have to: EP0 is how the host talks to a device it knows nothing about, before any descriptor has been read.
4. What We Are Building
usb_ep_table is the lookup that sits between a token arriving off the bus and the endpoint state that answers it. Four endpoint numbers, two directions, and the whole design is the insistence that those are eight endpoints rather than four.
Four numbers, eight endpoints
EP1 IN is halted and EP1 OUT is running, at the same moment, on the same endpoint number. That is the normal, correct state of a device whose read direction has failed and whose write direction has not.
Halting one direction, and the other carrying on
ep_halted alternates with tok_in from cycle 2 onward — high when the lookup names the IN direction, low when it names OUT. That alternation is the entire property, and a table indexed by number alone would show it high in both.
5. Seven Properties
| # | Property |
|---|---|
| 1 | A lookup reports the state of that (number, direction) and no other. |
| 2 | Halting one direction leaves the other's halt state untouched. |
| 3 | The two directions have independent data toggles. |
| 4 | The two directions have independent maximum packet sizes. |
| 5 | Endpoint 0 is bidirectional: configuring it configures both halves. |
| 6 | Endpoint 0 is not haltable, because clearing a halt requires it. |
| 7 | An endpoint that is out of range or unconfigured reports invalid, with packet size 0. |
6. Verilog-2005 RTL
// =====================================================================
// usb_ep_table -- "Explain endpoints" answered in hardware.
//
// The answer everybody gives is "a logical channel, numbered 0 to 15,
// with a direction and a maximum packet size". All true, and it hides
// the fact the question is actually about:
//
// Endpoint 1 IN and endpoint 1 OUT are TWO DIFFERENT ENDPOINTS.
//
// Separate buffers, separate data toggles, separate halt states,
// separate max packet sizes. The number is not an identifier; the pair
// (number, direction) is. A device with "four endpoints" may have four
// or eight of them depending on what the descriptors say, and a table
// indexed by number alone is a device where halting the IN direction
// silently halts the OUT direction with it.
//
// Endpoint 0 is the exception that proves the rule: it is the one
// endpoint that is genuinely bidirectional, because control transfers
// need both directions of the SAME conversation.
// =====================================================================
module usb_ep_table #(
parameter N_NUM = 4 // endpoint numbers 0..N_NUM-1
) (
input wire clk,
input wire rst_n,
// ---- a token, as it arrives off the bus ----
input wire tok_valid,
input wire [3:0] tok_ep, // endpoint NUMBER
input wire tok_in, // direction: 1 = IN (device to host)
// ---- configuration, from the descriptors ----
input wire cfg_wr,
input wire [3:0] cfg_ep,
input wire cfg_in,
input wire cfg_enable,
input wire [10:0] cfg_maxp,
// ---- halt control, per (number, direction) ----
input wire halt_wr,
input wire [3:0] halt_ep,
input wire halt_in,
input wire halt_set,
// ---- a transaction completing, which advances the toggle ----
input wire xact_ack,
// ---- lookup results for the token above ----
output wire ep_valid, // this (number, direction) exists
output wire ep_halted,
output wire [10:0] ep_maxp,
output wire ep_toggle,
// ---- observability ----
output wire [31:0] n_lookup,
output wire [31:0] n_unknown_ep,
output wire [31:0] n_halted_hit,
output wire [31:0] n_toggle_flip,
// Evidence that the two directions were driven INDEPENDENTLY: a halt
// applied to one direction while the other stayed unhalted. This is a
// coverage counter, not a self-check -- the design cannot detect its own
// cross-direction leak, because a leak is indistinguishable from a
// deliberate write from inside the module. The TESTBENCH does that, with
// an independent shadow of both directions.
output wire [31:0] n_split_halt
);
// ---- the table is indexed by (number, direction), never by number ----
//
// Two separate arrays rather than one array of structs, so that an index
// that forgets the direction cannot compile into something plausible.
// IN and OUT are not two views of one endpoint; they are two endpoints.
reg en_in [0:N_NUM-1];
reg en_out [0:N_NUM-1];
reg hl_in [0:N_NUM-1];
reg hl_out [0:N_NUM-1];
reg [10:0] mp_in [0:N_NUM-1];
reg [10:0] mp_out [0:N_NUM-1];
reg tg_in [0:N_NUM-1];
reg tg_out [0:N_NUM-1];
reg [31:0] look_c, unk_c, halt_c, flip_c, split_c;
// ---- endpoint 0 is bidirectional, and it is the ONLY one ----
//
// A control transfer is one conversation that runs both ways, so EP0's
// two directions share a halt state and a packet size by definition.
// Every other endpoint number describes two independent pipes that
// merely happen to be written with the same digit.
wire in_range = tok_valid && (tok_ep < N_NUM);
wire is_ep0 = (tok_ep == 4'd0);
wire sel_in = tok_in;
// The lookup. Note that every read is indexed by BOTH tok_ep and the
// direction -- there is no path in this module that reads a table entry
// without having decided which direction it wants.
wire e_valid = in_range && (sel_in ? en_in[tok_ep] : en_out[tok_ep]);
wire e_halted = in_range && (sel_in ? hl_in[tok_ep] : hl_out[tok_ep]);
wire [10:0] e_maxp = in_range ? (sel_in ? mp_in[tok_ep] : mp_out[tok_ep])
: 11'd0;
wire e_toggle = in_range && (sel_in ? tg_in[tok_ep] : tg_out[tok_ep]);
assign ep_valid = e_valid;
assign ep_halted = e_halted;
assign ep_maxp = e_valid ? e_maxp : 11'd0;
assign ep_toggle = e_toggle;
assign n_lookup = look_c;
assign n_unknown_ep = unk_c;
assign n_halted_hit = halt_c;
assign n_toggle_flip = flip_c;
assign n_split_halt = split_c;
integer i;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (i = 0; i < N_NUM; i = i + 1) begin
// Endpoint 0 exists in both directions from the moment the device
// powers up -- it is how the host talks to a device it knows
// nothing about. Every other endpoint exists only once a
// descriptor has said so.
en_in[i] <= (i == 0);
en_out[i] <= (i == 0);
hl_in[i] <= 1'b0;
hl_out[i] <= 1'b0;
mp_in[i] <= (i == 0) ? 11'd64 : 11'd0;
mp_out[i] <= (i == 0) ? 11'd64 : 11'd0;
tg_in[i] <= 1'b0;
tg_out[i] <= 1'b0;
end
look_c <= 32'd0;
unk_c <= 32'd0;
halt_c <= 32'd0;
flip_c <= 32'd0;
split_c <= 32'd0;
end else begin
// ---- configuration writes one direction of one number ----
if (cfg_wr && (cfg_ep < N_NUM)) begin
if (cfg_ep == 4'd0) begin
// EP0 is bidirectional: configuring it configures both halves,
// because they are one conversation rather than two pipes.
en_in[cfg_ep] <= cfg_enable;
en_out[cfg_ep] <= cfg_enable;
mp_in[cfg_ep] <= cfg_maxp;
mp_out[cfg_ep] <= cfg_maxp;
end else if (cfg_in) begin
en_in[cfg_ep] <= cfg_enable;
mp_in[cfg_ep] <= cfg_maxp;
end else begin
en_out[cfg_ep] <= cfg_enable;
mp_out[cfg_ep] <= cfg_maxp;
end
end
// ---- halt applies to ONE direction ----
//
// Except on EP0, where a halt would make the device unrecoverable:
// clearing a halt is itself a control transfer, so EP0 is not
// haltable at all.
if (halt_wr && (halt_ep < N_NUM) && (halt_ep != 4'd0)) begin
if (halt_in) hl_in[halt_ep] <= halt_set;
else hl_out[halt_ep] <= halt_set;
end
// ---- a completed transaction flips ONE toggle ----
if (tok_valid && in_range && e_valid && !e_halted && xact_ack) begin
if (sel_in) tg_in[tok_ep] <= ~tg_in[tok_ep];
else tg_out[tok_ep] <= ~tg_out[tok_ep];
flip_c <= flip_c + 32'd1;
end
// ---- counters ----
if (tok_valid) begin
look_c <= look_c + 32'd1;
if (!in_range || !e_valid) unk_c <= unk_c + 32'd1;
if (e_halted) halt_c <= halt_c + 32'd1;
end
// ---- coverage: the directions were driven SEPARATELY ----
//
// A halt set on one direction of a number whose other direction is
// not halted. If this reads zero, every halt in the run moved both
// directions together and the independence claim was never tested --
// which is the whole point of the chapter, so the number is published.
if (halt_wr && (halt_ep < N_NUM) && (halt_ep != 4'd0) && halt_set) begin
if (halt_in && !hl_out[halt_ep]) split_c <= split_c + 32'd1;
if (!halt_in && !hl_in[halt_ep]) split_c <= split_c + 32'd1;
end
end
end
endmodule7. SystemVerilog RTL
// =====================================================================
// usb_ep_table -- SystemVerilog.
//
// The eight parallel arrays of the Verilog become ONE array of a struct
// indexed by direction, which is the right shape: an endpoint's state
// belongs together, and the direction is part of its address rather
// than a reason to duplicate every field.
//
// It also makes the central claim structural. `ep[num][dir]` cannot be
// written without naming a direction, so the cross-direction leak this
// chapter is about becomes a thing you have to go out of your way to
// write rather than a thing you can do by forgetting an index.
//
// The answer everybody gives is "a logical channel, numbered 0 to 15,
// with a direction and a maximum packet size". All true, and it hides
// the fact the question is actually about:
//
// Endpoint 1 IN and endpoint 1 OUT are TWO DIFFERENT ENDPOINTS.
//
// Separate buffers, separate data toggles, separate halt states,
// separate max packet sizes. The number is not an identifier; the pair
// (number, direction) is. A device with "four endpoints" may have four
// or eight of them depending on what the descriptors say, and a table
// indexed by number alone is a device where halting the IN direction
// silently halts the OUT direction with it.
//
// Endpoint 0 is the exception that proves the rule: it is the one
// endpoint that is genuinely bidirectional, because control transfers
// need both directions of the SAME conversation.
// =====================================================================
module usb_ep_table #(
parameter int N_NUM = 4 // endpoint numbers 0..N_NUM-1
) (
input logic clk,
input logic rst_n,
// ---- a token, as it arrives off the bus ----
input logic tok_valid,
input logic [3:0] tok_ep, // endpoint NUMBER
input logic tok_in, // direction: 1 = IN (device to host)
// ---- configuration, from the descriptors ----
input logic cfg_wr,
input logic [3:0] cfg_ep,
input logic cfg_in,
input logic cfg_enable,
input logic [10:0]cfg_maxp,
// ---- halt control, per (number, direction) ----
input logic halt_wr,
input logic [3:0] halt_ep,
input logic halt_in,
input logic halt_set,
// ---- a transaction completing, which advances the toggle ----
input logic xact_ack,
// ---- lookup results for the token above ----
output logic ep_valid, // this (number, direction) exists
output logic ep_halted,
output logic [10:0]ep_maxp,
output logic ep_toggle,
// ---- observability ----
output logic [31:0]n_lookup,
output logic [31:0]n_unknown_ep,
output logic [31:0]n_halted_hit,
output logic [31:0]n_toggle_flip,
// Evidence that the two directions were driven INDEPENDENTLY: a halt
// applied to one direction while the other stayed unhalted. This is a
// coverage counter, not a self-check -- the design cannot detect its own
// cross-direction leak, because a leak is indistinguishable from a
// deliberate write from inside the module. The TESTBENCH does that, with
// an independent shadow of both directions.
output logic [31:0]n_split_halt
);
// ---- the table is indexed by (number, direction), never by number ----
//
// Two separate arrays rather than one array of structs, so that an index
// that forgets the direction cannot compile into something plausible.
// IN and OUT are not two views of one endpoint; they are two endpoints.
// Every field is indexed [number][direction]. Writing one of these
// without naming a direction is a compile error, which is exactly the
// property this chapter is about: the cross-direction leak becomes
// something you have to go out of your way to write.
//
// DIR_OUT is 0 and DIR_IN is 1, matching the token's direction bit, so
// the token's own field is the index with no translation.
//
// A packed struct would read better still, and Icarus Verilog 13 aborts
// its elaborator on a variable field-select into a 2-D unpacked array of
// one -- so the fields are separate arrays that share an index shape.
localparam int DIR_OUT = 0, DIR_IN = 1;
logic st_en [N_NUM][2];
logic st_hl [N_NUM][2];
logic [10:0] st_mp [N_NUM][2];
logic st_tg [N_NUM][2];
logic [31:0] look_c, unk_c, halt_c, flip_c, split_c;
// ---- endpoint 0 is bidirectional, and it is the ONLY one ----
//
// A control transfer is one conversation that runs both ways, so EP0's
// two directions share a halt state and a packet size by definition.
// Every other endpoint number describes two independent pipes that
// merely happen to be written with the same digit.
wire in_range = tok_valid && (tok_ep < N_NUM);
wire is_ep0 = (tok_ep == 4'd0);
wire sel_in = tok_in;
wire [0:0] d = tok_in; // the direction index
// The lookup. Note that every read is indexed by BOTH tok_ep and the
// direction -- there is no path in this module that reads a table entry
// without having decided which direction it wants.
wire e_valid = in_range && st_en[tok_ep][d];
wire e_halted = in_range && st_hl[tok_ep][d];
wire [10:0] e_maxp = in_range ? st_mp[tok_ep][d] : 11'd0;
wire e_toggle = in_range && st_tg[tok_ep][d];
assign ep_valid = e_valid;
assign ep_halted = e_halted;
assign ep_maxp = e_valid ? e_maxp : 11'd0;
assign ep_toggle = e_toggle;
assign n_lookup = look_c;
assign n_unknown_ep = unk_c;
assign n_halted_hit = halt_c;
assign n_toggle_flip = flip_c;
assign n_split_halt = split_c;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < N_NUM; i++) begin
// Endpoint 0 exists in both directions from the moment the device
// powers up -- it is how the host talks to a device it knows
// nothing about. Every other endpoint exists only once a
// descriptor has said so.
for (int j = 0; j < 2; j++) begin
st_en[i][j] <= (i == 0);
st_hl[i][j] <= 1'b0;
st_mp[i][j] <= (i == 0) ? 11'd64 : 11'd0;
st_tg[i][j] <= 1'b0;
end
end
look_c <= 32'd0;
unk_c <= 32'd0;
halt_c <= 32'd0;
flip_c <= 32'd0;
split_c <= 32'd0;
end else begin
// ---- configuration writes one direction of one number ----
if (cfg_wr && (cfg_ep < N_NUM)) begin
if (cfg_ep == 4'd0) begin
// EP0 is bidirectional: configuring it configures both halves,
// because they are one conversation rather than two pipes.
for (int j = 0; j < 2; j++) begin
st_en[cfg_ep][j] <= cfg_enable;
st_mp[cfg_ep][j] <= cfg_maxp;
end
end else begin
st_en[cfg_ep][cfg_in] <= cfg_enable;
st_mp[cfg_ep][cfg_in] <= cfg_maxp;
end
end
// ---- halt applies to ONE direction ----
//
// Except on EP0, where a halt would make the device unrecoverable:
// clearing a halt is itself a control transfer, so EP0 is not
// haltable at all.
if (halt_wr && (halt_ep < N_NUM) && (halt_ep != 4'd0)) begin
st_hl[halt_ep][halt_in] <= halt_set;
end
// ---- a completed transaction flips ONE toggle ----
if (tok_valid && in_range && e_valid && !e_halted && xact_ack) begin
st_tg[tok_ep][d] <= ~st_tg[tok_ep][d];
flip_c <= flip_c + 32'd1;
end
// ---- counters ----
if (tok_valid) begin
look_c <= look_c + 32'd1;
if (!in_range || !e_valid) unk_c <= unk_c + 32'd1;
if (e_halted) halt_c <= halt_c + 32'd1;
end
// ---- coverage: the directions were driven SEPARATELY ----
//
// A halt set on one direction of a number whose other direction is
// not halted. If this reads zero, every halt in the run moved both
// directions together and the independence claim was never tested --
// which is the whole point of the chapter, so the number is published.
if (halt_wr && (halt_ep < N_NUM) && (halt_ep != 4'd0) && halt_set) begin
if (!st_hl[halt_ep][!halt_in]) split_c <= split_c + 32'd1;
end
end
end
endmodule8. VHDL-2008 RTL
-- =====================================================================
-- usb_ep_table -- VHDL-2008.
--
-- VHDL gets the shape the SystemVerilog wanted: a record per endpoint
-- state, in a two-dimensional array indexed by (number, direction).
-- There is no way to write an entry without naming a direction, which
-- is the property this chapter is about -- a cross-direction leak has
-- to be written deliberately rather than by forgetting an index.
--
-- The Verilog carries eight parallel one-dimensional arrays instead,
-- because Verilog-2005 has neither records nor multi-dimensional
-- arrays of them. Eight arrays that must be kept in step is exactly
-- the situation in which a direction gets forgotten.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package ep_pkg is
-- DIR_OUT is 0 and DIR_IN is 1, matching the token's direction bit, so
-- the token's own field indexes the table with no translation.
constant DIR_OUT : natural := 0;
constant DIR_IN : natural := 1;
type ep_state_t is record
en : std_logic;
halted : std_logic;
maxp : unsigned(10 downto 0);
toggle : std_logic;
end record;
type ep_table_t is array (natural range <>, natural range <>) of ep_state_t;
end package;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.ep_pkg.all;
entity usb_ep_table is
generic (
N_NUM : natural := 4
);
port (
clk : in std_logic;
rst_n : in std_logic;
tok_valid : in std_logic;
tok_ep : in std_logic_vector(3 downto 0);
tok_in : in std_logic;
cfg_wr : in std_logic;
cfg_ep : in std_logic_vector(3 downto 0);
cfg_in : in std_logic;
cfg_enable : in std_logic;
cfg_maxp : in std_logic_vector(10 downto 0);
halt_wr : in std_logic;
halt_ep : in std_logic_vector(3 downto 0);
halt_in : in std_logic;
halt_set : in std_logic;
xact_ack : in std_logic;
ep_valid : out std_logic;
ep_halted : out std_logic;
ep_maxp : out std_logic_vector(10 downto 0);
ep_toggle : out std_logic;
n_lookup : out std_logic_vector(31 downto 0);
n_unknown_ep : out std_logic_vector(31 downto 0);
n_halted_hit : out std_logic_vector(31 downto 0);
n_toggle_flip : out std_logic_vector(31 downto 0);
n_split_halt : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of usb_ep_table is
signal tbl : ep_table_t(0 to N_NUM-1, 0 to 1);
signal look_c, unk_c, halt_c, flip_c, split_c : unsigned(31 downto 0)
:= (others => '0');
signal in_range : std_logic;
signal e_valid, e_halted, e_toggle : std_logic;
signal e_maxp : unsigned(10 downto 0);
-- The direction index, derived once from the token's own bit.
function dir_of(b : std_logic) return natural is
begin
if b = '1' then return DIR_IN; else return DIR_OUT; end if;
end function;
begin
in_range <= '1' when (tok_valid = '1' and
to_integer(unsigned(tok_ep)) < N_NUM) else '0';
-- Every read names BOTH the number and the direction. There is no path
-- through this architecture that reads a table entry without one.
-- The bound check is made INSIDE this process, from tok_ep directly.
-- Gating on the separate `in_range` signal is a crash rather than a
-- mis-read: a signal lags its inputs within a delta cycle, so tok_ep can
-- already be 13 while in_range is still '1' from the previous value, and
-- VHDL's range checking then aborts the run rather than returning
-- rubbish the way Verilog would.
lookup : process(all)
variable n, d : natural;
begin
n := to_integer(unsigned(tok_ep));
if tok_valid = '1' and n < N_NUM then
d := dir_of(tok_in);
e_valid <= tbl(n, d).en;
e_halted <= tbl(n, d).halted;
e_maxp <= tbl(n, d).maxp;
e_toggle <= tbl(n, d).toggle;
else
e_valid <= '0';
e_halted <= '0';
e_maxp <= (others => '0');
e_toggle <= '0';
end if;
end process;
ep_valid <= e_valid;
ep_halted <= e_halted;
ep_toggle <= e_toggle;
ep_maxp <= std_logic_vector(e_maxp) when e_valid = '1'
else (others => '0');
n_lookup <= std_logic_vector(look_c);
n_unknown_ep <= std_logic_vector(unk_c);
n_halted_hit <= std_logic_vector(halt_c);
n_toggle_flip <= std_logic_vector(flip_c);
n_split_halt <= std_logic_vector(split_c);
main : process(clk, rst_n)
variable cn, hn, tn, cd, hd, td : natural;
begin
if rst_n = '0' then
for i in 0 to N_NUM-1 loop
for j in 0 to 1 loop
-- Endpoint 0 exists in both directions from power-up: it is how
-- the host talks to a device it knows nothing about. Every other
-- endpoint exists only once a descriptor has said so.
if i = 0 then
tbl(i, j).en <= '1';
tbl(i, j).maxp <= to_unsigned(64, 11);
else
tbl(i, j).en <= '0';
tbl(i, j).maxp <= (others => '0');
end if;
tbl(i, j).halted <= '0';
tbl(i, j).toggle <= '0';
end loop;
end loop;
look_c <= (others => '0');
unk_c <= (others => '0');
halt_c <= (others => '0');
flip_c <= (others => '0');
split_c <= (others => '0');
elsif rising_edge(clk) then
-- ---- configuration writes one direction of one number ----
if cfg_wr = '1' and to_integer(unsigned(cfg_ep)) < N_NUM then
cn := to_integer(unsigned(cfg_ep));
cd := dir_of(cfg_in);
if cn = 0 then
-- EP0 is bidirectional: configuring it configures both halves,
-- because they are one conversation rather than two pipes.
for j in 0 to 1 loop
tbl(cn, j).en <= cfg_enable;
tbl(cn, j).maxp <= unsigned(cfg_maxp);
end loop;
else
tbl(cn, cd).en <= cfg_enable;
tbl(cn, cd).maxp <= unsigned(cfg_maxp);
end if;
end if;
-- ---- halt applies to ONE direction ----
--
-- Except on EP0, where a halt would make the device unrecoverable:
-- clearing a halt is itself a control transfer.
if halt_wr = '1' and to_integer(unsigned(halt_ep)) < N_NUM
and to_integer(unsigned(halt_ep)) /= 0 then
hn := to_integer(unsigned(halt_ep));
hd := dir_of(halt_in);
tbl(hn, hd).halted <= halt_set;
end if;
-- ---- coverage: the directions were driven SEPARATELY ----
--
-- A halt SET on one direction whose other direction is not halted.
-- Zero here means every halt in the run moved both directions
-- together and the independence claim was never tested.
--
-- Deliberately its OWN block with its own endpoint-0 exclusion, so
-- that it matches the Verilog statement for statement: folding it
-- inside the halt-write block above gave mutation D4 an extra effect
-- in VHDL that it does not have in Verilog, and the two directed
-- columns then read 296 against 319.
if halt_wr = '1' and to_integer(unsigned(halt_ep)) < N_NUM
and to_integer(unsigned(halt_ep)) /= 0 and halt_set = '1' then
hn := to_integer(unsigned(halt_ep));
hd := dir_of(halt_in);
if tbl(hn, 1 - hd).halted = '0' then
split_c <= split_c + 1;
end if;
end if;
-- ---- a completed transaction flips ONE toggle ----
if tok_valid = '1' and in_range = '1' and e_valid = '1'
and e_halted = '0' and xact_ack = '1' then
tn := to_integer(unsigned(tok_ep));
td := dir_of(tok_in);
tbl(tn, td).toggle <= not tbl(tn, td).toggle;
flip_c <= flip_c + 1;
end if;
-- ---- counters ----
if tok_valid = '1' then
look_c <= look_c + 1;
if in_range = '0' or e_valid = '0' then unk_c <= unk_c + 1; end if;
if e_halted = '1' then halt_c <= halt_c + 1; end if;
end if;
end if;
end process;
end architecture;9. The Testbench, and the Only Check That Can See a Leak
The headline property is negative and it is about independence, so the bench does something deliberately expensive: after every single operation it compares both directions of every endpoint number. Not the direction that was written. All of them.
Checking only the targeted direction:
halt(1, IN); check halted(1, IN) == 1 -- PASSES
A design that halts both directions passes that check
perfectly. The write landed correctly. It also landed
somewhere nobody looked.
Checking every direction of every number:
halt(1, IN);
for n in 0..3: for d in {IN, OUT}:
compare against an INDEPENDENT shadow -- FAILSThe shadow keeps the two directions in completely separate arrays — s_en_in / s_en_out and so on — and never merges them, so a leak in the design cannot be mirrored by a leak in the model.
Verilog-2005 testbench
// =====================================================================
// Testbench for usb_ep_table.
//
// The headline property is a NEGATIVE one about independence: touching
// one direction of an endpoint number must never move the other. So the
// shadow keeps the two directions in completely separate arrays and, after
// every single operation, compares BOTH directions of EVERY number --
// not only the one that was targeted.
//
// Checking only the targeted direction is how a cross-direction leak
// survives a testbench: the write landed correctly, and it also landed
// somewhere nobody looked.
// =====================================================================
`timescale 1ns/1ps
module tb_ep_v;
localparam N_NUM = 4;
reg clk = 1'b0, rst_n = 1'b0;
reg tok_valid = 1'b0;
reg [3:0] tok_ep = 4'd0;
reg tok_in = 1'b0;
reg cfg_wr = 1'b0;
reg [3:0] cfg_ep = 4'd0;
reg cfg_in = 1'b0;
reg cfg_enable = 1'b0;
reg [10:0] cfg_maxp = 11'd0;
reg halt_wr = 1'b0;
reg [3:0] halt_ep = 4'd0;
reg halt_in = 1'b0;
reg halt_set = 1'b0;
reg xact_ack = 1'b0;
wire ep_valid, ep_halted, ep_toggle;
wire [10:0] ep_maxp;
wire [31:0] n_lookup, n_unknown_ep, n_halted_hit, n_toggle_flip, n_split_halt;
usb_ep_table #(.N_NUM(N_NUM)) dut (
.clk(clk), .rst_n(rst_n),
.tok_valid(tok_valid), .tok_ep(tok_ep), .tok_in(tok_in),
.cfg_wr(cfg_wr), .cfg_ep(cfg_ep), .cfg_in(cfg_in),
.cfg_enable(cfg_enable), .cfg_maxp(cfg_maxp),
.halt_wr(halt_wr), .halt_ep(halt_ep), .halt_in(halt_in),
.halt_set(halt_set),
.xact_ack(xact_ack),
.ep_valid(ep_valid), .ep_halted(ep_halted),
.ep_maxp(ep_maxp), .ep_toggle(ep_toggle),
.n_lookup(n_lookup), .n_unknown_ep(n_unknown_ep),
.n_halted_hit(n_halted_hit), .n_toggle_flip(n_toggle_flip),
.n_split_halt(n_split_halt)
);
always #5 clk = ~clk;
integer errors = 0, checks = 0, steps = 0;
integer seed;
// ---- $random is SIGNED: mask the sign bit before any modulo ----
function [31:0] urand;
input dummy;
begin urand = $random(seed) & 32'h3FFF_FFFF; end
endfunction
// ---- the shadow: two directions, two sets of arrays, never merged ----
reg s_en_in [0:N_NUM-1];
reg s_en_out [0:N_NUM-1];
reg s_hl_in [0:N_NUM-1];
reg s_hl_out [0:N_NUM-1];
reg [10:0] s_mp_in [0:N_NUM-1];
reg [10:0] s_mp_out [0:N_NUM-1];
reg s_tg_in [0:N_NUM-1];
reg s_tg_out [0:N_NUM-1];
reg [31:0] x_look, x_unk, x_halt, x_flip, x_split;
// Run-wide totals. The DUT's counters are cleared by every reset, so a
// summary printed from them describes the last window rather than the run.
integer g_look = 0, g_unk = 0, g_halt = 0, g_flip = 0, g_split = 0;
// ---- the headline counter ----
//
// Every cycle in which any direction of any number disagreed with the
// shadow. A cross-direction leak shows up here and nowhere else, because
// the direction that was written is always correct.
integer n_cross = 0;
// ---- exhaustive reach over (number, dir, enabled, halted, toggle) ----
reg reach [0:63];
integer ri, n_reach;
task ck(input cond, input [255:0] what);
begin
checks = checks + 1;
if (!cond) begin
errors = errors + 1;
if (errors <= 20)
$display(" ERROR @%0t step=%0d: %0s", $time, steps, what);
end
end
endtask
// ---------------------------------------------------------------
// Compare EVERY direction of EVERY number against the shadow.
//
// Called after every operation. This is the expensive way to do it
// and it is the only way that can see a leak.
// ---------------------------------------------------------------
task audit_all;
integer a;
begin
for (a = 0; a < N_NUM; a = a + 1) begin
// drive a lookup at (a, IN) and check it, then (a, OUT)
tok_valid = 1'b1; tok_ep = a[3:0]; tok_in = 1'b1;
#1;
if (ep_valid !== s_en_in[a]) n_cross = n_cross + 1;
if (ep_halted !== s_hl_in[a]) n_cross = n_cross + 1;
if (ep_toggle !== s_tg_in[a]) n_cross = n_cross + 1;
ck(ep_valid === s_en_in[a], "IN enable disagrees");
ck(ep_halted === s_hl_in[a], "IN halt disagrees");
ck(ep_toggle === s_tg_in[a], "IN toggle disagrees");
ck(ep_maxp === (s_en_in[a] ? s_mp_in[a] : 11'd0),
"IN maxp disagrees");
tok_in = 1'b0;
#1;
if (ep_valid !== s_en_out[a]) n_cross = n_cross + 1;
if (ep_halted !== s_hl_out[a]) n_cross = n_cross + 1;
if (ep_toggle !== s_tg_out[a]) n_cross = n_cross + 1;
ck(ep_valid === s_en_out[a], "OUT enable disagrees");
ck(ep_halted === s_hl_out[a], "OUT halt disagrees");
ck(ep_toggle === s_tg_out[a], "OUT toggle disagrees");
ck(ep_maxp === (s_en_out[a] ? s_mp_out[a] : 11'd0),
"OUT maxp disagrees");
end
tok_valid = 1'b0;
ck(n_cross == 0, "a direction changed that nothing touched");
end
endtask
// ---------------------------------------------------------------
// A combinational lookup with NO clocked side effect.
//
// Positioned at a negedge deliberately. Driving tok_valid and then
// waiting #1 lands exactly on the next posedge once the preceding audit
// has consumed its 8 ns -- the DUT then counts a lookup the shadow knows
// nothing about. Two errors, entirely a testbench timing accident.
// ---------------------------------------------------------------
task peek(input [3:0] e, input d);
begin
@(negedge clk);
tok_valid = 1'b1; tok_ep = e; tok_in = d;
#1;
end
endtask
// ---------------------------------------------------------------
// One clocked operation, then a full audit.
// ---------------------------------------------------------------
task op(input cw, input [3:0] ce, input ci, input cen, input [10:0] cm,
input hw, input [3:0] he, input hi, input hs,
input tv, input [3:0] te, input ti, input xa);
begin
cfg_wr = cw; cfg_ep = ce; cfg_in = ci;
cfg_enable = cen; cfg_maxp = cm;
halt_wr = hw; halt_ep = he; halt_in = hi; halt_set = hs;
tok_valid = tv; tok_ep = te; tok_in = ti; xact_ack = xa;
// ---- advance the shadow ----
//
// The LOOKUP is evaluated first, because the design's lookup is a
// combinational read of REGISTERED table entries -- it sees the
// values as they stood before this edge. A shadow that applies the
// configuration write first sees the new values and disagrees on
// every cycle that configures and looks up at once.
if (tv && (te < N_NUM)) begin
x_look = x_look + 1; g_look = g_look + 1;
if (ti) begin
if (!s_en_in[te]) begin x_unk = x_unk + 1; g_unk = g_unk + 1; end
if (s_hl_in[te]) begin x_halt = x_halt + 1; g_halt = g_halt + 1; end
if (s_en_in[te] && !s_hl_in[te] && xa) begin
s_tg_in[te] = ~s_tg_in[te];
x_flip = x_flip + 1; g_flip = g_flip + 1;
end
end else begin
if (!s_en_out[te]) begin x_unk = x_unk + 1; g_unk = g_unk + 1; end
if (s_hl_out[te]) begin x_halt = x_halt + 1; g_halt = g_halt + 1; end
if (s_en_out[te] && !s_hl_out[te] && xa) begin
s_tg_out[te] = ~s_tg_out[te];
x_flip = x_flip + 1; g_flip = g_flip + 1;
end
end
end else if (tv) begin
x_look = x_look + 1; g_look = g_look + 1;
x_unk = x_unk + 1; g_unk = g_unk + 1;
end
if (cw && (ce < N_NUM)) begin
if (ce == 4'd0) begin
// EP0 is bidirectional: one conversation, both halves
s_en_in[ce] = cen; s_en_out[ce] = cen;
s_mp_in[ce] = cm; s_mp_out[ce] = cm;
end else if (ci) begin
s_en_in[ce] = cen; s_mp_in[ce] = cm;
end else begin
s_en_out[ce] = cen; s_mp_out[ce] = cm;
end
end
if (hw && (he < N_NUM) && (he != 4'd0)) begin
// The design counts a SPLIT HALT only when a halt is being SET,
// not when it is cleared. Omitting `hs` here made the shadow count
// clears as well -- 3202 errors, none of them the design.
if (hs) begin
if (hi && !s_hl_out[he]) begin x_split = x_split + 1; g_split = g_split + 1; end
if (!hi && !s_hl_in[he]) begin x_split = x_split + 1; g_split = g_split + 1; end
end
if (hi) s_hl_in[he] = hs;
else s_hl_out[he] = hs;
end
@(posedge clk);
#1;
steps = steps + 1;
cfg_wr = 1'b0; halt_wr = 1'b0; tok_valid = 1'b0; xact_ack = 1'b0;
ck(n_lookup === x_look, "lookup count disagrees");
ck(n_unknown_ep === x_unk, "unknown-endpoint count disagrees");
ck(n_halted_hit === x_halt, "halted-hit count disagrees");
ck(n_toggle_flip === x_flip, "toggle-flip count disagrees");
ck(n_split_halt === x_split, "split-halt count disagrees");
audit_all;
end
endtask
task reset_dut;
integer a;
begin
rst_n = 1'b0;
cfg_wr = 0; halt_wr = 0; tok_valid = 0; xact_ack = 0;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
for (a = 0; a < N_NUM; a = a + 1) begin
s_en_in[a] = (a == 0); s_en_out[a] = (a == 0);
s_hl_in[a] = 1'b0; s_hl_out[a] = 1'b0;
s_mp_in[a] = (a == 0) ? 11'd64 : 11'd0;
s_mp_out[a] = (a == 0) ? 11'd64 : 11'd0;
s_tg_in[a] = 1'b0; s_tg_out[a] = 1'b0;
end
x_look = 0; x_unk = 0; x_halt = 0; x_flip = 0; x_split = 0;
@(posedge clk); #1;
end
endtask
integer ei, di, en, hl, tg, k, a2;
initial begin
for (ri = 0; ri < 64; ri = ri + 1) reach[ri] = 1'b0;
seed = 32'd27004;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- every (number, direction) in
// every (enabled, halted, toggle) state. 4 x 2 x 2 x 2 x 2 = 64.
// =============================================================
for (ei = 0; ei < N_NUM; ei = ei + 1)
for (di = 0; di < 2; di = di + 1)
for (en = 0; en < 2; en = en + 1)
for (hl = 0; hl < 2; hl = hl + 1)
for (tg = 0; tg < 2; tg = tg + 1) begin
reset_dut;
// configure this direction
op(1'b1, ei[3:0], di[0], en[0], 11'd512,
1'b0, 4'd0, 1'b0, 1'b0, 1'b0, 4'd0, 1'b0, 1'b0);
// halt it if wanted
if (hl) op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b1, ei[3:0], di[0], 1'b1, 1'b0, 4'd0, 1'b0, 1'b0);
// flip the toggle if wanted
if (tg) op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b0, 4'd0, 1'b0, 1'b0, 1'b1, ei[3:0], di[0], 1'b1);
// and look it up
op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b0, 4'd0, 1'b0, 1'b0, 1'b1, ei[3:0], di[0], 1'b0);
ri = (ei << 4) | (di << 3) | (en << 2) | (hl << 1) | tg;
reach[ri] = 1'b1;
end
// =============================================================
// PHASE 2 (DIRECTED, EXHAUSTIVE) -- THE CHAPTER'S POINT.
//
// For every endpoint number except 0, enable BOTH directions, then
// halt exactly ONE of them, and prove the other is untouched --
// enable, halt, max packet size and toggle all independently.
//
// Both orders, both directions: 3 numbers x 2 directions x 2 orders.
// =============================================================
for (ei = 1; ei < N_NUM; ei = ei + 1)
for (di = 0; di < 2; di = di + 1)
for (k = 0; k < 2; k = k + 1) begin
reset_dut;
// both directions exist, with DIFFERENT packet sizes so a shared
// field cannot pass by accident
op(1'b1, ei[3:0], 1'b1, 1'b1, 11'd64,
1'b0, 4'd0, 1'b0, 1'b0, 1'b0, 4'd0, 1'b0, 1'b0);
op(1'b1, ei[3:0], 1'b0, 1'b1, 11'd512,
1'b0, 4'd0, 1'b0, 1'b0, 1'b0, 4'd0, 1'b0, 1'b0);
if (k == 0) begin
// advance one toggle, then halt one direction
op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b0, 4'd0, 1'b0, 1'b0, 1'b1, ei[3:0], di[0], 1'b1);
op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b1, ei[3:0], di[0], 1'b1, 1'b0, 4'd0, 1'b0, 1'b0);
end else begin
// halt one direction, then try to use the OTHER
op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b1, ei[3:0], di[0], 1'b1, 1'b0, 4'd0, 1'b0, 1'b0);
op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b0, 4'd0, 1'b0, 1'b0, 1'b1, ei[3:0], ~di[0], 1'b1);
end
// the untouched direction must still work
peek(ei[3:0], ~di[0]);
ck(ep_valid === 1'b1, "the other direction stopped existing");
ck(ep_halted === 1'b0, "halting one direction halted the other");
tok_valid = 1'b0;
// and clearing the halt leaves the other alone too
op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b1, ei[3:0], di[0], 1'b0, 1'b0, 4'd0, 1'b0, 1'b0);
end
// =============================================================
// PHASE 3 (DIRECTED) -- endpoint 0 is the exception.
//
// It is bidirectional, it exists from reset, and it is not
// haltable: clearing a halt is itself a control transfer, so a
// device that could halt EP0 would be unrecoverable.
// =============================================================
reset_dut;
for (di = 0; di < 2; di = di + 1) begin
// it exists in both directions before anything configures it
peek(4'd0, di[0]);
ck(ep_valid === 1'b1, "EP0 did not exist at reset");
ck(ep_maxp === 11'd64, "EP0 had no default packet size");
tok_valid = 1'b0;
// and a halt attempt does nothing
op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b1, 4'd0, di[0], 1'b1, 1'b0, 4'd0, 1'b0, 1'b0);
peek(4'd0, di[0]);
ck(ep_halted === 1'b0, "EP0 was halted, which is unrecoverable");
tok_valid = 1'b0;
end
// configuring EP0 configures both halves
op(1'b1, 4'd0, 1'b1, 1'b1, 11'd8,
1'b0, 4'd0, 1'b0, 1'b0, 1'b0, 4'd0, 1'b0, 1'b0);
for (di = 0; di < 2; di = di + 1) begin
peek(4'd0, di[0]);
ck(ep_maxp === 11'd8, "configuring EP0 did not apply to both directions");
tok_valid = 1'b0;
end
// =============================================================
// PHASE 4 (DIRECTED) -- an endpoint number that does not exist.
// =============================================================
reset_dut;
for (k = N_NUM; k < 16; k = k + 1)
for (di = 0; di < 2; di = di + 1) begin
peek(k[3:0], di[0]);
ck(ep_valid === 1'b0, "an out-of-range endpoint reported valid");
ck(ep_maxp === 11'd0, "an out-of-range endpoint reported a packet size");
tok_valid = 1'b0;
@(posedge clk); #1;
end
// =============================================================
// PHASE 5 (RANDOM) -- a driver configuring and halting freely.
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut;
for (k = 0; k < 8000; k = k + 1) begin
op((urand(0) % 4) == 0, urand(0) % 16, (urand(0) % 2) == 0,
(urand(0) % 4) != 0, (urand(0) % 2) ? 11'd64 : 11'd512,
(urand(0) % 5) == 0, urand(0) % 16, (urand(0) % 2) == 0,
(urand(0) % 3) != 0,
(urand(0) % 2) == 0, urand(0) % 16, (urand(0) % 2) == 0,
(urand(0) % 3) != 0);
if ((k % 128) == 127) reset_dut;
end
`endif
n_reach = 0;
for (ri = 0; ri < 64; ri = ri + 1) if (reach[ri]) n_reach = n_reach + 1;
$display("steps=%0d checks=%0d reach=%0d/64 errors=%0d",
steps, checks, n_reach, errors);
$display("[ep] lookups=%0d unknown=%0d halted_hits=%0d toggles=%0d split_halts=%0d",
g_look, g_unk, g_halt, g_flip, g_split);
$display("[the whole point] cross-direction leaks = %0d", n_cross);
if (n_reach != 64) begin
$display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
end
if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
else $display("FAIL: %0d errors in %0d checks", errors, checks);
$finish;
end
endmoduleSystemVerilog testbench
// =====================================================================
// Testbench for usb_ep_table.
//
// The headline property is a NEGATIVE one about independence: touching
// one direction of an endpoint number must never move the other. So the
// shadow keeps the two directions in completely separate arrays and, after
// every single operation, compares BOTH directions of EVERY number --
// not only the one that was targeted.
//
// Checking only the targeted direction is how a cross-direction leak
// survives a testbench: the write landed correctly, and it also landed
// somewhere nobody looked.
// =====================================================================
`timescale 1ns/1ps
module tb_ep_sv;
localparam N_NUM = 4;
logic clk = 1'b0, rst_n = 1'b0;
logic tok_valid = 1'b0;
logic [3:0] tok_ep = 4'd0;
logic tok_in = 1'b0;
logic cfg_wr = 1'b0;
logic [3:0] cfg_ep = 4'd0;
logic cfg_in = 1'b0;
logic cfg_enable = 1'b0;
logic [10:0] cfg_maxp = 11'd0;
logic halt_wr = 1'b0;
logic [3:0] halt_ep = 4'd0;
logic halt_in = 1'b0;
logic halt_set = 1'b0;
logic xact_ack = 1'b0;
logic ep_valid, ep_halted, ep_toggle;
logic [10:0] ep_maxp;
logic [31:0] n_lookup, n_unknown_ep, n_halted_hit, n_toggle_flip, n_split_halt;
usb_ep_table #(.N_NUM(N_NUM)) dut (
.clk(clk), .rst_n(rst_n),
.tok_valid(tok_valid), .tok_ep(tok_ep), .tok_in(tok_in),
.cfg_wr(cfg_wr), .cfg_ep(cfg_ep), .cfg_in(cfg_in),
.cfg_enable(cfg_enable), .cfg_maxp(cfg_maxp),
.halt_wr(halt_wr), .halt_ep(halt_ep), .halt_in(halt_in),
.halt_set(halt_set),
.xact_ack(xact_ack),
.ep_valid(ep_valid), .ep_halted(ep_halted),
.ep_maxp(ep_maxp), .ep_toggle(ep_toggle),
.n_lookup(n_lookup), .n_unknown_ep(n_unknown_ep),
.n_halted_hit(n_halted_hit), .n_toggle_flip(n_toggle_flip),
.n_split_halt(n_split_halt)
);
always #5 clk = ~clk;
integer errors = 0, checks = 0, steps = 0;
integer seed;
// ---- $random is SIGNED: mask the sign bit before any modulo ----
function automatic logic [31:0] urand(bit dummy);
return $random(seed) & 32'h3FFF_FFFF;
endfunction
// ---- the shadow: two directions, two sets of arrays, never merged ----
logic s_en_in [0:N_NUM-1];
logic s_en_out [0:N_NUM-1];
logic s_hl_in [0:N_NUM-1];
logic s_hl_out [0:N_NUM-1];
logic [10:0] s_mp_in [0:N_NUM-1];
logic [10:0] s_mp_out [0:N_NUM-1];
logic s_tg_in [0:N_NUM-1];
logic s_tg_out [0:N_NUM-1];
logic [31:0] x_look, x_unk, x_halt, x_flip, x_split;
// Run-wide totals. The DUT's counters are cleared by every reset, so a
// summary printed from them describes the last window rather than the run.
integer g_look = 0, g_unk = 0, g_halt = 0, g_flip = 0, g_split = 0;
// ---- the headline counter ----
//
// Every cycle in which any direction of any number disagreed with the
// shadow. A cross-direction leak shows up here and nowhere else, because
// the direction that was written is always correct.
integer n_cross = 0;
// ---- exhaustive reach over (number, dir, enabled, halted, toggle) ----
logic reach [0:63];
integer ri, n_reach;
task ck(input logic cond, input logic [255:0] what);
begin
checks = checks + 1;
if (!cond) begin
errors = errors + 1;
if (errors <= 20)
$display(" ERROR @%0t step=%0d: %0s", $time, steps, what);
end
end
endtask
// ---------------------------------------------------------------
// Compare EVERY direction of EVERY number against the shadow.
//
// Called after every operation. This is the expensive way to do it
// and it is the only way that can see a leak.
// ---------------------------------------------------------------
task audit_all;
integer a;
begin
for (a = 0; a < N_NUM; a = a + 1) begin
// drive a lookup at (a, IN) and check it, then (a, OUT)
tok_valid = 1'b1; tok_ep = a[3:0]; tok_in = 1'b1;
#1;
if (ep_valid !== s_en_in[a]) n_cross = n_cross + 1;
if (ep_halted !== s_hl_in[a]) n_cross = n_cross + 1;
if (ep_toggle !== s_tg_in[a]) n_cross = n_cross + 1;
ck(ep_valid === s_en_in[a], "IN enable disagrees");
ck(ep_halted === s_hl_in[a], "IN halt disagrees");
ck(ep_toggle === s_tg_in[a], "IN toggle disagrees");
ck(ep_maxp === (s_en_in[a] ? s_mp_in[a] : 11'd0),
"IN maxp disagrees");
tok_in = 1'b0;
#1;
if (ep_valid !== s_en_out[a]) n_cross = n_cross + 1;
if (ep_halted !== s_hl_out[a]) n_cross = n_cross + 1;
if (ep_toggle !== s_tg_out[a]) n_cross = n_cross + 1;
ck(ep_valid === s_en_out[a], "OUT enable disagrees");
ck(ep_halted === s_hl_out[a], "OUT halt disagrees");
ck(ep_toggle === s_tg_out[a], "OUT toggle disagrees");
ck(ep_maxp === (s_en_out[a] ? s_mp_out[a] : 11'd0),
"OUT maxp disagrees");
end
tok_valid = 1'b0;
ck(n_cross == 0, "a direction changed that nothing touched");
end
endtask
// ---------------------------------------------------------------
// A combinational lookup with NO clocked side effect.
//
// Positioned at a negedge deliberately. Driving tok_valid and then
// waiting #1 lands exactly on the next posedge once the preceding audit
// has consumed its 8 ns -- the DUT then counts a lookup the shadow knows
// nothing about. Two errors, entirely a testbench timing accident.
// ---------------------------------------------------------------
task peek(input logic [3:0] e, input logic d);
begin
@(negedge clk);
tok_valid = 1'b1; tok_ep = e; tok_in = d;
#1;
end
endtask
// ---------------------------------------------------------------
// One clocked operation, then a full audit.
// ---------------------------------------------------------------
task op(input logic cw, input logic [3:0] ce, input logic ci, input logic cen,
input logic [10:0] cm,
input logic hw, input logic [3:0] he, input logic hi, input logic hs,
input logic tv, input logic [3:0] te, input logic ti, input logic xa);
begin
cfg_wr = cw; cfg_ep = ce; cfg_in = ci;
cfg_enable = cen; cfg_maxp = cm;
halt_wr = hw; halt_ep = he; halt_in = hi; halt_set = hs;
tok_valid = tv; tok_ep = te; tok_in = ti; xact_ack = xa;
// ---- advance the shadow ----
//
// The LOOKUP is evaluated first, because the design's lookup is a
// combinational read of REGISTERED table entries -- it sees the
// values as they stood before this edge. A shadow that applies the
// configuration write first sees the new values and disagrees on
// every cycle that configures and looks up at once.
if (tv && (te < N_NUM)) begin
x_look = x_look + 1; g_look = g_look + 1;
if (ti) begin
if (!s_en_in[te]) begin x_unk = x_unk + 1; g_unk = g_unk + 1; end
if (s_hl_in[te]) begin x_halt = x_halt + 1; g_halt = g_halt + 1; end
if (s_en_in[te] && !s_hl_in[te] && xa) begin
s_tg_in[te] = ~s_tg_in[te];
x_flip = x_flip + 1; g_flip = g_flip + 1;
end
end else begin
if (!s_en_out[te]) begin x_unk = x_unk + 1; g_unk = g_unk + 1; end
if (s_hl_out[te]) begin x_halt = x_halt + 1; g_halt = g_halt + 1; end
if (s_en_out[te] && !s_hl_out[te] && xa) begin
s_tg_out[te] = ~s_tg_out[te];
x_flip = x_flip + 1; g_flip = g_flip + 1;
end
end
end else if (tv) begin
x_look = x_look + 1; g_look = g_look + 1;
x_unk = x_unk + 1; g_unk = g_unk + 1;
end
if (cw && (ce < N_NUM)) begin
if (ce == 4'd0) begin
// EP0 is bidirectional: one conversation, both halves
s_en_in[ce] = cen; s_en_out[ce] = cen;
s_mp_in[ce] = cm; s_mp_out[ce] = cm;
end else if (ci) begin
s_en_in[ce] = cen; s_mp_in[ce] = cm;
end else begin
s_en_out[ce] = cen; s_mp_out[ce] = cm;
end
end
if (hw && (he < N_NUM) && (he != 4'd0)) begin
// The design counts a SPLIT HALT only when a halt is being SET,
// not when it is cleared. Omitting `hs` here made the shadow count
// clears as well -- 3202 errors, none of them the design.
if (hs) begin
if (hi && !s_hl_out[he]) begin x_split = x_split + 1; g_split = g_split + 1; end
if (!hi && !s_hl_in[he]) begin x_split = x_split + 1; g_split = g_split + 1; end
end
if (hi) s_hl_in[he] = hs;
else s_hl_out[he] = hs;
end
@(posedge clk);
#1;
steps = steps + 1;
cfg_wr = 1'b0; halt_wr = 1'b0; tok_valid = 1'b0; xact_ack = 1'b0;
ck(n_lookup === x_look, "lookup count disagrees");
ck(n_unknown_ep === x_unk, "unknown-endpoint count disagrees");
ck(n_halted_hit === x_halt, "halted-hit count disagrees");
ck(n_toggle_flip === x_flip, "toggle-flip count disagrees");
ck(n_split_halt === x_split, "split-halt count disagrees");
audit_all;
end
endtask
task reset_dut;
integer a;
begin
rst_n = 1'b0;
cfg_wr = 0; halt_wr = 0; tok_valid = 0; xact_ack = 0;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
for (a = 0; a < N_NUM; a = a + 1) begin
s_en_in[a] = (a == 0); s_en_out[a] = (a == 0);
s_hl_in[a] = 1'b0; s_hl_out[a] = 1'b0;
s_mp_in[a] = (a == 0) ? 11'd64 : 11'd0;
s_mp_out[a] = (a == 0) ? 11'd64 : 11'd0;
s_tg_in[a] = 1'b0; s_tg_out[a] = 1'b0;
end
x_look = 0; x_unk = 0; x_halt = 0; x_flip = 0; x_split = 0;
@(posedge clk); #1;
end
endtask
integer ei, di, en, hl, tg, k, a2;
initial begin
for (ri = 0; ri < 64; ri = ri + 1) reach[ri] = 1'b0;
seed = 32'd27004;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- every (number, direction) in
// every (enabled, halted, toggle) state. 4 x 2 x 2 x 2 x 2 = 64.
// =============================================================
for (ei = 0; ei < N_NUM; ei = ei + 1)
for (di = 0; di < 2; di = di + 1)
for (en = 0; en < 2; en = en + 1)
for (hl = 0; hl < 2; hl = hl + 1)
for (tg = 0; tg < 2; tg = tg + 1) begin
reset_dut;
// configure this direction
op(1'b1, ei[3:0], di[0], en[0], 11'd512,
1'b0, 4'd0, 1'b0, 1'b0, 1'b0, 4'd0, 1'b0, 1'b0);
// halt it if wanted
if (hl) op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b1, ei[3:0], di[0], 1'b1, 1'b0, 4'd0, 1'b0, 1'b0);
// flip the toggle if wanted
if (tg) op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b0, 4'd0, 1'b0, 1'b0, 1'b1, ei[3:0], di[0], 1'b1);
// and look it up
op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b0, 4'd0, 1'b0, 1'b0, 1'b1, ei[3:0], di[0], 1'b0);
ri = (ei << 4) | (di << 3) | (en << 2) | (hl << 1) | tg;
reach[ri] = 1'b1;
end
// =============================================================
// PHASE 2 (DIRECTED, EXHAUSTIVE) -- THE CHAPTER'S POINT.
//
// For every endpoint number except 0, enable BOTH directions, then
// halt exactly ONE of them, and prove the other is untouched --
// enable, halt, max packet size and toggle all independently.
//
// Both orders, both directions: 3 numbers x 2 directions x 2 orders.
// =============================================================
for (ei = 1; ei < N_NUM; ei = ei + 1)
for (di = 0; di < 2; di = di + 1)
for (k = 0; k < 2; k = k + 1) begin
reset_dut;
// both directions exist, with DIFFERENT packet sizes so a shared
// field cannot pass by accident
op(1'b1, ei[3:0], 1'b1, 1'b1, 11'd64,
1'b0, 4'd0, 1'b0, 1'b0, 1'b0, 4'd0, 1'b0, 1'b0);
op(1'b1, ei[3:0], 1'b0, 1'b1, 11'd512,
1'b0, 4'd0, 1'b0, 1'b0, 1'b0, 4'd0, 1'b0, 1'b0);
if (k == 0) begin
// advance one toggle, then halt one direction
op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b0, 4'd0, 1'b0, 1'b0, 1'b1, ei[3:0], di[0], 1'b1);
op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b1, ei[3:0], di[0], 1'b1, 1'b0, 4'd0, 1'b0, 1'b0);
end else begin
// halt one direction, then try to use the OTHER
op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b1, ei[3:0], di[0], 1'b1, 1'b0, 4'd0, 1'b0, 1'b0);
op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b0, 4'd0, 1'b0, 1'b0, 1'b1, ei[3:0], ~di[0], 1'b1);
end
// the untouched direction must still work
peek(ei[3:0], ~di[0]);
ck(ep_valid === 1'b1, "the other direction stopped existing");
ck(ep_halted === 1'b0, "halting one direction halted the other");
tok_valid = 1'b0;
// and clearing the halt leaves the other alone too
op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b1, ei[3:0], di[0], 1'b0, 1'b0, 4'd0, 1'b0, 1'b0);
end
// =============================================================
// PHASE 3 (DIRECTED) -- endpoint 0 is the exception.
//
// It is bidirectional, it exists from reset, and it is not
// haltable: clearing a halt is itself a control transfer, so a
// device that could halt EP0 would be unrecoverable.
// =============================================================
reset_dut;
for (di = 0; di < 2; di = di + 1) begin
// it exists in both directions before anything configures it
peek(4'd0, di[0]);
ck(ep_valid === 1'b1, "EP0 did not exist at reset");
ck(ep_maxp === 11'd64, "EP0 had no default packet size");
tok_valid = 1'b0;
// and a halt attempt does nothing
op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
1'b1, 4'd0, di[0], 1'b1, 1'b0, 4'd0, 1'b0, 1'b0);
peek(4'd0, di[0]);
ck(ep_halted === 1'b0, "EP0 was halted, which is unrecoverable");
tok_valid = 1'b0;
end
// configuring EP0 configures both halves
op(1'b1, 4'd0, 1'b1, 1'b1, 11'd8,
1'b0, 4'd0, 1'b0, 1'b0, 1'b0, 4'd0, 1'b0, 1'b0);
for (di = 0; di < 2; di = di + 1) begin
peek(4'd0, di[0]);
ck(ep_maxp === 11'd8, "configuring EP0 did not apply to both directions");
tok_valid = 1'b0;
end
// =============================================================
// PHASE 4 (DIRECTED) -- an endpoint number that does not exist.
// =============================================================
reset_dut;
for (k = N_NUM; k < 16; k = k + 1)
for (di = 0; di < 2; di = di + 1) begin
peek(k[3:0], di[0]);
ck(ep_valid === 1'b0, "an out-of-range endpoint reported valid");
ck(ep_maxp === 11'd0, "an out-of-range endpoint reported a packet size");
tok_valid = 1'b0;
@(posedge clk); #1;
end
// =============================================================
// PHASE 5 (RANDOM) -- a driver configuring and halting freely.
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut;
for (k = 0; k < 8000; k = k + 1) begin
op((urand(0) % 4) == 0, urand(0) % 16, (urand(0) % 2) == 0,
(urand(0) % 4) != 0, (urand(0) % 2) ? 11'd64 : 11'd512,
(urand(0) % 5) == 0, urand(0) % 16, (urand(0) % 2) == 0,
(urand(0) % 3) != 0,
(urand(0) % 2) == 0, urand(0) % 16, (urand(0) % 2) == 0,
(urand(0) % 3) != 0);
if ((k % 128) == 127) reset_dut;
end
`endif
n_reach = 0;
for (ri = 0; ri < 64; ri = ri + 1) if (reach[ri]) n_reach = n_reach + 1;
$display("steps=%0d checks=%0d reach=%0d/64 errors=%0d",
steps, checks, n_reach, errors);
$display("[ep] lookups=%0d unknown=%0d halted_hits=%0d toggles=%0d split_halts=%0d",
g_look, g_unk, g_halt, g_flip, g_split);
$display("[the whole point] cross-direction leaks = %0d", n_cross);
if (n_reach != 64) begin
$display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
end
if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
else $display("FAIL: %0d errors in %0d checks", errors, checks);
$finish;
end
endmoduleVHDL-2008 testbench
-- =====================================================================
-- Testbench for usb_ep_table (VHDL-2008).
--
-- The headline property is negative and about independence: touching one
-- direction of an endpoint number must never move the other. So the
-- shadow keeps the two directions in separate arrays and, after every
-- operation, compares BOTH directions of EVERY number -- not only the one
-- that was targeted. Checking only the targeted direction is how a leak
-- survives a testbench: the write landed correctly, and it also landed
-- somewhere nobody looked.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.ep_pkg.all;
entity tb_ep_vhdl is
generic (DIRECTED_ONLY : boolean := false);
end entity;
architecture sim of tb_ep_vhdl is
constant N_NUM : natural := 4;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal tok_valid : std_logic := '0';
signal tok_ep : std_logic_vector(3 downto 0) := (others => '0');
signal tok_in : std_logic := '0';
signal cfg_wr : std_logic := '0';
signal cfg_ep : std_logic_vector(3 downto 0) := (others => '0');
signal cfg_in : std_logic := '0';
signal cfg_enable : std_logic := '0';
signal cfg_maxp : std_logic_vector(10 downto 0) := (others => '0');
signal halt_wr : std_logic := '0';
signal halt_ep : std_logic_vector(3 downto 0) := (others => '0');
signal halt_in : std_logic := '0';
signal halt_set : std_logic := '0';
signal xact_ack : std_logic := '0';
signal ep_valid, ep_halted, ep_toggle : std_logic;
signal ep_maxp : std_logic_vector(10 downto 0);
signal n_lookup, n_unknown_ep, n_halted_hit, n_toggle_flip, n_split_halt
: std_logic_vector(31 downto 0);
signal done : boolean := false;
begin
dut : entity work.usb_ep_table
generic map (N_NUM => N_NUM)
port map (
clk => clk, rst_n => rst_n,
tok_valid => tok_valid, tok_ep => tok_ep, tok_in => tok_in,
cfg_wr => cfg_wr, cfg_ep => cfg_ep, cfg_in => cfg_in,
cfg_enable => cfg_enable, cfg_maxp => cfg_maxp,
halt_wr => halt_wr, halt_ep => halt_ep, halt_in => halt_in,
halt_set => halt_set,
xact_ack => xact_ack,
ep_valid => ep_valid, ep_halted => ep_halted,
ep_maxp => ep_maxp, ep_toggle => ep_toggle,
n_lookup => n_lookup, n_unknown_ep => n_unknown_ep,
n_halted_hit => n_halted_hit, n_toggle_flip => n_toggle_flip,
n_split_halt => n_split_halt);
clk <= not clk after 5 ns when not done else '0';
stim : process
variable errors : natural := 0;
variable checks : natural := 0;
variable steps : natural := 0;
variable n_cross : natural := 0;
type sl_arr is array (0 to N_NUM-1) of std_logic;
type mp_arr is array (0 to N_NUM-1) of unsigned(10 downto 0);
variable s_en_in, s_en_out, s_hl_in, s_hl_out, s_tg_in, s_tg_out : sl_arr;
variable s_mp_in, s_mp_out : mp_arr;
variable x_look, x_unk, x_halt, x_flip, x_split : natural := 0;
-- Run-wide totals: the DUT's counters are cleared by every reset.
variable g_look, g_unk, g_halt, g_flip, g_split : natural := 0;
variable reach : std_logic_vector(0 to 63) := (others => '0');
variable n_reach : natural := 0;
variable rnd : unsigned(31 downto 0) := x"0003B7D1";
variable ln : line;
procedure ck(cond : boolean; what : string) is
begin
checks := checks + 1;
if not cond then
errors := errors + 1;
if errors <= 20 then
write(ln, string'(" ERROR step=") & integer'image(steps)
& string'(": ") & what);
writeline(output, ln);
end if;
end if;
end procedure;
-- boolean to std_logic. VHDL has no implicit conversion, and a
-- conditional expression in argument position is VHDL-2019, not 2008.
function sl_of(b : boolean) return std_logic is
begin
if b then return '1'; else return '0'; end if;
end function;
impure function nxt return natural is
begin
rnd := rnd xor (rnd sll 13);
rnd := rnd xor (rnd srl 17);
rnd := rnd xor (rnd sll 5);
return to_integer(rnd(14 downto 0));
end function;
-- Compare EVERY direction of EVERY number. The expensive way, and the
-- only way that can see a leak.
procedure audit_all is
begin
for a in 0 to N_NUM-1 loop
tok_valid <= '1';
tok_ep <= std_logic_vector(to_unsigned(a, 4));
tok_in <= '1';
wait for 1 ns;
if ep_valid /= s_en_in(a) then n_cross := n_cross + 1; end if;
if ep_halted /= s_hl_in(a) then n_cross := n_cross + 1; end if;
if ep_toggle /= s_tg_in(a) then n_cross := n_cross + 1; end if;
ck(ep_valid = s_en_in(a), "IN enable disagrees");
ck(ep_halted = s_hl_in(a), "IN halt disagrees");
ck(ep_toggle = s_tg_in(a), "IN toggle disagrees");
if s_en_in(a) = '1' then
ck(ep_maxp = std_logic_vector(s_mp_in(a)), "IN maxp disagrees");
else
ck(ep_maxp = std_logic_vector'("00000000000"), "IN maxp disagrees");
end if;
tok_in <= '0';
wait for 1 ns;
if ep_valid /= s_en_out(a) then n_cross := n_cross + 1; end if;
if ep_halted /= s_hl_out(a) then n_cross := n_cross + 1; end if;
if ep_toggle /= s_tg_out(a) then n_cross := n_cross + 1; end if;
ck(ep_valid = s_en_out(a), "OUT enable disagrees");
ck(ep_halted = s_hl_out(a), "OUT halt disagrees");
ck(ep_toggle = s_tg_out(a), "OUT toggle disagrees");
if s_en_out(a) = '1' then
ck(ep_maxp = std_logic_vector(s_mp_out(a)), "OUT maxp disagrees");
else
ck(ep_maxp = std_logic_vector'("00000000000"), "OUT maxp disagrees");
end if;
end loop;
tok_valid <= '0';
ck(n_cross = 0, "a direction changed that nothing touched");
end procedure;
procedure op(cw : std_logic; ce : std_logic_vector(3 downto 0);
ci, cen : std_logic; cm : std_logic_vector(10 downto 0);
hw : std_logic; he : std_logic_vector(3 downto 0);
hi, hs : std_logic;
tv : std_logic; te : std_logic_vector(3 downto 0);
ti, xa : std_logic) is
variable cn, hn, tn : natural;
begin
cfg_wr <= cw; cfg_ep <= ce; cfg_in <= ci;
cfg_enable <= cen; cfg_maxp <= cm;
halt_wr <= hw; halt_ep <= he; halt_in <= hi; halt_set <= hs;
tok_valid <= tv; tok_ep <= te; tok_in <= ti; xact_ack <= xa;
cn := to_integer(unsigned(ce));
hn := to_integer(unsigned(he));
tn := to_integer(unsigned(te));
-- ---- advance the shadow ----
--
-- The LOOKUP is evaluated first, because the design's lookup is a
-- combinational read of REGISTERED table entries: it sees the values
-- as they stood before this edge. A shadow that applies the
-- configuration write first disagrees on every cycle that configures
-- and looks up at once.
if tv = '1' and tn < N_NUM then
x_look := x_look + 1; g_look := g_look + 1;
if ti = '1' then
if s_en_in(tn) = '0' then x_unk := x_unk + 1; g_unk := g_unk + 1; end if;
if s_hl_in(tn) = '1' then x_halt := x_halt + 1; g_halt := g_halt + 1; end if;
if s_en_in(tn) = '1' and s_hl_in(tn) = '0' and xa = '1' then
s_tg_in(tn) := not s_tg_in(tn);
x_flip := x_flip + 1; g_flip := g_flip + 1;
end if;
else
if s_en_out(tn) = '0' then x_unk := x_unk + 1; g_unk := g_unk + 1; end if;
if s_hl_out(tn) = '1' then x_halt := x_halt + 1; g_halt := g_halt + 1; end if;
if s_en_out(tn) = '1' and s_hl_out(tn) = '0' and xa = '1' then
s_tg_out(tn) := not s_tg_out(tn);
x_flip := x_flip + 1; g_flip := g_flip + 1;
end if;
end if;
elsif tv = '1' then
x_look := x_look + 1; g_look := g_look + 1;
x_unk := x_unk + 1; g_unk := g_unk + 1;
end if;
if cw = '1' and cn < N_NUM then
if cn = 0 then
s_en_in(cn) := cen; s_en_out(cn) := cen;
s_mp_in(cn) := unsigned(cm); s_mp_out(cn) := unsigned(cm);
elsif ci = '1' then
s_en_in(cn) := cen; s_mp_in(cn) := unsigned(cm);
else
s_en_out(cn) := cen; s_mp_out(cn) := unsigned(cm);
end if;
end if;
if hw = '1' and hn < N_NUM and hn /= 0 then
-- A split halt is counted only when a halt is being SET.
if hs = '1' then
if hi = '1' and s_hl_out(hn) = '0' then
x_split := x_split + 1; g_split := g_split + 1;
end if;
if hi = '0' and s_hl_in(hn) = '0' then
x_split := x_split + 1; g_split := g_split + 1;
end if;
end if;
if hi = '1' then s_hl_in(hn) := hs;
else s_hl_out(hn) := hs;
end if;
end if;
wait until rising_edge(clk);
wait for 1 ns;
steps := steps + 1;
cfg_wr <= '0'; halt_wr <= '0'; tok_valid <= '0'; xact_ack <= '0';
ck(to_integer(unsigned(n_lookup)) = x_look, "lookup count disagrees");
ck(to_integer(unsigned(n_unknown_ep)) = x_unk, "unknown-endpoint count disagrees");
ck(to_integer(unsigned(n_halted_hit)) = x_halt, "halted-hit count disagrees");
ck(to_integer(unsigned(n_toggle_flip)) = x_flip, "toggle-flip count disagrees");
ck(to_integer(unsigned(n_split_halt)) = x_split, "split-halt count disagrees");
audit_all;
end procedure;
-- A combinational lookup with no clocked side effect, positioned at a
-- negedge deliberately: driving tok_valid then waiting 1 ns lands on the
-- next rising edge once the preceding audit has consumed its 8 ns, and
-- the DUT then counts a lookup the shadow knows nothing about.
procedure peek(e : std_logic_vector(3 downto 0); d : std_logic) is
begin
wait until falling_edge(clk);
tok_valid <= '1'; tok_ep <= e; tok_in <= d;
wait for 1 ns;
end procedure;
procedure reset_dut is
begin
rst_n <= '0';
cfg_wr <= '0'; halt_wr <= '0'; tok_valid <= '0'; xact_ack <= '0';
wait until rising_edge(clk);
wait until rising_edge(clk);
rst_n <= '1';
for a in 0 to N_NUM-1 loop
if a = 0 then
s_en_in(a) := '1'; s_en_out(a) := '1';
s_mp_in(a) := to_unsigned(64, 11); s_mp_out(a) := to_unsigned(64, 11);
else
s_en_in(a) := '0'; s_en_out(a) := '0';
s_mp_in(a) := (others => '0'); s_mp_out(a) := (others => '0');
end if;
s_hl_in(a) := '0'; s_hl_out(a) := '0';
s_tg_in(a) := '0'; s_tg_out(a) := '0';
end loop;
x_look := 0; x_unk := 0; x_halt := 0; x_flip := 0; x_split := 0;
wait until rising_edge(clk);
wait for 1 ns;
end procedure;
constant Z4 : std_logic_vector(3 downto 0) := "0000";
constant Z11 : std_logic_vector(10 downto 0) := (others => '0');
variable ri : natural;
variable ev, dv2 : std_logic_vector(3 downto 0);
variable db : std_logic;
variable m11 : std_logic_vector(10 downto 0);
begin
-- PHASE 1 (DIRECTED, EXHAUSTIVE) -- 4 numbers x 2 dirs x enabled x halted x toggle
for ei in 0 to N_NUM-1 loop
for di in 0 to 1 loop
for en in 0 to 1 loop
for hl in 0 to 1 loop
for tg in 0 to 1 loop
reset_dut;
ev := std_logic_vector(to_unsigned(ei, 4));
if di = 1 then db := '1'; else db := '0'; end if;
if en = 1 then
op('1', ev, db, '1', std_logic_vector(to_unsigned(512, 11)),
'0', Z4, '0', '0', '0', Z4, '0', '0');
else
op('1', ev, db, '0', std_logic_vector(to_unsigned(512, 11)),
'0', Z4, '0', '0', '0', Z4, '0', '0');
end if;
if hl = 1 then
op('0', Z4, '0', '0', Z11, '1', ev, db, '1', '0', Z4, '0', '0');
end if;
if tg = 1 then
op('0', Z4, '0', '0', Z11, '0', Z4, '0', '0', '1', ev, db, '1');
end if;
op('0', Z4, '0', '0', Z11, '0', Z4, '0', '0', '1', ev, db, '0');
ri := ei*16 + di*8 + en*4 + hl*2 + tg;
reach(ri) := '1';
end loop;
end loop;
end loop;
end loop;
end loop;
-- PHASE 2 (DIRECTED, EXHAUSTIVE) -- THE CHAPTER'S POINT.
-- Enable BOTH directions with DIFFERENT packet sizes, halt exactly one,
-- and prove the other is untouched in every field.
for ei in 1 to N_NUM-1 loop
for di in 0 to 1 loop
for k in 0 to 1 loop
reset_dut;
ev := std_logic_vector(to_unsigned(ei, 4));
if di = 1 then db := '1'; else db := '0'; end if;
op('1', ev, '1', '1', std_logic_vector(to_unsigned(64, 11)),
'0', Z4, '0', '0', '0', Z4, '0', '0');
op('1', ev, '0', '1', std_logic_vector(to_unsigned(512, 11)),
'0', Z4, '0', '0', '0', Z4, '0', '0');
if k = 0 then
op('0', Z4, '0', '0', Z11, '0', Z4, '0', '0', '1', ev, db, '1');
op('0', Z4, '0', '0', Z11, '1', ev, db, '1', '0', Z4, '0', '0');
else
op('0', Z4, '0', '0', Z11, '1', ev, db, '1', '0', Z4, '0', '0');
op('0', Z4, '0', '0', Z11, '0', Z4, '0', '0', '1', ev, not db, '1');
end if;
peek(ev, not db);
ck(ep_valid = '1', "the other direction stopped existing");
ck(ep_halted = '0', "halting one direction halted the other");
tok_valid <= '0';
op('0', Z4, '0', '0', Z11, '1', ev, db, '0', '0', Z4, '0', '0');
end loop;
end loop;
end loop;
-- PHASE 3 (DIRECTED) -- endpoint 0 is the exception
reset_dut;
for di in 0 to 1 loop
if di = 1 then db := '1'; else db := '0'; end if;
peek(Z4, db);
ck(ep_valid = '1', "EP0 did not exist at reset");
ck(ep_maxp = std_logic_vector(to_unsigned(64, 11)),
"EP0 had no default packet size");
tok_valid <= '0';
op('0', Z4, '0', '0', Z11, '1', Z4, db, '1', '0', Z4, '0', '0');
peek(Z4, db);
ck(ep_halted = '0', "EP0 was halted, which is unrecoverable");
tok_valid <= '0';
end loop;
op('1', Z4, '1', '1', std_logic_vector(to_unsigned(8, 11)),
'0', Z4, '0', '0', '0', Z4, '0', '0');
for di in 0 to 1 loop
if di = 1 then db := '1'; else db := '0'; end if;
peek(Z4, db);
ck(ep_maxp = std_logic_vector(to_unsigned(8, 11)),
"configuring EP0 did not apply to both directions");
tok_valid <= '0';
end loop;
-- PHASE 4 (DIRECTED) -- an endpoint number that does not exist
reset_dut;
for k in N_NUM to 15 loop
for di in 0 to 1 loop
if di = 1 then db := '1'; else db := '0'; end if;
peek(std_logic_vector(to_unsigned(k, 4)), db);
ck(ep_valid = '0', "an out-of-range endpoint reported valid");
ck(ep_maxp = std_logic_vector'("00000000000"),
"an out-of-range endpoint reported a packet size");
tok_valid <= '0';
wait until rising_edge(clk);
wait for 1 ns;
end loop;
end loop;
-- PHASE 5 (RANDOM)
if not DIRECTED_ONLY then
reset_dut;
for k in 0 to 7999 loop
if (nxt mod 2) = 1 then m11 := std_logic_vector(to_unsigned(64, 11));
else m11 := std_logic_vector(to_unsigned(512, 11));
end if;
op(sl_of(nxt mod 4 = 0), std_logic_vector(to_unsigned(nxt mod 16, 4)),
sl_of(nxt mod 2 = 0), sl_of(nxt mod 4 /= 0), m11,
sl_of(nxt mod 5 = 0), std_logic_vector(to_unsigned(nxt mod 16, 4)),
sl_of(nxt mod 2 = 0), sl_of(nxt mod 3 /= 0),
sl_of(nxt mod 2 = 0), std_logic_vector(to_unsigned(nxt mod 16, 4)),
sl_of(nxt mod 2 = 0), sl_of(nxt mod 3 /= 0));
if (k mod 128) = 127 then reset_dut; end if;
end loop;
end if;
n_reach := 0;
for i in 0 to 63 loop
if reach(i) = '1' then n_reach := n_reach + 1; end if;
end loop;
write(ln, string'("steps=") & integer'image(steps)
& string'(" checks=") & integer'image(checks)
& string'(" reach=") & integer'image(n_reach) & string'("/64")
& string'(" errors=") & integer'image(errors));
writeline(output, ln);
write(ln, string'("[ep] lookups=") & integer'image(g_look)
& string'(" unknown=") & integer'image(g_unk)
& string'(" halted_hits=") & integer'image(g_halt)
& string'(" toggles=") & integer'image(g_flip)
& string'(" split_halts=") & integer'image(g_split));
writeline(output, ln);
write(ln, string'("[the whole point] cross-direction leaks = ")
& integer'image(n_cross));
writeline(output, ln);
if n_reach /= 64 then
write(ln, string'("FAIL: exhaustive sweep incomplete"));
writeline(output, ln);
errors := errors + 1;
end if;
if errors = 0 then
write(ln, string'("PASS: 0 errors in ") & integer'image(checks)
& string'(" checks"));
else
write(ln, string'("FAIL: ") & integer'image(errors)
& string'(" errors in ") & integer'image(checks) & string'(" checks"));
end if;
writeline(output, ln);
done <= true;
wait;
end process;
end architecture;10. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| (number × direction × enabled × halted × toggle) reached | 64 / 64 | 64 / 64 | 64 / 64 |
| independence scenarios swept | 12 / 12 | 12 / 12 | 12 / 12 |
| out-of-range numbers swept | 24 / 24 | 24 / 24 | 24 / 24 |
| Steps | 8255 | 8255 | 8255 |
| Checks executed | 313770 | 313770 | 313770 |
| lookups | 4019 | 4019 | 4033 |
| lookups of an unconfigured endpoint | 3512 | 3512 | 3565 |
| lookups that hit a halted endpoint | 212 | 212 | 174 |
| toggle advances | 268 | 268 | 263 |
| halts applied to one direction only | 197 | 197 | 178 |
| cross-direction leaks | 0 | 0 | 0 |
| Result | PASS | PASS | PASS |
11. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| D5 | any in-range endpoint reports valid, configured or not | 77766 | 77766 | 83556 |
| D1 | a halt applies to BOTH directions of the number | 21360 | 21360 | 21025 |
| D3 | the two directions share one data toggle | 14473 | 14473 | 14568 |
| D4 | endpoint 0 becomes haltable | 12792 | 12792 | 13807 |
| D6 | a halted endpoint's toggle still advances | 10135 | 10135 | 9786 |
| D7 | a disabled endpoint still reports a packet size | 6063 | 6063 | 7541 |
| D2 | the packet size is read from the IN table whatever the direction | 5051 | 5051 | 5219 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages.
D1 is the mutation this chapter exists for, and it is worth noticing what it does not break. Every lookup still returns a defensible value. Every write lands where it was aimed. The device still enumerates, still transfers, still passes a functional test — and a driver that halts a failed read direction has silently killed its writes.
Directed against random
| # | V all | V directed | V random | VHDL all | VHDL directed | VHDL random |
|---|---|---|---|---|---|---|
| D1 | 21360 | 327 | 21033 | 21025 | 327 | 20698 |
| D2 | 5051 | 84 | 4967 | 5219 | 84 | 5135 |
| D3 | 14473 | 290 | 14183 | 14568 | 290 | 14278 |
| D4 | 12792 | 296 | 12496 | 13807 | 296 | 13511 |
| D5 | 77766 | 1861 | 75905 | 83556 | 1861 | 81695 |
| D6 | 10135 | 209 | 9926 | 9786 | 209 | 9577 |
| D7 | 6063 | 120 | 5943 | 7541 | 120 | 7421 |
Every directed column is identical across Verilog and VHDL, which localises the whole spread to the random half.
12. D4's Directed Columns Read 296 and 319, and That Was the Mutant Again
Every other directed column matched to the digit on the first run. D4's did not, and a 7% disagreement in a column that is supposed to be identical stimulus against identical logic is a much louder signal than a 30% disagreement in the random half.
The cause was structural rather than semantic. In the Verilog the split-halt coverage counter is its own if statement with its own endpoint-0 exclusion. In the VHDL it had been folded inside the halt-write block:
Verilog: VHDL (before):
if (halt && ep != 0) if halt and ep /= 0 then
write halt write halt
count split halt
if (halt && ep != 0 && set) end if
count split halt
D4 removes "ep != 0" from the halt-write condition.
In Verilog that changes ONE block.
In VHDL it changed TWO, because the counter was inside.So the VHDL mutation was strictly larger than the Verilog one — it also began counting split halts for endpoint 0 — and the two columns were measuring different experiments. Splitting the VHDL counter into its own block, statement for statement identical to the Verilog, brought D4's directed score to 296 in both.
13. Follow-Ups the Interviewer Will Ask
"How many endpoints does a device with endpoints 0, 1 and 2 have?" Between three and five. EP0 is one bidirectional endpoint; 1 and 2 are each up to two. The descriptors decide, and the question is deliberately ambiguous.
"What is the data toggle for?" Detecting a lost acknowledgement. If the device's ACK is lost, the host retries; the device sees a packet with the toggle it already accepted and discards it as a duplicate rather than writing the same bytes twice. It is per-endpoint-per-direction, which is why D3 is a data-corruption bug.
"What resets the toggle?" A bus reset, a SET_CONFIGURATION, and CLEAR_FEATURE(ENDPOINT_HALT) on that endpoint. A device that clears a halt without resetting the toggle is out of sync with the host by exactly one packet, forever.
"Can endpoint 1 IN be bulk while endpoint 1 OUT is interrupt?" Yes. They are separate endpoints with separate descriptors. It is unusual and entirely legal.
"Why can't endpoint 0 be halted?" Because clearing a halt is a control transfer over endpoint 0. A halted EP0 cannot be told to un-halt itself.
"What does a device do with a token for an endpoint it does not have?" Nothing — it does not respond at all, which the host sees as a timeout. That is different from STALL, which is an active refusal from an endpoint that exists.
"How big can a packet be?" Depends on transfer type and speed: 8–64 for full-speed control and bulk, up to 512 for high-speed bulk, up to 1024 for high-speed interrupt and isochronous. The field here is 11 bits for that reason.
14. UVM: A Register Model Indexed by Two Things
// The bug this component exists to catch is a WRITE THAT LANDS TWICE. The
// targeted location is always correct, so a scoreboard that checks the
// location it just wrote will never see it.
//
// The technique: model every endpoint independently, and after every
// operation audit EVERY entry -- then report how many entries were
// touched. A correct write touches exactly one.
typedef enum bit { DIR_OUT = 1'b0, DIR_IN = 1'b1 } ep_dir_e;
class ep_op extends uvm_sequence_item;
`uvm_object_utils(ep_op)
typedef enum { OP_LOOKUP, OP_CONFIG, OP_HALT } kind_e;
rand kind_e kind;
rand bit [3:0] num;
rand ep_dir_e dir;
rand bit enable;
rand bit [10:0] maxp;
rand bit halt_set;
rand bit xact_ack;
function new(string name = "ep_op"); super.new(name); endfunction
// The two directions must get DIFFERENT packet sizes, or a design that
// reads the wrong one cannot be distinguished from a correct one. This is
// the single most important constraint in the file.
constraint c_asymmetric_maxp {
(dir == DIR_IN) -> maxp inside {8, 64};
(dir == DIR_OUT) -> maxp inside {512, 1024};
}
// Out-of-range numbers on purpose: an endpoint a device does not have is
// a real thing a host asks for, and the answer is silence rather than STALL.
constraint c_some_out_of_range { num dist { [0:3] := 85, [4:15] := 15 }; }
endclass
class ep_scoreboard extends uvm_scoreboard;
`uvm_component_utils(ep_scoreboard)
uvm_analysis_imp #(ep_op, ep_scoreboard) ap;
localparam int N = 4;
// [number][direction] -- and the model has no field that is indexed by
// number alone, deliberately, so it cannot mirror the bug it is looking for.
bit m_en [N][2];
bit m_halted [N][2];
bit [10:0] m_maxp [N][2];
bit m_toggle [N][2];
int unsigned n_split_halt; // a halt on one direction, other unhalted
int unsigned n_asym_maxp; // the two directions held different sizes
int unsigned n_leak; // entries changed that nothing addressed
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
foreach (m_en[i, j]) begin
m_en[i][j] = (i == 0);
m_halted[i][j] = 1'b0;
m_maxp[i][j] = (i == 0) ? 11'd64 : 11'd0;
m_toggle[i][j] = 1'b0;
end
endfunction
function void write(ep_op t);
int d = int'(t.dir);
if (t.num >= N) return; // a device does not answer for what it lacks
case (t.kind)
OP_CONFIG: begin
if (t.num == 0) begin
// EP0 is one conversation, so both halves move together. This is
// the ONLY place in this model where two entries change at once,
// and it is deliberate rather than a leak.
for (int j = 0; j < 2; j++) begin
m_en[0][j] = t.enable;
m_maxp[0][j] = t.maxp;
end
end else begin
m_en[t.num][d] = t.enable;
m_maxp[t.num][d] = t.maxp;
end
if (m_maxp[t.num][0] != m_maxp[t.num][1]) n_asym_maxp++;
end
OP_HALT: begin
// EP0 is not haltable: clearing a halt is a control transfer over
// endpoint 0, so a halted EP0 could never be un-halted.
if (t.num == 0) return;
if (t.halt_set && !m_halted[t.num][1-d]) n_split_halt++;
m_halted[t.num][d] = t.halt_set;
end
OP_LOOKUP: begin
if (m_en[t.num][d] && !m_halted[t.num][d] && t.xact_ack)
m_toggle[t.num][d] = ~m_toggle[t.num][d];
end
endcase
endfunction
// Called by the monitor after every operation, with the DUT's whole table.
//
// Auditing everything is the only check that can see a write landing
// twice, because the location that was written is always right.
function void audit(bit dut_en [N][2], bit dut_hl [N][2],
bit [10:0] dut_mp [N][2], bit dut_tg [N][2]);
foreach (m_en[i, j]) begin
if (dut_en[i][j] !== m_en[i][j]) begin
n_leak++;
`uvm_error("EP/LEAK",
$sformatf("endpoint %0d %s enable is %0b, model says %0b",
i, j ? "IN" : "OUT", dut_en[i][j], m_en[i][j]))
end
if (dut_hl[i][j] !== m_halted[i][j]) begin
n_leak++;
`uvm_error("EP/LEAK",
$sformatf("endpoint %0d %s halt is %0b, model says %0b -- a halt reached a direction nobody addressed",
i, j ? "IN" : "OUT", dut_hl[i][j], m_halted[i][j]))
end
if (dut_mp[i][j] !== m_maxp[i][j]) begin
n_leak++;
`uvm_error("EP/LEAK",
$sformatf("endpoint %0d %s maxp is %0d, model says %0d",
i, j ? "IN" : "OUT", dut_mp[i][j], m_maxp[i][j]))
end
if (dut_tg[i][j] !== m_toggle[i][j]) begin
n_leak++;
`uvm_error("EP/LEAK",
$sformatf("endpoint %0d %s toggle is %0b, model says %0b -- the two directions are sharing one",
i, j ? "IN" : "OUT", dut_tg[i][j], m_toggle[i][j]))
end
end
endfunction
function void check_phase(uvm_phase phase);
super.check_phase(phase);
`uvm_info("EP",
$sformatf("%0d split halts | %0d asymmetric-size cycles | %0d leaks",
n_split_halt, n_asym_maxp, n_leak), UVM_LOW)
// Without these two, zero leaks is a statement about the stimulus.
if (n_split_halt == 0)
`uvm_error("EP/COV",
"every halt in this run moved both directions together: independence was never tested")
if (n_asym_maxp == 0)
`uvm_error("EP/COV",
"the two directions never held different packet sizes: a design reading the wrong one would have passed")
endfunction
endclass15. Common Misconceptions
"Endpoint 1 is one endpoint." It is up to two: endpoint 1 IN and endpoint 1 OUT, with independent everything.
"The endpoint number identifies the endpoint." The pair (number, direction) does. The number alone is ambiguous.
"A device with four endpoints has four endpoints." It has between four and eight. The phrase is ambiguous every time it is used.
"Halting an endpoint halts it." It halts one direction. The other keeps working, and that is correct.
"The data toggle is per endpoint number." Per number and direction. Sharing one corrupts data in both directions and reports nothing.
"Endpoint 0 is just endpoint 0." It is the one genuinely bidirectional endpoint, it exists before any descriptor is read, and it cannot be halted.
"Both directions have the same max packet size." Usually, and not necessarily — which is why a design that reads the wrong one passes most tests.
"An unknown endpoint gets a STALL." It gets silence. STALL is an active refusal from an endpoint that exists.
"A halted endpoint's toggle is frozen, so it does not matter." It must be frozen. D6 advances it and scores 10,135.
16. Exercises
1. A device has EP0, EP1 IN (bulk, 512), EP1 OUT (interrupt, 64) and EP2 IN (iso, 1024). How many endpoints is that, and what is the smallest table that can hold their state?
2. D2 reads the packet size from the wrong direction and scores lowest of the seven. Explain why, and write the one constraint that makes it the highest.
3. Construct the failure sequence for D3 (a shared data toggle): give the exact traffic pattern that causes a packet to be silently discarded as a duplicate.
4. Endpoint 0 cannot be halted. Work out what a host would have to do to recover a device that halted it anyway, and say why that is unacceptable.
5. The bench audits every direction of every number after every operation, which is O(2N) work per step. Propose a cheaper check that still catches a cross-direction leak, and say what it gives up.
6. D4's directed columns read 296 and 319 because one language's mutation covered two statements. Design a procedure that would have caught this before the scores were published.
7. Add CLEAR_FEATURE(ENDPOINT_HALT) with its toggle reset. Which of the seven properties change, and what new one is needed?
17. Summary
| Idea | Why it matters |
|---|---|
| (number, direction) is the identifier | the number alone is ambiguous |
| EP1 IN and EP1 OUT are two endpoints | separate buffer, toggle, halt, size, type |
| Halting one direction leaves the other running | and that is the correct behaviour |
| The data toggle is per direction | sharing it corrupts data silently |
| The packet sizes are independent | usually equal, which is why D2 hides |
| EP0 is bidirectional | one conversation, not two pipes |
| EP0 cannot be halted | clearing a halt needs EP0 |
| An unknown endpoint gets silence, not STALL | STALL is a refusal from something that exists |
| A halted endpoint's toggle is frozen | or it desynchronises on recovery |
| Eight parallel arrays is a bug generator | index by [num][dir] where the language allows |
| VHDL crashes on the out-of-range index | so the bound check must be local to its use |
| The design cannot self-check a leak | only an independent shadow can see it |
| Audit every entry after every write | the targeted one is always correct |
| A disagreeing directed column is the mutant | fourth time this module |
| 64 states, 7 mutations, 3 languages | 0 cross-direction leaks in 313,770 checks |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o ep_v.out ep_v.v ep_v_tb.v && ./ep_v.out |
| SystemVerilog | iverilog -g2012 -o ep_sv.out ep_sv.sv ep_sv_tb.sv && ./ep_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a ep_vhdl.vhd ep_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_ep_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_ep_vhdl |
| One mutation | iverilog -g2005 -DMUT_D1 -o mm ep_v_mut.v ep_v_tb.v && ./mm |
| Directed only (Verilog) | iverilog -g2005 -DDIRECTED_ONLY -o mm ep_v_mut.v ep_v_tb.v && ./mm |
| Directed only (VHDL) | nvc --std=2008 -e -gDIRECTED_ONLY=true tb_ep_vhdl |
All three implementations pass with 0 errors: all 64 combinations of number, direction, enable, halt and toggle; 197 halts applied to a single direction with both directions audited after each; asymmetric packet sizes throughout; zero cross-direction leaks in 313,770 checks; and every one of the seven mutations killed by directed stimulus alone, with all seven directed scores identical across languages.
Chapter 27.5 — The Transfer-Types Question moves from what an endpoint is to what it is for. Its central trade is the one candidates state backwards: isochronous transfers get guaranteed bandwidth and no retries, and those are the same property — a retry would need a slot the schedule has already given away.
Continue learning
Related tutorials
- Related topic
Endpoint Problems
A NAK is not an error and a STALL is not a NAK — one is flow control working, one is firmware refusing permanently, and a monitor that treats them alike either floods the log or misses the endpoint that has stopped.
- Related topic
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
What Is USB?
The opening interview question answered with one load-bearing idea instead of a list — USB is host-scheduled, and polling, NAK, the frame and the missing interrupt line are all consequences of it.
- Related topic
Host / Device / Hub Identification
Host and device take fifteen seconds; the hub is where the interview is decided — a hub is a repeater, not a switch, and no downstream port can ever reach another one.
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.
