一、枚举过程概述:USB设备的"入职流程"
USB设备插入电脑后,需要经过一个完整的枚举过程才能正常工作。这个过程就像新员工入职:
-
报到登记:获取基本信息(设备描述符)
-
分配工号:设置唯一地址
-
详细登记:再次获取完整信息
-
了解岗位:获取配置信息
-
正式上岗:选择配置并激活
二、SETUP事务:控制传输的"指挥令"
SETUP事务详解
SETUP事务是控制传输的起始信号,用于发送"命令"给设备。它由三个阶段组成:

关键点:
-
SETUP包永远是DATA0(不像普通数据包会交替)
-
地址和端点都是0(因为设备还没分配地址)
-
设备必须响应ACK(除非出严重故障)
三、Setup Data格式:8字节的"命令书"
8字节命令详解
SETUP事务的DATA0包中,固定包含8字节的命令数据,结构如下:
cs
字节偏移 | 字段名 | 大小 | 说明
--------|-----------------|------|-------------------------------
0 | bmRequestType | 1 | 请求类型(方向+类型+接收者)
1 | bRequest | 1 | 具体请求代码
2-3 | wValue | 2 | 参数值(依赖具体请求)
4-5 | wIndex | 2 | 索引或偏移(依赖具体请求)
6-7 | wLength | 2 | 数据阶段的长度
bmRequestType字段详解(最重要!)
1. D7位:数据传输方向
cs
0 = Host-to-device (主机→设备) - 写操作
1 = Device-to-host (设备→主机) - 读操作
2. D6-D5位:请求类型(你的补充)
cs
00 = Standard (标准请求) - USB规范定义
01 = Class (类特定请求) - 设备类别定义
10 = Vendor (厂商自定义) - 厂商特有
11 = Reserved (保留)
3. D4-D0位:接收者(你的补充)
cs
00000 = Device (设备) - 对整个设备操作
00001 = Interface (接口) - 对特定接口操作
00010 = Endpoint (端点) - 对特定端点操作
00011 = Other (其他) - 其他接收者
00100...11111 = Reserved(保留)
常用标准请求(bRequest字段)
| 请求代码 | 名称 | 作用 |
|---|---|---|
| 0x05 | SET_ADDRESS | 设置设备地址 |
| 0x06 | GET_DESCRIPTOR | 获取描述符 |
| 0x07 | SET_DESCRIPTOR | 设置描述符 |
| 0x08 | GET_CONFIGURATION | 获取当前配置 |
| 0x09 | SET_CONFIGURATION | 设置配置 |
四、控制传输的三种类型
1. 控制写(Control Write)
方向:数据从主机流向设备
cs
建立阶段:SETUP事务(命令:写数据)
数据阶段:一个或多个OUT事务(发送数据)
状态阶段:IN事务(设备报告状态)
2. 控制读(Control Read)
方向:数据从设备流向主机
cs
建立阶段:SETUP事务(命令:读数据)
数据阶段:一个或多个IN事务(读取数据)
状态阶段:OUT事务(主机发送0长度包,设备回应状态)
3. 无数据控制(No-data Control)
方向:只有命令,没有数据
cs
建立阶段:SETUP事务(命令本身包含所有信息)
状态阶段:IN/OUT事务(根据命令方向确定)
数据包交替规则
-
建立阶段:总是使用DATA0
-
数据阶段:DATA0和DATA1交替
-
状态阶段:总是使用DATA1(与数据阶段最后一个包交替)
五、实际抓包分析:获取设备描述符
对应图片

