USB · Module 31
“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.
1. The Belief
"Enumeration is host-side housekeeping. The device gets an address and the OS builds its device tree. The real work — my endpoint, my data — is separate from it, and I can test that part first."
Almost nobody states this as a proposition. Almost everybody acts on it, which is worse: an unstated belief cannot be argued with.
2. Why An Intelligent Engineer Believes It
IT IS INVISIBLE
Enumeration happens in milliseconds, before any user-visible
behaviour, and if it works you never see it. The engineer's entire
experience of USB begins AFTER it succeeded.
IT IS SOMEBODY ELSE'S CODE
On the host it is the stack's job; on the device it is often a
vendor's library, a soft-IP block, or ROM firmware. It arrives
working. Nothing about it looks like the thing being designed.
IT LOOKS LIKE ADMINISTRATION
"Assign an address, read descriptors, pick a configuration" reads
like a registration desk -- bookkeeping performed on a device that
already has all its capabilities.
EVERY OTHER BUS THE ENGINEER KNOWS WORKS THAT WAY
I2C address the slave and transact. No handshake first.
SPI assert chip select and clock. Nothing negotiated.
AXI the port exists because it is wired
UART there is not even an address
On all four, a peripheral is USABLE THE MOMENT IT IS CONNECTED.
That is the mental model being imported, and on those buses it is
completely correct.3. The Prediction It Makes
IF ENUMERATION WERE OPTIONAL HOUSEKEEPING, THEN:
1 a device's data endpoints would work as soon as it is attached,
or at least as soon as it has an address
2 a device with correct data-path silicon and wrong descriptors
would still move data, just be described badly
3 a bus reset would not undo anything important -- an address,
once given, would stay given
4 enumeration could be skipped or stubbed for bring-up, and the
endpoint tested directly
5 a device that "does nothing" would have a fault in its functionPrediction 5 is the expensive one, and it is the one an engineer acts on without ever having formed it deliberately.
4. The Counterexample
The counterexample is not exotic. It is the most common bring-up experience in the industry.
A DEVICE WITH A PERFECT DATA PATH AND ONE WRONG DESCRIPTOR BYTE
o the silicon is correct. Every endpoint buffer works, verified in
simulation, verified on the bench against a directed stimulus
o the descriptor declares a maximum packet size, or a total
length, or a configuration count, incorrectly
o the host reads the descriptors, finds them inconsistent, and
does not complete configuration
WHAT THE ENGINEER SEES
the device is "not recognised", or it appears and disappears, or
it enumerates and no pipe ever opens
NO endpoint ever receives a token
the data path -- which is flawless -- is never exercised at all
WHAT THE ENGINEER DOES, under the belief
debugs the data pathAnd the second counterexample, which falsifies prediction 3 and is a genuine hardware trap:
A BUS RESET RETURNS THE DEVICE TO DEFAULT
o address: gone. The device answers at address 0 again.
o configuration: gone. Every non-control endpoint STOPS EXISTING.
o a device that was working is now a thing on a wire.
And a bus reset is not rare. It happens on resume, on error
recovery, when a driver reloads, when the port is re-initialised.
A device that treats its address as permanent works until the first
one.What must happen before a function endpoint can carry one byte
Two features of that diagram are the chapter. The first message shows the device attached, and there are eight messages between it and the first byte of function data. And the third message is the belief: a token to a function endpoint before configuration, drawn in red because it cannot be a transfer.
5. The Corrected Model
FIVE THINGS THAT ARE NOT THE SAME THING
attached there is a device on the wire. Electrically present.
DEFAULT it has been reset and answers at address 0
ADDRESS it has an address of its own
CONFIGURED a configuration is active, so its declared
endpoints EXIST
usable the function can move data
Each line requires the line above it. The belief collapses all five
into the first one.
THE ONE-LINE INVARIANT
a function endpoint is enabled -> the device is CONFIGURED
AND THE EXCEPTION THAT MAKES THE SCHEME WORK
endpoint 0 is enabled from ATTACHMENT, because it is the channel
over which the other four lines are arranged. If it were gated
like everything else, nothing could ever be negotiated.
WHAT A BUS RESET DOES
returns to DEFAULT: address gone, configuration gone, every
non-control endpoint stops existing. It is a PROTOCOL-STATE
event, not a chip reset -- 29.3 separated those, and this is
where the separation earns its keep.6. The Hardware Contract
PURPOSE hold the device's protocol state, and gate the
function's endpoints on it. That gate is the chapter.
INPUTS clk, rst_n
attached electrically present
bus_reset one cycle: the host drove a reset
set_addr_valid, set_addr [6:0]
set_cfg_valid, set_cfg [7:0]
already-decoded standard requests
n_configs [7:0] how many configurations the
descriptors declare
OUTPUTS dev_state [1:0] DEFAULT / ADDRESS / CONFIGURED
dev_addr [6:0]
cfg_value [7:0]
ep0_en endpoint 0 enabled
fn_ep_en function endpoints enabled
five counters
AUTHORITATIVE STATE
state, addr_r, cfg_r
DERIVED THE TWO LINES THAT ARE THE WHOLE CHAPTER:
ep0_en = attached;
fn_ep_en = attached && (state == ST_CONFIGURED);
RESET rst_n is a CHIP reset: state DEFAULT, address 0, no
configuration. bus_reset is a PROTOCOL event with the
same effect on protocol state and no effect on
anything else.
PRIORITY detachment outranks every request. bus_reset outranks
a request arriving in the same cycle: a reset and a
SET_ADDRESS together leave the device in DEFAULT.
LATENCY one cycle per accepted request; the gate is
combinational in the state.
BOUNDARY SET_CONFIGURATION with value 0 is legal and UNCONFIGURES
-- it is not a rejection, and fn_ep_en falls.
A configuration value above n_configs is REJECTED and
changes nothing.
SET_ADDRESS while CONFIGURED is rejected: an address
change is not a mid-configuration operation.
SET_ADDRESS with value 0 returns to DEFAULT -- "answer
as nobody again".
SET_CONFIGURATION from DEFAULT is REJECTED: the address
step is not optional either, and E-M2 is exactly the
removal of that condition.
ASSUMPTIONS requests arrive pre-decoded as pulses. A real device
reaches this point only after a complete control
transfer with a data stage and a status stage.
OMISSIONS the control-transfer machine, SETUP packet decoding,
the descriptor store, the data and status stages,
string descriptors, alternate settings (31.3's
mechanism), suspend and resume, and every electrical
detail of attachment.
MISCONCEPTION DEMONSTRATED
"enumeration is optional -- attached is enough" and
"the address step is a formality"usb_fn_enable.v — the design, Verilog-2005
// =====================================================================
// usb_fn_enable -- the difference between being plugged in and being
// usable, as a state machine.
//
// CLASSIFICATION: simplified synthesisable teaching RTL. It is NOT an
// enumeration engine: there is no control-transfer machine, no
// descriptor store, no SETUP decoding, no data stage. The requests
// arrive here already decoded, as two pulses with a value each, and
// what the module does with them is the whole subject.
//
// THE FIVE THINGS THAT ARE NOT THE SAME THING
// -------------------------------------------
// attached there is a device on the wire and the host can see
// it. Nothing else.
// DEFAULT it has been reset and answers at address zero.
// ADDRESS it has been given an address of its own.
// CONFIGURED a configuration has been selected, so the
// endpoints the descriptors describe now exist.
// usable the function can move data -- which requires the
// line above it, and nothing less.
//
// THE INVARIANT
// fn_ep_en |-> (dev_state == ST_CONFIGURED)
//
// Endpoint zero is different and it is the reason the whole scheme
// works: it is enabled as soon as the device is attached, because it
// is the channel over which everything else is arranged. Every other
// endpoint is gated. Mutation E-M1 replaces the gate with `attached`,
// which is the misconception written as RTL.
// =====================================================================
module usb_fn_enable (
input wire clk,
input wire rst_n,
// The device is electrically present and the host has seen it.
input wire attached,
// One cycle: the host drove a bus reset.
input wire bus_reset,
// Already-decoded standard requests.
input wire set_addr_valid,
input wire [6:0] set_addr,
input wire set_cfg_valid,
input wire [7:0] set_cfg,
// How many configurations the descriptors declare. A request for one
// that does not exist is not a state change.
input wire [7:0] n_configs,
output wire [1:0] dev_state,
output wire [6:0] dev_addr,
output wire [7:0] cfg_value,
// Endpoint zero: available from attachment, because it is how the
// rest of this is negotiated.
output wire ep0_en,
// Every other endpoint: available only when configured.
output wire fn_ep_en,
output wire [15:0] n_resets,
output wire [15:0] n_addressed,
output wire [15:0] n_configured,
output wire [15:0] n_rejected,
// Cycles in which a non-control endpoint was enabled while the device
// was not configured. Structurally impossible here; E-M1 makes it the
// normal case.
output wire [15:0] n_premature
);
localparam [1:0] ST_DEFAULT = 2'd0,
ST_ADDRESS = 2'd1,
ST_CONFIGURED = 2'd2;
reg [1:0] state;
reg [6:0] addr_r;
reg [7:0] cfg_r;
reg [15:0] c_rst, c_addr, c_cfg, c_rej, c_prem;
// SET_ADDRESS is defined in Default and Address. SET_CONFIGURATION is
// defined in Address and Configured. A request outside its states is
// a request error, not a state change -- 30.3 built the table.
wire addr_legal = set_addr_valid && attached &&
((state == ST_DEFAULT) || (state == ST_ADDRESS));
wire cfg_legal = set_cfg_valid && attached &&
((state == ST_ADDRESS) || (state == ST_CONFIGURED)) &&
(set_cfg <= n_configs);
wire req_rejected = (set_addr_valid && !addr_legal) ||
(set_cfg_valid && !cfg_legal);
assign ep0_en = attached;
assign fn_ep_en = attached && (state == ST_CONFIGURED);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= ST_DEFAULT;
addr_r <= 7'd0;
cfg_r <= 8'd0;
c_rst <= 16'd0; c_addr <= 16'd0; c_cfg <= 16'd0;
c_rej <= 16'd0; c_prem <= 16'd0;
end else begin
if (!attached) begin
// Unplugging is not a protocol event the device negotiates. It
// returns to the state it had before anybody knew it existed.
state <= ST_DEFAULT;
addr_r <= 7'd0;
cfg_r <= 8'd0;
end else if (bus_reset) begin
// A bus reset returns the device to Default: address zero,
// unconfigured. Everything the host arranged is undone, and
// the host knows it because the host caused it.
state <= ST_DEFAULT;
addr_r <= 7'd0;
cfg_r <= 8'd0;
c_rst <= c_rst + 16'd1;
end else begin
if (addr_legal) begin
addr_r <= set_addr;
// Address zero means "go back to answering as nobody".
state <= (set_addr == 7'd0) ? ST_DEFAULT : ST_ADDRESS;
if (set_addr != 7'd0) c_addr <= c_addr + 16'd1;
end
if (cfg_legal) begin
cfg_r <= set_cfg;
// Configuration zero means "unconfigure" -- a legal request
// that takes a working device back to having no endpoints.
state <= (set_cfg == 8'd0) ? ST_ADDRESS : ST_CONFIGURED;
if (set_cfg != 8'd0) c_cfg <= c_cfg + 16'd1;
end
if (req_rejected) c_rej <= c_rej + 16'd1;
end
if (fn_ep_en && (state != ST_CONFIGURED)) c_prem <= c_prem + 16'd1;
end
end
assign dev_state = state;
assign dev_addr = addr_r;
assign cfg_value = cfg_r;
assign n_resets = c_rst;
assign n_addressed = c_addr;
assign n_configured = c_cfg;
assign n_rejected = c_rej;
assign n_premature = c_prem;
endmodule7. The Testbench
PHASE 1 THE GATE, stated in its own terms and consulting nothing.
Plug the device in and leave it there. TWO HUNDRED CYCLES of being
electrically present and visible to the host, and in every one of
them require:
fn_ep_en == 0 the function endpoints stay dark
ep0_en == 1 and endpoint zero works the whole time
then, after the 200:
dev_state == DEFAULT it did not drift
dev_addr == 0
n_premature == 0 the impossible counter is still zero
and only then do the work, one step at a time:
SET_ADDRESS -> addressed, and STILL NOT USABLE
SET_CONFIG -> configured: NOW usable
If enumeration were optional, the 200-cycle interval is where the
device would start working. It does not, and the loop counter is
itself checked -- attached_run must reach 200, or a loop that
never ran would have passed by not existing.
PHASE 2 the sweep, over the named axes below
PHASE 3 SCENARIOS -- the full enumeration; enumeration interrupted
at each stage; a reset from each state; unconfigure and
reconfigure; detach mid-enumeration; a reset arriving in
the same cycle as a requestPhase 1's first check is the counterexample from section 4 as a test: 200 cycles attached and unusable. That is the shape of an intent check for a negative architectural claim — it cannot be a comparison, because a model that shared the belief would enable the endpoint too and the comparison would pass.
tb_usb_fn_enable.v — the testbench, Verilog-2005
// =====================================================================
// tb_usb_fn_enable -- Verilog-2005 testbench for usb_fn_enable.
//
// PHASE 1 is the intent phase and it consults no model. It states the
// claim the chapter exists for and requires it every cycle:
//
// a non-control endpoint is NEVER enabled unless the device is
// configured -- no matter how long it has been plugged in
//
// PHASES
// 1 INTENT the gate, asserted directly, across a long attach
// 2 EXHAUSTIVE every (state, request, value class) triple
// 3 SCENARIO the full enumeration walk, and the ways back down
// 4 RANDOM supplementary, audited
// =====================================================================
`timescale 1ns/1ps
module tb_usb_fn_enable;
localparam [1:0] ST_DEFAULT = 2'd0, ST_ADDRESS = 2'd1, ST_CONFIGURED = 2'd2;
reg clk = 1'b0;
reg rst_n, attached, bus_reset;
reg set_addr_valid, set_cfg_valid;
reg [6:0] set_addr;
reg [7:0] set_cfg, n_configs;
wire [1:0] dev_state;
wire [6:0] dev_addr;
wire [7:0] cfg_value;
wire ep0_en, fn_ep_en;
wire [15:0] n_resets, n_addressed, n_configured, n_rejected, n_premature;
usb_fn_enable dut (
.clk(clk), .rst_n(rst_n), .attached(attached), .bus_reset(bus_reset),
.set_addr_valid(set_addr_valid), .set_addr(set_addr),
.set_cfg_valid(set_cfg_valid), .set_cfg(set_cfg),
.n_configs(n_configs),
.dev_state(dev_state), .dev_addr(dev_addr), .cfg_value(cfg_value),
.ep0_en(ep0_en), .fn_ep_en(fn_ep_en),
.n_resets(n_resets), .n_addressed(n_addressed),
.n_configured(n_configured), .n_rejected(n_rejected),
.n_premature(n_premature)
);
always #5 clk = ~clk;
// ---- the independent reference model ---------------------------
// The RULES, in words, then code:
// R1 unplugged is Default, address zero, unconfigured
// R2 a bus reset is the same, and is counted
// R3 SET_ADDRESS is defined in Default and Address only
// R4 SET_CONFIGURATION is defined in Address and Configured only,
// and only for a configuration that exists
// R5 value zero means "go back one step" in both cases
reg [1:0] rm_state;
reg [6:0] rm_addr;
reg [7:0] rm_cfg;
reg [15:0] rm_rst, rm_adr, rm_cfgc, rm_rej;
integer chk_dir, chk_rnd, err, in_random;
integer m_attach_cycles, m_cfg_cycles, m_fn_cycles, m_resets,
m_addressed, m_configured, m_rejected, m_unconfig, m_setupfail;
integer i, j, st, rq, vc;
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
task ref_step;
reg a_ok, c_ok;
begin
if (!rst_n) begin
rm_state = ST_DEFAULT; rm_addr = 0; rm_cfg = 0;
rm_rst = 0; rm_adr = 0; rm_cfgc = 0; rm_rej = 0;
end else if (!attached) begin
rm_state = ST_DEFAULT; rm_addr = 0; rm_cfg = 0; // R1
end else if (bus_reset) begin
rm_state = ST_DEFAULT; rm_addr = 0; rm_cfg = 0; // R2
rm_rst = rm_rst + 1;
m_resets = m_resets + 1;
end else begin
a_ok = set_addr_valid &&
((rm_state == ST_DEFAULT) || (rm_state == ST_ADDRESS)); // R3
c_ok = set_cfg_valid &&
((rm_state == ST_ADDRESS) || (rm_state == ST_CONFIGURED)) &&
(set_cfg <= n_configs); // R4
if (a_ok) begin
rm_addr = set_addr;
rm_state = (set_addr == 0) ? ST_DEFAULT : ST_ADDRESS; // R5
if (set_addr != 0) begin
rm_adr = rm_adr + 1; m_addressed = m_addressed + 1;
end
end
if (c_ok) begin
rm_cfg = set_cfg;
rm_state = (set_cfg == 0) ? ST_ADDRESS : ST_CONFIGURED; // R5
if (set_cfg != 0) begin
rm_cfgc = rm_cfgc + 1; m_configured = m_configured + 1;
end else m_unconfig = m_unconfig + 1;
end
if ((set_addr_valid && !a_ok) || (set_cfg_valid && !c_ok)) begin
rm_rej = rm_rej + 1; m_rejected = m_rejected + 1;
end
end
if (attached) m_attach_cycles = m_attach_cycles + 1;
if (rm_state == ST_CONFIGURED) m_cfg_cycles = m_cfg_cycles + 1;
end
endtask
task cmp; begin
ck("dev_state", {30'd0, dev_state}, {30'd0, rm_state});
ck("dev_addr", {25'd0, dev_addr}, {25'd0, rm_addr});
ck("cfg_value", {24'd0, cfg_value}, {24'd0, rm_cfg});
ck("ep0_en", {31'd0, ep0_en}, {31'd0, attached});
ck("fn_ep_en", {31'd0, fn_ep_en},
{31'd0, (attached && (rm_state == ST_CONFIGURED))});
ck("n_resets", {16'd0, n_resets}, {16'd0, rm_rst});
ck("n_addressed", {16'd0, n_addressed}, {16'd0, rm_adr});
ck("n_configured", {16'd0, n_configured},{16'd0, rm_cfgc});
ck("n_rejected", {16'd0, n_rejected}, {16'd0, rm_rej});
ck("n_premature", {16'd0, n_premature}, 32'd0);
if (fn_ep_en) m_fn_cycles = m_fn_cycles + 1;
end endtask
// The claim, asserted directly, every cycle of every phase.
task intent_check; begin
bump;
if (fn_ep_en && (dev_state != ST_CONFIGURED)) begin
err = err + 1;
$display(" ** INTENT VIOLATED: function endpoints enabled in state %0d (t=%0t)",
dev_state, $time);
end
bump;
if (fn_ep_en && !attached) begin
err = err + 1;
$display(" ** INTENT VIOLATED: function endpoints enabled while detached (t=%0t)",
$time);
end
end endtask
task step; begin
#1;
@(posedge clk);
ref_step;
#1;
cmp;
intent_check;
bus_reset = 0; set_addr_valid = 0; set_cfg_valid = 0;
set_addr = 0; set_cfg = 0;
end endtask
task idle; begin step; end endtask
task hard_reset; begin
rst_n = 0; attached = 0; bus_reset = 0;
set_addr_valid = 0; set_addr = 0; set_cfg_valid = 0; set_cfg = 0;
n_configs = 8'd2;
repeat (3) begin @(posedge clk); ref_step; end
#1; rst_n = 1;
@(posedge clk); ref_step; #1; cmp;
end endtask
task plug; begin attached = 1; step; end endtask
task unplug; begin attached = 0; step; end endtask
task do_reset; begin bus_reset = 1; step; end endtask
task addr_req; input [6:0] a;
begin set_addr_valid = 1; set_addr = a; step; end
endtask
task cfg_req; input [7:0] c;
begin set_cfg_valid = 1; set_cfg = c; step; end
endtask
// -----------------------------------------------------------------
// PHASE 1 -- THE GATE.
//
// Plug the device in and leave it there. Two hundred cycles of being
// electrically present, visible to the host, with endpoint zero
// working the whole time -- and the function endpoints stay dark.
// If enumeration were optional, this is the interval in which the
// device would start working. It does not.
// -----------------------------------------------------------------
integer attached_run;
task phase_intent;
begin
hard_reset;
plug;
ck("attached", {31'd0, attached}, 32'd1);
ck("endpoint zero is up", {31'd0, ep0_en}, 32'd1);
attached_run = 0;
for (i = 0; i < 200; i = i + 1) begin
idle;
ck("still nothing usable", {31'd0, fn_ep_en}, 32'd0);
ck("but EP0 still is", {31'd0, ep0_en}, 32'd1);
attached_run = attached_run + 1;
end
ck("still in Default", {30'd0, dev_state}, {30'd0, ST_DEFAULT});
ck("still address zero", {25'd0, dev_addr}, 32'd0);
ck("nothing premature", {16'd0, n_premature}, 32'd0);
// now do the work, and only then does the function exist
addr_req(7'd5);
ck("addressed, still not usable", {31'd0, fn_ep_en}, 32'd0);
cfg_req(8'd1);
ck("configured: now usable", {31'd0, fn_ep_en}, 32'd1);
bump;
if (attached_run < 200) begin
err = err + 1;
$display(" ** intent: only %0d attached cycles", attached_run);
end
end
endtask
// -----------------------------------------------------------------
// PHASE 2 -- the exhaustive sweep.
//
// AXES:
// device state DEFAULT, ADDRESS, CONFIGURED 3
// request none, SET_ADDRESS, SET_CONFIGURATION,
// bus reset 4
// value class zero, valid non-zero, out of range 3
// ---------------------------------------------------------------
// 3 x 4 x 3 = 36
//
// WHAT IS ABSENT: detachment, which is not a request and cannot be
// a value class. It is phase 3. An exhaustive sweep is exhaustive
// over the axes it has.
// -----------------------------------------------------------------
task setup_state;
input [1:0] want;
begin
hard_reset;
plug;
if (want >= ST_ADDRESS) addr_req(7'd9);
if (want == ST_CONFIGURED) cfg_req(8'd1);
bump;
if (dev_state !== want) begin
err = err + 1; m_setupfail = m_setupfail + 1;
$display(" ** setup: wanted state %0d, reached %0d", want, dev_state);
end
end
endtask
task phase_sweep;
begin
for (st = 0; st < 3; st = st + 1)
for (rq = 0; rq < 4; rq = rq + 1)
for (vc = 0; vc < 3; vc = vc + 1) begin
setup_state(st[1:0]);
case (rq)
0: idle;
1: addr_req((vc == 0) ? 7'd0 : (vc == 1) ? 7'd17 : 7'd126);
2: cfg_req ((vc == 0) ? 8'd0 : (vc == 1) ? 8'd1 : 8'd9);
3: do_reset;
endcase
idle;
end
end
endtask
// -----------------------------------------------------------------
// PHASE 3 -- the walk, and every way back down.
// -----------------------------------------------------------------
task phase_scenarios;
begin
// S1 the whole sequence, one step at a time, checking that the
// function is unusable at every step but the last.
hard_reset;
ck("S1 detached: EP0 down", {31'd0, ep0_en}, 32'd0);
plug;
ck("S1 attached: EP0 up", {31'd0, ep0_en}, 32'd1);
ck("S1 attached: fn down", {31'd0, fn_ep_en}, 32'd0);
do_reset;
ck("S1 reset: Default", {30'd0, dev_state},{30'd0, ST_DEFAULT});
ck("S1 reset: fn down", {31'd0, fn_ep_en}, 32'd0);
addr_req(7'd12);
ck("S1 addressed", {30'd0, dev_state},{30'd0, ST_ADDRESS});
ck("S1 addressed: fn down", {31'd0, fn_ep_en}, 32'd0);
ck("S1 the address stuck", {25'd0, dev_addr}, 32'd12);
cfg_req(8'd1);
ck("S1 configured", {30'd0, dev_state},{30'd0, ST_CONFIGURED});
ck("S1 NOW it is usable", {31'd0, fn_ep_en}, 32'd1);
// S2 a bus reset undoes all of it, from Configured, in one cycle.
do_reset;
ck("S2 back to Default", {30'd0, dev_state},{30'd0, ST_DEFAULT});
ck("S2 address gone", {25'd0, dev_addr}, 32'd0);
ck("S2 function gone", {31'd0, fn_ep_en}, 32'd0);
// S3 SET_CONFIGURATION(0) is a legal request that unconfigures.
hard_reset; plug; addr_req(7'd3); cfg_req(8'd1);
ck("S3 usable", {31'd0, fn_ep_en}, 32'd1);
cfg_req(8'd0);
ck("S3 unconfigured", {30'd0, dev_state},{30'd0, ST_ADDRESS});
ck("S3 not usable", {31'd0, fn_ep_en}, 32'd0);
ck("S3 but still addressed", {25'd0, dev_addr}, 32'd3);
// S4 SET_ADDRESS(0) returns to Default from Address.
hard_reset; plug; addr_req(7'd3);
addr_req(7'd0);
ck("S4 back to Default", {30'd0, dev_state},{30'd0, ST_DEFAULT});
// S5 SET_CONFIGURATION before an address is a request error, and
// the device does NOT become usable by asking nicely.
hard_reset; plug;
cfg_req(8'd1);
ck("S5 rejected", {16'd0, n_rejected}, 32'd1);
ck("S5 still Default", {30'd0, dev_state}, {30'd0, ST_DEFAULT});
ck("S5 still not usable", {31'd0, fn_ep_en}, 32'd0);
// S6 a configuration that does not exist is refused.
hard_reset; plug; addr_req(7'd4);
cfg_req(8'd7);
ck("S6 refused", {16'd0, n_rejected}, 32'd1);
ck("S6 still only addressed",{30'd0, dev_state}, {30'd0, ST_ADDRESS});
// S7 unplugging a configured device takes everything with it.
hard_reset; plug; addr_req(7'd6); cfg_req(8'd2);
ck("S7 usable", {31'd0, fn_ep_en}, 32'd1);
unplug;
ck("S7 EP0 down", {31'd0, ep0_en}, 32'd0);
ck("S7 function down", {31'd0, fn_ep_en}, 32'd0);
ck("S7 Default again", {30'd0, dev_state}, {30'd0, ST_DEFAULT});
plug;
ck("S7 replug is not resume",{31'd0, fn_ep_en}, 32'd0);
end
endtask
// -----------------------------------------------------------------
// PHASE 4 -- random, audited.
// -----------------------------------------------------------------
task phase_random;
integer r;
begin
in_random = 1;
hard_reset; plug;
for (j = 0; j < 4000; j = j + 1) begin
r = {$random} % 100;
if (r < 22) addr_req({$random} % 128);
else if (r < 44) cfg_req({$random} % 4);
else if (r < 54) do_reset;
else if (r < 60) begin unplug; plug; end
else if (r < 64) begin
n_configs = {$random} % 4;
idle;
end else idle;
end
in_random = 0;
end
endtask
initial begin
chk_dir = 0; chk_rnd = 0; err = 0; in_random = 0;
m_attach_cycles=0; m_cfg_cycles=0; m_fn_cycles=0; m_resets=0;
m_addressed=0; m_configured=0; m_rejected=0; m_unconfig=0;
m_setupfail=0;
phase_intent;
$display(" phase 1 intent : %0d checks, %0d errors (%0d attached cycles, unusable)",
chk_dir, err, attached_run);
phase_sweep;
$display(" phase 2 exhaustive : %0d checks, %0d errors (36 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(" cycles attached ........ %0d", m_attach_cycles);
$display(" cycles configured ...... %0d", m_cfg_cycles);
$display(" cycles function enabled %0d", m_fn_cycles);
$display(" bus resets ............. %0d", m_resets);
$display(" addresses assigned ..... %0d", m_addressed);
$display(" configurations selected %0d", m_configured);
$display(" unconfigure requests ... %0d", m_unconfig);
$display(" requests rejected ...... %0d", m_rejected);
$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 2,854 2,854 2,854
phase 2 exhaustive 4,978 4,978 4,978
phase 3 scenarios 5,330 5,330 5,330
---- DIRECTED 5,330 5,330 5,330
errors 0 0 0
TOTAL 56,400 56,400 56,532
measured reachability, Verilog run
cycles attached ..................... 4,369
cycles configured ..................... 533
cycles with the function enabled ...... 533
bus resets ............................ 431
addresses assigned .................... 776
configurations selected ............... 195
unconfigure requests .................. 115
requests rejected ..................... 660
setup failures .......................... 0Three rows carry the argument.
4,369 attached vs 533 configured
The device is attached for eight times as many cycles as it is
usable. The belief's model of USB has no vocabulary for the other
3,836 cycles.
533 configured == 533 function-enabled
The two counters are EQUAL, not approximately but exactly -- and
they are equal in the VHDL run too, at its own value of 771,
where the independent stimulus reached the configured state more
often. The VALUE varies with the stimulus; the EQUALITY does not.
That equality is the invariant, measured rather than asserted.
431 bus resets
Each one took a device from wherever it was back to DEFAULT.
Prediction 3 said this would not matter.The exhaustive sweep, and its named axes
AXES
device state DEFAULT, ADDRESS, CONFIGURED 3
request none, SET_ADDRESS, SET_CONFIGURATION,
bus reset 4
value class zero, valid non-zero, out of range 3
------------------------------------------------------------
3 x 4 x 3 = 36All 36 reached, and each one was reached by driving the device there through the
protocol — setup_state performs an attach, then a SET_ADDRESS, then a
SET_CONFIGURATION, and checks that it arrived before the sweep body runs.
8. SystemVerilog
usb_fn_enable_sv.sv — the design, SystemVerilog
// =====================================================================
// usb_fn_enable_sv -- the same contract in SystemVerilog. Same ports,
// same states, same gate, same reset, same latency.
//
// The device state is an ENUM here, so the three states are three
// values and there is no fourth to fall into -- and the SVA block
// states the gate itself as a property that E-M1 breaks.
//
// ------------------------------------------------------------------
// The difference between being plugged in and being
// usable, as a state machine.
//
// CLASSIFICATION: simplified synthesisable teaching RTL. It is NOT an
// enumeration engine: there is no control-transfer machine, no
// descriptor store, no SETUP decoding, no data stage. The requests
// arrive here already decoded, as two pulses with a value each, and
// what the module does with them is the whole subject.
//
// THE FIVE THINGS THAT ARE NOT THE SAME THING
// -------------------------------------------
// attached there is a device on the wire and the host can see
// it. Nothing else.
// DEFAULT it has been reset and answers at address zero.
// ADDRESS it has been given an address of its own.
// CONFIGURED a configuration has been selected, so the
// endpoints the descriptors describe now exist.
// usable the function can move data -- which requires the
// line above it, and nothing less.
//
// THE INVARIANT
// fn_ep_en |-> (dev_state == ST_CONFIGURED)
//
// Endpoint zero is different and it is the reason the whole scheme
// works: it is enabled as soon as the device is attached, because it
// is the channel over which everything else is arranged. Every other
// endpoint is gated. Mutation E-M1 replaces the gate with `attached`,
// which is the misconception written as RTL.
// =====================================================================
module usb_fn_enable_sv (
input logic clk,
input logic rst_n,
// The device is electrically present and the host has seen it.
input logic attached,
// One cycle: the host drove a bus reset.
input logic bus_reset,
// Already-decoded standard requests.
input logic set_addr_valid,
input logic [6:0] set_addr,
input logic set_cfg_valid,
input logic [7:0] set_cfg,
// How many configurations the descriptors declare. A request for one
// that does not exist is not a state change.
input logic [7:0] n_configs,
output logic [1:0] dev_state,
output logic [6:0] dev_addr,
output logic [7:0] cfg_value,
// Endpoint zero: available from attachment, because it is how the
// rest of this is negotiated.
output logic ep0_en,
// Every other endpoint: available only when configured.
output logic fn_ep_en,
output logic [15:0] n_resets,
output logic [15:0] n_addressed,
output logic [15:0] n_configured,
output logic [15:0] n_rejected,
// Cycles in which a non-control endpoint was enabled while the device
// was not configured. Structurally impossible here; E-M1 makes it the
// normal case.
output logic [15:0] n_premature
);
// The three device states as a type. There is no fourth value to
// fall into, and a transition to one that does not exist is a
// compile error rather than a default branch.
typedef enum logic [1:0] {
ST_DEFAULT = 2'd0,
ST_ADDRESS = 2'd1,
ST_CONFIGURED = 2'd2
} dev_state_e;
dev_state_e state;
logic [6:0] addr_r;
logic [7:0] cfg_r;
logic [15:0] c_rst, c_addr, c_cfg, c_rej, c_prem;
// SET_ADDRESS is defined in Default and Address. SET_CONFIGURATION is
// defined in Address and Configured. A request outside its states is
// a request error, not a state change -- 30.3 built the table.
logic addr_legal;
assign addr_legal = set_addr_valid && attached &&
((state == ST_DEFAULT) || (state == ST_ADDRESS));
logic cfg_legal;
assign cfg_legal = set_cfg_valid && attached &&
((state == ST_ADDRESS) || (state == ST_CONFIGURED)) &&
(set_cfg <= n_configs);
logic req_rejected;
assign req_rejected = (set_addr_valid && !addr_legal) ||
(set_cfg_valid && !cfg_legal);
assign ep0_en = attached;
assign fn_ep_en = attached && (state == ST_CONFIGURED);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= ST_DEFAULT;
addr_r <= 7'd0;
cfg_r <= 8'd0;
c_rst <= 16'd0; c_addr <= 16'd0; c_cfg <= 16'd0;
c_rej <= 16'd0; c_prem <= 16'd0;
end else begin
if (!attached) begin
// Unplugging is not a protocol event the device negotiates. It
// returns to the state it had before anybody knew it existed.
state <= ST_DEFAULT;
addr_r <= 7'd0;
cfg_r <= 8'd0;
end else if (bus_reset) begin
// A bus reset returns the device to Default: address zero,
// unconfigured. Everything the host arranged is undone, and
// the host knows it because the host caused it.
state <= ST_DEFAULT;
addr_r <= 7'd0;
cfg_r <= 8'd0;
c_rst <= c_rst + 16'd1;
end else begin
if (addr_legal) begin
addr_r <= set_addr;
// Address zero means "go back to answering as nobody".
state <= dev_state_e'((set_addr == 7'd0) ? ST_DEFAULT : ST_ADDRESS);
if (set_addr != 7'd0) c_addr <= c_addr + 16'd1;
end
if (cfg_legal) begin
cfg_r <= set_cfg;
// Configuration zero means "unconfigure" -- a legal request
// that takes a working device back to having no endpoints.
state <= dev_state_e'((set_cfg == 8'd0) ? ST_ADDRESS : ST_CONFIGURED);
if (set_cfg != 8'd0) c_cfg <= c_cfg + 16'd1;
end
if (req_rejected) c_rej <= c_rej + 16'd1;
end
if (fn_ep_en && (state != ST_CONFIGURED)) c_prem <= c_prem + 16'd1;
end
end
assign dev_state = state;
assign dev_addr = addr_r;
assign cfg_value = cfg_r;
assign n_resets = c_rst;
assign n_addressed = c_addr;
assign n_configured = c_cfg;
assign n_rejected = c_rej;
assign n_premature = c_prem;
`ifdef SVA_ON
// The gate as a property. Icarus rejects SVA; under Icarus the intent
// checks in the testbench enforce each of these.
// THE ONE. E-M1 replaces the gate with `attached` and this fails.
property p_function_requires_configured;
@(posedge clk) disable iff (!rst_n)
fn_ep_en |-> (state == ST_CONFIGURED);
endproperty
a_function_requires_configured: assert property (p_function_requires_configured);
// SAFETY. Endpoint zero is the exception, and its rule is simpler:
// it exists whenever the device does.
property p_ep0_follows_attachment;
@(posedge clk) disable iff (!rst_n) ep0_en == attached;
endproperty
a_ep0_follows_attachment: assert property (p_ep0_follows_attachment);
// SAFETY. A bus reset returns the device to Default, every time,
// from every state.
property p_reset_returns_to_default;
@(posedge clk) disable iff (!rst_n)
(attached && bus_reset) |=> (state == ST_DEFAULT) && (addr_r == '0);
endproperty
a_reset_returns_to_default: assert property (p_reset_returns_to_default);
// SAFETY. Configuration cannot be reached from Default: the address
// step is not optional either.
property p_no_configure_from_default;
@(posedge clk) disable iff (!rst_n)
(state == ST_DEFAULT) |=> (state != ST_CONFIGURED);
endproperty
a_no_configure_from_default: assert property (p_no_configure_from_default);
// PROGRESS. The sequence CAN be completed -- the gate is a gate, not
// a wall. Without this, a design that never configured would satisfy
// every safety property above.
property p_configuration_is_reachable;
@(posedge clk) disable iff (!rst_n)
(attached && (state == ST_ADDRESS) && cfg_legal && (set_cfg != '0))
|=> (state == ST_CONFIGURED) && fn_ep_en;
endproperty
a_configuration_is_reachable: assert property (p_configuration_is_reachable);
c_default: cover property (@(posedge clk) attached && state == ST_DEFAULT);
c_address: cover property (@(posedge clk) state == ST_ADDRESS);
c_configured: cover property (@(posedge clk) state == ST_CONFIGURED);
c_unconfig: cover property (@(posedge clk) cfg_legal && set_cfg == '0);
c_reject: cover property (@(posedge clk) req_rejected);
c_unplug_cfg: cover property (@(posedge clk) !attached && $past(fn_ep_en));
`endif
endmoduletb_usb_fn_enable_sv.sv — the testbench, SystemVerilog
// =====================================================================
// tb_usb_fn_enable -- Verilog-2005 testbench for usb_fn_enable.
//
// PHASE 1 is the intent phase and it consults no model. It states the
// claim the chapter exists for and requires it every cycle:
//
// a non-control endpoint is NEVER enabled unless the device is
// configured -- no matter how long it has been plugged in
//
// PHASES
// 1 INTENT the gate, asserted directly, across a long attach
// 2 EXHAUSTIVE every (state, request, value class) triple
// 3 SCENARIO the full enumeration walk, and the ways back down
// 4 RANDOM supplementary, audited
// =====================================================================
`timescale 1ns/1ps
module tb_usb_fn_enable_sv;
localparam [1:0] ST_DEFAULT = 2'd0, ST_ADDRESS = 2'd1, ST_CONFIGURED = 2'd2;
logic clk = 1'b0;
logic rst_n, attached, bus_reset;
logic set_addr_valid, set_cfg_valid;
logic [6:0] set_addr;
logic [7:0] set_cfg, n_configs;
wire [1:0] dev_state;
wire [6:0] dev_addr;
wire [7:0] cfg_value;
wire ep0_en, fn_ep_en;
wire [15:0] n_resets, n_addressed, n_configured, n_rejected, n_premature;
usb_fn_enable_sv dut (
.clk(clk), .rst_n(rst_n), .attached(attached), .bus_reset(bus_reset),
.set_addr_valid(set_addr_valid), .set_addr(set_addr),
.set_cfg_valid(set_cfg_valid), .set_cfg(set_cfg),
.n_configs(n_configs),
.dev_state(dev_state), .dev_addr(dev_addr), .cfg_value(cfg_value),
.ep0_en(ep0_en), .fn_ep_en(fn_ep_en),
.n_resets(n_resets), .n_addressed(n_addressed),
.n_configured(n_configured), .n_rejected(n_rejected),
.n_premature(n_premature)
);
always #5 clk = ~clk;
// ---- the independent reference model ---------------------------
// The RULES, in words, then code:
// R1 unplugged is Default, address zero, unconfigured
// R2 a bus reset is the same, and is counted
// R3 SET_ADDRESS is defined in Default and Address only
// R4 SET_CONFIGURATION is defined in Address and Configured only,
// and only for a configuration that exists
// R5 value zero means "go back one step" in both cases
logic [1:0] rm_state;
logic [6:0] rm_addr;
logic [7:0] rm_cfg;
logic [15:0] rm_rst, rm_adr, rm_cfgc, rm_rej;
int chk_dir, chk_rnd, err;
bit in_random;
int m_attach_cycles, m_cfg_cycles, m_fn_cycles, m_resets,
m_addressed, m_configured, m_rejected, m_unconfig, m_setupfail;
int i, j, st, rq, vc;
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
task ref_step;
logic a_ok, c_ok;
begin
if (!rst_n) begin
rm_state = ST_DEFAULT; rm_addr = 0; rm_cfg = 0;
rm_rst = 0; rm_adr = 0; rm_cfgc = 0; rm_rej = 0;
end else if (!attached) begin
rm_state = ST_DEFAULT; rm_addr = 0; rm_cfg = 0; // R1
end else if (bus_reset) begin
rm_state = ST_DEFAULT; rm_addr = 0; rm_cfg = 0; // R2
rm_rst = rm_rst + 1;
m_resets = m_resets + 1;
end else begin
a_ok = set_addr_valid &&
((rm_state == ST_DEFAULT) || (rm_state == ST_ADDRESS)); // R3
c_ok = set_cfg_valid &&
((rm_state == ST_ADDRESS) || (rm_state == ST_CONFIGURED)) &&
(set_cfg <= n_configs); // R4
if (a_ok) begin
rm_addr = set_addr;
rm_state = (set_addr == 0) ? ST_DEFAULT : ST_ADDRESS; // R5
if (set_addr != 0) begin
rm_adr = rm_adr + 1; m_addressed = m_addressed + 1;
end
end
if (c_ok) begin
rm_cfg = set_cfg;
rm_state = (set_cfg == 0) ? ST_ADDRESS : ST_CONFIGURED; // R5
if (set_cfg != 0) begin
rm_cfgc = rm_cfgc + 1; m_configured = m_configured + 1;
end else m_unconfig = m_unconfig + 1;
end
if ((set_addr_valid && !a_ok) || (set_cfg_valid && !c_ok)) begin
rm_rej = rm_rej + 1; m_rejected = m_rejected + 1;
end
end
if (attached) m_attach_cycles = m_attach_cycles + 1;
if (rm_state == ST_CONFIGURED) m_cfg_cycles = m_cfg_cycles + 1;
end
endtask
task cmp; begin
ck("dev_state", {30'd0, dev_state}, {30'd0, rm_state});
ck("dev_addr", {25'd0, dev_addr}, {25'd0, rm_addr});
ck("cfg_value", {24'd0, cfg_value}, {24'd0, rm_cfg});
ck("ep0_en", {31'd0, ep0_en}, {31'd0, attached});
ck("fn_ep_en", {31'd0, fn_ep_en},
{31'd0, (attached && (rm_state == ST_CONFIGURED))});
ck("n_resets", {16'd0, n_resets}, {16'd0, rm_rst});
ck("n_addressed", {16'd0, n_addressed}, {16'd0, rm_adr});
ck("n_configured", {16'd0, n_configured},{16'd0, rm_cfgc});
ck("n_rejected", {16'd0, n_rejected}, {16'd0, rm_rej});
ck("n_premature", {16'd0, n_premature}, 32'd0);
if (fn_ep_en) m_fn_cycles = m_fn_cycles + 1;
end endtask
// The claim, asserted directly, every cycle of every phase.
task intent_check; begin
bump;
if (fn_ep_en && (dev_state != ST_CONFIGURED)) begin
err = err + 1;
$display(" ** INTENT VIOLATED: function endpoints enabled in state %0d (t=%0t)",
dev_state, $time);
end
bump;
if (fn_ep_en && !attached) begin
err = err + 1;
$display(" ** INTENT VIOLATED: function endpoints enabled while detached (t=%0t)",
$time);
end
end endtask
task step; begin
#1;
@(posedge clk);
ref_step;
#1;
cmp;
intent_check;
bus_reset = 0; set_addr_valid = 0; set_cfg_valid = 0;
set_addr = 0; set_cfg = 0;
end endtask
task idle; step; endtask
task hard_reset; begin
rst_n = 0; attached = 0; bus_reset = 0;
set_addr_valid = 0; set_addr = 0; set_cfg_valid = 0; set_cfg = 0;
n_configs = 8'd2;
repeat (3) begin @(posedge clk); ref_step; end
#1; rst_n = 1;
@(posedge clk); ref_step; #1; cmp;
end endtask
task plug; attached = 1; step; endtask
task unplug; attached = 0; step; endtask
task do_reset; bus_reset = 1; step; endtask
task addr_req(logic [6:0] a);
set_addr_valid = 1; set_addr = a; step;
endtask
task cfg_req(logic [7:0] c);
set_cfg_valid = 1; set_cfg = c; step;
endtask
// -----------------------------------------------------------------
// PHASE 1 -- THE GATE.
//
// Plug the device in and leave it there. Two hundred cycles of being
// electrically present, visible to the host, with endpoint zero
// working the whole time -- and the function endpoints stay dark.
// If enumeration were optional, this is the interval in which the
// device would start working. It does not.
// -----------------------------------------------------------------
int attached_run;
task phase_intent;
begin
hard_reset;
plug;
ck("attached", {31'd0, attached}, 32'd1);
ck("endpoint zero is up", {31'd0, ep0_en}, 32'd1);
attached_run = 0;
for (i = 0; i < 200; i = i + 1) begin
idle;
ck("still nothing usable", {31'd0, fn_ep_en}, 32'd0);
ck("but EP0 still is", {31'd0, ep0_en}, 32'd1);
attached_run = attached_run + 1;
end
ck("still in Default", {30'd0, dev_state}, {30'd0, ST_DEFAULT});
ck("still address zero", {25'd0, dev_addr}, 32'd0);
ck("nothing premature", {16'd0, n_premature}, 32'd0);
// now do the work, and only then does the function exist
addr_req(7'd5);
ck("addressed, still not usable", {31'd0, fn_ep_en}, 32'd0);
cfg_req(8'd1);
ck("configured: now usable", {31'd0, fn_ep_en}, 32'd1);
bump;
if (attached_run < 200) begin
err = err + 1;
$display(" ** intent: only %0d attached cycles", attached_run);
end
end
endtask
// -----------------------------------------------------------------
// PHASE 2 -- the exhaustive sweep.
//
// AXES:
// device state DEFAULT, ADDRESS, CONFIGURED 3
// request none, SET_ADDRESS, SET_CONFIGURATION,
// bus reset 4
// value class zero, valid non-zero, out of range 3
// ---------------------------------------------------------------
// 3 x 4 x 3 = 36
//
// WHAT IS ABSENT: detachment, which is not a request and cannot be
// a value class. It is phase 3. An exhaustive sweep is exhaustive
// over the axes it has.
// -----------------------------------------------------------------
task setup_state(logic [1:0] want);
begin
hard_reset;
plug;
if (want >= ST_ADDRESS) addr_req(7'd9);
if (want == ST_CONFIGURED) cfg_req(8'd1);
bump;
if (dev_state !== want) begin
err = err + 1; m_setupfail = m_setupfail + 1;
$display(" ** setup: wanted state %0d, reached %0d", want, dev_state);
end
end
endtask
task phase_sweep;
begin
for (st = 0; st < 3; st = st + 1)
for (rq = 0; rq < 4; rq = rq + 1)
for (vc = 0; vc < 3; vc = vc + 1) begin
setup_state(2'(st));
case (rq)
0: idle;
1: addr_req((vc == 0) ? 7'd0 : (vc == 1) ? 7'd17 : 7'd126);
2: cfg_req ((vc == 0) ? 8'd0 : (vc == 1) ? 8'd1 : 8'd9);
3: do_reset;
endcase
idle;
end
end
endtask
// -----------------------------------------------------------------
// PHASE 3 -- the walk, and every way back down.
// -----------------------------------------------------------------
task phase_scenarios;
begin
// S1 the whole sequence, one step at a time, checking that the
// function is unusable at every step but the last.
hard_reset;
ck("S1 detached: EP0 down", {31'd0, ep0_en}, 32'd0);
plug;
ck("S1 attached: EP0 up", {31'd0, ep0_en}, 32'd1);
ck("S1 attached: fn down", {31'd0, fn_ep_en}, 32'd0);
do_reset;
ck("S1 reset: Default", {30'd0, dev_state},{30'd0, ST_DEFAULT});
ck("S1 reset: fn down", {31'd0, fn_ep_en}, 32'd0);
addr_req(7'd12);
ck("S1 addressed", {30'd0, dev_state},{30'd0, ST_ADDRESS});
ck("S1 addressed: fn down", {31'd0, fn_ep_en}, 32'd0);
ck("S1 the address stuck", {25'd0, dev_addr}, 32'd12);
cfg_req(8'd1);
ck("S1 configured", {30'd0, dev_state},{30'd0, ST_CONFIGURED});
ck("S1 NOW it is usable", {31'd0, fn_ep_en}, 32'd1);
// S2 a bus reset undoes all of it, from Configured, in one cycle.
do_reset;
ck("S2 back to Default", {30'd0, dev_state},{30'd0, ST_DEFAULT});
ck("S2 address gone", {25'd0, dev_addr}, 32'd0);
ck("S2 function gone", {31'd0, fn_ep_en}, 32'd0);
// S3 SET_CONFIGURATION(0) is a legal request that unconfigures.
hard_reset; plug; addr_req(7'd3); cfg_req(8'd1);
ck("S3 usable", {31'd0, fn_ep_en}, 32'd1);
cfg_req(8'd0);
ck("S3 unconfigured", {30'd0, dev_state},{30'd0, ST_ADDRESS});
ck("S3 not usable", {31'd0, fn_ep_en}, 32'd0);
ck("S3 but still addressed", {25'd0, dev_addr}, 32'd3);
// S4 SET_ADDRESS(0) returns to Default from Address.
hard_reset; plug; addr_req(7'd3);
addr_req(7'd0);
ck("S4 back to Default", {30'd0, dev_state},{30'd0, ST_DEFAULT});
// S5 SET_CONFIGURATION before an address is a request error, and
// the device does NOT become usable by asking nicely.
hard_reset; plug;
cfg_req(8'd1);
ck("S5 rejected", {16'd0, n_rejected}, 32'd1);
ck("S5 still Default", {30'd0, dev_state}, {30'd0, ST_DEFAULT});
ck("S5 still not usable", {31'd0, fn_ep_en}, 32'd0);
// S6 a configuration that does not exist is refused.
hard_reset; plug; addr_req(7'd4);
cfg_req(8'd7);
ck("S6 refused", {16'd0, n_rejected}, 32'd1);
ck("S6 still only addressed",{30'd0, dev_state}, {30'd0, ST_ADDRESS});
// S7 unplugging a configured device takes everything with it.
hard_reset; plug; addr_req(7'd6); cfg_req(8'd2);
ck("S7 usable", {31'd0, fn_ep_en}, 32'd1);
unplug;
ck("S7 EP0 down", {31'd0, ep0_en}, 32'd0);
ck("S7 function down", {31'd0, fn_ep_en}, 32'd0);
ck("S7 Default again", {30'd0, dev_state}, {30'd0, ST_DEFAULT});
plug;
ck("S7 replug is not resume",{31'd0, fn_ep_en}, 32'd0);
end
endtask
// -----------------------------------------------------------------
// PHASE 4 -- random, audited.
// -----------------------------------------------------------------
task phase_random;
int r;
begin
in_random = 1;
hard_reset; plug;
for (j = 0; j < 4000; j = j + 1) begin
r = $urandom_range(99);
if (r < 22) addr_req(7'($urandom_range(127)));
else if (r < 44) cfg_req(8'($urandom_range(3)));
else if (r < 54) do_reset;
else if (r < 60) begin unplug; plug; end
else if (r < 64) begin
n_configs = 8'($urandom_range(3));
idle;
end else idle;
end
in_random = 0;
end
endtask
initial begin
chk_dir = 0; chk_rnd = 0; err = 0; in_random = 0;
m_attach_cycles=0; m_cfg_cycles=0; m_fn_cycles=0; m_resets=0;
m_addressed=0; m_configured=0; m_rejected=0; m_unconfig=0;
m_setupfail=0;
phase_intent;
$display(" phase 1 intent : %0d checks, %0d errors (%0d attached cycles, unusable)",
chk_dir, err, attached_run);
phase_sweep;
$display(" phase 2 exhaustive : %0d checks, %0d errors (36 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(" cycles attached ........ %0d", m_attach_cycles);
$display(" cycles configured ...... %0d", m_cfg_cycles);
$display(" cycles function enabled %0d", m_fn_cycles);
$display(" bus resets ............. %0d", m_resets);
$display(" addresses assigned ..... %0d", m_addressed);
$display(" configurations selected %0d", m_configured);
$display(" unconfigure requests ... %0d", m_unconfig);
$display(" requests rejected ...... %0d", m_rejected);
$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 invariant is one property, and it is the shortest statement of this chapter anywhere in the module:
property p_function_requires_configured;
@(posedge clk) disable iff (!rst_n)
fn_ep_en |-> (state == ST_CONFIGURED);
endpropertyAnd three more, of which the last is the one people forget to write:
property p_ep0_follows_attachment;
@(posedge clk) disable iff (!rst_n) ep0_en == attached;
endproperty
property p_reset_returns_to_default;
@(posedge clk) disable iff (!rst_n)
(attached && bus_reset) |=> (state == ST_DEFAULT) && (addr_r == '0);
endproperty
property p_no_configure_from_default;
@(posedge clk) disable iff (!rst_n)
(state == ST_DEFAULT) |=> (state != ST_CONFIGURED);
endpropertyp_reset_returns_to_default says nothing about fn_ep_en, and it does not need to —
because p_function_requires_configured holds unconditionally, in every cycle,
so a one-cycle window with the gate open on an unconfigured device is already
excluded. That is worth noticing rather than assuming: had the gate property been
written with an antecedent, this reset property would have left exactly that window
unchecked, and 28.2 measured the same class of gap in a different context — a
property about state is not a property about the outputs derived from it. Here the
two properties cover each other, and the reason they do is that one of them has no
antecedent at all.
p_no_configure_from_default is the address step, as an obligation. Mutation E-M2
breaks precisely it.
9. VHDL-2008
usb_fn_enable.vhd — the design, VHDL-2008
-- =====================================================================
-- usb_fn_enable (VHDL-2008) -- the same contract. Same ports, same
-- states, same gate, same reset, same latency.
--
-- The device state is an enumerated type with three values and no
-- fourth, so "the state machine fell into an undefined state" is not
-- a sentence that can be written about this file. The gate itself --
-- fn_ep_en only in CONFIGURED -- is one concurrent assignment, and a
-- reviewer checking the chapter's claim reads that one line.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity usb_fn_enable is
port (
clk : in std_logic;
rst_n : in std_logic;
attached : in std_logic;
bus_reset : in std_logic;
set_addr_valid : in std_logic;
set_addr : in unsigned(6 downto 0);
set_cfg_valid : in std_logic;
set_cfg : in unsigned(7 downto 0);
n_configs : in unsigned(7 downto 0);
dev_state : out unsigned(1 downto 0);
dev_addr : out unsigned(6 downto 0);
cfg_value : out unsigned(7 downto 0);
ep0_en : out std_logic;
fn_ep_en : out std_logic;
n_resets : out unsigned(15 downto 0);
n_addressed : out unsigned(15 downto 0);
n_configured : out unsigned(15 downto 0);
n_rejected : out unsigned(15 downto 0);
n_premature : out unsigned(15 downto 0)
);
end entity usb_fn_enable;
architecture rtl of usb_fn_enable is
type dev_state_t is (ST_DEFAULT, ST_ADDRESS, ST_CONFIGURED);
signal state : dev_state_t := ST_DEFAULT;
signal addr_r : unsigned(6 downto 0) := (others => '0');
signal cfg_r : unsigned(7 downto 0) := (others => '0');
signal c_rst, c_addr, c_cfg, c_rej, c_prem : unsigned(15 downto 0)
:= (others => '0');
signal addr_legal, cfg_legal, req_rejected, fn_i : std_logic;
begin
addr_legal <= '1' when (set_addr_valid = '1' and attached = '1' and
(state = ST_DEFAULT or state = ST_ADDRESS))
else '0';
cfg_legal <= '1' when (set_cfg_valid = '1' and attached = '1' and
(state = ST_ADDRESS or state = ST_CONFIGURED) and
set_cfg <= n_configs)
else '0';
req_rejected <= '1' when ((set_addr_valid = '1' and addr_legal = '0') or
(set_cfg_valid = '1' and cfg_legal = '0'))
else '0';
-- The gate. One line.
fn_i <= '1' when (attached = '1' and state = ST_CONFIGURED) else '0';
fn_ep_en <= fn_i;
ep0_en <= attached;
seq : process (clk, rst_n)
begin
if rst_n = '0' then
state <= ST_DEFAULT;
addr_r <= (others => '0');
cfg_r <= (others => '0');
c_rst <= (others => '0'); c_addr <= (others => '0');
c_cfg <= (others => '0'); c_rej <= (others => '0');
c_prem <= (others => '0');
elsif rising_edge(clk) then
if attached = '0' then
state <= ST_DEFAULT;
addr_r <= (others => '0');
cfg_r <= (others => '0');
elsif bus_reset = '1' then
state <= ST_DEFAULT;
addr_r <= (others => '0');
cfg_r <= (others => '0');
c_rst <= c_rst + 1;
else
if addr_legal = '1' then
addr_r <= set_addr;
if set_addr = 0 then
state <= ST_DEFAULT;
else
state <= ST_ADDRESS;
c_addr <= c_addr + 1;
end if;
end if;
if cfg_legal = '1' then
cfg_r <= set_cfg;
if set_cfg = 0 then
state <= ST_ADDRESS;
else
state <= ST_CONFIGURED;
c_cfg <= c_cfg + 1;
end if;
end if;
if req_rejected = '1' then
c_rej <= c_rej + 1;
end if;
end if;
if fn_i = '1' and state /= ST_CONFIGURED then
c_prem <= c_prem + 1;
end if;
end if;
end process seq;
dev_state <= to_unsigned(dev_state_t'pos(state), 2);
dev_addr <= addr_r;
cfg_value <= cfg_r;
n_resets <= c_rst;
n_addressed <= c_addr;
n_configured <= c_cfg;
n_rejected <= c_rej;
n_premature <= c_prem;
end architecture rtl;tb_usb_fn_enable.vhd — the testbench, VHDL-2008
-- =====================================================================
-- tb_usb_fn_enable -- VHDL-2008 testbench for usb_fn_enable.
-- Phases 1-3 present the SAME directed stimulus as the other two
-- benches, so their directed counts must agree.
--
-- Phase 1 states the gate directly: two hundred cycles of being
-- plugged in, with endpoint zero working, and the function endpoints
-- dark the whole time.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
entity tb_usb_fn_enable is
end entity tb_usb_fn_enable;
architecture sim of tb_usb_fn_enable is
constant HALF : time := 10 ns;
signal clk, rst_n, attached, bus_reset : std_logic := '0';
signal set_addr_valid, set_cfg_valid : std_logic := '0';
signal set_addr : unsigned(6 downto 0) := (others => '0');
signal set_cfg : unsigned(7 downto 0) := (others => '0');
signal n_configs : unsigned(7 downto 0) := to_unsigned(2, 8);
signal dev_state : unsigned(1 downto 0);
signal dev_addr : unsigned(6 downto 0);
signal cfg_value : unsigned(7 downto 0);
signal ep0_en, fn_ep_en : std_logic;
signal n_resets, n_addressed, n_configured, n_rejected, n_premature
: 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_fn_enable
port map (clk => clk, rst_n => rst_n, attached => attached,
bus_reset => bus_reset, set_addr_valid => set_addr_valid,
set_addr => set_addr, set_cfg_valid => set_cfg_valid,
set_cfg => set_cfg, n_configs => n_configs,
dev_state => dev_state, dev_addr => dev_addr,
cfg_value => cfg_value, ep0_en => ep0_en, fn_ep_en => fn_ep_en,
n_resets => n_resets, n_addressed => n_addressed,
n_configured => n_configured, n_rejected => n_rejected,
n_premature => n_premature);
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
constant ST_DEFAULT : natural := 0;
constant ST_ADDRESS : natural := 1;
constant ST_CONFIG : natural := 2;
variable rm_state : natural := 0;
variable rm_addr : natural := 0;
variable rm_cfg : natural := 0;
variable rm_rst, rm_adr, rm_cfgc, rm_rej : natural := 0;
variable chk_dir, chk_rnd, errs, shown : natural := 0;
variable in_random : boolean := false;
variable m_attach_cycles, m_cfg_cycles, m_fn_cycles, m_resets : natural := 0;
variable m_addressed, m_configured, m_rejected, m_unconfig : natural := 0;
variable m_setupfail, attached_run : natural := 0;
variable seed1 : positive := 118_339; variable seed2 : positive := 90_071;
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;
procedure ref_step is
variable a_ok, c_ok : boolean;
begin
if rst_n = '0' then
rm_state := ST_DEFAULT; rm_addr := 0; rm_cfg := 0;
rm_rst := 0; rm_adr := 0; rm_cfgc := 0; rm_rej := 0;
elsif attached = '0' then
rm_state := ST_DEFAULT; rm_addr := 0; rm_cfg := 0;
elsif bus_reset = '1' then
rm_state := ST_DEFAULT; rm_addr := 0; rm_cfg := 0;
rm_rst := rm_rst + 1; m_resets := m_resets + 1;
else
a_ok := (set_addr_valid = '1') and
(rm_state = ST_DEFAULT or rm_state = ST_ADDRESS);
c_ok := (set_cfg_valid = '1') and
(rm_state = ST_ADDRESS or rm_state = ST_CONFIG) and
(set_cfg <= n_configs);
if a_ok then
rm_addr := to_integer(set_addr);
if set_addr = 0 then
rm_state := ST_DEFAULT;
else
rm_state := ST_ADDRESS;
rm_adr := rm_adr + 1; m_addressed := m_addressed + 1;
end if;
end if;
if c_ok then
rm_cfg := to_integer(set_cfg);
if set_cfg = 0 then
rm_state := ST_ADDRESS; m_unconfig := m_unconfig + 1;
else
rm_state := ST_CONFIG;
rm_cfgc := rm_cfgc + 1; m_configured := m_configured + 1;
end if;
end if;
if ((set_addr_valid = '1') and not a_ok) or
((set_cfg_valid = '1') and not c_ok) then
rm_rej := rm_rej + 1; m_rejected := m_rejected + 1;
end if;
end if;
if attached = '1' then m_attach_cycles := m_attach_cycles + 1; end if;
if rm_state = ST_CONFIG then m_cfg_cycles := m_cfg_cycles + 1; end if;
end procedure ref_step;
procedure cmp is
variable e_fn : integer;
begin
if attached = '1' and rm_state = ST_CONFIG then e_fn := 1; else e_fn := 0; end if;
ck("dev_state", to_integer(dev_state), rm_state);
ck("dev_addr", to_integer(dev_addr), rm_addr);
ck("cfg_value", to_integer(cfg_value), rm_cfg);
ck("ep0_en", b2i(ep0_en), b2i(attached));
ck("fn_ep_en", b2i(fn_ep_en), e_fn);
ck("n_resets", to_integer(n_resets), rm_rst);
ck("n_addressed", to_integer(n_addressed), rm_adr);
ck("n_configured", to_integer(n_configured), rm_cfgc);
ck("n_rejected", to_integer(n_rejected), rm_rej);
ck("n_premature", to_integer(n_premature), 0);
if fn_ep_en = '1' then m_fn_cycles := m_fn_cycles + 1; end if;
end procedure cmp;
procedure intent_check is
begin
bump;
if fn_ep_en = '1' and to_integer(dev_state) /= ST_CONFIG then
errs := errs + 1;
report " ** INTENT VIOLATED: function endpoints enabled unconfigured"
severity warning;
end if;
bump;
if fn_ep_en = '1' and attached = '0' then
errs := errs + 1;
report " ** INTENT VIOLATED: function endpoints enabled while detached"
severity warning;
end if;
end procedure intent_check;
procedure step is
begin
wait for 1 ns;
wait until rising_edge(clk);
ref_step;
wait for 1 ns;
cmp;
intent_check;
bus_reset <= '0'; set_addr_valid <= '0'; set_cfg_valid <= '0';
set_addr <= (others => '0'); set_cfg <= (others => '0');
end procedure step;
procedure idle is begin step; end procedure;
procedure hard_reset is
begin
rst_n <= '0'; attached <= '0'; bus_reset <= '0';
set_addr_valid <= '0'; set_addr <= (others => '0');
set_cfg_valid <= '0'; set_cfg <= (others => '0');
n_configs <= to_unsigned(2, 8);
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;
end procedure hard_reset;
procedure plug is begin attached <= '1'; step; end procedure;
procedure unplug is begin attached <= '0'; step; end procedure;
procedure do_reset is begin bus_reset <= '1'; step; end procedure;
procedure addr_req (a : natural) is
begin set_addr_valid <= '1'; set_addr <= to_unsigned(a, 7); step; end procedure;
procedure cfg_req (c : natural) is
begin set_cfg_valid <= '1'; set_cfg <= to_unsigned(c, 8); step; end procedure;
procedure setup_state (want : natural) is
begin
hard_reset;
plug;
if want >= ST_ADDRESS then addr_req(9); end if;
if want = ST_CONFIG then cfg_req(1); end if;
bump;
if to_integer(dev_state) /= want then
errs := errs + 1; m_setupfail := m_setupfail + 1;
report " ** setup: state not reached" severity warning;
end if;
end procedure setup_state;
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 r : natural;
begin
-- ---- PHASE 1 : the gate ----
hard_reset;
plug;
ck("attached", b2i(attached), 1);
ck("endpoint zero is up", b2i(ep0_en), 1);
attached_run := 0;
for i in 0 to 199 loop
idle;
ck("still nothing usable", b2i(fn_ep_en), 0);
ck("but EP0 still is", b2i(ep0_en), 1);
attached_run := attached_run + 1;
end loop;
ck("still in Default", to_integer(dev_state), ST_DEFAULT);
ck("still address zero", to_integer(dev_addr), 0);
ck("nothing premature", to_integer(n_premature), 0);
addr_req(5);
ck("addressed, still not usable", b2i(fn_ep_en), 0);
cfg_req(1);
ck("configured: now usable", b2i(fn_ep_en), 1);
bump;
if attached_run < 200 then
errs := errs + 1;
report " ** intent: too few attached cycles" severity warning;
end if;
report " phase 1 intent : " & integer'image(chk_dir) &
" checks, " & integer'image(errs) & " errors (" &
integer'image(attached_run) & " attached cycles, unusable)";
-- ---- PHASE 2 : 3 x 4 x 3 = 36 ----
for st in 0 to 2 loop
for rq in 0 to 3 loop
for vc in 0 to 2 loop
setup_state(st);
case rq is
when 0 => idle;
when 1 =>
if vc = 0 then addr_req(0);
elsif vc = 1 then addr_req(17);
else addr_req(126); end if;
when 2 =>
if vc = 0 then cfg_req(0);
elsif vc = 1 then cfg_req(1);
else cfg_req(9); end if;
when others => do_reset;
end case;
idle;
end loop;
end loop;
end loop;
report " phase 2 exhaustive : " & integer'image(chk_dir) &
" checks, " & integer'image(errs) & " errors (36 combinations)";
-- ---- PHASE 3 : the walk ----
hard_reset;
ck("S1 detached: EP0 down", b2i(ep0_en), 0);
plug;
ck("S1 attached: EP0 up", b2i(ep0_en), 1);
ck("S1 attached: fn down", b2i(fn_ep_en), 0);
do_reset;
ck("S1 reset: Default", to_integer(dev_state), ST_DEFAULT);
ck("S1 reset: fn down", b2i(fn_ep_en), 0);
addr_req(12);
ck("S1 addressed", to_integer(dev_state), ST_ADDRESS);
ck("S1 addressed: fn down", b2i(fn_ep_en), 0);
ck("S1 the address stuck", to_integer(dev_addr), 12);
cfg_req(1);
ck("S1 configured", to_integer(dev_state), ST_CONFIG);
ck("S1 NOW it is usable", b2i(fn_ep_en), 1);
do_reset;
ck("S2 back to Default", to_integer(dev_state), ST_DEFAULT);
ck("S2 address gone", to_integer(dev_addr), 0);
ck("S2 function gone", b2i(fn_ep_en), 0);
hard_reset; plug; addr_req(3); cfg_req(1);
ck("S3 usable", b2i(fn_ep_en), 1);
cfg_req(0);
ck("S3 unconfigured", to_integer(dev_state), ST_ADDRESS);
ck("S3 not usable", b2i(fn_ep_en), 0);
ck("S3 but still addressed",to_integer(dev_addr), 3);
hard_reset; plug; addr_req(3);
addr_req(0);
ck("S4 back to Default", to_integer(dev_state), ST_DEFAULT);
hard_reset; plug;
cfg_req(1);
ck("S5 rejected", to_integer(n_rejected), 1);
ck("S5 still Default", to_integer(dev_state), ST_DEFAULT);
ck("S5 still not usable", b2i(fn_ep_en), 0);
hard_reset; plug; addr_req(4);
cfg_req(7);
ck("S6 refused", to_integer(n_rejected), 1);
ck("S6 still only addressed", to_integer(dev_state), ST_ADDRESS);
hard_reset; plug; addr_req(6); cfg_req(2);
ck("S7 usable", b2i(fn_ep_en), 1);
unplug;
ck("S7 EP0 down", b2i(ep0_en), 0);
ck("S7 function down", b2i(fn_ep_en), 0);
ck("S7 Default again", to_integer(dev_state), ST_DEFAULT);
plug;
ck("S7 replug is not resume", b2i(fn_ep_en), 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; plug;
for j in 0 to 3999 loop
r := rnd(100);
if r < 22 then addr_req(rnd(128));
elsif r < 44 then cfg_req(rnd(4));
elsif r < 54 then do_reset;
elsif r < 60 then unplug; plug;
elsif r < 64 then
n_configs <= to_unsigned(rnd(4), 8);
idle;
else idle;
end if;
end loop;
in_random := false;
report " measured reachability (all phases)";
report " cycles attached ........ " & integer'image(m_attach_cycles);
report " cycles configured ...... " & integer'image(m_cfg_cycles);
report " cycles function enabled " & integer'image(m_fn_cycles);
report " bus resets ............. " & integer'image(m_resets);
report " addresses assigned ..... " & integer'image(m_addressed);
report " configurations selected " & integer'image(m_configured);
report " unconfigure requests ... " & integer'image(m_unconfig);
report " requests rejected ...... " & integer'image(m_rejected);
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 gives the five-line hierarchy a name per line, which for this subject is
worth more than it usually is: an enumerated dev_state_t makes a report say
ST_ADDRESS rather than 1, and a reviewer asking "which states enable the
function" reads one concurrent assignment.
10. The Misconception As Hardware
MUT THE BELIEF ENCODED V-DIR SV-DIR VH-DIR
E-M1 enumeration is optional -- attached is
enough (fn_ep_en = attached) 1,246 1,246 1,246
E-M2 the address step is a formality
(SET_CONFIGURATION accepted from
DEFAULT, skipping ADDRESS) 22 22 22BASE zero in all six columns; directed columns identical across all three
languages.
E-M1 is one term deleted:
assign fn_ep_en = attached && (state == ST_CONFIGURED); // architecture
assign fn_ep_en = attached; // the beliefIt scores 1,246 — the highest of any mutation in this module — because a device
built this way is wrong in every cycle in which it is attached and not configured,
and that is most of its life. It also starts n_premature counting — the
output that exists precisely so that a build in which the impossible became
possible would say so, and which reads zero in every correct run of all three
languages.
E-M2 scores 22, and its low score is the informative part. It accepts SET_CONFIGURATION from DEFAULT — skipping the address step entirely — which is wrong in exactly the cycles where somebody tries it. The directed suite tries it 22 times because a scenario was written to try it; a bench that only ever enumerated in the correct order would score zero and report a clean pass.
E-M1 wrong everywhere caught by everything
E-M2 wrong only when a
specific illegal
sequence is attempted caught only because someone attempted it
A suite that only exercises the LEGAL sequence cannot distinguish a
device that enforces ordering from one that does not.11. What The Wrong Model Does To Debugging
This is the chapter's real subject, because the belief costs weeks and never produces a single wrong bit.
SYMPTOM "the device does nothing"
WRONG MODEL attached means usable; enumeration is housekeeping
WRONG QUESTION what is wrong with my endpoint?
WASTED ON the data path, the buffers, the FIFO, the function
logic, the firmware's transmit routine -- all of
which are typically CORRECT, because they were
verified, and none of which has been reached
CORRECT MODEL a function endpoint exists only in the CONFIGURED
state
THE FIRST QUESTION, and it is one observation:
WHAT STATE DID THE DEVICE REACH?
and the four answers are four unrelated investigations:
never attached / no reset seen -> electrical. Power, D+/D-,
pull-up, connector, VBUS.
Not a logic problem at all.
reached DEFAULT, stuck there -> endpoint 0 is not answering
correctly. The control
machine, the SETUP decode, or
the device descriptor.
reached ADDRESS, stuck there -> the host read your
configuration descriptors and
declined. A DESCRIPTOR defect:
wLength, a size mismatch, an
endpoint count, requested
bandwidth the host cannot
grant.
reached CONFIGURED, no data -> NOW it is your endpoint, and
31.2's tree applies: is the
host issuing tokens to it?One observation. Four branches. Three of them are not in the data path, and the belief suggests looking only at the data path.
And the bring-up consequence, which follows from prediction 4 and is the most practical sentence in this chapter: you cannot stub enumeration to test the data path. There is no legal sequence in which a function endpoint receives a token without a configuration having been selected. An engineer who plans bring-up as "get the endpoint working, then do the enumeration bit" has planned the two phases in an impossible order.
12. Interview Reasoning
"A device is plugged in and nothing happens. Where do you start?"
I would start by establishing what state the device reached, because "nothing happens" has at least four unrelated causes and they are distinguished by one observation rather than by guessing.
The reason is that being attached and being usable are different things, with three steps between them. Electrically present is the first. Then a bus reset puts the device in Default at address 0, where its only channel is endpoint 0. Then SET_ADDRESS gives it an address. Then the host reads its configuration descriptors and issues SET_CONFIGURATION — and only then do the endpoints the descriptors declare exist. A function endpoint cannot carry a byte before that point; a token to it isn't a transfer, it's an error.
So the four branches. If no reset is ever seen, it is electrical — VBUS, the pull- up, the connector — and not a logic problem. If it reaches Default and sticks, the control endpoint or the device descriptor is at fault. If it reaches Address and sticks, the host read the configuration descriptors and declined them — that is a descriptor defect, and it is the one people misdiagnose, because the data path is perfect and has never been reached. If it reaches Configured and still no data, now it is the endpoint, and the first question there is whether the host is polling it at all.
Two things I would add because they catch people out. "It enumerated" is not one event — a device that reached Address appears in the host's device list, which reads as success. And a bus reset returns the device to Default and destroys the configuration, so every non-control endpoint stops existing; a device that treats its address as permanent works until the first resume.
13. Exercises
1 THE FIVE LINES
Write out the five things that are not the same thing, and give
an observable symptom for a device stuck at each one.
2 THE EXCEPTION
Explain why endpoint 0 must be enabled from attachment, and
describe what would be impossible if it were gated on
CONFIGURED.
3 BUS RESET
List everything a bus reset destroys and everything it leaves
alone. Which of these is a chip reset also responsible for?
4 VERILOG
Add the SUSPEND state. Which outputs does it gate, and is a
resume a bus reset?
5 SYSTEMVERILOG
Write the property that says an address, once assigned, survives
a SET_CONFIGURATION and does not survive a bus reset.
6 VHDL
Implement exercise 4 and say what the enumerated state type gave
you in the failure reports.
7 TESTBENCH
Phase 1 holds the device attached and unusable for 200 cycles.
Write the equivalent negative intent check for the SUSPEND state
in exercise 4.
8 MUTATION
E-M2 scores 22 only because a scenario attempts an illegal
sequence. Write a third mutation that a legal-sequences-only
bench would score ZERO on, and say what that proves about such a
bench.
9 DEBUG
A device works on Linux and not on one Windows machine, and
enumerates on both. Give the observation that distinguishes the
cases, and say which of the four branches each outcome puts you
in.
10 BRING-UP
Write the bring-up order for a new USB device, and justify why
the data path is not first.14. What Carries Forward
THE CORRECTION
o enumeration does not REGISTER a device's functionality, it
CREATES it
o five things that are not the same thing: attached, DEFAULT,
ADDRESS, CONFIGURED, usable -- each requiring the one above
o endpoint 0 is enabled from attachment because it is the channel
over which the rest is arranged; every other endpoint is gated
o a bus reset returns the device to DEFAULT and every non-control
endpoint stops existing -- and a bus reset is a routine event
o descriptors do not describe, they DECLARE: a device with a
perfect data path and one wrong descriptor byte moves no data at
all
o you cannot stub enumeration to bring up the data path. There is
no legal sequence in which a function endpoint is addressed
before a configuration is selected.
THE HARDWARE
o fn_ep_en = attached && (state == CONFIGURED), and ep0_en =
attached. The asymmetry is the architecture.
o an n_premature counter that is structurally zero, so that a
build in which it is not says so
o SET_CONFIGURATION(0) unconfigures and is not an error;
SET_ADDRESS while configured is rejected
THE METHOD
o a negative architectural claim ("attached is not usable") has to
be an intent check held over TIME -- 200 cycles -- because no
comparison against a belief-sharing model can express it
o reach a state the way the design promises, never by forcing, and
have the bench CHECK that it arrived: m_setupfail == 0 is
load-bearing
o a property about state is not a property about the outputs
derived from it; the reset property checks both
o a suite that only exercises the LEGAL sequence cannot tell a
device that enforces ordering from one that does not -- E-M2
would score zero
o the misconception is wrong everywhere (1,246), so removing its
dedicated phase leaves 437. Nobody ships this belief; they debug
with it.
THE DEBUG CONSEQUENCE
o "the device does nothing" -> ask WHAT STATE IT REACHED. One
observation, four branches, three of them outside the data path.
o "it enumerated" is not one event. Reached ADDRESS still appears
in the device list.One belief remains, and it is the only one in this module that is newer than the engineers who hold it — which is why it is the one most likely to be repeated in a design review this year.
Continue learning
Related tutorials
- 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
“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.
- Related topic
Descriptor Engine
wLength is the size of the host's buffer, not a preference — and whether a zero-length packet must follow depends on comparing what was sent against what was asked for, not against what exists.
Standards & specifications
- Governing standard
- USB-IF (Universal Serial Bus Specification)(opens USB Implementers Forum (USB-IF) in a new tab)
Defines the USB bus — its electrical signalling, connectors, packet and transaction model, device framework and the descriptors a device must expose — together with the device-class specifications layered on it. It does not define host-controller register interfaces (xHCI and EHCI are separate documents) nor any operating system's driver architecture.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the USB curriculum.
