3. 设备描述符详解
说到枚举,大家第一个接触的就是设备描述符。嗯,它就像USB设备的身份证,主机一上来就先要这18个字节。我刚开始做USB项目时,总觉得这玩意儿简单,不就是填几个数嘛。结果有一次设备死活枚举不上,最后发现是bMaxPacketSize0填错了——你说冤不冤?
描述符是什么?
描述符是一段结构化的数据,用来告诉主机“我是谁、我支持什么、怎么跟我通信”。设备描述符是枚举流程的第一个描述符,所有设备都必须支持它。主机通过控制传输、默认端点0、请求GET_DESCRIPTOR(DEVICE)拿到这段数据。
设备描述符结构
设备描述符固定18个字节,USB规范定义了每个字段的偏移、大小和含义。我自己最喜欢用一张表来对比记忆,各位可以看看:
| 偏移(Dec) | 字段名 | 大小(Byte) | 含义 | 我踩过的坑 |
|---|---|---|---|---|
| 0 | bLength | 1 | 描述符长度,固定0x12(18) | 有人写成0x10,主机直接拒绝 |
| 1 | bDescriptorType | 1 | 描述符类型,设备描述符固定0x01 | 填错类型上位机会解析乱码 |
| 2 | bcdUSB | 2 | USB规范版本号,BCD码,如0x0200表示USB 2.0 | USB 2.0设备填成0x0110会丢失高速能力 |
| 4 | bDeviceClass | 1 | 设备类代码,0x00表示在接口描述符定义 | 复合设备如果在这里填了非0类,可能冲突 |
| 5 | bDeviceSubClass | 1 | 子类代码,与bDeviceClass配合 | 一般填0x00,除非你知道自己在做什么 |
| 6 | bDeviceProtocol | 1 | 协议代码,0x00表示接口描述符定义 | HID设备这里必须填0x00 |
| 7 | bMaxPacketSize0 | 1 | 端点0最大包大小:8/16/32/64 | 这是重灾区。很多初学者把全速设备填成64,枚举直接挂掉 |
| 8 | idVendor | 2 | 供应商ID,从USB-IF购买 | 用别人的VID会被律师函警告 |
| 10 | idProduct | 2 | 产品ID,厂商自定义 | 同一供应商下不能重复 |
| 12 | bcdDevice | 2 | 设备版本号,BCD码 | 用于区分硬件版本,更新固件后记得改 |
| 14 | iManufacturer | 1 | 制造商字符串描述符索引,0表示无 | 建议至少提供一个厂商名,认证时方便debug |
| 15 | iProduct | 1 | 产品字符串描述符索引 | Windows会用它来显示设备名称 |
| 16 | iSerialNumber | 1 | 序列号字符串描述符索引,0表示无 | 多设备同时使用时,建议带序列号 |
| 17 | bNumConfigurations | 1 | 配置描述符数量,通常为1 | 至少1个,最多255个 |
字段详解
我们挑几个容易出错的字段重点说说。
bMaxPacketSize0:这个字段由设备速度决定。低速设备固定8字节,全速设备可以是8、16、32、64,高速设备固定64。有个冷知识:全速设备如果填64,主机可能会认为你是高速设备,然后出错。我遇到过一款定制芯片,默认配置就是64,低速模式下枚举始终失败,找了两天才发现。
bcdUSB:BCD码格式,高字节是高两位十进制数,低字节是低两位。例如0x0200表示USB 2.0,0x0110表示USB 1.1。注意USB 3.0设备这里要填0x0300,但枚举时的初始通信还是按USB 2.0走。
bDeviceClass/bDeviceSubClass/bDeviceProtocol:这三个字段共同决定设备类型。大部分复合设备(如带HID和MSC的复合体)应该全部填0x00,把类定义下放到接口描述符。我曾经接手过一个项目,产品经理要求在设备描述符填“ff”表示自定义类,结果导致Windows无法自动加载驱动,只能靠inf文件强匹配。
标准要求与常见问题
- 长度必须准确:bLength=0x12多一字节少一字节都不行,主机不会解析不完整的数据。
- bMaxPacketSize0合法值:低速8、全速8/16/32/64、高速64。我建议全速设备统一用64,这样传输效率更高,前提是你的芯片支持。
- 设备类不能乱填:如果你要实现U盘,bDeviceClass要填0x00(接口描述符定义),而不是0x08(大容量存储类在接口级别)。
- 字符串索引:iManufacturer、iProduct、iSerialNumber如果非零,必须对应一个有效的字符串描述符。空索引(0)不影响枚举,但用户看不到设备名称,debug体验很差。
- bNumConfigurations:至少为1。如果为0,主机认为设备无效。
代码示例
下面是一个标准的设备描述符结构体定义,我用C语言写的,各位可以拿去用:
typedef __packed struct {
uint8_t bLength; // 0x12
uint8_t bDescriptorType; // 0x01
uint16_t bcdUSB; // 0x0200
uint8_t bDeviceClass; // 0x00
uint8_t bDeviceSubClass; // 0x00
uint8_t bDeviceProtocol; // 0x00
uint8_t bMaxPacketSize0; // 64 (high speed)
uint16_t idVendor; // 0x1234
uint16_t idProduct; // 0x5678
uint16_t bcdDevice; // 0x0100
uint8_t iManufacturer; // 1
uint8_t iProduct; // 2
uint8_t iSerialNumber; // 3
uint8_t bNumConfigurations; // 1
} device_descriptor_t;
实际工程中,这些字段往往定义在USB配置数组中,或者由编译时宏生成。我个人习惯把VID/PID放在单独的配置头文件里,方便不同项目复用。
枚举流程中的角色
设备描述符是整个枚举的第一步。主机在复位设备后,通过端点0发送请求获取它。拿到数据后,主机会根据bMaxPacketSize0重新设置端点0的包大小,然后继续获取配置描述符。
上图展示了从主机复位到获取完整描述符的完整链条。一旦设备描述符出错,整个枚举流程就卡在第二步或第三步。我记得有一次帮客户调试,USB分析仪显示主机不断重试GET_DESCRIPTOR,最后发现是bMaxPacketSize0返回0x40(64)但设备实际上只支持全速,主机按高速去沟通,自然失败。
bcdUSB写成0x200(不加BCD转换),结果主机认为是USB 0.0200。这类低级错误往往出现在复制粘贴代码时。建议使用宏定义,比如#define USB_BCD_VERSION 0x0200,一目了然。
好了,设备描述符的内容就讲到这里。字段不多,但每个字都关系到枚举成败。下次遇到枚举失败,先抓个USB分析仪看看设备描述符返回的是什么——八成能找到答案。