V4L2 框架核心概念
Video for Linux 2 (V4L2) 是 Linux 内核中处理视频设备的标准框架,用于统一管理摄像头、视频采集卡等设备。设计 V4L2 框架的核心目的(思想):
- 用户空间和内核空间分离:应用通过标准系统调用与内核交互
- 设备抽象:所有视频设备通过
/dev/videoX节点暴露 - 缓冲区队列管理:通过
vb2_queue实现高效的零拷贝数据传输
struct v4l2_device {
struct device *dev;
struct media_device *mdev;
struct list_head subdevs;
spinlock_t lock;
char name[36];
void (*notify)(struct v4l2_subdev *sd, unsigned int notification, void *arg);
struct v4l2_ctrl_handler *ctrl_handler;
struct v4l2_prio_state prio;
struct kref ref;
void (*release)(struct v4l2_device *v4l2_dev);
};
获取当前的格式
struct v4l2_format current_format;
memset(¤t_format, 0, sizeof(current_format));
current_format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_G_FMT, ¤t_format)
流程步骤深度解析
标准采集流程顺序: 打开设备 → 查询能力 → 设置格式 → 申请缓冲区 (REQBUFS) → 查询并映射缓冲区 (QUERYBUF + mmap) → 缓冲区入队 (QBUF) → 启动视频流 (STREAMON) → 取帧循环 (DQBUF → 处理 → QBUF) → 停止与释放。
关键点:必须先把缓冲区 QBUF 入队,再 STREAMON 开流。因为开流后硬件立即开始往队列里的缓冲区填数据,若队列为空则硬件无处可写。
打开设备文件 /dev/videoX
系统调用:
int fd = open("/dev/video0", O_RDWR);关键机制:
- 内核通过 VFS(Virtual File System) 将设备文件映射到
v4l2_fops操作集 - 设备号(主设备号 81,次设备号 X)由内核在设备注册时分配(主设备号 81 专用于 V4L2)
- 内核通过 VFS(Virtual File System) 将设备文件映射到
错误处理:
ENODEV:设备不存在EBUSY:设备被其它进程占用
查询设备能力集
struct v4l2_capability cap;
ioctl(fd, VIDIOC_QUERYCAP, &cap)
struct v4l2_capability {
__u8 driver[16];
__u8 card[32];
__u8 bus_info[32];
__u32 version;
__u32 capabilities;
__u32 device_caps;
__u32 reserved[3];
};
| driver | 驱动程序名称。 |
|---|---|
| cap.driver | 内核驱动模块名(如 uvcvideo、rkisp、msm-camera 等) |
| cap.card | 设备名称(例如:“USB Camera”、“isp-video-output”) |
| cap.bus_info | 总线信息(设备挂载在哪条总线上) |
| cap.version | v4l2驱动版本 |
| cap.capabilities | 设备能力标志(bitmask) |
| cap.device_caps | 具体 node 的能力(更细粒度) |
| reserved | 保留字段 |
常见能力位
- V4L2_CAP_VIDEO_CAPTURE
- 支持视频采集(摄像头)
- V4L2_CAP_VIDEO_OUTPUT
- 支持视频输出(显示设备)
- V4L2_CAP_STREAMING
- 支持 mmap / userptr / dmabuf
- V4L2_CAP_READWRITE
- 支持
read()/write()方式
- 支持
- V4L2_CAP_META_CAPTURE
- 支持元数据采集(ISP metadata)
if (cap.capabilities & V4L2_CAP_STREAMING) {
// 支持零拷贝流
}
设置视频格式参数
struct v4l2_format fmt = {
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
.fmt.pix = {
.width = 1920,
.height = 1080,
.pixelformat = V4L2_PIX_FMT_YUYV,
.field = V4L2_FIELD_NONE
}
};
ioctl(fd, VIDIOC_S_FMT, &fmt);
参数协商机制
- 应用提出请求格式 → 驱动调整为最接近的硬件支持格式
- 需要用
VIDIOC_G_FMT验证最终生效的参数(或回读S_FMT调用后的fmt)
常见的格式
V4L2_PIX_FMT_YUYV:YUV 4:2:2 打包格式(裸数据,体积大)V4L2_PIX_FMT_MJPEG:压缩格式(节省带宽)V4L2_PIX_FMT_NV12:Android 常用的 YUV 4:2:0 格式
分配缓冲区(VIDIOC_REQBUFS)
struct v4l2_requestbuffers req = {
.count = 4, // 请求 4 个缓冲区
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
.memory = V4L2_MEMORY_MMAP // 内存类型
};
ioctl(fd, VIDIOC_REQBUFS, &req);
| 类型 | 说明 | 使用场景 |
|---|---|---|
| V4L2_MEMORY_MMAP | 内核分配 + mmap 映射 | 高性能场景(主流选择) |
| V4L2_MEMORY_USERPTR | 用户空间分配地址 | 特殊内存管理需求 |
| V4L2_MEMORY_DMABUF | DMA 共享缓冲区 | 多进程 / 跨设备共享数据型 |
流程
- 应用请求缓冲区数量
- 内核在
vb2_queue中预分配资源 - 驱动返回实际分配到的缓冲区数量(
req.count可能被调整,应回读确认)
查询并映射缓冲区(VIDIOC_QUERYBUF + mmap)
REQBUFS 只是预分配req.count 块缓冲区,应用还需逐个查询每块缓冲区的长度和偏移量,再用 mmap 映射到用户空间才能访问数据。
for (int i = 0; i < req.count; i++) {
struct v4l2_buffer buf = {
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
.memory = V4L2_MEMORY_MMAP,
.index = i
};
ioctl(fd, VIDIOC_QUERYBUF, &buf); // 拿到 buf.length 和 buf.m.offset
buffers[i].length = buf.length;
buffers[i].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE, // 注意是 PROT_WRITE
MAP_SHARED, fd, buf.m.offset);
}
//
mmap最后一个参数buf.m.offset必须来自VIDIOC_QUERYBUF的返回,不能凭空构造。
缓冲区入队(VIDIOC_QBUF)
在开流之前,先把所有缓冲区交给内核,让硬件有地方写数据。
for (int i = 0; i < req.count; i++) {
struct v4l2_buffer buf = {
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
.memory = V4L2_MEMORY_MMAP,
.index = i
};
ioctl(fd, VIDIOC_QBUF, &buf);
}
入队(QBUF)是把缓冲区加入队列的动作,由应用完成——这一点不要和 STREAMON 混淆。
启动视频流(VIDIOC_STREAMON)
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMON, &type);
内核行为
- 激活设备硬件(如启动摄像头传感器)
- 初始化 DMA 传输通道
- 开始让已入队的缓冲区被硬件填充(缓冲区是上一步 QBUF 放进队列的,不是 STREAMON 放进去的)
缓冲区循环处理(VIDIOC_DQBUF → 处理 → VIDIOC_QBUF)
开流后进入核心循环:出队拿到一帧满缓冲区 → 处理 → 再入队循环利用。
struct v4l2_buffer buf = {
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
.memory = V4L2_MEMORY_MMAP
};
ioctl(fd, VIDIOC_DQBUF, &buf); // 出队:拿到一帧
// buffers[buf.index].start 即该帧数据,buf.bytesused 为有效长度
process_frame(buffers[buf.index].start, buf.bytesused);
ioctl(fd, VIDIOC_QBUF, &buf); // 处理完再入队
sequenceDiagram
participant App as 应用(用户空间)
participant Kernel as 内核空间(V4L2 驱动 / vb2_queue)
participant HW as 硬件(传感器 / DMA)
App->>Kernel: VIDIOC_QBUF(入队空缓冲区)
Kernel->>HW: 触发 DMA 传输
HW-->>Kernel: 填充数据完成
App->>Kernel: VIDIOC_DQBUF(请求满缓冲区)
Kernel-->>App: 返回满缓冲区
Note over App: 处理视频数据
App->>Kernel: 再次 VIDIOC_QBUF(回收缓冲区)
出队(DQBUF)要点
阻塞行为:默认会等待直到有数据就绪(设备以 O_NONBLOCK 打开时变为非阻塞,配合 select/poll 使用)
bytesused:实际填充的数据长度sequence:帧序号(用于检测丢帧)flags & V4L2_BUF_FLAG_ERROR:指示传输错误
循环设计优势
- 零拷贝:数据直接从 DMA 缓冲区映射到用户空间,无需额外拷贝
- 双缓冲机制:至少需要 2 个缓冲区保证连续采集(一个被硬件写,一个被应用读)
- 动态调整:应用可动态调整处理速度(如跳过某些帧)
停止与释放
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMOFF, &type); // 停止采集
for (int i = 0; i < req.count; i++)
munmap(buffers[i].start, buffers[i].length); // 解除映射
close(fd); // 关闭设备
核心结构体
核心组件作用
| 组件 | 功能 |
|---|---|
video_device | 设备节点注册(创建 /dev/videoX) |
v4l2_device | 设备逻辑管理(电源 / 子设备协调) |
vb2_queue | 缓冲区队列管理(核心创新点) |
v4l2_subdev | 子设备抽象(传感器 / ISP 等) |
struct v4l2_buffer
用来描述"一块视频缓冲区"的所有元信息。它的常用字段大致有:
struct v4l2_buffer {
__u32 index;//这块缓冲区在缓冲区队列里的编号(0、1、2...),对应 REQBUFS 申请到的第几块
__u32 type;//缓冲区类型,V4L2_BUF_TYPE_VIDEO_CAPTURE(采集)
__u32 bytesused;//实际有效数据长度(采集完成后填充)
__u32 flags;//状态标志位,比如 V4L2_BUF_FLAG_DONE(已完成采集)等
__u32 field;
struct timeval timestamp;//采集这一帧的时间戳
struct v4l2_timecode timecode;
__u32 sequence;
/* memory location */
/*内存管理方式:V4L2_MEMORY_MMAP(内核分配,mmap映射)、V4L2_MEMORY_USERPTR(用户自己分配)、V4L2_MEMORY_DMABUF 等*/
__u32 memory;
union {
__u32 offset;//当 memory = MMAP 时有效,表示这块缓冲区在设备内存映射空间里的偏移量,供 mmap() 使用
unsigned long userptr;//当 memory = USERPTR 时有效,指向用户自己分配的内存地址
struct v4l2_plane *planes;
__s32 fd;
} m;
__u32 length;//缓冲区长度(字节数)
__u32 reserved2;
union {
__s32 request_fd;
__u32 reserved;
};
};