4. USB枚举过程:设备连接与复位、获取设备描述符、地址分配、配置描述符解析、Set Configuration完成枚举
枚举,说白了就是USB设备和主机之间的「握手仪式」。我刚开始做USB驱动时,总觉得这过程很神秘——设备插上去,怎么就能自动识别了?后来啃完协议才发现,其实每一步都是固定的套路。
今天咱们就把这五个步骤拆开揉碎了讲。你跟着走一遍,以后遇到枚举失败的问题,心里就有底了。
4.1 设备连接与复位
当USB设备插入主机时,主机是怎么知道的?
硬件上,USB接口的D+和D-线上有上拉电阻。全速设备在D+上拉,低速设备在D-上拉。主机端的根集线器会检测这些线上的电平变化。
我举个例子:你插上一个全速设备,D+线被拉高。主机集线器检测到这个状态变化,就会向主机控制器报告「有设备接入」。
接下来就是复位信号。主机通过集线器向设备发送复位信号——说白了就是把D+和D-同时拉低至少10ms。设备收到复位信号后,会做三件事:
- 复位内部状态机
- 将USB地址重置为0(默认地址)
- 准备好接收控制传输
关键点:复位完成后,设备处于默认地址(地址0),主机才能通过端点0与它通信。
我的经验:曾经有个项目,设备偶尔枚举失败。查了半天发现是复位时间不够——主机只拉了8ms,而设备要求至少10ms。嗯,这种时序问题最坑人。
4.2 获取设备描述符
复位完成后,主机要做的第一件事就是获取设备描述符。这是USB设备最基本的「身份证」。
主机通过控制传输的GET_DESCRIPTOR请求来获取。请求格式如下:
bmRequestType = 0x80 (设备到主机,标准请求,端点0)
bRequest = 0x06 (GET_DESCRIPTOR)
wValue = 0x0100 (描述符类型=设备描述符,索引=0)
wIndex = 0x0000
wLength = 0x0040 (最多64字节)
设备描述符的结构是这样的:
| 偏移 | 字段 | 大小 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 描述符长度(固定18字节) |
| 1 | bDescriptorType | 1 | 描述符类型(设备=0x01) |
| 2-3 | bcdUSB | 2 | USB规范版本号(如0x0200表示USB 2.0) |
| 4 | bDeviceClass | 1 | 设备类代码 |
| 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 | 配置描述符数量 |
注意:主机第一次获取设备描述符时,只请求前8个字节。为什么?因为第7字节bMaxPacketSize0还不知道,主机需要先知道端点0的最大包大小,才能正确接收后续数据。
获取到前8字节后,主机就知道了bMaxPacketSize0。然后它会重新发送GET_DESCRIPTOR请求,这次请求完整的18字节。
我个人习惯在调试时先检查bMaxPacketSize0——很多枚举失败都是因为这个值不对。比如全速设备应该是8、16、32或64,你填个0或者非法的值,主机直接就不认了。
4.3 地址分配
拿到设备描述符后,主机要给设备分配一个唯一的地址。这一步通过SET_ADDRESS请求完成。
bmRequestType = 0x00 (主机到设备,标准请求,端点0)
bRequest = 0x05 (SET_ADDRESS)
wValue = 0x0001 (地址1,范围1-127)
wIndex = 0x0000
wLength = 0x0000
设备收到这个请求后,不会立即切换地址。它会先返回一个空的数据包作为确认,然后才把内部地址寄存器更新为新地址。
为什么这么设计?你想想看,如果设备在收到请求后立刻切换地址,那它返回确认包时用的就是新地址——但主机还不知道设备已经切换了,就会收不到确认。
所以协议规定:设备在状态阶段完成后,才切换到新地址。
避坑指南:我曾经遇到过一个问题:设备在SET_ADDRESS后,主机发送的下一个请求总是超时。后来发现是设备切换地址后,需要等待一段时间(通常2ms)才能稳定响应。嗯,这个延迟在高速模式下尤其重要。
4.4 配置描述符解析
地址分配完成后,主机开始获取配置描述符。配置描述符比设备描述符复杂得多,因为它包含了接口、端点等详细信息。
主机先获取配置描述符的前9个字节,从中知道整个配置描述符集合的总长度。然后它再一次性获取全部数据。
配置描述符的结构:
| 偏移 | 字段 | 大小 | 说明 |
|---|---|---|---|
| 0 | bLength | 1 | 描述符长度(9字节) |
| 1 | bDescriptorType | 1 | 描述符类型(配置=0x02) |
| 2-3 | wTotalLength | 2 | 整个配置描述符集合的总长度 |
| 4 | bNumInterfaces | 1 | 该配置支持的接口数量 |
| 5 | bConfigurationValue | 1 | 配置值(用于Set Configuration) |
| 6 | iConfiguration | 1 | 配置字符串描述符索引 |
| 7 | bmAttributes | 1 | 配置特性(自供电、远程唤醒等) |
| 8 | bMaxPower | 1 | 最大功耗(单位2mA) |
配置描述符后面跟着接口描述符和端点描述符。一个配置可以有多个接口,一个接口可以有多个端点。
举个例子,一个USB音频设备可能有:
- 1个控制接口(端点0用于控制)
- 1个音频流接口(包含1个等时传输端点)
- 1个MIDI接口(包含2个中断端点)
主机解析这些描述符时,会构建一个树形结构。我建议你在调试时把这个树打印出来,一目了然。
核心要点:配置描述符解析是枚举过程中最复杂的部分。主机需要遍历所有描述符,检查它们的合法性,然后决定是否支持这个设备。
4.5 Set Configuration完成枚举
最后一步,主机发送SET_CONFIGURATION请求,告诉设备使用哪个配置。
bmRequestType = 0x00 (主机到设备,标准请求,端点0)
bRequest = 0x09 (SET_CONFIGURATION)
wValue = 0x0001 (配置值,对应bConfigurationValue)
wIndex = 0x0000
wLength = 0x0000
设备收到这个请求后,会根据配置描述符中的信息,初始化相应的端点、分配缓冲区、启动功能模块。
比如一个U盘设备,收到SET_CONFIGURATION后,会:
- 初始化Bulk IN和Bulk OUT端点
- 准备好接收SCSI命令
- 开始响应主机的读写请求
至此,枚举完成。设备进入配置状态,可以正常工作了。
注意:有些设备支持多个配置,但大多数设备只有一个。主机通常会选择第一个配置(bConfigurationValue=1)。如果你的设备有多个配置,记得在设备描述符中正确设置bNumConfigurations。
知识体系流程图
下面我用一张图总结整个枚举过程:
这张图把五个步骤串起来了。你跟着箭头走一遍,就能理解枚举的完整流程。
最后说一句:枚举失败是USB开发中最常见的问题。我建议你在调试时,用USB分析仪抓包,看看主机发了什么请求,设备回了什么数据。很多时候,问题就出在描述符数据上——比如长度不对、类型错误、或者某个字段填了非法值。
嗯,枚举这块内容就这么多。记住这五个步骤,以后遇到USB设备不识别的问题,你就能一步步排查了。
公众号:蓝海资料掘金营,微信deep3321