抓包数据解析
cs
Packet 2434: SETUP包
PID: 0xB4 = 10110100
高4位=1011 → SETUP包
低4位=0100 → 取反为1011,正确
地址: 0x00(新设备,地址为0)
端点: 0x00(端点0)
CRC5: 0x08(校验正确)
长度: 8字节
cs
Packet 2435: DATA0包(8字节命令数据)
数据: 80 06 00 01 00 00 40 00
让我们逐一解析:
字节0: 0x80 = 10000000B
D7=1 → Device-to-host(读)
D6-D5=00 → Standard(标准请求)
D4-D0=00000 → Device(接收者是设备)
字节1: 0x06 → GET_DESCRIPTOR(获取描述符)
字节2-3: 0x0001 → wValue
高字节=0x00 → 描述符索引(通常为0)
低字节=0x01 → 描述符类型(1=设备描述符)
字节4-5: 0x0000 → wIndex(这里为0)
字节6-7: 0x0040 → wLength=64(主机想读64字节)
注意:设备描述符实际只有18字节
CRC16: 0xBB29(校验正确)
cs
Packet 2436: ACK包
PID: 0x4B = 01001011
高4位=0100 → ACK包
低4位=1011 → 取反为0100,正确
设备回应ACK,表示收到命令
这个SETUP事务的意义
主机说:"设备0,端点0,请把你的设备描述符发给我,我想读64字节。"
六、数据阶段:实际读取描述符
数据阶段过程
SETUP事务后,主机发起IN事务读取数据:
cs
事务1:读取前8字节
IN包:请求设备发送数据
DATA1包:设备发送前8字节描述符
ACK包:主机确认收到
事务2:读取后8字节(如果描述符更长,继续)
IN包:请求更多数据
DATA0包:设备发送剩余数据
ACK包:主机确认收到
... 直到读取完整描述符
关键点:
-
第一次读取只读8字节:主机先读8字节获取端点0最大包大小
-
实际设备描述符18字节:分多次IN事务读取
-
数据包交替:DATA0和DATA1交替
七、状态阶段:完成控制传输
状态阶段分析
对于控制读(GET_DESCRIPTOR):
-
主机发送OUT包:地址+端点0,DATA1包(0长度数据)
-
设备回应ACK:表示成功完成
抓包数据:
cs
Packet 3637: OUT包
PID: 0x87 = 10000111 → OUT包
地址: 0x??(已分配的新地址)
端点: 0x00
Packet 3638: DATA1包
数据长度: 0字节(状态阶段的特征)
Packet 3639: ACK包
设备回应ACK,完成整个控制传输
八、完整枚举过程详解
步骤1:第一次获取设备描述符(获取基本信息)
cs
1. SETUP事务:GET_DESCRIPTOR(设备描述符)
bmRequestType = 0x80(读,标准,设备)
bRequest = 0x06(GET_DESCRIPTOR)
wValue = 0x0100(设备描述符,索引0)
wLength = 8(只读前8字节,获取bMaxPacketSize0)
2. 数据阶段:IN事务读取8字节
获取端点0最大包大小(重要!)
3. 状态阶段:OUT事务完成
为什么只读8字节?
-
设备描述符第7字节是
bMaxPacketSize0(端点0最大包大小) -
主机需要知道这个值才能正确通信
-
后续通信都基于这个包大小
步骤2:设置设备地址(分配唯一ID)
cs
1. SETUP事务:SET_ADDRESS(无数据控制)
bmRequestType = 0x00(写,标准,设备)
bRequest = 0x05(SET_ADDRESS)
wValue = 新地址(如0x05)
wLength = 0(无数据阶段)
2. 状态阶段:IN事务完成
设备在状态阶段后启用新地址
关键细节:
-
设备在状态阶段完成后才使用新地址
-
之前所有通信使用地址0
-
之后所有通信使用新地址
步骤3:再次获取完整设备描述符
cs
使用新地址,获取完整的18字节设备描述符
主机可以请求全部18字节,因为已经知道端点0最大包大小
步骤4:获取配置描述符
cs
1. SETUP事务:GET_DESCRIPTOR(配置描述符)
bmRequestType = 0x80(读,标准,设备)
bRequest = 0x06(GET_DESCRIPTOR)
wValue = 0x0200(配置描述符,索引0)
wLength = 配置总长度(从设备描述符获得)
2. 数据阶段:读取完整的配置描述符
包括所有接口描述符、端点描述符等
3. 状态阶段:完成
配置描述符的特点:
-
一次性返回所有相关描述符(配置+接口+端点)
-
wTotalLength字段告诉主机总长度 -
主机根据接口类型加载相应驱动
步骤5:设置配置(激活设备)
cs
1. SETUP事务:SET_CONFIGURATION(无数据控制)
bmRequestType = 0x00(写,标准,设备)
bRequest = 0x09(SET_CONFIGURATION)
wValue = 配置值(通常为1)
wLength = 0
2. 状态阶段:完成
设备激活该配置下的所有接口和端点
重要:即使设备只有一种配置,也必须执行此步骤!
九、枚举过程总结表
| 步骤 | SETUP事务命令 | 目的 | 关键数据 |
|---|---|---|---|
| 1 | GET_DESCRIPTOR(设备) | 获取端点0最大包大小 | wLength=8 |
| 2 | SET_ADDRESS | 分配唯一地址 | wValue=新地址 |
| 3 | GET_DESCRIPTOR(设备) | 获取完整设备信息 | wLength=18 |
| 4 | GET_DESCRIPTOR(配置) | 获取配置信息 | wLength=总长度 |
| 5 | SET_CONFIGURATION | 激活设备 | wValue=配置值 |
十、常见问题解答
Q1:为什么枚举过程中设备地址从0开始?
A:所有新设备默认地址为0,用于初始通信。主机分配新地址后,设备在状态阶段后切换到新地址。
Q2:如果设备描述符长度超过端点0最大包大小怎么办?
A:主机分多次IN事务读取。例如,如果端点0最大包为8字节,18字节描述符需要:
-
第1次IN:8字节(DATA1)
-
第2次IN:8字节(DATA0)
-
第3次IN:2字节(DATA1)
Q3:枚举失败可能的原因?
-
设备没有正确响应SETUP事务
-
描述符格式错误
-
端点0最大包大小设置不合理
-
设备耗电超过总线供电能力
Q4:枚举完成后设备如何工作?
A:枚举完成后:
-
设备有唯一地址
-
驱动程序已加载
-
配置已激活
-
可以开始正常数据传输(批量、中断、同步)
十一、实际开发注意事项
固件开发者:
-
必须正确实现标准请求:GET_DESCRIPTOR、SET_ADDRESS、SET_CONFIGURATION
-
描述符必须准确:特别是端点0最大包大小
-
及时响应:在规定时间内响应主机请求
驱动开发者:
-
遵循枚举流程:按标准步骤操作
-
错误处理:处理设备无响应、描述符错误等情况
-
资源管理:正确分配设备地址,避免冲突
调试技巧:
-
使用USB分析仪:捕获和分析枚举过程
-
检查描述符:确保格式和内容正确
-
模拟主机:使用工具模拟主机行为进行测试
十二、枚举过程完整示例
假设一个USB鼠标的枚举过程:
cs
1. 设备插入,D+被拉高,主机检测到全速设备
2. 主机复位设备
3. 第一次GET_DESCRIPTOR:读取8字节,得知端点0最大包为8
4. SET_ADDRESS:分配地址5
5. 第二次GET_DESCRIPTOR:读取完整18字节设备描述符
6. GET_DESCRIPTOR:读取配置描述符(9+9+7=25字节)
发现是HID设备(接口类=0x03)
7. 加载HID驱动程序
8. SET_CONFIGURATION:激活配置
9. 枚举完成,开始定期轮询中断端点
这个完整的枚举过程确保了USB设备的即插即用特性,是USB系统能够自动识别和配置设备的核心机制。理解这个过程对于开发和调试USB设备至关重要。




