3. USB包结构与事务处理:USB包的组成与事务三阶段

各位同学,今天我们来啃USB协议里最核心的一块——包结构与事务处理。说实话,很多做嵌入式开发的朋友,调USB驱动调到头秃,往往就是卡在这一块。我自己早年做USB摄像头项目时,就曾被一个错误的CRC校验折腾了整整三天。嗯,今天咱们就把这块彻底讲透。

3.1 USB包的组成:从SOP到EOP

USB总线上的数据传输,说白了就是一个个包的接力。每个包都有固定的格式,就像快递包裹一样,有封条、有地址、有内容、有校验。

一个完整的USB包,由以下几个字段组成:

字段 长度 说明
SOP(Start of Packet) 同步序列 包起始标志,告诉接收端“我要发数据了”
SYNC(同步域) 8位(低速/全速)或32位(高速) 用于时钟同步,让收发双方对齐节奏
PID(包标识符) 8位(4位类型+4位校验) 标识包的类型,比如令牌包、数据包、握手包
ADDR(地址域) 7位 目标设备地址,最多支持127个设备
ENDP(端点域) 4位 目标端点号,每个设备最多16个端点
CRC(循环冗余校验) 5位或16位 校验数据完整性,防止传输错误
EOP(End of Packet) 特定信号 包结束标志,告诉接收端“数据发完了”

关键点:PID字段包含4位类型码和4位校验码。校验码是类型码的按位取反。比如类型码为1000,校验码就是0111。接收端收到后,会检查类型码和校验码是否互补,如果不匹配,直接丢弃这个包。

我记得第一次看USB协议规范时,被这些字段搞得晕头转向。后来我画了一张图,把包的组成可视化,一下子就清晰了。下面这张SVG图,就是我当时画的简化版,分享给大家。

USB包结构组成 SOP SYNC PID ADDR ENDP CRC EOP 字段说明 • SOP/EOP:包起始和结束信号,由USB收发器硬件自动处理 • SYNC:同步时钟,高速模式下为32位,低速/全速为8位 • PID:包类型标识,决定这个包是令牌、数据还是握手 • ADDR+ENDP:目标设备地址和端点,类似“门牌号+房间号” • CRC:校验数据完整性,令牌包用5位CRC,数据包用16位CRC

个人经验:调试USB通信时,我习惯先用逻辑分析仪抓包,看PID字段是否正确。有一次我发现设备老是NAK,查了半天,结果是ADDR地址配置错了。你想想看,地址不对,主机根本找不到设备,自然一直NAK。

3.2 四种包类型详解

USB协议定义了四种包类型:令牌包、数据包、握手包、特殊包。每种包都有自己的PID编码和用途。

3.2.1 令牌包(Token Packet)

令牌包是主机发起的,用来告诉设备“我要开始一次传输了”。它包含PID、ADDR、ENDP和CRC5四个字段。常见的令牌包PID有:

  • OUT(0x01):主机要发数据给设备
  • IN(0x09):主机要从设备读取数据
  • SETUP(0x0D):用于控制传输的建立阶段
  • SOF(0x05):帧起始包,用于同步帧号

令牌包的长度是固定的,总共4字节。为什么这么短?因为它的作用就是“点名”,点名不需要说太多话。

3.2.2 数据包(Data Packet)

数据包携带实际的数据内容。它由PID、数据域(0~1023字节)和CRC16组成。数据包有两种PID:

  • DATA0(0xC3):数据包序列号0
  • DATA1(0x4B):数据包序列号1

为什么要有DATA0和DATA1?这是USB协议的一个巧妙设计——数据切换位(Data Toggle)。每次成功传输后,发送方和接收方都会翻转这个位。如果接收方发现数据包的PID和期望的不一致,就知道发生了丢包或重复传输。

避坑指南:我曾经在一个项目中,因为数据切换位没有正确同步,导致设备反复重传同一个数据包。调试了整整两天,最后发现是固件里忘记在每次成功传输后翻转DATA0/DATA1标志。记住:数据切换位是USB可靠传输的基石,千万别忽略。

3.2.3 握手包(Handshake Packet)

握手包只有PID字段,没有数据域,也没有CRC。它用来报告传输状态。常见的握手包PID有:

PID 名称 含义
0x5A ACK 数据接收成功,无错误
0x5B NAK 设备忙,暂时无法处理数据
0x1E STALL 端点被挂起,发生了不可恢复的错误
0x1F NYET 高速模式下,设备尚未准备好接收下一包

NAK和STALL有什么区别?我打个比方:NAK就像你打电话给对方,对方说“我现在忙,过会儿再打”;STALL就像对方直接说“这个号码已停机,别再打了”。

3.2.4 特殊包(Special Packet)

特殊包主要用于高速模式和预处理。常见的特殊包有:

  • PRE(0x3C):前导包,用于主机与低速设备通信时,先通知高速集线器切换模式
  • ERR(0x3D):错误包,集线器用这个包报告总线错误
  • SPLIT(0x7C/0x7D):分割事务包,用于高速主机与低速/全速设备通信时的拆分传输

