4. USB 设备枚举过程:描述符解析

USB 设备枚举是主机(Host)识别并配置新接入 USB 设备的完整流程。在枚举过程中,主机通过控制传输(Control Transfer)逐级获取设备的描述符(Descriptor),从而了解设备的类型、能力、供电需求以及端点(Endpoint)配置。对于驱动开发者而言,理解枚举过程是正确解析设备拓扑、匹配驱动以及配置 URB(USB Request Block)回调的基础。

核心要点:枚举过程由主机侧 USB 核心(USB Core)自动完成,设备端只需被动响应标准请求。驱动开发者关注的重点在于:如何从获取到的描述符中提取接口(Interface)和端点(Endpoint)信息,以绑定正确的驱动并初始化 URB。

4.1 枚举流程概览

当 USB 设备插入端口后,主机控制器检测到电平变化,触发以下标准枚举步骤:

  1. 设备连接与复位:主机检测到设备插入,向端口发送复位信号(Reset),设备地址恢复为默认地址 0。
  2. 获取设备描述符(前 8 字节):主机通过默认地址 0 发送 GET_DESCRIPTOR 请求,获取设备描述符的前 8 字节,以确定端点 0 的最大包长度(bMaxPacketSize0)。
  3. 再次复位:主机根据获取到的最大包长度重新配置端口,并再次复位设备。
  4. 设置地址:主机发送 SET_ADDRESS 请求,为设备分配一个唯一的地址(1~127)。
  5. 获取完整设备描述符:主机使用新地址再次获取完整的设备描述符(18 字节)。
  6. 获取配置描述符:主机发送 GET_DESCRIPTOR 请求获取配置描述符(9 字节),随后获取完整的配置描述符集合(包括接口描述符和端点描述符)。
  7. 选择配置:主机发送 SET_CONFIGURATION 请求,激活设备的某个配置(通常为第一个配置)。
驱动开发者视角:步骤 6 是驱动开发的关键阶段。主机获取到的配置描述符集合中包含了接口描述符和端点描述符,这些信息直接决定了驱动如何绑定接口以及如何初始化 URB 的端点地址、传输类型和最大包长。
USB 设备枚举流程 USB 设备枚举流程 设备插入 / 复位 获取设备描述符(前8B) 再次复位 设置地址 (SET_ADDRESS) 获取完整设备描述符 获取配置描述符集合 选择配置 (SET_CONFIG) 枚举完成 / 驱动绑定 关键步骤 获取配置描述符集合时, 主机同时获取接口描述符 和端点描述符。

4.2 设备描述符(Device Descriptor)

设备描述符是 USB 设备的第一级描述符,描述了设备的整体信息。主机在枚举初期首先获取该描述符。其结构由 struct usb_device_descriptor 定义(include/uapi/linux/usb/ch9.h)。

字段 偏移(字节) 大小(字节) 说明
bLength 0 1 描述符长度(固定 18)
bDescriptorType 1 1 描述符类型(设备描述符为 0x01)
bcdUSB 2 2 USB 规范版本号(BCD 码,如 0x0200 表示 USB 2.0)
bDeviceClass 4 1 设备类代码(0x00 表示在接口描述符中定义)
bDeviceSubClass 5 1 设备子类代码
bDeviceProtocol 6 1 设备协议代码
bMaxPacketSize0 7 1 端点 0 的最大包长度(8/16/32/64)
idVendor 8 2 厂商 ID(VID)
idProduct 10 2 产品 ID(PID)
bcdDevice 12 2 设备版本号(BCD 码)
iManufacturer 14 1 厂商字符串描述符索引
iProduct 15 1 产品字符串描述符索引
iSerialNumber 16 1 序列号字符串描述符索引
bNumConfigurations 17 1 支持的配置描述符数量
驱动匹配关键:驱动通常通过 idVendor 和 idProduct 来匹配设备。在 Linux 内核中,USB 驱动使用 usb_device_id 结构体声明支持的 VID/PID 组合。

4.3 配置描述符(Configuration Descriptor)

配置描述符描述设备的供电模式、接口数量以及整体功耗。一个设备可以有多个配置(Configuration),但同一时刻只能激活一个。配置描述符的结构由 struct usb_config_descriptor 定义。

字段 偏移(字节) 大小(字节) 说明
bLength 0 1 描述符长度(固定 9)
bDescriptorType 1 1 描述符类型(配置描述符为 0x02)
wTotalLength 2 2 配置描述符集合的总长度(包括接口和端点描述符)
bNumInterfaces 4 1 该配置下包含的接口数量
bConfigurationValue 5 1 用于 SET_CONFIGURATION 请求的配置值
iConfiguration 6 1 配置字符串描述符索引
bmAttributes 7 1 配置特性(自供电/远程唤醒等)
bMaxPower 8 1 最大电流(单位 2mA,如 50 表示 100mA)
注意:wTotalLength 字段非常重要。主机在获取配置描述符时,先读取前 9 字节得到 wTotalLength,然后根据该长度读取完整的配置描述符集合。驱动开发者在解析配置时,必须遍历该集合以提取所有接口和端点描述符。

4.4 接口描述符(Interface Descriptor)

接口描述符描述一个功能接口(Interface),一个配置可以包含多个接口。接口是驱动绑定的基本单位——一个 USB 驱动通常绑定到一个接口上。接口描述符由 struct usb_interface_descriptor 定义。

