第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「一切皆文件」哲学的典型体现。
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 完成。我总结了一套标准流程,你照着走基本不会错:
- 打开设备:
fd = open("/dev/video0", O_RDWR) - 查询能力:
ioctl(fd, VIDIOC_QUERYCAP, &cap),确认设备是视频捕获设备 - 设置格式:
ioctl(fd, VIDIOC_S_FMT, &fmt),告诉摄像头你要什么分辨率、什么像素格式 - 申请缓冲:
ioctl(fd, VIDIOC_REQBUFS, &req),向内核申请 3~4 个视频缓冲 - 查询并映射缓冲:
ioctl(fd, VIDIOC_QUERYBUF, &buf)+mmap() - 入队缓冲:
ioctl(fd, VIDIOC_QBUF, &buf),把空缓冲放回内核队列 - 开始采集:
ioctl(fd, VIDIOC_STREAMON, &type) - 出队处理:
ioctl(fd, VIDIOC_DQBUF, &buf),拿到填满数据的缓冲 - 处理完再入队:重复步骤 6 和 8
- 停止采集:
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。
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 返回失败,八成是设备节点选错了,或者驱动没加载。