2. 嵌入式Linux摄像头驱动框架:V4L2架构解析

好,咱们直接进入正题。V4L2,全称 Video for Linux 2,是 Linux 内核里处理视频设备的统一框架。说白了,它就是摄像头和应用程序之间的“翻译官”。你想想看,市面上那么多摄像头芯片,OV、索尼、安森美,每家寄存器都不一样。如果没有 V4L2,每个应用都得自己写驱动,那不乱套了?

我个人习惯把 V4L2 理解成三层结构:应用层、内核框架层、硬件驱动层。应用层调用 open/ioctl/mmap,内核层负责调度,驱动层直接操作寄存器。嗯,这个分层设计,其实和网络协议栈的思路很像。

2.1 Video设备节点注册

摄像头在用户空间长什么样?就是一个文件:/dev/video0。你打开它,就像打开一个普通文件一样。但背后的事情,远没那么简单。

驱动注册设备节点时,核心调用是 video_register_device()。我截取一段典型的初始化代码,你感受一下:

static struct video_device vdev = {
    .name = "MyCamera",
    .fops = &my_fops,
    .ioctl_ops = &my_ioctl_ops,
    .release = my_release,
};

static int my_probe(struct platform_device *pdev)
{
    struct video_device *vdev;
    int ret;

    vdev = video_device_alloc();
    if (!vdev)
        return -ENOMEM;

    strscpy(vdev->name, "MyCamera", sizeof(vdev->name));
    vdev->fops = &my_fops;
    vdev->ioctl_ops = &my_ioctl_ops;
    vdev->release = video_device_release;

    ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
    if (ret < 0) {
        video_device_release(vdev);
        return ret;
    }

    platform_set_drvdata(pdev, vdev);
    return 0;
}

这里有个坑,我当年踩过。注册时第三个参数 VFL_TYPE_VIDEO 决定了设备节点类型。如果你注册成 VFL_TYPE_VBI,应用层用标准 V4L2 接口是打不开的。我曾经在调试一个老款 sensor 时,发现 /dev/video0 死活打不开,查了半天才发现是类型写错了。

注意: 设备节点注册成功后,内核会自动创建设备文件。但如果你用的是 devtmpfs,记得确保内核配置了 CONFIG_DEVTMPFS_MOUNT。否则你只能看到 /dev/video0 文件,但无法 open。

2.2 ioctl控制流程

ioctl 是 V4L2 的灵魂。应用层通过它来查询能力、设置格式、请求缓冲区。整个流程,我习惯把它分成三个阶段:查询、配置、流控

先看查询阶段。应用调用 VIDIOC_QUERYCAP,驱动返回设备能力。比如是否支持视频捕获、是否支持流 I/O。代码长这样:

static int my_vidioc_querycap(struct file *file, void *priv,
                               struct v4l2_capability *cap)
{
    strscpy(cap->driver, "my_camera", sizeof(cap->driver));
    strscpy(cap->card, "My USB Camera", sizeof(cap->card));
    strscpy(cap->bus_info, "usb-0000:00:14.0-1", sizeof(cap->bus_info));
    cap->capabilities = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
    return 0;
}

然后是配置阶段。应用设置图像格式、帧率、裁剪区域。这里最常用的是 VIDIOC_S_FMT。我建议你在驱动里做一次格式校验,别完全相信应用传进来的参数。比如像素格式,有些应用会传 V4L2_PIX_FMT_YUYV,但你的 sensor 只支持 V4L2_PIX_FMT_MJPEG。这时候驱动要主动拒绝,或者做格式转换。

最后是流控阶段。应用调用 VIDIOC_STREAMON 开始采集,VIDIOC_STREAMOFF 停止。这里有个细节:STREAMON 之后,驱动要立刻启动 DMA 传输。如果硬件有延迟,记得用 workqueue 异步启动,别在 ioctl 上下文里等太久。

小技巧: 调试 ioctl 时,可以用 strace 抓应用的系统调用。比如 strace -e ioctl -f ./camera_app。我曾经靠这招抓到一个应用反复调用 VIDIOC_QBUF 但不调用 VIDIOC_DQBUF 的 bug。

2.3 内存映射(mmap)机制

摄像头数据量有多大?以 1080p@30fps 的 YUYV 格式为例,每秒数据量是 1920×1080×2×30 ≈ 124MB。如果用 read/write 逐帧拷贝,CPU 占用率直接拉满。所以 V4L2 提供了 mmap 机制,让应用和驱动共享内存。

mmap 的核心是 VIDIOC_REQBUFSVIDIOC_QUERYBUF。应用先请求缓冲区,然后查询每个缓冲区的物理地址,最后通过 mmap 映射到用户空间。驱动端要实现的回调是 vidioc_reqbufsvidioc_querybuf

我画个流程图,帮你理清关系:

步骤 应用调用 驱动行为
1 VIDIOC_REQBUFS 分配 DMA 缓冲区,记录物理地址
2 VIDIOC_QUERYBUF 返回缓冲区信息(offset、length)
3 mmap 将 DMA 缓冲区映射到用户空间
4 VIDIOC_QBUF 将缓冲区加入硬件采集队列
5 VIDIOC_DQBUF 从完成队列取出已采集的缓冲区

驱动里实现 mmap 时,核心是 vb2_mmap 或者自己实现 mmap 回调。我建议用内核的 videobuf2 框架,它帮你处理了大部分脏活累活。比如缓冲区队列管理、DMA 一致性、缓存同步。你只需要实现 queue_setupbuf_preparebuf_queue 这几个回调。

static int my_queue_setup(struct vb2_queue *q,
                           unsigned int *nbuffers,
                           unsigned int *nplanes,
                           unsigned int sizes[],
                           struct device *alloc_devs[])
{
    sizes[0] = MY_CAMERA_WIDTH * MY_CAMERA_HEIGHT * 2;
    *nplanes = 1;
    return 0;
}

static int my_buf_prepare(struct vb2_buffer *vb)
{
    // 检查缓冲区大小是否足够
    if (vb2_plane_size(vb, 0) < MY_CAMERA_WIDTH * MY_CAMERA_HEIGHT * 2)
        return -EINVAL;
    return 0;
}

static void my_buf_queue(struct vb2_buffer *vb)
{
    struct my_device *dev = vb2_get_drv_priv(vb->vb2_queue);
    // 将缓冲区加入硬件 DMA 链表
    list_add_tail(&vb->queued_entry, &dev->dma_list);
}
性能关键点: mmap 模式下,缓冲区是驱动和硬件共享的。如果 CPU 和 DMA 控制器访问同一块内存,记得处理缓存一致性问题。ARM 平台上,用 dma_alloc_coherent 分配的内存会自动处理。但如果你用 kmalloc 加手动映射,就得自己调用 dma_map_singledma_sync_single_for_device

嗯,这里还要提一个我踩过的坑。有一次我在 i.MX6 平台上调试摄像头,发现 mmap 后应用读到的图像是花屏的。查了两天,最后发现是 DMA 地址没有对齐到 cache line。ARM 的 cache line 是 64 字节,如果 DMA 缓冲区起始地址不是 64 的倍数,CPU 读到的数据就是错的。解决方案很简单:分配缓冲区时用 dma_alloc_coherent,它保证地址对齐。

最后总结一下。V4L2 框架的核心就是这三件事:设备节点让应用能找到硬件,ioctl 让应用能控制硬件,mmap 让数据能高效传输。你把这三点吃透了,嵌入式摄像头驱动这块就算入门了。下一章,我会讲怎么用 videobuf2 框架做缓冲区管理,那是性能优化的重头戏。