4、USB描述符详解:设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符、BOS描述符

描述符这东西,说白了就是USB设备的「身份证」。主机插上你的设备后,第一件事就是挨个儿读这些描述符,搞清楚你是什么设备、该怎么跟它通信。我刚开始做USB开发时,总觉得描述符就是填几个表格,后来踩了不少坑才明白——描述符写错了,设备连枚举都过不去。

嗯,咱们一个一个来看。

4.1 设备描述符(Device Descriptor)

这是主机读到的第一个描述符。它描述了设备的基本信息,比如USB版本、厂商ID、产品ID等。每个USB设备有且只有一个设备描述符。

它的结构长这样:

typedef struct {
    uint8_t  bLength;            // 描述符长度,固定18字节
    uint8_t  bDescriptorType;    // 描述符类型,设备描述符为0x01
    uint16_t bcdUSB;             // USB规范版本号,如0x0200表示USB 2.0
    uint8_t  bDeviceClass;       // 设备类代码
    uint8_t  bDeviceSubClass;    // 设备子类代码
    uint8_t  bDeviceProtocol;    // 设备协议代码
    uint8_t  bMaxPacketSize0;    // 端点0的最大包大小
    uint16_t idVendor;           // 厂商ID(VID)
    uint16_t idProduct;          // 产品ID(PID)
    uint16_t bcdDevice;          // 设备版本号
    uint8_t  iManufacturer;      // 厂商字符串描述符索引
    uint8_t  iProduct;           // 产品字符串描述符索引
    uint8_t  iSerialNumber;      // 序列号字符串描述符索引
    uint8_t  bNumConfigurations; // 支持的配置描述符数量
} usb_device_descriptor_t;

关键点:

  • bcdUSB:我建议你填0x0200或0x0300。别乱填,主机是根据这个来判断设备能力的。
  • bMaxPacketSize0:USB 2.0全速设备固定为64,高速设备也是64。USB 3.0可以到512。
  • idVendor/idProduct:VID需要向USB-IF购买,PID自己分配。我在项目中见过有人随便填了个0x0000,结果主机直接不认。

避坑指南: 我曾经遇到一个设备,枚举时总是失败。查了半天,发现bcdUSB填的是0x0300,但bMaxPacketSize0填了64——USB 3.0要求端点0最大包大小是512。主机一看版本号3.0,包大小却只有64,直接判定描述符无效。

4.2 配置描述符(Configuration Descriptor)

配置描述符描述了一个配置的总体信息,比如供电方式、最大电流等。一个设备可以有多个配置,但同一时刻只能激活一个。我一般只做一个配置,省事儿。

typedef struct {
    uint8_t  bLength;             // 长度,固定9字节
    uint8_t  bDescriptorType;     // 配置描述符类型,0x02
    uint16_t wTotalLength;        // 整个配置的总长度(包括接口、端点等)
    uint8_t  bNumInterfaces;      // 该配置下的接口数量
    uint8_t  bConfigurationValue; // 配置值,用于SetConfiguration命令
    uint8_t  iConfiguration;      // 配置字符串描述符索引
    uint8_t  bmAttributes;        // 配置特性,如自供电、远程唤醒
    uint8_t  bMaxPower;           // 最大电流,单位2mA
} usb_config_descriptor_t;

注意:

  • wTotalLength:这个值必须包含配置描述符本身 + 所有接口描述符 + 所有端点描述符。我见过有人只算了配置描述符的长度,结果主机读不全数据。
  • bMaxPower:单位是2mA。比如填50表示100mA。总线供电设备不要超过500mA(USB 2.0)或900mA(USB 3.0)。
  • bmAttributes:bit6必须为1,表示设备上电。bit5表示自供电,bit4表示远程唤醒。

4.3 接口描述符(Interface Descriptor)

接口描述符描述了一个功能单元。一个配置可以有多个接口,一个接口可以有多个端点。比如一个U盘,通常有一个接口用于Bulk传输,另一个接口用于中断传输。

typedef struct {
    uint8_t  bLength;            // 长度,固定9字节
    uint8_t  bDescriptorType;    // 接口描述符类型,0x04
    uint8_t  bInterfaceNumber;   // 接口编号,从0开始
    uint8_t  bAlternateSetting;  // 备用设置值
    uint8_t  bNumEndpoints;      // 该接口下的端点数量(不包括端点0)
    uint8_t  bInterfaceClass;    // 接口类代码
    uint8_t  bInterfaceSubClass; // 接口子类代码
    uint8_t  bInterfaceProtocol; // 接口协议代码
    uint8_t  iInterface;         // 接口字符串描述符索引
} usb_interface_descriptor_t;

个人经验: 我习惯把bInterfaceNumber从0开始顺序编号。如果你用了备用设置(Alternate Setting),记得bInterfaceNumber相同,但bAlternateSetting不同。主机通过SetInterface来选择使用哪个备用设置。

4.4 端点描述符(Endpoint Descriptor)

端点描述符描述了一个端点的属性,比如传输类型、方向、最大包大小等。除了端点0,每个端点都需要一个描述符。

