4. URB(USB Request Block)结构体详解
URB(USB Request Block)是 USB 设备驱动与主机控制器之间进行数据交互的核心载体。每个 URB 代表一次完整的 USB 传输请求,包含传输类型、目标端点、数据缓冲区、回调函数等关键信息。理解 struct urb 的成员、标志位和回调机制,是编写稳定 USB 驱动的基石。
核心概念:URB 是 USB 子系统中的“任务描述符”,驱动通过填充 URB 并提交给内核,由 USB 核心层调度执行,完成后通过回调函数通知驱动。
4.1 struct urb 核心成员
struct urb 定义在 <linux/usb.h> 中,包含数十个成员。以下按功能分类列出最常用的核心成员:
| 分类 | 成员名 | 类型 | 说明 |
|---|---|---|---|
| 目标标识 | dev |
struct usb_device * |
目标 USB 设备指针 |
pipe |
unsigned int |
端点管道,编码了端点号、方向、传输类型 | |
| 传输控制 | transfer_flags |
unsigned int |
URB 标志位,控制传输行为(见 4.2 节) |
transfer_buffer |
void * |
数据缓冲区指针(DMA 安全或普通内存) | |
| 数据描述 | transfer_buffer_length |
int |
缓冲区长度(字节) |
actual_length |
int |
实际传输的字节数(回调时有效) | |
| 回调机制 | complete |
usb_complete_t |
传输完成回调函数指针 |
context |
void * |
回调上下文参数,通常指向驱动私有数据 | |
| 状态与错误 | status |
int |
传输状态码(0 成功,负值表示错误) |
error_count |
int |
ISO 传输中的错误包计数 | |
| 中断/ISO 专用 | interval |
int |
中断/ISO 端点轮询间隔(毫秒或微帧) |
number_of_packets |
int |
ISO 传输的包数量 |
开发提示:在中断传输中,
interval 字段必须与端点描述符中的 bInterval 匹配。对于高速设备,interval 的单位是微帧(125 µs),全速设备是毫秒。
4.2 URB 标志位详解
transfer_flags 控制 URB 的传输行为,常用标志位如下:
| 标志位 | 值 | 含义 | 使用场景 |
|---|---|---|---|
URB_SHORT_NOT_OK |
0x0001 | 短包视为错误,status 置 -EREMOTEIO |
需要精确长度的批量/中断读 |
URB_ISO_ASAP |
0x0002 | ISO 传输尽快调度,而非指定帧号 | 实时音频/视频流 |
URB_NO_TRANSFER_DMA_MAP |
0x0004 | 缓冲区已做 DMA 映射,内核不再重复映射 | 高性能驱动自行管理 DMA |
URB_NO_INTERRUPT |
0x0080 | 完成时不触发中断回调(仅用于特定场景) | 批量传输的批量完成优化 |
URB_FREE_BUFFER |
0x0100 | 回调后自动释放 transfer_buffer |
一次性传输,避免手动释放 |
URB_DIR_IN |
0x0200 | 设备到主机方向(读) | 与 pipe 方向一致时使用 |
URB_DIR_OUT |
0x0400 | 主机到设备方向(写) | 与 pipe 方向一致时使用 |
常见错误:忘记设置
URB_SHORT_NOT_OK 可能导致驱动在收到短包时误认为传输成功,而实际数据不完整。对于需要固定长度数据包的协议(如 HID 报告),建议始终设置此标志。
4.3 回调函数机制
URB 的回调函数是异步传输完成后的通知入口,在软中断上下文中执行。回调函数原型如下:
typedef void (*usb_complete_t)(struct urb *urb);
回调函数通过 urb->context 获取驱动私有数据,通过 urb->status 判断传输结果。典型回调处理流程:
- 检查状态:判断
urb->status是否为 0,非 0 则记录错误并可能重提交。 - 处理数据:读取
urb->actual_length获取实际数据长度,从urb->transfer_buffer提取数据。 - 重新提交(可选):对于连续中断传输,在回调中重新填充并提交 URB。
- 清理资源:如果不再使用,调用
usb_free_urb()释放 URB。
关键约束:回调函数运行在软中断(softirq)上下文中,不能睡眠(不能调用
msleep()、wait_event()、mutex_lock() 等阻塞函数)。如果需要执行耗时操作,应使用工作队列(workqueue)或 tasklet 推迟处理。
4.4 URB 生命周期与状态机
下图展示了 URB 从创建到完成的完整状态转换:
4.5 中断 URB 的典型代码模式
以下代码展示了中断传输 URB 的创建、提交和回调处理的标准模式:
/* 中断 URB 回调函数 */
static void intr_urb_complete(struct urb *urb)
{
struct my_driver_data *priv = urb->context;
if (urb->status == 0) {
/* 传输成功,处理数据 */
process_received_data(priv, urb->transfer_buffer,
urb->actual_length);
} else if (urb->status == -ENOENT) {
/* URB 被主动取消,正常退出 */
return;
} else if (urb->status == -ECONNRESET) {
/* 设备断开 */
dev_err(&priv->intf->dev, "device disconnected\n");
return;
} else {
/* 其他错误,尝试重新提交 */
dev_dbg(&priv->intf->dev, "URB error %d, resubmitting\n",
urb->status);
}
/* 重新提交 URB 以接收下一个中断 */
retval = usb_submit_urb(urb, GFP_ATOMIC);
if (retval)
dev_err(&priv->intf->dev, "resubmit failed: %d\n", retval);
}
/* 初始化并提交中断 URB */
static int setup_intr_urb(struct usb_device *udev,
struct my_driver_data *priv)
{
struct urb *urb;
unsigned int pipe;
int retval;
/* 1. 分配 URB */
urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb)
return -ENOMEM;
/* 2. 获取中断输入管道 */
pipe = usb_rcvintpipe(udev, priv->ep_addr);
/* 3. 填充 URB */
usb_fill_int_urb(urb, udev, pipe,
priv->buffer, /* 数据缓冲区 */
priv->buffer_size, /* 缓冲区长度 */
intr_urb_complete, /* 回调函数 */
priv, /* 上下文指针 */
priv->interval); /* 轮询间隔 */
/* 4. 设置标志位:短包视为错误 */
urb->transfer_flags |= URB_SHORT_NOT_OK;
/* 5. 提交 URB */
retval = usb_submit_urb(urb, GFP_KERNEL);
if (retval) {
usb_free_urb(urb);
return retval;
}
priv->intr_urb = urb;
return 0;
}
最佳实践:在回调函数中重新提交 URB 时,必须使用
GFP_ATOMIC 标志,因为回调运行在原子上下文中。初始化时可以使用 GFP_KERNEL 允许睡眠。
4.6 URB 状态码速查表
回调函数中 urb->status 的常见值及其含义:
| 状态码 | 数值 | 含义 | 处理建议 |
|---|---|---|---|
0 |
0 | 传输成功 | 正常处理数据 |
-EINPROGRESS |
-115 | URB 仍在处理中 | 不应出现在回调中,属内部状态 |
-ENOENT |
-2 | URB 被 usb_kill_urb() 取消 |
正常退出,无需处理 |
-ECONNRESET |
-104 | URB 被 usb_unlink_urb() 取消 |
正常退出,无需处理 |
-EPROTO |
-71 | 协议错误(如 STALL、Babble) | 清除端点 halt,重试或复位设备 |
-ETIMEDOUT |
-110 | 传输超时 | 检查硬件连接,增加超时时间 |
-EREMOTEIO |
-121 | 短包错误(设置了 URB_SHORT_NOT_OK) |
检查数据长度是否匹配 |
-ESHUTDOWN |
-108 | 主机控制器已关闭(设备断开) | 停止所有 URB,准备卸载 |
4.7 URB 分配与释放 API
内核提供了一组辅助函数简化 URB 的管理:
| 函数 | 原型 | 说明 |
|---|---|---|
usb_alloc_urb |
struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags) |
分配 URB,iso_packets 为 ISO 传输的包数(非 ISO 传 0) |
usb_free_urb |
void usb_free_urb(struct urb *urb) |
释放 URB,必须在 URB 不再被内核引用时调用 |
usb_fill_int_urb |
void usb_fill_int_urb(struct urb *urb, ...) |
填充中断 URB 的常用字段(pipe、buffer、complete、interval) |
usb_fill_bulk_urb |
void usb_fill_bulk_urb(struct urb *urb, ...) |
填充批量 URB 的常用字段 |
usb_fill_control_urb |
void usb_fill_control_urb(struct urb *urb, ...) |
填充控制 URB 的常用字段 |
usb_submit_urb |
int usb_submit_urb(struct urb *urb, gfp_t mem_flags) |
提交 URB 到 USB 核心层调度执行 |
usb_kill_urb |
void usb_kill_urb(struct urb *urb) |
同步取消 URB,等待回调完成(可睡眠) |
usb_unlink_urb |
int usb_unlink_urb(struct urb *urb) |
异步取消 URB,立即返回(原子上下文可用) |
资源管理要点:
- 在驱动卸载时,必须先调用
usb_kill_urb()确保所有 URB 完成,再调用usb_free_urb()释放。 - 如果设置了
URB_FREE_BUFFER标志,内核会在回调后自动释放transfer_buffer,驱动不应再次释放。 - 回调函数中不要直接调用
usb_free_urb(),因为 URB 可能仍被内核引用;应在确保 URB 已取消后再释放。
4.8 中断 URB 的故障排查清单
当中断 URB 传输异常时,按以下清单逐项排查:
- 检查端点描述符:确认
bEndpointAddress方向正确,bInterval值合理(全速设备 1-255 ms,高速设备 1-16 微帧)。 - 验证管道创建:使用
usb_rcvintpipe()或usb_sndintpipe()创建管道,确保端点号与描述符一致。 - 检查缓冲区对齐:中断传输的缓冲区不需要特殊对齐,但 DMA 缓冲区建议使用
usb_alloc_coherent()分配。 - 确认回调上下文:回调中不能睡眠,检查是否有隐式阻塞调用(如
printk在特定配置下可能阻塞)。 - 监控状态码:在回调中打印
urb->status,对照 4.6 节速查表定位问题。 - 检查 USB 带宽:中断传输占用固定的 USB 帧时间,带宽不足时
usb_submit_urb()返回-ENOSPC。 - 验证设备电源状态:设备进入挂起状态后,URB 提交会失败,需先唤醒设备。
调试技巧:在
/sys/kernel/debug/usb/devices 中可以查看所有 USB 设备的端点配置和当前 URB 状态。使用 usbmon 工具可以捕获 URB 级别的传输日志,是排查中断传输问题的利器。