Skip to content
VLSI Mentor

USB · Module 7

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.

Chapter 7.4 completed the tree. Every node is open, every field connected to what it obliges, and the serving path exists end to end.

String descriptors sit outside that tree, and they are the descriptors most often treated as an afterthought — the human-readable names, not real protocol. That reading produces a specific and very common class of bug, because three things about them are not what an engineer's intuition predicts:

They are referenced by index, not nested. Index zero is not a string at all. And the text is not ASCII — one character occupies two bytes, and there is no terminator.

Each of those is a footnote in a specification and a day of debugging in a lab.

1. Why Strings Are Referenced, Not Nested

Four descriptors so far have carried a string index: Chapter 7.1's iManufacturer, iProduct and iSerialNumber, 7.2's iConfiguration, and 7.3's iInterface. None contained text.

Why not just embed the text? Because strings break every structural assumption the tree relies on.

They are variable-length and unbounded in a way descriptors are not. Every other descriptor has a fixed size — 18, 9, 9, 7 — which is what lets Chapter 7.3 §4's positional walk step through the tree by bLength. A product name can be any length.

They would bloat the configuration tree. Chapter 7.2 established that the whole tree arrives in one transfer on every enumeration. Embedding names would make every device pay that cost on every attach, for data most hosts display rarely and some never read at all.

They are shared. Several descriptors may reference the same string, and an index lets them share one copy.

And they are language-dependent, which is the decisive one. §2 is about what that implies.

So the tree carries indices, and the strings live in separately-requestable descriptors — which is why Chapter 7.2's lookup has entries for strings but not for interfaces or endpoints. Strings are the only descriptors below the device level that a host requests individually.

A diagram showing how descriptors reference strings by index. The device descriptor holds three string indices for manufacturer, product and serial number. The configuration descriptor holds one for its own name, and the interface descriptor holds one for its name. All of these point sideways into a flat set of string descriptors numbered from one, which sit outside the tree rather than nested within it. Separately, string descriptor index zero is not a string at all but a language table listing the language identifiers the device supports, and a host must read it before requesting any real string. The configuration and interface descriptors in this example both hold a zero, which references no string at all — a different meaning of zero from the language table.Device descriptoriManufacturer 1 · iProduct2 · iSerialNumber 3ConfigurationiConfiguration 0 — nostringInterfaceiInterface 0 — no stringString 0language table — NOT astringString 1"VLSI Mentor"Strings 2, 3"Sensor Bridge" · "VM0001"No stringa field of 0 referencesnothingindex 1indices 2, 3index 0index 0read FIRST12
Figure 1 — strings are referenced, not contained. Four descriptors across three levels of the tree point into a flat set of string descriptors, and index 0 is not among them: it is the language table that must be read before any of them can be requested.

2. Index Zero Is a Language Table

String descriptor index 0 is not a string. It is a list of the language IDs the device's strings are available in.

Why this exists. A product name might reasonably differ by language, and USB allows a device to carry several translations. A host asking for a string therefore specifies which string and in which language — and it needs to know which languages exist before it can ask.

That is a bootstrap, and it is the third in this course of study after Chapter 6.4's packet size and Chapter 7.2's total length. The resolution is the same shape: one reserved index answers the question what can I ask for?

The structure is the usual two-byte header followed by an array of 16-bit language identifiers, little-endian like everything else. The example device supports one language:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
04 03 09 04
│  │  └──┬─┘
│  │     └──── wLANGID[0] = 0x0409 — English (United States)   ← LE: 09 04
│  └────────── bDescriptorType = 3 (STRING)
└───────────── bLength = 4  →  (4 − 2) / 2 = one language ID

Note how the count is derived, because this is the structural point: there is no bNumLanguages field. The number of languages is (bLength − 2) / 2. The length is the count, which means a wrong bLength here does not produce a malformed descriptor — it produces a different number of languages, silently.

3. UTF-16LE, and Why "A" Is Four Bytes

String descriptors are not ASCII. The text is UTF-16LE, and there is no null terminator.

Take the shortest possible case. The single character "A":

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
04 03 41 00
│  │  └──┬─┘
│  │     └──── 'A' = U+0041, encoded UTF-16LE → 41 00
│  └────────── bDescriptorType = 3 (STRING)
└───────────── bLength = 4  →  (4 − 2) / 2 = one character

