USB 描述符概述
说到 USB 描述符,我把它看作是设备跟主机沟通的“身份证”。
没有描述符,主机连你是啥东西都不知道,更别提正常工作了。我记得刚做 USB 开发那会儿,总觉得描述符就是个固定格式的数据包,随便填填就能跑。直到有一次,一个 U 盘在 Windows 上能认,在 Mac 上就报“需要供电”,折腾了三天才发现是配置描述符里的 MaxPower 写错了。
嗯,从那以后我再也不敢轻视描述符了。
描述符的作用
描述符到底干了什么事?说白了,就是告诉主机三件事:
- 你是谁 —— 设备类型、厂商 ID、产品 ID
- 你能干啥 —— 有多少功能、每个功能怎么通信
- 你需要啥 —— 供电要求、端点带宽、描述符长度
主机通过枚举过程,一条一条地读取这些描述符,然后加载对应的驱动程序。你可以把主机想象成面试官,描述符就是你的简历。简历写错了,面试官直接把你 pass 掉。
我曾经踩过的坑:有一次我把设备描述符中的 bcdUSB 字段从 2.00 写成了 1.10,结果主机只允许低全速操作,高性能功能全被废了。虽然设备还能用,但吞吐量直接砍半。
描述符的层次结构
描述符不是平铺直叙的一大块数据,它是有层级关系的。我习惯把它比作一棵树:
- 根节点:设备描述符(设备整体信息)
- 一级分支:配置描述符(每种配置的全局参数)
- 二级分支:接口描述符(每个功能单元的属性)
- 三级分支:端点描述符(每个数据通道的规格)
另外还有字符串描述符,它是独立的分支,用于提供可读的文字信息。
这种树形结构,你想想看,设计得非常聪明。主机只需要顺着层级往下读,就能知道设备到底有多复杂。一个设备可以有多个配置,一个配置可以有多个接口,一个接口可以有多个端点。主机根据自身的需求,选择最合适的配置。
我个人的小习惯:每次写描述符前,先在纸上画出树形图,标出每个节点的长度和依赖关系。画完再写代码,能避免 80% 的遗漏。
标准描述符 vs 类/厂商描述符
这里有个容易混淆的点,我早期也搞混过。
标准描述符是 USB 规范里强制要求的,每个设备都必须有。它们有固定的格式、固定的字节位置,主机不用猜就知道怎么解析。比如设备描述符的第一个字节永远是长度,第二个字节永远是描述符类型。
类描述符和厂商描述符就不一样了。
- 类描述符:由特定设备类(如 HID、Mass Storage)的规范定义。拿 HID 设备来说,除了标准描述符,还必须包含一个 HID 描述符,用来描述报告格式。这个描述符的结构由 HID 类规范规定,主机通过类驱动来解析。
- 厂商描述符:完全由厂商自定义。只要你的驱动认识就行,主机不关心里面写了啥。我见过一些 USB 加密狗,厂商描述符里直接塞了一段 AES 密钥,这种玩法标准描述符是做不到的。
它们的核心区别在于:谁负责解析。标准描述符由主机操作系统解析,类描述符由类驱动解析,厂商描述符由你写的驱动解析。
| 类型 | 定义方 | 是否必须 | 解析方式 | 举例 |
|---|---|---|---|---|
| 标准描述符 | USB 规范 | 必须 | 主机 OS 默认解析 | 设备、配置、接口、端点、字符串 |
| 类描述符 | 设备类规范 | 按类要求 | 类驱动解析 | HID 描述符、UVC 描述符 |
| 厂商描述符 | 厂商自定义 | 可选 | 厂商驱动解析 | 自定义诊断数据、固件版本信息 |
重点提醒:不要为了省事,把标准描述符该表达的信息藏到厂商描述符里。我见过有工程师把端点大小写死在厂商描述符中,导致主机无法正确分配资源。标准描述符是“通用语言”,乱改的话,主机听不懂。
关于描述符的获取顺序,主机是严格遵循 0x00 → 0x01 → 0x02 → 0x03 → 0x04 这样的类型值来请求的。设备端只需要按标准格式返回就行。如果哪个描述符格式不对,主机通常直接报错,不给你二次机会。
我再说一个我早期犯过的错误。写厂商描述符时,我直接在 bDescriptorType 字段填了 0xFF(厂商自定义类型),但忘记改 bLength。主机收到后,按标准长度解析,直接越界读取,设备马上断开。调试了一天,才用逻辑分析仪抓到这个问题。
实用技巧:开发初期,先用一个最简单的配置跑通枚举。只保留一个配置、一个接口、一个端点,字符串描述符先不返。枚举成功后,再逐步添加复杂度。这样出了问题,排查范围非常小。
嗯,这一节就讲到这里。描述符的层次结构和类型区分,是后续所有调试的基础。你只要能画清楚这棵“描述符树”,USB 问题基本就能解决一大半了。