2. USB设备枚举过程:设备描述符、配置描述符、接口描述符、端点描述符

大家好,我是你们的老朋友。今天咱们聊聊USB枚举过程中最核心的四个描述符。说实话,我刚入行那会儿,觉得描述符就是一堆枯燥的字节。直到有一次调试一个U盘,死活枚举不过去,最后发现是配置描述符的长度字段写错了。从那以后,我对描述符的每一个字节都充满了敬畏。

枚举过程,说白了就是主机和设备互相认识的过程。设备插上去,主机问一句“你是谁?”,设备回答“我是谁、我能干什么”。这个问答靠的就是描述符。

核心要点:USB描述符是设备向主机报告自身能力的结构化数据。枚举失败,90%是描述符出了问题。

2.1 设备描述符:设备的身份证

设备描述符是主机拿到的第一份数据。它只有18个字节,但信息量巨大。我习惯把它比作身份证——上面写着设备类型、厂商ID、产品ID、版本号等基本信息。

偏移 字段 长度 说明
0 bLength 1 描述符长度,固定为18
1 bDescriptorType 1 描述符类型,设备描述符为0x01
2-3 bcdUSB 2 USB规范版本号,如0x0200表示USB 2.0
4 bDeviceClass 1 设备类代码,0xFF表示厂商自定义
5 bDeviceSubClass 1 子类代码
6 bDeviceProtocol 1 协议代码
7 bMaxPacketSize0 1 端点0的最大包大小
8-9 idVendor 2 厂商ID,由USB-IF分配
10-11 idProduct 2 产品ID,厂商自行定义
12-13 bcdDevice 2 设备版本号
14 iManufacturer 1 厂商字符串描述符索引
15 iProduct 1 产品字符串描述符索引
16 iSerialNumber 1 序列号字符串描述符索引
17 bNumConfigurations 1 支持的配置描述符数量

我的经验:bMaxPacketSize0这个字段特别容易踩坑。USB 2.0全速设备必须是8、16、32、64之一,高速设备固定为64。我曾经见过有人填了128,结果主机直接不认。

2.2 配置描述符:设备的说明书

拿到设备描述符后,主机接着要配置描述符。配置描述符告诉主机:这个设备需要多少电流、有多少个接口、支持什么属性。

配置描述符本身只有9个字节,但它后面跟着一堆接口描述符和端点描述符。所以它的总长度字段wTotalLength特别重要,告诉主机整个配置信息有多长。

// 配置描述符结构体(Linux内核中的定义)
struct usb_config_descriptor {
    __u8  bLength;          // 9
    __u8  bDescriptorType;  // 0x02
    __le16 wTotalLength;    // 整个配置的总长度
    __u8  bNumInterfaces;   // 接口数量
    __u8  bConfigurationValue; // 配置值,用于SetConfiguration
    __u8  iConfiguration;   // 配置字符串索引
    __u8  bmAttributes;     // 属性:自供电、远程唤醒等
    __u8  bMaxPower;        // 最大电流,单位2mA
} __attribute__ ((packed));

注意:bMaxPower字段的单位是2mA。比如填50,表示100mA。很多新手直接填电流值,结果设备只能拿到一半的电流。我调试过一个无线网卡,就是这个问题导致信号不稳定。

2.3 接口描述符:设备的功能模块

一个配置可以有多个接口。接口描述符描述了一个功能单元。比如一个USB摄像头,可能有视频接口和音频接口。

接口描述符也是9个字节。其中bInterfaceNumber是接口编号,bAlternateSetting用于备用设置——这个在视频设备里很常见,可以切换不同的分辨率。

字段 说明
bInterfaceNumber 接口编号,从0开始
bAlternateSetting 备用设置编号
bNumEndpoints 该接口使用的端点数量(不包括端点0)
bInterfaceClass 接口类代码
bInterfaceSubClass 接口子类代码
bInterfaceProtocol 接口协议代码
iInterface 接口字符串索引

嗯,这里要注意:bInterfaceClass、bInterfaceSubClass、bInterfaceProtocol这三个字段决定了主机加载哪个驱动。比如HID设备的类代码是0x03,大容量存储设备是0x08。如果填错了,主机可能加载错误的驱动。

