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截断
事务三阶段与包类型结构 事务(Transaction)三阶段——包类型结构 ① 令牌阶段 SYNC + PID (IN/OUT/SETUP) + 地址/端点 (7+4bit) + CRC5 + EOP ② 数据阶段 SYNC + PID (DATA0/1) + 数据载荷 (0~1024B) + CRC16 + EOP ③ 握手阶段 SYNC + PID (ACK / NAK / STALL / NYET) + EOP (无CRC) 💡 同步传输 无握手阶段(直接省略③),控制/批量/中断 三个阶段完整 高速批量传输可使用 PING 令牌提前探测端点状态,减少总线开销 包类型对应 PID(8位): 令牌包: IN(E1h), OUT(C1h), SETUP(2Dh) | 数据包: DATA0(C3h), DATA1(4Bh) | 握手包: ACK(D2h), NAK(5Ah), STALL(1Eh) 注意:PID 低4位是反码校验,用于发现单比特错误——故障注入时可以翻转任意一位看接收方行为

链路恢复:从故障中学到的

协议故障注入测试中,我对手阶段的部分包故意损坏,看看设备或 host 会怎么恢复。举个例子:

  • 损坏令牌包(比如翻转 IN 的 PID):接收方不响应,主机超时重发。如果连续超时,host 可能复位端口。
  • 损坏数据包 CRC:接收方回复 NAK 或直接丢弃(同步传输直接忽略),发方根据重试策略重新发送。
  • 损坏握手包(把 ACK 改成 NAK):可能触发数据重传,甚至传输死循环(取决于 host 驱动实现)。

曾经踩过的坑:我在一个 IoT 项目里用批量传输往设备里写固件,设备那边的 NAK 处理有 bug——收到 NAK 后不清空端点的缓冲区,导致后续数据错位。最后抓包发现主机一直在重传同一个 DATA0,但设备回了 ACK 后实际上没收到新数据。这教训就是:误投故障时不能光看包,还得看端点状态机的实现。

理解事务、包、帧模型,你才能有针对性地选择故障注入点。比如:对同步传输破坏数据包 CRC 能模拟线缆噪声;对控制传输破坏 SETUP 的 PID 可测试枚举异常。下一块知识会带你深入链路层时序,到时候这些包结构知识全用得着。