USB · Module 7
Configuration Descriptor
Not an object but the header of a tree: why the whole configuration arrives in one transfer, why wTotalLength is the highest-value bug source in USB, and the lookup RTL where an invalid selector must not quietly become a valid one.
Chapter 7.1 established the description as a tree, and read its root. The device descriptor was a comfortable object to start with: fixed size, self-contained, eighteen bytes that mean eighteen bytes.
The configuration descriptor is not like that, and the difference is the whole chapter.
A configuration descriptor is not an object. It is the header of a variable-length tree, and the tree arrives as one contiguous block whose size is declared by a field inside the header.
That single structural fact produces the most expensive class of descriptor bug in USB, generates a genuine bootstrap the host must solve, and explains why Chapter 6.4's short-read pattern had to occur a second time.
1. Why the Tree Arrives in One Piece
Start from the alternative, because the design decision is only visible against it.
The host could fetch each node separately. Request the configuration; learn it has two interfaces; request interface 0; learn it has two endpoints; request each endpoint. For the example device of Chapter 7.1 §3 that is six requests, and each is a complete control transfer.
Instead, requesting a configuration returns the configuration and everything beneath it — its interfaces, their endpoints, and any class-specific descriptors interleaved among them — as one contiguous byte sequence. Chapter 6.4 §2 stated this; here is why it is the right call.
It is fewer transfers, which matters because enumeration happens on every attach and users notice the latency.
It is atomic. Six separate requests could interleave with other activity, and a device whose descriptors are firmware-backed could in principle change between them. One block is one consistent snapshot.
And it removes a whole class of addressing problem. There is no need for a way to name interface 1 of configuration 2, because nothing is ever requested individually. The tree has no addressing scheme because it never needs one.
The cost is paid immediately, and it is the rest of this chapter. A variable-length block has a size the host cannot know in advance — because the size depends on the contents, and the contents are what the host is asking for.
2. The Header, Field by Purpose
The configuration descriptor header is 9 bytes and its type code is 2. Like every descriptor it opens with a length and a type — and here that bLength of 9 describes only the header, not the tree. Holding those two numbers apart is the single most important habit in this chapter.
"How big is the whole tree?" — wTotalLength, two bytes, little-endian. The size of the header plus every subordinate descriptor. §3 is entirely about this field.
"How many interfaces will I find?" — bNumInterfaces. A count the host uses to know when its parse is complete, and a structural claim that must match what the tree actually contains.
"What do I name this configuration when selecting it?" — bConfigurationValue. This is the value Chapter 6.5 sends to make the configuration active. It is not an array index. The host does not select the second configuration; it sends this specific value. And because zero is reserved to mean no configuration — 6.5's un-select — a configuration cannot use zero as its value.
"Is there a human-readable name?" — iConfiguration, a string index, owned by Chapter 7.5.
"What are this configuration's power characteristics?" — bmAttributes and bMaxPower, one byte each, covered in §4.
3. wTotalLength, and Why It Is the Highest-Value Bug Source
Here is the bootstrap, and it is Chapter 6.4 §3's pattern occurring a second time.
The host wants the configuration tree. To request it, it must state how many bytes to ask for. The size is in wTotalLength, which is inside the tree it is requesting.
The resolution is the same shape as before: the host asks for a short prefix — enough to contain the nine-byte header — reads wTotalLength from it, then re-requests the tree with the correct length. Two requests for the same descriptor with different lengths, which Chapter 6.4 §9 noted is the signature of this pattern working rather than of a retry.
Now the consequence. The host has just been told, by the device, how many bytes to read and how far the tree extends. It has no independent way to check. Whatever this field says is what the host believes.
What goes wrong when it is too small
Suppose the tree is genuinely 48 bytes but the header declares 32.
The host requests 32 bytes and parses them. It walks the header, interface 0, both its endpoints — and reaches offset 32, which it believes is the end. Interface 1 and its endpoint are never read. They exist in the device, they are perfectly well-formed, and the host has no idea they are there.
What does the user see? Not an error. The device enumerates, a configuration is selected, and one of its two functions is simply absent. A device that should present two interfaces presents one. If the missing interface was the one the user cared about, the device appears broken in a way that produces no diagnostic anywhere.
That is what makes this the highest-value bug in the module. It does not fail; it silently truncates — and notice why 32 is a plausible wrong value rather than a contrived one: 9 + 9 + 7 + 7 is exactly 32, so the truncated tree ends on a descriptor boundary and is perfectly well-formed. §8 measures which checks catch this and, more instructively, which do not.
What goes wrong when it is too large
Suppose the tree is 48 bytes and the header declares 64.
The host asks for 64. The device has 48. What it sends for the remaining 16 depends on its implementation — and Chapter 7.4's length limiter is the block whose job is making sure the answer is nothing rather than whatever is adjacent in the ROM.
The host then tries to parse past the real end of the tree, finds a bLength of zero or garbage, and typically rejects the configuration outright. This failure is at least loud — a rejected configuration produces an error. The too-small case is worse precisely because it does not.
4. Power, Briefly and Carefully
Two bytes of the header describe power. Module 19 owns power management; what belongs here is what the bytes mean, because they are a contract like everything else.
bmAttributes is a bit field. Bit 7 is reserved and must be set to 1 — a legacy requirement, and a device that leaves it clear is malformed in a way some hosts reject. Bit 6 indicates self-powered, bit 5 indicates the device supports remote wakeup.
bMaxPower states how much bus current this configuration draws — and it is not in milliamps. On USB 2.0 and below the unit is 2 mA, so the example device's value of 50 means 100 mA. On SuperSpeed the unit is 8 mA instead, so the same byte means something four times larger.
5. The Example Device's Configuration Tree
All 48 bytes, so that later chapters can point at individual descriptors within them.
offset bytes descriptor
────────────────────────────────────────────────────────────────────────
0.. 8 09 02 30 00 02 01 00 80 32 CONFIGURATION header
│ │ └──┬──┘ │ │ │ │ └─ bMaxPower = 50 → 100 mA (×2 mA)
│ │ │ │ │ │ └──── bmAttributes = 0x80, bit7 must-be-one,
│ │ │ │ │ │ bus-powered, no wakeup
│ │ │ │ │ └─────── iConfiguration = 0 (no string)
│ │ │ │ └────────── bConfigurationValue = 1 (NOT an index)
│ │ │ └───────────── bNumInterfaces = 2
│ │ └────────────────── wTotalLength = 0x0030 = 48
│ └──────────────────────── bDescriptorType = 2 (CONFIGURATION)
└─────────────────────────── bLength = 9 (the HEADER only)
9..17 09 04 00 00 02 FF 00 00 00 INTERFACE 0 (Chapter 7.3)
18..24 07 05 81 02 40 00 00 ENDPOINT 0x81 bulk IN (Chapter 7.4)
25..31 07 05 01 02 40 00 00 ENDPOINT 0x01 bulk OUT
32..40 09 04 01 00 01 FF 00 00 00 INTERFACE 1
41..47 07 05 82 03 08 00 0A ENDPOINT 0x82 interrupt IN
────────────────────────────────────────────────────────────────────────
48 bytes = wTotalLength ✓Check the arithmetic yourself, because this is exactly the sum a build-time checker should compute: 9 + 9 + 7 + 7 + 9 + 7 = 48. And note wTotalLength is 30 00 in the bytes — little-endian, Chapter 7.1 §6's discipline, and a field where a byte swap would declare 12,288 bytes instead of 48.
6. Descriptor Lookup — the RTL
Chapter 7.1 stored descriptors. Something must now decide which one a request refers to, and this is where an invalid request becomes dangerous.
// ─────────────────────────────────────────────────────────────────────────
// usb_desc_lookup
//
// Classification: SIMPLIFIED SYNTHESIZABLE TEACHING RTL. It models the
// translation from a descriptor SELECTOR to a ROM base and length, and the
// rejection of selectors the device does not implement.
//
// WHAT IT MODELS. (type, index) -> (valid, base, length), with an explicit
// invalid result for anything unsupported. The central rule is that an
// unsupported selector must produce valid=0 and NOT a base address --
// section 7 measures what happens when it silently aliases instead.
//
// WHAT IT DOES NOT MODEL. Storage (Chapter 7.1 owns the ROM), streaming
// (Chapter 7.3), limiting the response to the host's requested length
// (Chapter 7.4), control transfers (Module 13), packets (Modules 11-12),
// or the language-ID rules for string index 0 (Chapter 7.5 owns those --
// strings appear here only as a table range).
//
// COMBINATIONAL BY CHOICE. The lookup is a small constant decode with no
// state, so registering it would add a cycle for nothing. The consumer in
// Chapter 7.3 registers the result when it captures a request.
// ─────────────────────────────────────────────────────────────────────────
package usb_lookup_pkg;
import usb_desc_pkg::*;
// Where each descriptor sits in the Chapter 7.1 ROM image.
localparam logic [15:0] OFF_DEVICE = 16'd0; localparam logic [15:0] LEN_DEVICE = 16'd18;
localparam logic [15:0] OFF_CONFIG = 16'd18; localparam logic [15:0] LEN_CONFIG = 16'd48;
localparam logic [15:0] OFF_STR0 = 16'd66; localparam logic [15:0] LEN_STR0 = 16'd4;
localparam logic [15:0] OFF_STR1 = 16'd70; localparam logic [15:0] LEN_STR1 = 16'd24;
localparam logic [15:0] OFF_STR2 = 16'd94; localparam logic [15:0] LEN_STR2 = 16'd28;
localparam logic [15:0] OFF_STR3 = 16'd122; localparam logic [15:0] LEN_STR3 = 16'd14;
localparam logic [15:0] OFF_BOS = 16'd136; localparam logic [15:0] LEN_BOS = 16'd22;
localparam logic [7:0] N_CONFIGS = 8'd1; // bNumConfigurations
localparam logic [7:0] MAX_STR_IDX = 8'd3; // highest implemented string
typedef struct packed {
logic valid;
logic [15:0] base;
logic [15:0] length;
} descriptor_lookup_t;
endpackage
module usb_desc_lookup
import usb_desc_pkg::*, usb_lookup_pkg::*;
(
// A decoded descriptor selector. Module 13 owns extracting these from a
// control request; they arrive here already separated.
input logic [7:0] desc_type,
input logic [7:0] desc_index,
output descriptor_lookup_t lookup
);
always_comb begin
// THE DEFAULT MATTERS MORE THAN ANY CASE ARM. Anything not explicitly
// recognised below falls through to invalid, with a zero base and zero
// length. Defaulting to a real descriptor -- or to base 0, which IS a
// real descriptor -- is the aliasing bug of section 7.
lookup = '{valid: 1'b0, base: 16'd0, length: 16'd0};
unique case (desc_type)
DT_DEVICE:
// There is exactly one device descriptor. A nonzero index is not a
// request for a second one; it is a malformed request.
if (desc_index == 8'd0)
lookup = '{valid: 1'b1, base: OFF_DEVICE, length: LEN_DEVICE};
DT_CONFIG:
// Here the index IS meaningful: it selects among bNumConfigurations
// configurations, numbered from zero. This device has one, so only
// index 0 is valid -- and index 1 must be rejected rather than
// wrapped onto index 0, which would make the device appear to have
// an unbounded number of identical configurations.
if (desc_index < N_CONFIGS)
lookup = '{valid: 1'b1, base: OFF_CONFIG, length: LEN_CONFIG};
DT_STRING:
// Chapter 7.5 owns what these contain. What matters here is that the
// index is BOUNDS-CHECKED against what is implemented: an
// unimplemented string index must not return some other string.
case (desc_index)
8'd0: lookup = '{valid: 1'b1, base: OFF_STR0, length: LEN_STR0};
8'd1: lookup = '{valid: 1'b1, base: OFF_STR1, length: LEN_STR1};
8'd2: lookup = '{valid: 1'b1, base: OFF_STR2, length: LEN_STR2};
8'd3: lookup = '{valid: 1'b1, base: OFF_STR3, length: LEN_STR3};
default: ; // invalid -- keep the default above
endcase
DT_BOS:
if (desc_index == 8'd0)
lookup = '{valid: 1'b1, base: OFF_BOS, length: LEN_BOS};
// INTERFACE and ENDPOINT descriptors are deliberately absent. They are
// never requested on their own: they arrive inside the configuration
// tree (section 1). A device that served them individually would be
// answering a question no host asks.
default: ; // unsupported type -- keep the default above
endcase
end
endmoduleWhat it models. Selector-to-location translation, and explicit rejection of everything unsupported.
Why this block exists. Because the host can ask for anything, including things this device does not have, and what happens then is a design decision with security and debuggability consequences.
Inputs. A decoded descriptor type and index.
State retained. None — it is combinational, and §6's header explains why.
Outputs. A validity flag, a base address and a length, packed into one struct so they cannot be used separately by mistake.
Reset behaviour. None required; there is no state.
Hardware implied. A small constant decode — comparators and a multiplexer over constants.
Assumptions. That the type and index have already been extracted from the control request, and that the ROM layout constants match Chapter 7.1's image. That second assumption is a drift hazard, and §9 is about it.
Deliberately omits. Storage, streaming, length limiting, control transfers, packets, and string language rules.
What DV should verify. That every implemented selector returns the right base and length; that every unimplemented one returns valid = 0; that no invalid selector produces a nonzero base; that configuration index 1 is rejected rather than wrapped; and that a string index above the implemented maximum does not return another string.
Two decisions carry the teaching:
- The default is assigned first, before the case. Every arm that does not match leaves it in place. Writing the default last, as a
default:arm that assigns a real descriptor, is how aliasing gets introduced by someone trying to make an unsupported request "do something sensible". - Base, length and valid are one struct. A consumer cannot take the base while ignoring the valid flag, because it receives them together. §7's mutation shows what it costs when a consumer can.
7. Mutation Test
C1 — alias an unsupported selector onto descriptor zero
The tempting "robust" implementation: rather than fail, return something.
default: lookup = '{valid: 1'b1, base: OFF_DEVICE, length: LEN_DEVICE};Result, measured against two testbenches:
valid selectors only including invalid selectors
correct lookup OK (0 errors) OK (0 errors)
C1 (aliasing default) OK (0 errors) BROKEN (4 errors)Every valid selector still resolves correctly, so a testbench that only exercises the descriptors the device implements reports zero errors. Ask for a descriptor type the device does not have and the mutant returns a valid device descriptor — the four failures are a device-qualifier request, an interface request, an endpoint request, and pure garbage, each of which now yields eighteen well-formed bytes.
Think about what a host does with that. It requested, say, a device qualifier — a descriptor that says here is how I would behave at my other speed — and received eighteen bytes that are a perfectly well-formed device descriptor. The bytes parse. The type code inside them says 0x01, not what was asked for, so a careful host rejects them. A less careful one does not, and now the host's model of the device contains a descriptor that was never meant to exist.
The general failure is worse than this instance. A lookup that aliases invalid selectors onto valid content is a mechanism for returning arbitrary internal memory in response to malformed requests. In this teaching ROM there is nothing sensitive to leak. In a real controller whose descriptor table shares memory with anything else, there may be.
C2 — drop the bounds check on the configuration index
DT_CONFIG:
lookup = '{valid: 1'b1, base: OFF_CONFIG, length: LEN_CONFIG}; // MUTANT C2Result, measured. Identical shape: zero errors against valid selectors, and failures only on configuration index 1 and index 255. bNumConfigurations says 1, and the device now answers every configuration index with configuration 0. A host enumerating configurations by asking for index 0, 1, 2… would see an unbounded supply of identical configurations.
Why this one is particularly instructive. The mutant is consistent with the device descriptor in the only direction a naive check looks: index 0 works and returns the right thing. The inconsistency is that the device's own bNumConfigurations says there is exactly one, and the lookup does not enforce its own device's claim. The descriptor data and the logic that serves it disagree, which is Chapter 7.1 §11's semantic level appearing inside the device rather than between device and host.
8. Static Structural Checks
§3 argued that wTotalLength is the highest-value bug source. The good news is that it is also one of the cheapest to eliminate — and not at simulation time.
// ─────────────────────────────────────────────────────────────────────────
// Classification: STATIC DATA-INTEGRITY CHECKS (elaboration-time).
//
// These walk the configuration tree in the ROM image and verify that its
// declared structure matches its actual contents. No clock, no stimulus:
// they are facts about constant data, and they fail in every build rather
// than in the builds whose tests happen to read the right bytes.
// ─────────────────────────────────────────────────────────────────────────
initial begin : config_tree_checks
int unsigned total;
int unsigned walk;
int unsigned n_if;
int unsigned n_ep;
// The declared total, reassembled little-endian from the header.
total = {rom[OFF_CONFIG + 3], rom[OFF_CONFIG + 2]};
// T1 -- the header's own bLength describes the HEADER, not the tree.
// Confusing the two is section 2's warning, made checkable.
assert (rom[OFF_CONFIG] == 8'd9)
else $fatal(1, "configuration bLength is %0d, expected 9", rom[OFF_CONFIG]);
assert (rom[OFF_CONFIG + 1] == DT_CONFIG)
else $fatal(1, "configuration bDescriptorType is 0x%02h, expected 0x02",
rom[OFF_CONFIG + 1]);
// T2 -- THE CENTRAL ONE. Walk the tree descriptor by descriptor, stepping
// by each bLength, and confirm the walk lands EXACTLY on the declared
// total. Landing short or long are different bugs, so they are reported
// separately -- "the tree does not match" is not an actionable message.
walk = 0; n_if = 0; n_ep = 0;
while (walk < total) begin
int unsigned blen;
blen = rom[OFF_CONFIG + walk];
// A zero bLength would make this walk loop forever, and is also exactly
// what a host parser would choke on. Catch it here, not in simulation.
assert (blen >= 2)
else $fatal(1, {"descriptor at tree offset %0d has bLength %0d -- ",
"a walk cannot step over it"}, walk, blen);
if (rom[OFF_CONFIG + walk + 1] == DT_INTERFACE) n_if++;
if (rom[OFF_CONFIG + walk + 1] == DT_ENDPOINT) n_ep++;
walk += blen;
end
assert (walk == total)
else $fatal(1, {"configuration tree walk ended at %0d but ",
"wTotalLength declares %0d -- %s"}, walk, total,
walk > total ? "a descriptor overruns the declared end"
: "declared total exceeds the real tree");
// T3 -- the declared interface count must match what the tree contains.
// bNumInterfaces is a claim; this is the only thing that checks it.
assert (n_if == rom[OFF_CONFIG + 4])
else $fatal(1, {"bNumInterfaces says %0d, tree contains %0d ",
"interface descriptors"}, rom[OFF_CONFIG + 4], n_if);
// T4 -- bConfigurationValue must not be zero: zero is reserved to mean
// "no configuration" (Chapter 6.5), so a configuration named zero could
// never be selected -- selecting it would un-configure the device.
assert (rom[OFF_CONFIG + 5] != 8'd0)
else $fatal(1, "bConfigurationValue is 0, which is reserved for un-select");
// T5 -- bit 7 of bmAttributes is a must-be-one. It carries no information
// and some hosts reject a descriptor without it.
assert (rom[OFF_CONFIG + 7][7] == 1'b1)
else $fatal(1, "bmAttributes bit 7 must be set (reserved, must-be-one)");
$display("config tree OK: %0d bytes, %0d interfaces, %0d endpoints",
total, n_if, n_ep);
endAnd notice the error messages. T2 distinguishes a descriptor overruns the declared end from the declared total exceeds the real tree, because those are opposite bugs with opposite fixes. An assertion that fires with "tree mismatch" costs the reader the same investigation every time.
The checks were run against four corrupted images
Predicting which check catches which bug turns out to be harder than it looks, so the tree was deliberately corrupted four ways and the checker run against each. The results are measured.
correct image config tree OK: 48 bytes, 2 interfaces, 3 endpoints
wTotalLength = 32 T3 fires: bNumInterfaces says 2, tree contains 1
(too small) interface descriptors
wTotalLength = 64 T2 fires: walk ended at 76 but wTotalLength declares
(too large) 64 -- a descriptor overruns the declared end
wTotalLength swapped bLength guard fires: descriptor at tree offset 140
(0x3000 = 12288) has bLength 0 -- a walk cannot step over it
bNumInterfaces = 3 T3 fires: bNumInterfaces says 3, tree contains 2
(tree has 2) interface descriptorsBetween them, T2 and T3 make §3's failure mode structurally impossible — T2 in the general case, T3 in the boundary-aligned case that T2 cannot see. Together they are roughly twenty lines, they run at elaboration in every build, and they need no stimulus.
9. One Source of Truth
§6 flagged a hazard and §8's callout named it. It deserves its own section, because it is the architectural lesson of this chapter.
Count where the example device's structure is now written down:
- the ROM image bytes —
wTotalLength,bNumInterfaces, eachbLength - the lookup table's
LEN_CONFIGconstant - any verification reference model's expected byte array
- the firmware's view, if descriptors are firmware-backed
- the documentation
Five places, one fact. Change the device to add an endpoint and every one of them must change together. Miss one and the failure is §3's silent truncation, or a lookup that returns a length that no longer matches the object.
This is descriptor drift, and it is a leading cause of descriptor bugs in real products — not because engineers are careless, but because the copies live in different files, owned by different people, changed at different times.
The approaches that work all reduce the number of independent copies:
Generate them. One source description — a small table, a configuration file — from which the ROM image, the lookup constants and the reference model are all emitted. Totals and offsets are computed rather than typed, so they cannot disagree. This is the approach the example device's byte tables in this module use.
Derive at elaboration. Compute the lookup constants from the ROM image itself rather than declaring them separately, so there is one copy in the source.
Check at build time. Where copies must exist, make a static check compare them — §8's T2 is this applied to one relationship.
What does not work is a comment saying these must be kept in sync. The person who edits the ROM is not necessarily the person who read the comment, which is Chapter 6.7 §6's conclusion about load-bearing parameter relationships, reached from a different direction.
10. Verification
This chapter's descriptor is the first whose correctness is genuinely structural, so the plan looks different from Chapter 7.1's.
Stimulus. Every implemented selector; every unimplemented one that a host might plausibly send — unsupported types, a configuration index equal to and above bNumConfigurations, a nonzero device-descriptor index, a string index above the maximum; a request for the configuration at its short-prefix length and at its full length; and back-to-back requests for different descriptors.
The stimulus requirement §7 makes non-negotiable: invalid selectors must be generated deliberately. Both mutations pass completely against a test that only asks for descriptors the device implements, and that is the test most people write, because it is the test that mirrors what a working host does.
Observation. The valid flag together with the base and length — not the bytes alone. C1 returns perfectly good bytes; what is wrong is that it returned them at all.
Reference model. For this chapter the model is small: a function from (type, index) to an expected valid/base/length, driven from the same generated source as §9 recommends rather than typed independently.
Static checks carry most of the structural load, per §8. The DV plan should treat them as part of the verification, not as a separate concern — they are checks, they fail builds, and they cover a class of bug that simulation covers badly.
Representative coverage — crosses:
- descriptor type × valid index × invalid index, with invalid exercised for every type
- configuration index below, equal to, and above
bNumConfigurations - requested length: short prefix, exact total, and greater than total
- back-to-back requests for different types, to catch selector state that should not exist
- declared
wTotalLengthequal to, less than, and greater than the real tree, as a static-checker test
Negative cases with defined outcomes: an unsupported type must yield valid = 0 and a zero base; a configuration index of 1 on a single-configuration device must be rejected, not wrapped; and a tree whose descriptors do not sum to wTotalLength must fail the build rather than the simulation.
11. Debugging: the Interface That Disappeared
A device presents two interfaces. On the host, only the first appears. The device descriptor reads correctly, the configuration is selected successfully, and the interface that does appear works perfectly.
What does the first interface works eliminate? Essentially the entire transport path. The lookup found the configuration, streaming worked, and the bytes that arrived were parsed successfully. Nothing about reading descriptors is broken.
What does the second interface is absent, with no error point at? Truncation. The host stopped parsing before reaching it, and stopped believing it had finished — because a host that ran out of data mid-descriptor would report an error, and none was reported.
Which field decides where the host stops? wTotalLength. If it declares fewer bytes than the tree occupies, the host parses exactly as many as it was told to and concludes, correctly by its own reasoning, that the tree ended there.
How would you confirm it in one step? Compare the header's wTotalLength against the sum of the bLength values of every descriptor in the tree. For the example device that sum is 48; a header declaring 32 stops the host precisely after interface 0's second endpoint.
What does a protocol analyser show? That the host requested only the declared number of bytes and the device supplied exactly that. Both sides did what they were told. There is no error to find in the trace, which is why this bug survives trace review.
What would have prevented it? §8's T3 — and specifically not T2, which §8 measured passing on this exact corruption because 32 falls on a descriptor boundary. The count check is what notices that one interface descriptor is present where the header claims two. Both run at build time, with no simulation and no hardware.
And why was it not prevented? Usually §9: the total was maintained by hand in one place while the tree was edited in another.
12. Common Misconceptions
13. Reason It Through
A build-time checker walks the configuration tree and reports: walk ended at 55 but wTotalLength declares 48.
What does ended at 55 mean, given 48 was declared? The walk stepped past the declared end. Summing the bLength fields overshot by seven bytes.
Why seven specifically, and what does that suggest? Seven is an endpoint descriptor's size. The most likely cause is that an endpoint was added to the tree and wTotalLength was not updated — §9's drift, in its most common form.
Could it be something else? Yes, and the alternative matters. A corrupted bLength somewhere in the tree makes the walk step by the wrong amount and land anywhere. The way to distinguish them is to print each descriptor's type and length as the walk proceeds: a clean sequence of 9, 9, 7, 7, 9, 7, 7 says an endpoint was added; an implausible value says a length field is wrong.
What would the host have done with this device? Requested 48 bytes, parsed them, and stopped at offset 48 — mid-way through the final endpoint descriptor, or just before it. The last endpoint would be missing or malformed, and the interface owning it would present fewer endpoints than it declares.
Why is failing the build better than finding this in the lab? Because the lab symptom is one endpoint is missing, which sends an engineer to look at endpoint hardware — which is fine. The build failure names the byte offset and the discrepancy directly, at the moment the mistake was made, by the person who made it.
And what is the architectural fix, as opposed to the immediate one? The immediate fix is to correct the number. The architectural fix is to stop having the number be something a person types: compute wTotalLength from the tree, so adding an endpoint updates it automatically and the checker becomes a guard against a mistake that can no longer be made by hand.
14. Understanding Check
15. Summary
A configuration descriptor is not an object but the header of a tree. Its bLength of 9 describes the header; wTotalLength describes the whole tree, which arrives as one contiguous block containing the configuration's interfaces, their endpoints and anything interleaved among them.
One block rather than many requests buys fewer transfers, atomicity, and no need for any scheme to address individual nodes — which is why the lookup in §6 has no entries for interface or endpoint descriptors at all. The cost is a variable-length block whose size is inside itself, so Chapter 6.4's short-read bootstrap happens a second time.
wTotalLength is the highest-value bug source in USB descriptors. Too large is usually loud. Too small is silent: the host parses exactly what it was told to, stops, and never learns that further interfaces and endpoints exist. The device enumerates, a function is simply absent, and a protocol trace shows both sides behaving correctly.
The header's other fields are contracts too. bConfigurationValue is a name, not an index, and cannot be zero because zero means un-select. bMaxPower is in 2 mA units — 8 mA on SuperSpeed — so 50 means 100 mA, and it is a promise about current draw that a host allocates against.
In hardware, this chapter's block is lookup: selector to base and length, with explicit rejection of everything unimplemented. §7 measured both forgiving alternatives — aliasing unsupported types onto the device descriptor, and dropping the configuration index bound — passing entirely against a testbench that only requests what the device has. Bounds checks are what make the interface honest, not defensive padding.
And the structural correctness belongs at build time — with a caveat §8 measured rather than predicted. A walk that sums each bLength and compares against the declared total catches an over-declared tree, and does not catch the truncation bug at all: wTotalLength = 32 is exactly 9 + 9 + 7 + 7, so the truncated tree ends on a descriptor boundary and is perfectly self-consistent. That boundary alignment is precisely why the bug is silent everywhere else too. What catches it is the count check — one interface where the header claims two. When a structure declares both a size and a count, check both, because the dangerous corruption is the one that keeps the arithmetic right.
Which is the chapter's architectural lesson: the fix for descriptor drift is fewer independent copies, not more care.
16. What Comes Next
The tree's shape is now established, and a build-time walk can prove the shape is self-consistent. But the walk in §8 only counted descriptors and stepped over them — it never looked inside one.
Chapter 7.3 opens the first node beneath the configuration: the interface descriptor, which is where a device stops being one thing and becomes a collection of functions. It also introduces a mechanism with no analogue anywhere else in the tree — alternate settings, where several descriptors share an interface number and only one is active at a time — and the streaming RTL that finally emits these bytes, where a single off-by-one decides whether the last descriptor in the tree arrives intact.
Browse the full path on the USB tutorials index.
Continue learning
Related tutorials
- Related topic
String Descriptor
The descriptors outside the tree: why index zero is not a string, why UTF-16LE makes one character four bytes, why there is no terminator, and why the commonest string bug returns the wrong text instead of an error.
- 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
Device Descriptor
The root of the description tree: what the host infers from each field, why bMaxPacketSize0 carries a bootstrap, why little-endian is three representations that must agree, and the descriptor ROM whose byte-swap bug passes every structural check.
- Related topic
Endpoint Descriptor
Seven bytes that promise hardware: address and direction, transfer type, packet size and interval — the length limiter that bounds every response, and the consistency checker that catches a descriptor the silicon cannot keep.
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.
