Skip to content
VLSI Mentor

USB · Module 7

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.

Module 6 ended with a device that is addressed, described and configured — but it treated the description itself as opaque. Chapter 6.4 read descriptors without ever looking inside one, on purpose: the walk was the lesson, and the contents would have been a distraction.

This module opens them.

And it opens with a claim that sounds like an overstatement and is not:

A descriptor is not documentation. It is the interface contract between a device's hardware and every host that will ever talk to it — and a device whose bytes are wrong is indistinguishable, from the host's side, from a device whose hardware is broken.

That equivalence is the reason descriptors deserve a module rather than a table. Perfectly functional silicon can be made unusable by a single wrong byte in a ROM, and §8 shows exactly that failure with a one-byte cause.

1. Why Machine-Readable Description Is Necessary at All

Derive the requirement instead of accepting it.

The host has a device at a known address. It must now decide what to do with it — which driver to load, what resources to allocate, how to talk to it. What are its options?

Assume every device is the same. This is how the interfaces of Module 1 worked: a serial port was a serial port, and software knew what one was. It fails the moment one connector must serve keyboards, storage, cameras, audio devices and things not yet invented — which Chapter 1.6 established as USB's founding requirement.

Identify the device and look it up in a database. Better, and insufficient alone: the host would need prior knowledge of every device ever made, and a device released after the host's software could never work.

Have the device describe itself in a structure the host can parse without prior knowledge. This is the only option that survives contact with a universal bus, and it is what USB does.

Notice what the third option actually requires. The description must be machine-readable, self-delimiting (the host must know where each piece ends without knowing what it is), and extensible — a host from 1998 must not choke on a device from 2026. Those three requirements shape almost every structural decision in this module, and Chapter 7.6 is the one where extensibility becomes the whole subject.

2. The Description Is a Tree

This is the module's central mental model, and every later chapter is a node in it.

A device does not present a flat list of facts. It presents a hierarchy, because the things being described genuinely nest: a device offers configurations, a configuration contains interfaces, an interface owns endpoints.

A hierarchy diagram of USB descriptors. At the top is the device descriptor, which is the root and describes the device as a whole. Below it are one or more configuration descriptors, each describing a complete mode of operation. Below each configuration are one or more interface descriptors, each describing a logical function. Below each interface are its endpoint descriptors, each describing one communication resource. String descriptors sit to the side, referenced by index from several levels rather than nested under any one of them.Deviceidentity, capability, howmany configurationsConfigurationa complete mode ofoperationInterface 0a logical functionInterface 1another functionEndpoints0x81 bulk IN, 0x01 bulk OUTEndpoint0x82 interrupt INStringsreferenced by index, notnestedbNumConfigurationsbNumInterfacesbNumEndpointsbNumEndpointsby index12
Figure 1 — the description is a tree, and each level exists because the level above it can offer more than one of the level below. The plurality is the whole reason the structure is not flat.

Each level exists because the level above can have more than one of it. That is worth stating plainly, because it explains the structure rather than merely describing it. If a device could only ever have one configuration, the configuration level would be redundant. If a configuration could only have one interface, interfaces would be redundant.

And the tree is a contract, not an inventory. Every node is a promise about the implementation: this configuration exists and can be selected; this interface exists and has these endpoints; this endpoint exists and behaves this way. Chapter 7.4 is where that promise becomes a hardware obligation with teeth.

3. The Worked Example Device

One device runs through every chapter of this module. Meet it once here, and every byte you see later belongs to it.

The VM Sensor Bridge. A full-speed USB 2.0 device with one configuration and two interfaces: a data interface with a bulk IN and a bulk OUT endpoint, and a status interface with a single interrupt IN endpoint. Bus-powered, drawing 100 mA.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Device  "VLSI Mentor" / "Sensor Bridge" / serial "VM0001"
└── Configuration 1  — 2 interfaces, bus-powered, 100 mA
    ├── Interface 0  — vendor-specific, 2 endpoints
    │   ├── Endpoint 0x81  bulk IN,  64-byte packets
    │   └── Endpoint 0x01  bulk OUT, 64-byte packets
    └── Interface 1  — vendor-specific, 1 endpoint
        └── Endpoint 0x82  interrupt IN, 8-byte packets, 10 ms interval

4. The Device Descriptor, Field by Purpose