Four bytes for one character. Two of header, two of text. An engineer expecting ASCII predicts three — header plus one byte — or four with a terminator, and both intuitions are wrong in ways that produce different bugs.

Decompose the character itself, because this is Chapter 7.1 §6's little-endian discipline applied to text:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
'A' = U+0041   →   16-bit code unit 0x0041

              ┌───────────┴───────────┐
        low byte 0x41            high byte 0x00
              │                        │
       ROM[offset 2]             ROM[offset 3]
       first on the wire         second on the wire

For ordinary Latin text every second byte is zero, which is what makes the bug so recognisable in a protocol trace: a device sending 56 4D where 56 00 4D 00 was intended is emitting ASCII, and the host will decode it as a single character followed by garbage.

There is no terminator. bLength is the only thing that says where the string ends, so the character count is (bLength − 2) / 2 — the same derivation as the language table, and the same consequence. A string descriptor with a wrong bLength does not fail; it is a different string, truncated or extended into whatever follows it in ROM.

And that means bLength on a string descriptor is load-bearing in a way it is not elsewhere. For an interface descriptor, bLength is 9 and always 9; a wrong value is caught by Chapter 7.2 §8's walk landing in the wrong place. For a string, every value is plausible.

4. The Example Device's Strings

Generated from the same source as every other byte table in this module.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
index 0  (language table)
  04 03 09 04                                    English (United States)

index 1  "VLSI Mentor"   — iManufacturer          bLength = 2 + 2×11 = 24 = 0x18
  18 03 56 00 4C 00 53 00 49 00 20 00 4D 00 65 00 6E 00 74 00 6F 00 72 00
  │  │  V     L     S     I     ␣     M     e     n     t     o     r
  │  └─ type 3 (STRING)
  └──── bLength 0x18 = 24

index 2  "Sensor Bridge" — iProduct               bLength = 2 + 2×13 = 28 = 0x1C
  1C 03 53 00 65 00 6E 00 73 00 6F 00 72 00 20 00 42 00 72 00 69 00 64 00
        67 00 65 00

index 3  "VM0001"        — iSerialNumber          bLength = 2 + 2×6  = 14 = 0x0E
  0E 03 56 00 4D 00 30 00 30 00 30 00 31 00

Check one length by hand, because this is exactly the arithmetic a build-time checker should do rather than a person: "VLSI Mentor" is 11 characters including the space, so 2 + 2 × 11 = 24, and 24 is 0x18. The descriptor's first byte is 18. ✓

Note the space, at offset 12–13 of index 1: 20 00. It is a character like any other and costs two bytes. Counting characters by eye is exactly how these lengths go wrong.

5. Serving Strings — the RTL

Strings are the one part of the descriptor set where index validation genuinely matters, because the index space is large, sparse, and host-supplied.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// usb_string_lookup
//
// Classification: SIMPLIFIED SYNTHESIZABLE TEACHING RTL. It models string
// index validation and location, and nothing else.
//
// WHAT IT MODELS. A host-supplied string index is checked against what the
// device actually implements, and an unimplemented index is REFUSED rather
// than aliased onto some other string (section 7 measures why that matters
// more here than anywhere else in the module). It also models index 0 being
// structurally different -- a language table, not a string.
//
// WHAT IT DOES NOT MODEL. Storage (Chapter 7.1), streaming (Chapter 7.3),
// length limiting (Chapter 7.4), control transfers (Module 13), packets
// (Modules 11-12), or any encoding work: the ROM already holds UTF-16LE
// bytes, and nothing here converts anything.
//
// ON LANGUAGE ID. A real GetDescriptor for a string carries a language ID
// alongside the index. This block checks it for a NONZERO index only,
// because the index-0 request is the one that ASKS which languages exist --
// requiring a valid language ID to discover the language IDs would be the
// same circularity Chapter 6.4 resolved for packet size.
// ─────────────────────────────────────────────────────────────────────────
package usb_string_pkg;
  import usb_lookup_pkg::*;

  // The one language this device's strings are available in.
  localparam logic [15:0] LANGID_EN_US = 16'h0409;

  // Highest implemented string index. Indices run 1..MAX_STRING_INDEX;
  // index 0 is the language table and is handled separately.
  localparam int unsigned MAX_STRING_INDEX = 3;
