4. USB协议层基础:包结构、域定义、同步字段与EOP
各位同学,今天我们进入USB协议栈最核心的一层——协议层。说白了,就是USB总线上到底在传什么东西。你写固件的时候,所有收发操作最终都会落到一个个包(Packet)上。不理解包结构,调试USB就跟蒙着眼睛走路一样。
我记得刚做USB驱动那会儿,拿着逻辑分析仪抓了一堆波形,满眼都是0和1,完全不知道哪是哪。后来把包结构搞清楚了,再看波形就像看乐谱一样清晰。好,咱们开始。
4.1 包(Packet)是什么?
USB通信的最小单位就是包。你可以把它想象成网络通信里的“帧”。一次USB事务(Transaction)由多个包组成,而每个包又由若干域(Field)拼接而成。
USB包的结构是固定的,从同步字段开始,到包结束(EOP)结束。中间依次是:包标识符(PID)、地址域、帧号域、数据域、CRC校验。不同包类型,中间这些域的长度和内容不一样。
核心要点:USB协议层不关心电气信号怎么跳变,它只关心0和1的序列怎么解析。包结构就是协议层的数据契约。
4.2 域(Field)定义详解
每个域都有明确的位宽和含义。我习惯把域分成两类:固定域和可变域。固定域每个包都有,可变域看包类型。
4.2.1 同步字段(SYNC)
同步字段是每个包的“前导码”。USB 2.0高速模式下,同步字段是32位的特定序列:0000 0000 0000 0000 0000 0000 0000 0001。全速/低速模式下是8位:0000 0001。
为什么要有同步字段?因为USB是异步总线,接收器需要靠同步字段来锁定时钟相位。说白了,就是告诉接收器:“嘿,我要开始发数据了,你准备好!”
我的经验:调试时如果发现设备经常丢包,先检查同步字段是否完整。我曾经遇到一个案子,线缆太长导致同步字段的最后一个位被噪声吃掉,接收器根本不知道包开始了。换短一点线缆就解决了。
4.2.2 包标识符(PID)
PID是包的“身份证”,4位,但实际传输时会复制一份取反的PID,总共8位。为什么?为了检错。接收器收到后比较原PID和反相PID,不一致就丢弃。
PID分为四大类:
| PID类型 | PID值(二进制) | 用途 |
|---|---|---|
| 令牌包(Token) | OUT=0001, IN=1001, SOF=0101, SETUP=1101 | 启动一次事务,指定端点地址 |
| 数据包(Data) | DATA0=0011, DATA1=1011, DATA2=0111, MDATA=1111 | 传输数据负载 |
| 握手包(Handshake) | ACK=0010, NAK=1010, STALL=1110, NYET=0110 | 确认/否认/停止/未准备好 |
| 特殊包(Special) | PRE=1100, ERR=1100, SPLIT=1000, PING=0100 | 特殊控制(如低速前导、分叉事务) |
你想想看,PID只有4位,但USB协议定义了16种包类型。每种包都有固定的格式和用途。写固件时,解析PID是第一步,PID错了后面全错。
4.2.3 地址域(ADDR)和端点域(ENDP)
令牌包里有7位地址域和4位端点域。7位地址最多支持127个设备(地址0是默认地址,留给未配置的设备)。4位端点最多16个端点(端点0是控制端点,必须支持)。
实际项目中,一个USB复合设备可能占用多个地址?不,一个设备只有一个地址,但可以有多个端点。比如一个U盘,控制端点0、批量输入端点1、批量输出端点2。地址域告诉总线“找谁”,端点域告诉设备“用哪个通道”。
避坑指南:我曾经调试一个HID设备,主机一直NAK。抓包发现地址域是对的,但端点域写成了0x0F(15),而设备只支持到端点3。主机发IN令牌给端点15,设备根本不响应。所以,端点号一定要和设备描述符里声明的一致。
4.2.4 帧号域(Frame Number)
帧号域只在SOF(Start of Frame)包里出现,11位,范围0~2047。主机每1ms(全速/低速)或125μs(高速微帧)发一个SOF包,帧号递增。设备可以用帧号做时间同步或定时。
嗯,这里要注意:帧号域不是所有包都有,只有SOF包才带。其他包没有这个域。
4.2.5 数据域(Data Field)
数据域是数据包的核心,长度0~1024字节(高速批量传输最大1024字节)。数据域的内容由上层协议决定,比如BULK传输里就是SCSI命令或文件数据。
数据包有DATA0、DATA1、DATA2、MDATA四种PID,用于数据切换同步。这个机制叫“数据翻转”,后面讲事务层时会详细说。
4.2.6 CRC校验域
CRC是包的“最后一道防线”。令牌包用5位CRC(CRC5),数据包用16位CRC(CRC16)。CRC覆盖的范围是:令牌包覆盖ADDR+ENDP,数据包覆盖数据域。
为什么令牌包用5位CRC?因为地址域+端点域总共才11位,5位CRC足够检测所有单比特错误和大部分双比特错误。数据域可能很长,所以用16位CRC。
我的习惯:写固件时,CRC计算最好用硬件模块,别自己用软件算。USB控制器通常有硬件CRC生成和校验。如果非要软件算,记得查表法比逐位计算快10倍以上。
4.3 包结束(EOP)
EOP是包的结束标志。USB 2.0高速模式下,EOP是一个特定的信号状态:SE0(Single-Ended Zero)持续一段时间。全速/低速模式下,EOP是SE0持续2位时间。
EOP的作用有两个:
- 包边界标识:接收器知道包在哪里结束
- 总线释放:EOP之后总线进入空闲状态,其他设备可以开始传输
我记得有一次调试,设备发送完数据后EOP信号不完整,主机一直等待下一个包,导致总线挂死。后来发现是DMA配置问题,数据还没传完就释放了总线。所以,EOP的时序必须严格满足USB规范。
4.4 包结构总览
为了让你更直观地理解,我画了一张USB包结构的框架图。这张图展示了从同步字段到EOP的完整流程,以及不同包类型的域差异。
4.5 包类型与事务的关系
USB通信不是发一个包就完事,而是由多个包组成一个事务。举个例子,主机想从设备读数据:
- 主机发IN令牌包(告诉设备:我要读)
- 设备发DATA数据包(把数据发回来)
- 主机发ACK握手包(确认收到)
这就是一个完整的事务。每个包都有固定的结构,缺一不可。如果设备没准备好,它会回NAK握手包,主机过一会儿再重试。
实战要点:写固件时,你不需要手动拼装每个包的比特位。USB控制器硬件会自动处理SYNC、CRC、EOP。你只需要配置PID、地址、端点、数据缓冲区。但理解底层结构对调试异常情况至关重要。
4.6 常见问题与调试技巧
最后分享几个我在项目中踩过的坑:
- PID校验失败:检查硬件是否配置了正确的传输方向。我曾经把IN和OUT搞反,主机发IN令牌,设备却准备发送数据,结果PID对不上。
- CRC错误:数据域长度和实际发送长度不一致会导致CRC计算错误。确保DMA传输长度和包长度匹配。
- EOP丢失:总线竞争或信号质量问题可能导致EOP被截断。用示波器看SE0持续时间是否达标。
- 同步字段问题:低速设备插在高速集线器上,如果集线器没有正确插入同步序列,设备可能收不到任何包。
嗯,包结构这部分内容比较基础,但它是整个USB协议栈的基石。下一层我们讲事务层,你会看到包是怎么组合成事务的。今天就到这里,有问题随时问我。