Now the descriptor itself — taught as the host's questions, not as a field list. A table of names teaches nothing; what each field enables the host to do is the content.

The device descriptor is 18 bytes and its type code is 1. Every descriptor begins the same way, and that convention matters more than it looks:

OffsetFieldSizeWhat it is
0bLength1how many bytes this descriptor occupies
1bDescriptorType1what kind of descriptor this is — 1 for device

Those two bytes are the reason the format is parseable at all. A host reading a stream of descriptors it may not all understand can still walk it: read a length, read a type, skip forward by the length, repeat. A descriptor the host has never heard of costs it nothing — it knows how far to jump. That single convention is what makes the format extensible, and Chapter 7.6 is built entirely on it.

The remaining sixteen bytes answer specific host questions.

"What version of the protocol does this device speak?"bcdUSB, two bytes. It tells the host which structural rules apply, and in particular whether to expect the newer descriptors of Chapter 7.6.

"What kind of device is this, broadly?"bDeviceClass, bDeviceSubClass, bDeviceProtocol, one byte each. A device may answer here, or may defer the question to its interfaces by setting the class to zero, which is what the example device does and what most non-trivial devices do. That deferral is meaningful: it says I am not one kind of thing; ask my interfaces, and it is how a single device presents several unrelated functions.

"How large may my control transfers be?"bMaxPacketSize0, one byte. This is the field Chapter 6.4 had to bootstrap around, and it sits at offset 7 — inside the eight bytes that chapter proved are always safe to read. That placement is not luck. It is the specification arranging for its own bootstrap to be solvable.

"Who made it, what is it, and which one is it?"idVendor, idProduct, bcdDevice, two bytes each. Together they are the identity the host matches against its driver database.

"Where are the human-readable names?"iManufacturer, iProduct, iSerialNumber, one byte each. These are indices, not text: the strings live in separate descriptors that Chapter 7.5 owns. Zero means no string, which makes zero an unusable index and is the source of a specific bug that chapter measures.

"How many configurations does it offer?"bNumConfigurations, one byte. This is the value Chapter 6.4 §2 showed forces the walk's order: the host cannot ask for a configuration by number until it knows the range is valid.

5. The Example Device's 18 Bytes

Here is the descriptor the example device actually returns, byte for byte.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
offset:  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17
bytes:  12 01 00 02 00 00 00 40 FF FF 01 00 00 01 01 02 03 01
        │  │  └──┬──┘ │  │  │  │  └─┬──┘ └─┬──┘ └─┬──┘ │  │  │ │
        │  │     │    │  │  │  │    │      │      │    │  │  │ └─ bNumConfigurations = 1
        │  │     │    │  │  │  │    │      │      │    │  │  └─── iSerialNumber = 3
        │  │     │    │  │  │  │    │      │      │    │  └────── iProduct = 2
        │  │     │    │  │  │  │    │      │      │    └───────── iManufacturer = 1
        │  │     │    │  │  │  │    │      │      └────────────── bcdDevice = 0x0100
        │  │     │    │  │  │  │    │      └───────────────────── idProduct = 0x0001
        │  │     │    │  │  │  │    └──────────────────────────── idVendor = 0xFFFF
        │  │     │    │  │  │  └───────────────────────────────── bMaxPacketSize0 = 64
        │  │     │    │  │  └──────────────────────────────────── bDeviceProtocol = 0
        │  │     │    │  └─────────────────────────────────────── bDeviceSubClass = 0
        │  │     │    └────────────────────────────────────────── bDeviceClass = 0 (see interfaces)
        │  │     └─────────────────────────────────────────────── bcdUSB = 0x0200
        │  └───────────────────────────────────────────────────── bDescriptorType = 1 (DEVICE)
        └──────────────────────────────────────────────────────── bLength = 18 (0x12)

Read offsets 2–3 again, because they are where this chapter's bug lives. The value is 0x0200, and the bytes are 00 02low byte first.

6. Little-Endian Is Not a Footnote

Every multi-byte numeric field in a USB descriptor is transmitted least-significant byte first. Saying that is easy; the implementation consequences are where engineers lose days.

Take one concrete field. bcdUSB for a USB 2.0 device has the logical value 0x0200. Decompose it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logical value            0x0200

         ┌──────────────────┴──────────────────┐
   least-significant byte              most-significant byte
         0x00                                0x02
          │                                    │
    first on the wire                   second on the wire
          ▼                                    ▼
   ROM[offset 2] = 0x00                ROM[offset 3] = 0x02

