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的硬件结构。不过那是后面的章节了。

USB 枚举流程图 检测设备连接 主机复位设备 获取设备描述符前8字节 设置设备地址 获取完整设备描述符 获取配置描述符集 选择配置 加载驱动并probe 枚举失败的常见原因 • 最大包长度不匹配 • 描述符长度错误 调试建议 • 开启内核USB调试日志 • 使用USB分析仪抓包