4. USB包结构与事务:从比特流到一次完整传输
USB通信,说白了就是一场精心编排的“对话”。主机和设备之间,通过一个个固定格式的“包”来交换信息。这些包就像信封,有固定的地址、内容和校验。今天我就带你拆开这些信封,看看里面到底装了什么。
4.1 包格式:USB通信的最小单元
一个完整的USB包,由五个部分组成。我习惯把它叫做“五段式”结构。你想想看,任何通信协议,最怕的就是数据传错了没人知道。USB的包结构,就是为了解决这个问题。
| 字段 | 名称 | 长度 | 说明 |
|---|---|---|---|
| SOP | 包起始 | 同步序列 | 告诉接收方“我要发数据了” |
| PID | 包标识符 | 8位 | 包的类型和方向 |
| DATA | 数据字段 | 0~1024字节 | 实际传输的内容 |
| CRC | 循环冗余校验 | 5或16位 | 检查数据是否损坏 |
| EOP | 包结束 | SE0状态 | 告诉接收方“发完了” |
核心要点:PID字段是包的“身份证”。它由4位类型码和4位取反校验码组成。比如令牌包的PID是0x69(二进制0110 1001),后四位1001就是前四位0110的取反。硬件会自动校验,如果PID校验失败,整个包会被直接丢弃。
我在项目中遇到过一个问题:设备偶尔不响应主机。查了很久,最后发现是线缆过长导致SOP同步序列丢失。嗯,这里要注意,SOP其实是一串特殊的同步信号(KJ状态交替),接收方用它来锁定时钟相位。如果同步失败,后面所有数据都是废的。
4.2 令牌包:谁来说话,跟谁说
令牌包是主机发起的“命令”。它定义了接下来谁要说话,往哪个方向说。我个人习惯把令牌包看作USB总线的“调度令牌”。
令牌包只有三个字段:PID + 地址域(ADDR) + 端点域(ENDP) + CRC5。地址域7位,最多支持127个设备。端点域4位,每个设备最多16个端点。
// 令牌包结构示意(主机发送)
// PID: 0x69 (OUT令牌) 或 0x2D (IN令牌) 或 0x4D (SETUP令牌)
// ADDR: 设备地址 (0x00 ~ 0x7F)
// ENDP: 端点号 (0x00 ~ 0x0F)
// CRC5: 对ADDR+ENDP的5位CRC校验
// 例如:主机想向设备地址0x03的端点1发送数据
// 令牌包内容:PID=0x69, ADDR=0x03, ENDP=0x01, CRC5=计算值
实战技巧:我曾经调试一个USB音频设备,发现主机总是枚举失败。用逻辑分析仪抓包,发现SETUP令牌后的CRC5一直报错。最后定位到是地址线虚焊,导致ADDR字段被错误采样。记住:令牌包只有5位CRC,保护能力有限,电气完整性至关重要。
4.3 数据包:真正的内容载体
数据包负责承载实际传输的数据。它有两种类型:DATA0和DATA1。为什么要有两种?这是USB实现数据同步的巧妙设计——数据切换同步机制。
数据包的结构是:PID + 数据字段(0~1024字节) + CRC16。CRC16覆盖整个数据字段,保护能力比令牌包强得多。
| 数据包类型 | PID值 | 用途 |
|---|---|---|
| DATA0 | 0xC3 | 数据序列中的第一个包 |
| DATA1 | 0x4B | 数据序列中的后续包 |
| DATA2 | 0x87 | 高速高带宽等时传输 |
| MDATA | 0x0F | 高速拆分事务 |
数据切换同步机制,说白了就是主机和设备轮流使用DATA0和DATA1。比如主机先发DATA0,设备收到后回复ACK。主机下次就发DATA1,设备再回复ACK。如果设备收到重复的DATA0(比如主机没收到ACK重发了),设备知道这是重复包,直接丢弃但不回复ACK。这样就能防止数据重复或丢失。
避坑指南:我曾经在一个批量传输项目中,固件里忘记切换DATA0/DATA1的序列位。结果主机发DATA0,我回DATA1;主机发DATA1,我还是回DATA1。主机直接报错,传输卡死。这个bug查了我整整两天。记住:每次成功传输后,一定要翻转数据序列位。
4.4 握手包:一次传输的“确认键”
握手包是最简单的包——只有PID字段,没有数据,没有CRC。它用来告诉对方“我收到了”或者“我出错了”。
- ACK (0xD2):数据正确接收,可以发下一个。这是最常见的握手。
- NAK (0x5A):设备忙,暂时无法接收数据。不是错误,是“请稍后再试”。
- STALL (0x1E):设备出错,无法处理。通常意味着端点被挂起或功能不支持。
- NYET (0x96):高速模式下,设备还没准备好接收下一个数据包。
性能调优关键:NAK不是错误,但过多的NAK会严重降低总线利用率。我优化过一个U盘读写性能,发现设备频繁回复NAK。原因是固件中FIFO阈值设置太低,导致缓冲区经常满。把阈值从64字节调到512字节后,NAK率从30%降到了2%,读写速度提升了近一倍。
4.5 事务处理:包与包的“舞蹈”
一次完整的事务,由三个阶段组成:令牌阶段、数据阶段、握手阶段。这三个阶段缺一不可,顺序也不能乱。
三种事务类型,对应不同的传输场景:
- OUT事务:主机向设备发送数据。令牌是OUT,数据由主机发,设备回复握手。
- IN事务:主机从设备读取数据。令牌是IN,数据由设备发,主机回复握手。
- SETUP事务:控制传输专用。令牌是SETUP,数据由主机发,设备必须回复ACK。这是USB枚举的基石。
我的经验:调试USB设备时,逻辑分析仪是你的眼睛。我习惯先看握手包:如果看到连续NAK,说明设备处理不过来;如果看到STALL,说明端点配置有问题;如果看到ACK,说明传输正常。有一次设备枚举失败,我抓包发现SETUP事务后设备回了STALL。查手册发现是端点0的最大包长配置错了,从8字节改到64字节后问题解决。
4.6 事务的超时与重试机制
USB协议规定,接收方必须在16位时间内回复握手包。如果主机没收到握手,就会重试当前事务。重试次数没有硬性限制,但固件里最好自己加个超时保护。
// 固件中处理事务超时的伪代码
uint8_t usb_transaction_retry(uint8_t endpoint, uint8_t* data, uint16_t len, uint8_t retry_count) {
for (uint8_t i = 0; i < retry_count; i++) {
// 发送数据包
usb_send_data_packet(endpoint, data, len);
// 等待握手,超时时间设为500us
uint8_t handshake = usb_wait_handshake(500);
if (handshake == HANDSHAKE_ACK) {
return TRANSACTION_SUCCESS; // 成功,退出
} else if (handshake == HANDSHAKE_STALL) {
return TRANSACTION_STALL; // 设备报错,不再重试
}
// NAK或超时,继续重试
}
return TRANSACTION_TIMEOUT; // 重试耗尽,返回超时
}
避坑指南:我曾经在一个项目中,固件里没有限制重试次数。结果设备因为某个原因一直回复NAK,主机就无限重试,导致整个USB总线被这个事务占死,其他设备全部无法通信。后来我加了重试上限(通常3~5次),并在超时后复位端点,问题解决。记住:无限重试是USB固件的大忌。
4.7 本章小结
USB的包结构和事务处理,说白了就是一套严谨的“对话规则”。从SOP到EOP,从令牌到握手,每个字段都有它的使命。理解这些底层机制,是写好USB固件的基础。下次当你看到设备枚举失败或者传输卡顿时,不妨先用逻辑分析仪抓个包,看看是哪个阶段出了问题。相信我,90%的问题都能在包级别找到线索。