USB 设备枚举:从插上到工作的完整过程

枚举这事儿,说白了就是USB设备插上后,主机跟设备之间的“互相认识”过程。我刚开始做USB验证那会儿,觉得枚举就是个简单的握手流程,后来被坑了几次才明白——这里面的门道多着呢。每次枚举出问题,设备就像个哑巴一样不工作,连个错误码都不给你。嗯,咱们今天就来把这个过程彻底拆开来看。

一、枚举的整体骨架

USB设备枚举,官方定义是一系列标准请求和响应的序列。主机通过这个过程,把设备从“未知”变成“已知”,分配地址,加载驱动,最终让设备进入工作状态。

整个流程大概分这么几步:

  1. 端口复位——主机让设备“归零”
  2. 速度识别——判断设备是HS、FS还是LS
  3. 设备地址分配——给设备发身份证(Set Address)
  4. 描述符请求——问设备“你是谁?你能干嘛?”
  5. 配置选择——告诉设备“你就按这个方式工作”

核心要点:枚举不是一次性的对话,而是主机和设备之间多次“一问一答”的交互。每一步失败,设备都无法正常工作。

下面这张图,我把整个流程画出来了。你看一眼,心里就有个谱了。

USB 设备枚举过程状态机 端口检测 复位 + 速度识别 Set Address 描述符请求 配置选择 配置成功 检测设备插入,提供VBUS 检测D+/D-电平,识别速度 分配唯一设备地址 获取设备、配置、接口描述符 从配置描述符中选择一个 设备进入工作状态

二、第一步:端口复位与速度识别

复位到底干了什么?

主机检测到设备插入后,会立即对端口做复位。复位信号就是D+和D-都被拉低至少10ms。所有设备必须响应这个复位,不管你是啥速度等级。

我个人习惯把复位看成“让设备忘记之前所有对话”的操作。复位后,设备回到默认地址0,准备接收主机的下一个指令。

速度识别,这里有意思。

主机怎么知道设备是HS、FS还是LS?其实靠的是D+和D-上的上拉电阻位置。

  • 全速FS(12Mbps): D+线上有1.5kΩ上拉到3.3V
  • 低速LS(1.5Mbps): D-线上有1.5kΩ上拉到3.3V
  • 高速HS(480Mbps): 先以FS身份上拉D+,复位后通过Chirp J/K握手切换到HS

实战忠告: 做一致性测试时,我最喜欢抓复位后的信号线波形。速度识别失败是枚举失败最常见的原因。我曾经遇到一个项目,设备上电后D+的上拉电压不够,主机总是把FS设备认成LS,折腾了两天才发现是电源纹波太大。

三、Set Address:给设备发身份证

复位后,设备地址是0。但主机不能一直用地址0跟所有设备通信吧?所以枚举的第一个真正指令就是Set Address。

这个过程是这样的:

  1. 主机发一个Setup事务,请求Set Address,目标地址由主机决定(比如地址7)
  2. 设备接收到请求后,在Status阶段返回成功
  3. 设备从下一个事务开始,切换到新地址

注意: Set Address请求是USB协议中少数“延迟生效”的请求。设备不是在收到请求后就立即切换地址,而是必须在Status阶段完成后才切换。我曾经遇到过测试设备提前发送了下一笔事务,结果数据飞到地址0去了,设备根本不理你。

你可能会问,为什么要先分配地址再拿完整描述符?我记得有个协议规范的解释:主机需要先给设备一个唯一的ID,然后再跟设备深入交流,否则两个设备同时用地址0应答,那就乱套了。

四、描述符请求:挖出设备的所有秘密

地址分配完了,主机就开始“盘问”设备了。一个标准的USB设备,必须向主机提供至少以下几种描述符:

