2. USB设备枚举过程:设备描述符、配置描述符、接口描述符、端点描述符
大家好,我是你们的老朋友。今天咱们聊聊USB枚举过程中最核心的四个描述符。说实话,我刚入行那会儿,觉得描述符就是一堆枯燥的字节。直到有一次调试一个U盘,死活枚举不过去,最后发现是配置描述符的长度字段写错了。从那以后,我对描述符的每一个字节都充满了敬畏。
枚举过程,说白了就是主机和设备互相认识的过程。设备插上去,主机问一句“你是谁?”,设备回答“我是谁、我能干什么”。这个问答靠的就是描述符。
核心要点:USB描述符是设备向主机报告自身能力的结构化数据。枚举失败,90%是描述符出了问题。
2.1 设备描述符:设备的身份证
设备描述符是主机拿到的第一份数据。它只有18个字节,但信息量巨大。我习惯把它比作身份证——上面写着设备类型、厂商ID、产品ID、版本号等基本信息。
| 偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 描述符长度,固定为18 |
| 1 | bDescriptorType | 1 | 描述符类型,设备描述符为0x01 |
| 2-3 | bcdUSB | 2 | USB规范版本号,如0x0200表示USB 2.0 |
| 4 | bDeviceClass | 1 | 设备类代码,0xFF表示厂商自定义 |
| 5 | bDeviceSubClass | 1 | 子类代码 |
| 6 | bDeviceProtocol | 1 | 协议代码 |
| 7 | bMaxPacketSize0 | 1 | 端点0的最大包大小 |
| 8-9 | idVendor | 2 | 厂商ID,由USB-IF分配 |
| 10-11 | idProduct | 2 | 产品ID,厂商自行定义 |
| 12-13 | bcdDevice | 2 | 设备版本号 |
| 14 | iManufacturer | 1 | 厂商字符串描述符索引 |
| 15 | iProduct | 1 | 产品字符串描述符索引 |
| 16 | iSerialNumber | 1 | 序列号字符串描述符索引 |
| 17 | bNumConfigurations | 1 | 支持的配置描述符数量 |
我的经验:bMaxPacketSize0这个字段特别容易踩坑。USB 2.0全速设备必须是8、16、32、64之一,高速设备固定为64。我曾经见过有人填了128,结果主机直接不认。
2.2 配置描述符:设备的说明书
拿到设备描述符后,主机接着要配置描述符。配置描述符告诉主机:这个设备需要多少电流、有多少个接口、支持什么属性。
配置描述符本身只有9个字节,但它后面跟着一堆接口描述符和端点描述符。所以它的总长度字段wTotalLength特别重要,告诉主机整个配置信息有多长。
// 配置描述符结构体(Linux内核中的定义)
struct usb_config_descriptor {
__u8 bLength; // 9
__u8 bDescriptorType; // 0x02
__le16 wTotalLength; // 整个配置的总长度
__u8 bNumInterfaces; // 接口数量
__u8 bConfigurationValue; // 配置值,用于SetConfiguration
__u8 iConfiguration; // 配置字符串索引
__u8 bmAttributes; // 属性:自供电、远程唤醒等
__u8 bMaxPower; // 最大电流,单位2mA
} __attribute__ ((packed));
注意:bMaxPower字段的单位是2mA。比如填50,表示100mA。很多新手直接填电流值,结果设备只能拿到一半的电流。我调试过一个无线网卡,就是这个问题导致信号不稳定。
2.3 接口描述符:设备的功能模块
一个配置可以有多个接口。接口描述符描述了一个功能单元。比如一个USB摄像头,可能有视频接口和音频接口。
接口描述符也是9个字节。其中bInterfaceNumber是接口编号,bAlternateSetting用于备用设置——这个在视频设备里很常见,可以切换不同的分辨率。
| 字段 | 说明 |
|---|---|
| bInterfaceNumber | 接口编号,从0开始 |
| bAlternateSetting | 备用设置编号 |
| bNumEndpoints | 该接口使用的端点数量(不包括端点0) |
| bInterfaceClass | 接口类代码 |
| bInterfaceSubClass | 接口子类代码 |
| bInterfaceProtocol | 接口协议代码 |
| iInterface | 接口字符串索引 |
嗯,这里要注意:bInterfaceClass、bInterfaceSubClass、bInterfaceProtocol这三个字段决定了主机加载哪个驱动。比如HID设备的类代码是0x03,大容量存储设备是0x08。如果填错了,主机可能加载错误的驱动。
2.4 端点描述符:数据的通道
端点描述符是描述符链的最后一环。它描述了数据通道的属性:传输方向、传输类型、最大包大小、轮询间隔。
每个端点描述符7个字节。bEndpointAddress的低4位是端点号,最高位表示方向(0=OUT,1=IN)。bmAttributes字段指定传输类型:0=控制传输,1=等时传输,2=批量传输,3=中断传输。
// 端点描述符结构体
struct usb_endpoint_descriptor {
__u8 bLength; // 7
__u8 bDescriptorType; // 0x05
__u8 bEndpointAddress; // 端点地址和方向
__u8 bmAttributes; // 传输类型
__le16 wMaxPacketSize; // 最大包大小
__u8 bInterval; // 轮询间隔(毫秒或微帧)
} __attribute__ ((packed));
关键点:wMaxPacketSize字段在USB 2.0高速模式下有特殊编码。高2位表示每个微帧的额外传输次数。比如批量端点最大包大小是512字节,但高2位为0b10时,表示每个微帧可以传3个512字节的包。这个细节我当年在优化U盘读写速度时才发现。
2.5 描述符的层级关系
这四个描述符是树形结构。设备描述符在最顶层,下面挂配置描述符,配置描述符下面挂接口描述符,接口描述符下面挂端点描述符。主机枚举时,就是按照这个层级一层层往下读。
我画了一张图,帮你理解这个结构:
2.6 枚举流程实战
主机枚举一个设备,大致走这几步:
- 复位设备:主机检测到设备插入,发送复位信号
- 获取设备描述符:主机发请求,设备返回18字节的设备描述符
- 再次复位:根据设备描述符中的bMaxPacketSize0,主机确定端点0的最大包大小
- 设置地址:主机给设备分配一个唯一地址
- 获取配置描述符:主机先读9字节,得到wTotalLength,再读完整配置
- 解析描述符链:主机遍历配置描述符、接口描述符、端点描述符
- 选择配置:主机调用SetConfiguration,设备开始工作
避坑指南:我曾经在开发一个自定义HID设备时,配置描述符的wTotalLength算错了。主机读9字节后,再读完整配置时只读了部分数据,导致接口描述符和端点描述符没读到。设备能识别但无法使用。排查了两天才发现是长度字段少算了2个字节。
2.7 描述符的调试技巧
如果你在开发USB设备驱动,调试描述符是家常便饭。我推荐两个方法:
- 使用USB分析仪:抓取主机和设备之间的通信,直接看原始数据。我最常用的是Beagle USB协议分析仪。
- Linux下的lsusb命令:lsusb -v可以打印出设备的全部描述符信息。我经常用这个命令验证设备上报的数据是否正确。
# 查看设备描述符详细信息
lsusb -v -d 1234:5678
# 输出示例(部分)
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 2.00
bDeviceClass 0
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 64
idVendor 0x1234
idProduct 0x5678
bcdDevice 1.00
iManufacturer 1
iProduct 2
iSerialNumber 3
bNumConfigurations 1
嗯,说到调试,我再分享一个经验。有时候设备枚举失败,不一定是描述符数据错了,也可能是时序问题。比如设备上电后需要一段时间才能响应主机请求。我遇到过一款芯片,上电后要等200ms才能稳定工作。如果主机在100ms内就发请求,设备直接无响应。解决办法是在设备描述符中设置合适的bMaxPower,或者在上电后加一个延时。
好了,关于USB枚举的四个描述符,今天就聊到这里。记住一句话:描述符是设备和主机沟通的语言。语言不通,什么都干不了。下次遇到USB设备不工作,先拿lsusb看看描述符对不对,往往能省下不少时间。