第4章:配置描述符详解——结构、字段与多配置支持
配置描述符,这东西在USB枚举里到底扮演什么角色?
简单来说,它就是设备的“自我介绍书”。主机通过它才知道你这设备有多少功能、怎么用。我刚开始做USB驱动时,总觉得这东西可有可无。直到有一次,设备死活枚举失败,查了三天发现是配置描述符里一个字段填错了。嗯,从那以后我再也不敢小看它了。
4.1 配置描述符的整体结构
先看数据格式。每个配置描述符都是固定9字节,但标准规定长度可以更长。为什么?因为后面可以跟其他描述符。
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)
} __attribute__((packed)) usb_config_descriptor;
避坑指南: 千万别把bLength直接写死成9!我见过有人手写描述符,后面跟了接口和端点描述符,但wTotalLength忘了更新。结果主机只读了9个字节,后面全丢了。
4.2 核心字段解读
每个字段都有它的脾气。我们逐个过一遍。
| 字段 | 偏移 | 长度 | 说明 |
|---|---|---|---|
| bLength | 0 | 1字节 | 固定0x09 |
| bDescriptorType | 1 | 1字节 | 固定0x02 |
| wTotalLength | 2 | 2字节 | 小端格式,包括所有子描述符 |
| bNumInterfaces | 4 | 1字节 | 最少1个,最多255个 |
| bConfigurationValue | 5 | 1字节 | 1~255,0表示未配置 |
| iConfiguration | 6 | 1字节 | 0表示无字符串描述符 |
| bmAttributes | 7 | 1字节 | D7=1,D6=自供电,D5=远程唤醒 |
| bMaxPower | 8 | 1字节 | 实际功耗=数值×2mA |
bConfigurationValue 是关键。主机通过SetConfiguration(1)来激活哪个配置。我见过最惨的case是:有人把bConfigurationValue写成0,结果永远无法激活配置。
bmAttributes 里D7必须为1。D6如果是1表示设备自供电,D5为1表示支持远程唤醒。这几个位组合不对,Windows可能会直接报“未知USB设备”。
bMaxPower 单位是2mA。比如你写50,就是100mA。总线供电的设备不能超过500mA,自供电的设备也不能超过这个限制——这是规范规定的。
个人习惯: 我一般把bMaxPower写小一点。比如设备实际需要80mA,我写40(即80mA)。留点余量总是好的。特别是通过Hub供电时,电流限制更严格。
4.3 多配置支持
为什么要支持多个配置?说白了,就是一个设备可以有多种工作模式。
比如一个采集卡设备。配置1是高速模式,配置2是全速模式。主机根据总线速度选哪个。再比如,一个多接口复合设备:配置1启用全部功能,配置2只启用部分功能以省电。
每个配置必须有自己的描述符集合。主机通过GetDescriptor(Configuration, index)来轮询所有配置。Index从0开始,直到主机收到长度小于期望值的描述符——说明没了。
// 双配置示例(伪代码)
static const uint8_t config_desc_1[] = {
// 配置描述符(全功能)
0x09, 0x02, 0x2E, 0x00, // bLength, bType, wTotalLength=46
0x02, // bNumInterfaces=2
0x01, // bConfigurationValue=1
0x00, // iConfiguration=0(无字符串)
0x80, // bmAttributes=总线供电
0x32, // bMaxPower=100mA
// 接口0描述符(音频)
0x09, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00,
// 接口1描述符(HID)
0x09, 0x04, 0x01, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00,
};
static const uint8_t config_desc_2[] = {
// 配置描述符(仅HID,省电模式)
0x09, 0x02, 0x1E, 0x00, // wTotalLength=30
0x01, // bNumInterfaces=1
0x02, // bConfigurationValue=2
0x00,
0x80,
0x19, // bMaxPower=50mA
// 接口0描述符(仅HID)
0x09, 0x04, 0x00, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00,
};
核心原则: 每个配置的bConfigurationValue必须不同。主机用这个值来区分。另外,不同配置的接口号可以重叠,但同一个配置内不能重复。
4.4 枚举流程中的配置选择
主机是怎么选配置的?流程大概是这样的:
- 主机发送GetDescriptor(Configuration, 0),获取配置0的描述符集合
- 读取wTotalLength,判断描述符集合总大小
- 解析接口和端点描述符,加载对应驱动
- 如果设备支持多配置,主机通过索引继续读取其他配置
- 最终主机选择一个配置,发送SetConfiguration(n)
这里有个坑。Windows XP和Windows 7对多配置的支持不太一样。XP倾向于用第一个配置,Windows 7以后会评估各配置的功耗和功能。我建议如果不需要多配置,就别滥用。少即是多。
我曾经踩过的坑: 有一款复合设备,为了兼容不同平台做了4个配置。结果在Linux上主机反复切换配置,导致设备死锁。后来只保留2个配置,问题解决。嗯,多配置真不是越多越好。
现在你应该明白了。配置描述符就是设备的“功能目录”。主机根据它决定怎么用你。字段不能错,结构要对齐,多配置要谨慎。
好了,配置描述符这块就这些。字段要对,结构要对,多配置要谨慎。如果你跟着做,不会出大问题。