3. USB描述符与枚举过程:设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符,以及枚举流程分析
好,咱们今天聊点硬核的——USB描述符和枚举过程。
说实话,我刚入行那会儿,觉得枚举就是个黑盒子。插上设备,电脑“叮咚”一声,完事。直到有一次,我调试一个高速U盘,死活枚举不过去,抓包一看,描述符长度写错了。嗯,从那以后,我再也不敢小看这堆结构体了。
3.1 描述符家族:五兄弟各司其职
USB描述符,说白了就是设备的“身份证”和“说明书”。主机不认识你的设备,全靠这堆描述符来了解你。它们是一层套一层的树形结构,我习惯叫它“俄罗斯套娃”。
3.4.1 设备描述符(Device Descriptor)
这是最顶层的描述符,每个USB设备只能有一个。它告诉主机:你是谁家的孩子(VID/PID),用的是什么版本的USB协议,需要多少个配置。
关键字段:
- bLength:固定18字节,别写错了,我见过有人写成17,结果枚举直接挂掉。
- bcdUSB:USB规范版本号,比如0x0200表示USB 2.0,0x0300表示USB 3.0。
- bDeviceClass/bDeviceSubClass/bDeviceProtocol:设备大类、子类、协议。如果设为0,表示在接口描述符里定义。
- idVendor/idProduct:厂商ID和产品ID,这个得去USB-IF申请,别乱用。
- bNumConfigurations:配置数量,通常为1。
// 设备描述符结构体(USB 2.0)
typedef struct {
uint8_t bLength; // 18
uint8_t bDescriptorType; // 0x01
uint16_t bcdUSB; // 0x0200
uint8_t bDeviceClass;
uint8_t bDeviceSubClass;
uint8_t bDeviceProtocol;
uint8_t bMaxPacketSize0; // 端点0最大包长
uint16_t idVendor;
uint16_t idProduct;
uint16_t bcdDevice;
uint8_t iManufacturer; // 厂商字符串索引
uint8_t iProduct; // 产品字符串索引
uint8_t iSerialNumber; // 序列号字符串索引
uint8_t bNumConfigurations;
} usb_device_descriptor_t;
避坑指南:我曾经遇到一个设备,bMaxPacketSize0填了64,但实际硬件只支持8字节。结果主机发64字节的请求,设备直接不响应。记住,这个值必须和硬件能力匹配。
3.4.2 配置描述符(Configuration Descriptor)
一个设备可以有多个配置,但同一时间只能激活一个。配置描述符描述了设备的功耗、供电方式,以及包含多少个接口。
我个人习惯把配置描述符看作“电源方案说明书”。比如,总线供电还是自供电?最大电流多少?这些都在这里定义。
| 字段 | 说明 | 我的经验 |
|---|---|---|
| bConfigurationValue | 配置编号,主机用这个值来选择配置 | 从1开始编号,别用0 |
| bmAttributes | 供电属性,bit6=1表示自供电,bit5=1支持远程唤醒 | 总线供电设备记得把bit7置1 |
| bMaxPower | 最大电流,单位2mA | USB 2.0最大500mA,填250(500/2) |
3.4.3 接口描述符(Interface Descriptor)
接口描述符是配置描述符的孩子。一个配置可以有多个接口,比如一个音频设备可能有音频控制接口和音频数据接口。
你想想看,接口描述符其实就是“功能模块说明书”。它告诉主机:我这个接口是干啥的,用哪个驱动来伺候我。
关键点:
- bInterfaceNumber:接口编号,从0开始。
- bAlternateSetting:备用设置,用于切换接口参数。比如视频设备切换分辨率。
- bNumEndpoints:接口使用的端点数量(不包括端点0)。
- bInterfaceClass/bInterfaceSubClass/bInterfaceProtocol:接口类、子类、协议。比如HID类、大容量存储类。
3.4.4 端点描述符(Endpoint Descriptor)
端点,是USB通信的终点。每个接口可以有多个端点,但端点0是控制端点,不在这里描述。
端点描述符定义了数据传输的“管道参数”:传输方向、传输类型、最大包长、轮询间隔。
// 端点描述符结构体
typedef struct {
uint8_t bLength; // 7
uint8_t bDescriptorType; // 0x05
uint8_t bEndpointAddress; // bit7: 方向(0=OUT,1=IN), bit3-0: 端点号
uint8_t bmAttributes; // 传输类型: 0=控制,1=等时,2=批量,3=中断
uint16_t wMaxPacketSize; // 最大包长
uint8_t bInterval; // 轮询间隔(ms)
} usb_endpoint_descriptor_t;
注意:高速模式下,wMaxPacketSize有特殊编码。比如批量端点最大包长512字节,但等时端点可以到1024字节。我踩过坑——给高速等时端点填了512,结果带宽不够用,视频卡顿。
3.4.5 字符串描述符(String Descriptor)
字符串描述符是可选的,但建议加上。它让设备有个“名字”,方便用户识别。
字符串描述符使用Unicode编码(UTF-16LE)。第一个字节是长度,第二个字节是描述符类型(0x03),后面是Unicode字符串。
// 字符串描述符示例(厂商名)
const uint8_t string_desc[] = {
0x0E, // bLength = 14字节
0x03, // bDescriptorType = 字符串
'M', 0x00, // M
'y', 0x00, // y
'C', 0x00, // C
'o', 0x00, // o
' ', 0x00, // 空格
'I', 0x00, // I
'n', 0x00, // n
'c', 0x00 // c
};
3.2 枚举流程:主机和设备的一次握手
枚举过程,说白了就是主机和设备“相亲”的过程。主机问一句,设备答一句,直到主机觉得“嗯,我了解你了”,然后分配一个地址,加载驱动。
我记得第一次用逻辑分析仪抓枚举过程,看到那串标准请求,才真正理解什么叫“协议是聊出来的”。
3.2.1 枚举的七个步骤
- 设备连接:设备插入,主机检测到D+/D-上的电平变化。
- 复位:主机拉低D+/D-至少10ms,设备进入默认状态。
- 获取设备描述符:主机发请求获取前8字节(只取前8字节,为了知道端点0最大包长)。
- 再次复位:主机根据获取到的bMaxPacketSize0重新配置。
- 设置地址:主机分配一个唯一地址(0x01-0x7F)。
- 获取完整设备描述符:主机再次获取完整的18字节设备描述符。
- 获取配置描述符:主机获取配置描述符及其所有子描述符(接口、端点等)。
为什么主机要分两次获取设备描述符?
因为主机一开始不知道端点0的最大包长。如果设备描述符里写的是64字节,但主机按8字节去读,就会出问题。所以先读前8字节拿到bMaxPacketSize0,再重新配置,最后读完整描述符。
3.2.2 标准请求:主机和设备的对话语言
枚举过程中,主机通过控制传输发送标准请求。每个请求都是14字节的Setup包。
| 请求 | bmRequestType | bRequest | 说明 |
|---|---|---|---|
| GET_DESCRIPTOR | 0x80(设备到主机) | 0x06 | 获取描述符 |
| SET_ADDRESS | 0x00(主机到设备) | 0x05 | 设置地址 |
| SET_CONFIGURATION | 0x00 | 0x09 | 选择配置 |
| GET_CONFIGURATION | 0x80 | 0x08 | 获取当前配置 |
// Setup包结构
typedef struct {
uint8_t bmRequestType; // 请求方向、类型、接收者
uint8_t bRequest; // 请求码
uint16_t wValue; // 请求参数(如描述符类型和索引)
uint16_t wIndex; // 接口或端点号
uint16_t wLength; // 数据阶段长度
} usb_setup_packet_t;
3.2.3 枚举过程中的常见问题
问题1:描述符长度不匹配
我曾经调试一个设备,配置描述符的wTotalLength写的是50,但实际描述符数据只有40字节。主机读完50字节后,发现数据不对,直接放弃枚举。记住,wTotalLength必须等于所有子描述符长度之和。
问题2:端点0最大包长设置错误
USB 2.0全速设备端点0最大包长可以是8、16、32、64。高速设备固定64。如果你全速设备填了64但硬件只支持8,主机发64字节请求时,设备会NACK。我建议全速设备统一用64,省心。
问题3:字符串描述符索引无效
设备描述符里的iManufacturer、iProduct、iSerialNumber如果非0,必须对应有效的字符串描述符。否则主机请求字符串时,设备返回STALL,枚举失败。我习惯把不用的字符串索引设为0。
3.3 实战:一个完整的枚举抓包分析
咱们来看一个实际案例。我用USB分析仪抓了一个U盘的枚举过程,简化后的时序如下:
- 主机发GET_DESCRIPTOR(设备, 前8字节) → 设备返回:12 01 00 02 00 00 00 40
- 主机复位设备
- 主机发SET_ADDRESS(0x03) → 设备确认
- 主机发GET_DESCRIPTOR(设备, 完整18字节) → 设备返回完整描述符
- 主机发GET_DESCRIPTOR(配置, 完整) → 设备返回配置+接口+端点描述符
- 主机发SET_CONFIGURATION(1) → 设备确认,枚举完成
你想想看,整个过程也就几十毫秒。但任何一个描述符写错,设备就“失联”了。所以,写描述符的时候,我建议你对照USB规范逐字节核对,别偷懒。
嗯,关于描述符和枚举,今天就聊这么多。下一章咱们讲USB传输类型,那才是真正体现USB性能的地方。