2. USB枚举过程详解:设备连接与复位、获取设备描述符、地址分配、配置描述符解析、设置配置
枚举,说白了就是USB设备跟主机之间的「握手」过程。设备插上去之后,主机得知道你是谁、需要多少带宽、用什么协议通信。这个过程走不通,后面所有数据传输都是空谈。我调试过不少枚举失败的板子,十有八九都是时序或者描述符长度算错了。
2.1 设备连接与复位
设备插入USB口那一刻,D+或D-线上的上拉电阻会被主机检测到。全速设备在D+上拉1.5kΩ,低速设备在D-上拉。主机检测到电平变化后,就知道有设备来了。
接下来主机发送复位信号——把D+和D-同时拉低至少10ms。设备检测到复位后,需要做好两件事:
- 将USB引擎复位到默认状态
- 将设备地址重置为0
- 准备好响应主机请求
我个人习惯在复位中断服务函数里加一个GPIO翻转,用示波器抓一下,确认复位信号确实被MCU接收到了。有一次客户说设备枚举不稳定,结果发现是复位信号毛刺太多,MCU的USB模块根本没识别到。
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 |
2.3 地址分配
主机拿到完整的设备描述符后,会发送设置地址请求(bRequest = SET_ADDRESS,wValue = 分配的地址)。设备收到这个请求后,不能立即切换地址——必须在状态阶段完成后才生效。
具体流程是这样的:
- 主机发送SETUP事务,包含地址值
- 设备返回ACK,表示收到
- 主机发送OUT事务(状态阶段)
- 设备返回ACK后,内部切换地址
你想想看,如果设备在收到SETUP后就切换地址,那状态阶段的ACK就可能发不回去。很多初学者在这里踩坑。我建议在固件里用一个标志位,等状态阶段完成中断触发后再更新USB模块的地址寄存器。
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 = 配置值)。设备收到后,需要根据配置值初始化对应的接口和端点。
设备端的处理流程:
- 解析wValue,找到对应的配置编号
- 根据配置描述符中的接口和端点信息,初始化硬件
- 使能对应的端点FIFO
- 准备好接收数据
设置配置完成后,主机就会加载对应的驱动程序。对于HID设备,Windows会加载hidusb.sys;对于大容量存储设备,会加载usbstor.sys。驱动加载成功后,设备就可以正常通信了。
枚举流程总览
下面这张图把整个枚举过程串起来了,建议你保存下来对照调试。
枚举过程看起来步骤多,但每一步都有固定的协议约束。只要描述符数据正确、时序满足规范要求,大部分设备都能顺利通过枚举。我建议你在开发初期,先用USB分析仪抓一次完整的枚举包,跟协议规范逐条比对,这样能快速定位问题。