纸翼 · 加载中
2669 words
13 minutes
智能猫眼项目技术详解(v4l2框架的使用)

V4L2框架用法笔记

V4L2框架用法笔记#

V4L2(Video for Linux 2)是Linux系统下用于视频采集的标准框架,主要用于摄像头等视频设备的应用层开发(非驱动开发),核心是通过标准接口与摄像头驱动通信,实现画面采集、显示、拍照、录像等功能。本文结合实际代码,梳理V4L2框架的完整用法、核心函数及关键细节。

一、V4L2核心定位与开发场景#

  1. 定位:应用层与摄像头驱动的“通信桥梁”,驱动已由内核实现(如uvcvideo驱动),我们只需通过V4L2标准接口调用驱动,无需操作硬件寄存器。
  2. 开发场景:嵌入式Linux摄像头采集、预览、拍照、录像、推流等(本文基于MMAP内存映射模式,效率最高、最常用)。
  3. 核心原则:所有操作通过「ioctl函数+标准结构体」实现,驱动只识别V4L2标准接口,不识别自定义变量/结构体。

二、V4L2完整采集流程(必记)#

整个流程从设备初始化到采集结束,按顺序执行,缺一不可,对应代码中核心函数的调用顺序:

  1. 打开摄像头设备(open)
  2. 设置摄像头格式(分辨率、图像格式)→ v4l2_set_format
  3. 向驱动申请帧缓冲区 → v4l2_request_buffers
  4. 将内核缓冲区映射到应用层 → v4l2_mmap_buffers
  5. 将所有缓冲区放入驱动队列(QBUF)
  6. 开启视频流采集 → VIDIOC_STREAMON
  7. 循环采集:等待数据(select)→ 取帧(DQBUF)→ 处理画面(显示/拍照/录像)→ 还帧(QBUF)
  8. 停止视频流(VIDIOC_STREAMOFF)
  9. 释放资源(解除映射、关闭设备)

三、核心函数详解(结合实际代码)#

以下函数均为应用层核心函数,对应流程中的关键步骤,逐函数解析功能、参数及作用。

1. 打开/关闭摄像头设备(基础操作)#

// 打开摄像头(只读/读写模式)
g_ui.v4l2_fd = open("/dev/video0", O_RDWR);
// 关闭摄像头(资源释放)
close(g_ui.v4l2_fd);

关键:/dev/video0是摄像头设备节点,不同摄像头节点号可能不同(video1、video2等);打开失败会返回-1,需做错误处理。

2. 设置视频格式 → v4l2_set_format#

功能:告诉驱动,我们需要的画面分辨率、图像格式,驱动会返回实际支持的参数(避免设置不支持的分辨率)。

static int v4l2_set_format(int width, int height)
{
struct v4l2_format fmt;
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; // 图像格式:YUYV(裸数据、低延迟)
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; // 固定:逐行扫描
// 向驱动发送“设置格式”命令
if (ioctl(g_ui.v4l2_fd, VIDIOC_S_FMT, &fmt) < 0) {
LV_LOG_ERROR("Failed to set format: %s", strerror(errno));
return -1;
}
// 保存驱动实际返回的分辨率(可能与期望不同)
g_ui.v4l2_width = fmt.fmt.pix.width;
g_ui.v4l2_height = fmt.fmt.pix.height;
LV_LOG_INFO("V4L2 format set: %dx%d (YUYV)", g_ui.v4l2_width, g_ui.v4l2_height);
return 0;
}

关键:① 图像格式常用YUYV(无压缩),需与后续处理(显示、转码)一致;② 必须保存驱动返回的实际分辨率,后续操作需用此参数。

3. 申请帧缓冲区 → v4l2_request_buffers#

功能:向驱动申请指定数量的帧缓冲区(内存块),用于存放摄像头采集的画面,缓冲区数量一般设为4(循环使用,保证流畅)。

static int v4l2_request_buffers(void)
{
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));
req.count = V4L2_BUFFERS_COUNT; // 申请的缓冲区数量(如4)
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 视频采集类型
req.memory = V4L2_MEMORY_MMAP; // 内存模式:MMAP(零拷贝)
// 向驱动发送“申请缓冲区”命令
if (ioctl(g_ui.v4l2_fd, VIDIOC_REQBUFS, &req) < 0) {
LV_LOG_ERROR("Failed to request buffers: %s", strerror(errno));
return -1;
}
// 保存驱动实际分配的缓冲区数量(可能少于申请数量)
g_ui.v4l2_buffer_count = req.count;
LV_LOG_INFO("V4L2 buffers requested: %d", g_ui.v4l2_buffer_count);
return 0;
}

关键:① MMAP模式是V4L2效率最高的模式,无需拷贝数据;② 缓冲区数量固定,后续需循环使用、及时归还。

4. 内存映射 → v4l2_mmap_buffers#

功能:将驱动内核空间的帧缓冲区,映射到应用层地址空间,让应用程序能直接读写缓冲区(拿到画面数据),是V4L2核心步骤。

