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的完整流程,以及不同包类型的域差异。

USB包结构框架图 USB包(Packet) SYNC PID ADDR+ENDP 帧号 数据域 CRC EOP 不同包类型的域差异 令牌包:SYNC + PID + ADDR+ENDP + CRC5 + EOP(无数据域、无帧号) 数据包:SYNC + PID + 数据域 + CRC16 + EOP(无地址域、无帧号) SOF包:SYNC + PID + 帧号 + CRC5 + EOP(无地址域、无数据域) 握手包:SYNC + PID + EOP(只有PID,无其他域) 关键说明: • SYNC:高速32位,全速/低速8位,用于时钟同步 • EOP:SE0信号,标志包结束,释放总线

4.5 包类型与事务的关系

USB通信不是发一个包就完事,而是由多个包组成一个事务。举个例子,主机想从设备读数据:

  1. 主机发IN令牌包(告诉设备:我要读)
  2. 设备发DATA数据包(把数据发回来)
  3. 主机发ACK握手包(确认收到)

这就是一个完整的事务。每个包都有固定的结构,缺一不可。如果设备没准备好,它会回NAK握手包,主机过一会儿再重试。

实战要点:写固件时,你不需要手动拼装每个包的比特位。USB控制器硬件会自动处理SYNC、CRC、EOP。你只需要配置PID、地址、端点、数据缓冲区。但理解底层结构对调试异常情况至关重要。

4.6 常见问题与调试技巧

最后分享几个我在项目中踩过的坑:

  • PID校验失败:检查硬件是否配置了正确的传输方向。我曾经把IN和OUT搞反,主机发IN令牌,设备却准备发送数据,结果PID对不上。
  • CRC错误:数据域长度和实际发送长度不一致会导致CRC计算错误。确保DMA传输长度和包长度匹配。
  • EOP丢失:总线竞争或信号质量问题可能导致EOP被截断。用示波器看SE0持续时间是否达标。
  • 同步字段问题:低速设备插在高速集线器上,如果集线器没有正确插入同步序列,设备可能收不到任何包。

嗯,包结构这部分内容比较基础,但它是整个USB协议栈的基石。下一层我们讲事务层,你会看到包是怎么组合成事务的。今天就到这里,有问题随时问我。