3. V4L2设备注册与注销:video_register_device、video_unregister_device的调用流程与注意事项
好,咱们今天聊聊V4L2设备注册和注销。说实话,这块儿看起来就是两个函数调用,但坑是真不少。我早期做摄像头驱动的时候,就因为在注册时机上栽过跟头——设备节点创建了,但硬件还没准备好,用户空间一打开就崩。嗯,咱们今天就把这两个函数彻底讲透。
3.1 video_device结构体:你得先把它填对
注册之前,你得先有一个struct video_device。这个结构体是V4L2框架的核心,说白了就是用户空间看到的那个/dev/videoX节点背后的数据结构。
struct video_device {
const struct v4l2_file_operations *fops;
struct v4l2_ioctl_ops *ioctl_ops;
struct device dev;
struct v4l2_device *v4l2_dev;
unsigned long flags;
int minor;
int index;
// ... 还有不少字段
};
我个人习惯,分配这个结构体用video_device_alloc(),而不是直接kzalloc。为什么?因为video_device_alloc会帮你初始化引用计数和锁,省得你后面手动搞。当然,如果你用devm_系列接口,也可以直接用devm_kzalloc,但记得手动调用video_device_release回调。
关键字段设置:
v4l2_dev:指向父V4L2设备,必须设置,否则注册会失败fops:文件操作集,至少要实现open、release、ioctlioctl_ops:V4L2 ioctl操作集,这是核心flags:常用V4L2_FL_REGISTERED,表示设备已注册minor:一般设为-1,让内核自动分配
3.2 video_register_device:调用流程与时机
函数原型长这样:
int video_register_device(struct video_device *vdev, int type, int nr);
三个参数:
vdev:你填好的video_device指针type:设备类型,比如VFL_TYPE_VIDEO、VFL_TYPE_VBI、VFL_TYPE_SUBDEVnr:设备编号,传-1让内核自动分配,传0-63可以指定
调用流程大致如下:
- 检查vdev和v4l2_dev是否有效
- 分配minor号(如果nr为-1)
- 创建
/dev/videoX设备节点 - 初始化cdev和device结构
- 设置
V4L2_FL_REGISTERED标志 - 调用
v4l2_device_register_subdev_nodes(如果有子设备)
这里有个我踩过的坑:注册时机。你想想看,video_register_device一调用,用户空间就能打开这个设备了。如果你的硬件还没初始化完成,比如MIPI CSI的PHY还没起来,那用户空间一调用open或者ioctl,你的驱动可能就崩了。
我曾经犯过的错:在probe函数里先注册了video_device,再去初始化硬件。结果用户空间的应用程序在udev事件触发后立刻打开了设备,而此时我的MIPI DPHY还没准备好。从那以后,我都是先把硬件初始化完,最后一步才调用video_register_device。
3.3 注销流程:video_unregister_device
注销相对简单,但同样有讲究:
void video_unregister_device(struct video_device *vdev);
它会做这几件事:
- 清除
V4L2_FL_REGISTERED标志 - 删除设备节点
- 等待所有打开的文件描述符关闭
- 释放minor号
注意:这个函数不会释放video_device结构体本身。你需要自己调用video_device_release或者video_device_put来释放内存。我见过有人只调了video_unregister_device,忘了释放内存,结果每次insmod/rmmod都泄漏一次。
我的建议:在remove函数里,先调用video_unregister_device,然后调用video_device_put。如果你用了devm_系列接口,可以注册一个devres回调来自动处理。
3.4 完整示例:注册与注销的典型流程
static int my_probe(struct platform_device *pdev)
{
struct video_device *vdev;
int ret;
// 1. 分配video_device
vdev = video_device_alloc();
if (!vdev)
return -ENOMEM;
// 2. 设置字段
vdev->v4l2_dev = &my_v4l2_dev;
vdev->fops = &my_fops;
vdev->ioctl_ops = &my_ioctl_ops;
vdev->release = video_device_release;
vdev->minor = -1; // 自动分配
snprintf(vdev->name, sizeof(vdev->name), "my_camera");
// 3. 初始化硬件(先做!)
ret = my_hardware_init(pdev);
if (ret) {
video_device_release(vdev);
return ret;
}
// 4. 注册video设备(最后一步)
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret) {
my_hardware_deinit(pdev);
video_device_release(vdev);
return ret;
}
platform_set_drvdata(pdev, vdev);
return 0;
}
static int my_remove(struct platform_device *pdev)
{
struct video_device *vdev = platform_get_drvdata(pdev);
// 先注销,再释放硬件
video_unregister_device(vdev);
my_hardware_deinit(pdev);
// video_device_release 由 release 回调自动处理
return 0;
}
3.5 注意事项与避坑指南
| 注意事项 | 说明 |
|---|---|
| 注册前硬件就绪 | 确保MIPI CSI PHY、时钟、电源等都已初始化完成 |
| 注销顺序 | 先注销video_device,再释放硬件资源 |
| release回调 | 必须设置release回调,否则内核会WARN |
| 并发访问 | 注册后用户空间可能立即访问,驱动要能处理这种情况 |
| 设备编号冲突 | 指定nr时确保不与其他设备冲突,建议用-1自动分配 |
核心原则:注册是最后一步,注销是第一步。这个顺序能避免绝大多数竞态问题。
3.6 核心流程图
下面我用SVG画了一张图,把注册和注销的完整流程展示出来。你一看就明白了:
这张图把注册和注销的流程分左右两边展示。你看,注册是从上到下,硬件初始化在前,注册在后。注销也是从上到下,先注销设备,再释放硬件。这个顺序千万别搞反了。
一个小技巧:如果你在调试时发现video_register_device返回-ENOMEM,别急着怀疑内存不够。先检查一下你的v4l2_dev指针是不是NULL,或者minor号是不是被占用了。我遇到过好几次,都是因为忘了设置v4l2_dev。
好了,关于V4L2设备注册与注销,核心就是这些。记住:注册是最后一步,注销是第一步。这个原则能帮你避免90%的坑。剩下的10%,等你真正写代码的时候,咱们再慢慢聊。
可可精品资料库