特殊包在日常开发中接触得不多,但做USB Hub驱动或高速设备开发时,就绕不开它们了。

3.3 事务(Transaction)的三种阶段

包是USB通信的最小单位,但一次完整的“对话”叫做事务。一个事务由三个阶段组成:

  1. 令牌阶段(Token Stage):主机发送令牌包,点名设备
  2. 数据阶段(Data Stage):发送或接收数据包
  3. 握手阶段(Handshake Stage):接收方返回握手包,确认状态

下面这张SVG图,展示了IN事务和OUT事务的完整流程:

USB事务三阶段流程 IN事务(主机读取数据) 令牌阶段:IN令牌包 数据阶段:DATA0/1包 握手阶段:ACK/NAK 主机→设备 设备→主机 主机→设备 OUT事务(主机发送数据) 令牌阶段:OUT令牌包 数据阶段:DATA0/1包 握手阶段:ACK/NAK 主机→设备 主机→设备 设备→主机 注意:IN事务和OUT事务的握手阶段方向不同。 IN事务:设备发数据→主机收数据→主机发ACK/NAK;OUT事务:主机发数据→设备收数据→设备发ACK/NAK。

3.3.1 令牌阶段

令牌阶段由主机发起,发送一个令牌包。这个包告诉设备:

  • 这次事务是读(IN)还是写(OUT)
  • 目标设备是谁(ADDR)
  • 目标端点是什么(ENDP)

设备收到令牌包后,会检查ADDR是否匹配自己的地址。如果匹配,就准备进入数据阶段;如果不匹配,直接忽略这个包。

3.3.2 数据阶段

数据阶段传输实际的数据。根据事务类型不同,数据流向也不同:

  • IN事务:设备发送数据包,主机接收
  • OUT事务:主机发送数据包,设备接收
  • SETUP事务:主机发送8字节的SETUP数据包,设备接收

数据包的长度可以是0~1023字节。但实际传输中,数据包大小受端点描述符中wMaxPacketSize字段的限制。比如全速模式下,批量端点通常一次传64字节。

3.3.3 握手阶段

握手阶段是事务的收尾。接收方返回一个握手包,告诉发送方数据是否成功接收。三种常见的握手响应:

  • ACK:数据接收成功,CRC校验通过,数据切换位正确。发送方可以继续下一包。
  • NAK:设备忙,暂时无法处理数据。主机需要稍后重试。
  • STALL:端点发生错误,无法继续传输。主机需要发送CLEAR_FEATURE请求来清除STALL状态。

调试技巧:我习惯在固件中打印每个事务的握手状态。如果看到大量NAK,说明设备处理速度跟不上主机;如果看到STALL,说明端点配置有问题或者设备发生了异常。这两种情况,排查方向完全不同。

3.4 事务的完整示例

咱们来看一个实际的IN事务例子。假设主机要从设备端点1读取64字节数据:

// 阶段1:令牌阶段 - 主机发送IN令牌包
// PID=0x09 (IN), ADDR=0x05, ENDP=0x01, CRC5=0x1A
// 实际发送的字节:0x09 | (0x05 << 1) | (0x01 << 8) | CRC5
// 简化表示:0x09 0x0B 0x02 0x1A

// 阶段2:数据阶段 - 设备发送DATA0数据包
// PID=0xC3 (DATA0), 数据=64字节, CRC16=0x4A3B
// 实际发送:0xC3 + 64字节数据 + 0x4A 0x3B

// 阶段3:握手阶段 - 主机发送ACK握手包
// PID=0x5A (ACK)
// 实际发送:0x5A

你想想看,一次事务就完成了“点名→传数据→确认”三个步骤。如果任何一个阶段出错,整个事务就失败了,需要重传。

3.5 事务的超时与重试

USB协议规定了严格的事务超时机制:

  • 设备响应超时:主机发送令牌包后,设备必须在16位时间内响应。如果超时,主机认为设备无响应,会重试事务。
  • 握手超时:数据包发送后,接收方必须在16位时间内返回握手包。如果超时,发送方认为数据丢失,会重传数据包。

避坑指南:我曾经在一个项目中,因为设备的中断处理函数写得过长,导致响应超时。主机反复重试,最终认为设备故障,断开了连接。后来我把中断处理拆成两部分:紧急处理在中断中完成,耗时操作放到任务队列中。问题就解决了。

3.6 本章小结

好了,咱们把USB包结构和事务处理的核心内容过了一遍。总结几个关键点:

  • USB包由SOP、SYNC、PID、ADDR、ENDP、CRC、EOP组成,每个字段都有特定作用
  • 四种包类型:令牌包(点名)、数据包(传数据)、握手包(确认)、特殊包(辅助功能)
  • 事务三阶段:令牌阶段→数据阶段→握手阶段,缺一不可
  • 数据切换位(DATA0/DATA1)是USB可靠传输的关键机制
  • NAK和STALL的区别:NAK是临时忙,STALL是永久错误