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设备的「自我介绍」。写对了,主机就能正确识别你的设备;写错了,设备连门都进不去。我建议你每次写完描述符,先用工具验证一遍,再烧录到硬件上测试。这样能省下不少调试时间。