4. USB枚举过程:设备连接、复位、分配地址、读取描述符、配置驱动

枚举,说白了就是USB设备跟主机之间的「握手」和「自我介绍」环节。我刚开始做USB驱动时,总觉得枚举是个黑盒子——插上设备就能用,但到底发生了什么?直到有一次我调试一个U盘死活不识别,才被迫把枚举的每个字节都扒了一遍。

嗯,今天我们就来把这个过程拆开看。你想想看,一个USB设备从插上去到能用,主机和它之间要交换多少信息?

4.1 枚举的六个核心步骤

枚举不是一蹴而就的,它分六个阶段。我习惯把这六个阶段记成「连、复、址、描、配、驱」——六个字,好记。

步骤 动作 主机角色 设备角色
1 设备连接 检测D+/D-电平变化 上拉电阻生效
2 总线复位 拉低D+/D-至少10ms 进入默认状态
3 分配地址 发送SET_ADDRESS请求 接收并确认新地址
4 读取描述符 请求设备描述符、配置描述符等 逐级返回描述符数据
5 配置设备 发送SET_CONFIGURATION请求 按配置初始化内部资源
6 加载驱动 匹配并加载对应驱动程序 进入就绪状态

这六个步骤,缺一步设备都用不了。我曾经遇到一个设备卡在「读取描述符」阶段,抓包一看,设备返回的描述符长度字段写错了——主机读到一半就放弃了。

4.2 设备连接与复位

设备刚插上时,主机怎么知道有东西来了?

USB总线上的D+和D-在空闲时都是低电平。设备端的上拉电阻会拉高其中一条线——全速设备拉D+,低速设备拉D-。主机端的根集线器检测到这个电平变化,就知道有新设备接入。

关键点:主机检测到设备连接后,不会立即开始通信。它先等待至少100ms,让设备完成上电初始化。这个延迟是USB规范要求的,我见过有些廉价设备在这个窗口期内还没准备好,导致枚举失败。

复位信号是什么?主机把D+和D-同时拉低,持续至少10ms。设备检测到复位信号后,会做三件事:

  • 将设备地址重置为0(默认地址)
  • 将端点0(控制端点)设为就绪状态
  • 等待主机发来的第一个令牌包

我的经验:复位之后,设备必须在10ms内准备好响应主机的请求。有些MCU实现的USB设备在复位后要花时间初始化时钟,容易超时。我一般会在复位中断服务程序里只做最必要的状态重置,把耗时操作放到主循环里。

4.3 分配地址

复位之后,设备还在默认地址0上。但地址0是共享的——同一时刻只能有一个设备用地址0。所以主机要尽快给设备分配一个唯一地址。

主机发送SET_ADDRESS请求,格式如下:

请求类型: 0x00 (标准设备请求,方向主机到设备)
请求码: 0x05 (SET_ADDRESS)
wValue: 新地址 (1~127)
wIndex: 0x0000
wLength: 0x0000

这里有个坑。设备收到SET_ADDRESS后,必须在状态阶段完成后才能切换地址。也就是说:

  1. 主机发送SETUP包(包含地址请求)
  2. 设备返回ACK
  3. 主机发送OUT包(状态阶段)
  4. 设备返回ACK
  5. 设备切换到新地址

注意:设备不能在收到SETUP包后立即切换地址,否则状态阶段的ACK会从新地址发出,主机收不到。我调试一个自研USB设备时就踩过这个坑——设备在SETUP阶段就切了地址,结果主机一直收不到状态ACK,枚举卡死。

4.4 读取描述符

地址分配完了,主机开始问设备:「你是谁?你能干什么?」

描述符就是设备的「身份证」。主机通过标准请求GET_DESCRIPTOR来读取。描述符是树形结构:

  • 设备描述符:18字节,包含USB版本、设备类、最大包大小等
  • 配置描述符:9字节,包含接口数量、供电方式、最大电流等
  • 接口描述符:9字节,包含接口号、端点数量、协议类型等
  • 端点描述符:7字节,包含端点地址、传输类型、最大包大小等
  • 字符串描述符:可选,包含厂商名、产品名、序列号等

主机读取描述符的顺序是固定的。我画了一张流程图,你看一眼就明白了:

