4. USB请求块(URB)
USB请求块(USB Request Block,URB)是Linux USB子系统中最核心的数据结构之一,它充当了USB设备驱动与主机控制器硬件之间的通信载体。每个URB描述一次完整的USB数据传输事务,包括传输类型、目标端点、数据缓冲区、传输长度以及完成后的回调函数。理解URB的结构、生命周期以及提交/回调机制,是编写正确、高效的USB设备驱动的关键前提。
核心认知:URB的设计借鉴了网络协议栈中sk_buff的思想,将一次数据传输的所有元数据封装在一个独立的对象中,支持异步提交和完成通知,从而最大化USB总线利用率。
4.1 URB结构体详解
URB结构体定义在<linux/usb.h>头文件中,是驱动与USB核心层交互的主要数据结构。下面列出其关键字段及其作用:
| 字段名 | 类型 | 描述 |
|---|---|---|
urb_list |
struct list_head | 用于将URB链接到提交队列或完成队列的内核链表节点 |
pipe |
unsigned int | USB管道标识,编码了传输方向(IN/OUT)、端点号、传输类型和设备地址 |
dev |
struct usb_device * | 指向目标USB设备的指针,URB最终由该设备的主机控制器处理 |
transfer_buffer |
void * | 指向数据缓冲区的内核虚拟地址,用于存放待发送或接收的数据 |
transfer_buffer_length |
int | 数据缓冲区的长度(字节数),驱动必须正确设置此值 |
actual_length |
int | URB完成时实际传输的字节数,由USB核心层在回调前填充 |
status |
int | URB传输状态,0表示成功,负值表示错误码(如-EPIPE、-ENOENT) |
transfer_flags |
unsigned int | 传输标志位,如URB_NO_TRANSFER_DMA_MAP、URB_ISO_ASAP等 |
complete |
usb_complete_t | URB完成时的回调函数指针,驱动在此函数中处理传输结果 |
context |
void * | 回调函数的私有上下文指针,驱动可传入任意自定义数据 |
start_frame |
int | 等时传输中指定起始帧号,用于周期性传输的时序控制 |
number_of_packets |
int | 等时传输的包描述符数量,每个包对应一个iso_frame_desc结构 |
interval |
int | 中断传输的轮询间隔(以帧或微帧为单位),与端点描述符中的bInterval对应 |
关键字段解读:
- pipe:通过
usb_rcvbulkpipe()、usb_sndctrlpipe()等宏构造,驱动不应直接操作位域。 - transfer_flags:常用标志包括
URB_NO_TRANSFER_DMA_MAP(驱动自行管理DMA映射)和URB_FREE_BUFFER(完成时自动释放缓冲区)。 - actual_length:对于批量传输,此值可能小于请求长度,驱动必须检查并处理短包。
4.2 URB生命周期
一个URB从创建到销毁经历完整的生命周期,驱动必须遵循以下阶段操作URB:
- 创建(Allocation):使用
usb_alloc_urb()分配URB实例,传入等时包数量(非等时传输传0)。 - 初始化(Initialization):填充URB的关键字段,包括pipe、dev、transfer_buffer、complete回调等。对于批量/中断/控制传输,可使用辅助函数如
usb_fill_bulk_urb()简化初始化。 - 提交(Submission):调用
usb_submit_urb()将URB提交给USB核心层。提交后URB进入待处理状态,驱动不能再修改其关键字段。 - 处理(Processing):USB核心层将URB传递给主机控制器驱动(HCD),HCD执行实际的USB总线事务。此阶段URB处于不可用状态。
- 完成(Completion):传输完成后,USB核心层调用驱动注册的
complete回调函数,并更新status和actual_length字段。驱动在回调中处理数据或错误。 - 回收(Reclamation):驱动在回调中或之后调用
usb_free_urb()释放URB内存。如果URB需要复用,可调用usb_unlink_urb()取消未完成的URB后再重新初始化。
常见陷阱:
- 不要在提交URB后立即释放它——必须等待回调完成或调用
usb_kill_urb()确保URB不再被使用。 - 回调函数在中断上下文中执行,不能调用可能睡眠的函数(如
kmalloc(GFP_KERNEL)、mutex_lock())。 - 复用URB时,必须重新设置
transfer_buffer、transfer_buffer_length和complete字段,但pipe和dev通常保持不变。
4.3 URB提交与回调机制
URB的提交和回调是驱动与USB核心层交互的核心接口。驱动通过usb_submit_urb()将URB提交给核心层,核心层在传输完成后调用complete回调通知驱动。理解这两个接口的语义和约束,是编写可靠USB驱动的必要条件。
4.3.1 URB提交:usb_submit_urb()
usb_submit_urb()是异步非阻塞函数,调用后立即返回。其原型如下:
int usb_submit_urb(struct urb *urb, gfp_t mem_flags);
参数说明:
urb:已初始化完成的URB指针。mem_flags:内存分配标志,在中断上下文中必须使用GFP_ATOMIC,在进程上下文中可使用GFP_KERNEL。
返回值:0表示成功提交;负值表示错误,常见错误码包括:
| 错误码 | 含义 | 常见原因 |
|---|---|---|
-ENODEV |
设备已断开 | 提交时设备已被拔出或复位 |
-ENOMEM |
内存不足 | 无法分配DMA缓冲区或内部数据结构 |
-EINVAL |
参数无效 | URB字段设置错误,如pipe与端点不匹配 |
-ESHUTDOWN |
主机控制器已关闭 | 系统正在进入挂起或关机状态 |
提交规则:
- 同一个URB不能同时提交多次——必须等待前一次完成或取消后才能重新提交。
- 提交后,驱动不能再修改URB的
transfer_buffer、pipe、dev等字段,直到回调返回。 - 对于批量端点,USB规范允许主机控制器对短包进行特殊处理,驱动应检查
actual_length。
4.3.2 回调函数:complete 回调
回调函数的类型定义为:
typedef void (*usb_complete_t)(struct urb *urb);
回调函数在URB传输完成后被调用,驱动在此函数中:
- 检查
urb->status判断传输是否成功。 - 读取
urb->actual_length获取实际传输字节数。 - 处理接收到的数据(对于IN传输)或确认发送完成(对于OUT传输)。
- 决定是否重新提交URB以继续传输(如批量流式传输)。
- 调用
usb_free_urb()释放URB,或将其归还给URB池。
回调函数执行在中断上下文(或软中断上下文)中,因此必须遵守以下约束:
- 不能调用可能睡眠的函数(如
wait_event()、mutex_lock()、kmalloc(GFP_KERNEL))。 - 不能执行长时间运行的操作——应尽快返回,将耗时工作推迟到工作队列或tasklet中。
- 可以安全地调用
usb_submit_urb()重新提交URB(使用GFP_ATOMIC标志)。
4.3.3 典型URB提交代码示例
以下代码展示了一个完整的批量OUT传输URB的创建、初始化和提交流程:
#include <linux/usb.h>
/* 批量传输完成回调 */
static void bulk_write_complete(struct urb *urb)
{
struct my_device *dev = urb->context;
if (urb->status == 0) {
/* 传输成功,urb->actual_length 为实际发送字节数 */
dev_dbg(&dev->interface->dev, "Bulk write OK, %d bytes sent\n",
urb->actual_length);
} else if (urb->status == -ENOENT) {
/* URB被 usb_kill_urb() 取消,正常关闭路径 */
} else {
dev_err(&dev->interface->dev, "Bulk write failed: %d\n",
urb->status);
}
/* 通知等待的进程(如果有) */
complete(&dev->write_done);
}
/* 提交批量写URB */
int submit_bulk_write(struct my_device *dev, u8 *data, int len)
{
struct urb *urb;
int ret;
/* 1. 分配URB */
urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb)
return -ENOMEM;
/* 2. 初始化URB */
usb_fill_bulk_urb(urb, dev->udev,
usb_sndbulkpipe(dev->udev, dev->bulk_out_endpoint),
data, len,
bulk_write_complete, dev);
/* 3. 提交URB */
ret = usb_submit_urb(urb, GFP_KERNEL);
if (ret) {
dev_err(&dev->interface->dev,
"Failed to submit bulk URB: %d\n", ret);
usb_free_urb(urb);
return ret;
}
/* URB已提交,不要释放——回调中会处理 */
return 0;
}
最佳实践:
- 使用
usb_fill_bulk_urb()、usb_fill_int_urb()、usb_fill_control_urb()等辅助函数初始化URB,减少手动设置字段的错误。 - 对于频繁提交的URB,考虑使用URB池(
usb_alloc_urb()+ 复用)而不是每次分配/释放,以降低内存碎片和性能开销。 - 在驱动卸载或设备断开时,调用
usb_kill_urb()确保所有待处理的URB被安全取消,避免回调在驱动已卸载后执行。
4.4 URB传输类型与回调行为对比
不同的USB传输类型在URB提交和回调行为上存在差异,驱动开发者需要根据传输类型选择合适的处理策略:
| 传输类型 | 回调触发时机 | 典型回调处理 | 注意事项 |
|---|---|---|---|
| 控制传输 | Setup包+Data阶段(如有)+Status阶段完成后 | 检查status,读取响应数据 | USB核心层自动处理控制传输的各个阶段;驱动只需关注最终结果 |
| 批量传输 | 每个URB对应的批量事务完成后 | 检查actual_length,处理短包,重新提交以继续流 | 批量传输可能被拆分为多个事务;短包表示传输结束(流控制) |
| 中断传输 | 每个轮询间隔内的事务完成后 | 读取数据,重新提交URB以保持周期性轮询 | 中断URB通常被设计为持续提交的循环;驱动在回调中立即重新提交 |
| 等时传输 | 整个帧/微帧的所有包传输完成后 | 遍历iso_frame_desc数组,检查每个包的状态 | 等时传输没有重传机制;驱动必须处理丢包或错误包 |
关键区别:中断传输和等时传输具有周期性,驱动通常会在回调中立即重新提交URB以维持连续的传输流;而批量传输和控制传输通常是事务性的,一次提交对应一次请求-响应。