3. USB设备驱动框架
USB设备驱动是Linux内核中最为典型的总线-设备-驱动模型实现之一。理解其核心框架——probe/remove回调机制、URB(USB Request Block)的完整生命周期以及端点(Endpoint)操作——是进行USB驱动调试与内核崩溃定位的基础。本章将深入剖析这三个关键要素,并辅以SVG架构图与表格,帮助您建立清晰的工程化认知。
3.1 probe/remove回调:驱动的“插拔”契约
当USB设备插入系统时,USB核心(USB Core)会枚举设备并匹配驱动程序。匹配成功后,内核调用驱动的probe函数;设备拔出时,则调用remove函数。这两个回调是驱动生命周期的入口与出口,也是绝大多数初始化与资源释放的发生地。
关键原则: probe函数必须成功完成所有硬件初始化与资源申请,否则驱动应返回负错误码,内核将回滚所有已分配资源。remove函数则必须无遗漏地释放probe中申请的所有资源,包括URB、DMA缓冲区、内核线程等。
3.1.1 probe回调的标准流程
- 接口描述符解析: 从
struct usb_interface中获取struct usb_host_interface,解析端点描述符。 - 端点信息提取: 遍历
desc.bNumEndpoints,记录各端点的地址、类型、最大包长。 - URB分配与初始化: 调用
usb_alloc_urb()为每个批量/中断端点分配URB。 - DMA缓冲区申请: 使用
usb_alloc_coherent()或kmalloc配合DMA映射。 - 内核线程/工作队列创建: 若驱动需要异步处理URB完成事件。
- 提交初始URB: 调用
usb_submit_urb()启动数据流。
3.1.2 remove回调的逆序释放
remove函数必须按probe的逆序执行清理,且需考虑设备可能已处于半断开状态(disconnect)。
| 步骤 | 操作 | 关键函数/宏 |
|---|---|---|
| 1 | 杀死所有正在进行的URB | usb_kill_urb() / usb_unlink_urb() |
| 2 | 停止内核线程/工作队列 | kthread_stop() / cancel_work_sync() |
| 3 | 释放DMA缓冲区 | usb_free_coherent() |
| 4 | 释放URB | usb_free_urb() |
| 5 | 注销设备文件(若有) | device_destroy() / cdev_del() |
调试陷阱: 在remove中调用
usb_kill_urb()时,如果URB完成回调(complete callback)中持有自旋锁或互斥锁,且remove函数也在等待同一把锁,将导致死锁。这是内核崩溃的常见原因之一,调试时需特别关注锁的获取顺序。
3.2 URB(USB请求块)生命周期
URB是USB设备驱动中数据传输的核心载体。它封装了一次USB事务的所有信息:目标端点、数据缓冲区、传输长度、完成回调函数等。理解URB从创建到销毁的完整生命周期,是编写健壮USB驱动的必要条件。
类比: URB类似于网络协议栈中的sk_buff,是驱动与USB主机控制器之间的“数据信封”。
3.2.1 URB生命周期状态机
下图展示了URB从分配到释放的完整状态流转,以及各阶段对应的内核函数。
3.2.2 URB核心字段与操作函数
| 字段/函数 | 作用 | 调试关注点 |
|---|---|---|
urb->dev |
指向目标USB设备 | 设备断开后变为悬空指针 |
urb->pipe |
端点管道宏(类型+方向+地址) | 使用usb_pipetype()检查类型 |
urb->transfer_buffer |
数据缓冲区指针 | 确保缓冲区生命周期长于URB |
urb->transfer_buffer_length |
传输数据长度 | 不能超过端点最大包长×N |
urb->complete |
完成回调函数指针 | 回调中不能调用可能导致睡眠的函数 |
usb_submit_urb() |
提交URB到主机控制器 | 返回-EPERM/-ENODEV表示设备已断开 |
usb_kill_urb() |
同步杀死URB(等待回调完成) | 不能在中断上下文调用 |
3.3 端点操作:与USB设备通信的“端口”
USB端点(Endpoint)是主机与设备之间数据传输的逻辑通道。每个USB接口(Interface)包含若干端点,驱动通过端点地址与类型来区分它们。端点操作是URB提交的前置条件,也是驱动调试中频繁出错的环节。
3.3.1 端点类型与传输特性
| 端点类型 | 传输模式 | 典型用途 | 最大包长(FS/HS/SS) | 错误重试 |
|---|---|---|---|---|
| 控制(Control) | 双向,有握手 | 设备枚举、标准请求 | 8/64/512 | 硬件自动重试 |
| 批量(Bulk) | 单向,保证送达 | U盘、打印机数据 | 8/512/1024 | 硬件自动重试 |
| 中断(Interrupt) | 单向,保证延迟 | 键盘、鼠标、HID | 8/64/1024 | 硬件自动重试 |
| 等时(Isochronous) | 单向,无握手 | 音频、视频流 | 最大1023/1024/3072 | 无重试,数据可能丢失 |
3.3.2 端点信息提取与管道宏
在probe函数中,驱动通过struct usb_host_endpoint获取端点描述符,并使用管道宏构造URB所需的pipe值。
/* 从接口描述符中提取端点信息 */
struct usb_host_interface *altsetting = interface->cur_altsetting;
struct usb_endpoint_descriptor *ep_desc;
int i;
for (i = 0; i < altsetting->desc.bNumEndpoints; i++) {
ep_desc = &altsetting->endpoint[i].desc;
/* 判断端点方向 */
if (usb_endpoint_is_bulk_in(ep_desc)) {
bulk_in_pipe = usb_rcvbulkpipe(dev, ep_desc->bEndpointAddress);
bulk_in_maxp = usb_endpoint_maxp(ep_desc);
} else if (usb_endpoint_is_int_out(ep_desc)) {
int_out_pipe = usb_sndintpipe(dev, ep_desc->bEndpointAddress);
int_out_interval = ep_desc->bInterval;
}
}
管道宏速查表:
usb_rcvbulkpipe(dev, addr) — 批量IN管道usb_sndbulkpipe(dev, addr) — 批量OUT管道usb_rcvintpipe(dev, addr) — 中断IN管道usb_sndintpipe(dev, addr) — 中断OUT管道usb_rcvisocpipe(dev, addr) — 等时IN管道usb_sndisocpipe(dev, addr) — 等时OUT管道
3.3.3 端点操作常见调试陷阱
- 端点地址混淆: 端点地址的低4位是端点号,最高位(bit 7)表示方向(0=OUT,1=IN)。驱动必须使用正确的管道宏,否则
usb_submit_urb()将返回-EINVAL。 - 最大包长越界: 批量端点单次传输不能超过最大包长,但可以通过多个URB或URB的
transfer_buffer_length设置为最大包长的整数倍来实现大数据传输。 - 中断端点间隔忽略: 中断端点的
bInterval字段决定了URB重提交的轮询间隔,驱动必须遵守此值,否则可能导致主机控制器过载或设备无响应。 - 等时端点无重试: 等时传输不保证数据送达,驱动必须在完成回调中检查
urb->iso_frame_desc[i].status,处理丢帧情况。
3.4 综合案例:一个简单的批量传输驱动框架
以下代码片段展示了一个典型的USB批量传输驱动中probe、URB提交与remove的协作模式。
/* probe函数:初始化端点与URB */
static int my_usb_probe(struct usb_interface *intf,
const struct usb_device_id *id)
{
struct usb_device *dev = interface_to_usbdev(intf);
struct usb_host_interface *alt = intf->cur_altsetting;
struct my_driver_data *data;
int ret;
data = kzalloc(sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
/* 提取批量IN端点 */
data->bulk_in_pipe = usb_rcvbulkpipe(dev,
alt->endpoint[0].desc.bEndpointAddress);
data->bulk_in_maxp = usb_endpoint_maxp(
&alt->endpoint[0].desc);
/* 分配并填充URB */
data->urb = usb_alloc_urb(0, GFP_KERNEL);
if (!data->urb) {
kfree(data);
return -ENOMEM;
}
data->buffer = usb_alloc_coherent(dev, data->bulk_in_maxp,
GFP_KERNEL, &data->dma);
if (!data->buffer) {
usb_free_urb(data->urb);
kfree(data);
return -ENOMEM;
}
usb_fill_bulk_urb(data->urb, dev, data->bulk_in_pipe,
data->buffer, data->bulk_in_maxp,
my_urb_complete, data);
data->urb->transfer_dma = data->dma;
data->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
ret = usb_submit_urb(data->urb, GFP_KERNEL);
if (ret) {
dev_err(&intf->dev, "URB submit failed: %d\n", ret);
usb_free_coherent(dev, data->bulk_in_maxp,
data->buffer, data->dma);
usb_free_urb(data->urb);
kfree(data);
return ret;
}
usb_set_intfdata(intf, data);
return 0;
}
/* remove函数:逆序释放资源 */
static void my_usb_disconnect(struct usb_interface *intf)
{
struct my_driver_data *data = usb_get_intfdata(intf);
struct usb_device *dev = interface_to_usbdev(intf);
usb_kill_urb(data->urb); /* 同步杀死URB */
usb_free_coherent(dev, data->bulk_in_maxp,
data->buffer, data->dma);
usb_free_urb(data->urb);
kfree(data);
usb_set_intfdata(intf, NULL);
}
调试建议: 在probe和remove函数中增加
dev_dbg()打印,记录端点参数、URB提交结果和资源释放顺序。当内核崩溃发生时,通过dmesg日志可以快速定位是哪个阶段出错。
3.5 本章小结
- probe/remove回调是USB驱动的生命周期管理入口,必须严格遵循“先申请后释放、逆序释放”的原则,避免资源泄漏与死锁。
- URB生命周期从
usb_alloc_urb()到usb_free_urb(),中间经历填充、提交、硬件传输和完成回调。调试时重点关注usb_submit_urb()的返回值与完成回调中的status字段。 - 端点操作是URB提交的前置条件,必须使用正确的管道宏,并遵守端点的最大包长、传输类型和轮询间隔等约束。
- 掌握这三部分内容,您将能够快速定位USB驱动中80%以上的初始化与数据传输相关崩溃问题。