2. USB协议基础回顾:设备枚举过程的概念与GetDescriptor请求

好了,正式开始前,咱们先聊聊USB设备枚举这件事。

枚举,说白了就是USB主机和设备之间的一套“握手”流程。主机想知道你是什么设备,你得把你的“身份证”——描述符——一张张递过去。这个过程完全由主机主导,设备只能乖乖回答。

让主机能识别并配置好一个设备,这个过程就叫枚举。

刚开始接触USB开发时,我常把枚举和描述符搞混。枚举是主机发起的动态过程。描述符呢?是设备静态提供了它。这两者密不可分。

核心概念: 枚举是主机通过一系列标准请求(尤其是GetDescriptor),从设备那里获取信息,从而加载对应驱动、配置设备的过程。没有枚举,你的设备就是个“哑巴”。

2.1 枚举的八个关键步骤

一个完整的枚举过程,大致包括下面几步。我把它整理成一个表格,方便你对照理解:

步骤 动作 数据/请求 说明
1 设备连接 检测到D+/D-电平变化 主机Hub检测到设备插入
2 复位设备 复位信号 (SE0) 设备复位,默认地址为0
3 获取设备描述符(前8字节) GetDescriptor(Device, 0, 8) 主机先拿个“预览版”,确定端点0最大包长
4 再次复位 复位信号 (SE0) 第二次复位,设定新地址前的准备
5 设置地址 SetAddress(new_addr) 分配唯一地址,设备从地址0切换到新地址
6 获取完整设备描述符 GetDescriptor(Device, 0, 18) 获取全部18字节
7 获取配置描述符 GetDescriptor(Configuration, 0, 9) 先拿配置描述符头部,计算总长度
8 获取完整配置描述符集 GetDescriptor(Configuration, 0, total_len) 包括配置、接口、端点等所有描述符

嗯,这里要注意:步骤3只请求前8个字节,就是为了读到bMaxPacketSize0字段。我遇到过不少新手,在这里直接请求完整18字节,结果设备还是0地址、还不确定包长,数据量一大就出错了。

2.2 GetDescriptor请求的底层结构

所有的GetDescriptor请求,本质上都是一个8字节的Setup包。它的结构非常固定:

// 标准设备请求格式
// bmRequestType | bRequest | wValue | wIndex | wLength
// 1 byte        | 1 byte   | 2 bytes| 2 bytes| 2 bytes

// 获取设备描述符的Setup包示例
// bmRequestType: 0x80  (Device方向, 标准请求)
// bRequest:      0x06  (GET_DESCRIPTOR)
// wValue:        0x0100 (高位=描述符类型1=Device, 低位=索引0)
// wIndex:        0x0000 (Language ID, 设备描述符忽略)
// wLength:       0x0012 (请求18字节)
uint8_t setup_packet[] = {
    0x80, 0x06,
    0x00, 0x01, // 注意:wValue是小端格式,实际值为0x0100
    0x00, 0x00,
    0x12, 0x00
};

经验之谈: 我习惯在调试时,先用USB分析仪抓一份枚举过程的Setup包。对着协议规范一字节一字节地对照分析,看多了,自然就摸到门道了。

2.3 描述符类型与请求中的wValue

wValue的高字节(wValueH)指定了你要请求的描述符类型。标准类型定义如下:

类型值 (wValueH) 描述符类型 说明
0x01 DEVICE (设备) 每个USB设备必备
0x02 CONFIGURATION (配置) 每个配置对应一个
0x03 STRING (字符串) 可选,提供厂商/产品等文本
0x04 INTERFACE (接口) 描述设备的功能
0x05 ENDPOINT (端点) 每个非零端点对应一个
0x06 DEVICE_QUALIFIER USB 2.0高速设备特有
0x07 OTHER_SPEED_CONFIGURATION 另一种速度下的配置

看到这里,你可能会问:是不是每次都请求一种描述符?我告诉你,顺序很重要。设备描述符永远是第1步。没有它,主机连端点0的最大包长都不知道。

避坑指南: 我曾经调试过一个复合设备,固件里把字符串描述符的索引号写错了。结果主机请求字符串时,设备回了一串乱码,导致驱动加载失败。所以描述符的索引号务必和实际固件保持一一对应。

2.4 枚举过程的逻辑主线

为了让你更直观地理解整个枚举的脉络,我画了张流程图。看完这张图,你就能把刚才那8个步骤串起来了。

USB设备枚举过程及GetDescriptor请求流程 设备连接 总线复位 (SE0) GetDescriptor(Device, 0, 8) SetAddress(新地址) GetDescriptor(Device, 0, 18) GetDescriptor(Config, 0, total_len) 枚举完成 关键点 获取描述符请求(红色/紫色/橙色) 是枚举的核心数据交换

看完这张图,你会发现整个枚举过程,其实就是主机不断地发出各种GetDescriptor和SetAddress请求。设备需要做的,就是在指定端点(默认是端点0)上,准确无误地把固件里预先存储的数据返回给主机。

一点小建议:开发时,直接用逻辑分析仪抓取上行数据,把每个Setup包和返回的数据都打印出来对照分析。这就是我常说的“让它先跑起来,再看对不对”。