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在提交阶段的作用:
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下的典型处理路径:
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。