USB协议基础:从物理信号到设备枚举
各位同学,我们今天聊聊USB协议。别急着翻数据手册——协议这东西,光靠死记硬背是没用的。我个人习惯是先理解“为什么”,再去看“是什么”。
USB协议从1.0走到3.2,速度翻了上万倍。但核心思想没变:主机控制一切。所有通信都由主机发起,设备只能被动响应。嗯,这句话贯穿整个USB体系,你先记住它。
USB 1.0/2.0/3.0 核心差异
很多刚入行的朋友问我,这三个版本到底差在哪?我一般用一张表说明白:
| 版本 | 理论速率 | 信号编码 | 线缆最大长度 | 总线拓扑 |
|---|---|---|---|---|
| USB 1.0/1.1 | 1.5 Mbps (Low-Speed) 12 Mbps (Full-Speed) |
NRZI + 位填充 | 3 米 (LS), 5 米 (FS) | 星型 + 级联Hub |
| USB 2.0 | 480 Mbps (High-Speed) | NRZI + 位填充 | 5 米 | 星型 + 级联Hub |
| USB 3.0/3.1 | 5 Gbps (SuperSpeed) 10 Gbps (SuperSpeed+) |
8b/10b 编码 | 3 米 (铜缆) | 双总线 + 点对点 |
一个小技巧:写驱动时别只看速率。USB 2.0 High-Speed和USB 3.0 SuperSpeed的数据包结构完全不同。我见过有人用2.0的抓包工具去分析3.0的链路——结果一帧都看不懂。
USB 3.0其实是个“双面人”:它保留了USB 2.0的全套D+ D-差分信号线做兼容,又新增了一对SSTX/SSRX超速差分对。所以3.0线缆里面是9根线,不是4根。
四种传输类型
USB协议定义了四种传输类型。每一种都有明确的适用场景。你想想看,鼠标的移动数据有多重要?丢了包光标乱跳,用户肯定摔键盘。
| 传输类型 | 方向 | 特点 | 典型设备 | 纠错机制 |
|---|---|---|---|---|
| 控制传输 | 双向 | 可靠,保证送达,延迟不确定 | 枚举过程、标准请求 | 重试 + CRC |
| 批量传输 | 单向或双向 | 可靠,尽力而为,带宽弹性 | U盘、打印机、网络适配器 | 重试 + CRC |
| 中断传输 | 单向 | 可靠,保证轮询时间 | 鼠标、键盘、游戏手柄 | 重试 + CRC |
| 等时传输 | 单向 | 不可靠,保证带宽,低延迟 | USB摄像头、音频设备 | 无重试,仅CRC检测 |
避坑指南:我曾经调试过一个USB视频采集卡,死活丢帧。折腾两天才发现,等时传输的端点描述符里“bInterval”字段写错了。这个值在USB 2.0下单位是125μs,在USB 1.1下是1ms。千万别搞混。
我个人的选择原则很简单:数据丢了能重传的用批量,实时性要求高的用等时,人机交互类用中断。至于控制传输——那是USB协议的“官方语言”,所有设备都得会。
描述符:USB设备的身份证
描述符是USB设备用来“自我介绍”的数据结构。主机通过枚举过程,一条一条地把这些描述符读出来,才知道插上来的到底是个什么东西。
描述符是分层嵌套的,就像俄罗斯套娃:
- 设备描述符:最外层。告诉你设备支持哪个USB版本,有多少个配置,VID/PID是多少。
- 配置描述符:第二层。描述电源模式(自供电还是总线供电)、最大电流、接口数量。
- 接口描述符:第三层。定义功能类型(HID、Mass Storage、CDC等),以及这个接口下有几个端点。
- 端点描述符:最内层。描述每个端点的传输方向、传输类型、最大包长度、轮询间隔。
- 字符串描述符:可选。提供制造商名、产品名、序列号等人眼可读信息。
// 一个典型的设备描述符结构(USB 2.0全速设备)
struct usb_device_descriptor {
uint8_t bLength; // 描述符长度,固定18字节
uint8_t bDescriptorType; // 描述符类型,0x01
uint16_t bcdUSB; // USB规范版本号,例如0x0200
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));
实操经验:写Linux USB设备驱动时,经常要跟驱动匹配表的id_table打交道。你填写的VID/PID就是从设备描述符里读来的。我曾经遇到一个坑:厂商把PID写成了0x0000(未定义值),结果驱动怎么都匹配不上。后来走投无路用了个“USB_DEVICE_INFO”宏匹配接口描述符才搞定。
枚举过程
枚举是USB设备的“开机自检”。主机插上设备到驱动加载完毕,这里面大概走八步。我习惯把它拆成三个阶段:
| 步骤 | 主机操作 | 设备响应 | 传输类型 |
|---|---|---|---|
| 1. 检测设备 | 检测到D+/D-电平变化 | 上拉电阻激活状态 | 硬件信号 |
| 2. 复位设备 | 拉低D+/D-至少10ms | 恢复到默认地址0 | 总线状态 |
| 3. 获取设备描述符 | 发送GET_DESCRIPTOR请求 | 返回前8字节(最大包长度) | 控制传输 |
| 4. 分配地址 | 发送SET_ADDRESS请求 | 切换到新地址 | 控制传输 |
| 5. 获取完整设备描述符 | 再次发送GET_DESCRIPTOR | 返回18字节完整描述符 | 控制传输 |
| 6. 获取配置描述符 | GET_DESCRIPTOR请求配置 | 返回配置+接口+端点描述符 | 控制传输 |
| 7. 选择配置 | SET_CONFIGURATION请求 | 进入工作状态 | 控制传输 |
| 8. 加载驱动 | 内核根据VID/PID匹配驱动 | 驱动probe函数执行 | 内核机制 |
枚举过程不是一帆风顺的。我遇到过几次枚举失败:
- 最大包长度不匹配:设备描述符里写bMaxPacketSize0=64,但实际硬件只支持8字节。主机读不到完整数据,枚举卡死。
- 地址冲突:用了同一个地址的克隆设备,主机识别成老设备。拔插也没用,得重启!
- 描述符长度错误:配置描述符的wTotalLength写少了,主机后续还没读完就停,缺少接口描述符。
调试利器:在Linux内核里打开USB调试日志(echo 'module usbcore +p' > /sys/kernel/debug/dynamic_debug/control),可以看到枚举的每一步。我曾经靠这条日志定位到一个USB集线器的硬件缺陷。
好了,我们把USB协议的基础知识梳理了一遍。接下来进入核心——USB OTG的硬件结构。不过那是后面的章节了。