Now the three representations that must agree, because mixing them up is the actual bug:

In a SystemVerilog vector, the value is a number: 16'h0200. Bit 9 is set. Nothing about the vector says anything about byte order — a vector is not bytes until something makes it bytes.

In a byte-addressed ROM, it is two separate entries, and the low byte occupies the lower address. A descriptor ROM initialised from 16-bit constants without an explicit byte split is a descriptor ROM that will be wrong in a way that compiles cleanly.

On the transfer path, the lower address emerges first, because a descriptor is streamed in ascending offset order.

7. Serving the Descriptor — the ROM

The host asks; something must answer. This is the first of the module's implementation blocks, and it is deliberately the simplest one: storage and byte order, nothing else. Chapter 7.2 adds lookup, 7.3 adds streaming.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// usb_desc_rom
//
// Classification: SIMPLIFIED SYNTHESIZABLE TEACHING RTL. It models
// descriptor STORAGE and BYTE ORDER, and nothing else.
//
// WHAT IT MODELS. A byte-addressed read-only descriptor image, and the
// discipline that a multi-byte descriptor field occupies consecutive
// addresses LOW BYTE FIRST (section 6). The helper function exists to make
// that discipline structural rather than a thing the author must remember
// at every initialiser entry.
//
// WHAT IT DOES NOT MODEL. Which descriptor a request selects (Chapter 7.2
// owns lookup), streaming and termination (Chapter 7.3), length limiting
// (Chapter 7.4), control transfers (Module 13), packets (Modules 11-12),
// or any firmware-writable descriptor source (section 9 discusses those
// architectures; this block is the fixed-function case).
//
// SYNTHESIS NOTE. A constant unpacked byte array read with a registered
// output is the shape most tools infer as a ROM. On an FPGA this typically
// becomes block RAM initialised from the bitstream; on an ASIC it is
// usually synthesised logic at this size, and a real design would use a
// compiled ROM macro instead. The registered output is deliberate: a
// combinational read of a large constant array becomes a wide multiplexer
// and will not meet timing at USB rates.
// ─────────────────────────────────────────────────────────────────────────
package usb_desc_pkg;

  // Descriptor type codes. Only the ones this module uses.
  localparam logic [7:0] DT_DEVICE    = 8'h01;
  localparam logic [7:0] DT_CONFIG    = 8'h02;
  localparam logic [7:0] DT_STRING    = 8'h03;
  localparam logic [7:0] DT_INTERFACE = 8'h04;
  localparam logic [7:0] DT_ENDPOINT  = 8'h05;
  localparam logic [7:0] DT_BOS       = 8'h0F;
  localparam logic [7:0] DT_DEV_CAP   = 8'h10;

  localparam int unsigned DESC_ROM_BYTES = 256;

  // The section 6 discipline, as a function rather than as a convention.
  // Every 16-bit descriptor field is written through this, so no initialiser
  // entry can get byte order wrong by being typed carelessly.
  function automatic logic [7:0] le16_lo(input logic [15:0] v);
    return v[7:0];
  endfunction
  function automatic logic [7:0] le16_hi(input logic [15:0] v);
    return v[15:8];
  endfunction

endpackage

module usb_desc_rom
  import usb_desc_pkg::*;
