USB · Module 6
End-to-End Enumeration Flow
The whole sequence from attach to usable device as one flow and one state machine — and the class of defect that lives only between steps, where every block verifies correctly and the composition still fails.
Five chapters, five commit points: attached, reset, addressed, described, configured. Each was studied alone, with its own RTL, its own properties, and its own characteristic failure.
This chapter puts them together, and it is worth doing for a reason beyond review.
Every step in this module has now been verified against its own rules. The bugs that remain are the ones that live between steps.
That is not a rhetorical flourish. Chapter 6.3 §7 already produced one: an address register whose defect was invisible in every property written about addressing, and whose damage appeared inside the state machine of Chapter 6.2. A module-level view is how that class of defect becomes visible at all.
1. The Whole Flow
Read this as one narrative before examining any part of it.
A device is plugged in. The hub notices an electrical change and, crucially, waits — Chapter 6.1 showed why the wait is a correctness requirement rather than a convenience. Once the connection is stable the host enables the port and asserts a reset, which Chapter 6.2 showed is a statement rather than a question: the device ends in Default, reachable at the default address, with no address and no configuration.
The host then assigns an address. Chapter 6.3 showed that the device captures it, completes the transfer at the old address, and only afterwards begins answering at the new one — then the host allows a recovery interval. The device is now in Address: reachable, and useless.
Next the host asks the device to describe itself. Chapter 6.4 showed the first read is deliberately short, because the packet size the host needs is inside the description it is fetching, and that the same bootstrap recurs when the configuration subtree is read. The host now knows what the device is.
Finally the host selects a configuration. Chapter 6.5 showed this resolves the device's alternatives, brings up its endpoints, and commits a power budget. The device is in Configured, and usable.
2. The Flow as One State Machine
One observation carries most of the weight of this diagram. Reset reaches Default from every state. The device's protocol state can always be forced to a known value by an action the host can take unilaterally, at any moment, without the device's cooperation and without knowing what state it is in.
That is why enumeration is restartable. Any step can fail, at any point, and the host's recovery is always the same: assert reset and begin again. The flow needs no error-recovery paths of its own because it has exactly one, and it is the same edge that starts the sequence.
And it is why the reset scope of Chapter 6.2 matters so much. If a reset failed to clear some piece of protocol state, that state would survive a recovery — and the restart, which is supposed to be a clean slate, would instead be the mechanism by which stale state propagates into the next attempt.
3. Where the Seams Are
Now the part a module-level view exists for.
Each chapter verified one step against its own rules. That is necessary and it is not sufficient, because correctness of the parts does not compose into correctness of the whole when the parts share state or ordering. Three seams in this flow carry that risk.
Seam 1 — the address commit and the device state machine. Chapter 6.3 produces a commit pulse; Chapter 6.2 consumes it as the device has been addressed. Neither owns the pulse's meaning jointly. 6.3 §7 measured the consequence: a register that emits a spurious commit pulse passes every property written about addresses, and knocks a Configured device back to Address — a defect in one module whose damage is entirely in another.
Seam 2 — a reset arriving mid-sequence. Every chapter handles a reset correctly in isolation. The composition question is different: when a reset arrives during the descriptor walk, does everything learned and committed go away together? The address register must clear, the walk must abandon its learned lengths, the configuration must clear, and the state must return to Default — and they must do so consistently, because a device that clears three of the four is in a state the protocol has no name for.
Seam 3 — ordering between steps. Nothing in the individual chapters prevents a configuration commit from being accepted by a device that has not yet been addressed, because each chapter verified its own transition. Order is a property of the composition, and so it must be checked there.
4. The Enumeration Reference Model
This is a verification model, not a design. It exists to be compared against, and the difference matters: it is written for clarity and checkability, with no concern for synthesis.
// ─────────────────────────────────────────────────────────────────────────
// usb_enum_model
//
// Classification: VERIFICATION MODEL. NOT synthesizable, NOT a design, and
// NOT a USB device controller. It is a golden reference for the ENUMERATION
// FLOW: what protocol state the device should be in after each event, and
// which events are legal in each state.
//
// WHY IT EXISTS. Chapters 6.1-6.5 each verified one step against its own
// rules. This model exists to check the SEAMS of section 3 -- ordering
// between steps, and the consistency of everything a reset must clear.
//
// WHAT IT MODELS. The protocol state, the address, the configuration, and
// how far the descriptor walk has progressed -- the four pieces of state
// the flow as a whole is about.
//
// WHAT IT DOES NOT MODEL. Everything else, deliberately: transfer
// mechanics, packets, endpoints, timing (Chapter 6.7 owns durations; this
// model is event-ordered and has no notion of a nanosecond), descriptor
// contents, hubs, power, or any speed-dependent behaviour.
//
// HOW TO USE IT. Drive it with the same decoded events as the DUT and
// compare after every event. Divergence is a bug in one of them; which one
// is a question the model cannot answer and a human must.
// ─────────────────────────────────────────────────────────────────────────
package usb_enum_model_pkg;
typedef enum {
ST_ATTACHED, // present, no reset seen
ST_DEFAULT, // reset complete -- answers at the default address
ST_ADDRESS, // has an address, not usable
ST_CONFIGURED // a configuration is selected, endpoints live
} model_state_e;
typedef enum {
EV_ATTACH,
EV_BUS_RESET,
EV_ADDR_COMMIT, // carries a value
EV_DESC_PREFIX_OK, // a short read completed
EV_DESC_FULL_OK, // a full read completed
EV_CFG_COMMIT, // carries a value; zero is an un-select
EV_DETACH
} model_event_e;
endpackage
// The import sits OUTSIDE the class: a package import is not a legal class
// item, only a compilation-unit, module or package item.
import usb_enum_model_pkg::*;
class usb_enum_model;
model_state_e state;
int unsigned addr;
int unsigned config_val;
int unsigned desc_reads_done; // how far the walk has progressed
int unsigned illegal_events; // events the device should have refused
function new();
reset_to_detached();
endfunction
// Everything the model knows, cleared. Distinct from a BUS reset, which
// is a protocol event and leaves the device attached.
function void reset_to_detached();
state = ST_ATTACHED;
addr = 0;
config_val = 0;
desc_reads_done = 0;
illegal_events = 0;
endfunction
// ── The seam-2 rule, in one place ───────────────────────────────────────
// A bus reset clears EVERY piece of protocol state together. Keeping this
// in a single function is deliberate: the defect it guards against is a
// design that clears some of these and not others, and a model that
// scattered the clearing across the event handler could grow exactly the
// same defect and then agree with the DUT about it.
function void apply_bus_reset();
state = ST_DEFAULT;
addr = 0;
config_val = 0;
desc_reads_done = 0;
endfunction
function void event_in(model_event_e ev, int unsigned val = 0);
case (ev)
EV_ATTACH: begin
reset_to_detached();
end
EV_DETACH: begin
reset_to_detached();
state = ST_ATTACHED;
end
// Legal from ANY state -- this is section 2's central observation, and
// the reason enumeration is restartable.
EV_BUS_RESET: begin
apply_bus_reset();
end
// ── Seam 3: ordering ──────────────────────────────────────────────
// An address may only be committed by a device that has been reset.
EV_ADDR_COMMIT: begin
if (state != ST_DEFAULT && state != ST_ADDRESS
&& state != ST_CONFIGURED) begin
illegal_events++;
end else if (val == 0) begin
// Committing the default address is not an assignment.
illegal_events++;
end else begin
addr = val;
state = ST_ADDRESS;
// Re-addressing a configured device is not something a host does,
// but if it happens the configuration cannot survive it: the
// device has been re-identified.
config_val = 0;
end
end
EV_DESC_PREFIX_OK: begin
if (state != ST_ADDRESS && state != ST_CONFIGURED) illegal_events++;
else desc_reads_done++;
end
EV_DESC_FULL_OK: begin
// A full read is only meaningful after the prefix read that gave it
// a length -- Chapter 6.4's gate, checked here at flow level.
if (state != ST_ADDRESS && state != ST_CONFIGURED) illegal_events++;
else if (desc_reads_done == 0) illegal_events++;
else desc_reads_done++;
end
// ── Seam 3 again, and Chapter 6.5's value rule ────────────────────
EV_CFG_COMMIT: begin
if (state != ST_ADDRESS && state != ST_CONFIGURED) begin
// A device that has not been addressed cannot be configured.
illegal_events++;
end else begin
config_val = val;
state = (val == 0) ? ST_ADDRESS : ST_CONFIGURED;
end
end
default: illegal_events++;
endcase
endfunction
// ── Checks the model asserts about ITSELF ───────────────────────────────
// A reference model with an internal contradiction is worse than no model,
// because the comparison then passes on a state neither side should be in.
function bit self_consistent();
if (state == ST_CONFIGURED && config_val == 0) return 0;
if (state == ST_CONFIGURED && addr == 0) return 0;
if (state == ST_ADDRESS && addr == 0) return 0;
if (state == ST_DEFAULT && (addr != 0 || config_val != 0)) return 0;
return 1;
endfunction
endclassClassification. Verification model. Not synthesizable and not a design.
What it models. The four pieces of state the flow is about, the legality of each event in each state, and the single rule that a bus reset clears everything together.
Why it exists. To check §3's seams, which no chapter's own properties cover.
What it deliberately omits. Transfer mechanics, packets, endpoints, timing, descriptor contents, hubs, power and speed. It is event-ordered and has no notion of duration — Chapter 6.7 owns that, and mixing the two would make this model wrong in a way that is hard to see.
How to use it. Drive it with the same decoded events as the DUT, compare after each, and treat divergence as a bug in one of them without assuming which.
Two design decisions inside it are worth naming, because both are about the model's own trustworthiness:
apply_bus_resetis one function. The defect being hunted is a design that clears some protocol state and not the rest. A model that scattered the clearing across its event handler could acquire that same defect and would then agree with the broken DUT, which is the worst possible outcome for a reference model.self_consistentexists at all. A model that has drifted into a contradictory state will happily compare against a DUT in the same contradictory state. Checking the model against its own invariants is what stops a comparison from passing on a state neither side should occupy.
5. Flow-Level Assertions
These are properties of the composition, not of any one chapter's block. They are the ones nobody's block-level property set contains.
// ─────────────────────────────────────────────────────────────────────────
// Flow-level assertions. Written against the DEVICE STATE and the decoded
// commit events, i.e. across the seams of section 3.
//
// SCOPE NOTE. These bind to a COMPOSED environment, not to any one of this
// module's blocks -- that is the point of them. Two of the signals they use
// exist only at that level: `walk_in_progress`, from Chapter 6.4's
// sequencer, and `request_outstanding`, which the environment tracks the way
// Chapter 6.3's A6 does, deliberately NOT reusing any design's own pending
// flag. A property that borrows the flag it is meant to police cannot
// police it.
// ─────────────────────────────────────────────────────────────────────────
// F1 -- ORDERING. A device cannot be configured before it is addressed.
// No chapter's own properties cover this: 6.5 verified its transition, and
// "was there ever an address" is not a fact 6.5 owns.
property p_no_config_before_address;
@(posedge clk) disable iff (!rst_n)
cfg_committed |-> (dev_state == CS_ADDRESS || dev_state == CS_CONFIGURED);
endproperty
assert property (p_no_config_before_address);
// F2 -- SEAM 2. A bus reset clears every piece of protocol state TOGETHER.
// The bug this exists for is a design that clears three of four, leaving
// the device in a combination the protocol has no name for.
property p_reset_clears_everything_together;
@(posedge clk) disable iff (!rst_n)
bus_reset |=> (dev_state == CS_DEFAULT
&& active_addr == 7'd0
&& active_config == 8'd0
&& !endpoints_live
&& !walk_in_progress);
endproperty
assert property (p_reset_clears_everything_together);
// F3 -- SEAM 1. Every commit event must be one the flow actually asked for.
// Generalises Chapter 6.3's A6 to the flow: a commit pulse of any kind with
// no outstanding request is a spurious event, and section 3 showed that its
// damage lands in a different block from the one that emitted it.
property p_commits_have_causes;
@(posedge clk) disable iff (!rst_n)
(addr_committed || cfg_committed) |-> request_outstanding;
endproperty
assert property (p_commits_have_causes);
// F4 -- the device is never usable without being identifiable. Endpoints
// live while the device has no address is a device sending traffic nobody
// can attribute.
property p_usable_implies_addressed;
@(posedge clk) disable iff (!rst_n)
endpoints_live |-> (active_addr != 7'd0);
endproperty
assert property (p_usable_implies_addressed);
// F5 -- RESTARTABILITY, the property section 2 is about. From ANY state, a
// bus reset reaches Default. Written with no antecedent state constraint
// deliberately: the moment a property about reset needs to know where the
// device was, reset has stopped being unconditional.
property p_reset_always_reaches_default;
@(posedge clk) disable iff (!rst_n)
bus_reset |=> (dev_state == CS_DEFAULT);
endproperty
assert property (p_reset_always_reaches_default);F1 and F3 are the two that exist only here. Every other property in this module belongs to some chapter; these two belong to the flow, and nothing smaller than the flow can state them.
F5 deserves its comment. The temptation when writing a reset property is to enumerate the states it applies from. Doing so quietly converts an unconditional rule into a conditional one, and the state that gets left out of the enumeration is the one where the bug is.
6. What the Flow View Catches
Two defects, both of which pass every block-level property in this module. Both were run; the outputs below are measured.
A spurious commit pulse. Chapter 6.3 §7 measured this one: an address register that leaves its pending flag set re-commits on the next unrelated completion. Its own five properties all pass — there is no wrong address anywhere — and wiring it to Chapter 6.2's state machine showed a Configured device falling back to Address, silently losing its configuration. F3 catches it at flow level for the same reason 6.3's A6 catches it at block level: a commit is an event that requires a cause.
A partial reset. Suppose a controller's reset handling clears the address and the configuration but leaves the descriptor walk holding what it learned. This one was run against §4's model with exactly that mutation, and the result is worth reading in full.
Every per-block reset check passes:
PASS 6.2's check: state returned to DEFAULT
PASS 6.3's check: address cleared
PASS 6.5's check: configuration cleared
PER-BLOCK RESULT: ALL PASS (0 errors)Three chapters, three correct properties, three passes. Each checks the state it owns, and each is satisfied. Chapter 6.4's W6 would catch it — if the walk sequencer is being verified at the same time, with reset stimulus, which at integration it frequently is not.
The flow-level check fails immediately:
F2-FAIL: reset did not clear everything together (walk progress = 2)And the measured consequence is the part to carry away. On the second enumeration, the model was driven with a full descriptor read that had no preceding prefix read — an illegal sequence, and one the correct model records as illegal. The mutant accepted it:
GOLDEN second enumeration: illegal_events=1 desc_reads_done=0
Q1 second enumeration: illegal_events=0 desc_reads_done=3
>>> the un-gated full read was ACCEPTED -- stale walk progress authorised itRead that last line carefully. The stale value did not merely linger; it authorised an operation that should have been refused. Chapter 6.4's gate was structurally intact and gating on a fact left over from the previous enumeration.
F2 catches it because it checks the clearing as one atomic requirement rather than as four separate ones. That is the whole difference between four properties that each pass and one property that fails: the conjunction is the requirement, and splitting it across owners loses it.
7. Verification
This chapter's commit point is enumeration complete — the whole flow, and the only one no single block owns.
Stimulus. A clean enumeration end to end; then the same flow with a bus reset injected at each step in turn, including between a request and its commit; a failed transfer at each step; a device that is detached mid-enumeration; an un-select followed by a re-select; and a second full enumeration after a completed one, which is where stale state from the first attempt becomes visible.
The requirement that makes this chapter's stimulus different from every earlier one: the interesting cases are crossings, not points. A reset during the descriptor walk is not covered by testing reset and testing the walk. §6's second defect exists precisely in that crossing.
Observation. The four model variables together — state, address, configuration, walk progress — compared after every event. Comparing them individually is what lets a partial reset pass.
Reference model. §4's, driven from the same decoded events. Check self_consistent() after each comparison as well, so that a model which has drifted cannot silently endorse a DUT that has drifted with it.
Representative coverage — crosses:
- bus reset × every step of the flow, including mid-transfer
- transfer failure × every step
- device state × each commit event, so that illegal orderings are exercised rather than assumed absent
- un-select × re-select × reset, in each order
- second enumeration after a complete first one × after an aborted first one
Negative cases with defined outcomes: a configuration commit on an unaddressed device must be refused, not accepted; an address commit of the default address is not an assignment and must not move the device to Address; and a second enumeration must produce a device indistinguishable from a first-time one, with nothing carried over.
8. Common Misconceptions
9. Reason It Through
A device enumerates perfectly. It is unplugged and plugged back in. The second enumeration fails — the host reports the device as unresponsive partway through the descriptor walk. Unplugging it and waiting several seconds makes the next attempt succeed.
What does the timing dependence tell you? That something persists across the re-attach and decays or is cleared with time. That points at state which survived when it should not have, rather than at a logic error in any single step — a step that was simply wrong would fail on the first enumeration too.
Why does the first enumeration always work? Because there is no stale state to survive into it. A defect of this shape is invisible by construction on a first attempt, which is why §7 requires a second full enumeration as explicit stimulus.
Which seam is this? §3's second: a reset that does not clear everything together. Something from the first enumeration — a learned length, a pending value, a walk stage — was not cleared, and the second attempt begins with a device that believes it already knows something.
Why does the failure land in the descriptor walk specifically? Because the walk is the first step whose behaviour depends on remembered values rather than on the request in front of it. Reset and addressing are self-contained; the walk acts on lengths learned earlier. A stale length is used the moment the walk reaches a full read.
What would have caught it before silicon? F2, as one property. Note what would not have: each block's own reset property, all of which pass — §6 measured exactly that, three per-block reset checks passing against a design that leaves the walk's state behind.
And §6 measured the mechanism too: the stale value does not merely linger, it authorises a read that should have been refused, because the gate that was supposed to permit it was satisfied by a fact left over from the previous enumeration.
And the debugging lesson? Works the first time, fails the second, recovers after a delay is a signature worth memorising. It says state, not logic — and it points at whatever was supposed to clear that state rather than at whatever failed.
10. Understanding Check
11. Summary
Enumeration is five commit points — attached, reset, addressed, described, configured — and one unconditional edge. Reset reaches Default from every state, which is what makes the flow restartable and why no individual step needs an error-recovery path: there is exactly one, and it is the same edge that starts the sequence.
Verifying each step against its own rules is necessary and not sufficient. Requirements that span blocks — cleared together, only after, never without — are lost the moment they are distributed into per-block properties that each pass. Two measured defects in this module have exactly that shape: a spurious commit pulse, whose damage lands in a different block from the defect, and a partial reset, where four correct per-block reset properties coexist with a device that clears three things out of four.
The flow-level properties are the ones nobody's block owns: a device cannot be configured before it is addressed; a reset clears everything together, as one atomic requirement; every commit event corresponds to something that was requested; a device is never usable without being identifiable; and a reset reaches Default from anywhere, written with no antecedent state constraint, because enumerating states converts an unconditional rule into a conditional one.
The reference model is a verification model — event-ordered, with no notion of duration — and two things about it are deliberate: the reset clearing is one function, so the model cannot grow the defect it is hunting and then agree with the DUT; and it checks its own invariants, so a drifted model cannot endorse a drifted DUT.
And the stimulus that separates this chapter from every earlier one: the interesting cases are crossings. A reset during the descriptor walk is not covered by testing reset and testing the walk separately, and a second full enumeration is the only stimulus under which stale state can be observed at all.
12. What Comes Next
The flow is complete and its structure is understood. One dimension has been deliberately held back throughout: time.
Every chapter so far has been about order — what must happen before what. Nothing has said how long any of it takes, how long the host waits, or what happens when a step takes longer than it should. Figure 1's vertical axis was explicitly not time.
Chapter 6.7 supplies those numbers, and it is more interesting than a table of durations. Each one exists for a reason that can be derived from something this module has already established, and the timeouts form layers — the host's patience at one level is bounded by its patience at the level above. A device that is merely slow and a device that is broken must eventually become the same thing to a host, and where that line falls is a design decision with consequences for every controller that has to meet it.
Browse the full path on the USB tutorials index.
Continue learning
Related tutorials
- Related topic
USB Reset
Reset as an assertion rather than a question: what a bus reset clears, what it deliberately does not, and why a protocol bus reset and an RTL reset are different mechanisms with different scopes.
- Related topic
Address Assignment
Receiving an address and beginning to answer to it are not the same event. Why the default address exists, where the commit boundary falls, the pending-versus-active register architecture that follows, and three mutation tests — one of which forced a sixth assertion.
- Related topic
Descriptor Discovery
Why the host's first descriptor read is deliberately incomplete, why that partial read is a recurring pattern rather than a workaround, why the walk order is forced, and the gated sequencer RTL that intuition gets wrong.
- Related topic
Configuration Selection
Addressed is not configured. What selecting a configuration switches on, why configuration zero is an un-select rather than a choice, and the RTL consequence of a state entered and left by the same request.
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.
