4. USB设备请求:标准设备请求与厂商自定义请求的实现
设备请求,说白了就是主机跟设备之间的“对话协议”。主机想知道你是谁、你能干什么,或者想让你切换工作模式,都得通过设备请求来沟通。这一章咱们就深入看看这些请求到底怎么发、怎么收、怎么在代码里落地。
4.1 标准设备请求的结构
每个USB设备请求都是一个14字节的数据包,通过默认控制端点(端点0)传输。结构非常固定:
| 偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 0 | bmRequestType | 1字节 | 方向、类型、接收方 |
| 1 | bRequest | 1字节 | 请求码 |
| 2-3 | wValue | 2字节 | 参数,依请求而定 |
| 4-5 | wIndex | 2字节 | 索引,通常指向接口或端点 |
| 6-7 | wLength | 2字节 | 数据阶段长度 |
这里有个关键点——bmRequestType。它决定了请求的方向和接收方。我刚开始做USB驱动时,就因为这个字段搞反了方向,设备死活不响应。后来才彻底搞明白:
- D7位:方向。0=主机到设备,1=设备到主机
- D6-D5位:类型。0=标准,1=类,2=厂商
- D4-D0位:接收方。0=设备,1=接口,2=端点,3=其他
经验之谈:解析bmRequestType时,我习惯用位掩码来提取。比如判断是否是厂商请求:(bmRequestType & 0x60) == 0x40。这样代码可读性高,也不容易出错。
4.2 标准设备请求详解
USB规范定义了11个标准请求,但实际开发中常用的就几个。咱们挑最重要的说。
4.2.1 GetDescriptor(获取描述符)
这是主机枚举设备时最频繁使用的请求。主机通过它获取设备的各种描述符——设备描述符、配置描述符、字符串描述符等等。
请求格式:
- bmRequestType:0x80(设备到主机,标准,设备)
- bRequest:0x06
- wValue高字节:描述符类型(1=设备,2=配置,3=字符串...)
- wValue低字节:描述符索引(字符串描述符用)
- wIndex:语言ID(字符串描述符用)
- wLength:期望返回的字节数
我在项目中遇到过一个问题:主机请求设备描述符时,wLength通常设为18(设备描述符固定长度),但有些主机第一次只请求前8个字节。如果你的设备驱动不支持这种“部分读取”,枚举就会失败。嗯,这里要注意——必须支持wLength小于实际描述符长度的场景,只返回请求的字节数即可。
4.2.2 SetConfiguration(设置配置)
枚举完成后,主机通过这个请求告诉设备使用哪个配置。设备收到后,需要根据配置值初始化相应的端点、分配缓冲区、启动硬件。
请求格式:
- bmRequestType:0x00(主机到设备,标准,设备)
- bRequest:0x09
- wValue低字节:配置值(对应配置描述符中的bConfigurationValue)
避坑指南:我曾经在一个项目中,配置值从1开始编号,但设备默认配置是0(未配置状态)。结果主机发送SetConfiguration(1)时,设备直接返回STALL。查了半天才发现是配置描述符里的bConfigurationValue写成了0。记住:配置值必须从1开始,0表示未配置状态。
4.2.3 其他常用标准请求
| 请求 | bRequest | 用途 |
|---|---|---|
| GetConfiguration | 0x08 | 获取当前配置值 |
| SetInterface | 0x0B | 切换备用设置 |
| GetInterface | 0x0A | 获取当前备用设置 |
| ClearFeature | 0x01 | 清除端点暂停等特性 |
| SetAddress | 0x05 | 分配设备地址 |
4.3 厂商自定义请求的实现
标准请求只能满足通用需求。实际产品中,厂商自定义请求才是真正体现设备特色的地方。比如固件升级、读取传感器数据、配置设备参数——这些都得靠厂商请求。
4.3.1 请求定义
厂商请求的bmRequestType中,类型位设为0x40(厂商)。方向、接收方可以根据需要自由组合。我一般这样定义:
// 厂商请求码定义
#define VENDOR_REQ_READ_VERSION 0xA0
#define VENDOR_REQ_UPDATE_FIRMWARE 0xA1
#define VENDOR_REQ_SET_PARAM 0xA2
#define VENDOR_REQ_GET_STATUS 0xA3
// 请求结构体
typedef struct {
uint8_t bmRequestType;
uint8_t bRequest;
uint16_t wValue;
uint16_t wIndex;
uint16_t wLength;
uint8_t *pData;
} vendor_request_t;
4.3.2 处理流程
设备端收到请求后,需要解析bmRequestType,判断是标准请求还是厂商请求。标准请求走通用处理,厂商请求走自定义逻辑。
我习惯在USB中断服务程序里做初步分发:
void USB_SetupPacketHandler(uint8_t *pSetup) {
setup_packet_t *pReq = (setup_packet_t *)pSetup;
// 判断请求类型
if ((pReq->bmRequestType & 0x60) == 0x00) {
// 标准请求
HandleStandardRequest(pReq);
} else if ((pReq->bmRequestType & 0x60) == 0x40) {
// 厂商请求
HandleVendorRequest(pReq);
} else {
// 类请求或其他
HandleClassRequest(pReq);
}
}
个人习惯:厂商请求的处理函数里,我会用一个switch-case结构,每个case对应一个请求码。这样代码清晰,也方便后续扩展。如果请求码超过10个,我会改用函数指针表,性能更好。
4.3.3 一个完整的厂商请求示例
假设我们要实现一个“读取固件版本”的厂商请求:
void HandleVendorRequest(setup_packet_t *pReq) {
uint8_t *pBuffer;
switch (pReq->bRequest) {
case VENDOR_REQ_READ_VERSION:
// 主机到设备?还是设备到主机?
if ((pReq->bmRequestType & 0x80) == 0x80) {
// 设备到主机:返回版本信息
pBuffer = GetFirmwareVersion();
USB_EP0_SendData(pBuffer, pReq->wLength);
} else {
// 主机到设备:非法方向,返回STALL
USB_EP0_SetStall();
}
break;
case VENDOR_REQ_SET_PARAM:
// 主机到设备:接收参数
if ((pReq->bmRequestType & 0x80) == 0x00) {
// 准备接收数据
USB_EP0_ReceiveData(g_ParamBuffer, pReq->wLength);
} else {
USB_EP0_SetStall();
}
break;
default:
// 不支持的请求,返回STALL
USB_EP0_SetStall();
break;
}
}
4.4 请求处理的核心逻辑
说了这么多,咱们用一张图把整个请求处理流程串起来:
4.5 实现中的关键细节
写设备请求处理代码时,有几个坑我踩过,分享给大家:
- wLength的处理:主机请求的数据长度可能小于实际数据。比如GetDescriptor时,主机可能只请求前8个字节。你的代码必须能处理这种情况,只返回wLength指定的字节数。
- STALL的正确使用:不支持的请求、非法的参数、错误的方向——统统返回STALL。但要注意,STALL后需要主机发送ClearFeature来清除端点暂停状态。
- 零长度包:当数据长度正好是端点最大包长的整数倍时,需要发送一个零长度包来结束传输。这个细节很容易被忽略。
- 多配置支持:如果设备有多个配置,SetConfiguration时一定要做参数校验。我曾经见过一个设备,配置值传3也能接受,但实际只有2个配置——结果设备直接跑飞了。
避坑指南:我曾经在一个量产项目中,厂商请求的wValue字段被用来传递一个16位的参数。结果测试发现,某些主机在发送请求时会把wValue的高字节和低字节搞反。后来我统一在设备端做字节序转换,才彻底解决这个问题。记住:USB协议使用小端字节序,但某些主控芯片的USB IP核可能已经做了转换,一定要确认清楚。
4.6 调试技巧
设备请求的调试,说白了就是看主机发了什么、设备回了什么。我常用的方法:
- USB分析仪:最直接的方式。抓包看请求内容、设备响应、错误码。推荐用带硬件触发的,能精确定位时序问题。
- 日志输出:在设备端打印每个收到的请求。我习惯在Setup包处理函数入口加一个日志,输出bmRequestType、bRequest、wValue等关键字段。
- 端点0状态监控:控制传输的状态机比较复杂。我会在代码里加一个状态变量,记录当前控制传输处于哪个阶段(Setup、Data、Status),方便定位卡在哪个环节。