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事务由三个阶段组成:

  1. 令牌阶段——主机发Token包,指定传输方向、地址和端点
  2. 数据阶段——根据方向,主机或设备发Data包
  3. 握手阶段——接收方回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通信的完整层次结构。我每次培训都会画这个图,因为它能帮你理解“包”和“事务”到底是怎么拼在一起的。

USB包与事务层次结构 USB事务(Transaction) 一次完整的“请求-响应-确认”过程 由令牌阶段 + 数据阶段 + 握手阶段组成 令牌阶段 主机发Token包 指定方向/地址/端点 数据阶段 主机或设备发Data包 携带实际数据+CRC16 握手阶段 接收方回Handshake包 ACK/NAK/STALL Token包 PID + ADDR + ENDP + CRC5 类型:IN/OUT/SETUP/SOF Data包 PID + 数据负载 + CRC16 类型:DATA0 / DATA1 Handshake包 只有PID,无数据无CRC 类型:ACK/NAK/STALL 每个包的通用结构 SOP(同步) → PID(类型标识) → 数据字段(ADDR/ENDP/负载) → CRC(校验) → EOP(结束)

4.5 三种事务的对比总结

事务类型 Token PID 数据方向 数据阶段 握手方 典型场景
IN 1001 设备→主机 设备发Data包 主机回ACK 读取描述符、批量输入
OUT 0001 主机→设备 主机发Data包 设备回ACK 发送数据、批量输出
SETUP 1101 主机→设备 主机发DATA0 设备回ACK 枚举、控制请求

个人经验:调试USB时,我习惯先看握手包。如果看到连续的NAK,说明设备忙,检查设备的中断处理或缓冲区。如果看到STALL,说明设备不支持这个请求,检查描述符或请求处理逻辑。如果什么都没看到(超时),大概率是CRC错误或地址不对。这个排查思路帮我解决过不下20个USB问题。

嗯,包和事务这块就讲到这里。你只要记住:包是数据的最小单元,事务是通信的最小单位。一个事务由三个包组成,每个包都有固定的结构和类型。下一层我们就要把这些事务串起来,形成真正的传输——控制传输、批量传输、中断传输、同步传输。到时候你会发现,理解了包和事务,整个USB协议栈就像搭积木一样清晰。