4. V4L2设备注册与注销:video_register_device、video_unregister_device、设备节点创建
好,咱们继续往下走。上一章我们把V4L2的设备结构体搭好了,这一章要解决一个核心问题:怎么让这个设备在系统里“活”起来?说白了,就是怎么注册它,怎么让用户空间能看到它,以及怎么优雅地把它卸掉。
我个人习惯把设备注册比作“上户口”。你结构体定义得再好,不往内核的名单里登记,系统就不认你。而设备节点,就是用户空间访问这个户口的“门牌号”。
4.1 核心API:video_register_device
这个函数,是咱们驱动注册的入口。它的原型长这样:
int video_register_device(struct video_device *vdev,
int type,
int nr);
三个参数,我一个个说清楚。
- vdev:你上一章辛辛苦苦初始化好的video_device结构体指针。注意,它里面的v4l2_dev、queue、fops这些成员必须已经赋值好了,不然注册会失败。
- type:设备类型。对于Camera来说,绝大多数情况是
VFL_TYPE_VIDEO。这决定了你的设备节点是/dev/videoX还是/dev/vbiX之类的。 - nr:设备编号。你可以指定一个固定的数字,比如传0,那节点就是
/dev/video0。如果传-1,内核会自动分配一个空闲的编号。
嗯,这里要注意:千万别传一个已经被占用的编号。我在项目中遇到过,有人图省事直接写死nr = 0,结果板子上已经有别的摄像头占了video0,注册直接返回-EBUSY。后来我改成传-1,让内核去管,世界清净了。
4.2 注册背后的那些事
你调用video_register_device时,内核到底干了啥?我简单梳理一下:
- 检查vdev的合法性,比如fops不能为空,v4l2_dev不能为空。
- 根据type和nr,在
/dev下创建对应的设备节点。比如type是VFL_TYPE_VIDEO,nr是2,那节点就是/dev/video2。 - 把这个设备挂到V4L2的设备链表里,这样用户空间通过
open("/dev/video2")就能找到你的驱动。 - 初始化一些内核内部的锁和等待队列。
你想想看,这一步其实挺重的。所以注册失败时,一定要检查返回值。常见的错误码有:
| 错误码 | 含义 | 常见原因 |
|---|---|---|
| -ENOMEM | 内存不足 | 内核堆空间不够,或者你分配的video_device太大 |
| -EBUSY | 设备号冲突 | 你指定的nr已经被占用了 |
| -EINVAL | 参数无效 | type传错了,或者vdev里有空指针 |
4.3 设备节点创建的细节
设备节点,就是/dev/videoX这个文件。它的创建过程,其实是内核的device_create机制在背后工作。V4L2框架封装了一层,让你不用直接跟class_create打交道。
但有个细节很多人会忽略:设备节点的权限和所属组。默认情况下,/dev/videoX的权限是0600,属主是root。这意味着普通用户打不开你的摄像头。
我一般会在驱动里加一个udev规则,或者在内核配置里打开CONFIG_VIDEO_FIXED_MINOR_RANGES,然后手动调整权限。不过更常见的做法是,在应用层通过chmod或者udev规则来解决。
举个例子,如果你的系统用udev,可以写个规则文件:
SUBSYSTEM=="video4linux", GROUP="video", MODE="0660"
这样所有/dev/videoX节点都属于video组,权限是0660。把用户加到video组里,就能用了。
4.4 注销:video_unregister_device
有注册就有注销。这个函数相对简单:
void video_unregister_device(struct video_device *vdev);
它做的事情和注册相反:
- 从V4L2设备链表中移除该设备。
- 删除
/dev/videoX节点。 - 唤醒所有正在等待这个设备的进程(比如正在阻塞的poll)。
- 标记设备为“已注销”状态。
这里有个关键点:注销后,video_device结构体并不会被释放。它只是从系统里“下线”了。如果你用的是动态分配的video_device,记得在注销后调用video_device_release来释放内存。
-ENODEV。
4.5 一个完整的注册与注销示例
说了这么多,不如直接看代码。这是我常用的模板:
static int mycam_probe(struct platform_device *pdev)
{
struct video_device *vdev;
int ret;
// 分配video_device
vdev = video_device_alloc();
if (!vdev)
return -ENOMEM;
// 初始化...(省略fops、queue等设置)
// 注册
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret) {
video_device_release(vdev);
return ret;
}
platform_set_drvdata(pdev, vdev);
return 0;
}
static int mycam_remove(struct platform_device *pdev)
{
struct video_device *vdev = platform_get_drvdata(pdev);
// 注销
video_unregister_device(vdev);
// 释放内存
video_device_release(vdev);
return 0;
}
你看,结构很清晰。probe里分配、初始化、注册;remove里注销、释放。顺序不能乱。
我个人习惯在注册成功后,打印一条日志:
dev_info(&pdev->dev, "Registered %s\n", video_device_node_name(vdev));
这样调试时一眼就能看到设备节点是哪个,方便。
4.6 避坑指南
最后,分享几个我实际项目中遇到的坑:
- 重复注册:别在probe里调用两次
video_register_device。内核会报错,而且第二次会失败。 - 注销顺序:如果驱动里有中断处理程序或者workqueue,一定要在注销设备之前把它们停掉。否则注销后中断来了,访问已经释放的video_device,直接内核panic。
- 设备节点残留:如果驱动异常卸载,
/dev/videoX节点可能没被清理。重启系统或者手动rmmod再insmod一般能解决。但最好在驱动里做好错误处理。
嗯,这一章的内容就这些。设备注册与注销,说白了就是一对“创建”和“销毁”的操作。但细节决定成败,尤其是资源释放的顺序,搞不好就是内核崩溃。下一章我们聊聊更刺激的——buffer管理,那才是Camera驱动的核心战场。