2. USB传输模型
做 USB 开发,我最怕的就是“懂协议但不会调试”。协议讲白了就是一堆包在线上跑来跑去,但你不把事务、包、帧这三个层次理清楚,遇到链路异常就一脸懵。这一节咱们把传输模型的骨架拎出来,再配上四种传输类型——嗯,这些是后续做故障注入的知识地基。
协议的基础单元:事务(Transaction)
事务是 USB 协议里最小的“有始有终”的交互单元。我习惯把它理解成一次对话:主机问一句,设备答一句,最后主机点个头。
一次完整的事务包含三个阶段:
- 令牌阶段 —— 主机发出令牌包,告诉设备和端点号,以及数据传输方向(IN / OUT / SETUP)。
- 数据阶段 —— 数据源(主机或设备)发出数据包,载荷大小由端点描述符定义。
- 握手阶段 —— 接收方响应 ACK / NAK / STALL / NYET,表示“收到了”、“忙请重试”、“出错”、“还没准备好”。
关键点:只有控制传输的 SETUP 事务是必须经过三个阶段,其他事务可省略握手或数据阶段(比如同步传输没有握手)。
我在项目里定位设备不枚举的 bug 时,常先拿抓包工具看 SETUP 事务是否收到了 ACK。如果握手阶段一直 NAK,多半是设备没准备好端点 0。
包(Packet)—— 线上的原子消息
事务是由一个个“包”拼起来的。包就是物理线路上的一段最小数据单元。USB 2.0 的包结构固定:
SYNC(8bit) + PID(8bit) + 数据字段(0~1024字节) + CRC(5/16bit) + EOP(若干位)
包分为四类:
| 包类型 | PID 值(高4位/低4位) | 作用 | 举例 |
|---|---|---|---|
| 令牌包 | IN / OUT / SETUP / SOF | 标识事务开始和方向 | 主机发 IN(设备→主机) |
| 数据包 | DATA0 / DATA1 / DATA2 / MDATA | 传输用户数据,按数据切换序列 | 批量传输使用 DATA0 / DATA1 交替 |
| 握手包 | ACK / NAK / STALL / NYET | 确认、拒绝、停止、未就绪 | 设备收数据后回 ACK |
| 特殊包 | PRE / ERR / SPLIT / PING | 低速前同步、错误、分拆、高速流控 | 高速 PING 探测端点是否就绪 |
注意:PID 的 CRC 校验只覆盖 PID 本身(8位),但数据包的 CRC 覆盖数据载荷。我做故障注入时,经常只损坏数据包 CRC 而不动 PID,模拟物理层噪声。
帧与微帧——总线的时间骨架
USB 总线的时间是按“帧”来组织的。全速/低速下 1 帧 = 1ms,高速下 1 微帧 = 125μs(即 1ms 分 8 个微帧)。主机在每个帧/微帧起始时广播一个 SOF 包,携带着帧号。
为什么要有帧?因为中断传输和同步传输需要周期性调度。我记得以前排查一个高速 UVC 摄像头丢帧问题,最后发现是 host 控制器在某个微帧里插入了太多批量事务,导致同步事务超时。说白了就是帧调度被挤爆了。
四种传输类型——你该选哪种?
USB 定义了四种事务传输类型,每种对应不同的可靠性、带宽和延迟要求。我直接对照着讲:
1. 控制传输
- 用途:设备枚举、配置、类命令(如 HID 报告描述符请求)
- 特点:双向,分为建立、数据(可选)、状态三个阶段,保证可靠(必须握手)
- 带宽:全速/低速占 10% 保留带宽,高速占 20%
- 故障场景:STALL 最常见——比如主机请求了不支持的请求值
我曾经遇到一个设备枚举到“获取配置描述符”后反复复位,原因是设备返回的长度和实际描述符长度不匹配。用逻辑分析仪抓到数据包 PID 序列错乱,最后发现是设备固件漏更新了 DATA0/DATA1 的切换标志位。
2. 批量传输
- 用途:大块数据,如 U 盘读写、打印机
- 特点:非周期性,错误重传(CRC校验+握手),保证送达但不保证延迟
- 带宽:仅当总线空闲时抢占,无保留带宽
- 故障场景:NAK 风暴——设备处理不过来一直回 NAK,导致总线利用率下降
我建议在故障注入测试里故意让设备回 NAK 15 次以上,看看 host 控制器的重试策略和超时行为。有些廉价 host 会在 NAK 无限重试时死锁。
3. 中断传输
- 用途:鼠标、键盘、HID 设备——需要定期查询但数据量小
- 特点:主机保证在 bInterval 指定间隔内轮询一次,但数据丢失不重传(仅重试一次)
- 带宽:全速最大每帧一次,高速最多每微帧一次
- 故障场景:设备在中断 IN 事务中不回响应,主机认为设备断开
4. 同步传输
- 用途:音频、视频流,数据时效性高于可靠性
- 特点:无握手阶段,不保证数据完整性,但固定带宽(保留带宽)
- 带宽:全速最多每帧 1023 字节,高速每微帧 1024 字节
- 故障场景:CRC 错误但主机不重传 —— 画面或音频出现爆音、花屏
特别提醒:同步传输没有握手,所以 任何数据包损坏都不会触发重传。做故障注入时,对同步端点注入 CRC 错误,相当于模拟线缆噪声……这是测试容错处理的极佳手段。
四种传输类型对比
| 特性 | 控制 | 批量 | 中断 | 同步 |
|---|---|---|---|---|
| 可靠性 | 高(握手+重试) | 高(握手+重试) | 中(仅重试一次) | 低(无握手) |
| 延迟保证 | 无 | 无 | 有(bInterval) | 有(固定带宽) |
| 双向性 | 双向 | 单向(端点方向) | 单向 | 单向 |
| 典型带宽 | 10%~20%保留 | 剩余带宽 | ≤90%保留带宽 | ≤90%保留带宽 |
| 事务握手 | 必须 | 必须 | 必须(NAK/STALL) | 无 |
| 故障注入点 | PID翻转、CRC、STALL | 连续NAK、CRC | 拔掉设备、NAK雪崩 | CRC损坏、EOP截断 |
链路恢复:从故障中学到的
协议故障注入测试中,我对手阶段的部分包故意损坏,看看设备或 host 会怎么恢复。举个例子:
- 损坏令牌包(比如翻转 IN 的 PID):接收方不响应,主机超时重发。如果连续超时,host 可能复位端口。
- 损坏数据包 CRC:接收方回复 NAK 或直接丢弃(同步传输直接忽略),发方根据重试策略重新发送。
- 损坏握手包(把 ACK 改成 NAK):可能触发数据重传,甚至传输死循环(取决于 host 驱动实现)。
曾经踩过的坑:我在一个 IoT 项目里用批量传输往设备里写固件,设备那边的 NAK 处理有 bug——收到 NAK 后不清空端点的缓冲区,导致后续数据错位。最后抓包发现主机一直在重传同一个 DATA0,但设备回了 ACK 后实际上没收到新数据。这教训就是:误投故障时不能光看包,还得看端点状态机的实现。
理解事务、包、帧模型,你才能有针对性地选择故障注入点。比如:对同步传输破坏数据包 CRC 能模拟线缆噪声;对控制传输破坏 SETUP 的 PID 可测试枚举异常。下一块知识会带你深入链路层时序,到时候这些包结构知识全用得着。