4. USB设备驱动框架:usb_driver结构体、probe与disconnect回调函数

好,咱们今天聊聊USB设备驱动的核心骨架。说白了,就是那个usb_driver结构体,以及它里面最重要的两个回调函数——probedisconnect

我刚开始写USB驱动时,总觉得这东西很神秘。后来发现,其实内核已经帮我们搭好了舞台,我们要做的,就是写好演员的剧本。嗯,这个比喻可能不太准确,但意思到了。

4.1 usb_driver结构体:驱动的身份证

每个USB设备驱动,都需要注册一个usb_driver实例。它就像驱动的身份证,告诉内核:我能管哪些设备,我该怎么初始化,我该怎么退出。

来看一下这个结构体的核心成员:

static struct usb_driver my_usb_driver = {
    .name       = "my_usb_device",
    .id_table   = my_usb_id_table,
    .probe      = my_usb_probe,
    .disconnect = my_usb_disconnect,
    .suspend    = my_usb_suspend,
    .resume     = my_usb_resume,
};

我个人习惯,把name字段起得有意义一些。比如用"厂商名_设备名"的格式。这样在lsusb -t或者dmesg里一眼就能认出来。

字段 作用 必填?
name 驱动名称,用于sysfs和调试
id_table 支持的设备列表(VID/PID匹配)
probe 设备插入时调用
disconnect 设备拔出时调用
suspend/resume 电源管理回调 可选
核心要点:id_table是驱动和设备之间的"媒人"。内核通过它来判断,某个USB设备该不该由你的驱动来管。

4.2 id_table:如何告诉内核"这是我的菜"

id_table是一个结构体数组,每个元素描述一个设备。最常用的匹配方式是VID(厂商ID)和PID(产品ID)。

static const struct usb_device_id my_usb_id_table[] = {
    { USB_DEVICE(0x1234, 0x5678) },
    { USB_DEVICE(0x1234, 0x5679) },
    { }  /* 终止项,必须要有 */
};
MODULE_DEVICE_TABLE(usb, my_usb_id_table);

这里有个坑,我曾经踩过:MODULE_DEVICE_TABLE这个宏一定要写。如果不写,驱动加载后,内核根本不知道它能支持哪些设备。嗯,我当时调试了一下午才发现。

除了VID/PID,还可以用USB_DEVICE_INFO按设备类、子类、协议来匹配。比如匹配所有HID设备:

{ USB_DEVICE_INFO(0x03, 0x00, 0x00) },  /* HID设备 */

4.3 probe回调:设备插入时的"欢迎仪式"

当用户插入一个USB设备,内核发现id_table匹配成功,就会调用你的probe函数。这是驱动初始化的主战场。

static int my_usb_probe(struct usb_interface *interface,
                        const struct usb_device_id *id)
{
    struct usb_device *dev = interface_to_usbdev(interface);
    struct usb_host_interface *iface_desc;
    struct usb_endpoint_descriptor *endpoint;
    int ret;

    printk(KERN_INFO "My USB device probed\n");

    /* 1. 获取接口描述符 */
    iface_desc = interface->cur_altsetting;

    /* 2. 遍历端点,找到我们需要的 */
    for (int i = 0; i < iface_desc->desc.bNumEndpoints; i++) {
        endpoint = &iface_desc->endpoint[i].desc;

        if (usb_endpoint_is_bulk_in(endpoint)) {
            /* 找到了批量输入端点 */
            printk("Bulk IN endpoint found\n");
        }
    }

    /* 3. 分配私有数据结构 */
    /* 4. 注册字符设备或输入设备 */
    /* 5. 提交URB开始通信 */

    return 0;
}

我个人习惯,在probe里做三件事:

  • 解析端点:搞清楚设备有哪些端点,类型是什么
  • 分配资源:kmalloc、注册设备、初始化锁
  • 启动通信:提交第一个URB,开始干活
小技巧:probe函数里不要做耗时操作。如果初始化需要时间,可以考虑用工作队列延迟处理。否则用户插入设备时,系统会卡住。

4.4 disconnect回调:设备拔出时的"善后工作"

设备拔出时,内核调用disconnect。你的任务就是:把probe里分配的东西,全部还回去。

static void my_usb_disconnect(struct usb_interface *interface)
{
    struct my_usb_dev *my_dev = usb_get_intfdata(interface);

    printk(KERN_INFO "My USB device disconnected\n");

    /* 1. 停止所有URB传输 */
    usb_kill_anchored_urbs(&my_dev->urb_submitted);

    /* 2. 注销设备节点 */
    /* 3. 释放内存 */
    kfree(my_dev);

    /* 4. 清空接口私有数据 */
    usb_set_intfdata(interface, NULL);
}

我曾经犯过一个错误:在disconnect里忘了调用usb_kill_urb。结果设备拔了,URB还在飞,内核直接oops。嗯,从那以后,我每次写disconnect,第一行就是杀掉所有URB。

警告:disconnect可能在probe执行到一半时被调用!比如设备在probe过程中突然拔出。所以你的代码要能处理这种"半初始化"状态。我一般用标志位来跟踪初始化进度。

4.5 注册与注销:让内核认识你的驱动

写好了结构体,还需要在模块初始化时注册,在退出时注销。

static int __init my_usb_init(void)
{
    int ret = usb_register(&my_usb_driver);
    if (ret)
        printk(KERN_ERR "Failed to register USB driver\n");
    return ret;
}

static void __exit my_usb_exit(void)
{
    usb_deregister(&my_usb_driver);
}

module_init(my_usb_init);
module_exit(my_usb_exit);
MODULE_LICENSE("GPL");

这里有个细节:usb_register返回0表示成功。如果返回负数,别急着重试,先看看dmesg里有什么错误信息。

4.6 核心流程:一张图看懂

下面这张图,展示了从设备插入到驱动工作的完整流程。我画的时候特意把关键路径标红了。

USB设备驱动核心流程 USB设备插入 内核USB子系统枚举 匹配id_table 匹配? 调用probe回调 使用其他驱动 驱动正常工作 调用disconnect

4.7 避坑指南:我踩过的几个坑

最后,分享几个实战中容易出问题的地方:

  • probe被多次调用:同一个设备可能因为接口切换(比如从配置0切换到配置1),导致probe再次被调用。你的代码要能处理重复probe。
  • disconnect时URB还在飞:一定要先usb_kill_urb再释放资源。顺序反了,系统就崩了。
  • id_table忘记终止项:数组最后必须有一个全零的{ }。否则内核遍历时会越界。
  • 并发问题:probe和disconnect可能在不同CPU上同时执行。用锁保护共享数据,别偷懒。
我的习惯:每次写完驱动,先用modprobe加载,然后反复插拔设备10次。如果没出问题,基本就稳了。如果挂了,dmesg会告诉你答案。

好了,关于USB设备驱动框架的核心内容,就聊到这里。记住:usb_driver是骨架,probedisconnect是灵魂。把这两个回调写好,你的驱动就成功了一大半。


公众号:蓝海资料掘金营,微信deep3321