3. 设备描述符详解

说到枚举,大家第一个接触的就是设备描述符。嗯,它就像USB设备的身份证,主机一上来就先要这18个字节。我刚开始做USB项目时,总觉得这玩意儿简单,不就是填几个数嘛。结果有一次设备死活枚举不上,最后发现是bMaxPacketSize0填错了——你说冤不冤?

描述符是什么?

描述符是一段结构化的数据,用来告诉主机“我是谁、我支持什么、怎么跟我通信”。设备描述符是枚举流程的第一个描述符,所有设备都必须支持它。主机通过控制传输、默认端点0、请求GET_DESCRIPTOR(DEVICE)拿到这段数据。

注意:设备描述符必须在设备加电后随时可以返回,不能有任何延迟或条件判断。我曾经见过一个产品,在未初始化GPIO时返回了错误数据,结果Windows直接报“未知设备”。

设备描述符结构

设备描述符固定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个
小提示:上面这张表,大家可以打印出来贴在工位上。我当年就是靠它排查了不下30次枚举问题。

字段详解

我们挑几个容易出错的字段重点说说。

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,主机认为设备无效。
USB-IF认证要点:认证实验室会检查设备描述符字段是否与性能声明一致。比如你声称是高速设备,但bcdUSB写0x0110,直接Fail。另外,bMaxPacketSize0必须在设备加电初始化后稳定存在,不能动态改变。

代码示例

下面是一个标准的设备描述符结构体定义,我用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的包大小,然后继续获取配置描述符。

设备描述符在枚举流程中的位置 1. 主机复位USB设备 2. 主机请求设备描述符 3. 设备返回18字节描述符 4. 主机解析字段 5. 更新端点0包大小(bMaxPacketSize0) 6. 继续获取配置/接口/端点描述符 关键检查点 bLength=0x12 bMaxPacketSize0必须合法

上图展示了从主机复位到获取完整描述符的完整链条。一旦设备描述符出错,整个枚举流程就卡在第二步或第三步。我记得有一次帮客户调试,USB分析仪显示主机不断重试GET_DESCRIPTOR,最后发现是bMaxPacketSize0返回0x40(64)但设备实际上只支持全速,主机按高速去沟通,自然失败。

避坑指南:我曾经看到一个新手把bcdUSB写成0x200(不加BCD转换),结果主机认为是USB 0.0200。这类低级错误往往出现在复制粘贴代码时。建议使用宏定义,比如#define USB_BCD_VERSION 0x0200,一目了然。

好了,设备描述符的内容就讲到这里。字段不多,但每个字都关系到枚举成败。下次遇到枚举失败,先抓个USB分析仪看看设备描述符返回的是什么——八成能找到答案。