4. USB枚举过程详解:复位、地址分配、读取描述符、配置设备

USB枚举是主机(Host)发现并初始化一个新接入设备的过程。对于USB Gadget而言,设备端(Device)必须严格按照USB 2.0规范规定的状态机响应主机的枚举请求。本章将深入剖析枚举的四个核心阶段:总线复位、地址分配、描述符读取和设备配置,并结合Linux Gadget驱动源码分析其实现机制。

核心要点:枚举过程由主机主导,设备端被动响应。任何阶段超时或响应错误都会导致枚举失败,主机将断开该设备。

4.1 枚举状态机总览

USB 2.0规范定义了设备枚举的六个主要状态:上电(Powered)、默认(Default)、地址(Address)、配置(Configured)、挂起(Suspended)。其中,复位和地址分配发生在Default与Address状态之间。下图展示了枚举的完整流程:

USB设备枚举状态机与流程 设备接入 总线复位 默认状态 分配地址 地址状态 读取描述符 配置设备 配置状态 正常传输 说明: 主机通过控制传输(端点0)完成所有枚举步骤。设备端必须正确解析Setup包并返回符合规范的数据。 每个步骤都有超时限制(通常为5秒),超时后主机将复位或断开设备。

4.2 总线复位(Bus Reset)

当主机检测到设备插入或用户请求复位时,主机向总线发送复位信号(SE0状态持续至少10ms)。设备端检测到复位后,必须执行以下操作:

  • 恢复默认地址:将设备地址重置为0(默认地址)。
  • 复位端点状态:所有端点(除端点0外)进入未配置状态。
  • 清空缓冲区:丢弃所有未处理的数据。
  • 进入默认状态:准备响应端点0上的控制传输。
Gadget驱动提示:在Linux Gadget框架中,复位事件通过UDC(USB Device Controller)驱动回调函数通知上层。驱动应在udc_start()中注册复位处理函数,通常调用usb_gadget_udc_reset()来重置Gadget状态。

复位完成后,主机以12Mbps(全速)或480Mbps(高速)的速率与设备通信,并开始发送SOF(Start of Frame)包。

4.3 地址分配(Address Assignment)

主机通过控制传输的SET_ADDRESS请求为设备分配唯一地址。该过程分为两步:

  1. 主机发送SET_ADDRESS请求:使用默认地址0,请求包中包含新地址(1~127)。
  2. 设备确认并切换地址:设备在状态阶段完成后立即切换到新地址,后续所有通信使用新地址。
字段 值 说明
bmRequestType 0x00 主机到设备,标准请求,接收方为设备
bRequest 0x05 SET_ADDRESS
wValue 新地址 (1~127) 主机分配的唯一地址
wIndex 0x0000 未使用
wLength 0x0000 无数据阶段
关键约束:设备必须在收到SET_ADDRESS请求后的状态阶段完成后,才能将内部地址寄存器更新为新地址。如果在状态阶段之前切换地址,将导致主机无法确认状态完成,枚举失败。

在Linux Gadget驱动中,地址分配由UDC硬件自动完成或由驱动软件处理。典型的处理流程如下:

/* Gadget驱动中SET_ADDRESS处理示例 */
static int composite_setup(struct usb_gadget *gadget,
                           const struct usb_ctrlrequest *ctrl)
{
    int ret = -EOPNOTSUPP;

    if ((ctrl->bRequestType & USB_TYPE_MASK) == USB_TYPE_STANDARD) {
        switch (ctrl->bRequest) {
        case USB_REQ_SET_ADDRESS:
            /* 保存新地址,在状态阶段后生效 */
            gadget->dev_addr = ctrl->wValue;
            ret = 0;
            break;
        /* 其他请求处理... */
        }
    }
    return ret;
}

4.4 读取描述符(Descriptor Retrieval)

地址分配完成后,主机开始读取设备的描述符以了解设备的能力和配置。主机通常按以下顺序读取:

  1. 设备描述符(Device Descriptor):前18字节,包含USB版本、设备类、最大包大小等信息。
  2. 配置描述符(Configuration Descriptor):包含配置信息、接口数量、供电方式等。
  3. 接口描述符(Interface Descriptor):描述每个接口的功能和端点数量。
  4. 端点描述符(Endpoint Descriptor):描述每个端点的地址、方向、传输类型和最大包大小。
  5. 字符串描述符(String Descriptor):可选的制造商、产品、序列号信息。

