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通信时,我习惯先用逻辑分析仪抓包,看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通信的最小单位,但一次完整的“对话”叫做事务。一个事务由三个阶段组成:
- 令牌阶段(Token Stage):主机发送令牌包,点名设备
- 数据阶段(Data Stage):发送或接收数据包
- 握手阶段(Handshake Stage):接收方返回握手包,确认状态
下面这张SVG图,展示了IN事务和OUT事务的完整流程:
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是永久错误