4. USB设备驱动框架:usb_driver结构体、probe与disconnect回调函数
好,咱们今天聊聊USB设备驱动的核心骨架。说白了,就是那个usb_driver结构体,以及它里面最重要的两个回调函数——probe和disconnect。
我刚开始写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 | 电源管理回调 | 可选 |
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,开始干活
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。
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 核心流程:一张图看懂
下面这张图,展示了从设备插入到驱动工作的完整流程。我画的时候特意把关键路径标红了。
4.7 避坑指南:我踩过的几个坑
最后,分享几个实战中容易出问题的地方:
- probe被多次调用:同一个设备可能因为接口切换(比如从配置0切换到配置1),导致probe再次被调用。你的代码要能处理重复probe。
- disconnect时URB还在飞:一定要先
usb_kill_urb再释放资源。顺序反了,系统就崩了。 - id_table忘记终止项:数组最后必须有一个全零的
{ }。否则内核遍历时会越界。 - 并发问题:probe和disconnect可能在不同CPU上同时执行。用锁保护共享数据,别偷懒。
modprobe加载,然后反复插拔设备10次。如果没出问题,基本就稳了。如果挂了,dmesg会告诉你答案。
好了,关于USB设备驱动框架的核心内容,就聊到这里。记住:usb_driver是骨架,probe和disconnect是灵魂。把这两个回调写好,你的驱动就成功了一大半。
公众号:蓝海资料掘金营,微信deep3321