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 判断传输结果。典型回调处理流程:

  1. 检查状态:判断 urb->status 是否为 0,非 0 则记录错误并可能重提交。
  2. 处理数据:读取 urb->actual_length 获取实际数据长度,从 urb->transfer_buffer 提取数据。
  3. 重新提交(可选):对于连续中断传输,在回调中重新填充并提交 URB。
  4. 清理资源:如果不再使用,调用 usb_free_urb() 释放 URB。
关键约束:回调函数运行在软中断(softirq)上下文中,不能睡眠(不能调用 msleep()、wait_event()、mutex_lock() 等阻塞函数)。如果需要执行耗时操作,应使用工作队列(workqueue)或 tasklet 推迟处理。

4.4 URB 生命周期与状态机

下图展示了 URB 从创建到完成的完整状态转换:

URB 生命周期状态机 创建 (usb_alloc_urb) 已初始化 (填充成员) 已提交 (usb_submit_urb) 传输中 (硬件处理) 完成 (回调触发) 错误/取消 重新提交 usb_free_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 传输异常时,按以下清单逐项排查:

  1. 检查端点描述符:确认 bEndpointAddress 方向正确,bInterval 值合理(全速设备 1-255 ms,高速设备 1-16 微帧)。
  2. 验证管道创建:使用 usb_rcvintpipe() 或 usb_sndintpipe() 创建管道,确保端点号与描述符一致。
  3. 检查缓冲区对齐:中断传输的缓冲区不需要特殊对齐,但 DMA 缓冲区建议使用 usb_alloc_coherent() 分配。
  4. 确认回调上下文:回调中不能睡眠,检查是否有隐式阻塞调用(如 printk 在特定配置下可能阻塞)。
  5. 监控状态码:在回调中打印 urb->status,对照 4.6 节速查表定位问题。
  6. 检查 USB 带宽:中断传输占用固定的 USB 帧时间,带宽不足时 usb_submit_urb() 返回 -ENOSPC。
  7. 验证设备电源状态:设备进入挂起状态后,URB 提交会失败,需先唤醒设备。
调试技巧:在 /sys/kernel/debug/usb/devices 中可以查看所有 USB 设备的端点配置和当前 URB 状态。使用 usbmon 工具可以捕获 URB 级别的传输日志,是排查中断传输问题的利器。