描述符类型 描述符长度 关键字段
设备描述符 (Device) 18字节 bcdUSB, idVendor, idProduct
配置描述符 (Configuration) 9字节(+后续) bNumInterfaces, bMaxPower
接口描述符 (Interface) 9字节 bInterfaceClass, bInterfaceProtocol
端点描述符 (Endpoint) 7字节 bEndpointAddress, wMaxPacketSize
字符串描述符 (String) 可变 厂商/产品/序列号文字

说说常见的坑:

描述符的长度不能填错。比如设备描述符必须是18字节,你少写一个字节,主机就会认为设备不听话,直接fail。我遇到过一位同事,他把wMaxPacketSize字段在描述符里写成了512,可设备实际上不支持那么大的包,结果每次枚举到配置阶段就卡住。嗯,这就是典型的描述符与硬件能力不匹配。

// USB 2.0 规范中的设备描述符结构
// 注意每个字段的偏移和长度
typedef struct {
    uint8_t  bLength;         // 固定 18
    uint8_t  bDescriptorType; // 固定 0x01
    uint16_t bcdUSB;          // USB规范版本,如 0x0200
    uint8_t  bDeviceClass;
    uint8_t  bDeviceSubClass;
    uint8_t  bDeviceProtocol;
    uint8_t  bMaxPacketSize0; // 端0最大包长
    uint16_t idVendor;
    uint16_t idProduct;
    uint16_t bcdDevice;
    uint8_t  iManufacturer;
    uint8_t  iProduct;
    uint8_t  iSerialNumber;
    uint8_t  bNumConfigurations;
} __attribute__((packed)) usb_device_descriptor_t;

关键点: 描述符请求的顺序是固定的。主机先要设备描述符的前8字节(看端点0最大包长),然后根据这个包长重新发一次完整请求。这叫“先试探,再正事”。

五、配置选择:告诉设备怎么干活

描述符拿全了,主机就知道设备有哪些能力了。接下来就是选一个合适的配置,告诉设备:“你就按这个方式干活。”

配置选择是通过Set Configuration请求完成的。这个请求很简单:主机把选中的配置号(bConfigurationValue)发给设备,设备必须立刻切换到该配置下的工作模式。

说白了,设备可以有多个配置,但同一时间只能用一个。比如一个摄像头,可以配置成“视频+音频”或“纯视频”,由主机根据用户需求选。

我个人习惯在验证时,检查设备在Set Configuration之后的端点使能状态。有些设备在配置选择前,非0端点是不启用的。如果设备在未配置时就发送数据,那肯定是违反规范的。

配置选择后的验证点:

  • 设备的所有端点是否已经使能,并准备好接收主机的事务
  • 设备的bMaxPower是否与配置描述符一致(通常单位是2mA)
  • 如果配置了多个接口,各接口的备用设置是否正确

六、枚举阶段的避坑清单

最后,我把自己这些年踩过的坑总结成清单,你们直接拿去用:

  1. 复位时间不足: 主机必须保证复位持续时间至少10ms。我早期用某个FPGA做的USB主机,复位只拉了8ms,结果高速设备根本进不了HS模式。
  2. Set Address之后不要立即用新地址: 设备需要50us的“地址切换稳定期”,你紧接着就发新地址的事务,大概率等来一个超时。
  3. 描述符长度校验: 设备的描述符长度字段(bLength)必须与实际返回长度一致,多一个或少一个字节都会导致枚举失败。
  4. 最大数据包大小必须真实: bMaxPacketSize0是多少,端点0就真的只能收发这么多数据。我曾经见过一个设备声称支持64字节包,但硬件FIFO只有32字节,结果一传大包就丢。
  5. 配置选择后不要忘记挂起恢复: 配置选择不是终点,设备可能还需要进入挂起恢复状态才能开始实际数据传输。

终极警告: 如果你在做USB-IF认证测试,枚举阶段的每个时序都得用示波器量一遍。主机发出的每个信号、设备的每个响应,都在测试脚本的放大镜下。我在一次认证测试中,就因为Set Address的Status阶段返回了错误的PID,直接被判定为FAIL,那叫一个冤啊。