3. 枚举过程详解:设备连接、复位、分配地址、读取描述符、配置生效

枚举,说白了就是 USB 设备从插上到能用的全过程。很多初学者觉得枚举很神秘,其实它就是一套固定的对话流程——主机问一句,设备答一句。我刚开始做 USB 驱动时,花了整整一周才把枚举流程跑通,后来发现大部分问题都出在描述符上。

这一节,我们就把枚举的每一步拆开来看。你跟着走一遍,以后遇到设备不识别的问题,心里就有底了。

枚举的本质:主机通过控制传输(默认端点 0)与设备交互,获取设备信息并分配资源,最终让设备进入配置状态。

3.1 枚举的完整流程

先看一张总图,把枚举的六个关键步骤串起来。我个人习惯把枚举分成三个阶段:连接检测、地址分配、描述符获取与配置。

USB 枚举完整流程 ① 设备连接 D+ / D- 上拉电阻检测 ② 总线复位 SE0 信号持续 10ms ③ 分配地址 SET_ADDRESS 请求 ④ 读取设备描述符 GET_DESCRIPTOR (Device) ⑤ 读取配置描述符 配置 + 接口 + 端点 ⑥ 配置生效 SET_CONFIGURATION 请求 关键说明 • 步骤①-③:设备从默认状态 → 地址状态 • 步骤④-⑤:主机获取设备能力描述 • 步骤⑥:设备进入配置状态,功能可用 • 整个枚举过程通过端点 0 的控制传输完成

3.2 第一步:设备连接检测

设备插入 USB 口时,主机怎么知道有东西插进来了?

关键在于 D+ 和 D- 线上的上拉电阻。全速设备会在 D+ 线上接一个 1.5kΩ 上拉电阻,低速设备则在 D- 线上接。主机端的 15kΩ 下拉电阻平时把这两条线拉低,一旦设备插入,对应数据线被拉高,主机就检测到了电平变化。

经验之谈:我曾经遇到一个设备,插上后主机毫无反应。查了半天,发现是 D+ 上拉电阻虚焊了。USB 调试时,先量一下 D+ 或 D- 的电平,能快速判断设备是否被主机识别。

3.3 第二步:总线复位

主机检测到设备后,会立即发送复位信号——把 D+ 和 D- 同时拉低(SE0)并保持至少 10ms。设备收到复位信号后,需要做两件事:

  • 复位内部状态机:回到默认状态,地址恢复为 0
  • 确定速度:根据复位结束后 D+ 或 D- 上的电平,判断自己是全速还是低速

嗯,这里要注意:复位期间设备不能响应任何请求。复位结束后,设备就处于「默认状态」,地址为 0,端点 0 可用。

3.4 第三步:分配地址

复位完成后,主机通过端点 0 发送 SET_ADDRESS 请求。这个请求很特殊——它不需要数据阶段,设备收到后立即返回 ACK,然后切换地址。

为什么说它特殊?因为设备必须在收到请求后、状态阶段完成前就切换地址。我见过不少固件在这个地方踩坑:

注意:SET_ADDRESS 请求的处理时序非常关键。设备必须在状态阶段开始前设置新地址,否则后续的请求会因地址不匹配而失败。我曾经调试一个设备,总是枚举到一半就断开,最后发现是地址切换早了——在状态阶段还没开始就改了地址。

地址分配后,设备进入「地址状态」。此时主机就可以用新地址与设备通信了。

3.5 第四步:读取设备描述符

地址分配完成后,主机发送 GET_DESCRIPTOR 请求,读取设备描述符。设备描述符只有 18 字节,包含以下关键信息:

偏移 字段 长度 说明
0 bLength 1 描述符长度(固定 18)
1 bDescriptorType 1 描述符类型(设备描述符 = 1)
2-3 bcdUSB 2 USB 规范版本号(如 0x0200 表示 USB 2.0)
4 bDeviceClass 1 设备类代码(0 表示在接口描述符中定义)
5 bDeviceSubClass 1 子类代码
6 bDeviceProtocol 1 协议代码
7 bMaxPacketSize0 1 端点 0 最大包长(全速 8/16/32/64)
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 字节——因为需要先知道端点 0 的最大包长。拿到 bMaxPacketSize0 后,再请求完整的 18 字节。

关键点:bMaxPacketSize0 决定了后续所有控制传输的数据包大小。如果这个值填错了,后续的枚举步骤大概率会失败。全速设备常见值是 8、16、32、64,高速设备固定为 64。

3.6 第五步:读取配置描述符

设备描述符告诉主机有多少个配置,接下来主机就要读取每个配置的详细信息。配置描述符是一个「描述符集合」,包含配置描述符本身、接口描述符、端点描述符,还可能包含其他类特定描述符。

主机先请求配置描述符的前 9 字节,得到总长度后,再一次性读取整个配置描述符集合。典型的配置描述符结构如下:

配置描述符 (9 字节)
├── 接口描述符 1 (9 字节)
│   ├── 端点描述符 1 (7 字节)
│   └── 端点描述符 2 (7 字节)
├── 接口描述符 2 (9 字节)
│   └── 端点描述符 3 (7 字节)
└── ...

配置描述符的关键字段:

字段 说明
wTotalLength 整个配置描述符集合的总字节数
bNumInterfaces 该配置下的接口数量
bConfigurationValue 配置编号(用于 SET_CONFIGURATION)
bmAttributes 配置特性(自供电/总线供电/远程唤醒)
bMaxPower 最大功耗(单位 2mA)

接口描述符定义了一个功能单元,每个接口可以有多个端点。端点描述符则定义了端点的传输类型、方向、最大包长等。

避坑指南:我曾经写过一个 HID 设备的描述符,接口描述符里的 bInterfaceProtocol 填错了,导致 Windows 和 Linux 的行为完全不同——Windows 能识别,Linux 直接报错。不同操作系统对描述符的容错能力不一样,写固件时最好严格遵循规范。

3.7 第六步:配置生效

主机读完所有描述符后,会选择一个合适的配置,发送 SET_CONFIGURATION 请求。设备收到后,需要做以下事情:

  1. 验证配置值:确认 bConfigurationValue 是否有效
  2. 初始化端点:根据端点描述符配置端点寄存器
  3. 启动功能:如果设备有硬件功能(如传感器、马达),此时可以初始化
  4. 返回 ACK:配置成功

配置生效后,设备进入「配置状态」,所有非默认端点都可以使用了。此时主机就可以通过各端点进行数据传输。

重要提醒:SET_CONFIGURATION 请求可能被发送多次。设备需要支持重新配置,即先取消当前配置,再应用新配置。我见过一些设备在重新配置时没有正确复位端点状态,导致后续传输异常。

3.8 枚举过程中的常见问题

最后,总结几个我在项目中遇到的枚举问题,供你参考:

  • 设备不响应 SET_ADDRESS:检查端点 0 的初始化代码,确保复位后端点 0 可用
  • 描述符返回错误:用 USB 分析仪抓包,对比规范检查每个字节
  • 配置失败:确认 bConfigurationValue 与 SET_CONFIGURATION 请求中的值一致
  • 枚举到一半断开:检查电源是否足够,bMaxPower 是否设置正确

枚举是 USB 通信的基础。把这一步吃透了,后面的批量传输、中断传输、同步传输就都好理解了。你想想看,整个枚举过程其实就是主机和设备之间的一次「握手」——互相认识、交换能力、建立连接。搞明白这个,USB 开发就入门了。