3. 设备描述符详解:核心字段含义与配置

设备描述符(Device Descriptor)是 USB 枚举过程中主机读取的第一个描述符,它定义了设备的整体属性,包括 USB 规范版本、厂商/产品标识、设备类别以及配置数量等。该描述符位于每个 USB 设备的描述符树根节点,是枚举流程的起点。任何解析错误或字段配置不当,都会直接导致枚举失败或设备无法被正确识别。

关键事实: 设备描述符的长度固定为 18 字节(bLength = 18),主机通过控制传输的默认端点(Endpoint 0)以标准请求 GET_DESCRIPTOR 获取该描述符。Gadget 驱动必须在枚举开始前正确填充该结构体。

3.1 设备描述符结构总览

USB 2.0 规范定义的设备描述符包含 14 个字段,总长度为 18 字节。下表列出了所有字段及其偏移、大小和用途:

偏移 (字节)字段大小 (字节)说明
0bLength1描述符长度,固定为 18
1bDescriptorType1描述符类型,设备描述符固定为 0x01
2bcdUSB2USB 规范版本号(BCD 编码),如 0x0200 表示 USB 2.0
4bDeviceClass1设备类代码(由 USB-IF 分配),0x00 表示在接口描述符中定义
5bDeviceSubClass1设备子类代码,与 bDeviceClass 配合使用
6bDeviceProtocol1设备协议代码,0x00 表示不使用接口类定义的协议
7bMaxPacketSize01端点 0 的最大包大小(字节),USB 2.0 通常为 64
8idVendor2厂商 ID(由 USB-IF 分配),如 0x1D6B 表示 Linux 基金会
10idProduct2产品 ID(厂商自行分配),用于区分同一厂商的不同产品
12bcdDevice2设备版本号(BCD 编码),如 0x0100 表示 1.00 版
14iManufacturer1厂商字符串描述符的索引,0 表示无字符串
15iProduct1产品字符串描述符的索引
16iSerialNumber1序列号字符串描述符的索引
17bNumConfigurations1支持的配置描述符数量,至少为 1
调试提示: 在 Linux 主机端,使用 lsusb -v 可以查看已连接设备的完整描述符树。如果设备描述符解析失败,dmesg 通常会输出类似 device descriptor read/64, error -71 的错误信息。

3.2 核心字段深度解析

3.2.1 bLength 与 bDescriptorType

这两个字段是所有 USB 描述符的公共头部。bLength 固定为 18,bDescriptorType 固定为 0x01。主机通过 bDescriptorType 区分当前读取的是设备描述符、配置描述符还是其他类型。如果 Gadget 返回的 bLength 不等于 18,主机将丢弃该描述符并重试枚举。

3.2.2 bcdUSB:USB 规范版本

bcdUSB 使用二进制编码十进制(BCD)格式表示设备遵循的 USB 规范版本。高字节表示主版本号,低字节表示次版本号。常见值如下:

  • 0x0110 — USB 1.1(全速 12 Mbps)
  • 0x0200 — USB 2.0(高速 480 Mbps)
  • 0x0201 — USB 2.0 修订版 1.0(部分控制器使用)
  • 0x0300 — USB 3.0(超高速 5 Gbps)

Gadget 驱动应根据硬件能力正确设置该字段。例如,一个仅支持全速的 Gadget 若设置 bcdUSB = 0x0200,主机可能尝试以高速模式通信,导致传输失败。

3.2.3 idVendor 与 idProduct

这两个字段共同构成设备的唯一标识。idVendor 由 USB 实施者论坛(USB-IF)统一分配,厂商需缴纳年费获得。idProduct 由厂商自行管理,用于区分同一厂商下的不同产品型号。

常见厂商 ID 示例:

  • 0x1D6B — Linux 基金会(用于 Linux Gadget)
  • 0x046D — Logitech
  • 0x8087 — Intel
  • 0x0483 — STMicroelectronics

在 Linux Gadget 驱动中,idVendor 和 idProduct 通常通过模块参数或 configfs 配置。例如:

// 内核模块参数方式
modprobe g_serial vendor=0x1D6B product=0x0104

// configfs 方式
echo "0x1D6B" > /sys/kernel/config/usb_gadget/g1/idVendor
echo "0x0104" > /sys/kernel/config/usb_gadget/g1/idProduct

3.2.4 bcdDevice:设备版本号

bcdDevice 同样采用 BCD 编码,表示设备的固件或硬件版本。该字段用于主机端驱动根据设备版本执行不同的初始化流程。例如,同一产品在固件升级后,bcdDevice 从 0x0100 变为 0x0200,主机驱动可以据此启用新功能或修复已知问题。

常见故障: 如果 bcdDevice 设置不当(例如全速设备误设为 0x0200),主机可能认为设备支持高速,在复位后尝试进行高速握手(Chirp J/K),导致设备无响应。此时 dmesg 会显示 reset high speed USB device number X using ehci 后跟超时错误。

3.3 设备描述符的枚举流程

下图展示了主机获取设备描述符的完整流程,以及 Gadget 端需要响应的关键步骤:

设备描述符枚举流程 主机:复位总线 主机:分配地址 主机:GET_DESCRIPTOR 主机:解析描述符 主机:继续枚举 Gadget:检测复位 Gadget:等待 SET_ADDRESS Gadget:返回 18 字节 Gadget:校验字段 Gadget:进入配置阶段 关键:Gadget 必须在 100ms 内响应 否则主机将超时复位