USB枚举描述符读取流程 USB枚举描述符读取流程 1. 读取设备描述符 (18字节) 2. 读取配置描述符头部 (9字节) 3. 读取完整配置描述符 (含接口+端点) 有字符串描述符? 是 4a. 读取字符串描述符 否 4b. 跳过字符串描述符 5. 描述符读取完成,准备配置 注意:主机先读9字节配置 描述符头部,从中获取总长度 再读完整配置描述符

这里有个细节:主机先读设备描述符的前8字节,从中拿到bMaxPacketSize0(端点0的最大包大小),然后再读完整的18字节。为什么要分两次?因为主机一开始不知道端点0的包大小,只能用默认的8字节来读。

避坑指南:设备描述符中的bMaxPacketSize0必须写对。我曾经把一个全速设备的这个字段写成64,但实际端点0只支持8字节——主机读第二次描述符时直接超时。USB规范规定全速设备端点0最大包大小可以是8、16、32、64,但很多主机只支持8或64,保守起见写8最安全。

4.5 配置设备

描述符读完了,主机知道了设备的能力。接下来主机要选择一个配置来激活设备。

主机发送SET_CONFIGURATION请求:

请求类型: 0x00
请求码: 0x09 (SET_CONFIGURATION)
wValue: 配置值 (对应配置描述符中的bConfigurationValue)
wIndex: 0x0000
wLength: 0x0000

设备收到这个请求后,要按配置描述符中的要求初始化所有接口和端点。比如一个复合设备可能有多个接口,每个接口有各自的端点,设备需要分配缓冲区、启动端点传输等。

我的习惯:在固件里把SET_CONFIGURATION的处理做成状态机。先禁用所有端点,再按配置描述符逐一使能。这样即使主机发来重复的配置请求,设备也不会乱。我见过有些设备在收到第二次配置请求时直接死机,就是因为没有做状态保护。

4.6 加载驱动

配置完成后,主机根据设备描述符中的bDeviceClass、bDeviceSubClass、bDeviceProtocol以及接口描述符中的对应字段,来匹配驱动程序。

匹配优先级是这样的:

  • 首先匹配设备类驱动(如HUB类、音频类)
  • 如果设备类为0(由接口定义),则按接口描述符匹配
  • 如果接口类也匹配不上,主机尝试使用厂商专用驱动
  • 最后,如果都没有,主机弹出「未知设备」提示
设备类代码 含义 常见驱动
0x00 由接口定义类 按接口描述符匹配
0x03 HID(人机交互设备) HID驱动(键盘、鼠标)
0x08 大容量存储 Mass Storage驱动(U盘)
0x09 集线器 Hub驱动
0xFF 厂商专用 需要安装厂商驱动

注意:Windows和Linux的驱动匹配逻辑有细微差别。Windows会优先匹配硬件ID(VID/PID),而Linux更依赖类代码。我调试一个跨平台USB设备时,在Windows上工作正常,插到Linux上却加载了错误的驱动——就是因为设备描述符中的类代码写得太宽泛了。

4.7 枚举失败的常见原因

枚举失败是USB调试中最常见的问题。我整理了几个高频故障点:

  • 设备连接后无响应:检查上拉电阻是否接对,全速设备上拉电阻接D+,低速设备接D-。电阻值一般是1.5kΩ。
  • 复位后设备不ACK:设备固件可能没有正确检测复位信号,或者端点0没有初始化。
  • SET_ADDRESS超时:设备切换地址的时机不对,或者状态阶段处理有误。
  • 描述符数据错误:长度字段、校验和、描述符类型写错。用USB分析仪抓包对比规范最直接。
  • 配置请求失败:设备不支持主机选择的配置值,或者端点初始化失败。

我的调试工具链:USB分析仪(比如Beagle USB 480) + Wireshark抓包。先用分析仪看物理层信号,再用Wireshark看协议层交互。两个工具配合,90%的枚举问题都能定位。如果手头没有分析仪,至少用lsusb -v(Linux)或USBView(Windows)看设备描述符是否被正确读取。

枚举过程说白了就是主机和设备之间的一次「相亲」——主机问,设备答。双方都按规范来,一次就能成功。但实际项目中,设备端固件往往有各种小毛病,导致枚举失败。嗯,把枚举的每个步骤吃透,你就能在调试时快速定位问题出在哪一步。