字段 偏移(字节) 大小(字节) 说明
bLength 0 1 描述符长度(固定 9)
bDescriptorType 1 1 描述符类型(接口描述符为 0x04)
bInterfaceNumber 2 1 接口编号(从 0 开始)
bAlternateSetting 3 1 备用设置编号(用于切换接口配置)
bNumEndpoints 4 1 该接口使用的端点数量(不包括端点 0)
bInterfaceClass 5 1 接口类代码
bInterfaceSubClass 6 1 接口子类代码
bInterfaceProtocol 7 1 接口协议代码
iInterface 8 1 接口字符串描述符索引
驱动绑定依据:Linux USB 核心在枚举时,会遍历配置描述符集合中的每个接口描述符,并根据 bInterfaceClass、bInterfaceSubClass、bInterfaceProtocol 以及 VID/PID 来匹配已注册的 usb_driver。匹配成功后,驱动核心调用驱动的 probe() 函数,并将接口描述符信息传递给驱动。

4.5 端点描述符(Endpoint Descriptor)

端点描述符是驱动开发者最关注的描述符,它定义了除默认端点(端点 0)之外的所有端点的属性。对于中断端点(Interrupt Endpoint)的 URB 回调配置,端点描述符中的 bInterval 字段直接决定了 URB 的轮询间隔。端点描述符由 struct usb_endpoint_descriptor 定义。

字段 偏移(字节) 大小(字节) 说明
bLength 0 1 描述符长度(固定 7)
bDescriptorType 1 1 描述符类型(端点描述符为 0x05)
bEndpointAddress 2 1 端点地址(位 3-0:端点号,位 7:方向 0=OUT,1=IN)
bmAttributes 3 1 端点属性(位 1-0:传输类型 0=控制,1=等时,2=批量,3=中断)
wMaxPacketSize 4 2 端点支持的最大包长度(字节)
bInterval 6 1 轮询间隔(单位:帧/微帧,中断端点必填)
中断端点关键字段:
  • bEndpointAddress:驱动通过该字段确定 URB 的目标端点号和传输方向。例如 0x81 表示端点 1 IN。
  • bmAttributes:位 1-0 必须为 0x03(中断传输)。
  • wMaxPacketSize:决定 URB 缓冲区大小。对于高速中断端点,该字段还包含每个微帧的额外事务数。
  • bInterval:对于全速设备,单位为帧(1ms);对于高速设备,单位为微帧(125μs)。该值直接用于设置 URB 的 interval 字段。
USB 描述符层级结构 USB 描述符层级结构 设备描述符 配置描述符 接口描述符 端点描述符 1 端点描述符 2 端点描述符 N 描述符层级关系 • 一个设备有且仅有一个设备描述符。 • 一个设备描述符下可以包含多个配置描述符(由 bNumConfigurations 指定)。 • 一个配置描述符下可以包含多个接口描述符(由 bNumInterfaces 指定)。 • 一个接口描述符下可以包含多个端点描述符(由 bNumEndpoints 指定,不包括端点 0)。

4.6 内核中描述符的解析与使用

在 Linux 内核中,USB 核心在枚举完成后,会将解析好的描述符信息存储在 struct usb_device 和 struct usb_interface 结构体中。驱动开发者可以通过以下方式访问这些信息:

// 在 probe() 函数中获取接口和端点信息
static int my_driver_probe(struct usb_interface *intf,
                           const struct usb_device_id *id)
{
    struct usb_device *udev = interface_to_usbdev(intf);
    struct usb_host_interface *altsetting = intf->cur_altsetting;
    struct usb_endpoint_descriptor *ep_desc;
    int i;

    // 打印设备描述符信息
    dev_info(&intf->dev, "VID: 0x%04x, PID: 0x%04x\n",
             udev->descriptor.idVendor,
             udev->descriptor.idProduct);

    // 遍历当前备用设置中的所有端点
    for (i = 0; i < altsetting->desc.bNumEndpoints; i++) {
        ep_desc = &altsetting->endpoint[i].desc;

        // 检查是否为中断端点
        if (usb_endpoint_is_int_in(ep_desc)) {
            dev_info(&intf->dev,
                     "发现中断 IN 端点: 地址=0x%02x, 最大包长=%d, 间隔=%d\n",
                     ep_desc->bEndpointAddress,
                     le16_to_cpu(ep_desc->wMaxPacketSize),
                     ep_desc->bInterval);

            // 保存端点信息供后续 URB 初始化使用
            // ...
        }
    }

    return 0;
}
关键 API 说明:
  • usb_endpoint_is_int_in(epd):检查端点描述符是否为中断 IN 端点。
  • usb_endpoint_is_int_out(epd):检查端点描述符是否为中断 OUT 端点。
  • usb_endpoint_num(epd):从 bEndpointAddress 中提取端点号。
  • usb_endpoint_dir_in(epd):检查端点方向是否为 IN。
  • usb_endpoint_maxp(epd):获取 wMaxPacketSize 的值。
  • usb_endpoint_interval(epd):获取 bInterval 的值(已转换为帧/微帧单位)。

4.7 枚举过程中的常见问题与调试

问题现象 可能原因 调试方法
设备无法被识别 设备描述符解析失败,端点 0 最大包长不匹配 使用 lsusb -v 查看设备描述符,检查 bMaxPacketSize0
驱动 probe 不被调用 VID/PID 或接口类/子类/协议不匹配 检查 usb_device_id 表,使用 modprobe usbmon 抓取 URB 日志
中断 URB 提交失败 端点地址错误或端点类型不匹配 确认 bEndpointAddress 和 bmAttributes 字段
中断传输间隔异常 bInterval 值未正确转换 使用 usb_endpoint_interval() 获取正确间隔值
调试工具推荐:
  • lsusb -v:以详细模式列出所有 USB 设备的描述符信息。
  • usbmon:内核 USB 监控工具,可以捕获所有 USB 传输的 URB 数据。
  • /sys/kernel/debug/usb/devices:提供内核中 USB 设备树的详细描述符信息。

4.8 本章小结