2. USB枚举过程详解:设备连接与复位、获取设备描述符、地址分配、配置描述符解析、设置配置

枚举,说白了就是USB设备跟主机之间的「握手」过程。设备插上去之后,主机得知道你是谁、需要多少带宽、用什么协议通信。这个过程走不通,后面所有数据传输都是空谈。我调试过不少枚举失败的板子,十有八九都是时序或者描述符长度算错了。

枚举的本质:主机通过默认地址0,向设备发送标准请求,逐步获取设备信息,最终分配唯一地址并加载驱动。

2.1 设备连接与复位

设备插入USB口那一刻,D+或D-线上的上拉电阻会被主机检测到。全速设备在D+上拉1.5kΩ,低速设备在D-上拉。主机检测到电平变化后,就知道有设备来了。

接下来主机发送复位信号——把D+和D-同时拉低至少10ms。设备检测到复位后,需要做好两件事:

  • 将USB引擎复位到默认状态
  • 将设备地址重置为0
  • 准备好响应主机请求

我个人习惯在复位中断服务函数里加一个GPIO翻转,用示波器抓一下,确认复位信号确实被MCU接收到了。有一次客户说设备枚举不稳定,结果发现是复位信号毛刺太多,MCU的USB模块根本没识别到。

避坑指南:我曾经遇到过复位信号宽度不够的问题。主机要求至少10ms,但有些USB Hub芯片只给了8ms。解决方案是在设备端做软件滤波,检测到复位后主动等待2ms再初始化。

2.2 获取设备描述符

复位完成后,主机通过默认地址0发送获取设备描述符请求(bRequest = GET_DESCRIPTOR,wValue = 0x0100)。这里有个细节:主机第一次只读取前8个字节,而不是整个18字节的描述符。

为什么会这样?因为主机还不知道设备的端点0最大包长度是多少。设备描述符的第7字节(bMaxPacketSize0)就告诉主机这个值。主机拿到前8字节后,解析出bMaxPacketSize0,然后再发一次请求获取完整的18字节。

偏移 字段 长度 说明
0 bLength 1 描述符长度,固定18
1 bDescriptorType 1 设备描述符类型,固定0x01
2-3 bcdUSB 2 USB规范版本号,如0x0200
4 bDeviceClass 1 设备类代码,0表示每个接口自行定义
5 bDeviceSubClass 1 子类代码
6 bDeviceProtocol 1 协议代码
7 bMaxPacketSize0 1 端点0最大包长,全速设备常见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 配置描述符数量,通常为1
注意:设备描述符中的bMaxPacketSize0必须与硬件实际配置一致。我曾经见过一个项目,描述符里写64,但硬件端点FIFO只配了32字节,结果主机发64字节请求时设备直接STALL,枚举卡死。

2.3 地址分配

主机拿到完整的设备描述符后,会发送设置地址请求(bRequest = SET_ADDRESS,wValue = 分配的地址)。设备收到这个请求后,不能立即切换地址——必须在状态阶段完成后才生效。

具体流程是这样的:

  1. 主机发送SETUP事务,包含地址值
  2. 设备返回ACK,表示收到
  3. 主机发送OUT事务(状态阶段)
  4. 设备返回ACK后,内部切换地址

你想想看,如果设备在收到SETUP后就切换地址,那状态阶段的ACK就可能发不回去。很多初学者在这里踩坑。我建议在固件里用一个标志位,等状态阶段完成中断触发后再更新USB模块的地址寄存器。

调试技巧:用USB分析仪抓包时,重点关注SET_ADDRESS请求之后设备是否返回了正确的ACK。如果看到设备STALL或者无响应,八成是地址切换时机出了问题。

2.4 配置描述符解析

地址分配完成后,主机开始获取配置描述符。这里比设备描述符复杂得多,因为配置描述符是嵌套结构:

  • 配置描述符(9字节)
  • 接口描述符(9字节)
  • 端点描述符(7字节)
  • 可能还有额外的类特定描述符

主机第一次请求时,wLength填9,只读取配置描述符的前9字节。从这9字节里,主机拿到wTotalLength字段,知道整个配置描述符集合的总长度。然后第二次请求,一次性读取全部数据。

我遇到过最头疼的问题,就是wTotalLength算错了。少算一个字节,主机解析到一半就卡住。后来我写了一个脚本,自动从描述符数组里计算总长度,跟声明的值做比对,再也没出过问题。

// 配置描述符集合示例(HID键盘)
static const uint8_t ConfigDescriptor[] = {
    // 配置描述符头
    0x09,           // bLength
    0x02,           // bDescriptorType (CONFIGURATION)
    0x29, 0x00,     // wTotalLength = 41
    0x01,           // bNumInterfaces
    0x01,           // bConfigurationValue
    0x00,           // iConfiguration
    0xA0,           // bmAttributes (自供电)
    0x32,           // bMaxPower (100mA)

    // 接口描述符
    0x09,           // bLength
    0x04,           // bDescriptorType (INTERFACE)
    0x00,           // bInterfaceNumber
    0x00,           // bAlternateSetting
    0x01,           // bNumEndpoints
    0x03,           // bInterfaceClass (HID)
    0x01,           // bInterfaceSubClass (Boot)
    0x01,           // bInterfaceProtocol (Keyboard)
    0x00,           // iInterface

    // HID描述符
    0x09,           // bLength
    0x21,           // bDescriptorType (HID)
    0x11, 0x01,     // bcdHID
    0x00,           // bCountryCode
    0x01,           // bNumDescriptors
    0x22,           // bDescriptorType (Report)
    0x3E, 0x00,     // wDescriptorLength

    // 端点描述符
    0x07,           // bLength
    0x05,           // bDescriptorType (ENDPOINT)
    0x81,           // bEndpointAddress (IN, EP1)
    0x03,           // bmAttributes (Interrupt)
    0x08, 0x00,     // wMaxPacketSize
    0x0A            // bInterval (10ms)
};

2.5 设置配置

这是枚举的最后一步。主机发送设置配置请求(bRequest = SET_CONFIGURATION,wValue = 配置值)。设备收到后,需要根据配置值初始化对应的接口和端点。

设备端的处理流程:

  1. 解析wValue,找到对应的配置编号
  2. 根据配置描述符中的接口和端点信息,初始化硬件
  3. 使能对应的端点FIFO
  4. 准备好接收数据

设置配置完成后,主机就会加载对应的驱动程序。对于HID设备,Windows会加载hidusb.sys;对于大容量存储设备,会加载usbstor.sys。驱动加载成功后,设备就可以正常通信了。

常见问题:设置配置后设备没有响应。我排查过的一个案例是,设备在SET_CONFIGURATION后没有正确初始化端点FIFO,导致主机发送IN请求时设备返回NAK。解决方案是在配置处理函数里,显式清空端点FIFO并重新使能。

枚举流程总览

下面这张图把整个枚举过程串起来了,建议你保存下来对照调试。

USB枚举流程总览 USB枚举流程总览 设备插入 主机检测到设备 主机发送复位信号 获取设备描述符(分两次) 设置地址 获取配置描述符(分两次) 设置配置 关键时序点 • 复位至少10ms • 地址在状态阶段后切换 • 描述符长度必须精确 常见失败原因 • 描述符长度错误 • 地址切换时机不对 • 端点FIFO未初始化

枚举过程看起来步骤多,但每一步都有固定的协议约束。只要描述符数据正确、时序满足规范要求,大部分设备都能顺利通过枚举。我建议你在开发初期,先用USB分析仪抓一次完整的枚举包,跟协议规范逐条比对,这样能快速定位问题。