USB · Module 7
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.
Chapter 7.3 reached the leaves of the tree. This is the last node, and it is different in kind from the three above it.
The device, configuration and interface descriptors describe structure — what exists, what contains what, what it is called. An endpoint descriptor describes capability, and every field in it is a commitment:
An endpoint descriptor is a promise the device controller must keep. There is an endpoint at this address, in this direction, of this type, able to carry packets of this size. Nothing checks any of it, and the host will begin transferring data on the strength of the claim alone.
This is where Chapter 7.1 §11's third level stops being abstract. A descriptor can be byte-perfect and structurally valid and still describe hardware that does not exist.
Scope note. This chapter owns what the endpoint descriptor says and what implementation must agree with it. Module 9 owns what an endpoint is — its buffering, its data toggle, its halt condition, its microarchitecture. Modules 11 to 13 own the transfers that flow through it.
1. Seven Bytes, Four Promises
The endpoint descriptor is 7 bytes and its type code is 5. Like the interface descriptor it is never requested alone; it arrives inside the configuration tree, positionally owned by the interface descriptor before it.
Four of its fields are promises, and each binds different hardware.
bEndpointAddress — there is an endpoint here, in this direction
One byte carrying two things. Bits 3:0 are the endpoint number — which is why a device has at most 16 endpoint numbers, and why Chapter 7.1's endpoint zero is number 0. Bit 7 is the direction: set means IN (device to host), clear means OUT (host to device). Bits 6:4 are unused.
The direction is part of the address, not a separate property, and that is a design decision with a consequence worth understanding: 0x81 and 0x01 are different endpoints that happen to share the number 1. The example device has both, and they are independent pieces of hardware with independent buffers.
What it commits the controller to: a buffer, a state machine, and the logic to respond when that address is addressed. An advertised endpoint that does not exist means the host sends traffic nothing answers.
bmAttributes — this endpoint behaves this way
Bits 1:0 give the transfer type: 0 control, 1 isochronous, 2 bulk, 3 interrupt. The remaining bits carry synchronisation and usage information for isochronous endpoints, which Module 9 owns.
What it commits the controller to depends on the type, and the differences are large. A bulk endpoint must retry on failure and may be starved of bandwidth indefinitely. An isochronous endpoint gets guaranteed bandwidth and no retry — a lost packet is lost. An interrupt endpoint is polled at a guaranteed rate. Declaring one type and implementing another's behaviour is a defect the descriptor cannot express and no structural check can see.
wMaxPacketSize — packets up to this size will be handled
Two bytes, little-endian, and the field where the promise is most literal. Bits 10:0 are the maximum packet size. For high-speed periodic endpoints, bits 12:11 additionally encode the number of additional transactions per microframe.
What it commits the controller to: a buffer at least this large. A descriptor declaring 64 bytes in front of a 32-byte buffer is a device that will corrupt data or stall the first time the host uses the full size — and the host has every right to, because the device said so.
This is the single most consequential field in the descriptor, and §7's consistency checker exists mainly for it.
bInterval — poll me this often
One byte, meaningful for interrupt and isochronous endpoints, and its encoding depends on the speed — frames at low and full speed, and a power-of-two exponent of microframes at high speed and above. A value copied unchanged between a full-speed and a high-speed version of a product means something entirely different in each.
What it commits the controller to: having data ready at that rate, and to the host reserving bandwidth for it whether or not the device uses it.
2. The Example Device's Endpoint Descriptors
Three endpoints, decoded field by field.
07 05 81 02 40 00 00 ENDPOINT 0x81
│ │ │ │ └──┬──┘ └─ bInterval = 0 (ignored for bulk)
│ │ │ │ └─────── wMaxPacketSize = 0x0040 = 64 ← LE: 40 00
│ │ │ └───────────── bmAttributes = 0x02 → BULK
│ │ └──────────────── bEndpointAddress = 0x81 → number 1, bit7 set = IN
│ └─────────────────── bDescriptorType = 5 (ENDPOINT)
└────────────────────── bLength = 7
07 05 01 02 40 00 00 ENDPOINT 0x01 — number 1, bit7 clear = OUT, bulk, 64
07 05 82 03 08 00 0A ENDPOINT 0x82 — number 2, IN, INTERRUPT, 8 bytes,
bInterval = 10Note 0x81 and 0x01 again. Same endpoint number, opposite directions, two separate endpoints. A controller implementing "endpoint 1" as a single bidirectional resource has misread the descriptor in a way that works until both directions are used at once.
And note 40 00. Chapter 7.1 §6's discipline: 64 is 0x0040, low byte first. Swapped, it reads 0x4000 — which, masked to bits 10:0, is zero. A byte-swapped packet size does not declare a large endpoint; it declares an impossible one.
3. Bounding the Response — the Length Limiter
Before the consistency checking, one more piece of the serving path. Chapter 7.3's streamer took a length and emitted exactly that many bytes — and its contract said the length arrives already limited. This is the block that limits it.
Three numbers constrain how many bytes a descriptor response may contain:
bytes_to_send = min( host requested length,
descriptor's available length )Why the host's request can be smaller is Chapter 6.4: the short prefix read deliberately asks for fewer bytes than the descriptor contains, and the device must honour that rather than sending the whole thing.
Why it can be larger is that the host may ask for more than exists — either deliberately, to say give me everything, or because a wrong wTotalLength told it to.
// ─────────────────────────────────────────────────────────────────────────
// usb_desc_len_limit
//
// Classification: SIMPLIFIED SYNTHESIZABLE TEACHING RTL. It computes the
// bounded response length, and nothing else.
//
// WHAT IT MODELS. The minimum of what the host asked for and what the
// descriptor actually contains, plus the flag that says whether the
// response is SHORTER than the host requested -- which is not the same
// question and is needed by the transfer engine, not by this block.
//
// WHAT IT DOES NOT MODEL. Streaming (Chapter 7.3), lookup (Chapter 7.2),
// storage (Chapter 7.1), control transfers (Module 13 -- in particular,
// what a transfer engine DOES with `is_short` is that module's subject),
// packets (Modules 11-12), or any per-packet segmentation.
//
// COMBINATIONAL BY CHOICE: it is a comparator and a multiplexer, consumed
// in the same cycle the lookup result is captured.
// ─────────────────────────────────────────────────────────────────────────
module usb_desc_len_limit (
// What the host asked for. 16 bits, unsigned, and zero is legal.
input logic [15:0] requested_len,
// What the selected descriptor actually contains, from Chapter 7.2's
// lookup. Zero here means "no descriptor", not "an empty descriptor".
input logic [15:0] available_len,
input logic lookup_valid,
output logic [15:0] send_len,
output logic is_short // response is shorter than requested
);
always_comb begin
if (!lookup_valid) begin
// No descriptor was selected, so there is nothing to bound. Sending
// zero bytes is NOT the right answer to an invalid request -- Chapter
// 7.2 established that an unsupported selector must be refused, and
// refusing is the transfer engine's job (Module 13). This block only
// guarantees it will not hand the streamer a length to run with.
send_len = 16'd0;
is_short = 1'b0;
end else if (requested_len < available_len) begin
// The host asked for less than exists -- the Chapter 6.4 short read.
// Honour it exactly: sending more would overrun the host's buffer.
send_len = requested_len;
is_short = 1'b0; // exactly what was asked for
end else begin
// The host asked for at least everything. Send what exists.
send_len = available_len;
// Strictly less, not less-or-equal: an exact-length response is NOT
// short, and the distinction decides whether a transfer needs an
// explicit terminating signal. Getting this comparison wrong is the
// subtlest bug in this block -- section 8 measures it.
is_short = (available_len < requested_len);
end
end
endmoduleWhat it models. The bounded length, and whether the response falls short of the request.
Why this block exists. Because three independent parties have opinions about how many bytes should flow — the host, the descriptor, and the engine — and exactly one number can be right.
Inputs. The requested length, the available length, and the lookup's validity.
State retained. None.
Outputs. The length to send, and a short-response indication.
Reset behaviour. None required.
Hardware implied. A 16-bit comparator and a multiplexer.
Assumptions. That both lengths are unsigned; that available_len is the true size of the selected descriptor; and that the consumer treats send_len as authoritative.
Deliberately omits. Streaming, lookup, storage, control transfers, packets, and per-packet segmentation.
What DV should verify. Every ordering of requested against available — less, equal, greater — plus zero on each input; that is_short is true only when strictly fewer bytes are sent than requested; and that an invalid lookup yields zero.
4. The Length Relationships, Enumerated
The table this block's verification is built from.
requested | available | send_len | is_short | Why |
|---|---|---|---|---|
| 8 | 18 | 8 | false | Chapter 6.4's short prefix read — the host got what it asked for |
| 18 | 18 | 18 | false | exact — the ambiguous case §3's callout is about |
| 64 | 18 | 18 | true | host said give me everything; the response falls short |
| 0 | 18 | 0 | false | a legal request for nothing |
| 18 | 0 | 0 | true | lookup returned an empty object |
| any | — | 0 | false | lookup invalid — refusal is Module 13's job |
Rows two and three are the whole point. They differ by one in requested and differ in is_short, and an implementation using <= gets row two wrong while getting every other row right.
5. The Assertions
// ─────────────────────────────────────────────────────────────────────────
// Classification: TEACHING ASSERTIONS about the length limiter.
// This block is combinational, so these are immediate assertions rather
// than concurrent properties -- there is no time dimension to reason about,
// and wrapping them in @(posedge clk) would add one artificially.
//
// They are shown here beside the design for readability. In a real tree they
// belong in a checker bound to the module, or inside a synthesis-excluded
// region: an always_comb containing $error is simulation construct, and a
// synthesis tool is entitled to complain about it.
// ─────────────────────────────────────────────────────────────────────────
always_comb begin : len_limit_checks
// L1 -- never send more than the host asked for. Overrunning the host's
// buffer is the failure this block exists to prevent.
assert (send_len <= requested_len)
else $error("send_len %0d exceeds requested %0d", send_len, requested_len);
// L2 -- never send more than the descriptor contains. Sending more reads
// past the object, which in a packed ROM is the next descriptor.
assert (!lookup_valid || (send_len <= available_len))
else $error("send_len %0d exceeds available %0d", send_len, available_len);
// L3 -- send as much as both allow. L1 and L2 together are satisfied by a
// design that always sends zero; this is what forbids that.
assert (!lookup_valid
|| (send_len == ((requested_len < available_len) ? requested_len
: available_len)))
else $error("send_len %0d is not min(%0d, %0d)",
send_len, requested_len, available_len);
// L4 -- is_short means STRICTLY fewer bytes than requested. The equality
// case is the one that separates a correct implementation from a
// plausible one (section 3's callout, section 8's measurement).
assert (is_short == (lookup_valid && (send_len < requested_len)))
else $error("is_short=%0b with send_len %0d, requested %0d",
is_short, send_len, requested_len);
// L5 -- an invalid lookup produces nothing to stream.
assert (lookup_valid || (send_len == 16'd0))
else $error("invalid lookup produced send_len %0d", send_len);
endL3 exists because L1 and L2 are both satisfied by a design that always sends zero. A pair of upper bounds is not a specification — something must require the value to be as large as it is allowed to be. This is the same vacuity lesson Chapter 7.3 §9 measured, arriving from a different direction: there, every property had an event antecedent and a design that did nothing passed them all; here, every property is an inequality and a design that sends nothing passes them all.
6. The Promise and the Silicon
Now the chapter's real subject.
Everything so far checks descriptors against themselves. §1 argued that an endpoint descriptor's fields are claims about hardware, and no amount of internal consistency can validate a claim about something outside the data.
Consider the failure concretely. A descriptor declares endpoint 0x83, bulk IN, 64 bytes. The controller implements endpoints 0x81, 0x01 and 0x82 — there is no 0x83.
What happens? Enumeration succeeds completely. Every byte is correct. Chapter 7.2's tree walk passes, Chapter 7.3's grouping check passes, the host parses the tree, binds a driver, and the driver opens endpoint 0x83 — and gets nothing. The device is silent on an endpoint it advertised.
Where does the blame land? On the endpoint hardware, where a debugging engineer will spend hours, because the descriptor is demonstrably correct and the host is demonstrably doing the right thing. The defect is in a ROM initialiser.
The only way to catch this is to check the descriptor against the design, and that check has to be written deliberately because nothing else in the system has both pieces of information.
// ─────────────────────────────────────────────────────────────────────────
// usb_desc_consistency
//
// Classification: VERIFICATION MODEL. Not synthesizable and not part of any
// device. It is a build-time consistency check between the DESCRIPTOR IMAGE
// and the ENDPOINTS THE DESIGN ACTUALLY IMPLEMENTS.
//
// WHY IT EXISTS. Every other check in this module compares descriptor data
// against itself. This one is the only thing in the module with access to
// BOTH the bytes and the design, which makes it the only thing that can
// check a semantic claim (Chapter 7.1 section 11's third level).
//
// HOW IT IS USED. The implemented-endpoint list must come from the DESIGN
// -- a parameter the controller is instantiated with, or a value elaborated
// from its own generate structure -- NOT from a second hand-written list.
// A check that compares a hand-written list against a hand-written ROM
// verifies that one person typed the same thing twice.
// ─────────────────────────────────────────────────────────────────────────
module usb_desc_consistency #(
// What the CONTROLLER implements. Sourced from the design.
parameter int unsigned N_IMPLEMENTED = 3,
parameter logic [7:0] IMPL_ADDR [0:2] = '{8'h81, 8'h01, 8'h82},
parameter int unsigned IMPL_MAX_PKT [0:2] = '{64, 64, 8 },
parameter logic [1:0] IMPL_XFER_TYPE [0:2] = '{2'd2, 2'd2, 2'd3 }
)(
// The descriptor image, passed in explicitly rather than reached for with
// a hierarchical reference. Making the data dependency a PORT is the point:
// this model consumes two independent sources -- the bytes here, and the
// design's own endpoint list in the parameters above -- and a reader can
// see that both arrive from outside.
input logic [7:0] rom [0:usb_desc_pkg::DESC_ROM_BYTES-1]
);
import usb_desc_pkg::*;
import usb_lookup_pkg::*;
// Bits 1:0 of bmAttributes. Chapter 7.4 section 1.
localparam logic [1:0] XFER_CONTROL = 2'd0;
localparam logic [1:0] XFER_ISOC = 2'd1;
localparam logic [1:0] XFER_BULK = 2'd2;
localparam logic [1:0] XFER_INT = 2'd3;
initial begin : consistency
int unsigned walk, total, found, idx;
logic [7:0] addr, attrs;
logic [15:0] mps;
bit matched;
total = {rom[OFF_CONFIG + 3], rom[OFF_CONFIG + 2]};
walk = rom[OFF_CONFIG];
found = 0;
while (walk < total) begin
int unsigned blen;
blen = rom[OFF_CONFIG + walk];
if (rom[OFF_CONFIG + walk + 1] == DT_ENDPOINT) begin
addr = rom[OFF_CONFIG + walk + 2];
attrs = rom[OFF_CONFIG + walk + 3];
// Little-endian, then masked to bits 10:0 -- the upper bits are the
// high-speed additional-transactions field, not part of the size.
mps = {rom[OFF_CONFIG + walk + 5], rom[OFF_CONFIG + walk + 4]}
& 16'h07FF;
found++;
// ── E1: the advertised endpoint must EXIST in the design ────────
matched = 1'b0;
idx = 0;
for (int i = 0; i < N_IMPLEMENTED; i++)
if (IMPL_ADDR[i] == addr) begin matched = 1'b1; idx = i; end
assert (matched)
else $fatal(1, {"descriptor advertises endpoint 0x%02h but the ",
"design does not implement it"}, addr);
// ── E2: the advertised packet size must FIT the real buffer ─────
// Declaring less than the buffer holds is wasteful but safe.
// Declaring MORE is the promise the hardware cannot keep.
assert (mps <= IMPL_MAX_PKT[idx])
else $fatal(1, {"endpoint 0x%02h advertises wMaxPacketSize %0d ",
"but its buffer is only %0d bytes"},
addr, mps, IMPL_MAX_PKT[idx]);
// ── E3: the advertised transfer type must match the design ──────
assert (attrs[1:0] == IMPL_XFER_TYPE[idx])
else $fatal(1, {"endpoint 0x%02h advertises transfer type %0d but ",
"the design implements type %0d"},
addr, attrs[1:0], IMPL_XFER_TYPE[idx]);
// ── E4: endpoint zero must not appear in a configuration ────────
// The control endpoint is implicit and is described by the device
// descriptor's bMaxPacketSize0, not by an endpoint descriptor.
assert (addr[3:0] != 4'd0)
else $fatal(1, {"endpoint descriptor for endpoint 0 -- the control ",
"endpoint is implicit and must not be described here"});
end
walk += blen;
end
// ── E5: the design must not implement endpoints it never advertises ──
// Not a protocol violation, but a design smell worth failing on: an
// endpoint no descriptor mentions is either dead silicon or a descriptor
// that was never updated. Both are worth knowing about at build time.
assert (found == N_IMPLEMENTED)
else $fatal(1, {"descriptor advertises %0d endpoints but the design ",
"implements %0d"}, found, N_IMPLEMENTED);
$display("descriptor/implementation consistency OK: %0d endpoints", found);
end
endmoduleClassification. Verification model — not synthesizable, not part of any device.
What it models. The four semantic relationships between advertised endpoints and implemented ones: existence, packet size, transfer type, and the reserved status of endpoint zero — plus a count check in the reverse direction.
Why it exists. It is the only thing in this module with access to both the descriptor bytes and the design, so it is the only thing that can check a claim about hardware.
Inputs. The descriptor image as a port, and the implemented-endpoint list as parameters sourced from the design. Both arrive from outside, which is deliberate: the model's entire value is that its two sides are independent, and a port makes that visible where a hierarchical reference would hide it.
What it deliberately does not do. Check anything a structural checker already covers, or run at simulation time — it is elaboration-time, like Chapter 7.2 §8's walk, and for the same reasons.
The header's usage note is the load-bearing part. If the implemented-endpoint list is a second hand-written table, the check verifies that someone typed the same thing twice and will pass a build in which both copies are wrong together. The list must come from the design — a parameter the controller is instantiated with, or a value elaborated from its generate structure — which is Chapter 7.2 §9's one-source-of-truth principle applied to the one check that spans two worlds.
7. Mutation Test
Four mutations, measured. The first two are the length limiter; the last two corrupt the descriptor to test the consistency model.
P1 — use the requested length without limiting it
send_len = requested_len; // MUTANT P1Result. L2 and L3 fire whenever the host asks for more than exists. The streamer then reads past the descriptor, which is Chapter 7.3 §9's overrun arriving from a different cause — and note that the streamer is blameless here: it emitted exactly the length it was given.
Which is the point worth taking. A bounds failure in one block frequently manifests in another. The streamer's own assertions cannot catch this, because from inside the streamer nothing is wrong.
P2 — is_short with <= instead of <
is_short = (available_len <= requested_len); // MUTANT P2Result, measured across eleven cases — §4's table plus additional boundaries. The mutant differs from the correct implementation on exactly two, and both are the same case:
req=18 avail=18 send=18 short=1 (expected 0) <- exact length
req=0 avail=0 send=0 short=1 (expected 0) <- exact length, zero
every other row -- 8/18, 64/18, 0/18, 18/0, 1/18, 17/18, 19/18, 65535/48 --
agrees with the correct implementationThe mutant is wrong precisely when requested == available, and nowhere else. That is the exact-length request — what a host sends after learning a length from a prefix read, which is to say constantly.
Why this is the module's subtlest bug. The response contains the right bytes, in the right order, in the right quantity. A byte-for-byte scoreboard passes. Only a flag is wrong, and it is wrong in the one case a testbench built from round numbers exercises most and checks least.
P3 — advertise an endpoint the design does not implement
Change the third endpoint's address from 0x82 to 0x83, leaving everything else intact.
Result, measured, with all three check layers running:
config tree OK: 48 bytes, 2 interfaces, 3 endpoints <- 7.2's walk PASSES
interface grouping OK <- 7.3's grouping PASSES
FATAL: descriptor advertises endpoint 0x83 but the
design does not implement it <- E1 firesThis is the §6 failure, caught before simulation, by the one check that can see both sides — and note that the two structural layers report the corrupted image as entirely healthy first.
P4 — advertise a packet size larger than the buffer
Change endpoint 0x82's wMaxPacketSize from 8 to 64, leaving the design's 8-byte buffer alone.
Result, measured — the same shape:
config tree OK: 48 bytes, 2 interfaces, 3 endpoints <- PASSES
interface grouping OK <- PASSES
FATAL: endpoint 0x82 advertises wMaxPacketSize 64 but
its buffer is only 8 bytes <- E2 fires8. Verification
This chapter's commit point is the description matches the device — the module's last, and the only one that cannot be checked from descriptor data alone.
Stimulus for the length limiter. §4's table in full — every ordering of requested against available, both zeroes, and an invalid lookup. This is a combinational block with a small input space and there is no excuse for sampling it.
The stimulus requirement §7 makes non-negotiable: include requested == available explicitly. It is the only case that distinguishes P2, and a test suite built from round numbers will produce it by accident constantly while checking it never.
Stimulus for consistency. Descriptor images that advertise a non-existent endpoint, a packet size exceeding the buffer, a mismatched transfer type, an endpoint zero descriptor, and a design implementing an endpoint the descriptor omits. All are build-time cases.
Observation. For the limiter, send_len and is_short — P2 changes only the flag. For consistency, the elaboration result: it passes or it fails the build.
Reference model. The limiter's is one line — min(requested, available) with a strict comparison for the flag — and it is worth writing precisely because it is one line: it costs nothing and it encodes the equality case that the DUT may get wrong.
Three levels, applied to this chapter:
| Level | What to check here | Where |
|---|---|---|
| Byte | emitted bytes match the ROM | 7.3's scoreboard |
| Structural | grouping, counts, lengths | 7.2 and 7.3 static checks |
| Semantic | advertised endpoints exist, fit, and behave as typed | §6's model, at elaboration |
Representative coverage — crosses:
- requested × available: less, equal, greater, with zero on each side
- lookup valid × invalid, crossed with every length relationship
- endpoint address × direction bit, so both
0x81and0x01are exercised as distinct endpoints - transfer type × advertised versus implemented, including a deliberate mismatch
- advertised packet size × buffer size: smaller, equal, larger
Negative cases with defined outcomes: an invalid lookup must yield zero and must not be turned into a zero-length response, which is a different thing and Module 13's to decide; an advertised endpoint with no implementation must fail the build; and an endpoint zero descriptor inside a configuration must fail the build.
9. Debugging: Enumerates Perfectly, Does Not Work
A device enumerates flawlessly on every host. The driver binds. Opening the data endpoint succeeds. Then no data ever arrives, and the host eventually times out.
What does enumerates flawlessly eliminate? The entire descriptor-serving path. Lookup, streaming, lengths and structure are all demonstrably correct — the host received and parsed the tree without complaint, which means every check in Chapters 7.2 and 7.3 would pass.
What does the driver binds and opening the endpoint succeeds tell you? That the host is operating entirely on descriptor information. Binding matches the interface's class triple; opening an endpoint is a host-side allocation based on the endpoint descriptor. Neither step touches the device. Both succeed against a device that does not implement the endpoint at all.
So what is the first thing to check? Not the endpoint hardware. The endpoint address in the descriptor, against the address the controller decodes. §7's P3 produces exactly this signature.
How would you confirm it in one step? A protocol analyser shows the host addressing an endpoint and nothing responding. Compare that address against the addresses the controller actually implements. If the host is polling 0x83 and the controller decodes 0x82, the descriptor is wrong and the hardware is fine.
What is the second candidate? §7's P4 — the size promise. The symptom differs slightly: transfers begin and then fail or corrupt once a packet exceeds the real buffer, rather than never starting.
Why does this class of bug consume so much time? Because every piece of evidence points away from the cause. The descriptor is byte-perfect. The host is behaving correctly. The endpoint hardware is fine in isolation and fails only when addressed the way the descriptor told the host to address it. The bug is in the relationship, and nothing in the system observes relationships — which is precisely why §6's checker has to exist as a deliberate, separate artifact.
10. Common Misconceptions
11. Reason It Through
A consistency check compares the descriptor's endpoint list against a table of implemented endpoints and passes. In the lab, the device advertises an endpoint the controller does not decode.
How can both be true? Only if the table the checker consulted is not the design. If someone maintained a separate list of endpoints we implement alongside the RTL, the checker compared one hand-written list against another hand-written list — and both were written by the same person from the same misunderstanding.
What did the check actually verify? That two copies agree. Which is a real property and a much weaker one than it appears, because the copies have a common origin.
Why is this the same failure as a reference model written from a misunderstanding? Because agreement is only evidence when the things agreeing are independent. Chapter 6.3 §6 made this point about a DV model that shared the DUT's assumption; here the shared origin is a person rather than a specification reading, and the consequence is identical.
What would make the check meaningful? Sourcing the implemented-endpoint list from the design — a parameter the controller is instantiated with, or a value elaborated from the generate structure that creates the endpoint hardware. Then the check spans two genuinely different things, and an endpoint that does not exist cannot appear in the list.
And the general principle? A cross-check is worth exactly as much as the independence of the two things it compares. Before trusting one, ask where each side came from — and if the answer is the same place, the check is a formatting test.
12. Understanding Check
13. Summary
An endpoint descriptor is 7 bytes, type 5, and unlike everything above it in the tree it describes capability rather than structure. Each field is a promise binding different silicon: bEndpointAddress — with direction in bit 7, making 0x81 and 0x01 two distinct endpoints — commits an address decoder and a buffer; bmAttributes commits transfer-type behaviour; wMaxPacketSize commits a buffer at least that large; and bInterval commits a timing obligation whose encoding changes with speed.
Nothing in USB checks any of it. The host reads, believes, allocates and transfers.
The serving path gains its final piece: the length limiter, computing min(requested, available). send_len is easy; is_short is not, because an exact-length response is not short while an over-request is — and §7 measured <= in place of < getting exactly one row of the table wrong, the row a host produces constantly after a prefix read. Its assertions also needed L3 alongside the two bounds, because a pair of upper bounds is satisfied by a design that sends nothing.
And the chapter's real subject: semantic correctness cannot be reached from the data. Two mutations — advertising an endpoint that does not exist, and advertising a packet size larger than the buffer — are byte-perfect and structurally flawless, pass every check from the two preceding chapters, and describe hardware that is not there. Their signature is a device that enumerates perfectly and then does not work, which sends every investigation away from the ROM.
Catching them needs a checker with access to both the bytes and the design — and its value depends entirely on the implemented-endpoint list coming from the design itself. A check comparing two hand-written lists verifies that someone typed the same thing twice. A cross-check is worth exactly the independence of what it compares.
14. What Comes Next
The tree is complete. Every node has been opened, every field connected to what it obliges, and the serving path — store, select, limit, stream — exists end to end with its correctness checked at all three levels.
Two descriptors remain, and both sit outside the tree.
Chapter 7.5 is the one every device has and few implement carefully: string descriptors, referenced by index from the tree rather than nested in it. They are not ASCII, index zero is not a string at all, and the most common bug returns the wrong text rather than an error.
Browse the full path on the USB tutorials index.
Continue learning
Related tutorials
- 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
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.
- 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.
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.