typedef struct {
    uint8_t  bLength;          // 长度,固定7字节
    uint8_t  bDescriptorType;  // 端点描述符类型,0x05
    uint8_t  bEndpointAddress; // 端点地址,bit7表示方向(0=OUT,1=IN)
    uint8_t  bmAttributes;     // 传输类型:0=控制,1=等时,2=批量,3=中断
    uint16_t wMaxPacketSize;   // 最大包大小
    uint8_t  bInterval;        // 轮询间隔,单位ms(中断端点)或125μs(高速)
} usb_endpoint_descriptor_t;

警告:

  • bEndpointAddress:端点号不能重复。比如你不能有两个端点地址都是0x81(IN端点1)。我见过有人把IN和OUT端点号写成了同一个,结果主机枚举时直接报错。
  • wMaxPacketSize:对于高速批量端点,这个值必须是512的倍数。全速批量端点最大64。等时端点有特殊限制,需要查规范。
  • bInterval:中断端点的轮询间隔。全速设备范围1-255ms,高速设备范围1-16(单位125μs)。我一般全速设10ms,高速设1(即125μs)。

4.5 字符串描述符(String Descriptor)

字符串描述符是可选的,但强烈建议加上。它提供了人类可读的厂商名、产品名、序列号等信息。主机通过索引来请求对应的字符串。

// 字符串描述符结构(可变长度)
typedef struct {
    uint8_t  bLength;         // 长度 = 2 + 字符串长度 * 2
    uint8_t  bDescriptorType; // 字符串描述符类型,0x03
    uint16_t bString[];       // Unicode编码的字符串
} usb_string_descriptor_t;

要点:

  • 字符串使用UTF-16LE编码。每个字符占2字节。
  • 索引0是特殊的语言ID描述符,返回设备支持的语言列表。我一般只支持英语(0x0409)。
  • 字符串长度不能超过255字节(包括头)。

避坑指南: 我曾经写了一个字符串描述符,里面包含了中文。结果在Windows上显示正常,但在Linux上乱码了。后来发现是UTF-16编码方式不对。Windows默认用UTF-16LE,但有些Linux驱动期望UTF-16BE。嗯,稳妥起见,我建议只用ASCII字符。

4.6 BOS描述符(Binary Object Store Descriptor)

BOS描述符是USB 3.0引入的,用于描述设备的高级特性,比如支持USB 3.0、支持LPM(链路电源管理)等。USB 2.0设备也可以有BOS描述符,但很少见。

typedef struct {
    uint8_t  bLength;         // 长度,固定5字节
    uint8_t  bDescriptorType; // BOS描述符类型,0x0F
    uint16_t wTotalLength;    // 整个BOS描述符的总长度
    uint8_t  bNumDeviceCaps;  // 设备能力描述符的数量
} usb_bos_descriptor_t;

BOS描述符后面跟着若干个设备能力描述符(Device Capability Descriptor)。常见的包括:

  • USB 2.0 Extension Descriptor:描述LPM支持情况。
  • SuperSpeed USB Device Capability Descriptor:描述USB 3.0支持情况。
  • Container ID Descriptor:提供一个唯一的128位ID。

注意: BOS描述符不是必须的。如果你的设备只支持USB 2.0,完全可以不实现。但如果你要做USB 3.0设备,BOS描述符是必须的。我建议你至少实现一个Container ID,方便主机识别设备。

4.7 描述符的层次关系

描述符之间是有层次结构的。主机枚举时,先读设备描述符,再读配置描述符,然后根据配置描述符中的信息,依次读取接口描述符和端点描述符。字符串描述符是独立请求的。

用一张图来表示:

设备描述符
  └── 配置描述符
        ├── 接口描述符1
        │     ├── 端点描述符1
        │     └── 端点描述符2
        ├── 接口描述符2
        │     └── 端点描述符3
        └── ...
  └── 字符串描述符(可选)
  └── BOS描述符(可选)

个人习惯: 我写描述符时,会先画一个层次图,确保每个接口下的端点数量正确,每个配置的总长度计算准确。然后才动手写代码。这样能避免很多低级错误。

4.8 常见问题与调试技巧

描述符写错了,设备就枚举失败。我总结几个常见问题:

  • wTotalLength算错了:这是最常见的错误。我建议你写一个函数,自动计算所有描述符的总长度。
  • bNumEndpoints写错了:接口描述符中的bNumEndpoints必须与实际端点描述符数量一致。多了或少了都不行。
  • 端点地址冲突:同一个配置下,不能有两个端点地址相同的描述符。
  • 字符串描述符格式错误:字符串描述符的bLength必须正确,且字符串必须是UTF-16LE编码。

调试建议:

  • 用USB分析仪抓包,看主机请求了什么描述符,设备返回了什么数据。
  • 在Windows上用USBView工具,可以直观地看到描述符的解析结果。
  • 在Linux上用lsusb -v命令,也能看到详细的描述符信息。

嗯,描述符这部分就讲到这里。说白了,描述符就是USB设备的「自我介绍」。写对了,主机就能正确识别你的设备;写错了,设备连门都进不去。我建议你每次写完描述符,先用工具验证一遍,再烧录到硬件上测试。这样能省下不少调试时间。