endpackage

module usb_string_lookup
  import usb_desc_pkg::*, usb_lookup_pkg::*, usb_string_pkg::*;
(
  input  logic [7:0]         str_index,
  input  logic [15:0]        langid,      // from the control request

  output descriptor_lookup_t lookup,
  output logic               bad_langid   // index valid, language is not
);

  always_comb begin
    lookup     = '{valid: 1'b0, base: 16'd0, length: 16'd0};
    bad_langid = 1'b0;

    if (str_index == 8'd0) begin
      // The LANGUAGE TABLE. Deliberately not language-checked: this is the
      // request that tells the host which languages exist.
      lookup = '{valid: 1'b1, base: OFF_STR0, length: LEN_STR0};
    end
    else if (langid != LANGID_EN_US) begin
      // A real string in a language this device does not have. This is NOT
      // the same failure as an unimplemented index, and conflating them
      // costs a host the information it needs to retry sensibly -- so it is
      // reported separately rather than folded into `valid`.
      bad_langid = 1'b1;
    end
    else begin
      // Bounds-checked against what is implemented. An index above the
      // maximum must NOT wrap or clamp: section 7 measures what clamping
      // does, and it is worse here than anywhere else in the module.
      case (str_index)
        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: ;   // unimplemented -- keep the invalid default
      endcase
    end
  end

endmodule

What it models. Index validation, index-0's special status, and language checking for real strings.

Why this block exists. Because the string index is an 8-bit host-supplied value with at most a handful of valid values, and what happens to the other 250-odd decides whether the device is debuggable.

Inputs. The string index and the language ID from the request.

State retained. None — combinational, like Chapter 7.2's lookup and for the same reason.

Outputs. The lookup result, and a separate bad-language indication.

Reset behaviour. None required.

Hardware implied. A small comparator tree and a multiplexer over constants.

Assumptions. That the index and language ID have been extracted from the control request by Module 13; that the ROM holds UTF-16LE bytes already; and that the offsets match Chapter 7.1's image.

Deliberately omits. Storage, streaming, limiting, transfers, packets, and any encoding conversion.

What DV should verify. That index 0 returns the language table regardless of language ID; that every implemented index with the right language returns the right object; that an unimplemented index is refused rather than aliased; that a wrong language ID on a real string is reported as a language failure and not as an invalid index; and that the two failure modes are distinguishable.

The two-output design is the decision worth noting. Index does not exist and language does not exist are different facts, and a block that collapses them into a single valid = 0 throws away the information a host needs to retry in a language the device does have.

6. Static Checks: Lengths and Encoding

