第4章:标准设备描述符结构解析——每个字节都不是废话
设备描述符,几乎是所有USB设备与主机建立沟通时的第一张名片。它只有18个字节,但每个字节都在告诉主机“我是谁”“我能干什么”“我得按什么规矩来”。
我接手过不少设备枚举失败的案子,十有八九都是设备描述符里埋了雷。比如bcdUSB写错版本号,或者bMaxPacketSize0搞了个0,主机直接不理人。这章我们就从字节0开始,一个一个掰开看。
整体结构一览
标准设备描述符的长度是固定的:18字节,偏移量0x00到0x11。主机在枚举过程中会先发一个“Get Descriptor(Device)”请求,拿到这18字节后就开始解析。所以,你的固件必须保证这18个字节的数据完全正确,任何一个位错位都会导致枚举失败。
我习惯先把它当一张表格背下来,工作时对照着调,效率高很多。
| 偏移量 | 字段名 | 长度(字节) | 功能简述 |
|---|---|---|---|
| 0 | bLength | 1 | 描述符自身长度(固定18) |
| 1 | bDescriptorType | 1 | 描述符类型(设备描述符值为1) |
| 2-3 | bcdUSB | 2 | USB规范版本号(BCD格式) |
| 4 | bDeviceClass | 1 | 设备类代码(0或大于0xE0有特殊含义) |
| 5 | bDeviceSubClass | 1 | 设备子类代码 |
| 6 | bDeviceProtocol | 1 | 设备协议代码 |
| 7 | bMaxPacketSize0 | 1 | 端点0最大包长度 |
| 8-9 | idVendor | 2 | 厂商ID(VID) |
| 10-11 | idProduct | 2 | 产品ID(PID) |
| 12-13 | bcdDevice | 2 | 设备版本号(BCD格式) |
| 14 | iManufacturer | 1 | 厂商字符串描述符索引 |
| 15 | iProduct | 1 | 产品字符串描述符索引 |
| 16 | iSerialNumber | 1 | 序列号字符串描述符索引 |
| 17 | bNumConfigurations | 1 | 支持的配置描述符数量 |
重点字段逐个拆解
bLength —— 18是硬性规定
这个字段必须填18。我曾经遇到过一个实习生把这里填成0,主机连描述符都不往下读,直接返回“设备无法识别”。嗯,这个小坑后来成了我们团队代码审查的必检项。
bDescriptorType —— 类型标识就是1
设备描述符的类型值固定为0x01。主机通过这个字段判断当前正在解析的是不是设备描述符。如果固件误填成其他值(比如配置描述符的0x02),主机会直接扔掉这包数据。
bcdUSB —— 版本号不能乱编
这个字段用BCD编码表示USB规范版本。比如0x0200代表USB 2.0,0x0110代表USB 1.1。要注意高字节是主版本号,低字节是次版本号。我调试过一个奇怪的问题:设备声称支持USB 2.0,但实际硬件只能跑全速,主机端检测HS握手失败就反复复位——最后发现是bcdUSB写成了0x0300(USB 3.0)但硬件根本不是。主机很聪明,它不会只信你填的数字,还会靠电信号检测实际速度。但你填错了会误导主机驱动,导致奇怪的兼容性问题。
个人技巧:如果你在做全速设备(12 Mbps)但希望兼容USB 2.0的驱动,bcdUSB可以填0x0200,但bMaxPacketSize0要保持全速的64字节上限。USB-IF官方建议如实填写,别玩花招。
bDeviceClass / bDeviceSubClass / bDeviceProtocol —— 类定义三兄弟
bDeviceClass决定设备整体类别。如果填0,表示每个接口单独定义类(最常见的情况)。如果填0x02(通信设备类),子类和协议就要按CDC ACM规范来。如果填0xEF,表示混合设备(比如既有HID又有Mass Storage)。
我曾经在做一个多功能设备时,不小心把bDeviceClass填成了0x01(音频类),结果主机只加载了音频驱动,大容量存储接口完全没反应。查找两天才发现是这里错了。
- bDeviceClass = 0:每个接口用自己的类描述符,最灵活
- bDeviceClass > 0xE0:由USB-IF统一分配,个人项目别乱用
- bDeviceSubClass和bDeviceProtocol通常与类代码配合使用,详细定义见USB Class Codes文档
bMaxPacketSize0 —— 端点0的缓冲区大小
这个字段表示控制端点0在一次IN/OUT事务中能传输的最大字节数。低速设备只能是8,全速设备可以是8/16/32/64(实际取决于主机控制器和芯片能力),高速设备必须填64。
我踩过的一个坑:在全速设备里填了128,芯片硬件不支持那么大包,主机发请求后设备没响应,枚举卡死。
idVendor / idProduct —— 就是设备的身份证
VID由USB-IF分配,一个厂商一个ID,不能重复。PID由厂商自己分配,用于区分不同产品。这两个字段在驱动匹配中至关重要。Windows和Linux都靠VID+PID来加载对应驱动。如果你只是做内部工具,可以用特殊的测试VID,但正式产品一定要购买官方VID,否则可能和别的设备冲突。
重点提示:枚举过程中,主机第一次请求设备描述符时只要求前8字节(bLength到bMaxPacketSize0),拿到后根据bMaxPacketSize0重新配置端点0的最大包长度,然后再请求完整18字节。所以你的固件必须能正确响应部分读取。
bcdDevice —— 版本迭代的标记
这个字段通常由厂商自定义,比如0x0100代表第一版,0x0200代表第二版。有些驱动的inf文件会校验这个版本号,不要随意修改。
iManufacturer / iProduct / iSerialNumber —— 字符串索引
这些是字符串描述符的索引值。如果为0表示没有对应的字符串。主机读取设备描述符后,如果需要显示厂商名、产品名或序列号,就会再发Get Descriptor(String)请求获取具体字符串。注意序列号索引如果有值,主机可能用它来区分同一VID/PID的不同设备。
bNumConfigurations —— 你有几个配置表
这个字段告诉主机当前设备有多少个配置描述符。一般普通设备只支持一个配置,填1就行。如果需要支持不同电源模式(自供电/总线供电),可以填多个配置,主机通过Set Configuration切换。
我曾经给一个双模式设备(全速/高速切换)做了两个配置,枚举时主机选了不合适的配置导致功能异常,后来发现是配置号搞反了。所以多配置场景下务必核对配置描述符的bConfigurationValue与设备描述符中bNumConfigurations的关系。
注意事项:主机在读取设备描述符时,一次最多读取8字节的请求是不保证后续字节正确的。你的固件必须能处理这种情况(比如用一个状态机记录当前读取的偏移量)。不要假设主机一定会一次读完18字节。
设备描述符读取流程
下面这张图帮你理清主机和固件之间的交互顺序:
看到没?主机不是一次就拿到全部数据,它分两步走。第一步拿到端点的能力(bMaxPacketSize0),第二步才拿完整描述符。这个设计是为了确保在最大包长度未知时,主机也能安全地读取数据。
代码示例:解析设备描述符
以下是我常用的解析函数(C语言风格),把18字节的原始数据填入结构体:
typedef struct {
uint8_t bLength; // 0: 18
uint8_t bDescriptorType; // 1: 0x01
uint16_t bcdUSB; // 2-3: BCD格式版本
uint8_t bDeviceClass; // 4
uint8_t bDeviceSubClass; // 5
uint8_t bDeviceProtocol; // 6
uint8_t bMaxPacketSize0; // 7
uint16_t idVendor; // 8-9
uint16_t idProduct; // 10-11
uint16_t bcdDevice; // 12-13
uint8_t iManufacturer; // 14
uint8_t iProduct; // 15
uint8_t iSerialNumber; // 16
uint8_t bNumConfigurations; // 17
} device_descriptor_t;
device_descriptor_t* parse_device_desc(uint8_t* raw_data) {
device_descriptor_t* desc = (device_descriptor_t*)raw_data;
// 这里可以加校验
if (desc->bLength != 18) {
// 报错,长度异常
return NULL;
}
if (desc->bDescriptorType != 0x01) {
// 类型不对,可能不是设备描述符
return NULL;
}
return desc;
}
实际项目中,我会在parse函数里额外判断bMaxPacketSize0是否在合理范围内(高速必须64,全速≤64等),防止硬件配置错误。这个小习惯救过我好几次,值得你借鉴。
避坑清单
- bLength不等于18:大部分USB IP核会直接填入固定值,但如果自己手动构建描述符数组,别写错。
- bcdUSB版本号与硬件速度不符:比如用USB 1.1全速硬件却填0x0200,主机可能会尝试高速握手导致设备复位。
- bMaxPacketSize0不要超过硬件缓冲区:全速设备最大64,但有些低端芯片只能支持8或16,查手册确认。
- VID/PID不要写成0或0xFFFF:这两种值是保留的,主机驱动会忽略。
- 字符串索引不为0但实际没有对应字符串:主机去读字符串描述符时返回失败,可能导致枚举耗时过长甚至失败。
设备描述符是整个USB描述符体系的基石,后续所有描述符(配置、接口、端点)都在它的基础上展开。吃透这18个字节,后面就轻松多了。