1. V4L2 (Video4Linux2)
1.1 简介
V4L2 (Video4Linux2) 是 Linux 内核提供的标准视频设备接口,作为访问摄像头最底层的原生方式。该接口通过设备文件(如 /dev/video0、/dev/video1)与内核驱动通信,实现对视频设备的全面控制。

1.1.1 发展历史
V4L2的发展历程展现了Linux视频子系统的逐步完善:
1998年 - V4L问世:Bill Dirks开发了最初的Video4Linux(V4L),为Linux系统提供了基础视频设备支持功能
2002年 - V4L2推出:为克服V4L的局限性,Video4Linux2(V4L2)被引入Linux 2.5内核,带来更强大灵活的API
2003年 - 正式纳入内核:V4L2成为Linux 2.6内核的标准视频接口
持续优化:伴随硬件技术进步,V4L2持续扩展功能,支持更多设备类型(如摄像头、TV调谐器、SDR等)和先进特性(包括多平面格式、M2M设备等)
1.1.2 技术定位
V4L2在Linux视频生态系统中的核心定位体现在以下三个方面:
1. 内核层接口
- 作为Linux内核视频子系统的核心接口
- 运行于内核空间,直接与硬件驱动进行交互
- 通过统一的设备抽象层屏蔽底层硬件差异
2. 底层基础架构
- 是Linux系统访问视频设备的最低层标准接口
- 为OpenCV、GStreamer、FFmpeg等高级库提供底层支持
- 向上层应用提供稳定的硬件抽象服务
3. 标准化规范
- 制定统一的API标准,确保各类硬件使用相同接口
- 兼容多种视频设备:包括USB摄像头、MIPI CSI摄像头、TV调谐器和SDR设备等
- 具备跨平台特性,所有主流Linux发行版均原生支持
4.总体架构
V4L2 驱动框架分为三层,从上到下依次为:
cpp
┌─────────────────────────────────────────┐
│ 用户空间应用程序 │
│ (打开 /dev/videoX, ioctl, read/write) │
└─────────────────────────────────────────┘
↕ (系统调用)
┌─────────────────────────────────────────┐
│ V4L2 核心层 │
│ - 设备节点管理 │
│ - ioctl 派发 │
│ - 缓冲区管理(videobuf2) │
│ - 媒体控制器辅助 │
└─────────────────────────────────────────┘
↕ (回调 / 注册)
┌─────────────────────────────────────────┐
│ V4L2 设备驱动层 │
│ - 实现具体的硬件操作 │
│ - 注册 video_device 结构体 │
│ - 提供 v4l2_ioctl_ops / v4l2_file_ops │
│ - 使用 vb2_queue 管理视频缓冲 │
└─────────────────────────────────────────┘
↕ (硬件操作)
┌─────────────────────────────────────────┐
│ 硬件设备 │
│ (摄像头、HDMI采集卡、CODEC等) │
└─────────────────────────────────────────┘
1.2 原理
1.2.1 Linux架构原理
Linux内核架构概述:

Linux 内核架构说明:
1.用户空间(User Space)
- 应用程序:用户态运行的程序
- 系统库:封装系统调用的库函数(如 libc、libpthread)
- 特点:受限访问,必须通过系统调用进入内核
2.内核空间(Kernel Space) a) 系统调用接口层
- 功能:用户/内核空间交互桥梁
- 特点:触发 CPU 特权级切换
- 接口:提供标准系统调用(open/read/write/ioctl)
b) 虚拟文件系统层(VFS)
- 作用:统一文件操作接口
- 功能:路由操作到具体文件系统
- 支持:ext4、procfs、sysfs 等文件系统
c) 内核核心子系统
- 进程管理:调度与同步
- 内存管理:虚拟内存与分配
- 中断处理:硬件中断管理
- 定时器:系统时间维护
d) 设备子系统框架
- V4L2:视频设备接口
- ALSA:音频设备接口
- GPIO:通用 IO 管理
- I2C/USB:总线管理
e) 驱动层
- 功能:直接硬件操作
- 类型:字符/块/网络设备驱动
- 特点:硬件专属实现
3.硬件层
- 物理设备:各类硬件控制器
- 交互方式:寄存器操作与中断处理
设计特点:
- 分层抽象:各层职责明确
- 统一接口:标准化设备访问
- 模块化:子系统独立可扩展
- 安全性:强制内核态硬件访问
对比: 单片机开发直接操作硬件寄存器

