1. USB协议基础回顾:USB 2.0与USB 3.x架构差异、电气特性与信号完整性基础、描述符体系详解
做USB驱动开发,绕不开协议基础。很多新手一上来就扎进代码,结果遇到设备枚举失败、传输超时,完全摸不着头脑。我个人习惯是,先花点时间把协议层面的东西理清楚,后面调试才能有的放矢。
这一节,我们重点看三个东西:USB 2.0和USB 3.x到底差在哪、信号完整性为什么是硬门槛、以及那套让无数人头疼的描述符体系。嗯,都是干货。
1.1 USB 2.0与USB 3.x架构差异
先看一张整体架构对比图,心里有个谱。
说白了,USB 2.0 和 USB 3.x 是两套完全不同的体系。不只是速度翻倍那么简单。
| 对比项 | USB 2.0 (高速) | USB 3.x (超高速) |
|---|---|---|
| 信号速率 | 480 Mbps | 5 Gbps (3.0) / 10 Gbps (3.1) / 20 Gbps (3.2) |
| 传输模式 | 半双工 (半双工) | 全双工 (全双工) |
| 物理层 | 差分对 D+/D- | 差分对 SSTX+/SSTX- 和 SSRX+/SSRX- |
| 编码方式 | NRZI + 位填充 | 8b/10b (3.0) / 128b/132b (3.1+) |
| 电源管理 | 轮询式,设备被动 | 异步通知,支持 U0-U3 链路状态 |
| 主机控制器 | EHCI (或 OHCI/UHCI) | xHCI (可同时管理 USB 2.0 和 3.x) |
我记得刚接触 USB 3.0 时,最让我不适应的是它的链路训练机制。USB 2.0 上电就能跑,USB 3.0 却要先做链路训练(LTSSM),状态机复杂得多。调试时如果发现设备枚举卡在某个状态,多半是链路训练没通过。
1.2 电气特性与信号完整性基础
做驱动开发,电气特性可以不用深究到晶体管级别,但信号完整性(SI)的基本概念必须懂。为什么?因为很多驱动层面的诡异问题,根因都在物理层。
举个例子。USB 2.0 高速模式要求 D+ 和 D- 的差分阻抗为 90Ω ±15%。如果 PCB 走线阻抗不匹配,信号反射就会导致眼图闭合。眼图一闭合,CRC 错误就来了,驱动层看到的就是传输超时或设备断开。
我曾经遇到一个项目,设备在实验室跑得好好的,到客户现场就频繁掉线。折腾了两周,最后发现是客户用的 USB 延长线质量太差,阻抗不匹配。换根线就好了。嗯,这种坑踩过一次就记住了。
| 参数 | USB 2.0 高速 | USB 3.0 超高速 | 说明 |
|---|---|---|---|
| 差分阻抗 | 90Ω ±15% | 90Ω ±15% | 走线必须严格控制 |
| 共模电压 | 200 mV (摆幅) | 800-1200 mV (差分摆幅) | USB 3.x 信号幅度更大 |
| 抖动容限 | ±100 ps | ±25 ps | USB 3.x 对抖动更敏感 |
| 最大线缆长度 | 5 米 | 3 米 (推荐) | 超过长度信号衰减严重 |
1.3 描述符体系详解
描述符是 USB 设备的“身份证”。驱动通过枚举过程读取描述符,才知道设备是什么、能干什么、怎么通信。我见过太多人栽在描述符上,尤其是配置描述符和接口描述符的嵌套关系搞不清楚。
描述符的层级关系,看这张图就明白了。
枚举时,主机先发 GET_DESCRIPTOR 请求获取设备描述符,然后获取配置描述符(注意,配置描述符会连同接口和端点描述符一起返回)。驱动解析时,必须按这个层级结构逐层解析。
下面是一个典型的设备描述符结构,用 C 语言定义看更直观。
// USB 设备描述符 (USB 2.0 规范 9.6.1)
struct usb_device_descriptor {
uint8_t bLength; // 描述符长度 (18字节)
uint8_t bDescriptorType; // 描述符类型 (0x01)
uint16_t bcdUSB; // USB 规范版本号 (如 0x0200 表示 USB 2.0)
uint8_t bDeviceClass; // 设备类
uint8_t bDeviceSubClass; // 设备子类
uint8_t bDeviceProtocol; // 设备协议
uint8_t bMaxPacketSize0; // 端点0最大包大小
uint16_t idVendor; // 厂商ID (VID)
uint16_t idProduct; // 产品ID (PID)
uint16_t bcdDevice; // 设备版本号
uint8_t iManufacturer; // 厂商字符串描述符索引
uint8_t iProduct; // 产品字符串描述符索引
uint8_t iSerialNumber; // 序列号字符串描述符索引
uint8_t bNumConfigurations; // 配置描述符数量
} __attribute__((packed));
bcdUSB、idVendor、idProduct 和 bMaxPacketSize0。如果 bMaxPacketSize0 的值不对(比如 USB 2.0 高速设备应该是 64,结果读到 8),那基本可以断定设备描述符有问题,或者枚举过程被干扰了。
接口描述符和端点描述符是驱动开发最常打交道的。接口描述符定义了一个功能单元,端点描述符定义了实际的数据通道。
// 接口描述符
struct usb_interface_descriptor {
uint8_t bLength; // 9字节
uint8_t bDescriptorType; // 0x04
uint8_t bInterfaceNumber; // 接口编号
uint8_t bAlternateSetting; // 备用设置
uint8_t bNumEndpoints; // 端点数量 (不包括端点0)
uint8_t bInterfaceClass; // 接口类
uint8_t bInterfaceSubClass; // 接口子类
uint8_t bInterfaceProtocol; // 接口协议
uint8_t iInterface; // 接口字符串索引
} __attribute__((packed));
// 端点描述符
struct usb_endpoint_descriptor {
uint8_t bLength; // 7字节
uint8_t bDescriptorType; // 0x05
uint8_t bEndpointAddress; // 端点地址 (bit7: 方向, bit3-0: 端点号)
uint8_t bmAttributes; // 传输类型 (bit1-0: 0=控制, 1=等时, 2=批量, 3=中断)
uint16_t wMaxPacketSize; // 最大包大小
uint8_t bInterval; // 轮询间隔 (中断/等时端点)
} __attribute__((packed));
这里有个容易踩的坑:bEndpointAddress 的 bit7 表示方向,1 是 IN(设备到主机),0 是 OUT(主机到设备)。很多新手搞反了,导致数据传输一直失败。我刚开始也犯过这个错,后来养成了习惯,每次解析端点描述符都打印一下方向位。
bAlternateSetting 没有正确设置。音频设备通常有多个备用设置(比如不同采样率),驱动必须根据需求选择正确的备用设置。如果选错了,端点描述符里的 wMaxPacketSize 就不对,数据传不过去。所以,处理复合接口或多备用设置的设备时,一定要仔细检查 bAlternateSetting 的切换逻辑。
最后,总结一下描述符调试的核心思路:
- 设备描述符:确认 VID/PID 是否正确,bcdUSB 是否匹配预期版本。
- 配置描述符:检查总长度,确保解析时没有截断。
- 接口描述符:确认接口类/子类/协议,这是驱动匹配的关键。
- 端点描述符:核对端点地址、传输类型、最大包大小,这三个参数决定了驱动怎么收发数据。
嗯,这一节的内容就到这里。USB 协议基础是后面所有调试工作的根基,花时间吃透它,绝对值。