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 请求处理的核心逻辑

说了这么多,咱们用一张图把整个请求处理流程串起来:

USB设备请求处理流程 收到Setup包 解析bmRequestType 标准请求处理 厂商请求处理 类请求处理 GetDescriptor / SetConfiguration SetAddress / GetConfiguration SetInterface / ClearFeature 读取固件版本 / 设备状态 固件升级 / 参数配置 自定义数据传输 HID报告描述符请求 CDC通信类请求 大容量存储类请求 返回数据或STALL 不支持的请求统一返回STALL,避免主机超时重试导致总线拥堵

4.5 实现中的关键细节

写设备请求处理代码时,有几个坑我踩过,分享给大家:

  1. wLength的处理:主机请求的数据长度可能小于实际数据。比如GetDescriptor时,主机可能只请求前8个字节。你的代码必须能处理这种情况,只返回wLength指定的字节数。
  2. STALL的正确使用:不支持的请求、非法的参数、错误的方向——统统返回STALL。但要注意,STALL后需要主机发送ClearFeature来清除端点暂停状态。
  3. 零长度包:当数据长度正好是端点最大包长的整数倍时,需要发送一个零长度包来结束传输。这个细节很容易被忽略。
  4. 多配置支持:如果设备有多个配置,SetConfiguration时一定要做参数校验。我曾经见过一个设备,配置值传3也能接受,但实际只有2个配置——结果设备直接跑飞了。

避坑指南:我曾经在一个量产项目中,厂商请求的wValue字段被用来传递一个16位的参数。结果测试发现,某些主机在发送请求时会把wValue的高字节和低字节搞反。后来我统一在设备端做字节序转换,才彻底解决这个问题。记住:USB协议使用小端字节序,但某些主控芯片的USB IP核可能已经做了转换,一定要确认清楚。

4.6 调试技巧

设备请求的调试,说白了就是看主机发了什么、设备回了什么。我常用的方法:

  • USB分析仪:最直接的方式。抓包看请求内容、设备响应、错误码。推荐用带硬件触发的,能精确定位时序问题。
  • 日志输出:在设备端打印每个收到的请求。我习惯在Setup包处理函数入口加一个日志,输出bmRequestType、bRequest、wValue等关键字段。
  • 端点0状态监控:控制传输的状态机比较复杂。我会在代码里加一个状态变量,记录当前控制传输处于哪个阶段(Setup、Data、Status),方便定位卡在哪个环节。