下图展示了描述符的层次结构:

USB描述符层次结构 设备描述符 配置描述符 接口描述符 0 接口描述符 1 端点描述符 IN 端点描述符 OUT 端点描述符 IN 端点描述符 OUT 说明: 主机通过GET_DESCRIPTOR请求读取每个描述符。设备必须按规范返回完整且正确的描述符数据。 配置描述符的总长度决定了主机需要读取的数据量,通常包含所有接口和端点描述符。

主机通过GET_DESCRIPTOR请求读取描述符。设备描述符的前8字节包含总长度,主机据此决定后续读取量。关键字段解析:

描述符类型 bDescriptorType值 最小长度 关键字段
设备描述符 0x01 18字节 bcdUSB, idVendor, idProduct, bMaxPacketSize0
配置描述符 0x02 9字节 wTotalLength, bNumInterfaces, bConfigurationValue
接口描述符 0x04 9字节 bInterfaceNumber, bAlternateSetting, bNumEndpoints
端点描述符 0x05 7字节 bEndpointAddress, bmAttributes, wMaxPacketSize
字符串描述符 0x03 可变 UNICODE字符串
Gadget驱动实现:在Linux中,描述符通过usb_gadget_driver结构体中的bind()回调注册。驱动使用usb_assign_descriptors()函数将描述符链表绑定到Gadget实例。主机请求时,UDC驱动自动从链表中查找并返回对应描述符。

4.5 配置设备(Configuration)

读取完所有描述符后,主机发送SET_CONFIGURATION请求来激活设备的某个配置。设备收到后必须:

  • 验证配置值:检查wValue是否与某个配置描述符的bConfigurationValue匹配。
  • 初始化端点:根据配置描述符中的接口和端点信息,初始化所有非默认端点。
  • 分配资源:为每个端点分配FIFO缓冲区、DMA通道等硬件资源。
  • 进入配置状态:设备现在可以开始正常的数据传输。

SET_CONFIGURATION请求格式:

字段 值 说明
bmRequestType 0x00 主机到设备,标准请求,接收方为设备
bRequest 0x09 SET_CONFIGURATION
wValue 配置值 (1~255) 要激活的配置编号,0表示取消配置
wIndex 0x0000 未使用
wLength 0x0000 无数据阶段
多配置支持:一个设备可以定义多个配置描述符(例如:高功耗模式和低功耗模式)。主机根据供电能力选择合适的配置。Gadget驱动通过usb_gadget_config_number()管理多配置。

配置完成后,主机可以发送SET_INTERFACE请求选择备用接口设置(Alternate Setting),用于切换接口的端点配置。例如,音频设备可以通过备用设置切换采样率。

4.6 枚举失败常见原因与调试

嵌入式开发中枚举失败是最常见的问题。下表总结了典型失败原因及排查方法:

失败现象 可能原因 排查方法
主机无响应 硬件连接错误、D+上拉电阻缺失 用示波器检查D+/D-信号,确认上拉电阻(1.5kΩ)连接正确
复位后无通信 端点0最大包大小不匹配 检查设备描述符中bMaxPacketSize0是否与硬件能力一致
地址分配失败 状态阶段处理错误 确认设备在状态阶段完成后才切换地址,使用USB分析仪抓包
描述符读取超时 描述符数据错误或长度不匹配 使用lsusb -v查看主机侧接收到的描述符,对比预期值
配置失败 端点资源不足或描述符冲突 检查端点地址是否重复,确认FIFO大小是否满足wMaxPacketSize
调试利器:在Linux主机上使用usbmon或wireshark抓取USB总线数据,可以完整看到枚举过程中的每个Setup包、数据阶段和状态阶段。对于Gadget端,开启内核的CONFIG_USB_GADGET_DEBUG和CONFIG_USB_GADGET_VERBOSE可以打印详细的枚举日志。

4.7 小结