(
  input  logic        clk,
  input  logic        rst_n,
  input  logic        rd_en,
  input  logic [15:0] rd_addr,
  output logic [7:0]  rd_data,
  output logic        rd_valid     // rd_data is valid for the address of the
);                                 // PREVIOUS cycle's rd_en -- 1-cycle latency

  // ── The descriptor image ────────────────────────────────────────────────
  // Offsets here are the example device of section 3.
  logic [7:0] rom [0:DESC_ROM_BYTES-1];

  localparam logic [15:0] BCD_USB     = 16'h0200;
  localparam logic [15:0] ID_VENDOR   = 16'hFFFF;  // NOT a USB-IF assignment
  localparam logic [15:0] ID_PRODUCT  = 16'h0001;
  localparam logic [15:0] BCD_DEVICE  = 16'h0100;

  initial begin
    for (int i = 0; i < DESC_ROM_BYTES; i++) rom[i] = 8'h00;

    // ── DEVICE descriptor, 18 bytes, at offset 0 ────────────────────────
    rom[ 0] = 8'd18;              // bLength
    rom[ 1] = DT_DEVICE;          // bDescriptorType
    rom[ 2] = le16_lo(BCD_USB);   // bcdUSB          -- LOW byte first
    rom[ 3] = le16_hi(BCD_USB);
    rom[ 4] = 8'h00;              // bDeviceClass    -- deferred to interfaces
    rom[ 5] = 8'h00;              // bDeviceSubClass
    rom[ 6] = 8'h00;              // bDeviceProtocol
    rom[ 7] = 8'd64;              // bMaxPacketSize0 -- 8/16/32/64, or 9 on SS
    rom[ 8] = le16_lo(ID_VENDOR);
    rom[ 9] = le16_hi(ID_VENDOR);
    rom[10] = le16_lo(ID_PRODUCT);
    rom[11] = le16_hi(ID_PRODUCT);
    rom[12] = le16_lo(BCD_DEVICE);
    rom[13] = le16_hi(BCD_DEVICE);
    rom[14] = 8'd1;               // iManufacturer
    rom[15] = 8'd2;               // iProduct
    rom[16] = 8'd3;               // iSerialNumber
    rom[17] = 8'd1;               // bNumConfigurations
  end

  // ── Registered read ─────────────────────────────────────────────────────
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      rd_data  <= 8'h00;
      rd_valid <= 1'b0;
    end else begin
      rd_valid <= rd_en;
      // An out-of-range address returns zero rather than wrapping. Wrapping
      // would alias a bad address onto real descriptor content, which is the
      // exact failure Chapter 7.2 is about; returning zero is at least
      // recognisably wrong.
      rd_data  <= (rd_en && (rd_addr < DESC_ROM_BYTES)) ? rom[rd_addr] : 8'h00;
    end
  end

endmodule

What it models. Byte-addressed descriptor storage and the little-endian field discipline.

Why this block exists. Because a descriptor is ultimately bytes at addresses, and the two mistakes that live at this level — wrong byte order and unguarded addressing — are both invisible to every check written about anything else.

Inputs. Clock, reset, a read enable and a byte address.

State retained. The registered output data and its valid flag. The ROM contents are constant.

Outputs. One byte per read, one cycle after the request, with a valid indication.

Reset behaviour. Clears the output register and the valid flag. The ROM contents are not reset — they are constant, and a reset that could alter them would not be a ROM.

Hardware implied. A constant byte array plus an output register: block RAM on an FPGA, synthesised logic or a compiled macro on an ASIC.

Assumptions. That rd_addr is stable when rd_en is asserted, and that the consumer honours the one-cycle latency.

Deliberately omits. Selection, streaming, length limiting, control transfers, packets, and any writable descriptor source.

What DV should verify. That every multi-byte field reads back in low-byte-first order; that bLength at offset 0 equals the descriptor's true size; that bMaxPacketSize0 is one of the legal values; that an out-of-range address returns zero rather than aliasing; and that the valid flag tracks the one-cycle latency exactly.

One decision carries the teaching: the le16_lo/le16_hi helpers. They do nothing a hand-written 8'h00, 8'h02 pair would not — and that is the point. Byte order becomes structural rather than remembered, so no initialiser entry can be wrong by being typed in the intuitive order. §10's mutation shows what that buys.

8. Two Levels of Checking, and Why Both Are Needed

Descriptor correctness splits cleanly into properties a simulator checks over time and properties that are true or false of the data itself, before any clock ticks. Forcing the second kind into temporal assertions is a common and costly mistake.