3.4 Linux 内核中的设备描述符实现

在 Linux Gadget 框架中,设备描述符由 struct usb_device_descriptor 结构体表示,定义在 include/uapi/linux/usb/ch9.h 中:

struct usb_device_descriptor {
    __u8  bLength;            // 固定为 18
    __u8  bDescriptorType;    // 固定为 USB_DT_DEVICE (0x01)
    __le16 bcdUSB;            // USB 规范版本号(小端)
    __u8  bDeviceClass;       // 设备类
    __u8  bDeviceSubClass;    // 设备子类
    __u8  bDeviceProtocol;    // 设备协议
    __u8  bMaxPacketSize0;    // 端点 0 最大包大小
    __le16 idVendor;          // 厂商 ID
    __le16 idProduct;         // 产品 ID
    __le16 bcdDevice;         // 设备版本号
    __u8  iManufacturer;      // 厂商字符串索引
    __u8  iProduct;           // 产品字符串索引
    __u8  iSerialNumber;      // 序列号字符串索引
    __u8  bNumConfigurations; // 配置描述符数量
} __attribute__ ((packed));

Gadget 驱动通常通过 usb_assign_descriptors() 函数将描述符注册到 UDC(USB Device Controller)层。一个典型的初始化示例如下:

static struct usb_device_descriptor gadget_device_desc = {
    .bLength            = sizeof(struct usb_device_descriptor),
    .bDescriptorType    = USB_DT_DEVICE,
    .bcdUSB             = cpu_to_le16(0x0200),  // USB 2.0
    .bDeviceClass       = 0x00,                 // 在接口中定义
    .bDeviceSubClass    = 0x00,
    .bDeviceProtocol    = 0x00,
    .bMaxPacketSize0    = 64,                   // 全速/高速端点 0 最大包
    .idVendor           = cpu_to_le16(0x1D6B),  // Linux 基金会
    .idProduct          = cpu_to_le16(0x0104),  // 自定义产品
    .bcdDevice          = cpu_to_le16(0x0100),  // 版本 1.00
    .iManufacturer      = 0x01,                 // 指向厂商字符串
    .iProduct           = 0x02,                 // 指向产品字符串
    .iSerialNumber      = 0x03,                 // 指向序列号字符串
    .bNumConfigurations = 0x01,                 // 一个配置
};

3.5 常见枚举故障与定位

设备描述符配置错误是枚举失败的常见原因。下表总结了典型故障现象及其根因:

故障现象可能原因定位方法
主机反复复位,dmesg 显示 device descriptor read/64, error -71bMaxPacketSize0 与实际硬件能力不匹配(如全速设备设为 8 而非 64)检查 UDC 驱动支持的端点 0 最大包大小,确保与描述符一致
设备被识别为未知设备(lsusb 显示 ID 0000:0000)idVendor 或 idProduct 为 0,或描述符校验和错误使用 USB 分析仪抓取 GET_DESCRIPTOR 响应,验证原始字节
枚举成功但主机加载错误驱动bDeviceClass/bDeviceSubClass/bDeviceProtocol 组合与预期不符确认类代码是否与接口描述符中的类代码冲突(建议设为 0x00)
高速设备被降级为全速运行bcdUSB 设置为 0x0110 而非 0x0200,或硬件未正确实现高速握手检查 bcdUSB 字段,并验证 UDC 是否支持高速模式
字符串描述符无法读取iManufacturer/iProduct/iSerialNumber 索引指向不存在的字符串描述符确保字符串描述符已注册,且索引值在有效范围内
调试工具推荐: 在 Linux 主机端,使用 usbmon 或 wireshark 抓取 USB 总线数据包,可以精确查看主机发送的 GET_DESCRIPTOR 请求以及 Gadget 返回的原始描述符字节。对于 Gadget 端,CONFIG_USB_GADGET_DEBUG_FILES 内核选项可以导出描述符信息到 debugfs。

3.6 设备描述符字段配置检查清单

在编写或调试 Gadget 驱动时,建议逐项核对以下清单:

  1. bLength — 是否等于 sizeof(struct usb_device_descriptor)(18)?
  2. bDescriptorType — 是否设置为 USB_DT_DEVICE(0x01)?
  3. bcdUSB — 是否与硬件支持的 USB 速度匹配?全速用 0x0110,高速用 0x0200。
  4. bMaxPacketSize0 — 是否与 UDC 硬件实际支持的端点 0 包大小一致?
  5. idVendor — 是否为有效的 USB-IF 分配 ID?测试阶段可使用 Linux 基金会 ID(0x1D6B)。
  6. idProduct — 是否在厂商内部唯一?避免与已有产品冲突。
  7. bcdDevice — 是否反映了当前固件版本?建议从 0x0100 开始递增。
  8. 字符串索引 — iManufacturer、iProduct、iSerialNumber 是否指向已注册的字符串描述符?
  9. bNumConfigurations — 是否与实际注册的配置描述符数量一致?至少为 1。
  10. 字节序 — 所有多字节字段(bcdUSB、idVendor、idProduct、bcdDevice)是否使用了 cpu_to_le16() 转换?
重要提醒: 在 USB 2.0 规范中,设备描述符的 bMaxPacketSize0 字段对于全速设备必须为 8、16、32 或 64;对于高速设备必须为 64。如果设置错误,主机将在第一次控制传输时失败,导致枚举无法继续。这是最常见的低级配置错误之一。