第4章 V4L2框架入门:从架构到实战

V4L2,全称 Video for Linux 2,是 Linux 内核里处理视频设备的统一框架。说白了,它就是应用程序和摄像头硬件之间的翻译官。你写一个 open()、ioctl(),V4L2 帮你把命令翻译成硬件能懂的信号。

我刚开始接触嵌入式摄像头开发时,也被 V4L2 那一堆结构体和 ioctl 搞得头晕。后来摸清了它的骨架,才发现这东西设计得其实挺巧妙。今天咱们就把它的架构、核心数据结构、调用流程和设备节点一次性讲透。

4.1 V4L2 架构概述

V4L2 的架构分三层,我习惯叫它「三层蛋糕」:

  • 应用层:你的用户态程序,调用 open/ioctl/mmap 等标准接口。
  • V4L2 核心层:内核中的视频设备驱动框架,负责解析 ioctl、管理视频缓冲队列。
  • 硬件驱动层:具体芯片的驱动代码,直接操作 I2C、CSI、DMA 等硬件资源。

你想想看,如果没有 V4L2,每个摄像头芯片都得自己定义一套 API,那应用层代码就乱套了。V4L2 把差异都挡在驱动层下面,上层只用跟标准接口打交道。

核心思想:V4L2 把摄像头抽象成一个「视频设备文件」,所有操作都通过文件 I/O 完成。这是 Linux「一切皆文件」哲学的典型体现。

V4L2 三层架构 应用层(用户空间) 应用程序:open / ioctl / mmap / poll V4L2 核心层(内核空间) videodev.ko:ioctl 分发、缓冲队列管理、事件通知 硬件驱动层(内核空间) sensor 驱动 / CSI 控制器 / DMA 引擎 应用层通过标准系统调用 → V4L2 核心层解析 → 硬件驱动层执行

4.2 核心数据结构

V4L2 里有一堆结构体,但真正高频使用的就几个。我列个表,你对照着看:

结构体 用途 关键字段
struct v4l2_capability 查询设备能力 driver, card, bus_info, capabilities
struct v4l2_format 设置/获取图像格式 type, fmt.pix.width, fmt.pix.height, fmt.pix.pixelformat
struct v4l2_requestbuffers 申请视频缓冲 count, type, memory
struct v4l2_buffer 管理单个缓冲 index, bytesused, flags, timestamp, memory
struct v4l2_control 设置摄像头控制参数 id, value

我的经验:v4l2_format 里的 pixelformat 字段最容易踩坑。V4L2_PIX_FMT_YUYV 和 V4L2_PIX_FMT_UYVY 就差一个字节顺序,但图像颜色完全不对。我建议你拿到摄像头后,先用 v4l2-ctl --list-formats 确认它支持哪些格式。

4.3 ioctl 调用流程

V4L2 的核心操作都通过 ioctl 完成。我总结了一套标准流程,你照着走基本不会错:

  1. 打开设备:fd = open("/dev/video0", O_RDWR)
  2. 查询能力:ioctl(fd, VIDIOC_QUERYCAP, &cap),确认设备是视频捕获设备
  3. 设置格式:ioctl(fd, VIDIOC_S_FMT, &fmt),告诉摄像头你要什么分辨率、什么像素格式
  4. 申请缓冲:ioctl(fd, VIDIOC_REQBUFS, &req),向内核申请 3~4 个视频缓冲
  5. 查询并映射缓冲:ioctl(fd, VIDIOC_QUERYBUF, &buf) + mmap()
  6. 入队缓冲:ioctl(fd, VIDIOC_QBUF, &buf),把空缓冲放回内核队列
  7. 开始采集:ioctl(fd, VIDIOC_STREAMON, &type)
  8. 出队处理:ioctl(fd, VIDIOC_DQBUF, &buf),拿到填满数据的缓冲
  9. 处理完再入队:重复步骤 6 和 8
  10. 停止采集:ioctl(fd, VIDIOC_STREAMOFF, &type)

为什么会这样设计?说白了就是「流水线」思想。内核维护一个缓冲队列,你从队尾取数据,处理完再放回队首,循环往复。这样 CPU 和 DMA 可以并行工作,效率很高。

避坑指南:我曾经在 VIDIOC_STREAMON 之后忘记先 QBUF,结果 DQBUF 一直阻塞。后来查了内核文档才发现,启动流之前必须保证至少有一个缓冲在队列里。嗯,这个坑我替你们踩过了。

4.4 视频设备节点

V4L2 设备节点通常出现在 /dev/videoX,X 从 0 开始。但实际项目中,设备节点可能不止一个:

节点 典型用途 备注
/dev/video0 主视频捕获 最常见的摄像头节点
/dev/video1 第二个摄像头或 MIPI CSI 虚拟通道 多摄像头系统常见
/dev/media0 Media Controller 设备 用于复杂 pipeline 拓扑管理
/dev/v4l-subdev0 V4L2 子设备 sensor 或 ISP 的单独控制节点

我个人的习惯是,拿到一块新开发板后,先执行 ls /dev/video* 看看有哪些节点。然后用 v4l2-ctl -d /dev/video0 --all 查看设备信息。这一步能帮你快速确认驱动是否加载成功。

关键点:在嵌入式平台上,/dev/video0 不一定对应物理上的第一个摄像头。设备节点的编号由驱动注册顺序决定,跟硬件位置没有固定关系。如果你的板子有多个摄像头接口,建议通过 udev 规则绑定固定的 symlink,比如 /dev/camera_front、/dev/camera_rear。

V4L2 ioctl 标准调用流程 open("/dev/video0") VIDIOC_QUERYCAP VIDIOC_S_FMT VIDIOC_REQBUFS mmap() 映射缓冲 VIDIOC_STREAMON 之后循环执行 QBUF → DQBUF → 处理数据 → QBUF,直到 VIDIOC_STREAMOFF

4.5 实战:一个最简单的 V4L2 采集程序骨架

光说不练假把式。下面这个代码片段,是我在实际项目中提炼出来的最小可用框架。你把它跑通,就算入门了:

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>

int main() {
    int fd;
    struct v4l2_capability cap;
    struct v4l2_format fmt;

    // 1. 打开设备
    fd = open("/dev/video0", O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    // 2. 查询能力
    ioctl(fd, VIDIOC_QUERYCAP, &cap);
    printf("Driver: %s\n", cap.driver);
    printf("Card: %s\n", cap.card);

    // 3. 设置格式:640x480, YUYV
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = 640;
    fmt.fmt.pix.height = 480;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
    fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
    ioctl(fd, VIDIOC_S_FMT, &fmt);

    printf("Set format: %dx%d\n", fmt.fmt.pix.width, fmt.fmt.pix.height);

    close(fd);
    return 0;
}

小提示:这段代码只做了格式设置,没有涉及缓冲和采集。但它是所有 V4L2 程序的起点。我建议你编译运行一下,看看你的摄像头驱动打印了什么信息。如果 VIDIOC_QUERYCAP 返回失败,八成是设备节点选错了,或者驱动没加载。