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枚举过程流程图 1. 设备连接与复位 2. 获取设备描述符 3. 地址分配 (SET_ADDRESS) 4. 配置描述符解析 5. Set Configuration 完成枚举 关键说明 • 复位后设备地址=0 • 先取前8字节 • 再取完整18字节 • 地址范围1-127 • 状态阶段后切换 • 包含接口+端点 • 树形结构解析 • 选择配置值 • 初始化端点 • 设备进入配置状态

这张图把五个步骤串起来了。你跟着箭头走一遍,就能理解枚举的完整流程。

最后说一句:枚举失败是USB开发中最常见的问题。我建议你在调试时,用USB分析仪抓包,看看主机发了什么请求,设备回了什么数据。很多时候,问题就出在描述符数据上——比如长度不对、类型错误、或者某个字段填了非法值。

嗯,枚举这块内容就这么多。记住这五个步骤,以后遇到USB设备不识别的问题,你就能一步步排查了。


公众号:蓝海资料掘金营,微信deep3321