Linux用户态与内核态开发框架(以GPIO控制LED为例):
Linux GPIO 调用流程说明:
-
用户态调用
应用程序通过文件操作命令 **
echo 1 > /sys/class/gpio/gpio18/value**发起调用 -
系统调用
触发**
write()**系统调用,CPU 切换至内核态 -
VFS 层处理
虚拟文件系统 (VFS) 作为统一抽象层,提供以下功能:
- 统一接口:为应用程序提供标准文件操作接口 (open/read/write 等)
- 文件系统识别:根据路径 (如**
/sys/**) 识别对应的文件系统类型 (sysfs/ext4/proc 等) - 路由分发:将文件操作请求路由至对应的文件系统实现
- 抽象隔离:屏蔽底层文件系统实现细节,提高可移植性
-
Sysfs 层处理
sysfs 子系统解析路径,定位目标 GPIO 设备
-
GPIO 子系统
处理 GPIO 操作请求
-
GPIO 驱动
具体驱动操作硬件寄存器
-
硬件执行
GPIO 硬件输出高电平,点亮 LED 灯
关键特性对比:
| 特性 | 单片机 | Linux系统 |
|---|---|---|
| 代码复杂度 | 简单,直接寄存器操作 | 复杂,需经过多层抽象 |
| 执行路径 | 程序→寄存器→硬件 | 程序→系统调用→VFS→子系统→驱动→硬件 |
| 安全性 | 无保护机制,可直接访问硬件 | 内核保护机制,用户态无法直接访问硬件 |
| 多任务支持 | 仅支持单任务或简单RTOS | 完整支持多进程、多线程,需资源管理 |
| 可移植性 | 硬件相关,移植困难 | 通过驱动抽象,移植性强 |
| 开发效率 | 简单任务开发快速 | 复杂系统管理便捷 |
Linux采用复杂架构主要基于以下关键考量:
- 安全隔离机制:通过内核层隔离用户程序与硬件交互,有效防止误操作导致的系统崩溃
- 并发控制需求:协调多个进程对共享硬件的并发访问,确保系统稳定运行
- 硬件抽象层:提供统一系统调用接口,使应用程序摆脱硬件依赖,提升可移植性
- 集中式资源调度:由内核统一分配硬件资源,避免使用冲突和资源争用
- 模块化扩展设计:采用驱动框架支持新硬件接入,保持应用程序兼容性
Linux分层架构对比(文件、音频、视频):
文件系统架构(以磁盘文件为例):

音频系统架构(以ALSA为例):

V4L2采用类似的分层架构:

分层架构说明:
应用层(浅灰色):用户空间程序,通过系统调用访问设备
Linux内核框架(暗蓝色):系统调用接口、VFS、字符设备框架,提供内核基础服务
V4L2核心层(暗黄色):V4L2 API、核心逻辑、缓冲区管理,提供统一的视频设备接口
驱动层(深蓝灰色):具体的硬件驱动(UVC驱动、MIPI CSI驱动、V4L2子设备驱动等)
硬件层(深灰色):物理摄像头设备(USB摄像头、MIPI摄像头等)
1.2.2 工作流程
- 设备发现:内核检测到摄像头并创建/dev/video*设备节点
- 设备打开:应用程序调用open()函数打开设备文件
- 能力查询:使用VIDIOC_QUERYCAP指令查询设备功能支持
- 格式配置:通过VIDIOC_S_FMT设置视频格式参数(包括分辨率与像素格式)
- 缓冲区申请:调用VIDIOC_REQBUFS指令申请视频缓冲区
- 内存映射:使用VIDIOC_QUERYBUF和mmap()将缓冲区映射至用户空间
- 数据采集:通过VIDIOC_QBUF将缓冲区加入队列,使用VIDIOC_DQBUF获取填充数据
- 流控制:执行VIDIOC_STREAMON启动采集,调用VIDIOC_STREAMOFF停止采集
1.2.3 I/O方式
V4L2 支持三种 I/O 方式:
- read/write:实现简单但性能较低,适合处理小数据量
- mmap(内存映射):采用零拷贝技术,性能最优,是推荐的首选方案
1.3 开发流程
1.3.1 基本开发步骤
- 设备检查:确认**
/dev/video0** 设备存在并具备访问权限 - 设备开启:通过**
open()** 函数打开设备文件 - 能力查询:调用 **
VIDIOC_QUERYCAP**获取设备基本信息 - 格式枚举:使用
VIDIOC_ENUM_FMT列出设备支持的视频格式 - 格式配置:通过 **
VIDIOC_S_FMT**设置所需的视频格式参数 - 缓冲申请:执行**
VIDIOC_REQBUFS** 请求视频缓冲区 - 内存映射:使用**
mmap()** 将缓冲区映射到用户空间 - 采集启动:发送
VIDIOC_STREAMON指令开始视频采集 - 帧循环处理:交替调用 **
VIDIOC_QBUF和VIDIOC_DQBUF**进行帧数据获取 - 采集停止:发送
VIDIOC_STREAMOFF指令终止视频采集 - 资源释放:解除缓冲区映射并关闭设备文件
1.3.2 错误处理
务必检查所有 ioctl() 调用的返回值 通过 errno 获取具体的错误信息 出现错误时需妥善释放已分配资源
1.4 核心API函数说明
案例中使用的API汇总表:
V4L2 API 函数说明
配置流程
| 函数/命令 | 功能说明 | 关键结构体 | 使用频率 |
|---|---|---|---|
open() |
打开设备文件,获取文件描述符 | - | 必需 |
ioctl(fd, VIDIOC_QUERYCAP, ...) |
查询设备能力,检查支持的功能 | v4l2_capability |
必需 |
ioctl(fd, VIDIOC_ENUM_FMT, ...) |
枚举设备支持的像素格式 | v4l2_fmtdesc |
可选 |
ioctl(fd, VIDIOC_S_FMT, ...) |
设置视频格式(分辨率、像素格式等) | v4l2_format |
必需 |
ioctl(fd, VIDIOC_REQBUFS, ...) |
申请指定数量的视频缓冲区 | v4l2_requestbuffers |
必需 |
缓冲区流程
| 函数/命令 | 功能说明 | 关键结构体 | 使用频率 |
|---|---|---|---|
ioctl(fd, VIDIOC_QUERYBUF, ...) |
查询缓冲区信息(偏移量、长度) | v4l2_buffer |
必需 |
mmap() |
将内核缓冲区映射到用户空间 | - | 必需 |
采集流程
| 函数/命令 | 功能说明 | 关键结构体 | 使用频率 |
|---|---|---|---|
ioctl(fd, VIDIOC_QBUF, ...) |
将缓冲区加入采集队列 | v4l2_buffer |
必需 |
ioctl(fd, VIDIOC_STREAMON, ...) |
启动视频流,开始采集 | - | 必需 |
ioctl(fd, VIDIOC_DQBUF, ...) |
从队列取出已填充的缓冲区(循环使用) | v4l2_buffer |
必需 |
ioctl(fd, VIDIOC_STREAMOFF, ...) |
停止视频流 | - | 必需 |
munmap() |
取消内存映射,释放缓冲区 | - | 必需 |
close() |
关闭设备文件 | - | 必需 |
说明:
所属流程 :配置流程、缓冲区流程、采集流程
使用频率:
- 必需:必须调用
- 可选:可选调用
- 循环:在循环中多次调用
关键结构体:每个API需要配合使用的V4L2结构体
ioctl函数原型及说明:
c
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
功能说明: ioctl(Input/Output Control)是Linux系统中用于设备控制的系统调用。
参数说明:
- fd:文件描述符,通过open()函数打开设备文件获取
- request:控制命令,在V4L2中对应VIDIOC_*系列宏定义
- ...:可变参数,通常为结构体指针,具体类型根据request命令而定
返回值:
- 成功:返回0
- 失败:返回-1,并设置errno指示具体错误
应用场景: 在V4L2框架中,ioctl广泛用于设备操作,包括:
- 设备能力查询
- 视频格式设置
- 缓冲区管理等核心功能
常用V4L2 ioctl命令:
markdown
| ioctl命令 | 功能 | 输入结构体 | 输出结构体 | 说明 |
|--------------------|--------------------|---------------------|---------------------|--------------------------|
| VIDIOC_QUERYCAP | 查询设备能力 | - | v4l2_capability | 获取设备信息和支持的功能 |
| VIDIOC_ENUM_FMT | 枚举像素格式 | v4l2_fmtdesc | v4l2_fmtdesc | 查询设备支持的像素格式 |
| VIDIOC_S_FMT | 设置视频格式 | v4l2_format | v4l2_format | 设置分辨率、像素格式等 |
| VIDIOC_G_FMT | 获取当前格式 | - | v4l2_format | 获取当前设置的视频格式 |
| VIDIOC_REQBUFS | 申请缓冲区 | v4l2_requestbuffers | v4l2_requestbuffers | 申请指定数量的缓冲区 |
| VIDIOC_QUERYBUF | 查询缓冲区信息 | - | v4l2_buffer | 获取缓冲区的偏移量等信息 |
| VIDIOC_QBUF | 缓冲区入队 | v4l2_buffer | v4l2_buffer | 将缓冲区加入采集队列 |
| VIDIOC_DQBUF | 缓冲区出队 | - | v4l2_buffer | 从队列取出已填充缓冲区 |
| VIDIOC_STREAMON | 启动视频流 | enum v4l2_buf_type | - | 开始视频采集 |
| VIDIOC_STREAMOFF | 停止视频流 | enum v4l2_buf_type | - | 停止视频采集 |
| VIDIOC_QUERYCTRL | 查询控制参数 | v4l2_queryctrl | v4l2_queryctrl | 查询控制参数的范围和属性 |
| VIDIOC_G_CTRL | 获取控制参数值 | v4l2_control | v4l2_control | 获取控制参数的当前值 |
| VIDIOC_S_CTRL | 设置控制参数值 | v4l2_control | v4l2_control | 设置控制参数的值 |
使用示例:
cpp
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <errno.h>
int fd; // 设备文件描述符
struct v4l2_capability cap;
// 示例1:查询设备能力
memset(&cap, 0, sizeof(cap));
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
perror("VIDIOC_QUERYCAP failed");
return -1;
}
// 示例2:设置视频格式
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
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;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
perror("VIDIOC_S_FMT failed");
return -1;
}
// 示例3:申请缓冲区
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
perror("VIDIOC_REQBUFS failed");
return -1;
}
// 示例4:启动视频流
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {
perror("VIDIOC_STREAMON failed");
return -1;
}
错误处理:
cpp
// 所有ioctl调用都应该检查返回值
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
// 检查errno获取详细错误信息
if (errno == EINVAL) {
fprintf(stderr, "Invalid argument\n");
} else if (errno == ENODEV) {
fprintf(stderr, "Device not found\n");
} else if (errno == EBUSY) {
fprintf(stderr, "Device is busy\n");
} else {
perror("ioctl failed");
}
return -1;
}
使用ioctl时的注意事项:
- 结构体初始化:调用ioctl前必须用memset()清零结构体,防止未初始化数据引发错误
- 输入输出参数:部分ioctl命令(如VIDIOC_S_FMT)使用同一结构体作为输入和输出参数,调用后需验证实际设置值
- 错误处理:每次ioctl调用都应检查返回值,失败时通过errno获取具体错误原因
- 阻塞特性:某些ioctl操作(如VIDIOC_DQBUF)在阻塞模式下会等待操作完成
- 线程安全:多线程环境中使用ioctl时,需确保对同一设备的访问进行同步控制
1. 配置流程

流程详细说明:
-
设备打开(open)概念说明:
-
设备文件:在Linux系统中,硬件设备被抽象为文件形式进行管理。常见的摄像头设备文件通常命名为/dev/video0、/dev/video1等。
-
文件描述符(fd):open()函数调用后会返回一个整数值,该值作为已打开设备的唯一标识符,后续所有设备操作都需通过此文件描述符进行。
-
打开模式:
- O_RDWR:表示以可读写模式打开设备
-
- O_NONBLOCK:表示采用非阻塞模式打开(可选参数)
伪代码:
cpp
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
// 1. 定义设备路径
const char *device = "/dev/video0";
// 2. 打开设备文件
// open() 参数说明:
// - device: 设备文件路径
// - O_RDWR: 可读写模式
// - O_NONBLOCK: 非阻塞模式(可选,如果使用阻塞模式则设为0)
// 返回值: 文件描述符(fd),失败返回-1
int fd = open(device, O_RDWR | O_NONBLOCK, 0);
if (fd < 0) {
// 错误处理:设备不存在、权限不足、被占用等
fprintf(stderr, "Cannot open device %s: %s\n", device, strerror(errno));
return -1;
}
// 3. fd 用于后续所有V4L2操作
// 后续使用:ioctl(fd, ...)
printf("Device opened successfully, fd=%d\n", fd);
开发要点:
-
设备检测
- 检查设备节点:
ls /dev/video* - 权限验证:确保当前用户具有访问权限,必要时使用sudo
- 检查设备节点:
-
设备访问模式
- 独占访问:同一时刻仅允许一个程序访问设备
- 操作模式选择:
- 阻塞模式(O_RDWR):VIDIOC_DQBUF将阻塞直至数据可用
- 非阻塞模式(O_RDWR | O_NONBLOCK):需配合select()使用
-
设备能力查询(VIDIOC_QUERYCAP)
- 功能支持检测:验证设备支持的视频功能
- 数据结构说明:
- v4l2_capability:包含驱动名称、设备标识、总线信息及能力标志
- 关键能力标志:
- V4L2_CAP_VIDEO_CAPTURE:视频采集支持
- V4L2_CAP_STREAMING:流式I/O支持(mmap必需)
- V4L2_CAP_READWRITE:read/write操作支持
伪代码实现:
cpp
#include <linux/videodev2.h>
#include <sys/ioctl.h>
// 1. 定义并初始化结构体
struct v4l2_capability cap; // 设备能力结构体
memset(&cap, 0, sizeof(cap)); // 清零,避免未初始化数据
// 2. 调用ioctl查询能力
// ioctl() 参数说明:
// - fd: 设备文件描述符
// - VIDIOC_QUERYCAP: 查询设备能力的命令
// - &cap: 指向v4l2_capability结构体的指针
// 返回值: 成功返回0,失败返回-1
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
perror("VIDIOC_QUERYCAP failed");
return -1;
}
// 3. 打印设备信息(调试用)
printf("Driver: %s\n", cap.driver); // 驱动名称
printf("Card: %s\n", cap.card); // 设备名称
printf("Bus: %s\n", cap.bus_info); // 总线信息
printf("Version: %d.%d.%d\n",
(cap.version >> 16) & 0xFF, // 主版本号
(cap.version >> 8) & 0xFF, // 次版本号
cap.version & 0xFF); // 修订版本号
// 4. 检查必需的能力标志
// V4L2_CAP_VIDEO_CAPTURE: 设备必须支持视频采集
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
fprintf(stderr, "Device does not support video capture\n");
return -1;
}
// V4L2_CAP_STREAMING: 如果使用mmap方式,必须支持流式I/O
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
fprintf(stderr, "Device does not support streaming I/O\n");
return -1;
}
// V4L2_CAP_READWRITE: 如果使用read/write方式,需要检查此标志
if (cap.capabilities & V4L2_CAP_READWRITE) {
printf("Device supports read/write I/O\n");
}
printf("Device capabilities verified\n");
开发要点:
-
枚举格式(可选但推荐):
- 帮助识别设备支持的视频格式
- 常用格式:YUYV(未压缩)或MJPEG(压缩高质量)
- 格式码为四字符标识符(如"YUYV"、"MJPG")
-
枚举结果判断:
- 当errno==EINVAL时表示枚举正常结束
-
设置视频格式(VIDIOC_S_FMT):
- 关键参数配置:
- 分辨率(如640x480、1280x720)
- 像素格式(需选择设备支持的格式)
- 场模式(推荐V4L2_FIELD_INTERLACED或V4L2_FIELD_NONE)
- 使用v4l2_format结构体:
- 包含缓冲区类型和格式信息
- 关键参数配置:
- 用于指定采集参数
伪代码:
cpp
// 1. 定义并初始化结构体
struct v4l2_format fmt; // 视频格式结构体
memset(&fmt, 0, sizeof(fmt));
// 2. 设置格式参数
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; // 像素格式(YUYV未压缩格式)
// 或者使用MJPEG格式:
// fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; // 场模式
// 3. 设置格式(驱动可能会调整参数)
// ioctl() 参数说明:
// - fd: 设备文件描述符
// - VIDIOC_S_FMT: 设置格式的命令
// - &fmt: 指向v4l2_format结构体的指针(输入输出参数)
// 返回值: 成功返回0,失败返回-1
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
perror("VIDIOC_S_FMT failed");
return -1;
}
// 4. 检查实际设置的格式(驱动可能调整了分辨率)
printf("Requested format: 640x480, YUYV\n");
printf("Actual format: %dx%d, pixelformat: %.4s\n",
fmt.fmt.pix.width,
fmt.fmt.pix.height,
(char*)&fmt.fmt.pix.pixelformat);
// 5. 检查格式是否被调整
if (fmt.fmt.pix.width != 640 || fmt.fmt.pix.height != 480) {
printf("Warning: Resolution adjusted by driver\n");
}
// 6. 获取图像大小(用于后续缓冲区分配)
unsigned int image_size = fmt.fmt.pix.sizeimage; // 一帧图像的实际大小(字节)
unsigned int bytesperline = fmt.fmt.pix.bytesperline; // 每行的字节数(可能有对齐)
printf("Image size: %u bytes\n", image_size);
printf("Bytes per line: %u\n", bytesperline);
// 7. 计算实际需要的缓冲区大小
// 注意:某些格式(如MJPEG)的图像大小是变化的
// 建议使用驱动返回的sizeimage值
开发注意事项:
- 驱动程序可能会自动调整分辨率,设置后需验证实际获取的值
- sizeimage字段表示单帧图像的实际存储大小(单位:字节)
- bytesperline字段表示每行数据的字节数(可能存在内存对齐填充)
- 部分压缩格式(如MJPEG)的图像大小会随内容变化
- 当驱动不支持请求的格式时,将返回最接近的可用格式
完整配置流程示例:
cpp
// 完整的设备配置函数
static int configure_device(const char *device_path, int width, int height) {
int fd;
struct v4l2_capability cap;
struct v4l2_format fmt;
// 1. 打开设备
fd = open(device_path, O_RDWR | O_NONBLOCK, 0);
if (fd < 0) {
fprintf(stderr, "Cannot open device %s: %s\n",
device_path, strerror(errno));
return -1;
}
// 2. 查询设备能力
memset(&cap, 0, sizeof(cap));
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
perror("VIDIOC_QUERYCAP failed");
close(fd);
return -1;
}
// 检查能力
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
fprintf(stderr, "Device does not support video capture\n");
close(fd);
return -1;
}
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
fprintf(stderr, "Device does not support streaming I/O\n");
close(fd);
return -1;
}
// 3. 枚举格式(可选)
struct v4l2_fmtdesc fmt_desc;
memset(&fmt_desc, 0, sizeof(fmt_desc));
fmt_desc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt_desc.index = 0;
printf("Supported formats:\n");
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt_desc) == 0) {
printf(" [%d] %s (%.4s)\n",
fmt_desc.index,
fmt_desc.description,
(char*)&fmt_desc.pixelformat);
fmt_desc.index++;
}
// 4. 设置格式
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = width;
fmt.fmt.pix.height = height;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
perror("VIDIOC_S_FMT failed");
close(fd);
return -1;
}
printf("Format set: %dx%d, pixelformat: %.4s\n",
fmt.fmt.pix.width,
fmt.fmt.pix.height,
(char*)&fmt.fmt.pix.pixelformat);
return fd; // 返回文件描述符,供后续使用
}
2. 缓冲区流程

操作流程说明:
- 缓冲区申请(VIDIOC_REQBUFS)
核心概念:
- 缓冲区申请:向内核申请指定数量的视频缓冲区,用于存储采集的视频帧数据
- v4l2_requestbuffers结构体:包含缓冲区数量、类型和内存类型等关键参数
内存类型详解:
- V4L2_MEMORY_MMAP(推荐):采用内存映射方式,由内核分配缓冲区,用户空间通过mmap访问(实现零拷贝)
- V4L2_MEMORY_USERPTR:用户指针方式,缓冲区由用户空间自行分配
- V4L2_MEMORY_DMABUF:DMA缓冲区方式,专为零拷贝传输设计
最佳实践: 建议申请4个缓冲区,通过流水线处理机制提升系统效率
伪代码:
cpp
#include <linux/videodev2.h>
// 1. 定义用户空间缓冲区结构体(用于存储映射后的地址)
struct buffer {
void *start; // 映射后的内存地址
size_t length; // 缓冲区长度
};
#define BUFFER_COUNT 4 // 申请4个缓冲区
struct buffer *buffers = NULL; // 用户空间缓冲区数组
unsigned int n_buffers = 0; // 实际分配的缓冲区数量
// 2. 定义并初始化v4l2_requestbuffers结构体
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));
// 3. 设置申请参数
req.count = BUFFER_COUNT; // 申请4个缓冲区
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 视频采集类型
req.memory = V4L2_MEMORY_MMAP; // 内存映射方式
// 4. 调用ioctl申请缓冲区
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
perror("VIDIOC_REQBUFS failed");
return -1;
}
// 5. 检查实际分配的缓冲区数量(驱动可能会调整)
if (req.count < 2) {
fprintf(stderr, "Insufficient buffer memory (got %d, need at least 2)\n", req.count);
return -1;
}
n_buffers = req.count; // 保存实际分配的缓冲区数量
printf("Allocated %d buffers\n", n_buffers);
// 6. 分配用户空间缓冲区数组(用于存储映射后的地址)
buffers = calloc(n_buffers, sizeof(*buffers));
if (!buffers) {
fprintf(stderr, "Out of memory\n");
return -1;
}
开发要点:
-
缓冲区配置建议:
- 数量设置为4个为宜,过少可能导致丢帧,过多则浪费内存资源
- 必须验证实际分配的缓冲区数量,驱动程序可能自动调整
-
内存优化方案:
- 采用V4L2_MEMORY_MMAP方式实现零拷贝机制,可获得最佳性能表现
-
缓冲区信息查询(VIDIOC_QUERYBUF):
- 功能:获取每个缓冲区的详细参数(偏移量、长度等),为内存映射做准备
- 数据结构:v4l2_buffer结构体包含以下关键字段:
- 缓冲区索引
- 缓冲区类型
- 内存类型
- 偏移量(设备文件中的位置,用于mmap映射)
- 长度(缓冲区字节大小,通常等于单帧图像尺寸)
- 时间戳信息
伪代码:
cpp
// 循环查询每个缓冲区的信息
for (unsigned int i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf; // 缓冲区信息结构体
// 1. 初始化结构体
memset(&buf, 0, sizeof(buf));
// 2. 设置查询参数
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 缓冲区类型
buf.memory = V4L2_MEMORY_MMAP; // 内存类型
buf.index = i; // 缓冲区索引(0, 1, 2, 3...)
// 3. 调用ioctl查询缓冲区信息
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
perror("VIDIOC_QUERYBUF failed");
return -1;
}
// 4. 保存缓冲区信息(用于后续mmap)
buffers[i].length = buf.length; // 缓冲区长度
// 5. 打印调试信息(可选)
printf("Buffer %d: offset=0x%x, length=%u\n",
i, buf.m.offset, buf.length);
// 注意:buf.m.offset 是偏移量,将在mmap时使用
// 注意:buf.length 是缓冲区长度,将在mmap时使用
}
开发要点:
- 每个缓冲区都需要调用VIDIOC_QUERYBUF获取相关信息
- buf.m.offset表示内核缓冲区在设备文件中的偏移地址,该值将用于mmap映射
- buf.length指定缓冲区大小,既用于mmap操作也影响后续数据访问
内存映射(mmap)核心概念:
- 通过mmap将内核缓冲区直接映射到用户空间,实现零拷贝访问
- mmap系统调用可将设备文件的指定区域映射为用户空间内存
- 零拷贝机制允许用户程序直接操作内核缓冲区,无需数据复制,性能最优
- 映射关系:用户空间buffers[i].start指针直接对应内核缓冲区地址
伪代码:
cpp
#include <sys/mman.h> // mmap需要此头文件
// 循环映射每个缓冲区
for (unsigned int i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf; // 需要再次查询,或保存之前查询的结果
// 1. 查询缓冲区信息(如果之前没有保存)
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
perror("VIDIOC_QUERYBUF failed");
return -1;
}
// 2. 使用mmap映射缓冲区到用户空间
// mmap参数说明:
// - NULL: 让系统自动选择映射地址
// - buf.length: 映射长度(缓冲区大小)
// - PROT_READ | PROT_WRITE: 可读可写权限
// - MAP_SHARED: 共享映射(与内核共享内存)
// - fd: 设备文件描述符
// - buf.m.offset: 缓冲区在设备文件中的偏移量
buffers[i].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, buf.m.offset);
// 3. 检查映射是否成功
if (buffers[i].start == MAP_FAILED) {
perror("mmap failed");
return -1;
}
// 4. 保存缓冲区长度(已保存,这里只是确认)
buffers[i].length = buf.length;
printf("Buffer %d mapped at %p, length=%zu\n",
i, buffers[i].start, buffers[i].length);
}
// 映射完成后,可以通过 buffers[i].start 直接访问帧数据
// 例如:访问第0个缓冲区的数据
// unsigned char *frame_data = (unsigned char *)buffers[0].start;
// frame_data[0] 是第一个字节的数据
开发要点:
- 调用mmap()会返回映射后的用户空间地址,若失败则返回MAP_FAILED
- 映射后的内存可直接读写,无需通过系统调用访问
- 使用MAP_SHARED标志保证内存区域与内核共享
- 映射成功后,buffers[i].start直接指向内核缓冲区数据
概念知识:
启动视频流:通知驱动开始采集视频数据
VIDIOC_STREAMON:启动指定类型的视频流
缓冲区队列:在启动前,需要先将缓冲区加入队列(VIDIOC_QBUF)
流类型:通常使用V4L2_BUF_TYPE_VIDEO_CAPTURE表示视频采集流
伪代码:
cpp
// 1. 先将所有缓冲区加入队列(VIDIOC_QBUF)
// 这一步很重要:驱动需要知道哪些缓冲区可以用来填充数据
for (unsigned int i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i; // 指定要加入队列的缓冲区索引
// 将缓冲区加入队列,等待驱动填充数据
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
perror("VIDIOC_QBUF failed");
return -1;
}
printf("Buffer %d queued\n", i);
}
// 2. 启动视频流
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {
perror("VIDIOC_STREAMON failed");
return -1;
}
printf("Stream started\n");
// 3. 此时驱动开始采集数据,填充到队列中的缓冲区
// 应用程序可以通过 VIDIOC_DQBUF 取出已填充的缓冲区
开发注意事项:
- 初始化阶段需调用VIDIOC_QBUF将所有缓冲区加入队列
- 调用VIDIOC_STREAMON后,驱动程序将自动开始采集数据
- 运行过程中,可通过VIDIOC_DQBUF获取已采集完成的帧数据
完整缓冲区流程函数封装:
cpp
// 完整的缓冲区初始化并启动采集函数
// 功能:整合申请缓冲区、查询缓冲区、映射缓冲区、加入队列、启动采集
// 参数:
// - buffer_count: 申请的缓冲区数量(建议4个)
// 返回值:成功返回0,失败返回-1
static int init_buffers_and_start(int buffer_count) {
struct v4l2_requestbuffers req;
unsigned int i;
enum v4l2_buf_type type;
// ========== 步骤1:申请缓冲区 ==========
memset(&req, 0, sizeof(req));
req.count = buffer_count; // 申请指定数量的缓冲区
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 视频采集类型
req.memory = V4L2_MEMORY_MMAP; // 内存映射方式
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
perror("VIDIOC_REQBUFS failed");
return -1;
}
// 检查实际分配的缓冲区数量
if (req.count < 2) {
fprintf(stderr, "Insufficient buffer memory (got %d, need at least 2)\n",
req.count);
return -1;
}
printf("Allocated %d buffers (requested %d)\n", req.count, buffer_count);
n_buffers = req.count; // 保存实际分配的缓冲区数量
// 分配用户空间缓冲区数组
buffers = calloc(n_buffers, sizeof(*buffers));
if (!buffers) {
fprintf(stderr, "Out of memory\n");
return -1;
}
// ========== 步骤2:查询并映射每个缓冲区 ==========
for (i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf; // 局部变量,每次循环都重新定义
// 查询缓冲区信息
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i; // 设置要查询的缓冲区索引
// ioctl是同步系统调用:设置buf.index作为输入,驱动填充buf的其他字段作为输出
// 调用完成后,buf中已经包含了驱动返回的完整信息(length, offset等)
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
perror("VIDIOC_QUERYBUF failed");
// 清理已分配的缓冲区
for (unsigned int j = 0; j < i; ++j) {
if (buffers[j].start != MAP_FAILED) {
munmap(buffers[j].start, buffers[j].length);
}
}
free(buffers);
buffers = NULL;
return -1;
}
// 重要:立即将buf中的信息保存到全局数组buffers[i]中
// 因为buf是局部变量,循环结束后会被销毁,但数据已经保存到buffers[i]了
buffers[i].length = buf.length; // 保存缓冲区长度
// 映射缓冲区到用户空间(使用buf.m.offset,这是驱动返回的偏移量)
buffers[i].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, buf.m.offset);
if (buffers[i].start == MAP_FAILED) {
perror("mmap failed");
// 清理已分配的缓冲区
for (unsigned int j = 0; j < i; ++j) {
if (buffers[j].start != MAP_FAILED) {
munmap(buffers[j].start, buffers[j].length);
}
}
free(buffers);
buffers = NULL;
return -1;
}
printf("Buffer %d: mapped at %p, length=%zu, offset=0x%x\n",
i, buffers[i].start, buffers[i].length, buf.m.offset);
// 注意:这里buf是局部变量,循环结束后会自动销毁
// 但数据已经保存到buffers[i]中,所以完全没问题
}
// ========== 步骤3:将所有缓冲区加入队列 ==========
for (i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf; // 局部变量,每次循环都重新定义
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i; // 指定要加入队列的缓冲区索引
// ioctl是同步系统调用:设置buf.index作为输入,驱动读取这个值
// 驱动内部会记录:缓冲区i已经加入队列
// 调用完成后,buf的内容不再需要,因为驱动已经记住了这个缓冲区
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
perror("VIDIOC_QBUF failed");
// 清理资源
for (unsigned int j = 0; j < n_buffers; ++j) {
if (buffers[j].start != MAP_FAILED) {
munmap(buffers[j].start, buffers[j].length);
}
}
free(buffers);
buffers = NULL;
return -1;
}
printf("Buffer %d queued\n", i);
// 注意:这里buf是局部变量,循环结束后会自动销毁
// 但驱动已经记住了缓冲区i在队列中,所以完全没问题
}
// ========== 步骤4:启动视频流 ==========
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {
perror("VIDIOC_STREAMON failed");
// 清理资源
for (i = 0; i < n_buffers; ++i) {
if (buffers[i].start != MAP_FAILED) {
munmap(buffers[i].start, buffers[i].length);
}
}
free(buffers);
buffers = NULL;
return -1;
}
printf("Stream started, ready for capture\n");
return 0;
}
// 使用示例
int main(int argc, char **argv) {
const char *dev_name = "/dev/video0";
int buffer_count = 4; // 申请4个缓冲区
// ... 打开设备和设置格式的代码 ...
// 使用完整函数初始化缓冲区并启动采集
if (init_buffers_and_start(buffer_count) < 0) {
fprintf(stderr, "Failed to initialize buffers\n");
close_device();
return 1;
}
// 现在可以开始采集了
// ... 采集循环代码 ...
// 清理资源
stop_capturing();
uninit_mmap();
close_device();
return 0;
}
函数特点:
完整性:整合了缓冲区流程的所有步骤(申请、查询、映射、入队、启动)
错误处理:每个步骤都有错误检查,失败时正确清理已分配的资源
资源管理:确保在任何步骤失败时都能正确释放已分配的资源
易于使用:一个函数调用完成所有缓冲区初始化工作
调试信息:打印每个步骤的执行情况,便于调试
关于局部变量 struct v4l2_buffer buf 的使用说明:
许多初学者对循环中使用局部变量 struct v4l2_buffer buf 存在疑虑,实际上这是完全正确且推荐的做法,原因如下:
1. ioctl是同步系统调用
- ioctl() 是同步系统调用,操作会立即完成并返回
- 无需维持 buf 的生命周期,调用结束后即可释放
2.VIDIOC_QUERYBUF的工作机制
c
// 输入:设置 buf.index = i(指定查询的缓冲区)
// 输出:驱动填充 buf.length, buf.m.offset 等字段
ioctl(fd, VIDIOC_QUERYBUF, &buf);
// 调用完成后立即保存数据到全局数组
buffers[i].length = buf.length; // 数据已持久化
buffers[i].start = mmap(..., buf.m.offset); // 立即建立映射
- 驱动仅在 ioctl 调用期间填充 buf 字段
- 数据已及时保存至 buffers[i],局部变量销毁不影响
3. VIDIOC_QBUF的工作机制
c
// 输入:设置 buf.index = i(指定入队的缓冲区)
// 驱动:读取 buf.index 并记录"缓冲区i已入队"
ioctl(fd, VIDIOC_QBUF, &buf);
// 调用完成后驱动已保存队列信息
- 驱动在调用期间读取 buf.index
- 队列状态由驱动内部维护,与 buf 生命周期无关
4. 为什么不用全局变量?
- 独立性:每次循环都是独立操作,局部变量完全满足需求
- 安全性:避免变量重用导致的数据污染
- 可读性:每次循环重新初始化,代码逻辑更清晰
- 性能:局部变量在栈上分配,内存效率更高
5. 对比:什么时候需要全局变量?
c
// 采集循环中,buf需要保存到循环外使用
struct v4l2_buffer buf; // 在循环外定义或使用全局变量
while (1) {
ioctl(fd, VIDIOC_DQBUF, &buf);
// 使用 buf.index 访问 buffers[buf.index].start
// 处理完数据后需要再次使用同一个buf调用 VIDIOC_QBUF
ioctl(fd, VIDIOC_QBUF, &buf);
}
但在初始化阶段,每次循环都是独立操作,局部变量完全够用。
总结:
✅ 初始化阶段:使用局部变量 struct v4l2_buffer buf; 完全正确
✅ ioctl 是同步的:调用完成后数据已保存或驱动已记录
✅ 局部变量优势:避免变量重用,代码更清晰
❌ 不需要全局变量:除非需要在多个函数间共享同一个buf
3. 采集流程
前提条件:已完成缓冲区流程设置,视频流已启动(已调用VIDIOC_STREAMON),所有缓冲区均已加入队列并开始采集数据。

流程说明:
前置条件: 缓冲区初始化已完成以下步骤:
- 申请缓冲区(VIDIOC_REQBUFS)
- 查询并映射缓冲区(VIDIOC_QUERYBUF + mmap)
- 将所有缓冲区加入队列(VIDIOC_QBUF)
- 启动视频流(VIDIOC_STREAMON)
此时驱动已开始采集数据,缓冲区中存在待处理的帧数据。
采集流程: 循环执行以下操作:
- 取出已填充的缓冲区(VIDIOC_DQBUF)
- 处理缓冲区数据
- 将缓冲区重新加入队列
关键概念:
- 缓冲区出队:从队列中获取包含有效数据的缓冲区
- VIDIOC_DQBUF:用于取出缓冲区的ioctl命令
- 阻塞模式:若无可用数据,将等待至数据就绪
- 非阻塞模式:若无可用数据,立即返回EAGAIN错误
- 返回信息:v4l2_buffer结构体包含缓冲区索引、数据长度、时间戳等关键信息
1.取出缓冲区
伪代码:
cpp
// 从队列取出已填充的缓冲区
int dequeue_buffer(struct v4l2_buffer *buf) {
// 1. 初始化结构体
memset(buf, 0, sizeof(*buf));
// 2. 设置缓冲区类型和内存类型
buf->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf->memory = V4L2_MEMORY_MMAP;
// 注意:不需要设置index,驱动会返回已填充的缓冲区索引
// 3. 从队列取出已填充的缓冲区
if (ioctl(fd, VIDIOC_DQBUF, buf) < 0) {
if (errno == EAGAIN) {
// 非阻塞模式:没有数据可用
return 0; // 返回0表示没有数据,但不算错误
}
perror("VIDIOC_DQBUF failed");
return -1;
}
return 1; // 返回1表示成功获取一帧数据
}
开发要点:
- 必须通过返回的buf.index访问对应的缓冲区数据
- 阻塞模式下,DQBUF操作会持续等待直到获取可用数据
- 非阻塞模式下,需结合select()或轮询机制使用
- 数据处理完成后,必须将缓冲区重新加入队列
2. 访问用户空间缓冲区
概念知识:
用户空间缓冲区:通过mmap映射的内核缓冲区,可以直接在用户空间访问
缓冲区访问:使用buffers[buf.index].start获取数据指针
数据长度:使用buf.length获取实际数据长度
重要:必须使用buf.index,不要使用循环变量或其他索引
伪代码:
cpp
// 访问用户空间缓冲区
void access_buffer(struct v4l2_buffer *buf) {
// 1. 获取缓冲区索引(由驱动返回)
unsigned int buffer_index = buf->index;
// 2. 访问用户空间缓冲区
// 重要:必须使用 buf->index,不要使用循环变量
void *frame_data = buffers[buffer_index].start; // 数据指针
size_t frame_size = buf->length; // 数据长度
// 3. 验证缓冲区有效性
if (frame_data == MAP_FAILED || frame_data == NULL) {
fprintf(stderr, "Invalid buffer at index %d\n", buffer_index);
return;
}
}
开发要点:
- 必须通过buf.index访问缓冲区,该索引由驱动提供且保证正确
- 缓冲区数据位于用户空间,可直接访问,无需进行额外拷贝操作
- 注意数据长度可能动态变化,这在压缩格式(如MJPEG)中尤为常见
3. 打印帧数据信息
概念知识:
帧信息:包括缓冲区索引、数据长度、时间戳等
调试信息:打印帧信息有助于调试和监控采集状态
时间戳:使用buf.timestamp获取帧采集时间
伪代码:
cpp
// 打印帧数据信息
void print_frame_info(struct v4l2_buffer *buf, int frame_count) {
// 1. 打印基本帧信息
printf("========================================\n");
printf("Frame #%d\n", frame_count);
printf(" Buffer Index: %d\n", buf->index);
printf(" Data Length: %u bytes (%.2f KB)\n",
buf->length, buf->length / 1024.0);
// 2. 打印时间戳
struct timeval *ts = &buf->timestamp;
printf(" Timestamp: %ld.%06ld seconds\n",
ts->tv_sec, ts->tv_usec);
// 3. 打印缓冲区地址(用于调试)
void *frame_data = buffers[buf->index].start;
printf(" Buffer Address: %p\n", frame_data);
// 4. 打印数据格式信息(如果之前保存了)
if (current_pixelformat == V4L2_PIX_FMT_YUYV) {
printf(" Format: YUYV (YUV 4:2:2)\n");
printf(" Resolution: %dx%d\n", current_width, current_height);
} else if (current_pixelformat == V4L2_PIX_FMT_MJPEG) {
printf(" Format: MJPEG (Compressed)\n");
printf(" Resolution: %dx%d\n", current_width, current_height);
}
printf("========================================\n");
}
开发要点:
- 打印帧信息便于调试和实时监控
- 利用时间戳计算帧率指标
- 通过数据长度校验确保数据完整性
4. 保存图片到本地
概念知识:
图像保存:根据像素格式选择不同的保存方式
YUYV格式:可以保存为原始数据,或转换为RGB后保存为JPEG
MJPEG格式:可以直接保存为JPEG文件
文件操作:使用标准C库函数进行文件读写
完整代码示例:
cpp
// 全局变量:保存当前格式信息(在set_format时设置)
static unsigned int current_pixelformat = 0;
static unsigned int current_width = 0;
static unsigned int current_height = 0;
static int frame_count = 0;
// 保存MJPEG数据为JPEG文件
static int save_mjpeg_frame(const char *filename, void *data, size_t size) {
FILE *fp = fopen(filename, "wb");
if (!fp) {
fprintf(stderr, "Cannot open file %s: %s\n", filename, strerror(errno));
return -1;
}
size_t written = fwrite(data, 1, size, fp);
fclose(fp);
if (written != size) {
fprintf(stderr, "Failed to write all data to %s\n", filename);
return -1;
}
printf("Saved MJPEG frame to %s (%zu bytes)\n", filename, size);
return 0;
}
// 完整的采集和处理函数
static int capture_and_save_frame(void) {
struct v4l2_buffer buf;
char filename[256];
// 1. 从队列取出已填充的缓冲区
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
if (errno == EAGAIN) {
return 0; // 没有数据,非阻塞返回
}
fprintf(stderr, "VIDIOC_DQBUF failed: %s\n", strerror(errno));
return -1;
}
// 2. 访问用户空间缓冲区
// 重要:必须使用 buf.index 来访问对应的缓冲区
void *frame_data = buffers[buf.index].start;
size_t frame_size = buf.length;
// 3. 打印帧数据信息
printf("========================================\n");
printf("Frame #%d\n", frame_count);
printf(" Buffer Index: %d\n", buf.index);
printf(" Data Length: %zu bytes (%.2f KB)\n",
frame_size, frame_size / 1024.0);
printf(" Buffer Address: %p\n", frame_data);
printf(" Timestamp: %ld.%06ld seconds\n",
buf.timestamp.tv_sec, buf.timestamp.tv_usec);
// 4. 根据像素格式保存图像
if (current_pixelformat == V4L2_PIX_FMT_MJPEG) {
// MJPEG格式:直接保存为JPEG文件
snprintf(filename, sizeof(filename), "frame_%03d.jpg", frame_count);
save_mjpeg_frame(filename, frame_data, frame_size);
printf(" Format: MJPEG -> Saved as %s\n", filename);
} else if (current_pixelformat == V4L2_PIX_FMT_YUYV) {
// YUYV格式:保存原始数据
snprintf(filename, sizeof(filename), "frame_%03d.yuyv", frame_count);
save_raw_frame(filename, frame_data, frame_size);
printf(" Format: YUYV -> Saved as %s\n", filename);
printf(" Resolution: %dx%d\n", current_width, current_height);
// 可选:转换为JPEG保存(需要libjpeg库)
// snprintf(filename, sizeof(filename), "frame_%03d.jpg", frame_count);
// save_yuyv_as_jpeg(filename, (unsigned char *)frame_data,
// current_width, current_height, 90);
} else {
// 其他格式:保存原始数据
snprintf(filename, sizeof(filename), "frame_%03d.raw", frame_count);
save_raw_frame(filename, frame_data, frame_size);
printf(" Format: Unknown -> Saved as %s\n", filename);
}
printf("========================================\n");
frame_count++;
// 5. 将缓冲区重新加入队列,继续采集
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
fprintf(stderr, "VIDIOC_QBUF failed: %s\n", strerror(errno));
return -1;
}
return 1; // 返回1表示成功处理一帧
}
// 采集循环主函数
static int capture_loop(int max_frames) {
printf("Start capturing %d frames...\n", max_frames);
frame_count = 0;
while (frame_count < max_frames) {
int ret = capture_and_save_frame();
if (ret < 0) {
fprintf(stderr, "Capture failed\n");
return -1;
}
// ret == 0 表示非阻塞模式下没有数据,继续等待
}
printf("Capture finished, saved %d frames\n", frame_count);
return 0;
}
开发要点:
- 缓冲区访问必须通过buf.index实现,禁止使用循环变量或其他索引方式
- 需根据像素格式差异采用对应的数据保存方案
- 数据处理完成后务必及时将缓冲区重新加入队列
- 需特别注意数据长度可能发生变化(压缩格式下尤为明显)
5. 停止采集(VIDIOC_STREAMOFF)
概念知识:
停止视频流:通知驱动停止采集数据
VIDIOC_STREAMOFF:停止指定类型视频流的ioctl命令
流类型:使用V4L2_BUF_TYPE_VIDEO_CAPTURE表示视频采集流
停止后:驱动不再填充新的数据到缓冲区
伪代码:
cpp
// 停止视频流
int stop_streaming(void) {
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
// ioctl() 参数说明:
// - fd: 设备文件描述符
// - VIDIOC_STREAMOFF: 停止视频流的命令
// - &type: 指向流类型的指针
// 返回值: 成功返回0,失败返回-1
if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0) {
perror("VIDIOC_STREAMOFF failed");
return -1;
}
printf("Stream stopped\n");
return 0;
}
// 在采集循环结束后调用
stop_streaming();
开发要点:
- 停止采集后,驱动将停止填充新数据
- 停止操作前,需确保所有缓冲区处理完毕
- 停止操作完成后,可安全释放缓冲区
6. 释放缓冲区(munmap)
概念知识:
取消内存映射:释放通过mmap映射的内存
munmap系统调用:取消之前mmap建立的映射关系
释放顺序:先停止采集,再释放缓冲区
释放所有缓冲区:需要为每个映射的缓冲区调用munmap
伪代码:
cpp
#include <sys/mman.h> // munmap需要此头文件
// 释放所有缓冲区
void unmap_buffers(void) {
unsigned int i;
// 循环释放每个缓冲区
for (i = 0; i < n_buffers; ++i) {
// 检查缓冲区是否已映射
if (buffers[i].start != MAP_FAILED && buffers[i].start != NULL) {
// munmap() 参数说明:
// - buffers[i].start: 映射的内存地址
// - buffers[i].length: 映射的长度
// 返回值: 成功返回0,失败返回-1
if (munmap(buffers[i].start, buffers[i].length) < 0) {
perror("munmap failed");
} else {
printf("Buffer %d unmapped\n", i);
}
// 清空指针,避免重复释放
buffers[i].start = NULL;
buffers[i].length = 0;
}
}
// 释放缓冲区数组
if (buffers) {
free(buffers);
buffers = NULL;
}
n_buffers = 0;
printf("All buffers released\n");
}
// 在停止采集后调用
stop_streaming();
unmap_buffers();
开发注意事项:
- 必须为每个映射的缓冲区执行munmap操作
- 操作前需验证指针有效性,防止重复释放
- 释放内存后应及时置空指针,避免产生野指针
- 操作顺序要求:先停止数据采集,再进行缓冲区释放
7. 关闭设备(close)
概念知识:
关闭设备文件:释放文件描述符
close系统调用:关闭打开的设备文件
资源清理:关闭后,设备可以被其他程序使用
清理顺序:先停止采集,再释放缓冲区,最后关闭设备
伪代码:
cpp
#include <unistd.h> // close需要此头文件
// 关闭设备
void close_device(void) {
if (fd >= 0) {
// close() 参数说明:
// - fd: 设备文件描述符
// 返回值: 成功返回0,失败返回-1
if (close(fd) < 0) {
perror("close device failed");
} else {
printf("Device closed\n");
}
fd = -1; // 清空文件描述符,避免重复关闭
}
}
// 完整的资源清理流程
void cleanup(void) {
// 1. 停止采集
stop_streaming();
// 2. 释放缓冲区
unmap_buffers();
// 3. 关闭设备
close_device();
printf("Cleanup completed\n");
}
开发要点:
- 设备关闭后,文件描述符将自动失效
- 关闭操作完成后应及时清空fd,防止出现重复关闭的情况
- 资源清理必须遵循以下顺序:
- 首先停止数据采集
- 然后释放缓冲区资源
- 最后关闭设备
1.5 完整示例代码
1.5.1 采集示例
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>
#include <stdlib.h>
#include <linux/videodev2.h>
#include <errno.h>
#include <unistd.h>
#include <sys/mman.h>
#define CAM_PATH "/dev/video0"
// 1. 定义用户空间缓冲区结构体(用于存储映射后的地址)
struct buffer
{
void *start; // 映射后的内存地址
size_t length; // 缓冲区长度
};
#define BUFFER_COUNT 4 // 申请4个缓冲区
struct buffer *buffers = NULL; // 用户空间缓冲区数组
unsigned int n_buffers = 0; // 实际分配的缓冲区数量
int fd;
// 完整的设备配置函数
static int configure_device(int width, int height)
{
struct v4l2_capability cap;
struct v4l2_format fmt;
// 1. 打开设备
// fd = open(CAM_PATH, O_RDWR | O_NONBLOCK, 0);
fd = open(CAM_PATH, O_RDWR, 0);
if (fd < 0)
{
fprintf(stderr, "Cannot open device %s: %s\n",
CAM_PATH, strerror(errno));
return -1;
}
// 2. 查询设备能力
memset(&cap, 0, sizeof(cap));
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0)
{
perror("VIDIOC_QUERYCAP failed");
close(fd);
return -1;
}
// 检查能力
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE))
{
fprintf(stderr, "Device does not support video capture\n");
close(fd);
return -1;
}
if (!(cap.capabilities & V4L2_CAP_STREAMING))
{
fprintf(stderr, "Device does not support streaming I/O\n");
close(fd);
return -1;
}
// 3. 枚举格式(可选)
struct v4l2_fmtdesc fmt_desc;
memset(&fmt_desc, 0, sizeof(fmt_desc));
fmt_desc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt_desc.index = 0;
printf("Supported formats:\n");
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt_desc) == 0)
{
printf(" [%d] %s (%.4s)\n",
fmt_desc.index,
fmt_desc.description,
(char *)&fmt_desc.pixelformat);
fmt_desc.index++;
}
// 4. 设置格式
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = width;
fmt.fmt.pix.height = height;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0)
{
perror("VIDIOC_S_FMT failed");
close(fd);
return -1;
}
printf("Format set: %dx%d, pixelformat: %.4s\n",
fmt.fmt.pix.width,
fmt.fmt.pix.height,
(char *)&fmt.fmt.pix.pixelformat);
return fd; // 返回文件描述符,供后续使用
}
// 完整的缓冲区初始化并启动采集函数
// 功能:整合申请缓冲区、查询缓冲区、映射缓冲区、加入队列、启动采集
// 参数:
// - buffer_count: 申请的缓冲区数量(建议4个)
// 返回值:成功返回0,失败返回-1
static int init_buffers_and_start(int buffer_count)
{
struct v4l2_requestbuffers req;
unsigned int i;
enum v4l2_buf_type type;
// ========== 步骤1:申请缓冲区 ==========
memset(&req, 0, sizeof(req));
req.count = buffer_count; // 申请指定数量的缓冲区
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 视频采集类型
req.memory = V4L2_MEMORY_MMAP; // 内存映射方式
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0)
{
perror("VIDIOC_REQBUFS failed");
return -1;
}
// 检查实际分配的缓冲区数量
if (req.count < 2)
{
fprintf(stderr, "Insufficient buffer memory (got %d, need at least 2)\n",
req.count);
return -1;
}
printf("Allocated %d buffers (requested %d)\n", req.count, buffer_count);
n_buffers = req.count; // 保存实际分配的缓冲区数量
// 分配用户空间缓冲区数组
buffers = calloc(n_buffers, sizeof(*buffers));
if (!buffers)
{
fprintf(stderr, "Out of memory\n");
return -1;
}
// ========== 步骤2:查询并映射每个缓冲区 ==========
for (i = 0; i < n_buffers; ++i)
{
struct v4l2_buffer buf; // 局部变量,每次循环都重新定义
// 查询缓冲区信息
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i; // 设置要查询的缓冲区索引
// ioctl是同步系统调用:设置buf.index作为输入,驱动填充buf的其他字段作为输出
// 调用完成后,buf中已经包含了驱动返回的完整信息(length, offset等)
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0)
{
perror("VIDIOC_QUERYBUF failed");
// 清理已分配的缓冲区
for (unsigned int j = 0; j < i; ++j)
{
if (buffers[j].start != MAP_FAILED)
{
munmap(buffers[j].start, buffers[j].length);
}
}
free(buffers);
buffers = NULL;
return -1;
}
// 重要:立即将buf中的信息保存到全局数组buffers[i]中
// 因为buf是局部变量,循环结束后会被销毁,但数据已经保存到buffers[i]了
buffers[i].length = buf.length; // 保存缓冲区长度
// 映射缓冲区到用户空间(使用buf.m.offset,这是驱动返回的偏移量)
buffers[i].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, buf.m.offset);
if (buffers[i].start == MAP_FAILED)
{
perror("mmap failed");
// 清理已分配的缓冲区
for (unsigned int j = 0; j < i; ++j)
{
if (buffers[j].start != MAP_FAILED)
{
munmap(buffers[j].start, buffers[j].length);
}
}
free(buffers);
buffers = NULL;
return -1;
}
printf("Buffer %d: mapped at %p, length=%zu, offset=0x%x\n",
i, buffers[i].start, buffers[i].length, buf.m.offset);
// 注意:这里buf是局部变量,循环结束后会自动销毁
// 但数据已经保存到buffers[i]中,所以完全没问题
}
// ========== 步骤3:将所有缓冲区加入队列 ==========
for (i = 0; i < n_buffers; ++i)
{
struct v4l2_buffer buf; // 局部变量,每次循环都重新定义
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i; // 指定要加入队列的缓冲区索引
// ioctl是同步系统调用:设置buf.index作为输入,驱动读取这个值
// 驱动内部会记录:缓冲区i已经加入队列
// 调用完成后,buf的内容不再需要,因为驱动已经记住了这个缓冲区
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0)
{
perror("VIDIOC_QBUF failed");
// 清理资源
for (unsigned int j = 0; j < n_buffers; ++j)
{
if (buffers[j].start != MAP_FAILED)
{
munmap(buffers[j].start, buffers[j].length);
}
}
free(buffers);
buffers = NULL;
return -1;
}
printf("Buffer %d queued\n", i);
// 注意:这里buf是局部变量,循环结束后会自动销毁
// 但驱动已经记住了缓冲区i在队列中,所以完全没问题
}
// ========== 步骤4:启动视频流 ==========
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0)
{
perror("VIDIOC_STREAMON failed");
// 清理资源
for (i = 0; i < n_buffers; ++i)
{
if (buffers[i].start != MAP_FAILED)
{
munmap(buffers[i].start, buffers[i].length);
}
}
free(buffers);
buffers = NULL;
return -1;
}
printf("Stream started, ready for capture\n");
return 0;
}
int start_capture(int cnt)
{
struct v4l2_buffer buf;
char filename[12];
int i = 0;
while (cnt--)
{
memset(&buf, 0, sizeof(buf));
memset(filename, 0, sizeof(filename));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0)
{
if (errno == EAGAIN)
{
// 非阻塞模式:没有数据可用
continue; // 继续等待下一帧
}
perror("VIDIOC_DQBUF failed");
return -1;
}
printf("index=%d,dataLength=%d\n", buf.index, buf.length);
int index = buf.index;
void *buffer_frame = buffers[buf.index].start;
int size = buf.length;
// 保存一张图片
sprintf(filename, "%d_.jpg", ++i);
FILE *fp = fopen(filename, "wb");
if (fp == NULL)
{
fprintf(stderr, "Cannot open file %s: %s\n", filename, strerror(errno));
// 即使保存失败,也要将缓冲区重新加入队列
}
else
{
size_t written = fwrite(buffer_frame, 1, size, fp);
fclose(fp);
if (written != size)
{
fprintf(stderr, "Failed to write all data to %s\n", filename);
}
else
{
printf("Saved frame to %s (%zu bytes)\n", filename, written);
}
}
// 重新让内核缓存入队,以备下一轮使用
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = index;
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0)
{
perror("VIDIOC_QBUF failed");
return -1;
}
// 这里是虚假sleep,用户空间每隔2s从底层拿到数据,本质上底层是没有sleep
sleep(2);
}
printf("Capture completed, saved %d frames\n", i);
return 0;
}
// 停止视频流
static int stop_streaming(void)
{
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0)
{
perror("VIDIOC_STREAMOFF failed");
return -1;
}
printf("Stream stopped\n");
return 0;
}
// 释放所有缓冲区
static void unmap_buffers(void)
{
unsigned int i;
if (buffers == NULL)
{
return;
}
// 循环释放每个缓冲区
for (i = 0; i < n_buffers; ++i)
{
// 检查缓冲区是否已映射
if (buffers[i].start != MAP_FAILED && buffers[i].start != NULL)
{
// 取消内存映射
if (munmap(buffers[i].start, buffers[i].length) < 0)
{
perror("munmap failed");
}
else
{
printf("Buffer %d unmapped\n", i);
}
// 清空指针,避免重复释放
buffers[i].start = NULL;
buffers[i].length = 0;
}
}
// 释放缓冲区数组
free(buffers);
buffers = NULL;
n_buffers = 0;
printf("All buffers released\n");
}
// 关闭设备
static void close_device(void)
{
if (fd >= 0)
{
if (close(fd) < 0)
{
perror("close device failed");
}
else
{
printf("Device closed\n");
}
fd = -1; // 清空文件描述符,避免重复关闭
}
}
// 完整的资源清理函数
static void cleanup(void)
{
// 1. 停止采集
stop_streaming();
// 2. 释放缓冲区
unmap_buffers();
// 3. 关闭设备
close_device();
printf("Cleanup completed\n");
}
int main()
{
int ret = 0;
// 1. 配置摄像头图像参数,分辨率,格式
fd = configure_device(640, 480);
if (fd < 0)
{
fprintf(stderr, "Failed to configure device\n");
return 1;
}
// 2. 申请缓冲区并完成用户控件缓存的映射,把内核缓冲区入队,启动视频数据采集
if (init_buffers_and_start(4) < 0)
{
fprintf(stderr, "Failed to initialize buffers\n");
close_device();
return 1;
}
// 3. 获得参数张图片
if (start_capture(5) < 0)
{
fprintf(stderr, "Failed to capture frames\n");
cleanup();
return 1;
}
// 4. 清理资源(按照正确顺序:停止采集 -> 释放缓冲区 -> 关闭设备)
cleanup();
return 0;
}