USB · Module 31
“Endpoints Are Physical Ports”
Ordinary English makes an endpoint sound like a place, and every neighbouring bus has ports that are sockets. The prediction is that endpoint 2 is one thing. A four-line decoder shows that an endpoint is a number, a direction, and a declaration.
1. The Belief
"An endpoint is a place on the device — a port, a channel, a socket. A device with four endpoints has four of something, and you find them by looking at the hardware."
2. Why An Intelligent Engineer Believes It
This one is almost forced by the vocabulary, and it is reinforced by every neighbouring technology.
THE WORD
"Endpoint" in ordinary English is a PLACE -- the end of a line, a
terminus. "Port" is its synonym everywhere else in engineering.
THE NEIGHBOURS
TCP/IP a port is a number, but it identifies a SOCKET that
software opens and closes -- a place data goes
an SoC bus a port is pins. Physically countable.
a switch a port is a hole you plug a cable into
AXI a port is a bundle of wires with a name
THE DESCRIPTORS
A device's descriptors LIST its endpoints, one entry each, with an
address and attributes -- which reads exactly like an inventory of
physical resources.
THE DOCUMENTATION
Every USB device datasheet says things like "4 endpoints, 64 bytes
each", in the same table and the same tone as "2 UARTs, 1 SPI".3. The Prediction It Makes
IF AN ENDPOINT WERE A PHYSICAL PORT, THEN:
1 "endpoint 2" would name ONE thing, and IN and OUT would be
directions of travel through it -- not part of its name
2 a device's endpoint count would be a property of the silicon,
fixed and discoverable by inspection
3 the same endpoint number could not simultaneously be two
differently-behaved things
4 an endpoint would exist as soon as the hardware exists -- before
enumeration, before configuration, always
5 a descriptor would only be DESCRIBING a resource that already
exists independently of it4. The Counterexample
Two of them, because they falsify different predictions.
4a. Endpoint 2 is two endpoints
Read the endpoint descriptors of a device that has both:
bEndpointAddress = 0x02 -> endpoint 2, OUT
bEndpointAddress = 0x82 -> endpoint 2, IN
TWO descriptors. TWO entries. And they may differ in:
transfer type one bulk, one interrupt
max packet size 64 vs 8
interval meaningless vs 10 ms
halted state one STALLed, the other running fineTwo endpoints, both "number 2", with different types, different sizes, independent halt state and independent data toggles. Prediction 1 fails, and prediction 3 fails with it. "Endpoint 2" is not an address. It is half of one.
4b. The count changes without the silicon changing
One device. Two of its interface alternate settings:
alt 0 0 endpoints (the bandwidth-free idle setting)
alt 1 1 isochronous IN endpoint, 192 bytes/frame
alt 2 1 isochronous IN endpoint, 384 bytes/frame
A SET_INTERFACE request changes which of these is in force, and
therefore changes how many endpoints the device HAS -- with no
change to the silicon whatsoever.This is the standard structure of every USB audio and video device, and it exists precisely so the host can grant bandwidth it has and refuse bandwidth it does not. Prediction 2 fails, and prediction 4 fails: before configuration, a device has exactly one endpoint — endpoint 0 — no matter what its silicon contains.
5. The Corrected Model
AN ENDPOINT IS IDENTIFIED BY A PAIR
( endpoint number , direction )
and both halves are part of the name. 0x82 is not "endpoint 130";
it is the top bit meaning IN, and 2 meaning number 2.
SO:
the address space is 16 numbers x 2 directions = 32 names
endpoint 0 is special it exists in BOTH directions, always,
and is the only one that does
every other endpoint exists because a descriptor in the
ACTIVE CONFIGURATION says it does
AND THE THINGS THAT ARE PER-ENDPOINT ARE PER-PAIR:
the data toggle EP2 IN and EP2 OUT have separate ones
the halt condition independent
the max packet size independent
the transfer type independent THREE LEVELS THAT THE BELIEF COLLAPSES INTO ONE
the NAME (number, direction) -- an address
the DECLARATION a descriptor in the active configuration
the IMPLEMENTATION a buffer and some control bits
The belief says these are the same thing. The protocol keeps them
separate, and every one of the three can exist without the others:
a name with no declaration -> the host will not address it;
a request to it is an error
a declaration with no hardware -> a device that NAKs forever, or
worse, a device that lies
hardware with no declaration -> a buffer nobody can reach. Very
common, and the engineer stares
at the buffer.6. The Hardware Contract
The point of this specimen is that both halves of the name are physically part of the decode. If you drop either one, the design still compiles and still looks sensible.
PURPOSE given an endpoint number and a direction arriving on ONE
shared token path, select the one logical endpoint they
name -- or refuse, if the configuration does not declare
it
PARAMETERS NEP = 4 endpoint numbers 0..3
NIDX = 8 NEP x 2 directions
INPUTS clk, rst_n
cfg_en [7:0] which of the 8 (direction, number)
pairs the configuration declares.
Bit index is {dir, number}.
tok_valid a token arrived
tok_ep [3:0] the number it carries
tok_is_in the direction it carries
data_in [7:0]
data_we an OUT payload accompanies it
OUTPUTS sel_valid a declared endpoint was selected
sel_index [2:0] which one: {direction, number}
sel_stall addressed something not there
buf_out [7:0] the selected endpoint's byte
n_sel, n_stall
n_multi two endpoints selected at once
AUTHORITATIVE STATE
ep_buf[0:7] ONE BYTE PER LOGICAL ENDPOINT.
Eight of them. One token path.
DERIVED THE IDENTITY, and it is one line:
idx = {tok_is_in, tok_ep[1:0]}
num_ok = (tok_ep < NEP)
exists = num_ok && cfg_en[idx]
sel_valid = tok_valid && exists
sel_stall = tok_valid && !exists
RESET every buffer and counter cleared. cfg_en is an input,
so what exists is not this module's state at all.
WRITE RULE an OUT payload lands in the buffer the token selected
and in NO OTHER. The IN buffer of the SAME NUMBER is
untouched -- which is the whole claim, as a write.
LATENCY the decode is combinational in the token; the buffers
are registered.
BOUNDARY number 2 IN and number 2 OUT land on DIFFERENT indices
(6 and 2) with different storage. A number at or above
NEP is out of range and stalls. A declared-but-disabled
endpoint stalls too, and the two reasons are counted
separately.
ASSUMPTIONS four numbers and two directions, so an eight-bit
cfg_en. The real space is 16 x 2. One byte of storage
stands in for what a real part implements as a RAM.
OMISSIONS the PHY, the serial interface engine, packet framing,
descriptor parsing, alternate settings as a MECHANISM
(cfg_en simply arrives), halt state, data toggles,
max packet sizes, and endpoint 0's special status --
which is 31.5's subject.
MISCONCEPTION DEMONSTRATED
"an endpoint is a number" and
"an endpoint exists because the silicon has one"usb_ep_decode.v — the design, Verilog-2005
// =====================================================================
// usb_ep_decode -- what an endpoint actually IS, in hardware.
//
// CLASSIFICATION: simplified synthesisable teaching RTL, built for one
// purpose: to show that an endpoint's identity is a NUMBER AND A
// DIRECTION, resolved from a token arriving on ONE shared physical
// connection -- not a connector, not a pin, not a wire.
//
// It is NOT a USB device controller. There is no PHY, no serial
// interface engine, no packet framing, no data toggle, no transfer
// layer. One byte of storage per logical endpoint stands in for what a
// real part would implement as a RAM.
//
// THE THING TO NOTICE IN THE PORT LIST
// ------------------------------------
// There is ONE tok_* group and ONE data path. Eight logical endpoints
// are served by it. If endpoints were physical, this module would need
// eight of something, and it needs one.
//
// IDENTITY
// index = {direction, number}
// Endpoint 1 IN and endpoint 1 OUT are DIFFERENT ENDPOINTS with
// different storage, different enables and different behaviour. They
// share a number and nothing else. Mutation D-M1 drops the direction
// from the index -- which is the misconception, written as RTL.
// =====================================================================
module usb_ep_decode #(
// Endpoint numbers 0 .. NEP-1, each with an IN and an OUT form.
parameter integer NEP = 4,
parameter integer NIDX = 8 // NEP * 2
) (
input wire clk,
input wire rst_n,
// ---- firmware configuration: which logical endpoints exist ----
// Bit {dir, number}. An endpoint that the descriptors do not declare
// is not there, and addressing it is a request error.
input wire [NIDX-1:0] cfg_en,
// ---- ONE token path, ONE data path ----
input wire tok_valid,
input wire [3:0] tok_ep, // the number carried by the token
input wire tok_is_in, // the direction carried by the token
input wire [7:0] data_in,
input wire data_we, // an OUT payload accompanies the token
// ---- what got selected ----
output wire sel_valid,
output wire [2:0] sel_index, // {direction, number}
output wire sel_stall, // addressed an endpoint that is not there
output wire [7:0] buf_out, // the selected endpoint's byte
output wire [15:0] n_sel,
output wire [15:0] n_stall,
// Two endpoints selected at once. Structurally impossible: the index
// is a single value. The counter exists so that a build in which it
// stopped being a single value would say so.
output wire [15:0] n_multi
);
// One byte per LOGICAL endpoint. Eight of these, one connector.
reg [7:0] ep_buf [0:NIDX-1];
reg [15:0] c_sel, c_stall, c_multi;
integer k;
// Identity. Both halves, always.
wire num_ok = (tok_ep < NEP[3:0]);
wire [2:0] idx = {tok_is_in, tok_ep[1:0]};
wire exists = num_ok && cfg_en[idx];
assign sel_valid = tok_valid && exists;
assign sel_index = idx;
assign sel_stall = tok_valid && !exists;
assign buf_out = ep_buf[idx];
// A one-hot decode of the same index, used only to check the claim
// that exactly one endpoint is ever selected.
reg [NIDX-1:0] onehot;
always @(*) begin
onehot = {NIDX{1'b0}};
if (sel_valid) onehot[idx] = 1'b1;
end
reg [3:0] popcnt;
always @(*) begin
popcnt = 4'd0;
for (k = 0; k < NIDX; k = k + 1)
if (onehot[k]) popcnt = popcnt + 4'd1;
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (k = 0; k < NIDX; k = k + 1) ep_buf[k] <= 8'd0;
c_sel <= 16'd0;
c_stall <= 16'd0;
c_multi <= 16'd0;
end else begin
// An OUT payload lands in the buffer the token selected, and in
// no other. The IN buffer of the same NUMBER is untouched.
if (sel_valid && data_we && !tok_is_in) ep_buf[idx] <= data_in;
if (sel_valid) c_sel <= c_sel + 16'd1;
if (sel_stall) c_stall <= c_stall + 16'd1;
if (popcnt > 4'd1) c_multi <= c_multi + 16'd1;
end
end
assign n_sel = c_sel;
assign n_stall = c_stall;
assign n_multi = c_multi;
endmoduleThe three levels the belief collapses into one
Three of the diagram's features are the chapter:
EP2 appears TWICE in the left column, and its two rows end at
declarations with different TYPES and different SIZES
EP3 IN has a NAME and no declaration -- the arrow stops at a red
box. A perfectly good number, addressable in principle, and a token
to it raises sel_stall rather than selecting anything
BUFFER D has silicon and no name. It cannot be reached by any
token. An engineer who believes the buffer IS the endpoint will
spend a day looking at that buffer.7. The Testbench
The intent phase states the corrected model in its own terms and consults no model at all:
FOR EVERY ENDPOINT NUMBER
write a distinct byte to its OUT form
then read BOTH forms
and require that they are not the same storageThat is the whole of phase 1, and it is worth seeing why it has to be shaped that way. If endpoints were identified by number alone — which is what "endpoints are ports" amounts to in hardware — the two reads would return the same byte. The check therefore asserts that two things differ, which is a claim about the architecture rather than about any implementation of it.
A reference model that indexed its own storage the way the design does would agree with the design about this even if both ignored direction. That is exactly the failure 30.4 produced, where a design and its model shared one wrong expression and agreed for 39,108 checks. So the claim is written as a paired write-and-read across the two directions, and not as a comparison against a mirror of the RTL.
PHASES
1 INTENT IN and OUT of one number are independent
2 EXHAUSTIVE every (number, direction, enabled) triple
3 SCENARIO the named cases, including out-of-range numbers
4 RANDOM supplementary, auditedtb_usb_ep_decode.v — the testbench, Verilog-2005
// =====================================================================
// tb_usb_ep_decode -- Verilog-2005 testbench for usb_ep_decode.
//
// PHASE 1 is the intent phase and it does not consult a model. It
// states the architectural claim and requires it:
//
// endpoint N IN and endpoint N OUT are DIFFERENT ENDPOINTS.
// Writing one does not change the other.
//
// A reference model that indexed its own storage the same way the
// design does would agree with the design about this even if both
// ignored direction -- which is exactly the failure 30.4 produced.
// So the claim is written as a paired write-and-read over the two
// directions, not as a comparison against a mirror of the RTL.
//
// PHASES
// 1 INTENT IN and OUT of one number are independent
// 2 EXHAUSTIVE every (number, direction, enabled) triple
// 3 SCENARIO the named cases, including out-of-range numbers
// 4 RANDOM supplementary, audited
// =====================================================================
`timescale 1ns/1ps
module tb_usb_ep_decode;
localparam integer NEP = 4;
localparam integer NIDX = 8;
reg clk = 1'b0;
reg rst_n;
reg [NIDX-1:0] cfg_en;
reg tok_valid, tok_is_in, data_we;
reg [3:0] tok_ep;
reg [7:0] data_in;
wire sel_valid, sel_stall;
wire [2:0] sel_index;
wire [7:0] buf_out;
wire [15:0] n_sel, n_stall, n_multi;
usb_ep_decode #(.NEP(NEP), .NIDX(NIDX)) dut (
.clk(clk), .rst_n(rst_n), .cfg_en(cfg_en),
.tok_valid(tok_valid), .tok_ep(tok_ep), .tok_is_in(tok_is_in),
.data_in(data_in), .data_we(data_we),
.sel_valid(sel_valid), .sel_index(sel_index), .sel_stall(sel_stall),
.buf_out(buf_out), .n_sel(n_sel), .n_stall(n_stall), .n_multi(n_multi)
);
always #5 clk = ~clk;
// ---- the independent reference model ---------------------------
// Written from the architectural rules:
// R1 a token names an endpoint by NUMBER AND DIRECTION
// R2 an endpoint that is not configured is not addressable
// R3 a number outside the implemented range is not addressable
// R4 an OUT payload lands in that endpoint alone
// The storage below is deliberately a flat array indexed by a value
// the model computes ITSELF from the two halves of the identity.
reg [7:0] rm_buf [0:NIDX-1];
reg [15:0] rm_sel, rm_stall;
integer chk_dir, chk_rnd, err, in_random;
integer m_sel, m_stall, m_in, m_out, m_writes, m_oob, m_disabled,
m_setupfail;
integer i, j, e, d, g;
reg [7:0] cap_buf;
reg cap_valid, cap_stall;
reg [2:0] cap_index;
task bump; begin
if (in_random) chk_rnd = chk_rnd + 1; else chk_dir = chk_dir + 1;
end endtask
task ck;
input [255:0] what;
input [31:0] got;
input [31:0] exp;
begin
bump;
if (got !== exp) begin
err = err + 1;
if (!in_random && err <= 40)
$display(" ** %0s: got %0d expected %0d (t=%0t)", what, got, exp, $time);
end
end
endtask
// The model's own identity computation, from the rules rather than
// from the RTL's expression.
function [2:0] ref_index;
input dir;
input [3:0] num;
begin ref_index = {dir, num[1:0]}; end
endfunction
function ref_exists;
input dir;
input [3:0] num;
begin
ref_exists = (num < NEP) && cfg_en[ref_index(dir, num)];
end
endfunction
task ref_step;
begin
if (!rst_n) begin
for (i = 0; i < NIDX; i = i + 1) rm_buf[i] = 8'd0;
rm_sel = 0; rm_stall = 0;
end else if (tok_valid) begin
if (ref_exists(tok_is_in, tok_ep)) begin
rm_sel = rm_sel + 1;
if (data_we && !tok_is_in)
rm_buf[ref_index(tok_is_in, tok_ep)] = data_in;
m_sel = m_sel + 1;
if (tok_is_in) m_in = m_in + 1; else m_out = m_out + 1;
if (data_we && !tok_is_in) m_writes = m_writes + 1;
end else begin
rm_stall = rm_stall + 1;
m_stall = m_stall + 1;
if (tok_ep >= NEP) m_oob = m_oob + 1;
else m_disabled = m_disabled + 1;
end
end
end
endtask
task cmp_comb;
reg exp_valid, exp_stall;
begin
exp_valid = tok_valid && ref_exists(tok_is_in, tok_ep);
exp_stall = tok_valid && !ref_exists(tok_is_in, tok_ep);
cap_valid = sel_valid; cap_stall = sel_stall;
cap_index = sel_index; cap_buf = buf_out;
ck("sel_valid", {31'd0, sel_valid}, {31'd0, exp_valid});
ck("sel_stall", {31'd0, sel_stall}, {31'd0, exp_stall});
ck("sel_index", {29'd0, sel_index}, {29'd0, ref_index(tok_is_in, tok_ep)});
if (exp_valid)
ck("buf_out", {24'd0, buf_out}, {24'd0, rm_buf[ref_index(tok_is_in, tok_ep)]});
else bump;
end
endtask
task cmp_regs; begin
ck("n_sel", {16'd0, n_sel}, {16'd0, rm_sel});
ck("n_stall", {16'd0, n_stall}, {16'd0, rm_stall});
ck("n_multi", {16'd0, n_multi}, 32'd0);
end endtask
// The claim, asserted directly: at most one endpoint at a time.
task intent_check; begin
bump;
if (sel_valid && sel_stall) begin
err = err + 1;
$display(" ** INTENT VIOLATED: selected and stalled at once (t=%0t)", $time);
end
end endtask
task step; begin
#1;
cmp_comb;
intent_check;
@(posedge clk);
ref_step;
#1;
cmp_regs;
tok_valid = 0; tok_ep = 0; tok_is_in = 0; data_we = 0; data_in = 0;
end endtask
task idle; begin step; end endtask
task hard_reset; begin
rst_n = 0; cfg_en = {NIDX{1'b1}};
tok_valid = 0; tok_ep = 0; tok_is_in = 0; data_we = 0; data_in = 0;
repeat (3) begin @(posedge clk); ref_step; end
#1; rst_n = 1;
@(posedge clk); ref_step; #1; cmp_regs;
end endtask
task token;
input dir;
input [3:0] num;
input we;
input [7:0] v;
begin
tok_valid = 1; tok_is_in = dir; tok_ep = num;
data_we = we; data_in = v;
step;
end
endtask
// -----------------------------------------------------------------
// PHASE 1 -- THE INDEPENDENCE CLAIM.
//
// For every endpoint NUMBER: write a distinct byte to its OUT form,
// then read both forms and require that they are not the same
// storage. If endpoints were identified by number alone -- which is
// what "endpoints are ports" amounts to in hardware -- these two
// would be one thing and the check would fail.
// -----------------------------------------------------------------
integer pairs_tested;
task phase_intent;
reg [7:0] out_val, in_val;
begin
hard_reset;
pairs_tested = 0;
for (e = 0; e < NEP; e = e + 1) begin
// seed the IN form of this number with a known, different byte
// by writing its OUT form and then checking the IN form is
// untouched. The IN buffers all start at zero.
token(1'b0, e[3:0], 1'b1, 8'hE0 + e[7:0]); // OUT write
token(1'b0, e[3:0], 1'b0, 8'h00); // OUT read back
out_val = cap_buf;
token(1'b1, e[3:0], 1'b0, 8'h00); // IN read
in_val = cap_buf;
ck("OUT holds what was written", {24'd0, out_val}, {24'd0, 8'hE0 + e[7:0]});
ck("IN of the same number is untouched", {24'd0, in_val}, 32'd0);
bump;
if (out_val === in_val) begin
err = err + 1;
$display(" ** INTENT VIOLATED: endpoint %0d IN and OUT are one buffer", e);
end
pairs_tested = pairs_tested + 1;
end
// and the reverse direction of the same claim: writing every OUT
// form leaves every IN form at zero
for (e = 0; e < NEP; e = e + 1) begin
token(1'b1, e[3:0], 1'b0, 8'h00);
ck("every IN still zero", {24'd0, cap_buf}, 32'd0);
end
bump;
if (pairs_tested != NEP) begin
err = err + 1;
$display(" ** intent: only %0d pairs tested", pairs_tested);
end
end
endtask
// -----------------------------------------------------------------
// PHASE 2 -- the exhaustive sweep.
//
// AXES:
// endpoint number 0 .. NEP-1, plus one out of range 5
// direction OUT, IN 2
// configured no, yes 2
// ------------------------------------------------------------
// 5 x 2 x 2 = 20
//
// WHAT IS ABSENT: history. Two tokens in a row to related
// endpoints is phase 3. An exhaustive sweep is exhaustive over the
// axes it has.
// -----------------------------------------------------------------
task phase_sweep;
begin
for (e = 0; e < NEP + 1; e = e + 1)
for (d = 0; d < 2; d = d + 1)
for (g = 0; g < 2; g = g + 1) begin
hard_reset;
cfg_en = g ? {NIDX{1'b1}} : {NIDX{1'b0}};
idle;
bump;
if (cfg_en !== (g ? {NIDX{1'b1}} : {NIDX{1'b0}})) begin
err = err + 1; m_setupfail = m_setupfail + 1;
$display(" ** setup: configuration not applied");
end
token(d[0], e[3:0], 1'b1, 8'h90 + e[7:0]);
idle;
end
end
endtask
// -----------------------------------------------------------------
// PHASE 3 -- the named scenarios.
// -----------------------------------------------------------------
task phase_scenarios;
begin
// S1 eight logical endpoints, one token path. Write all four OUT
// forms with distinct bytes and read them all back.
hard_reset;
for (e = 0; e < NEP; e = e + 1) token(1'b0, e[3:0], 1'b1, 8'h10 + e[7:0]);
for (e = 0; e < NEP; e = e + 1) begin
token(1'b0, e[3:0], 1'b0, 8'h00);
ck("S1 each OUT kept its own byte", {24'd0, cap_buf}, {24'd0, 8'h10 + e[7:0]});
end
ck("S1 eight selections", {16'd0, n_sel}, 32'd8);
// S2 a number outside the implemented range is not an endpoint.
hard_reset;
token(1'b0, 4'd7, 1'b1, 8'hFF);
ck("S2 stalled", {31'd0, cap_stall}, 32'd1);
ck("S2 not selected", {31'd0, cap_valid}, 32'd0);
ck("S2 counted", {16'd0, n_stall}, 32'd1);
// S3 a number that exists but is not configured is not addressable.
// "Present in the silicon" and "declared by the descriptors"
// are different things, and the host only knows the second.
hard_reset;
cfg_en = 8'b0000_0001; // only OUT endpoint 0
idle;
token(1'b0, 4'd0, 1'b0, 8'h00);
ck("S3 EP0 OUT exists", {31'd0, cap_valid}, 32'd1);
token(1'b0, 4'd1, 1'b0, 8'h00);
ck("S3 EP1 OUT does not", {31'd0, cap_stall}, 32'd1);
token(1'b1, 4'd0, 1'b0, 8'h00);
ck("S3 EP0 IN does not either", {31'd0, cap_stall}, 32'd1);
// S4 the direction half of the identity, isolated. Same number,
// same cycle count, different endpoint.
hard_reset;
token(1'b0, 4'd2, 1'b1, 8'h55);
token(1'b1, 4'd2, 1'b0, 8'h00);
ck("S4 IN 2 is not OUT 2", {24'd0, cap_buf}, 32'd0);
ck("S4 and its index differs", {29'd0, cap_index}, 32'd6);
token(1'b0, 4'd2, 1'b0, 8'h00);
ck("S4 OUT 2 still has it", {24'd0, cap_buf}, {24'd0, 8'h55});
ck("S4 and its index", {29'd0, cap_index}, 32'd2);
// S5 never two at once.
ck("S5 nothing multi-selected", {16'd0, n_multi}, 32'd0);
end
endtask
// -----------------------------------------------------------------
// PHASE 4 -- random, audited.
// -----------------------------------------------------------------
task phase_random;
integer r;
begin
in_random = 1;
hard_reset;
for (j = 0; j < 4000; j = j + 1) begin
r = {$random} % 100;
if (r < 70) begin
token(({$random} % 2), ({$random} % 6), (({$random} % 100) < 60),
({$random} % 256));
end else if (r < 82) begin
// reconfigure: which endpoints the descriptors declare
cfg_en = {$random} % 256;
idle;
end else begin
idle;
end
end
in_random = 0;
end
endtask
initial begin
chk_dir = 0; chk_rnd = 0; err = 0; in_random = 0;
m_sel=0; m_stall=0; m_in=0; m_out=0; m_writes=0; m_oob=0;
m_disabled=0; m_setupfail=0;
phase_intent;
$display(" phase 1 intent : %0d checks, %0d errors (%0d IN/OUT pairs)",
chk_dir, err, pairs_tested);
phase_sweep;
$display(" phase 2 exhaustive : %0d checks, %0d errors (20 combinations)", chk_dir, err);
phase_scenarios;
$display(" phase 3 scenarios : %0d checks, %0d errors", chk_dir, err);
$display(" ---- DIRECTED-ONLY : %0d checks, %0d errors ----", chk_dir, err);
phase_random;
$display("");
$display(" measured reachability (all phases)");
$display(" endpoints selected ..... %0d", m_sel);
$display(" IN forms ............. %0d", m_in);
$display(" OUT forms ............ %0d", m_out);
$display(" OUT payloads written ... %0d", m_writes);
$display(" stalled: out of range .. %0d", m_oob);
$display(" stalled: not configured %0d", m_disabled);
$display(" setup failures ......... %0d", m_setupfail);
$display("");
$display(" directed checks ........ %0d", chk_dir);
$display(" random checks .......... %0d", chk_rnd);
$display(" TOTAL checks ........... %0d", chk_dir + chk_rnd);
$display(" ERRORS ................. %0d", err);
if (err == 0) $display(" PASS"); else $display(" FAIL");
$finish;
end
endmodule VERILOG SYSTEMVERILOG VHDL-2008
phase 1 intent 148 148 148
phase 2 exhaustive 708 708 708
phase 3 scenarios 864 864 864
---- DIRECTED 864 864 864
errors 0 0 0
TOTAL 32,867 32,867 32,867
measured reachability, Verilog run
endpoints selected ................... 961
IN forms ........................... 515
OUT forms .......................... 446
OUT payloads written ................. 277
stalled: number out of range ......... 958
stalled: not declared by cfg_en ...... 908
two endpoints selected at once ......... 0Four of those rows carry the argument.
515 IN + 446 OUT SELECTIONS
Both forms of the same four numbers, reached separately. Under the
belief there would be four endpoints here and these two rows
would be one row.
277 OUT PAYLOADS WRITTEN
Each one landed in exactly one of eight buffers and left the IN
buffer of the same number untouched. That is the independence
claim, executed 277 times rather than asserted once.
958 OUT-OF-RANGE vs 908 NOT-DECLARED
TWO DIFFERENT REASONS to refuse, counted separately, because they
are different failures with different fixes: one is a number that
cannot exist, the other is a name the configuration did not
declare. The belief has no vocabulary for the second.
0 TWO-AT-ONCE
A structural zero, from a one-hot decode of the same index that
exists only to be counted. It is the negative control: a build in
which "one endpoint" stopped being one value would say so.The exhaustive sweep, and its named axes
AXES
endpoint number 0, 1, 2, 3, plus one OUT OF RANGE 5
direction OUT, IN 2
configured no, yes 2
------------------------------------------------------------
5 x 2 x 2 = 20All 20 reached. The direction axis is the one the misconception lives on, and
the fifth value of the first axis — a number at or above NEP — is what
separates "this number cannot exist" from "this name was not declared".
8. SystemVerilog
usb_ep_decode_sv.sv — the design, SystemVerilog
// =====================================================================
// usb_ep_decode_sv -- the same contract in SystemVerilog. Same ports,
// same identity rule, same reset, same latency.
//
// What the types add is that the claim "exactly one endpoint is
// selected" becomes $countones of a one-hot vector, which says the
// claim rather than computing it -- and the SVA block at the bottom
// states the identity rule itself as a property that D-M1 breaks.
//
// ------------------------------------------------------------------
// What an endpoint actually IS, in hardware.
//
// CLASSIFICATION: simplified synthesisable teaching RTL, built for one
// purpose: to show that an endpoint's identity is a NUMBER AND A
// DIRECTION, resolved from a token arriving on ONE shared physical
// connection -- not a connector, not a pin, not a wire.
//
// It is NOT a USB device controller. There is no PHY, no serial
// interface engine, no packet framing, no data toggle, no transfer
// layer. One byte of storage per logical endpoint stands in for what a
// real part would implement as a RAM.
//
// THE THING TO NOTICE IN THE PORT LIST
// ------------------------------------
// There is ONE tok_* group and ONE data path. Eight logical endpoints
// are served by it. If endpoints were physical, this module would need
// eight of something, and it needs one.
//
// IDENTITY
// index = {direction, number}
// Endpoint 1 IN and endpoint 1 OUT are DIFFERENT ENDPOINTS with
// different storage, different enables and different behaviour. They
// share a number and nothing else. Mutation D-M1 drops the direction
// from the index -- which is the misconception, written as RTL.
// =====================================================================
module usb_ep_decode_sv #(
// Endpoint numbers 0 .. NEP-1, each with an IN and an OUT form.
parameter int NEP = 4,
parameter int NIDX = 8 // NEP * 2
) (
input logic clk,
input logic rst_n,
// ---- firmware configuration: which logical endpoints exist ----
// Bit {dir, number}. An endpoint that the descriptors do not declare
// is not there, and addressing it is a request error.
input logic [NIDX-1:0] cfg_en,
// ---- ONE token path, ONE data path ----
input logic tok_valid,
input logic [3:0] tok_ep, // the number carried by the token
input logic tok_is_in, // the direction carried by the token
input logic [7:0] data_in,
input logic data_we, // an OUT payload accompanies the token
// ---- what got selected ----
output logic sel_valid,
output logic [2:0] sel_index, // {direction, number}
output logic sel_stall, // addressed an endpoint that is not there
output logic [7:0] buf_out, // the selected endpoint's byte
output logic [15:0] n_sel,
output logic [15:0] n_stall,
// Two endpoints selected at once. Structurally impossible: the index
// is a single value. The counter exists so that a build in which it
// stopped being a single value would say so.
output logic [15:0] n_multi
);
// One byte per LOGICAL endpoint. Eight of these, one connector.
logic [7:0] ep_buf [NIDX];
logic [15:0] c_sel, c_stall, c_multi;
int k;
// Identity. Both halves, always.
logic num_ok;
assign num_ok = (tok_ep < 4'(NEP));
logic [2:0] idx;
assign idx = {tok_is_in, tok_ep[1:0]};
logic exists;
assign exists = num_ok && cfg_en[idx];
assign sel_valid = tok_valid && exists;
assign sel_index = idx;
assign sel_stall = tok_valid && !exists;
assign buf_out = ep_buf[idx];
// A one-hot decode of the same index, used only to check the claim
// that exactly one endpoint is ever selected.
logic [NIDX-1:0] onehot;
always_comb begin
onehot = '0;
if (sel_valid) onehot[idx] = 1'b1;
end
// $countones states the claim -- "exactly one" -- in the language of
// the claim rather than as a loop that happens to compute it.
logic [3:0] popcnt;
assign popcnt = 4'($countones(onehot));
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (k = 0; k < NIDX; k = k + 1) ep_buf[k] <= 8'd0;
c_sel <= 16'd0;
c_stall <= 16'd0;
c_multi <= 16'd0;
end else begin
// An OUT payload lands in the buffer the token selected, and in
// no other. The IN buffer of the same NUMBER is untouched.
if (sel_valid && data_we && !tok_is_in) ep_buf[idx] <= data_in;
if (sel_valid) c_sel <= c_sel + 16'd1;
if (sel_stall) c_stall <= c_stall + 16'd1;
if (popcnt > 4'd1) c_multi <= c_multi + 16'd1;
end
end
assign n_sel = c_sel;
assign n_stall = c_stall;
assign n_multi = c_multi;
`ifdef SVA_ON
// The misconception as a property: endpoint identity has TWO halves.
// Icarus rejects SVA; under Icarus the named procedural checks in the
// testbench enforce each of these.
// SAFETY. The index always carries the direction. D-M1 drops it.
property p_index_carries_direction;
@(posedge clk) disable iff (!rst_n) sel_index[2] == tok_is_in;
endproperty
a_index_carries_direction: assert property (p_index_carries_direction);
// SAFETY. Exactly one endpoint, or none. Never two.
property p_at_most_one;
@(posedge clk) disable iff (!rst_n) popcnt <= 4'd1;
endproperty
a_at_most_one: assert property (p_at_most_one);
// SAFETY. Selected and stalled are exclusive.
property p_select_xor_stall;
@(posedge clk) disable iff (!rst_n) !(sel_valid && sel_stall);
endproperty
a_select_xor_stall: assert property (p_select_xor_stall);
// SAFETY. An OUT payload lands in ONE buffer -- the one addressed.
// Written as: no buffer changes unless it is the selected index.
property p_write_is_local;
@(posedge clk) disable iff (!rst_n)
(sel_valid && data_we && !tok_is_in) |=> (ep_buf[$past(idx)] == $past(data_in));
endproperty
a_write_is_local: assert property (p_write_is_local);
c_in_sel: cover property (@(posedge clk) sel_valid && tok_is_in);
c_out_sel: cover property (@(posedge clk) sel_valid && !tok_is_in);
c_oob: cover property (@(posedge clk) tok_valid && !num_ok);
c_disabled: cover property (@(posedge clk) tok_valid && num_ok && !cfg_en[idx]);
`endif
endmoduletb_usb_ep_decode_sv.sv — the testbench, SystemVerilog
// =====================================================================
// tb_usb_ep_decode -- Verilog-2005 testbench for usb_ep_decode.
//
// PHASE 1 is the intent phase and it does not consult a model. It
// states the architectural claim and requires it:
//
// endpoint N IN and endpoint N OUT are DIFFERENT ENDPOINTS.
// Writing one does not change the other.
//
// A reference model that indexed its own storage the same way the
// design does would agree with the design about this even if both
// ignored direction -- which is exactly the failure 30.4 produced.
// So the claim is written as a paired write-and-read over the two
// directions, not as a comparison against a mirror of the RTL.
//
// PHASES
// 1 INTENT IN and OUT of one number are independent
// 2 EXHAUSTIVE every (number, direction, enabled) triple
// 3 SCENARIO the named cases, including out-of-range numbers
// 4 RANDOM supplementary, audited
// =====================================================================
`timescale 1ns/1ps
module tb_usb_ep_decode_sv;
localparam int NEP = 4;
localparam int NIDX = 8;
logic clk = 1'b0;
logic rst_n;
logic [NIDX-1:0] cfg_en;
logic tok_valid, tok_is_in, data_we;
logic [3:0] tok_ep;
logic [7:0] data_in;
wire sel_valid, sel_stall;
wire [2:0] sel_index;
wire [7:0] buf_out;
wire [15:0] n_sel, n_stall, n_multi;
usb_ep_decode_sv #(.NEP(NEP), .NIDX(NIDX)) dut (
.clk(clk), .rst_n(rst_n), .cfg_en(cfg_en),
.tok_valid(tok_valid), .tok_ep(tok_ep), .tok_is_in(tok_is_in),
.data_in(data_in), .data_we(data_we),
.sel_valid(sel_valid), .sel_index(sel_index), .sel_stall(sel_stall),
.buf_out(buf_out), .n_sel(n_sel), .n_stall(n_stall), .n_multi(n_multi)
);
always #5 clk = ~clk;
// ---- the independent reference model ---------------------------
// Written from the architectural rules:
// R1 a token names an endpoint by NUMBER AND DIRECTION
// R2 an endpoint that is not configured is not addressable
// R3 a number outside the implemented range is not addressable
// R4 an OUT payload lands in that endpoint alone
// The storage below is deliberately a flat array indexed by a value
// the model computes ITSELF from the two halves of the identity.
logic [7:0] rm_buf [NIDX];
logic [15:0] rm_sel, rm_stall;
int chk_dir, chk_rnd, err;
bit in_random;
int m_sel, m_stall, m_in, m_out, m_writes, m_oob, m_disabled,
m_setupfail;
int i, j, e, d, g;
logic [7:0] cap_buf;
logic cap_valid, cap_stall;
logic [2:0] cap_index;
task bump; begin
if (in_random) chk_rnd = chk_rnd + 1; else chk_dir = chk_dir + 1;
end endtask
task ck(string what, logic [31:0] got, logic [31:0] exp);
begin
bump;
if (got !== exp) begin
err = err + 1;
if (!in_random && err <= 40)
$display(" ** %s: got %0d expected %0d (t=%0t)", what, got, exp, $time);
end
end
endtask
// The model's own identity computation, from the rules rather than
// from the RTL's expression.
function logic [2:0] ref_index(logic dir, logic [3:0] num);
return {dir, num[1:0]};
endfunction
function logic ref_exists(logic dir, logic [3:0] num);
return (num < NEP) && cfg_en[ref_index(dir, num)];
endfunction
task ref_step;
begin
if (!rst_n) begin
for (i = 0; i < NIDX; i = i + 1) rm_buf[i] = 8'd0;
rm_sel = 0; rm_stall = 0;
end else if (tok_valid) begin
if (ref_exists(tok_is_in, tok_ep)) begin
rm_sel = rm_sel + 1;
if (data_we && !tok_is_in)
rm_buf[ref_index(tok_is_in, tok_ep)] = data_in;
m_sel = m_sel + 1;
if (tok_is_in) m_in = m_in + 1; else m_out = m_out + 1;
if (data_we && !tok_is_in) m_writes = m_writes + 1;
end else begin
rm_stall = rm_stall + 1;
m_stall = m_stall + 1;
if (tok_ep >= NEP) m_oob = m_oob + 1;
else m_disabled = m_disabled + 1;
end
end
end
endtask
task cmp_comb;
logic exp_valid, exp_stall;
begin
exp_valid = tok_valid && ref_exists(tok_is_in, tok_ep);
exp_stall = tok_valid && !ref_exists(tok_is_in, tok_ep);
cap_valid = sel_valid; cap_stall = sel_stall;
cap_index = sel_index; cap_buf = buf_out;
ck("sel_valid", {31'd0, sel_valid}, {31'd0, exp_valid});
ck("sel_stall", {31'd0, sel_stall}, {31'd0, exp_stall});
ck("sel_index", {29'd0, sel_index}, {29'd0, ref_index(tok_is_in, tok_ep)});
if (exp_valid)
ck("buf_out", {24'd0, buf_out}, {24'd0, rm_buf[ref_index(tok_is_in, tok_ep)]});
else bump;
end
endtask
task cmp_regs; begin
ck("n_sel", {16'd0, n_sel}, {16'd0, rm_sel});
ck("n_stall", {16'd0, n_stall}, {16'd0, rm_stall});
ck("n_multi", {16'd0, n_multi}, 32'd0);
end endtask
// The claim, asserted directly: at most one endpoint at a time.
task intent_check; begin
bump;
if (sel_valid && sel_stall) begin
err = err + 1;
$display(" ** INTENT VIOLATED: selected and stalled at once (t=%0t)", $time);
end
end endtask
task step; begin
#1;
cmp_comb;
intent_check;
@(posedge clk);
ref_step;
#1;
cmp_regs;
tok_valid = 0; tok_ep = 0; tok_is_in = 0; data_we = 0; data_in = 0;
end endtask
task idle; step; endtask
task hard_reset; begin
rst_n = 0; cfg_en = {NIDX{1'b1}};
tok_valid = 0; tok_ep = 0; tok_is_in = 0; data_we = 0; data_in = 0;
repeat (3) begin @(posedge clk); ref_step; end
#1; rst_n = 1;
@(posedge clk); ref_step; #1; cmp_regs;
end endtask
task token(logic dir, logic [3:0] num, logic we, logic [7:0] v);
begin
tok_valid = 1; tok_is_in = dir; tok_ep = num;
data_we = we; data_in = v;
step;
end
endtask
// -----------------------------------------------------------------
// PHASE 1 -- THE INDEPENDENCE CLAIM.
//
// For every endpoint NUMBER: write a distinct byte to its OUT form,
// then read both forms and require that they are not the same
// storage. If endpoints were identified by number alone -- which is
// what "endpoints are ports" amounts to in hardware -- these two
// would be one thing and the check would fail.
// -----------------------------------------------------------------
int pairs_tested;
task phase_intent;
logic [7:0] out_val, in_val;
begin
hard_reset;
pairs_tested = 0;
for (e = 0; e < NEP; e = e + 1) begin
// seed the IN form of this number with a known, different byte
// by writing its OUT form and then checking the IN form is
// untouched. The IN buffers all start at zero.
token(1'b0, 4'(e), 1'b1, 8'hE0 + 8'(e)); // OUT write
token(1'b0, 4'(e), 1'b0, 8'h00); // OUT read back
out_val = cap_buf;
token(1'b1, 4'(e), 1'b0, 8'h00); // IN read
in_val = cap_buf;
ck("OUT holds what was written", {24'd0, out_val}, {24'd0, 8'hE0 + 8'(e)});
ck("IN of the same number is untouched", {24'd0, in_val}, 32'd0);
bump;
if (out_val === in_val) begin
err = err + 1;
$display(" ** INTENT VIOLATED: endpoint %0d IN and OUT are one buffer", e);
end
pairs_tested = pairs_tested + 1;
end
// and the reverse direction of the same claim: writing every OUT
// form leaves every IN form at zero
for (e = 0; e < NEP; e = e + 1) begin
token(1'b1, 4'(e), 1'b0, 8'h00);
ck("every IN still zero", {24'd0, cap_buf}, 32'd0);
end
bump;
if (pairs_tested != NEP) begin
err = err + 1;
$display(" ** intent: only %0d pairs tested", pairs_tested);
end
end
endtask
// -----------------------------------------------------------------
// PHASE 2 -- the exhaustive sweep.
//
// AXES:
// endpoint number 0 .. NEP-1, plus one out of range 5
// direction OUT, IN 2
// configured no, yes 2
// ------------------------------------------------------------
// 5 x 2 x 2 = 20
//
// WHAT IS ABSENT: history. Two tokens in a row to related
// endpoints is phase 3. An exhaustive sweep is exhaustive over the
// axes it has.
// -----------------------------------------------------------------
task phase_sweep;
begin
for (e = 0; e < NEP + 1; e = e + 1)
for (d = 0; d < 2; d = d + 1)
for (g = 0; g < 2; g = g + 1) begin
hard_reset;
cfg_en = g ? {NIDX{1'b1}} : {NIDX{1'b0}};
idle;
bump;
if (cfg_en !== (g ? {NIDX{1'b1}} : {NIDX{1'b0}})) begin
err = err + 1; m_setupfail = m_setupfail + 1;
$display(" ** setup: configuration not applied");
end
token(d[0], 4'(e), 1'b1, 8'h90 + 8'(e));
idle;
end
end
endtask
// -----------------------------------------------------------------
// PHASE 3 -- the named scenarios.
// -----------------------------------------------------------------
task phase_scenarios;
begin
// S1 eight logical endpoints, one token path. Write all four OUT
// forms with distinct bytes and read them all back.
hard_reset;
for (e = 0; e < NEP; e = e + 1) token(1'b0, 4'(e), 1'b1, 8'h10 + 8'(e));
for (e = 0; e < NEP; e = e + 1) begin
token(1'b0, 4'(e), 1'b0, 8'h00);
ck("S1 each OUT kept its own byte", {24'd0, cap_buf}, {24'd0, 8'h10 + 8'(e)});
end
ck("S1 eight selections", {16'd0, n_sel}, 32'd8);
// S2 a number outside the implemented range is not an endpoint.
hard_reset;
token(1'b0, 4'd7, 1'b1, 8'hFF);
ck("S2 stalled", {31'd0, cap_stall}, 32'd1);
ck("S2 not selected", {31'd0, cap_valid}, 32'd0);
ck("S2 counted", {16'd0, n_stall}, 32'd1);
// S3 a number that exists but is not configured is not addressable.
// "Present in the silicon" and "declared by the descriptors"
// are different things, and the host only knows the second.
hard_reset;
cfg_en = 8'b0000_0001; // only OUT endpoint 0
idle;
token(1'b0, 4'd0, 1'b0, 8'h00);
ck("S3 EP0 OUT exists", {31'd0, cap_valid}, 32'd1);
token(1'b0, 4'd1, 1'b0, 8'h00);
ck("S3 EP1 OUT does not", {31'd0, cap_stall}, 32'd1);
token(1'b1, 4'd0, 1'b0, 8'h00);
ck("S3 EP0 IN does not either", {31'd0, cap_stall}, 32'd1);
// S4 the direction half of the identity, isolated. Same number,
// same cycle count, different endpoint.
hard_reset;
token(1'b0, 4'd2, 1'b1, 8'h55);
token(1'b1, 4'd2, 1'b0, 8'h00);
ck("S4 IN 2 is not OUT 2", {24'd0, cap_buf}, 32'd0);
ck("S4 and its index differs", {29'd0, cap_index}, 32'd6);
token(1'b0, 4'd2, 1'b0, 8'h00);
ck("S4 OUT 2 still has it", {24'd0, cap_buf}, {24'd0, 8'h55});
ck("S4 and its index", {29'd0, cap_index}, 32'd2);
// S5 never two at once.
ck("S5 nothing multi-selected", {16'd0, n_multi}, 32'd0);
end
endtask
// -----------------------------------------------------------------
// PHASE 4 -- random, audited.
// -----------------------------------------------------------------
task phase_random;
int r;
begin
in_random = 1;
hard_reset;
for (j = 0; j < 4000; j = j + 1) begin
r = $urandom_range(99);
if (r < 70) begin
token(1'($urandom_range(1)), 4'($urandom_range(5)),
($urandom_range(99) < 60), 8'($urandom_range(255)));
end else if (r < 82) begin
// reconfigure: which endpoints the descriptors declare
cfg_en = NIDX'($urandom_range(255));
idle;
end else begin
idle;
end
end
in_random = 0;
end
endtask
initial begin
chk_dir = 0; chk_rnd = 0; err = 0; in_random = 0;
m_sel=0; m_stall=0; m_in=0; m_out=0; m_writes=0; m_oob=0;
m_disabled=0; m_setupfail=0;
phase_intent;
$display(" phase 1 intent : %0d checks, %0d errors (%0d IN/OUT pairs)",
chk_dir, err, pairs_tested);
phase_sweep;
$display(" phase 2 exhaustive : %0d checks, %0d errors (20 combinations)", chk_dir, err);
phase_scenarios;
$display(" phase 3 scenarios : %0d checks, %0d errors", chk_dir, err);
$display(" ---- DIRECTED-ONLY : %0d checks, %0d errors ----", chk_dir, err);
phase_random;
$display("");
$display(" measured reachability (all phases)");
$display(" endpoints selected ..... %0d", m_sel);
$display(" IN forms ............. %0d", m_in);
$display(" OUT forms ............ %0d", m_out);
$display(" OUT payloads written ... %0d", m_writes);
$display(" stalled: out of range .. %0d", m_oob);
$display(" stalled: not configured %0d", m_disabled);
$display(" setup failures ......... %0d", m_setupfail);
$display("");
$display(" directed checks ........ %0d", chk_dir);
$display(" random checks .......... %0d", chk_rnd);
$display(" TOTAL checks ........... %0d", chk_dir + chk_rnd);
$display(" ERRORS ................. %0d", err);
if (err == 0) $display(" PASS"); else $display(" FAIL");
$finish;
end
endmoduleThe structural claim becomes a one-line property:
property p_index_carries_direction;
@(posedge clk) disable iff (!rst_n) sel_index[2] == tok_is_in;
endpropertyWhich is unusual and worth flagging: it is an assertion about an encoding, not about a behaviour over time. No antecedent delay, no consequent sequence, no implication at all — just an equality that must hold in every cycle. It is written that way because the encoding is the architectural claim, and a mutation that changes it is a mutation that changes what an endpoint is.
Beside it sit the negative control and the locality claim, and the second one is the independence check promoted from the testbench into one line:
property p_at_most_one;
@(posedge clk) disable iff (!rst_n) popcnt <= 4'd1;
endproperty
property p_write_is_local; // an OUT payload changes ONE bufferp_at_most_one is the assertion form of the n_multi counter: both say "exactly
one endpoint, or none, ever", and both are structurally satisfied. Having the claim
in both forms is deliberate — Icarus rejects concurrent SVA, so under Icarus the
counter and the named procedural checks in the testbench are what enforce it.
9. VHDL-2008
usb_ep_decode.vhd — the design, VHDL-2008
-- =====================================================================
-- usb_ep_decode (VHDL-2008) -- the same contract. Same ports, same
-- identity rule, same reset, same latency.
--
-- VHDL's contribution to THIS misconception is that the identity has
-- to be constructed explicitly from its two halves and converted to an
-- index with a written conversion. There is no way to write the index
-- without mentioning the direction, which is precisely the thing the
-- misconception leaves out.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity usb_ep_decode is
generic (NEP : natural := 4; NIDX : natural := 8);
port (
clk : in std_logic;
rst_n : in std_logic;
cfg_en : in std_logic_vector(NIDX - 1 downto 0);
tok_valid : in std_logic;
tok_ep : in unsigned(3 downto 0);
tok_is_in : in std_logic;
data_in : in std_logic_vector(7 downto 0);
data_we : in std_logic;
sel_valid : out std_logic;
sel_index : out unsigned(2 downto 0);
sel_stall : out std_logic;
buf_out : out std_logic_vector(7 downto 0);
n_sel : out unsigned(15 downto 0);
n_stall : out unsigned(15 downto 0);
n_multi : out unsigned(15 downto 0)
);
end entity usb_ep_decode;
architecture rtl of usb_ep_decode is
type buf_arr is array (0 to NIDX - 1) of std_logic_vector(7 downto 0);
signal ep_buf : buf_arr := (others => (others => '0'));
signal c_sel, c_stall, c_multi : unsigned(15 downto 0) := (others => '0');
signal num_ok, exists, sv_i, ss_i : std_logic;
signal idx : unsigned(2 downto 0);
signal onehot : std_logic_vector(NIDX - 1 downto 0);
signal popcnt : natural range 0 to NIDX;
function popcount (v : std_logic_vector) return natural is
variable n : natural := 0;
begin
for i in v'range loop
if v(i) = '1' then n := n + 1; end if;
end loop;
return n;
end function popcount;
begin
num_ok <= '1' when tok_ep < NEP else '0';
-- The identity, both halves, written out.
idx <= tok_is_in & tok_ep(1 downto 0);
exists <= '1' when (num_ok = '1' and cfg_en(to_integer(idx)) = '1') else '0';
sv_i <= tok_valid and exists;
ss_i <= tok_valid and (not exists);
sel_valid <= sv_i;
sel_index <= idx;
sel_stall <= ss_i;
buf_out <= ep_buf(to_integer(idx));
oh : process (sv_i, idx)
variable v : std_logic_vector(NIDX - 1 downto 0);
begin
v := (others => '0');
if sv_i = '1' then v(to_integer(idx)) := '1'; end if;
onehot <= v;
end process oh;
popcnt <= popcount(onehot);
seq : process (clk, rst_n)
begin
if rst_n = '0' then
ep_buf <= (others => (others => '0'));
c_sel <= (others => '0');
c_stall <= (others => '0');
c_multi <= (others => '0');
elsif rising_edge(clk) then
if sv_i = '1' and data_we = '1' and tok_is_in = '0' then
ep_buf(to_integer(idx)) <= data_in;
end if;
if sv_i = '1' then c_sel <= c_sel + 1; end if;
if ss_i = '1' then c_stall <= c_stall + 1; end if;
if popcnt > 1 then c_multi <= c_multi + 1; end if;
end if;
end process seq;
n_sel <= c_sel;
n_stall <= c_stall;
n_multi <= c_multi;
end architecture rtl;tb_usb_ep_decode.vhd — the testbench, VHDL-2008
-- =====================================================================
-- tb_usb_ep_decode -- VHDL-2008 testbench for usb_ep_decode.
-- Phases 1-3 present the SAME directed stimulus as the Verilog and
-- SystemVerilog benches, so their directed counts must agree.
--
-- Phase 1 states the independence claim directly rather than comparing
-- against a model that indexes its storage the way the design does.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
entity tb_usb_ep_decode is
end entity tb_usb_ep_decode;
architecture sim of tb_usb_ep_decode is
constant NEP : natural := 4;
constant NIDX : natural := 8;
constant HALF : time := 10 ns;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal cfg_en : std_logic_vector(NIDX-1 downto 0) := (others => '1');
signal tok_valid : std_logic := '0';
signal tok_ep : unsigned(3 downto 0) := (others => '0');
signal tok_is_in : std_logic := '0';
signal data_in : std_logic_vector(7 downto 0) := (others => '0');
signal data_we : std_logic := '0';
signal sel_valid, sel_stall : std_logic;
signal sel_index : unsigned(2 downto 0);
signal buf_out : std_logic_vector(7 downto 0);
signal n_sel, n_stall, n_multi : unsigned(15 downto 0);
signal done_flag : boolean := false;
function b2i (s : std_logic) return integer is
begin
if s = '1' then return 1; else return 0; end if;
end function b2i;
begin
dut : entity work.usb_ep_decode
generic map (NEP => NEP, NIDX => NIDX)
port map (clk => clk, rst_n => rst_n, cfg_en => cfg_en,
tok_valid => tok_valid, tok_ep => tok_ep, tok_is_in => tok_is_in,
data_in => data_in, data_we => data_we,
sel_valid => sel_valid, sel_index => sel_index,
sel_stall => sel_stall, buf_out => buf_out,
n_sel => n_sel, n_stall => n_stall, n_multi => n_multi);
clkgen : process
begin
while not done_flag loop
clk <= '0'; wait for HALF;
clk <= '1'; wait for HALF;
end loop;
wait;
end process clkgen;
stim : process
type buf_arr is array (0 to NIDX-1) of std_logic_vector(7 downto 0);
variable rm_buf : buf_arr := (others => (others => '0'));
variable rm_sel, rm_stall : natural := 0;
variable chk_dir, chk_rnd, errs, shown : natural := 0;
variable in_random : boolean := false;
variable m_sel, m_stall, m_in, m_out, m_writes : natural := 0;
variable m_oob, m_disabled, m_setupfail, pairs_tested : natural := 0;
variable cap_valid, cap_stall : std_logic := '0';
variable cap_index : unsigned(2 downto 0) := (others => '0');
variable cap_buf : std_logic_vector(7 downto 0) := (others => '0');
variable seed1 : positive := 337_711;
variable seed2 : positive := 64_223;
procedure bump is
begin
if in_random then chk_rnd := chk_rnd + 1; else chk_dir := chk_dir + 1; end if;
end procedure bump;
procedure ck (what : string; got : integer; exp : integer) is
begin
bump;
if got /= exp then
errs := errs + 1;
if (not in_random) and shown < 40 then
shown := shown + 1;
report " ** " & what & ": got " & integer'image(got) &
" expected " & integer'image(exp) severity warning;
end if;
end if;
end procedure ck;
-- The model computes the identity from the two halves ITSELF.
impure function ref_index (dir : std_logic; num : unsigned(3 downto 0))
return integer is
begin
return b2i(dir) * 4 + to_integer(num(1 downto 0));
end function ref_index;
impure function ref_exists (dir : std_logic; num : unsigned(3 downto 0))
return boolean is
begin
return (num < NEP) and (cfg_en(ref_index(dir, num)) = '1');
end function ref_exists;
procedure ref_step is
begin
if rst_n = '0' then
rm_buf := (others => (others => '0'));
rm_sel := 0; rm_stall := 0;
elsif tok_valid = '1' then
if ref_exists(tok_is_in, tok_ep) then
rm_sel := rm_sel + 1;
if data_we = '1' and tok_is_in = '0' then
rm_buf(ref_index(tok_is_in, tok_ep)) := data_in;
m_writes := m_writes + 1;
end if;
m_sel := m_sel + 1;
if tok_is_in = '1' then m_in := m_in + 1; else m_out := m_out + 1; end if;
else
rm_stall := rm_stall + 1;
m_stall := m_stall + 1;
if tok_ep >= NEP then m_oob := m_oob + 1;
else m_disabled := m_disabled + 1; end if;
end if;
end if;
end procedure ref_step;
procedure cmp_comb is
variable ev, es : integer;
begin
if tok_valid = '1' and ref_exists(tok_is_in, tok_ep) then ev := 1; else ev := 0; end if;
if tok_valid = '1' and not ref_exists(tok_is_in, tok_ep) then es := 1; else es := 0; end if;
cap_valid := sel_valid; cap_stall := sel_stall;
cap_index := sel_index; cap_buf := buf_out;
ck("sel_valid", b2i(sel_valid), ev);
ck("sel_stall", b2i(sel_stall), es);
ck("sel_index", to_integer(sel_index), ref_index(tok_is_in, tok_ep));
if ev = 1 then
ck("buf_out", to_integer(unsigned(buf_out)),
to_integer(unsigned(rm_buf(ref_index(tok_is_in, tok_ep)))));
else
bump;
end if;
end procedure cmp_comb;
procedure cmp_regs is
begin
ck("n_sel", to_integer(n_sel), rm_sel);
ck("n_stall", to_integer(n_stall), rm_stall);
ck("n_multi", to_integer(n_multi), 0);
end procedure cmp_regs;
procedure intent_check is
begin
bump;
if sel_valid = '1' and sel_stall = '1' then
errs := errs + 1;
report " ** INTENT VIOLATED: selected and stalled at once" severity warning;
end if;
end procedure intent_check;
procedure step is
begin
wait for 1 ns;
cmp_comb;
intent_check;
wait until rising_edge(clk);
ref_step;
wait for 1 ns;
cmp_regs;
tok_valid <= '0'; tok_ep <= (others => '0'); tok_is_in <= '0';
data_we <= '0'; data_in <= (others => '0');
end procedure step;
procedure idle is begin step; end procedure;
procedure hard_reset is
begin
rst_n <= '0'; cfg_en <= (others => '1');
tok_valid <= '0'; tok_ep <= (others => '0'); tok_is_in <= '0';
data_we <= '0'; data_in <= (others => '0');
for i in 0 to 2 loop wait until rising_edge(clk); ref_step; end loop;
wait for 1 ns; rst_n <= '1';
wait until rising_edge(clk); ref_step; wait for 1 ns; cmp_regs;
end procedure hard_reset;
procedure token (dir : std_logic; num : natural; we : std_logic; v : natural) is
begin
tok_valid <= '1'; tok_is_in <= dir;
tok_ep <= to_unsigned(num, 4);
data_we <= we;
data_in <= std_logic_vector(to_unsigned(v, 8));
step;
end procedure token;
impure function rnd (n : positive) return natural is
variable x : real;
begin
uniform(seed1, seed2, x);
return natural(real(n - 1) * x);
end function rnd;
variable out_val, in_val : std_logic_vector(7 downto 0);
variable r : natural;
begin
-- ---- PHASE 1 : the independence claim ----
hard_reset;
pairs_tested := 0;
for e in 0 to NEP-1 loop
token('0', e, '1', 16#E0# + e);
token('0', e, '0', 0);
out_val := cap_buf;
token('1', e, '0', 0);
in_val := cap_buf;
ck("OUT holds what was written", to_integer(unsigned(out_val)), 16#E0# + e);
ck("IN of the same number is untouched", to_integer(unsigned(in_val)), 0);
bump;
if out_val = in_val then
errs := errs + 1;
report " ** INTENT VIOLATED: IN and OUT are one buffer" severity warning;
end if;
pairs_tested := pairs_tested + 1;
end loop;
for e in 0 to NEP-1 loop
token('1', e, '0', 0);
ck("every IN still zero", to_integer(unsigned(cap_buf)), 0);
end loop;
bump;
if pairs_tested /= NEP then
errs := errs + 1;
report " ** intent: too few pairs" severity warning;
end if;
report " phase 1 intent : " & integer'image(chk_dir) &
" checks, " & integer'image(errs) & " errors (" &
integer'image(pairs_tested) & " IN/OUT pairs)";
-- ---- PHASE 2 : 5 x 2 x 2 = 20 ----
for e in 0 to NEP loop
for d in 0 to 1 loop
for g in 0 to 1 loop
hard_reset;
if g = 1 then cfg_en <= (others => '1'); else cfg_en <= (others => '0'); end if;
idle;
bump;
if (g = 1 and cfg_en /= (cfg_en'range => '1')) or
(g = 0 and cfg_en /= (cfg_en'range => '0')) then
errs := errs + 1; m_setupfail := m_setupfail + 1;
report " ** setup: configuration not applied" severity warning;
end if;
if d = 1 then token('1', e, '1', 16#90# + e);
else token('0', e, '1', 16#90# + e); end if;
idle;
end loop;
end loop;
end loop;
report " phase 2 exhaustive : " & integer'image(chk_dir) &
" checks, " & integer'image(errs) & " errors (20 combinations)";
-- ---- PHASE 3 : the named scenarios ----
hard_reset;
for e in 0 to NEP-1 loop token('0', e, '1', 16#10# + e); end loop;
for e in 0 to NEP-1 loop
token('0', e, '0', 0);
ck("S1 each OUT kept its own byte", to_integer(unsigned(cap_buf)), 16#10# + e);
end loop;
ck("S1 eight selections", to_integer(n_sel), 8);
hard_reset;
token('0', 7, '1', 16#FF#);
ck("S2 stalled", b2i(cap_stall), 1);
ck("S2 not selected", b2i(cap_valid), 0);
ck("S2 counted", to_integer(n_stall), 1);
hard_reset;
cfg_en <= "00000001";
idle;
token('0', 0, '0', 0);
ck("S3 EP0 OUT exists", b2i(cap_valid), 1);
token('0', 1, '0', 0);
ck("S3 EP1 OUT does not", b2i(cap_stall), 1);
token('1', 0, '0', 0);
ck("S3 EP0 IN does not either", b2i(cap_stall), 1);
hard_reset;
token('0', 2, '1', 16#55#);
token('1', 2, '0', 0);
ck("S4 IN 2 is not OUT 2", to_integer(unsigned(cap_buf)), 0);
ck("S4 and its index differs", to_integer(cap_index), 6);
token('0', 2, '0', 0);
ck("S4 OUT 2 still has it", to_integer(unsigned(cap_buf)), 16#55#);
ck("S4 and its index", to_integer(cap_index), 2);
ck("S5 nothing multi-selected", to_integer(n_multi), 0);
report " phase 3 scenarios : " & integer'image(chk_dir) &
" checks, " & integer'image(errs) & " errors";
report " ---- DIRECTED-ONLY : " & integer'image(chk_dir) &
" checks, " & integer'image(errs) & " errors ----";
-- ---- PHASE 4 : random ----
in_random := true;
hard_reset;
for j in 0 to 3999 loop
r := rnd(100);
if r < 70 then
if rnd(2) = 1 then
token('1', rnd(6), '0', rnd(256));
else
if rnd(100) < 60 then token('0', rnd(6), '1', rnd(256));
else token('0', rnd(6), '0', rnd(256)); end if;
end if;
elsif r < 82 then
cfg_en <= std_logic_vector(to_unsigned(rnd(256), NIDX));
idle;
else
idle;
end if;
end loop;
in_random := false;
report " measured reachability (all phases)";
report " endpoints selected ..... " & integer'image(m_sel);
report " IN forms ............. " & integer'image(m_in);
report " OUT forms ............ " & integer'image(m_out);
report " OUT payloads written ... " & integer'image(m_writes);
report " stalled: out of range .. " & integer'image(m_oob);
report " stalled: not configured " & integer'image(m_disabled);
report " setup failures ......... " & integer'image(m_setupfail);
report " directed checks ........ " & integer'image(chk_dir);
report " random checks .......... " & integer'image(chk_rnd);
report " TOTAL checks ........... " & integer'image(chk_dir + chk_rnd);
report " ERRORS ................. " & integer'image(errs);
if errs = 0 then report " PASS"; else report " FAIL" severity failure; end if;
done_flag <= true;
wait;
end process stim;
end architecture sim;VHDL makes the index construction unusually legible, because the concatenation is explicit about what is the high bit:
idx <= tok_is_in & tok_ep(1 downto 0);
exists <= '1' when (num_ok = '1' and cfg_en(to_integer(idx)) = '1') else '0';The direction is the leading element of the concatenation that addresses the buffer
array, and cfg_en is indexed by the result. So "is direction part of the address"
and "does existence depend on the declaration" are both answered by reading two
lines, with nothing to trace and no comment to trust.
10. The Misconception As Hardware
MUT THE BELIEF ENCODED V-DIR SV-DIR VH-DIR
D-M1 an endpoint is a number
(direction dropped from the index) 48 48 48
D-M2 an endpoint exists because the
silicon has one (map ignored) 58 58 58BASE is zero in all six columns; both directed columns match across the
languages, so the entire detection is from directed stimulus.
D-M1 scores 48, and the shape of those 48 is the interesting part. The mutant collapses IN and OUT of one number onto one buffer, so it is wrong only where the two forms are supposed to hold different bytes. A stimulus that wrote one form and read the same form back would find nothing: the mutant is correct whenever the two directions are never distinguished. This is why 48 rather than hundreds, and why the paired write-and-read of phase 1 is the check that finds it.
D-M2 scores 58. It makes every name that the silicon implements exist, regardless of the configuration's map. A device built this way responds to endpoints it never declared, which in the field looks like a device that works until a host validates its descriptors — and then looks like a host bug.
11. What The Wrong Model Does To Debugging
SYMPTOM "endpoint 2 works in one direction and not the other"
WRONG MODEL endpoint 2 is one thing
WRONG QUESTION why is my endpoint working intermittently /
partially / only for reads?
WASTED ON buffer sizing, arbitration between the directions,
a suspected shared-resource conflict inside one
endpoint -- an investigation of a thing that does
not exist
CORRECT MODEL those are TWO endpoints
THE QUESTIONS, and they are ordinary once asked:
are BOTH declared in the active configuration?
do their descriptors have the sizes and types you
think they do?
is one of them HALTED? (independent state)
are their data toggles independent, and did you reset
the right one? SYMPTOM "the host never touches my endpoint"
WRONG MODEL the endpoint exists because I built it
WRONG QUESTION why won't the host talk to my endpoint?
WASTED ON the endpoint's logic, which is fine
CORRECT MODEL an endpoint exists because the active configuration
declares it
THE QUESTION is it in the descriptors of the configuration and
alternate setting currently in force -- not the
descriptors you wrote, the ones the host READ?
Dump them from the host side.The second one deserves emphasis because of how often it happens with alternate
settings. A device that works after a manual SET_INTERFACE and not otherwise
has silicon that is entirely correct and a declaration that the host never
activated. The buffer is innocent every time.
12. Interview Reasoning
"How many endpoints can a USB device have?"
The question has a trap in it, and walking into the trap knowingly is the answer.
The address space is four bits of endpoint number and one bit of direction, so 32 names — but the count is not really the interesting part, because an endpoint is identified by the pair (number, direction), not by the number alone. Endpoint 2 IN and endpoint 2 OUT are two different endpoints with their own transfer type, their own maximum packet size, their own halt state and their own data toggle.
Endpoint 0 is the exception that shows the rule: it exists in both directions, always, and it exists by specification rather than because a descriptor declares it. It is the only one like that.
And the other half of the answer is that a device does not "have" endpoints in a fixed sense at all. Before configuration it has exactly one — endpoint 0 — no matter what its silicon contains. After configuration it has the ones that the active configuration's descriptors declare, which is why a device with alternate settings can change how many endpoints it has with a single control request and no change in hardware. That mechanism is how isochronous devices negotiate bandwidth the host actually has.
So the practical version of the answer is: 32 names available, one endpoint before configuration, and after that, exactly as many as the active configuration says — which is a property of the descriptors, not of the chip.
13. Exercises
1 IDENTITY
Write out the (number, direction) pair for 0x03, 0x83, 0x00 and
0x80, and say which of the four always exist.
2 DESCRIPTORS
A device declares 0x02 as bulk OUT 64 B and 0x82 as interrupt
IN 8 B. List every piece of state these two do NOT share.
3 ALTERNATE SETTINGS
Explain why an isochronous device offers an alternate setting
with zero endpoints, and what breaks if it does not.
4 VERILOG
Set NEP to 16. What is the index width, what is the width of
cfg_en, and which lines of the module change?
5 SYSTEMVERILOG
Add per-endpoint halt state. Write the property that says a
halted endpoint in one direction does not affect the other.
6 VHDL
Implement the extension in exercise 4 and say what the range
constraint buys you that the Verilog version does not have.
7 TESTBENCH
Phase 2 sweeps three axes and contains no history. Name the
fourth axis that halt state would add, and the first SEQUENCE it
would make necessary.
8 MUTATION
Write a third mutation for the belief "endpoint 0 is just
another endpoint" and predict its score. Then say which intent
check catches it.
9 DEBUG
A device's bulk IN works and its bulk OUT NAKs forever. Give
four hypotheses in the order you would test them, and say which
ones the wrong model would never generate.
10 REFERENCE MODEL
Write the decode a belief-sharing model would contain, and prove
to yourself that it agrees with D-M1 in every cycle -- so that
a comparison against it reports a clean pass.14. What Carries Forward
THE CORRECTION
o an endpoint is a (NUMBER, DIRECTION) pair, and 0x82 is
"number 2, IN" rather than "endpoint 130"
o 32 names, of which endpoint 0's two are the only ones that
always exist and the only ones declared by the SPECIFICATION
o every other endpoint exists because the ACTIVE CONFIGURATION
declares it -- so the count changes with SET_INTERFACE and not
with the silicon
o toggle, halt, size and type are per-PAIR, not per-number
o three levels the belief collapses: NAME, DECLARATION,
IMPLEMENTATION -- and each can exist without the others
THE HARDWARE
o idx = {tok_is_in, tok_ep} -- the direction is the high bit of
the address, not a qualifier
o exists needs BOTH halves of the name AND the declaration:
num_ok && cfg_en[idx]
o a token to a name with no declaration is an ERROR, not a NAK
THE METHOD
o an intent check can say "these two must DIFFER", which no
comparison against a belief-sharing model can say
o an assertion can be about an ENCODING rather than a behaviour,
when the encoding is the architectural claim
o the check that catches the misconception is the one that asserts
two things DIFFER -- and it is only obvious once the mutation has
been named: write the mutation first, then the check
THE DEBUG CONSEQUENCE
o "endpoint 2 half-works" is a question about a thing that does
not exist. There are two endpoints; ask about each.
o "the host ignores my endpoint" is answered in the descriptors
the host READ, never in the buffer.The next belief is about speed, and it is the one that survives longest because it is usually true — which makes it far more dangerous than a belief that is simply wrong.
Continue learning
Related tutorials
- Related topic
“Enumeration Is Optional”
Nobody says this out loud; they act on it — by testing a data path before the device has an address, and by debugging an endpoint the host never opened a pipe to. A state machine with one gated output is the whole refutation.
- Related topic
Compliance Review Checklist
Four different questions get called compliance, and confusing them is how a device that works on every desk fails certification. Separating functional correctness from specification conformance, worked on an exhaustively verified Chapter 9 request-legality table.
- Related topic
“USB Devices Initiate Transfers”
A mouse appears to send, and the transfer type is literally called interrupt — so the belief has two strong supports. Its prediction is that a device with data can put a transaction on an idle bus, and a sixty-line block makes that impossible.
- Related topic
“Bulk Transfers Are Always Fastest”
Usually true, which is what makes it dangerous. Bulk has the largest packets and no rate limit, and on a quiet bus it wins every benchmark. A frame-budget allocator shows whose property throughput actually is.
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.
