4. 控制传输与标准请求处理
USB 控制传输是主机与设备之间唯一在枚举阶段就必然使用的传输类型,它承载了设备描述符读取、地址分配、配置选择等关键动作。emUSB-Device 协议栈把控制传输的底层握手、数据包分片、状态阶段处理都封装在协议栈内部,开发者真正需要介入的,是标准请求的响应数据准备,以及自定义请求的分发与应答。理解这一层分工,是写出稳定 USB 设备固件的前提。
控制传输的四个阶段与 emUSB 的处理位置
一次控制传输在总线上由 SETUP、可选 DATA(IN 或 OUT)、STATUS 三个阶段组成,其中 SETUP 阶段固定为 8 字节,格式为 bmRequestType、bRequest、wValue、wIndex、wLength。emUSB-Device 在底层完成 SETUP 包的解析后,会先判断该请求属于标准请求、类请求还是厂商自定义请求,再决定是协议栈内部直接应答,还是回调到用户注册的处理函数。
需要特别注意的是,标准请求并不总是由协议栈“全自动”完成。GET_DESCRIPTOR 中的设备描述符、配置描述符、字符串描述符,emUSB 会从用户通过 API 注册的描述符数据中取用;SET_CONFIGURATION 则会触发协议栈内部的端点使能、接口状态切换,并可能回调用户的配置变更通知。因此,开发者必须清楚哪些数据由自己提供,哪些状态由协议栈维护。
标准请求的处理要点
GET_DESCRIPTOR 是最频繁的标准请求。主机在枚举时会先以 wLength=64 读取设备描述符的前 8 字节,确认 bMaxPacketSize0 后,再以完整长度重新读取。emUSB 内部维护描述符表,用户通过 USB_DEVICE_SetDesc 或类似接口注册设备描述符、配置描述符集合和字符串描述符。配置描述符集合必须包含配置描述符、接口描述符、端点描述符,且 wTotalLength 要精确等于整个集合的字节数,否则主机会在读取时截断或报错。
SET_CONFIGURATION 请求的 wValue 低字节是目标配置编号。emUSB 收到该请求后,会查找匹配的配置描述符,使能其中声明的端点,并调用用户注册的配置变更回调。此时用户应完成与端点相关的初始化,例如重置数据缓冲区、启动接收、更新应用状态机。若 wValue 为 0,表示进入未配置状态,所有非控制端点应被禁用,用户回调中需要停止相关传输并释放资源。
GET_STATUS、CLEAR_FEATURE、SET_FEATURE 等请求通常由协议栈内部处理,但涉及远程唤醒、端点 halt 状态时,用户可能需要通过回调感知。例如端点 STALL 后,主机发送 CLEAR_FEATURE 清除 halt,emUSB 会恢复端点,用户若在回调中维护了错误计数,应同步清零。
使用 emUSB 回调实现自定义控制请求
厂商自定义请求的 bmRequestType 中 Type 字段为 2(Vendor),emUSB 在标准请求和类请求都未匹配时,会调用用户注册的厂商请求处理函数。典型注册方式如下:
static int MyVendorRequest(USB_DEVICE_HANDLE hDevice,
U8 bRequest,
U16 wValue,
U16 wIndex,
U16 wLength,
U8 *pBuffer)
{
switch (bRequest) {
case 0x01: /* 读取设备运行状态 */
pBuffer[0] = g_deviceState;
pBuffer[1] = g_errorCode;
return 2; /* 返回实际写入的字节数 */
case 0x02: /* 设置工作模式 */
g_workMode = (U8)wValue;
return 0; /* 无数据阶段 */
default:
return -1; /* 返回负值,协议栈回 STALL */
}
}
/* 注册回调 */
USB_DEVICE_SetVendorRequestCallback(hDevice, MyVendorRequest);
回调的返回值语义需要明确:返回非负值表示成功,协议栈会按该值组织 IN 数据或确认 OUT 数据;返回负值则协议栈在状态阶段回 STALL,主机端会收到传输失败。对于有数据阶段的请求,若 wLength 大于实际返回长度,协议栈会自动补零或按短包处理,具体行为取决于端点最大包长和主机期望。
在实现自定义请求时,wIndex 常被用作接口号或端点号,wValue 常被用作参数或寄存器地址。建议在回调入口先做参数合法性检查,例如 wIndex 超出接口数量、wLength 超过缓冲区容量时直接返回错误,避免越界写入。下面给出一个可直接参考的请求处理检查表:
| 检查项 | 判断依据 | 处理动作 |
|---|---|---|
| 请求类型 | bmRequestType.Type == 2 | 进入厂商回调,否则由协议栈处理 |
| 方向 | bmRequestType.Dir | 0 为 OUT,1 为 IN,决定数据阶段方向 |
| wLength 容量 | wLength ≤ 本地缓冲区大小 | 超出则返回 -1,回 STALL |
| wIndex 范围 | wIndex < 接口总数 | 越界则拒绝,防止误操作其他接口 |
| 返回值 | 实际写入字节数或 -1 | 非负成功,负值 STALL |
调试与验证方法
验证控制传输是否按预期工作,最直接的方式是使用 USB 协议分析仪抓取枚举过程,观察 SETUP 包的 bRequest、wValue、wLength 以及设备返回的数据长度。若没有分析仪,可以在 emUSB 的回调中通过 RTT 或串口打印请求摘要,例如打印 bRequest、wValue、wIndex、wLength 和返回值。常见异常包括:描述符 wTotalLength 与实际字节数不一致导致主机读取配置失败;自定义请求返回长度超过 wLength 导致协议栈行为异常;SET_CONFIGURATION 回调中未正确使能端点,导致后续数据传输无响应。
控制传输的稳定性直接影响设备能否被主机识别。把标准请求交给协议栈、把描述符数据和自定义请求逻辑交给用户代码,是 emUSB-Device 的基本分工。掌握回调的注册方式、返回值语义和参数检查方法,就能在枚举阶段和运行阶段都获得可预期的控制通道行为。