2.4 端点描述符:数据的通道

端点描述符是描述符链的最后一环。它描述了数据通道的属性:传输方向、传输类型、最大包大小、轮询间隔。

每个端点描述符7个字节。bEndpointAddress的低4位是端点号,最高位表示方向(0=OUT,1=IN)。bmAttributes字段指定传输类型:0=控制传输,1=等时传输,2=批量传输,3=中断传输。

// 端点描述符结构体
struct usb_endpoint_descriptor {
    __u8  bLength;          // 7
    __u8  bDescriptorType;  // 0x05
    __u8  bEndpointAddress; // 端点地址和方向
    __u8  bmAttributes;     // 传输类型
    __le16 wMaxPacketSize;  // 最大包大小
    __u8  bInterval;        // 轮询间隔(毫秒或微帧)
} __attribute__ ((packed));

关键点:wMaxPacketSize字段在USB 2.0高速模式下有特殊编码。高2位表示每个微帧的额外传输次数。比如批量端点最大包大小是512字节,但高2位为0b10时,表示每个微帧可以传3个512字节的包。这个细节我当年在优化U盘读写速度时才发现。

2.5 描述符的层级关系

这四个描述符是树形结构。设备描述符在最顶层,下面挂配置描述符,配置描述符下面挂接口描述符,接口描述符下面挂端点描述符。主机枚举时,就是按照这个层级一层层往下读。

我画了一张图,帮你理解这个结构:

设备描述符 配置描述符 接口描述符 端点描述符 1 端点描述符 2 设备级 配置级 接口级 端点级

2.6 枚举流程实战

主机枚举一个设备,大致走这几步:

  1. 复位设备:主机检测到设备插入,发送复位信号
  2. 获取设备描述符:主机发请求,设备返回18字节的设备描述符
  3. 再次复位:根据设备描述符中的bMaxPacketSize0,主机确定端点0的最大包大小
  4. 设置地址:主机给设备分配一个唯一地址
  5. 获取配置描述符:主机先读9字节,得到wTotalLength,再读完整配置
  6. 解析描述符链:主机遍历配置描述符、接口描述符、端点描述符
  7. 选择配置:主机调用SetConfiguration,设备开始工作

避坑指南:我曾经在开发一个自定义HID设备时,配置描述符的wTotalLength算错了。主机读9字节后,再读完整配置时只读了部分数据,导致接口描述符和端点描述符没读到。设备能识别但无法使用。排查了两天才发现是长度字段少算了2个字节。

2.7 描述符的调试技巧

如果你在开发USB设备驱动,调试描述符是家常便饭。我推荐两个方法:

  • 使用USB分析仪:抓取主机和设备之间的通信,直接看原始数据。我最常用的是Beagle USB协议分析仪。
  • Linux下的lsusb命令:lsusb -v可以打印出设备的全部描述符信息。我经常用这个命令验证设备上报的数据是否正确。
# 查看设备描述符详细信息
lsusb -v -d 1234:5678

# 输出示例(部分)
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               2.00
  bDeviceClass            0 
  bDeviceSubClass         0 
  bDeviceProtocol         0 
  bMaxPacketSize0        64
  idVendor           0x1234
  idProduct          0x5678
  bcdDevice            1.00
  iManufacturer           1 
  iProduct                2 
  iSerialNumber           3 
  bNumConfigurations      1

嗯,说到调试,我再分享一个经验。有时候设备枚举失败,不一定是描述符数据错了,也可能是时序问题。比如设备上电后需要一段时间才能响应主机请求。我遇到过一款芯片,上电后要等200ms才能稳定工作。如果主机在100ms内就发请求,设备直接无响应。解决办法是在设备描述符中设置合适的bMaxPower,或者在上电后加一个延时。

好了,关于USB枚举的四个描述符,今天就聊到这里。记住一句话:描述符是设备和主机沟通的语言。语言不通,什么都干不了。下次遇到USB设备不工作,先拿lsusb看看描述符对不对,往往能省下不少时间。


专注资料整理