4. 控制传输详解
控制传输是USB里最基础、也最容易出幺蛾子的传输类型。我当年刚接触USB时,总觉得控制传输就是发几个命令而已,直到抓包发现设备不回ACK、状态阶段卡死……才老老实实把每个阶段都吃透。
说白了,控制传输就是“一问一答”的流程。主机发一个请求,设备回应数据,最后确认结果。整个过程分三个环节:设置阶段、数据阶段(可选)、状态阶段。嗯,我们一个一个拆。
设置阶段——一切请求的起点
设置阶段由主机发起,固定8字节。这个包叫Setup包,结构很紧凑:
| 偏移 | 字段 | 说明 |
|---|---|---|
| 0 | bmRequestType | 方向(主机→设备/设备→主机)、类型(标准/类/厂商)、接收方(设备/接口/端点/其他) |
| 1 | bRequest | 请求码(如GET_DESCRIPTOR=0x06) |
| 2-3 | wValue | 参数,比如描述符类型和索引 |
| 4-5 | wIndex | 接口号或端点号 |
| 6-7 | wLength | 数据阶段期望传输的字节数(0表示无数据阶段) |
举个实战例子。我在调试一块HID键盘时,主机发送了GET_DESCRIPTOR请求:
bmRequestType = 0x80 (设备→主机,标准,设备)
bRequest = 0x06 (GET_DESCRIPTOR)
wValue = 0x0100 (描述符类型=设备描述符,索引=0)
wIndex = 0x0000
wLength = 0x0012 (18字节)
为什么抓包看到这个包是SETUP令牌+DATA0+ACK?因为控制传输默认用DATA0做设置阶段的数据包。硬件自动处理,我们只要关注这8字节内容就行。
数据阶段——该来的数据总会来
数据阶段不是必选的。如果wLength=0,就没有数据阶段,直接进状态阶段。有数据时,方向由Setup包的bit7决定。
- 主机到设备(OUT):主机发送DATA1/DATA0交替,设备回复ACK。
- 设备到主机(IN):主机发IN令牌,设备回复DATA包,主机回ACK。
这里有个坑:标准设备请求中,数据阶段长度由wLength指定,但设备实际返回的数据可能更少。比如请求100字节,设备只返回20字节,然后状态阶段提前握手。我曾经遇到一个U盘,厂商描述符声称有40字节,实际只发16字节就把状态阶段走了,导致我的分析仪一直等剩余数据——超时了才反应过来。
USB协议规定:设备可以在数据阶段提前返回短包(小于端点最大包长),或者返回空包,来结束数据阶段。状态阶段必须由主机发起控制传输的最后一笔事务。
状态阶段——收尾定生死
状态阶段的方向和前面数据阶段相反。如果数据阶段是IN(设备发数据),状态阶段就是OUT(主机发0长度包,设备回ACK);反之亦然。
说白了,状态阶段就是主机说“活干完了,你觉得行吗?”设备用ACK说“行”,或者用STALL说“不行”。
打个比方:
- 主机发SET_CONFIGURATION(1),配置成功了,状态阶段设备回ACK。
- 主机发一个不存在的请求,设备直接STALL,状态阶段都不正常走完。
标准设备请求一览
USB 2.0定义了11个标准设备请求。我列几个常见的:
| 请求名 | bRequest | 用途 |
|---|---|---|
| GET_DESCRIPTOR | 0x06 | 读取描述符(设备、配置、字符串等) |
| SET_ADDRESS | 0x05 | 给设备分配地址 |
| SET_CONFIGURATION | 0x09 | 选择一个配置 |
| GET_CONFIGURATION | 0x08 | 获取当前配置值 |
| GET_INTERFACE | 0x0A | 获取接口的备用设置 |
| SET_INTERFACE | 0x0B | 切换接口的备用设置 |
| CLEAR_FEATURE | 0x01 | 清除端点暂停等特性 |
| SET_FEATURE | 0x03 | 设置特性 |
你想想看,枚举过程就是靠这些请求一个个握手完成的。主机先发GET_DESCRIPTOR读设备描述符,再发SET_ADDRESS分配地址,然后发GET_DESCRIPTOR读配置描述符,最后发SET_CONFIGURATION激活设备。
抓包实战:如何快速定位控制传输问题
我在项目中常用这条排查思路:
- 看设置阶段:bmRequestType方向对不对?wLength合理吗?
- 看数据阶段:有没有短包结束?数据内容是否符合描述符结构?
- 看状态阶段:是ACK还是STALL?STALL通常意味着设备不认识这个请求。
- 如果状态阶段卡住:检查DATA PID,或者设备没有及时完成内部处理。
举个实际案例。某次调试一个复合设备,枚举到一半断开了。抓包发现:主机发了GET_DESCRIPTOR请求配置描述符,wLength=0x0200,设备正常返回了前9字节,之后又返回了剩余数据。但状态阶段主机发了OUT包,设备居然回了STALL。原来设备在处理配置描述符时,内部解析出错,没来得及完成状态控制。解决方法是让设备在返回数据后,必须准备好接收状态阶段,不能因为内部忙就STALL。
这张图把控制传输的三阶段和标准请求串在一起了。看明白它,你就能看懂大部分枚举抓包了。