String lengths are derived arithmetic, which makes them exactly the kind of thing a build-time check should own.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// Classification: STATIC DATA-INTEGRITY CHECKS (elaboration-time).
//
// String descriptors have no terminator, so bLength is the ONLY thing that
// says where the text ends. Unlike a fixed-size descriptor, every value of
// bLength is structurally plausible -- so these checks are the only thing
// standing between a miscounted character and a silently different string.
//
// SCOPE. This block reads the ROM image and the lookup table's offsets and
// lengths, so it belongs in a scope that has `rom` visible and imports both
// usb_lookup_pkg (for OFF_STR*/LEN_STR*) and usb_string_pkg (for
// MAX_STRING_INDEX). Chapter 7.2's tree walk has the same requirement.
// ─────────────────────────────────────────────────────────────────────────
initial begin : string_checks

  int unsigned base, blen, i;

  // ── The language table at index 0 ────────────────────────────────────
  assert (rom[OFF_STR0 + 1] == DT_STRING)
    else $fatal(1, "string 0 has type 0x%02h, expected 0x03", rom[OFF_STR0 + 1]);

  // L1 -- a language table must be at least one language long. A bLength of
  // 2 is an empty table: structurally valid, and it means the device claims
  // to support NO language while still advertising strings.
  assert (rom[OFF_STR0] >= 8'd4)
    else $fatal(1, {"string 0 bLength is %0d -- a language table must ",
                    "contain at least one language ID"}, rom[OFF_STR0]);

  // L2 -- the table is a whole number of 16-bit language IDs.
  assert (((rom[OFF_STR0] - 2) % 2) == 0)
    else $fatal(1, {"string 0 bLength %0d leaves a half language ID -- ",
                    "(bLength - 2) must be even"}, rom[OFF_STR0]);

  // ── Every real string ────────────────────────────────────────────────
  for (int idx = 1; idx <= MAX_STRING_INDEX; idx++) begin
    case (idx)
      1: begin base = OFF_STR1; blen = LEN_STR1; end
      2: begin base = OFF_STR2; blen = LEN_STR2; end
      3: begin base = OFF_STR3; blen = LEN_STR3; end
      default: begin base = 0; blen = 0; end
    endcase

    // L3 -- the lookup table's length must match the descriptor's own
    // bLength. These are two independent copies of one fact (Chapter 7.2
    // section 9's drift), and this is what stops them diverging.
    assert (rom[base] == blen[7:0])
      else $fatal(1, {"string %0d: descriptor bLength is %0d but the lookup ",
                      "table says %0d"}, idx, rom[base], blen);

    assert (rom[base + 1] == DT_STRING)
      else $fatal(1, "string %0d has type 0x%02h, expected 0x03",
                  idx, rom[base + 1]);

    // L4 -- UTF-16LE means the payload is a whole number of 16-bit code
    // units, so bLength must be EVEN. An odd bLength is a string that was
    // built as if it were ASCII.
    assert ((rom[base] % 2) == 0)
      else $fatal(1, {"string %0d bLength %0d is odd -- UTF-16LE payloads ",
                      "are a whole number of 2-byte code units"},
                  idx, rom[base]);

    // L5 -- a string must contain at least one character. bLength 2 is a
    // header with no text: legal to parse, useless to display, and almost
    // always a generator that produced nothing.
    assert (rom[base] >= 8'd4)
      else $fatal(1, "string %0d is empty (bLength %0d)", idx, rom[base]);

    // L6 -- ENCODING SANITY. For Latin text every second byte is zero. A
    // nonzero high byte is legitimate for non-Latin scripts, so this cannot
    // be an assertion -- but a string whose high bytes are ALL nonzero is
    // almost certainly ASCII that was never converted, and saying so at
    // build time is worth far more than discovering it in a trace.
    begin
      int unsigned nonzero_high;
      nonzero_high = 0;
      for (i = 3; i < rom[base]; i += 2)
        if (rom[base + i] != 8'h00) nonzero_high++;
      if ((nonzero_high * 2) >= (rom[base] - 2))
        $warning({"string %0d: every high byte is nonzero. Correct for a ",
                  "non-Latin script; for Latin text it means the payload ",
                  "is ASCII rather than UTF-16LE"}, idx);
    end
  end

  $display("string descriptors OK: %0d strings + language table",
           MAX_STRING_INDEX);
end

L3 is the one that pays for itself. Chapter 7.2 §9 identified descriptor drift — the same fact written in several places — and a string's length is written in exactly two: the descriptor's own bLength, and the lookup table's length constant. L3 compares them, so they cannot diverge silently.

L6 is deliberately a warning rather than an assertion, and the distinction is the verification judgment worth taking from this section. Every high byte is nonzero is correct for a non-Latin script and almost certainly a bug for Latin text. A check that cannot distinguish those must not fail the build — but it should still speak, because the alternative is discovering unconverted ASCII from a protocol trace.

7. Mutation Test

Three mutations, measured — two of the lookup, one of the data.

S1 — clamp an out-of-range string index

The most tempting "robust" implementation in the whole module.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
default: lookup = '{valid: 1'b1, base: OFF_STR1, length: LEN_STR1};   // MUTANT S1

Result, measured — the lookup probed across every index from 0 to 7:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
            GOLDEN                      S1 (clamping)
  index 0   valid  base  66  len  4     valid  base  66  len  4
  index 1   valid  base  70  len 24     valid  base  70  len 24
  index 2   valid  base  94  len 28     valid  base  94  len 28
  index 3   valid  base 122  len 14     valid  base 122  len 14
  index 4   INVALID                     valid  base  70  len 24   <- string 1
  index 5   INVALID                     valid  base  70  len 24   <- string 1
  index 6   INVALID                     valid  base  70  len 24   <- string 1
  index 7   INVALID                     valid  base  70  len 24   <- string 1

Every implemented index still resolves correctly, so a testbench asking only for strings 0 to 3 reports zero errors. Every unimplemented index returns base 70 — which is string 1, "VLSI Mentor", the manufacturer name.

Why this is worse here than anywhere else in the module. Chapter 7.2 §7's aliasing returned a descriptor of the wrong type, which a careful host detects by checking the type code. This mutant returns a perfectly valid string descriptor — right type, right length, well-formed UTF-16LE. There is nothing for a host to detect.

So the failure is not an error. It is wrong text. A device whose serial number index is off by one displays the manufacturer name as its serial number, on every host, forever, with no diagnostic anywhere. And because it looks like a working device, the bug is usually found by a human reading a dialog box.

S2 — treat index 0 like any other string

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// (the `if (str_index == 8'd0)` branch removed)

Result, measured. Requests for index 0 with a language ID of zero — which is what a host sends before it has learned any — and with an arbitrary language ID both fail. The host therefore never learns which languages the device supports, and never successfully requests any string at all.

Unlike S1, this mutant is caught by an ordinary testbench, because index 0 is a request every host makes. It is included here for the opposite reason to S1: to show that the two failure modes of this block are genuinely different in how they are found, not only in what they mean.

This is Chapter 6.4's circularity, reintroduced by a plausible simplification: requiring a valid language ID in order to discover the valid language IDs. The device's strings become unreachable while every other part of it works perfectly.

S3 — build a string as ASCII

Not an RTL mutation but a data one: replace index 3's UTF-16LE payload with ASCII bytes and halve its bLength accordingly.

Result, measured. L3 passes (the lookup length was regenerated to match), L4 passes — the halved length of 8 is still even — and L5 passes. The build reports string descriptors OK, and alongside it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
WARNING: string 3: every high byte is nonzero. Correct for a non-Latin
         script; for Latin text it means the payload is ASCII rather
         than UTF-16LE

Every structural check passed. The only thing that noticed was the one check that is not structural at all.

And that is precisely why L6 exists as a warning rather than being omitted. The descriptor is structurally impeccable: valid type, self-consistent length, even payload. Only the encoding is wrong, and encoding is not something a length check can see. The host would decode "VM0001" as two or three Chinese characters.

8. Verification

Stimulus. Index 0 with several language IDs including zero; every implemented index with the correct language; every implemented index with a wrong language; indices just above the maximum, at 0xFF, and scattered in between; and each string requested at a short prefix length and at its exact length — Chapter 7.4's equality case, which applies here too.

The stimulus requirement §7 makes non-negotiable: request unimplemented indices. S1 passes completely without them, and S1 is the defect most likely to reach a customer, because it produces working hardware that says the wrong thing.

Observation. The valid flag, the base, and bad_langid — as three separate facts. S2 is only visible if the two failure modes are distinguished, and a testbench that checks "did the request fail" rather than "how did it fail" cannot see it.

Reference model. A function from (index, langid) to an expected result, driven from the same generated source as the ROM. String lengths in particular must be computed by the model rather than written down, or the model reproduces whatever miscount the ROM contains.

Static checks carry the length and encoding load, per §6, including the encoding warning that no runtime check could produce.

Representative coverage — crosses:

  • index 0 × arbitrary language ID, including zero and a wrong one
  • implemented index × correct language × wrong language
  • unimplemented index: maximum + 1, a scattered middle value, 0xFF
  • requested length × string length: shorter, equal, longer
  • string content: Latin text, text containing a space, and a non-Latin string if the device carries one

Negative cases with defined outcomes: an unimplemented index must be refused, not clamped; a wrong language on a valid index must report a language failure rather than an invalid index; and index 0 must succeed regardless of the language ID supplied.

9. Debugging: the Wrong Name

A device works correctly in every respect. On every host, its serial number is displayed as the product name.

What does works correctly in every respect tell you? That enumeration, configuration, endpoints and transfers are all fine. This is not a functional fault at all — which is why it is usually reported by a person rather than by software.

What are the candidate causes? Only three things can produce this. The iSerialNumber index in the device descriptor points at the wrong string. The lookup maps that index to the wrong object. Or the ROM content at that location is the wrong text.

How do you tell them apart in one step? Read the device descriptor's iSerialNumber byte and ask for exactly that index. If the index is 2 where 3 was intended, the bug is in the device descriptor. If the index is 3 and string 3 returns the product name, the bug is in the lookup table or the ROM layout.

What would a protocol analyser show? A successful request and a well-formed string descriptor containing the wrong text. Nothing is malformed, which is why this bug is invisible to every automated check that looks for errors.

What is the one check that would have caught it before the lab? Chapter 7.4 §6's principle applied to strings: a build-time check comparing each string index used in the tree against the string the lookup resolves it to — and comparing the content against the source description the ROM was generated from. Like the endpoint consistency check, it is only meaningful if the two sides are independent.

And why is the manufacturer name the most likely wrong answer? Because §7's S1 clamps to string 1, and string 1 is conventionally iManufacturer. The wrong string is specifically the manufacturer name is nearly diagnostic of a clamping bounds check.

10. Common Misconceptions

11. Reason It Through

A build-time checker reports: string 2 bLength 27 is odd — UTF-16LE payloads are a whole number of 2-byte code units.

What does an odd bLength on a string imply? That the payload is an odd number of bytes, which UTF-16 cannot produce. Either the length was computed wrongly, or the payload is not UTF-16.

Which is more likely, given 27? String 2 is "Sensor Bridge" — 13 characters, so the correct length is 2 + 26 = 28. 27 is 28 minus one, which points at a computation error rather than an encoding one: an encoding error would have produced 2 + 13 = 15.

What would the host have done with 27? Read 25 payload bytes, which is 12 complete code units and one orphan. It would decode twelve characters and either drop or mangle the thirteenth — displaying "Sensor Bridg" and possibly a replacement character.

Why did no other check catch it? Because a string's bLength has no external reference. There is no terminator to disagree with, and there is no tree walk to land in the wrong place — strings are requested individually, so a wrong length simply produces a shorter response.

What is the architectural fix? §4's arithmetic should never be performed by a person. bLength = 2 + 2 × len(text) computed by whatever generates the ROM makes the mistake unrepresentable, and L3's cross-check against the lookup table catches the case where only one of two copies was regenerated.

And the general lesson? A field whose every value is plausible has no self-defence. Fields like that need either a redundant copy to check against or a generator that makes them impossible to get wrong — and preferably both, which is what §6's L3 and L4 provide together.

12. Understanding Check

13. Summary

String descriptors sit outside the tree, referenced by index rather than nested, because they are variable-length, shareable, language-dependent, and would otherwise bloat a configuration tree that is fetched on every enumeration.

Zero means two different things and neither is text. In a descriptor field, 0 means there is no string — so device string indices start at 1. In a request, index 0 returns the language table, a list of 16-bit language IDs whose count is derived as (bLength − 2) / 2 because there is no count field.

The text is UTF-16LE with no terminator. "A" is four bytes — two of header, two of payload, low byte first. For Latin text every second byte is zero, which makes unconverted ASCII recognisable on sight in a trace. And because there is no terminator, bLength alone says where the text ends: for a string every value is plausible, so a wrong one produces a different string rather than a malformed one.

In hardware the block is index validation, and its two-output design matters: index does not exist and language does not exist are different facts a host can act on differently.

§7 measured three defects that all produce working devices. Clamping an out-of-range index returns a perfectly valid descriptor containing the wrong text — nothing for a host to detect, and conventionally the manufacturer name, which makes that specific wrong answer nearly diagnostic. Treating index 0 as an ordinary string reintroduces Chapter 6.4's circularity and makes every string unreachable. And an ASCII payload is structurally impeccable — valid type, even self-consistent length — and fails only an encoding check.

Which is the chapter's distinguishing point: every other defect in this module makes something fail, and failures get investigated. String bugs make something wrong — and the only detector in the entire system is a person noticing that a dialog box says something odd.

14. What Comes Next

One descriptor remains, and it is the newest and the most forward-looking.

Chapter 7.6 opens the BOS descriptor — the Binary Object Store — which exists because USB needed a way to describe capabilities that did not exist when the descriptor format was designed. It is the place where Chapter 7.1 §4's length-then-type convention stops being a parsing convenience and becomes the mechanism that lets a twenty-year-old host coexist with a device carrying capabilities nobody had imagined.

It also revisits wTotalLength in a second, independent tree — with the same bug potential and one new one.

Browse the full path on the USB tutorials index.

Continue learning

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.