static int v4l2_mmap_buffers(void)
{
unsigned int i;
// 循环映射每一个缓冲区
for (i = 0; i < g_ui.v4l2_buffer_count; 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; // 要查询的第i号缓冲区
// 第一步:查询第i号缓冲区的信息(大小、内核偏移地址)
if (ioctl(g_ui.v4l2_fd, VIDIOC_QUERYBUF, &buf) < 0) {
LV_LOG_ERROR("Failed to query buffer %d: %s", i, strerror(errno));
return -1;
}
// 第二步:将内核缓冲区映射到应用层,直接操作指向内核空间的指针
g_ui.v4l2_buffers[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,
MAP_SHARED, g_ui.v4l2_fd, buf.m.offset);
// 映射失败判断
if (g_ui.v4l2_buffers[i] == MAP_FAILED) {
LV_LOG_ERROR("Failed to mmap buffer %d: %s", i, strerror(errno));
return -1;
}
}
LV_LOG_INFO("V4L2 buffers mmapped");
return 0;
}

关键细节:

  • VIDIOC_QUERYBUF:查询缓冲区信息,获取缓冲区大小(buf.length)和内核偏移地址(buf.m.offset),为映射做准备;
  • mmap参数:PROT_READ | PROT_WRITE(可读可写)、MAP_SHARED(共享内存,驱动和应用可同时访问);
  • 映射后,g_ui.v4l2_buffers[i] 是应用层指针,直接指向画面数据,后续显示、拍照均用此指针。

5. 开启/停止视频流#

功能:控制摄像头开始/停止采集画面,开启后驱动才会真正向缓冲区填充数据。

// 开启视频流
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(g_ui.v4l2_fd, VIDIOC_STREAMON, &type) < 0) {
LV_LOG_ERROR("Failed to start stream");
return -1;
}
// 停止视频流(采集结束时调用)
ioctl(g_ui.v4l2_fd, VIDIOC_STREAMOFF, &type);

关键:必须在“缓冲区映射+入队”完成后,才能开启视频流;停止流后,需释放资源。

6. 循环采集(核心业务逻辑)#

开启视频流后,通过循环实现持续采集,核心是“等待数据→取帧→处理→还帧”,配合select实现高效等待(不浪费CPU)。

// 循环采集逻辑(一般在单独线程中执行)
while (g_ui.v4l2_thread_running) {
fd_set fds;
struct timeval tv;
// 1. 初始化监听集合,等待摄像头数据
FD_ZERO(&fds); // 清空监听列表
FD_SET(g_ui.v4l2_fd, &fds); // 监听摄像头文件描述符
tv.tv_sec = 2; // 超时时间2秒
tv.tv_usec = 0;
// 等待摄像头有数据(阻塞,无数据则休眠)
int r = select(g_ui.v4l2_fd + 1, &fds, NULL, NULL, &tv);//监听文件描述符集合里的内容
if (r < 0) break; // 出错退出
if (r == 0) continue; // 超时,继续等待
// 2. 取帧(DQBUF):从驱动队列取出一帧就绪画面
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (ioctl(g_ui.v4l2_fd, VIDIOC_DQBUF, &buf) < 0) {
LV_LOG_ERROR("DQBUF error: %s", strerror(errno));
break;
}
// 3. 处理画面(显示、拍照、录像、推流)
if (g_ui.display_enabled) {
// 显示画面:传入画面数据指针和数据大小
display_frame(g_ui.v4l2_buffers[buf.index], buf.bytesused);
}
if (g_ui.photo_requested) {
// 拍照:保存当前帧,用ffmpeg转成JPG
save_photo(g_ui.v4l2_buffers[buf.index], buf.bytesused);
}
if (g_ui.record_requested && g_ui.record_file) {
// 录像:将当前帧写入文件
fwrite(g_ui.v4l2_buffers[buf.index], 1, buf.bytesused, g_ui.record_file);
fflush(g_ui.record_file); // 强制写入磁盘,避免数据丢失
}
// 4. 还帧(QBUF):将用完的缓冲区还给驱动,循环使用
if (ioctl(g_ui.v4l2_fd, VIDIOC_QBUF, &buf) < 0) {
LV_LOG_ERROR("QBUF error: %s", strerror(errno));
break;
}
}

关键细节:

  • select:监听摄像头数据,无数据时休眠,有数据时唤醒,避免死循环占用CPU;
  • VIDIOC_DQBUF:取帧,驱动会返回就绪缓冲区的索引(buf.index),通过该索引找到画面数据(g_ui.v4l2_buffers[buf.index]);
  • VIDIOC_QBUF:还帧,必须执行!缓冲区数量固定,不归还会导致缓冲区耗尽,摄像头卡死;
  • buf.bytesused:当前帧的实际数据大小,用于显示、写入文件时避免越界。

7. 资源释放(收尾操作)#

