3、摄像头驱动开发入门:编写第一个虚拟摄像头驱动、注册与注销、实现基本的open/close/ioctl
好,咱们正式开始动手写代码了。
前面两章聊了不少概念,什么V4L2框架、video_device结构体,你可能觉得有点虚。说实话,我当年刚接触摄像头驱动时,也是看了半天文档,脑子里还是一团浆糊。直到我亲手写了一个虚拟摄像头驱动,把那些结构体一个个填进去,才真正明白它们是怎么串起来的。
这一章,我们就来干这件事。写一个最简单的虚拟摄像头驱动,不涉及硬件,只涉及内存里的数据。你想想看,这其实是最纯粹的驱动开发体验——没有硬件bug干扰,没有时序问题,只有代码和内核的交互。
3.1 虚拟摄像头驱动的设计思路
虚拟摄像头,说白了就是模拟一个真实的摄像头设备。它不连接任何物理硬件,但会在/dev目录下生成一个video节点。应用程序打开它,就像打开一个真实的摄像头一样。
我个人的习惯是,在写任何驱动之前,先想清楚三个问题:
- 设备文件叫什么? 比如 /dev/video0
- 支持哪些操作? open、close、ioctl,这是最基本的
- 数据从哪里来? 虚拟摄像头嘛,数据就是内存里的一块缓冲区,我们可以填充一些彩条或者纯色
嗯,这里要注意,我们这一章只实现open、close和ioctl。ioctl里只处理一个命令——查询设备能力。至于数据流(read、mmap、poll),那是后面章节的事。
3.2 驱动框架:注册与注销
每个Linux驱动都有一个入口和出口。入口是module_init,出口是module_exit。这是内核的规矩,谁也绕不开。
我在项目中遇到过不少新手,一上来就写复杂的逻辑,结果连驱动都加载不进去。所以我建议,先搭一个空壳子,确保能注册成功,再往里填功能。
来看代码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/videodev2.h>
#include <media/v4l2-device.h>
#include <media/v4l2-dev.h>
#include <media/v4l2-fh.h>
#include <media/v4l2-ioctl.h>
#define VIRTUAL_CAMERA_NAME "virtual_camera"
static struct video_device *vdev;
static struct v4l2_device v4l2_dev;
/* 文件操作函数声明 */
static int virtual_camera_open(struct file *filp);
static int virtual_camera_close(struct file *filp);
static long virtual_camera_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);
/* 文件操作结构体 */
static const struct v4l2_file_operations virtual_camera_fops = {
.owner = THIS_MODULE,
.open = virtual_camera_open,
.release = virtual_camera_close,
.unlocked_ioctl = virtual_camera_ioctl,
};
static int __init virtual_camera_init(void)
{
int ret;
pr_info("Virtual camera driver initializing...\n");
/* 1. 注册v4l2_device */
ret = v4l2_device_register(NULL, &v4l2_dev);
if (ret < 0) {
pr_err("Failed to register v4l2 device\n");
return ret;
}
/* 2. 分配video_device结构体 */
vdev = video_device_alloc();
if (!vdev) {
pr_err("Failed to allocate video device\n");
v4l2_device_unregister(&v4l2_dev);
return -ENOMEM;
}
/* 3. 初始化video_device */
vdev->v4l2_dev = &v4l2_dev;
vdev->fops = &virtual_camera_fops;
vdev->release = video_device_release;
strscpy(vdev->name, VIRTUAL_CAMERA_NAME, sizeof(vdev->name));
/* 4. 注册video_device */
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret < 0) {
pr_err("Failed to register video device\n");
video_device_release(vdev);
v4l2_device_unregister(&v4l2_dev);
return ret;
}
pr_info("Virtual camera registered as /dev/video%d\n", vdev->num);
return 0;
}
static void __exit virtual_camera_exit(void)
{
pr_info("Virtual camera driver exiting...\n");
/* 注销顺序与注册顺序相反 */
video_unregister_device(vdev);
v4l2_device_unregister(&v4l2_dev);
pr_info("Virtual camera driver unregistered\n");
}
module_init(virtual_camera_init);
module_exit(virtual_camera_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple virtual camera driver");
关键点解析:
v4l2_device_register是V4L2框架的根设备,所有子设备都挂在这个下面video_device_alloc和video_register_device是两步走,先分配再注册- 注销时顺序要反过来,先注销video_device,再注销v4l2_device
我的经验: 注册时第三个参数传-1,表示让内核自动分配设备号。如果你有特殊需求(比如固定为video5),可以传一个正数。但建议用-1,避免冲突。
3.3 实现open和close
open和close是驱动里最简单的两个函数。它们主要做两件事:权限检查和资源初始化/清理。
对于虚拟摄像头,open时我们只需要增加一个使用计数,close时减少计数。但为了演示V4L2框架的用法,我习惯用v4l2_fh_open和v4l2_fh_release这两个辅助函数。
static int virtual_camera_open(struct file *filp)
{
struct video_device *vdev = video_devdata(filp);
struct v4l2_fh *fh;
pr_info("Virtual camera opened\n");
/* 使用V4L2框架的文件句柄管理 */
fh = kzalloc(sizeof(*fh), GFP_KERNEL);
if (!fh)
return -ENOMEM;
v4l2_fh_init(fh, vdev);
v4l2_fh_add(fh);
/* 将文件句柄关联到file结构体 */
filp->private_data = fh;
return 0;
}
static int virtual_camera_close(struct file *filp)
{
struct v4l2_fh *fh = filp->private_data;
pr_info("Virtual camera closed\n");
if (fh) {
v4l2_fh_del(fh);
v4l2_fh_exit(fh);
kfree(fh);
}
return 0;
}
注意: 我曾经犯过一个错误——在close里忘了调用v4l2_fh_del,结果导致文件句柄泄漏。内核虽然不会崩溃,但长时间运行后,内存会慢慢被吃掉。所以,open里分配了什么,close里一定要释放干净。
3.4 实现ioctl:查询设备能力
ioctl是驱动里最复杂的部分,因为它要处理各种命令。对于摄像头驱动,最基础的ioctl命令是VIDIOC_QUERYCAP,用来查询设备的能力。
说白了,就是应用程序问驱动:「你是什么设备?能干什么?」驱动回答:「我是视频采集设备,支持流式I/O。」
static long virtual_camera_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct v4l2_fh *fh = filp->private_data;
struct video_device *vdev = fh->vdev;
void __user *argp = (void __user *)arg;
switch (cmd) {
case VIDIOC_QUERYCAP:
{
struct v4l2_capability cap = {0};
strscpy(cap.driver, "virtual_cam", sizeof(cap.driver));
strscpy(cap.card, "Virtual Camera", sizeof(cap.card));
strscpy(cap.bus_info, "platform:virtual_camera", sizeof(cap.bus_info));
cap.version = KERNEL_VERSION(5, 10, 0);
cap.capabilities = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
if (copy_to_user(argp, &cap, sizeof(cap)))
return -EFAULT;
pr_info("VIDIOC_QUERYCAP called\n");
return 0;
}
default:
pr_warn("Unsupported ioctl command: 0x%x\n", cmd);
return -ENOTTY;
}
}
关于capabilities字段:
V4L2_CAP_VIDEO_CAPTURE表示这是一个视频捕获设备V4L2_CAP_STREAMING表示支持流式I/O(后面章节会实现)- 如果你只做最基本的测试,可以只声明VIDEO_CAPTURE
你可能会问:为什么用copy_to_user而不是直接赋值?因为argp指向的是用户空间的内存,内核不能直接访问。这是内核编程的基本规则——用户空间和内核空间是隔离的。
3.5 编译与测试
驱动写好了,怎么验证它能不能跑?我一般分三步走:
- 编译: 把代码放到内核源码树的drivers/media/test-drivers目录下,或者用外部模块编译
- 加载: insmod virtual_camera.ko,看dmesg有没有打印
- 测试: 用v4l2-ctl工具查询设备信息
测试命令如下:
# 加载驱动
sudo insmod virtual_camera.ko
# 查看设备节点
ls -l /dev/video*
# 查询设备能力
v4l2-ctl -d /dev/video0 --all
# 卸载驱动
sudo rmmod virtual_camera
如果一切正常,v4l2-ctl会输出类似这样的信息:
Driver Info:
Driver name : virtual_cam
Card type : Virtual Camera
Bus info : platform:virtual_camera
Driver version : 5.10.0
Capabilities : 0x84200001
Video Capture
Streaming
避坑指南: 我曾经在测试时发现v4l2-ctl报错「Device or resource busy」。排查了半天,发现是之前加载的驱动没有卸载干净。所以建议每次测试前,先执行一次rmmod,确保设备没有被占用。
3.6 本章小结
这一章我们完成了三件事:
- 搭建了虚拟摄像头驱动的框架,实现了注册和注销
- 实现了open和close,用V4L2框架管理文件句柄
- 实现了VIDIOC_QUERYCAP,让应用程序能识别我们的设备
说实话,这个驱动还很简陋。它不能产生任何图像数据,也不能设置格式。但它的意义在于——你亲手写了一个能跑起来的摄像头驱动。从0到1这一步,比什么都重要。
下一章,我们会给这个驱动加上格式设置和帧缓冲,让它真正能「产生」图像数据。到时候,你就可以用ffplay或者gstreamer来预览虚拟摄像头输出的画面了。
课后思考:
- 如果同时打开多个应用程序访问同一个video节点,open函数会怎么处理?
- 为什么ioctl里要用copy_to_user,而不是直接memcpy?
- 尝试修改capabilities字段,去掉STREAMING标志,看看v4l2-ctl的输出有什么变化
嗯,先到这里。动手试试吧,代码写出来跑通了,比看十遍文档都管用。