USB协议分层:从物理信号到应用数据的拆解
做USB开发这些年,我见过不少工程师一上来就扎进寄存器配置,结果遇到枚举失败、传输超时,完全不知道问题出在哪一层。说白了,USB协议栈就像一栋五层小楼——物理层是地基,应用层是屋顶。哪一层出问题,症状都不一样。
今天我就把这五层拆开,一层一层讲清楚。
USB协议栈的整体架构
先看一张总图,心里有个框架。
这张图我画了很多遍。每次给团队新人讲USB,我都先让他们看这张图。记住一点:数据从上往下走是封装,从下往上走是解析。
物理层:D+和D-上的故事
物理层是USB最底下那层,负责把0和1变成电信号。USB用的是差分传输——两根线D+和D-,信号取两者的电压差。
为什么用差分?抗干扰。我在一个电机驱动项目里吃过亏,单端信号线被电机噪声打得乱七八糟,换成差分传输就稳了。USB从一开始就选了这条路。
| 信号状态 | D+电平 | D-电平 | 说明 |
|---|---|---|---|
| J状态(空闲) | 高 | 低 | 全速/低速空闲时的默认状态 |
| K状态 | 低 | 高 | 与J状态相反,用于同步 |
| SE0 | 低 | 低 | 复位信号,两根线都拉低 |
| SE1 | 高 | 高 | 非法状态,正常通信不会出现 |
物理层还负责设备速度检测。主机在D+或D-上拉一个1.5kΩ电阻——全速设备拉D+,低速设备拉D-。主机检测到电平变化,就知道插了个什么设备。
数据链路层:把比特流变成数据包
物理层传的是原始比特流,数据链路层负责把这些比特流组织成有意义的包。这一层干三件事:
- 包定界:用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。
事务处理层:一次完整的对话
事务处理层是USB协议里最核心的一层。一个事务由三个阶段组成:
- Token阶段:主机发出Token包,告诉设备要做什么(读还是写,端点号是多少)
- Data阶段:传输数据(主机发数据或设备发数据)
- 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等)来读取这些信息。
枚举过程大致是这样的:
- 设备插入,主机检测到设备
- 主机复位设备,设备地址为0
- 主机发送Get Descriptor请求,读取设备描述符的前8个字节
- 主机分配新地址(Set Address)
- 主机在新地址上重新读取完整的设备描述符
- 主机读取配置描述符、接口描述符、端点描述符
- 主机选择配置(Set Configuration),设备进入配置状态
枚举失败是嵌入式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协议栈的五层架构就讲到这里。下一层一层深入的时候,你会发现每一层都有不少细节和坑。但有了这个整体框架,你至少知道自己在哪一层干活。