USB协议分层:从物理信号到应用数据的拆解

做USB开发这些年,我见过不少工程师一上来就扎进寄存器配置,结果遇到枚举失败、传输超时,完全不知道问题出在哪一层。说白了,USB协议栈就像一栋五层小楼——物理层是地基,应用层是屋顶。哪一层出问题,症状都不一样。

今天我就把这五层拆开,一层一层讲清楚。

USB协议栈的整体架构

先看一张总图,心里有个框架。

USB协议栈五层架构 应用层 HID类、Mass Storage类、CDC类、自定义类 协议层 标准设备请求、描述符、枚举过程 事务处理层 Token包、Data包、Handshake包 数据链路层 SOP、EOP、位填充、CRC校验 物理层 D+/D-差分信号 电气特性 数据流向 应用数据 → 协议封装 → 事务拆分 → 帧打包 → 差分信号

这张图我画了很多遍。每次给团队新人讲USB,我都先让他们看这张图。记住一点:数据从上往下走是封装,从下往上走是解析。

物理层:D+和D-上的故事

物理层是USB最底下那层,负责把0和1变成电信号。USB用的是差分传输——两根线D+和D-,信号取两者的电压差。

为什么用差分?抗干扰。我在一个电机驱动项目里吃过亏,单端信号线被电机噪声打得乱七八糟,换成差分传输就稳了。USB从一开始就选了这条路。

信号状态 D+电平 D-电平 说明
J状态(空闲) 高 低 全速/低速空闲时的默认状态
K状态 低 高 与J状态相反,用于同步
SE0 低 低 复位信号,两根线都拉低
SE1 高 高 非法状态,正常通信不会出现

物理层还负责设备速度检测。主机在D+或D-上拉一个1.5kΩ电阻——全速设备拉D+,低速设备拉D-。主机检测到电平变化,就知道插了个什么设备。

小经验:调试时如果主机始终检测不到设备,先量D+和D-的电压。全速设备空闲时D+应该是3.3V左右,D-接近0V。如果两根线都是0V,检查上拉电阻有没有焊好。

数据链路层:把比特流变成数据包

物理层传的是原始比特流,数据链路层负责把这些比特流组织成有意义的包。这一层干三件事:

  • 包定界:用SOP(Start of Packet)和EOP(End of Packet)标记包的开始和结束
  • 位填充:连续6个1之后插入一个0,防止接收端失去同步
  • CRC校验:检查数据在传输过程中有没有出错

位填充这个机制,我第一次看觉得多此一举。后来用逻辑分析仪抓波形,发现没有位填充时,长串的1会让接收端的PLL失锁,数据全乱。嗯,USB设计者早就想到了。

数据链路层还定义了包的格式。一个标准的USB包长这样:

+--------+----------+--------+--------+
|  SOP   |  PID     | 数据域 |  CRC   |
| (8位)  | (8位)    | (可变) | (5/16位)|
+--------+----------+--------+--------+

PID是包标识符,告诉接收端这是个什么包——Token包、Data包还是Handshake包。CRC分两种:Token包用5位CRC,Data包用16位CRC。

注意:CRC只能检错,不能纠错。发现错误怎么办?交给上层重传。我曾经在一个项目中忽略了CRC错误处理,结果设备偶尔传错数据,排查了两天才找到原因。

事务处理层:一次完整的对话

事务处理层是USB协议里最核心的一层。一个事务由三个阶段组成:

  1. Token阶段:主机发出Token包,告诉设备要做什么(读还是写,端点号是多少)
  2. Data阶段:传输数据(主机发数据或设备发数据)
  3. Handshake阶段:接收方回应ACK/NAK/STALL,告诉发送方数据收到了还是没准备好

举个例子,主机想从设备读数据:

主机 → 设备: IN Token包 (端点1)
设备 → 主机: Data包 (64字节)
主机 → 设备: ACK包 (确认收到)

如果设备还没准备好数据,它会回复NAK。主机收到NAK后,过一会儿再重试。这就是USB的流控机制——简单但有效。

我调试过一个U盘读写速度慢的问题,用USB分析仪抓包发现设备频繁回复NAK。查到最后是Flash写入太慢,缓冲区经常空。后来加大了端点缓冲区,问题解决。

事务类型 Token包 数据流向 典型场景
IN事务 IN Token 设备 → 主机 主机读取设备数据
OUT事务 OUT Token 主机 → 设备 主机发送数据给设备
SETUP事务 SETUP Token 主机 → 设备 控制传输,发送标准请求
PING事务 PING Token 设备 → 主机(握手) 高速模式,查询设备是否可接收数据

协议层:设备枚举与标准请求

协议层处理的是USB设备枚举和标准设备请求。这一层定义了设备怎么向主机报告自己的能力——描述符就是干这个的。

设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符……这些描述符构成了设备的"身份证"。主机通过标准请求(Get Descriptor、Set Address、Set Configuration等)来读取这些信息。

枚举过程大致是这样的:

  1. 设备插入,主机检测到设备
  2. 主机复位设备,设备地址为0
  3. 主机发送Get Descriptor请求,读取设备描述符的前8个字节
  4. 主机分配新地址(Set Address)
  5. 主机在新地址上重新读取完整的设备描述符
  6. 主机读取配置描述符、接口描述符、端点描述符
  7. 主机选择配置(Set Configuration),设备进入配置状态

枚举失败是嵌入式USB开发中最常见的问题。我遇到过最离谱的一次——描述符里端点大小写错了,主机直接不认设备。查了两天,最后用USB分析仪对比正常设备的描述符才发现。

核心要点:协议层是主机和设备"握手"的地方。枚举不通过,后面什么都干不了。调试枚举问题,USB分析仪是最好的工具。

应用层:你的设备到底干什么

应用层是离用户最近的一层。这一层定义了设备的具体功能——是键盘鼠标(HID类),还是U盘(Mass Storage类),还是虚拟串口(CDC类)。

每个设备类都有自己的协议规范。比如HID设备用报告描述符定义按键和数据的格式,Mass Storage设备用SCSI命令读写块数据。

应用层的数据最终要经过下面四层的封装才能发出去。反过来,收到的数据也要从物理层一路解析到应用层。

我建议初学者先从HID设备入手。HID协议简单,不需要写驱动,Windows/Linux/macOS都原生支持。我带的实习生第一个USB项目就是做个自定义HID设备,一周就调通了。

各层之间的协作关系

五层不是各自为政,而是紧密配合。数据从应用层往下走,每一层加上自己的"信封":

应用层:   HID报告数据 (8字节)
协议层:   封装成控制传输或中断传输
事务层:   拆成IN/OUT事务
数据链路层: 加上SOP、PID、CRC
物理层:   变成D+/D-上的差分信号

接收端反过来,一层层剥掉信封,最后拿到原始数据。

这种分层设计的好处是——每一层只关心自己的事。物理层不管数据是什么含义,只管传比特。应用层不管信号怎么编码,只管解析数据。出了问题,定位也快。

调试思路:USB通信出问题,从底层往上层排查。先看物理层——D+/D-电压对不对?再看数据链路层——有没有CRC错误?然后看事务层——有没有NAK风暴?最后看协议层——枚举过了没有?一层一层排除,问题跑不掉。

好了,USB协议栈的五层架构就讲到这里。下一层一层深入的时候,你会发现每一层都有不少细节和坑。但有了这个整体框架,你至少知道自己在哪一层干活。