2. URB数据结构深度解析:struct urb成员详解、传输标志位、回调函数机制

URB(USB Request Block)是USB主机控制器与设备驱动程序之间交换数据的核心载体。每个URB实例封装了一次USB传输所需的所有信息:目标端点、数据缓冲区、传输长度、标志位以及完成回调函数。理解struct urb的每个成员,是编写可靠USB驱动的第一步。

核心认知: URB类似于网络协议栈中的sk_buff,是USB子系统中最基础的数据交换单元。驱动开发者通过填充URB字段,将传输请求提交给USB核心层,由核心层调度给主机控制器硬件执行。

2.1 struct urb 核心成员一览

内核头文件 <linux/usb.h> 中定义了 struct urb,其成员按功能可分为:目标定位、传输控制、数据缓冲区、完成回调、状态反馈五大类。下表列出了最常用的成员:

分类 成员名 类型 说明
目标定位 dev struct usb_device * 目标USB设备指针,URB提交前必须赋值
pipe unsigned int 端点管道宏编码,包含端点号、方向、传输类型
endpoint unsigned int 端点描述符编号(调试用,非必须)
传输控制 transfer_flags unsigned int 传输标志位掩码,控制URB行为(见2.2节)
interval int 中断/等时传输的轮询间隔(单位:帧/微帧)
number_of_packets int 等时传输的包数量,批量/中断传输固定为0
数据缓冲区 transfer_buffer void * 数据缓冲区虚拟地址,驱动分配或使用DMA缓冲区
transfer_dma dma_addr_t DMA缓冲区物理地址(配合URB_NO_TRANSFER_DMA_MAP标志)
transfer_buffer_length int 缓冲区长度(字节),实际传输长度可能小于此值
完成回调 complete usb_complete_t URB完成时的回调函数指针(见2.3节)
状态反馈 status int 传输完成状态:0成功,负值为错误码
actual_length int 实际传输的数据字节数
上下文 context void * 驱动私有上下文指针,回调中通过urb->context获取
编程要点: 在提交URB之前,必须设置 dev、pipe、transfer_buffer、transfer_buffer_length 和 complete 五个字段。遗漏任何一个都会导致内核崩溃或URB被拒绝。

2.2 传输标志位(transfer_flags)详解

transfer_flags 是一个位掩码字段,控制URB在提交、映射和完成阶段的行为。以下是最常用的标志位:

标志位 值 作用 使用场景
URB_NO_TRANSFER_DMA_MAP 0x0001 告知USB核心:transfer_dma已由驱动完成映射,无需再映射 驱动使用DMA一致性缓冲区或流式映射时
URB_NO_SETUP_DMA_MAP 0x0002 控制传输setup包已由驱动完成DMA映射 控制传输使用DMA缓冲区时
URB_ASYNC_UNLINK 0x0004 允许异步取消URB,不等待取消完成 驱动在中断上下文或不想阻塞时取消URB
URB_SHORT_NOT_OK 0x0008 当实际传输长度小于请求长度时,将status置为-EREMOTEIO 批量传输要求精确长度时(如固件下载)
URB_FREE_BUFFER 0x0010 URB完成时自动释放transfer_buffer 驱动使用kmalloc分配一次性缓冲区时
URB_DIR_IN 0x0020 数据传输方向:设备到主机(读) 批量/中断/等时读传输
URB_DIR_OUT 0x0040 数据传输方向:主机到设备(写) 批量/中断/等时写传输
URB_ZERO_PACKET 0x0080 批量OUT传输结束时自动发送零长度包 批量写传输,数据长度为端点最大包长整数倍时
常见错误: 方向标志位 URB_DIR_IN / URB_DIR_OUT 必须与pipe宏编码的方向一致。例如使用 usb_rcvbulkpipe() 时,必须设置 URB_DIR_IN;使用 usb_sndbulkpipe() 时,必须设置 URB_DIR_OUT。方向不一致会导致URB被拒绝或数据损坏。

下面的SVG图展示了URB从创建、填充标志位、提交到完成回调的完整生命周期,重点标注了transfer_flags在提交阶段的作用:

URB生命周期与transfer_flags作用点 1. usb_alloc_urb() 2. 填充成员 3. 设置transfer_flags URB_SHORT_NOT_OK URB_ZERO_PACKET 等 4. usb_submit_urb() 5. HCD硬件传输 6. complete回调 说明: transfer_flags 在步骤3设置,影响步骤4的DMA映射行为和步骤6的完成状态判断。 URB_SHORT_NOT_OK 和 URB_ZERO_PACKET 是批量传输中最常用的两个标志位。

2.3 回调函数机制(complete callback)

URB的完成回调函数是异步通知的核心机制。当USB主机控制器完成一次数据传输(无论成功、失败还是被取消),内核都会在软中断上下文中调用驱动注册的complete函数。回调函数的原型如下:

typedef void (*usb_complete_t)(struct urb *urb);

回调函数接收一个参数——指向已完成URB的指针。驱动通过检查urb->status判断传输结果,通过urb->actual_length获取实际传输字节数。

2.3.1 回调函数执行上下文

属性 说明
执行上下文 软中断(SoftIRQ),不可睡眠
可调用函数限制 不能调用可能睡眠的函数(如 kmalloc(GFP_KERNEL)、mutex_lock())
推荐操作 使用 GFP_ATOMIC 分配内存,使用自旋锁保护共享数据
数据传递 通过 urb->context 传递驱动私有数据结构
URB重用 回调中可重新填充并再次提交同一个URB(需先检查status)
最佳实践: 回调函数应保持轻量,仅做状态记录、数据搬运和唤醒等待队列。如果需要执行复杂处理,应使用工作队列(workqueue)将任务推迟到进程上下文执行。

