4. USB设备请求与标准命令:控制传输、标准设备请求
控制传输,说白了就是USB通信里的“控制通道”。所有设备枚举、配置、状态查询,都得走这条路。我刚开始接触USB时,总觉得控制传输不如批量传输“高级”,后来踩了坑才明白——没有控制传输,设备连门都进不去。
4.1 控制传输的结构
控制传输由两个或三个阶段组成:
- 建立阶段(Setup Stage):主机发送8字节的Setup包,告诉设备“我要干什么”。
- 数据阶段(Data Stage,可选):传输实际数据,方向由Setup包决定。
- 状态阶段(Status Stage):设备返回握手包,确认操作完成。
你想想看,这就像你去柜台办事:先递单子(Setup),然后交材料或拿结果(Data),最后柜台给你回执(Status)。
关键点:控制传输的端点号固定为0,所有USB设备都必须支持端点0。这是硬性规定,没得商量。
4.2 标准设备请求详解
USB规范定义了11个标准设备请求。枚举过程中,主机主要使用以下几个:
| 请求名称 | bmRequestType | bRequest | 用途 |
|---|---|---|---|
| Get_Descriptor | 0x80(设备→主机) | 0x06 | 获取设备、配置、字符串等描述符 |
| Set_Address | 0x00(主机→设备) | 0x05 | 分配设备地址 |
| Get_Configuration | 0x80 | 0x08 | 查询当前配置值 |
| Set_Configuration | 0x00 | 0x09 | 选择配置 |
| Get_Interface | 0x81 | 0x0A | 查询接口当前设置 |
| Set_Interface | 0x01 | 0x0B | 切换接口设置 |
| Clear_Feature | 0x02 | 0x01 | 清除端点特性(如STALL) |
4.3 Get_Descriptor:枚举的第一步
主机上电后,第一件事就是发Get_Descriptor请求,要设备描述符。设备描述符只有18字节,但包含了设备的所有基本信息。
// 设备描述符结构
struct usb_device_descriptor {
uint8_t bLength; // 描述符长度(18)
uint8_t bDescriptorType; // 描述符类型(0x01)
uint16_t bcdUSB; // USB版本号(BCD码)
uint8_t bDeviceClass; // 设备类
uint8_t bDeviceSubClass; // 设备子类
uint8_t bDeviceProtocol; // 设备协议
uint8_t bMaxPacketSize0; // 端点0最大包大小
uint16_t idVendor; // 厂商ID
uint16_t idProduct; // 产品ID
uint16_t bcdDevice; // 设备版本号
uint8_t iManufacturer; // 厂商字符串索引
uint8_t iProduct; // 产品字符串索引
uint8_t iSerialNumber; // 序列号字符串索引
uint8_t bNumConfigurations; // 配置描述符数量
} __attribute__((packed));
我的经验:bMaxPacketSize0这个字段特别容易出错。全速设备必须是8、16、32或64,高速设备固定为64。我曾经见过一个设备填了32,但实际只支持8字节包,结果主机读前8字节没问题,读后续数据时直接卡死。
4.4 Set_Address:给设备上户口
主机读完设备描述符后,会发Set_Address请求,给设备分配一个唯一地址。地址范围是1~127,0是默认地址。
这里有个容易踩的坑:设备必须在收到Set_Address后,完成状态阶段,才能切换到新地址。也就是说,状态阶段用的还是旧地址(0),状态阶段完成后才切到新地址。
我曾经在一个项目中,固件在收到Set_Address后立刻切换地址,结果状态阶段握手包主机收不到,枚举直接失败。排查了两天才发现是切换时机不对。
4.5 枚举流程中的请求序列
标准的枚举流程,主机发送的请求顺序大致如下:
- Get_Descriptor(设备描述符):只读前8字节,获取bMaxPacketSize0。
- Set_Address:分配地址。
- Get_Descriptor(设备描述符):用新地址再读一次完整描述符。
- Get_Descriptor(配置描述符):先读配置描述符头(9字节),获取总长度。
- Get_Descriptor(完整配置描述符):读全部配置描述符,包括接口、端点等。
- Set_Configuration:选择配置,设备进入配置状态。
为什么第一步只读8字节?因为主机还不知道端点0的最大包大小,读多了可能出问题。先读8字节拿到bMaxPacketSize0,后续通信就有底了。
4.6 其他常用标准请求
除了Get_Descriptor和Set_Address,还有几个请求在调试中经常遇到:
- Clear_Feature(ENDPOINT_HALT):清除端点的STALL状态。设备返回STALL后,主机用这个请求恢复通信。
- Get_Configuration:查询当前配置值。如果返回0,说明设备未配置。
- Set_Configuration:激活指定配置。配置值必须与配置描述符中的bConfigurationValue一致。
避坑指南:Set_Configuration请求中,如果配置值为0,设备进入地址状态(未配置)。有些设备在收到配置值为0时没有正确处理,导致后续无法重新配置。我建议在固件中专门处理这个边界情况。
4.7 控制传输的握手与错误处理
控制传输的状态阶段,设备必须返回握手包。握手包有三种:
| 握手包 | 含义 | 常见原因 |
|---|---|---|
| ACK | 成功 | 正常完成 |
| NAK | 忙,请重试 | 设备还没准备好数据 |
| STALL | 错误,不支持 | 请求无效或端点不支持 |
如果设备在数据阶段返回STALL,主机通常会重试几次,然后放弃枚举。我在调试时,经常用逻辑分析仪抓STALL包,一看就知道设备哪个请求没处理好。
4.8 控制传输的SVG流程图
下面这张图展示了控制传输的完整流程,以及枚举过程中标准请求的交互关系:
4.9 调试控制传输的实用技巧
枚举失败,十有八九是控制传输出了问题。我总结了几条调试经验:
- 用逻辑分析仪抓Setup包:看主机发了什么请求,设备回了什么数据。这是最直接的方法。
- 检查描述符长度:描述符的bLength字段必须与实际长度一致。多一个字节或少一个字节,主机都可能拒绝。
- 注意字节序:USB使用小端序,多字节字段(如idVendor)低字节在前。搞反了会导致厂商ID识别错误。
- 验证端点0的包大小:bMaxPacketSize0必须与硬件实际支持的一致。不一致会导致数据阶段丢包。
我的习惯:在固件中加一个调试串口,把收到的每个Setup包内容打印出来。这样不用接逻辑分析仪也能看到主机发了什么请求,排查问题快很多。
4.10 常见枚举失败场景
最后,我列几个实际项目中遇到的枚举失败场景,你可以对照排查:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 主机不认设备 | 设备描述符前8字节错误 | 检查bLength、bDescriptorType、bcdUSB、bMaxPacketSize0 |
| 枚举到一半卡死 | Set_Address切换时机不对 | 确认状态阶段完成后才切换地址 |
| 配置描述符读不全 | wTotalLength与实际长度不符 | 检查配置描述符总长度计算 |
| Set_Configuration失败 | 配置值与描述符不一致 | 检查bConfigurationValue是否匹配 |