采集结束后,需按顺序释放资源,避免内存泄漏、设备占用。

// 停止视频流
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(g_ui.v4l2_fd, VIDIOC_STREAMOFF, &type);
// 解除内存映射
for (int i = 0; i < g_ui.v4l2_buffer_count; i++) {
munmap(g_ui.v4l2_buffers[i], buf.length);
}
// 关闭摄像头设备
close(g_ui.v4l2_fd);

8. 具体用法#

void start_camera(void)
{
if (g_ui.v4l2_thread_running) {
LV_LOG_WARN("Camera already running");
return;
}
ensure_photo_dir();
init_v4l2_flags();
int width = get_resolution_width(g_ui.resolution);
int height = get_resolution_height(g_ui.resolution);
/* 1. 打开 V4L2 设备 */
if (v4l2_open_device() < 0) {
return;
}
/* 2. 设置格式 */
if (v4l2_set_format(width, height) < 0) {
v4l2_close_device();
return;
}
/* 3. 申请缓冲区 */
if (v4l2_request_buffers() < 0) {
v4l2_close_device();
return;
}
/* 4. mmap 缓冲区 */
if (v4l2_mmap_buffers() < 0) {
v4l2_unmap_buffers();
v4l2_close_device();
return;
}
/* 5. 把缓冲区放入队列 */
if (v4l2_queue_buffers() < 0) {
v4l2_unmap_buffers();
v4l2_close_device();
return;
}
/* 6. 开始流采集 */
if (v4l2_start_streaming() < 0) {
v4l2_unmap_buffers();
v4l2_close_device();
return;
}
/* 7. 启动采集线程 */
g_ui.v4l2_thread_running = true;
/* 启用画面显示 */
g_ui.display_enabled = true;
if (pthread_create(&g_ui.v4l2_thread, NULL, v4l2_capture_thread, NULL) != 0) {
LV_LOG_ERROR("Failed to create capture thread");
g_ui.v4l2_thread_running = false;
g_ui.display_enabled = false;
v4l2_stop_streaming();
v4l2_unmap_buffers();
v4l2_close_device();
return;
}
LV_LOG_INFO("Camera started (V4L2)");
}

四、核心结构体说明#

V4L2的所有操作都依赖标准结构体,重点掌握3个核心结构体,均来自头文件 <linux/videodev2.h>。

1. struct v4l2_format#

用途:设置/获取视频格式(分辨率、图像格式),核心成员:

  • type:固定为V4L2_BUF_TYPE_VIDEO_CAPTURE(视频采集);
  • fmt.pix.width/height:画面宽高;
  • fmt.pix.pixelformat:图像格式(如V4L2_PIX_FMT_YUYV);
  • fmt.pix.field:扫描方式(固定为V4L2_FIELD_INTERLACED)。

2. struct v4l2_requestbuffers#

用途:向驱动申请缓冲区,核心成员:

  • count:申请的缓冲区数量;
  • type:V4L2_BUF_TYPE_VIDEO_CAPTURE;
  • memory:V4L2_MEMORY_MMAP(MMAP模式)。

3. struct v4l2_buffer#

用途:最常用的“万能传话筒”,不同场景下使用不同成员,核心场景:

  • 查询缓冲区(VIDIOC_QUERYBUF):用index(缓冲区索引)、length(缓冲区大小)、m.offset(内核偏移);
  • 取帧/还帧(DQBUF/QBUF):用index(就绪缓冲区索引)、bytesused(当前帧大小)。

五、关键注意事项(避坑重点)#

  1. 所有V4L2结构体使用前必须用memset清零,避免旧数据导致驱动误判;
  2. DQBUF和QBUF必须成对出现,取帧后必须归还,否则缓冲区耗尽,摄像头卡死;
  3. 设置分辨率时,驱动可能返回与期望不同的参数,必须保存实际返回的宽高;
  4. mmap映射后,必须用munmap解除映射,否则会造成内存泄漏;
  5. 采集线程中,用select实现高效等待,避免死循环占用CPU;
  6. 录像时,用fflush强制将数据写入磁盘,避免掉电、程序崩溃导致视频损坏;
  7. 所有ioctl操作都要做错误判断,确保程序健壮性。

六、总结#

V4L2框架的核心逻辑是“通过标准接口与驱动通信,实现缓冲区的循环使用”,核心流程可简化为: 初始化(打开设备→设置格式→申请缓冲区→映射内存)→ 开启采集→循环(等待→取帧→处理→还帧)→ 停止采集→释放资源。 掌握上述流程和核心函数,就能实现Linux下摄像头的基础采集、显示、拍照、录像等功能,后续可基于此扩展推流、编码等更复杂的需求。

智能猫眼项目技术详解(v4l2框架的使用)
https://blog.huangzy.xyz/posts/智能猫眼项目技术详解v4l2框架的使用/
Author
纸翼
Published at
2026-03-29
License
CC BY-NC-SA 4.0

Some information may be outdated