4. USB包与事务:包结构、包类型、事务
好,咱们今天聊点实在的。USB通信最底层的东西,就是包和事务。很多工程师调了几年USB,遇到枚举失败还是只会抓波形瞎看,其实就是这块没吃透。我当年刚做嵌入式时也踩过这个坑——明明设备描述符都对了,就是枚举不过,最后发现是一个包的CRC算错了。嗯,从那以后我再也不敢小看这些基础结构了。
4.1 包结构:USB通信的最小单元
一个USB包长什么样?说白了,就是一段有头有尾的数据串。USB协议里,每个包都遵循固定的格式:SOP + PID + 数据字段 + CRC + EOP。
| 字段 | 全称 | 长度 | 说明 |
|---|---|---|---|
| SOP | Start of Packet | 同步序列 | 告诉接收方“我要发数据了” |
| PID | Packet Identifier | 8位 | 包的类型标识,高4位是校验位 |
| ADDR | Address | 7位 | 目标设备地址,0是默认地址 |
| ENDP | Endpoint | 4位 | 端点号,0是控制端点 |
| CRC | Cyclic Redundancy Check | 5位或16位 | 校验数据完整性 |
| EOP | End of Packet | SE0状态 | 包结束信号 |
你想想看,主机和设备之间通信,就像两个人传纸条。SOP就是“喂,看这里”,PID告诉你是“命令”还是“数据”,ADDR和ENDP指定给谁、给哪个端口,CRC保证纸条没被撕坏。EOP就是“我说完了”。
核心要点:PID字段的高4位是低4位的取反校验。比如令牌包的PID是0001,那高4位就是1110。这个设计是为了防止误判包类型。我在项目中遇到过因为信号反射导致PID被误读的情况,幸亏有这个校验机制,否则设备会乱套。
4.2 包类型:Token、Data、Handshake
USB包按功能分三大类。我习惯把它们比作一次对话的三个角色:
- Token包(令牌)——发起者,告诉谁要干什么
- Data包(数据)——内容载体,真正要传的信息
- Handshake包(握手)——回应者,告诉对方“收到了”或“出错了”
4.2.1 Token包
Token包由主机发出,结构固定:PID + ADDR + ENDP + CRC5。常见的Token PID有:
| PID | 名称 | 用途 |
|---|---|---|
| 0001 | OUT | 主机要发数据给设备 |
| 1001 | IN | 主机要从设备读取数据 |
| 1101 | SETUP | 主机发起控制传输(枚举专用) |
| 0010 | SOF | 帧起始,用于同步 |
我个人习惯把SETUP看作一种特殊的OUT——它也是主机发数据,但数据内容必须是标准的设置请求。设备收到SETUP包后,就知道“哦,主机在跟我谈正事”,而不是普通的数据传输。
4.2.2 Data包
Data包跟在Token包后面。结构是:PID + 数据负载 + CRC16。数据负载最大1024字节(高速模式)。
Data PID有两种:
- DATA0(PID = 1100)——数据序列的第一个包
- DATA1(PID = 1101)——数据序列的第二个包
为什么要有DATA0和DATA1?这是USB协议的一个精妙设计——数据切换位。每次成功传输后,主机和设备都会翻转这个位。如果接收方发现PID不对,就知道发生了丢包或重复包。我曾经调试一个U盘读写失败的问题,抓了三天波形,最后发现是设备端的DATA切换逻辑写反了。
避坑指南:写固件时,一定要在控制传输的Data阶段正确维护DATA0/DATA1切换。很多初学者在枚举阶段只发DATA0,结果主机收不到正确的握手信号,枚举卡在Get Descriptor。我踩过这个坑,后来在代码里加了个状态机专门管理PID切换。
4.2.3 Handshake包
Handshake包最简单,只有PID,没有数据字段,也没有CRC。常见的握手PID:
| PID | 名称 | 含义 |
|---|---|---|
| 0010 | ACK | 数据接收成功,CRC校验通过 |
| 1010 | NAK | 设备忙,暂时无法处理 |
| 1110 | STALL | 设备出错,请求不被支持 |
| 0110 | NYET | 高速模式下,设备还没准备好 |
你想想看,NAK其实不是错误——它只是说“我现在没空,你过会儿再来”。STALL才是真正的错误,表示“这个请求我根本做不了”。我在做USB键盘时,主机发了一个不支持的HID请求,设备直接回STALL,主机就知道这个功能不支持,不会再发了。
4.3 事务:三个包组成一次完整对话
一个USB事务由三个阶段组成:
- 令牌阶段——主机发Token包,指定传输方向、地址和端点
- 数据阶段——根据方向,主机或设备发Data包
- 握手阶段——接收方回Handshake包,确认结果
说白了,一次事务就是一次完整的“请求-响应-确认”过程。USB协议里没有“一次发完”这种事,所有数据都是拆成一个个事务来传的。
4.3.1 IN事务
主机要从设备读数据。流程:
主机发 IN Token(PID=1001, ADDR, ENDP)
↓
设备发 Data包(DATA0/DATA1 + 数据 + CRC16)
↓
主机回 ACK(如果数据正确)
如果设备没准备好,它不回Data包,而是直接回NAK。主机收到NAK后,会重试这个IN事务。这就是为什么USB是轮询的——主机不停地发IN事务,直到设备准备好数据。
4.3.2 OUT事务
主机要写数据给设备。流程:
主机发 OUT Token(PID=0001, ADDR, ENDP)
↓
主机发 Data包(DATA0/DATA1 + 数据 + CRC16)
↓
设备回 ACK(如果数据正确)
如果设备忙,回NAK。如果设备发现数据有问题(比如CRC错误),不回任何握手包——这叫超时。主机等不到握手,就会重发这个OUT事务。
4.3.3 SETUP事务
SETUP事务是控制传输的起点,只在枚举阶段使用。它的结构特殊:
主机发 SETUP Token(PID=1101, ADDR=0, ENDP=0)
↓
主机发 DATA0包(8字节标准请求)
↓
设备回 ACK(必须立即响应,不能NAK或STALL)
注意:SETUP事务的数据阶段必须是DATA0,不能是DATA1。设备收到SETUP后,必须无条件接受,然后解析这8字节的请求内容。如果设备在SETUP阶段回了NAK或STALL,主机就会认为设备不可用,直接复位总线。
重要提醒:SETUP事务是USB枚举的基石。设备在收到SETUP后,必须清空端点缓冲区,准备接收新的控制请求。我见过一个设备因为没清缓冲区,导致后续的Get Descriptor请求数据错位,枚举失败。这个问题查了整整两天。
4.4 事务与包的关系:一张图看懂
下面这张图展示了USB通信的完整层次结构。我每次培训都会画这个图,因为它能帮你理解“包”和“事务”到底是怎么拼在一起的。
4.5 三种事务的对比总结
| 事务类型 | Token PID | 数据方向 | 数据阶段 | 握手方 | 典型场景 |
|---|---|---|---|---|---|
| IN | 1001 | 设备→主机 | 设备发Data包 | 主机回ACK | 读取描述符、批量输入 |
| OUT | 0001 | 主机→设备 | 主机发Data包 | 设备回ACK | 发送数据、批量输出 |
| SETUP | 1101 | 主机→设备 | 主机发DATA0 | 设备回ACK | 枚举、控制请求 |
个人经验:调试USB时,我习惯先看握手包。如果看到连续的NAK,说明设备忙,检查设备的中断处理或缓冲区。如果看到STALL,说明设备不支持这个请求,检查描述符或请求处理逻辑。如果什么都没看到(超时),大概率是CRC错误或地址不对。这个排查思路帮我解决过不下20个USB问题。
嗯,包和事务这块就讲到这里。你只要记住:包是数据的最小单元,事务是通信的最小单位。一个事务由三个包组成,每个包都有固定的结构和类型。下一层我们就要把这些事务串起来,形成真正的传输——控制传输、批量传输、中断传输、同步传输。到时候你会发现,理解了包和事务,整个USB协议栈就像搭积木一样清晰。