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_allocvideo_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 编译与测试

驱动写好了,怎么验证它能不能跑?我一般分三步走:

  1. 编译: 把代码放到内核源码树的drivers/media/test-drivers目录下,或者用外部模块编译
  2. 加载: insmod virtual_camera.ko,看dmesg有没有打印
  3. 测试: 用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的输出有什么变化

嗯,先到这里。动手试试吧,代码写出来跑通了,比看十遍文档都管用。