USB 描述符概述

说到 USB 描述符,我把它看作是设备跟主机沟通的“身份证”。

没有描述符,主机连你是啥东西都不知道,更别提正常工作了。我记得刚做 USB 开发那会儿,总觉得描述符就是个固定格式的数据包,随便填填就能跑。直到有一次,一个 U 盘在 Windows 上能认,在 Mac 上就报“需要供电”,折腾了三天才发现是配置描述符里的 MaxPower 写错了。

嗯,从那以后我再也不敢轻视描述符了。

描述符的作用

描述符到底干了什么事?说白了,就是告诉主机三件事:

  • 你是谁 —— 设备类型、厂商 ID、产品 ID
  • 你能干啥 —— 有多少功能、每个功能怎么通信
  • 你需要啥 —— 供电要求、端点带宽、描述符长度

主机通过枚举过程,一条一条地读取这些描述符,然后加载对应的驱动程序。你可以把主机想象成面试官,描述符就是你的简历。简历写错了,面试官直接把你 pass 掉。

我曾经踩过的坑:有一次我把设备描述符中的 bcdUSB 字段从 2.00 写成了 1.10,结果主机只允许低全速操作,高性能功能全被废了。虽然设备还能用,但吞吐量直接砍半。

描述符的层次结构

描述符不是平铺直叙的一大块数据,它是有层级关系的。我习惯把它比作一棵树:

  • 根节点:设备描述符(设备整体信息)
  • 一级分支:配置描述符(每种配置的全局参数)
  • 二级分支:接口描述符(每个功能单元的属性)
  • 三级分支:端点描述符(每个数据通道的规格)

另外还有字符串描述符,它是独立的分支,用于提供可读的文字信息。

这种树形结构,你想想看,设计得非常聪明。主机只需要顺着层级往下读,就能知道设备到底有多复杂。一个设备可以有多个配置,一个配置可以有多个接口,一个接口可以有多个端点。主机根据自身的需求,选择最合适的配置。

我个人的小习惯:每次写描述符前,先在纸上画出树形图,标出每个节点的长度和依赖关系。画完再写代码,能避免 80% 的遗漏。

USB描述符层次结构 设备描述符 (Device Descriptor) 配置描述符 1 (Configuration Descriptor) 配置描述符 N (Configuration Descriptor) 接口描述符 1 (Interface Descriptor) 接口描述符 2 (Interface Descriptor) 端点描述符 端点描述符 端点描述符 端点描述符 字符串描述符 字符串描述符是独立分支 不绑定到具体配置或接口

标准描述符 vs 类/厂商描述符

这里有个容易混淆的点,我早期也搞混过。

标准描述符是 USB 规范里强制要求的,每个设备都必须有。它们有固定的格式、固定的字节位置,主机不用猜就知道怎么解析。比如设备描述符的第一个字节永远是长度,第二个字节永远是描述符类型。

类描述符和厂商描述符就不一样了。

  • 类描述符:由特定设备类(如 HID、Mass Storage)的规范定义。拿 HID 设备来说,除了标准描述符,还必须包含一个 HID 描述符,用来描述报告格式。这个描述符的结构由 HID 类规范规定,主机通过类驱动来解析。
  • 厂商描述符:完全由厂商自定义。只要你的驱动认识就行,主机不关心里面写了啥。我见过一些 USB 加密狗,厂商描述符里直接塞了一段 AES 密钥,这种玩法标准描述符是做不到的。

它们的核心区别在于:谁负责解析。标准描述符由主机操作系统解析,类描述符由类驱动解析,厂商描述符由你写的驱动解析。

类型 定义方 是否必须 解析方式 举例
标准描述符 USB 规范 必须 主机 OS 默认解析 设备、配置、接口、端点、字符串
类描述符 设备类规范 按类要求 类驱动解析 HID 描述符、UVC 描述符
厂商描述符 厂商自定义 可选 厂商驱动解析 自定义诊断数据、固件版本信息

重点提醒:不要为了省事,把标准描述符该表达的信息藏到厂商描述符里。我见过有工程师把端点大小写死在厂商描述符中,导致主机无法正确分配资源。标准描述符是“通用语言”,乱改的话,主机听不懂。

关于描述符的获取顺序,主机是严格遵循 0x00 → 0x01 → 0x02 → 0x03 → 0x04 这样的类型值来请求的。设备端只需要按标准格式返回就行。如果哪个描述符格式不对,主机通常直接报错,不给你二次机会。

我再说一个我早期犯过的错误。写厂商描述符时,我直接在 bDescriptorType 字段填了 0xFF(厂商自定义类型),但忘记改 bLength。主机收到后,按标准长度解析,直接越界读取,设备马上断开。调试了一天,才用逻辑分析仪抓到这个问题。

实用技巧:开发初期,先用一个最简单的配置跑通枚举。只保留一个配置、一个接口、一个端点,字符串描述符先不返。枚举成功后,再逐步添加复杂度。这样出了问题,排查范围非常小。

嗯,这一节就讲到这里。描述符的层次结构和类型区分,是后续所有调试的基础。你只要能画清楚这棵“描述符树”,USB 问题基本就能解决一大半了。