3、USB协议分层架构:物理层、数据链路层、事务层、协议层功能划分,以及各层之间的数据封装与传递机制
USB协议栈之所以能够支撑从高速存储到低速键盘的广泛设备类型,其核心在于采用了清晰的分层架构。与OSI七层模型类似,USB将通信过程划分为物理层、数据链路层、事务层和协议层四个主要层级。每一层只负责特定的职责,并通过标准化的接口与上下层交互。理解这种分层设计,不仅是阅读USB 2.0/3.x规范的基础,更是后续进行HID(人机接口设备)和CDC(通信设备类)固件开发时,定位问题、优化吞吐量的关键前提。本章将逐一拆解各层的功能边界,并重点剖析数据在发送与接收路径上的封装与解封装过程。
上图展示了USB 2.0协议栈的纵向分层结构以及数据在Host与Device之间的双向流动路径。在后续的章节中,我们将沿着这条路径,从最底层的电气信号开始,逐层向上剖析每一层的具体职责、关键机制以及常见的设计陷阱。
3.1 物理层:比特流的电气承载
物理层是USB体系中最接近硬件的层级,它定义了信号如何通过D+和D-两根差分线进行传输。与UART、I2C等单端信号不同,USB采用差分信号传输,这意味着逻辑电平由两根线之间的电压差决定,而非单根线对地的电压。这种设计带来了更强的抗共模干扰能力,也是USB线缆可以做到较长距离(全速5米)稳定传输的基础。
核心原理:差分信号与速度识别
在USB 2.0规范中,物理层需要支持三种速率模式:低速(Low-Speed,1.5 Mbps)、全速(Full-Speed,12 Mbps)和高速(High-Speed,480 Mbps)。设备在上电或连接时,通过D+/D-线上的上拉电阻位置来宣告自己的速率能力:全速设备在D+线上接1.5kΩ上拉电阻,低速设备则在D-线上接1.5kΩ上拉电阻。高速设备首先以全速模式宣告,然后在复位序列中通过特定的握手信号(Chirp K-J序列)切换到高速模式。
物理层的另一个关键职责是信号编码。USB使用NRZI(Non-Return-to-Zero Inverted)编码,即电平跳变表示逻辑0,电平不变表示逻辑1。为了确保接收端能够从数据流中恢复时钟,发送端在连续遇到6个逻辑1时,会强制插入一个逻辑0(位填充)。接收端在解码时需要识别并丢弃这个填充位。
判断方法:物理层故障的快速定位
- 检查设备枚举状态:如果Host完全无法识别设备,首先用万用表测量D+/D-对地电压。空闲状态下,全速/高速设备D+应为3.3V左右,D-应为0V;低速设备则相反。
- 观察复位时序:使用示波器捕获设备插入瞬间的波形。正常全速设备会先拉高D+,Host检测到后发送复位信号(SE0状态,即D+/D-均为低),持续至少10ms。
- 测量信号眼图:对于高速设备,在Host与Device之间接入USB测试夹具,使用眼图模板(Mask)测试。若眼图触碰模板边界,则说明信号质量不佳,需要检查PCB走线、连接器焊接或线缆质量。
案例拆解:高速设备枚举失败
背景:某产品使用STM32H743(内置USB 2.0高速PHY)设计了一款数据采集设备,PCB为4层板,D+/D-走线长度约25mm,但未做阻抗控制。
现象:设备连接电脑后,系统提示“无法识别的USB设备”,设备管理器中无任何新增节点。
分析过程:首先测量D+电压,发现为0V,说明设备未正确上拉。进一步检查固件,发现PHY时钟配置错误,导致内部PLL未锁定,PHY无法正常工作。修正时钟配置后,D+电压恢复为3.3V,但设备仍无法枚举。此时用示波器观察复位信号后的Chirp序列,发现设备发出的K-J脉冲幅度不足(仅200mV,正常应大于800mV)。
根因:由于PCB未做阻抗控制,实际差分阻抗约为120Ω,与要求的90Ω偏差过大,导致信号反射严重,Chirp信号衰减。同时,D+/D-走线长度不一致(相差约8mm),引入了共模噪声。
解决方案:重新投板,将D+/D-走线调整为等长(误差<1mm),并在靠近芯片端并联一个100Ω的差分终端电阻(实际使用两个50Ω串联到虚拟地)。修改后,设备成功枚举,高速模式握手正常。
3.2 数据链路层:包的可靠传输
数据链路层负责将物理层传来的原始比特流组织成具有特定格式的“包”(Packet),并确保包在传输过程中的完整性。这一层是USB协议中最具“通信协议”色彩的部分,它定义了包的边界、类型以及错误检测机制。
核心原理:包格式与PID
USB的包由以下几个字段组成:
- SYNC字段:每个包都以SYNC开始,用于接收端的时钟同步。全速/低速模式下SYNC为8位(00000001),高速模式下为32位。
- PID字段:包标识符,共8位,其中低4位是包类型编码,高4位是低4位的取反,用于错误校验。常见的PID包括:令牌包(IN、OUT、SETUP、SOF)、数据包(DATA0、DATA1、DATA2、MDATA)、握手包(ACK、NAK、STALL、NYET)以及特殊包(PRE、ERR等)。
- 地址字段(ADDR):7位,用于指定设备地址,最多支持127个设备。
- 端点字段(ENDP):4位,用于指定设备内的端点号,每个设备最多支持16个端点。
- 帧号字段(Frame Number):仅SOF包包含,11位,用于同步帧计数。
- 数据字段(Data):0~1024字节(高速模式),具体长度由端点描述符决定。
- CRC字段:循环冗余校验。令牌包使用5位CRC,数据包使用16位CRC。
- EOP字段:包结束标志,由SE0状态(D+/D-均为低)表示,持续2个位时间。
操作流程:发送一个数据包的封装步骤
- 协议层将待发送的数据(如HID报告)交给事务层。
- 事务层根据传输类型(控制、批量、中断、同步)决定使用DATA0还是DATA1数据包(数据切换机制,用于检测丢包)。
- 数据链路层在数据字段前添加SYNC、PID、ADDR、ENDP字段,在数据字段后计算并添加CRC16。
- 物理层对上述比特流进行NRZI编码,并在连续6个1后插入填充0。
- 发送完成后,物理层输出EOP信号。
案例拆解:批量传输中的数据切换(Data Toggle)
场景:Host向一个批量输出端点发送64字节数据。数据被分成两个包:DATA0和DATA1。
过程:Host先发送DATA0包,设备正确接收后回复ACK。Host收到ACK后,将数据切换位翻转,发送DATA1包。如果设备在接收DATA1时发现CRC错误,则丢弃该包,不回复ACK。Host等待ACK超时后,会重发DATA1包(注意:不是DATA0)。设备由于之前已成功接收DATA0,其内部数据切换位已翻转为1,因此能够识别重发的DATA1是新的数据。
关键点:如果设备在接收DATA0后回复ACK,但ACK在传输过程中丢失,Host会重发DATA0。此时设备的数据切换位已翻转为1,收到DATA0后发现不匹配,会将其视为重复包并丢弃,但仍回复ACK以同步Host的状态。这种机制有效防止了因ACK丢失导致的数据重复。
3.3 事务层:一次完整的交互
事务层是USB协议的核心调度单元。一个事务由一系列包组成,通常包括一个令牌包、一个可选的数据包和一个可选的握手包。事务层负责管理这些包的顺序、时序以及错误重试。
核心原理:三种事务类型
| 事务类型 | 令牌包 | 数据包 | 握手包 | 典型应用 |
|---|---|---|---|---|
| IN事务 | IN (Host→Device) | Device→Host (DATA0/1) | Host→Device (ACK/NAK/STALL) | 读取数据(如鼠标移动、CDC接收) |
| OUT事务 | OUT (Host→Device) | Host→Device (DATA0/1) | Device→Host (ACK/NAK/STALL) | 发送数据(如键盘LED、CDC发送) |
| SETUP事务 | SETUP (Host→Device) | Host→Device (DATA0,固定8字节) | Device→Host (ACK/NAK/STALL) | 控制传输的建立阶段 |
事务层还负责处理流控。当设备端点尚未准备好接收数据时,会回复NAK,Host收到NAK后会稍后重试。当端点发生错误时,会回复STALL,Host将停止对该端点的访问并通知驱动程序。
操作流程:控制传输的三个阶段
控制传输是USB中唯一必须支持传输类型,它由建立阶段、数据阶段(可选)和状态阶段组成。以读取设备描述符为例:
- 建立阶段:Host发送SETUP事务,数据包为8字节的标准请求(bmRequestType=0x80, bRequest=0x06, wValue=0x0100, wIndex=0x0000, wLength=0x12)。设备必须回复ACK。
- 数据阶段:Host发送IN事务,设备返回18字节的设备描述符数据。如果数据超过端点最大包长(通常为64字节),则分多个IN事务传输。每个事务的数据包PID在DATA0和DATA1之间交替。
- 状态阶段:Host发送OUT事务,数据包长度为0,设备回复ACK,表示整个控制传输成功完成。
案例拆解:中断传输的NAK风暴
背景:一个HID键盘设备,中断IN端点配置为轮询间隔10ms,最大包长8字节。
现象:设备工作正常,但通过USB分析仪抓包发现,Host每10ms发送一次IN令牌,设备在没有按键时回复NAK。这导致总线上充满了无意义的NAK包,占用了约0.8%的带宽(8字节/10ms / 12Mbps ≈ 0.8%)。
分析:对于HID设备,在没有新数据时回复NAK是规范允许的,但效率较低。更优的做法是使用“唤醒”机制或减少轮询间隔。然而,对于电池供电的无线USB设备,频繁的NAK会消耗设备端的接收功耗。
优化方案:在固件中,当没有按键变化时,将端点设置为STALL状态(不推荐,会导致Host报错),或者利用USB 2.0的“链路电源管理”(LPM)扩展,让设备在空闲时进入低功耗状态,Host会暂停轮询。对于有线设备,保持NAK是简单且可靠的做法,无需过度优化。
3.4 协议层:设备功能的语义化表达
协议层是USB协议栈中最接近应用的一层,它定义了Host与设备之间交换的数据所代表的“含义”。这一层不再关心比特和包,而是关注“这个设备是什么”以及“如何与它通信”。
核心原理:描述符与类协议
协议层的核心是描述符(Descriptor)体系。设备通过描述符向Host宣告自身的能力和配置需求。主要的描述符包括:
- 设备描述符(Device Descriptor):包含USB版本号、设备类、厂商ID(VID)、产品ID(PID)等信息。
- 配置描述符(Configuration Descriptor):描述设备的电源需求、接口数量等。
- 接口描述符(Interface Descriptor):描述接口的类、子类、协议以及端点数量。HID设备通常使用接口描述符中的bInterfaceClass=0x03。
- 端点描述符(Endpoint Descriptor):描述端点的地址、传输类型、最大包长和轮询间隔。
- 字符串描述符(String Descriptor):提供可读的厂商名、产品名等。
类协议(如HID、CDC、Mass Storage)则定义了在标准描述符之上更具体的通信格式。例如,HID协议使用报告描述符(Report Descriptor)来详细描述输入/输出数据的位字段含义;CDC协议则定义了抽象控制模型(ACM)和以太网控制模型(NCM)等子类。
操作流程:设备枚举过程中的协议层交互
- Host发送GET_DESCRIPTOR请求获取设备描述符(前8字节)。
- Host发送SET_ADDRESS请求,为设备分配唯一地址(1~127)。
- Host再次发送GET_DESCRIPTOR请求获取完整设备描述符。
- Host发送GET_DESCRIPTOR请求获取配置描述符(包括接口和端点描述符)。
- Host根据配置描述符加载相应的类驱动程序(如HID驱动、CDC驱动)。
- Host发送SET_CONFIGURATION请求,设备进入配置状态,开始正常工作。
案例拆解:自定义HID设备的报告描述符错误
背景:开发一个自定义HID设备,用于传输16通道的16位ADC数据。报告描述符中定义了16个Input字段,每个字段16位。
现象:设备在Windows下枚举成功,但使用HIDAPI读取数据时,每次只能读取到前4个通道的数据,后12个通道的数据始终为0。
分析:检查报告描述符,发现Input字段的Report Size设置为16,Report Count设置为16,但逻辑最小值(Logical Minimum)设置为-32768,逻辑最大值(Logical Maximum)设置为32767。问题在于,Windows的HID驱动默认将Input报告解析为有符号整数,而ADC输出的是无符号整数(0~65535)。当ADC值超过32767时,被解析为负数,在应用层转换为有符号short后,高字节被截断。
解决方案:将报告描述符中的Logical Minimum改为0,Logical Maximum改为65535,并将Usage设置为Generic Desktop/Undefined。修改后,所有16通道数据均能正确读取。
3.5 分层架构的协同与数据封装全景
理解USB分层架构的关键在于认识到每一层都是“无状态”的——它们只负责处理自己那一层的协议单元,而不关心上层数据的含义。这种设计使得USB协议具有极强的扩展性:例如,USB 3.0在物理层引入了8b/10b编码和双单工通道,但事务层和协议层的基本概念仍然沿用。
在实际开发中,分层架构带来的最大好处是“关注点分离”。当设备出现通信故障时,我们可以按照从底层到顶层的顺序逐层排查:先看物理层信号是否正常,再看链路层包格式是否正确,然后分析事务层是否有NAK/STALL异常,最后检查协议层描述符和类协议是否符合规范。这种系统化的排查方法,远比盲目修改代码要高效得多。