Static checks — data integrity, no clock involved

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// Classification: STATIC DATA-INTEGRITY CHECKS (elaboration-time).
//
// These are properties of the descriptor IMAGE, not of any behaviour over
// time. They need no clock, no stimulus and no coverage, and they fail at
// elaboration rather than after a test happens to exercise the right path.
// Writing them as temporal SVA would make them strictly weaker: a property
// that only fires when a byte is read is silent about a byte never read.
// ─────────────────────────────────────────────────────────────────────────
initial begin : desc_static_checks

  // S1 -- the declared length must equal the real one. This is the single
  // most valuable descriptor check in existence, and it costs one line.
  assert (rom[0] == 8'd18)
    else $fatal(1, "device descriptor bLength is %0d, expected 18", rom[0]);

  // S2 -- the type code must be the one the host will ask for.
  assert (rom[1] == DT_DEVICE)
    else $fatal(1, "device descriptor type is 0x%02h, expected 0x01", rom[1]);

  // S3 -- bMaxPacketSize0 has an enumerated legal set, not a range.
  // 8/16/32/64 are sizes; 9 is the SuperSpeed exponent form meaning 512.
  assert (rom[7] inside {8'd8, 8'd16, 8'd32, 8'd64, 8'd9})
    else $fatal(1, "bMaxPacketSize0 = %0d is not a legal value", rom[7]);

  // S4 -- a device that offers no configurations cannot be configured, and
  // is therefore a device that can never be used.
  assert (rom[17] >= 8'd1)
    else $fatal(1, "bNumConfigurations is 0 -- device can never be used");

  // S5 -- string INDICES must be plausible. Index 0 is reserved (Chapter
  // 7.5), so a nonzero index here is a claim that a string exists; whether
  // it does is a cross-object check that chapter owns.
  assert (rom[14] != rom[15] || rom[14] == 8'd0)
    else $fatal(1, "iManufacturer and iProduct share a nonzero string index");

end

Temporal checks — behaviour over time

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// Classification: TEACHING ASSERTIONS about the ROM's read interface.
// These genuinely need a clock: they are about timing, not about data.
// ─────────────────────────────────────────────────────────────────────────

// R1 -- the valid flag tracks the read enable by exactly one cycle. A
// consumer that trusts rd_valid must be able to trust its timing.
property p_valid_follows_enable;
  @(posedge clk) disable iff (!rst_n)
    rd_valid == $past(rd_en);
endproperty
assert property (p_valid_follows_enable);

// R2 -- an out-of-range read must not alias onto real content. The value
// returned matters less than the guarantee that it is NOT descriptor data.
property p_no_out_of_range_alias;
  @(posedge clk) disable iff (!rst_n)
    ($past(rd_en) && ($past(rd_addr) >= DESC_ROM_BYTES)) |-> (rd_data == 8'h00);
endproperty
assert property (p_no_out_of_range_alias);

9. Where Descriptors Actually Live

The ROM of §7 is one architecture among several, and a tutorial that presented it as the way would be misleading. The choice is a real engineering decision with real trade-offs.

ArchitectureDescriptors live inStrong whenCosts
Constant RTL / ROMsynthesised constantsfixed-function device, descriptors never changeany change is a respin or rebuild; no per-unit variation
ROM generated at build timeROM image emitted by a build stepdescriptors derive from one source of truthneeds build infrastructure; still fixed after tape-out
Firmware-owned tablesRAM the CPU writesdescriptors vary by product, SKU or runtime statefirmware must be correct and ready before the host asks; ownership races (§9.1)
Hybridhardware streams, firmware supplies contentmost real controllersthe interface between them becomes a contract of its own

No architecture is universally right. A fixed-function peripheral with one configuration has no reason to pay for firmware-backed descriptors. A controller shipped as reusable IP cannot hard-code them, because it does not know what product it will be used in.

And not every byte belongs in the same place. A useful distinction the table hides:

  • Compile-time constants — descriptor types, lengths, structure
  • Build-time values — total lengths, table offsets, anything derived from the others
  • Manufacturing-programmed — serial numbers, per-unit identity, values read from fuses or one-time-programmable memory
  • Runtime-dependent — content that genuinely varies with device state

A serial number that differs per unit cannot be an RTL constant, and treating the entire descriptor image as either fully static or fully dynamic misses that most real devices are both.

9.1 The ownership problem

Firmware-backed descriptors introduce a hazard that fixed ROMs cannot have: the content can change while it is being sent.

The host requests a descriptor. Hardware begins streaming from a buffer. Firmware, running concurrently, writes to that buffer. The host receives a descriptor that is half one version and half another — structurally valid, internally inconsistent, and reproducible only under the timing that caused it.

Rather than prescribe one answer, note that every real solution answers the same question: who owns the buffer while a transfer is in flight? Common approaches are to make descriptors immutable after the device is enabled, to double-buffer with an explicit swap, to have hardware snapshot the metadata (base and length) at request time so a later firmware edit cannot move the target, or to use an explicit ownership handshake.

What must not happen is leaving the question unanswered, because the resulting bug appears rarely, under load, and vanishes when instrumented. Chapter 7.3 returns to this once streaming exists to make it concrete.

10. Mutation Test

Two mutations of the §7 ROM. Both were run against two different check sets, so that what each level of checking does and does not catch is measured rather than asserted.

E1 — byte-swap a 16-bit field

The §6 bug, introduced the way it actually happens: an initialiser written in the intuitive order.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
rom[ 2] = le16_hi(BCD_USB);   // MUTANT E1: high byte first
rom[ 3] = le16_lo(BCD_USB);

Result, measured against two check sets. Against a plan containing byte-level and structural checks only, the mutant reports zero errors — S1 through S5 are silent, because the length is still 18, the type is still 1, bMaxPacketSize0 is untouched and the configuration count is right; R1 and R2 are silent, because the timing and bounds behaviour did not change. The ROM emits the correct number of bytes, in the correct order, from the correct offsets. Only two values are transposed.

Add checks that reassemble each multi-byte field and compare it against its intended value, and the mutant fails immediately:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
byte + structural checks only      OK (0 errors)
with semantic field reassembly     BROKEN
    FAIL bcdUSB reassembles to 0x0200
    FAIL low byte of bcdUSB is at the LOWER offset

What catches it is therefore a check that knows what the field is supposed to mean — reading offsets 2 and 3 back as a little-endian pair and comparing against 0x0200. It is trivially easy to write and is missing from most descriptor testbenches, because it feels redundant next to an initialiser sitting right there in the source.

The measured lesson. This defect is invisible to structural correctness entirely — it lives one level up, in §11's semantic layer. And note what the le16_lo/le16_hi helpers achieve: they do not make the mutation impossible, but they make it visibly wrong at the point of the mistake, because le16_hi on the low byte reads as an error in a way 8'h02 does not.

E2 — let an out-of-range address wrap

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
rd_data <= rd_en ? rom[rd_addr[7:0]] : 8'h00;   // MUTANT E2: index wraps

Result, probed directly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
              GOLDEN (bounds-checked)   E2 (index wraps)
  addr    0        0x12                     0x12
  addr  256        0x00                     0x12   <- bLength, from offset 0
  addr  257        0x00                     0x01   <- bDescriptorType, offset 1
  addr  300        0x00                     0x00

The mutant does not fail loudly. It returns plausible descriptor data for an invalid request — a host would receive a well-formed-looking byte and nothing in the system would report an error. That is the seed of the aliasing failure Chapter 7.2 develops into a far more serious one, where an invalid descriptor selector returns a valid descriptor.

11. The Three Levels of Descriptor Correctness

§8 and §10 have both bumped into the same structure. It is worth naming, because it organises the whole module and it is the framework to bring to any descriptor bug.

LevelQuestionExample failureCaught by
Byteare the emitted bytes the intended bytes?wrong ROM offset; truncated readbyte-level reference model comparison
Structuraldo lengths, types and the hierarchy agree?bLength ≠ real size; total length inconsistentstatic data-integrity checks (§8)
Semanticdoes the description match the implementation?declared 64-byte endpoint 0 in front of a 16-byte buffer; byte-swapped fieldcross-checks against the design itself

The levels are independent, and that is the point. §10's E1 is byte-correct — every byte the ROM was asked for is the byte it emitted — and structurally correct — every length and type is right — and semantically wrong. A verification plan that only checks the first two will pass it, which is exactly what §10 measured.

This framework recurs in every remaining chapter: 7.2's total length is structural, 7.4's advertised-versus-implemented endpoints is semantic, and 7.3's off-by-one is byte-level.

12. Debugging: the First Bad Byte

A host reports that a device failed to enumerate. That message is almost content-free — it is the end of a causal chain, and starting there means guessing.

The discipline is to find the first point at which expected and observed diverge, and then ask what layer that divergence belongs to.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
host reports an enumeration or descriptor failure
        ↓   which descriptor was requested, with what type, index and length?
        ↓   what did the controller decode the request as?
        ↓   which descriptor source did it select — what base and length?
        ↓   which bytes did it actually emit?
        ↓   at which offset do expected and observed first differ?

   classify the divergence:
     value wrong, position right   → data or byte order        (§6, §10)
     position wrong from the start → lookup or base address    (Chapter 7.2)
     right bytes, wrong count      → length or termination     (Chapter 7.3)
     bytes right, host still unhappy → structural or semantic  (§11)

Each tool sees a different part of this chain, and using the wrong one is why descriptor bugs take longer than they should:

  • A protocol analyser shows what bytes actually reached the bus. It is the ground truth for what the device sent and says nothing about why.
  • A controller trace or ILA shows the decoded request, the selected base and length, the read address, the byte counter, and the control state. It is the only view that distinguishes the lookup was wrong from the ROM content was wrong.
  • A firmware log shows what software believed it was providing — essential when descriptors are firmware-backed, and useless when they are in ROM.
  • Host logs show the host's interpretation, which is a conclusion rather than evidence.

The fastest descriptor debugging correlates two layers: what the analyser saw against what the ROM or table contains. A single offset where those disagree localises the bug to one of the three levels above, usually in minutes.

13. Common Misconceptions

14. Reason It Through

A device works perfectly on the engineer's bench. Shipped units are reported as unknown device — they enumerate far enough that the host clearly received a device descriptor, but no driver ever binds. A protocol trace shows all 18 bytes present, with correct bLength and correct bDescriptorType.

What does the host got all 18 bytes with the right header eliminate? Almost the entire byte and structural layer. The lookup found the right object, the length was right, streaming terminated correctly, the type code was right. Whatever is wrong is in the values, not the transport.

What does no driver binds point at? Driver binding keys on identity — idVendor and idProduct. That narrows four bytes out of eighteen.

What would you compare, and against what? The four identity bytes as they appear in the trace against the intended values, as little-endian pairs. If the trace shows 01 00 where 0x0001 was intended, that is correct. If it shows 00 01, the field is byte-swapped.

Why did the bench not catch it? Because the bench had drivers installed that matched, or matched by other means, or the engineer recognised the device by sight and never checked that the host had identified it correctly. It enumerated is not the same as it was identified.

Which of §11's levels is this? Semantic. Every byte was the byte the ROM held, and every structural field was right. The bytes simply meant something other than what was intended.

And what is the one-line prevention? A check that reads each multi-byte field back as a little-endian pair and compares it against the intended constant. It looks redundant next to the initialiser and it is the only thing that catches this class of defect.

15. Understanding Check

16. Summary

A descriptor is the interface contract between a device and every host that will ever talk to it, not documentation. The host reads what a device claims to be and acts on the claim; nothing verifies it. That is why wrong bytes and broken hardware are indistinguishable from outside.

The description is a tree — device, configuration, interface, endpoint — and each level exists because the level above it can have more than one of the level below. Strings sit beside the tree, referenced by index rather than nested.

Every descriptor begins with a length and a type, and that convention is what makes the format extensible: a host can skip a descriptor it has never heard of without understanding it.

The device descriptor is 18 bytes, type 1, and its fields answer specific host questions — protocol version, broad device class (or a deliberate deferral to the interfaces), identity for driver binding, string indices, and how many configurations exist. bMaxPacketSize0 is the field with a bootstrap attached: it is what Chapter 6.4's short first read exists to obtain, it is restricted to 8, 16, 32, 64 or 9-meaning-2⁹, and it is a promise the hardware must keep.

Every multi-byte field is little-endian, and that is three representations which must agree: a vector, a byte array, and a transmission order. §10 measured a byte-swapped field passing every static and temporal check — correct byte count, correct order, correct offsets, correct lengths and types, wrong meaning.

Which produces the module's organising framework: byte, structural and semantic correctness are independent, and a plan covering only the first two is complete-looking and will miss the defect that matters.

Finally, the checks themselves split. Data integrity is static — declared length, legal value sets, type codes — and belongs at elaboration, where it is unconditional. Behaviour is temporal — latency, bounds, termination — and needs a clock. A data property written as SVA inherits a dependency on stimulus it never needed.

17. What Comes Next

The device descriptor is one flat object of a known size. The host read it in two transfers and now knows what the device is and that it offers one configuration.

Chapter 7.2 opens that configuration — and it is a structurally different problem, because a configuration descriptor is not a self-contained object. It is the header of a variable-length tree that arrives as one contiguous block, whose size is declared by a field inside it. That field is the single highest-value bug source in USB descriptors: get it wrong and interfaces and endpoints silently disappear from the host's model of a device that is otherwise perfect.

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.