2.3.2 回调函数典型实现模板

static void my_bulk_complete(struct urb *urb)
{
    struct my_device *dev = urb->context;

    /* 检查传输状态 */
    if (urb->status == 0) {
        /* 传输成功,处理 urb->actual_length 字节数据 */
        dev_info(&dev->interface->dev,
                 "Bulk transfer succeeded, %d bytes received\n",
                 urb->actual_length);
        /* 将数据从 urb->transfer_buffer 拷贝到驱动缓冲区 */
        memcpy(dev->data_buf, urb->transfer_buffer,
               min(urb->actual_length, dev->buf_size));
    } else if (urb->status == -ENOENT) {
        /* URB被 usb_kill_urb() 取消,正常退出 */
        dev_dbg(&dev->interface->dev, "URB killed\n");
    } else if (urb->status == -ECONNRESET) {
        /* URB被 usb_unlink_urb() 取消 */
        dev_dbg(&dev->interface->dev, "URB unlinked\n");
    } else {
        /* 其他错误,如 -EPIPE(端点停止)、-ETIMEDOUT(超时) */
        dev_err(&dev->interface->dev,
                "Bulk transfer failed: %d\n", urb->status);
    }

    /* 唤醒等待队列,通知上层数据已到达 */
    complete(&dev->transfer_done);
}

2.3.3 回调函数中的status值速查

status值 含义 驱动处理建议
0 传输成功 正常处理数据
-ENOENT URB被 usb_kill_urb() 同步取消 释放资源,不重提URB
-ECONNRESET URB被 usb_unlink_urb() 异步取消 释放资源,不重提URB
-EPIPE 端点停止(stall),通常需要 usb_clear_halt() 恢复 调用 usb_clear_halt() 后重试
-ETIMEDOUT 批量传输超时(由HCD驱动检测) 重试或复位端点
-EREMOTEIO 短包错误(仅当设置了 URB_SHORT_NOT_OK 时) 检查数据完整性,决定是否重传
-ESHUTDOWN 主机控制器或设备已断开 停止所有URB,准备设备移除

下面的SVG图展示了回调函数内部的状态判断流程,以及驱动在不同status下的典型处理路径:

回调函数状态判断与处理流程 complete(urb) 进入 status? status == 0 处理 actual_length 数据 -ENOENT / -ECONNRESET 释放资源,不重提 -EPIPE (端点停止) usb_clear_halt() 后重试 -ETIMEDOUT (超时) 重试或复位端点 -ESHUTDOWN (设备断开) 停止所有URB,准备移除 其他错误 记录错误日志 唤醒等待队列 / 返回

2.4 URB内存管理关键函数

与URB数据结构配套的内存管理函数,驱动开发者必须掌握:

函数 原型 说明
usb_alloc_urb struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags) 分配URB结构体,iso_packets为等时传输包数(批量/中断传0)
usb_free_urb void usb_free_urb(struct urb *urb) 释放URB,如果URB已被提交且未完成,调用者需先取消
usb_fill_bulk_urb void usb_fill_bulk_urb(struct urb *urb, ...) 辅助函数,填充批量传输URB的常用字段
usb_fill_int_urb void usb_fill_int_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核心层,mem_flags通常为GFP_ATOMIC(中断上下文)
usb_kill_urb void usb_kill_urb(struct urb *urb) 同步取消URB,等待回调执行完毕后才返回
usb_unlink_urb int usb_unlink_urb(struct urb *urb) 异步取消URB,立即返回,回调可能在之后执行
内存管理黄金法则:
  • 谁分配,谁释放——usb_alloc_urb() 与 usb_free_urb() 必须配对使用。
  • 提交后的URB不能立即释放——必须先调用 usb_kill_urb() 或 usb_unlink_urb() 确保URB不再被使用。
  • 回调函数中不要释放URB本身——将释放操作推迟到调用 usb_kill_urb() 之后。

2.5 批量传输URB填充示例

以下代码展示了如何分配、填充并提交一个批量IN传输的URB:

struct urb *urb;
struct my_device *dev;
int ret;

/* 1. 分配URB(批量传输,iso_packets = 0) */
urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb) {
    dev_err(&dev->interface->dev, "Failed to allocate URB\n");
    return -ENOMEM;
}

/* 2. 分配数据缓冲区 */
dev->buf = kmalloc(BUF_SIZE, GFP_KERNEL);
if (!dev->buf) {
    usb_free_urb(urb);
    return -ENOMEM;
}

/* 3. 使用辅助函数填充URB */
usb_fill_bulk_urb(urb,                     /* URB指针 */
                  dev->udev,               /* USB设备 */
                  usb_rcvbulkpipe(dev->udev, 0x01), /* 批量IN管道,端点1 */
                  dev->buf,                /* 数据缓冲区 */
                  BUF_SIZE,                /* 缓冲区长度 */
                  my_bulk_complete,        /* 回调函数 */
                  dev);                    /* context上下文 */

/* 4. 设置传输标志位 */
urb->transfer_flags |= URB_SHORT_NOT_OK;  /* 短包视为错误 */

/* 5. 提交URB */
ret = usb_submit_urb(urb, GFP_KERNEL);
if (ret) {
    dev_err(&dev->interface->dev,
            "Failed to submit URB: %d\n", ret);
    kfree(dev->buf);
    usb_free_urb(urb);
    return ret;
}

/* URB已提交,等待回调执行 */
注意: 上述示例在进程上下文中使用 GFP_KERNEL。如果在中断上下文或持有自旋锁时提交URB,必须使用 GFP_ATOMIC。错误的内存标志会导致内核内存分配失败,返回 -ENOMEM。