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回调的标准流程

  1. 接口描述符解析: 从struct usb_interface中获取struct usb_host_interface,解析端点描述符。
  2. 端点信息提取: 遍历desc.bNumEndpoints,记录各端点的地址、类型、最大包长。
  3. URB分配与初始化: 调用usb_alloc_urb()为每个批量/中断端点分配URB。
  4. DMA缓冲区申请: 使用usb_alloc_coherent()或kmalloc配合DMA映射。
  5. 内核线程/工作队列创建: 若驱动需要异步处理URB完成事件。
  6. 提交初始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从分配到释放的完整状态流转,以及各阶段对应的内核函数。

URB生命周期状态机 1. usb_alloc_urb() 分配URB结构体 2. usb_fill_*_urb() 填充URB参数 3. usb_submit_urb() 提交到主机控制器 4. 硬件传输中 等待完成/超时 5. 完成回调 检查status/重提交 6. usb_free_urb() 释放URB内存 关键调试点 • 步骤3失败:检查端点号/类型 • 步骤5 status非0:查看-EPROTO • 步骤6前必须kill_urb 常见崩溃原因 • 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%以上的初始化与数据传输相关崩溃问题。