【Linux V4L2】视频采集流程全景笔记(ioctl 命令、mmap、缓冲区轮转、DVP vs MIPI)
本文整理自课堂板书,主题是 Linux 视频采集子系统 V4L2(Video for Linux 2):从 OV5640 摄像头怎么接到 SoC,到应用程序用九步 ioctl 流程拿到一帧图像,再到 mmap 零拷贝和缓冲区轮转的设计思想。适合复习摄像头驱动/应用开发,也可作为 V4L2 入门路线图。
| 项目 | 说明 |
|---|---|
| 摄像头模组 | OV5640(500W 像素,支持 DVP / MIPI 两种接口) |
| 平台 | IMX6ULL(CSI 接口),Linux 4.1.15 |
| 涉及层次 | 应用层 ioctl / 内核 V4L2 框架 / 传感器驱动 |
| 核心头文件 | linux/videodev2.h(所有结构体与命令宏都在这里) |
目录
- [一、V4L2 是什么:先看清整条数据通路](#一、V4L2 是什么:先看清整条数据通路)
- 二、采集九步流程总览
- 三、逐步拆解(含结构体)
- [四、缓冲区轮转:为什么是 4 块 buffer](#四、缓冲区轮转:为什么是 4 块 buffer)
- [五、mmap 内存映射详解](#五、mmap 内存映射详解)
- 六、完整示例代码
- [七、像素格式怎么选:YUV / RGB / MJPEG](#七、像素格式怎么选:YUV / RGB / MJPEG)
- [八、调试与 FAQ](#八、调试与 FAQ)
- 九、复习清单
一、V4L2 是什么:先看清整条数据通路
V4L2 是内核的视频采集框架 :向上给应用提供统一的 /dev/videoN 接口(open/read/ioctl),向下管理各种摄像头驱动。先看硬件通路,再谈软件流程。
!外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=images_v4l2%2Ffig1_camera_hw.png\&pos_id=img-BkEHYoXj-1792511975105
关键认知:摄像头和 SoC 之间是"两条路"------
- 控制路(慢):I2C/SCCB 总线,用来读写传感器的寄存器(配置分辨率、增益、曝光等),数据量小
- 数据路(快):图像像素流,走 DVP 并行接口或 MIPI CSI 串行接口,由 SoC 的 CSI 控制器接进内存
1.1 DVP vs MIPI CSI 对比
| 对比项 | DVP(并行) | MIPI CSI-2(串行) |
|---|---|---|
| 信号线 | PCLK + HREF/VSYNC + D0~D7(多根) | CLK± / D0± / D1±(差分对,线少) |
| 抗干扰 | 一般 | 强(差分传输) |
| 速率 | 低(几十 MHz) | 高(Gbps 级) |
| 典型场景 | 低分辨率、老平台、学习板 | 高分辨率、手机/产品 |
| IMX6ULL | CSI 控制器支持 | 部分型号支持 |
记忆点:MIPI = 差分 = 高速;DVP 引脚多、速率低但简单直观,教学板常用。
模组上还有一根 MCLK:SoC 提供给传感器的基准时钟,没有它传感器不工作------排查"摄像头点不亮"时先量它。
1.2 软件框架的三块基石
| 组成 | 说明 |
|---|---|
videodev2.h |
V4L2 的"字典":所有数据结构(v4l2_format、v4l2_buffer...)和命令宏(VIDIOC_xxx)都在这个头文件里,复习时对着它看最有效 |
/dev/videoN |
每个采集设备一个节点(/dev/video0...),应用层所有操作都围绕它 |
ioctl 万能接口 |
V4L2 没有独立的 read/write 风格 API ,一切操作(查询、设置格式、申请缓冲、启停流)都通过 ioctl(fd, VIDIOC_XXX, &arg) 完成,这是它和我们之前写的字符设备驱动最大的差异 |
二、采集九步流程总览
| 步骤 | ioctl 命令 | 作用 | 关键结构体 |
|---|---|---|---|
| 1 | open("/dev/video0", O_RDWR) |
打开设备节点 | --- |
| 2 | VIDIOC_QUERYCAP |
查询设备能力(是否支持采集流) | v4l2_capability |
| 3 | VIDIOC_S_FMT(G_FMT / ENUM_FMT) |
设置/读取/枚举图像格式(宽高+像素格式) | v4l2_format |
| 4 | VIDIOC_REQBUFS |
向内核申请 N 块缓冲区(MMAP 方式) | v4l2_requestbuffers |
| 5 | VIDIOC_QUERYBUF + mmap |
逐块查询缓冲区信息,映射到用户空间 | v4l2_buffer |
| 6 | VIDIOC_QBUF |
把所有空 buffer 入队交给驱动 | v4l2_buffer |
| 7 | VIDIOC_STREAMON |
启动采集流,传感器数据开始填充 buffer | v4l2_buf_type |
| 8 | VIDIOC_DQBUF → 处理 → VIDIOC_QBUF |
循环:出队一帧 → 处理 → 还回队列 | v4l2_buffer |
| 9 | VIDIOC_STREAMOFF |
停止采集,释放资源(munmap/close) | --- |
一句话框架:前面 1~7 步是"布置现场",第 8 步是"循环干活",第 9 步"收拾走人" 。所有命令都以 VIDIOC_ 开头,正是 Video IO Control 的缩写。
三、逐步拆解(含结构体)
3.1 打开设备 + 查询能力
c
#include <linux/videodev2.h>
int fd = open("/dev/video0", O_RDWR);
struct v4l2_capability cap;
ioctl(fd, VIDIOC_QUERYCAP, &cap);
printf("driver=%s card=%s\n", cap.driver, cap.card);
/* 必做的合理性检查:是否支持视频采集 */
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE))
fprintf(stderr, "不是采集设备\n");
3.2 设置图像格式(VIDIOC_S_FMT)
c
struct v4l2_format fmt = {0};
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; /* 四字符码,见第七节 */
ioctl(fd, VIDIOC_S_FMT, &fmt);
v4l2_pix_format 常用字段 |
含义 |
|---|---|
width / height |
分辨率 |
pixelformat |
像素格式四字符码(V4L2_PIX_FMT_YUYV、V4L2_PIX_FMT_MJPEG、V4L2_PIX_FMT_RGB565...) |
bytesperline |
一行字节数(内核按格式算好回填) |
sizeimage |
一帧总字节数(申请 buffer 大小的依据) |
想知道传感器支持哪些格式,用
VIDIOC_ENUM_FMT枚举一遍即可。
3.3 申请缓冲区(VIDIOC_REQBUFS)
c
struct v4l2_requestbuffers req = {0};
req.count = 4; /* 一般 3~4 块,见第四节 */
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP; /* 内核分配,应用映射 */
ioctl(fd, VIDIOC_REQBUFS, &req);
3.4 查询并映射(VIDIOC_QUERYBUF + mmap)
c
struct buffer { void *start; size_t length; } bufs[4];
for (int i = 0; i < 4; i++) {
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i; /* 第 i 块 */
ioctl(fd, VIDIOC_QUERYBUF, &buf); /* 查询长度和偏移 */
bufs[i].length = buf.length;
bufs[i].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE, MAP_SHARED,
fd, buf.m.offset);
}
3.5 入队 + 启动流(VIDIOC_QBUF / STREAMON)
c
for (int i = 0; i < 4; i++) {
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl(fd, VIDIOC_QBUF, &buf); /* 全部交给驱动 */
}
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMON, &type); /* 开始采集 */
3.6 采集循环(DQBUF → 处理 → QBUF)
c
for (;;) {
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_DQBUF, &buf); /* 阻塞直到一帧就绪 */
/* buf.index 指明是哪块 buffer,数据就在 bufs[buf.index].start */
process(bufs[buf.index].start, buf.bytesused);
ioctl(fd, VIDIOC_QBUF, &buf); /* 处理完立刻还回去 */
}
最容易忘的一步:处理完必须 QBUF 还回队列 。还回去的 buffer 会被驱动继续用来装下一帧------不还,几帧之后队列就空了,DQBUF 永久阻塞。
四、缓冲区轮转:为什么是 4 块 buffer
V4L2 的流式采集本质是一个生产者-消费者环形队列:
- 生产者:CSI 控制器/驱动,把传感器数据 DMA 写进"已入队"的空 buffer
- 消费者 :应用程序,
DQBUF取走满 buffer 处理,处理完QBUF归还 - 为什么申请 3~4 块 :驱动写 buffer A 的同时,应用还在处理 buffer B,读写并行,一帧都不丢;只有 1 块的话,处理期间新数据无处可写,只能丢帧
板书里的记忆图:入队 → buffer1 第一帧图像;出队处理 → buffer2 第二帧图像...... 四块 buffer 轮流值班,循环往复。
v4l2_buffer 出队后最常用的字段:
| 字段 | 含义 |
|---|---|
index |
是哪块 buffer(用它去自己的 bufs[] 数组取数据指针) |
bytesused |
这一帧实际有效字节数(处理时别超过它) |
flags |
状态标志(V4L2_BUF_FLAG_DONE 表示已填好数据) |
timestamp |
帧时间戳 |
五、mmap 内存映射详解
为什么要 mmap :普通 read() 要把数据从内核拷到用户缓冲区,一帧 640×480 YUV 就是 600KB,30fps 下每秒白拷 18MB。mmap 把内核 buffer 直接映射进应用的虚拟地址空间,两边看到的是同一块物理内存,零拷贝。
函数原型:
c
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
| 参数 | 传什么 | 说明 |
|---|---|---|
addr |
NULL |
映射起始地址,一般交由内核自动选择 |
length |
buf.length |
映射长度(QUERYBUF 查回来的) |
prot |
`PROT_READ | PROT_WRITE` |
flags |
MAP_SHARED |
共享映射(写回可见);MAP_PRIVATE 为私有写时复制,采集场景不用 |
fd |
设备描述符 | 要映射的文件/设备 |
offset |
buf.m.offset |
被映射对象内的偏移量,V4L2 里来自 QUERYBUF |
- 返回值 :成功返回映射区首地址(应用直接拿它当图像数据用),失败返回
MAP_FAILED(即(void *)-1,注意不是 NULL) - 用完记得
munmap(p, length)逐块解除映射
六、完整示例代码
把九步串起来,就是一段可以直接编译跑的采集骨架:
c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#define BUF_CNT 4
#define DEV "/dev/video0"
struct buffer { void *start; size_t length; };
static struct buffer bufs[BUF_CNT];
static void xioctl(int fd, unsigned long cmd, void *arg)
{
if (ioctl(fd, cmd, arg) < 0) {
perror("ioctl");
exit(1);
}
}
int main(void)
{
/* 1. 打开设备 */
int fd = open(DEV, O_RDWR);
/* 2. 查询能力 */
struct v4l2_capability cap;
xioctl(fd, VIDIOC_QUERYCAP, &cap);
printf("driver=%s card=%s\n", cap.driver, cap.card);
/* 3. 设置格式 */
struct v4l2_format fmt = {0};
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;
xioctl(fd, VIDIOC_S_FMT, &fmt);
printf("sizeimage=%u\n", fmt.fmt.pix.sizeimage);
/* 4. 申请缓冲区 */
struct v4l2_requestbuffers req = {0};
req.count = BUF_CNT;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
xioctl(fd, VIDIOC_REQBUFS, &req);
/* 5+6. 查询、映射、全部入队 */
for (int i = 0; i < BUF_CNT; i++) {
struct v4l2_buffer b = {0};
b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
b.memory = V4L2_MEMORY_MMAP;
b.index = i;
xioctl(fd, VIDIOC_QUERYBUF, &b);
bufs[i].length = b.length;
bufs[i].start = mmap(NULL, b.length,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, b.m.offset);
if (bufs[i].start == MAP_FAILED) { perror("mmap"); return 1; }
xioctl(fd, VIDIOC_QBUF, &b);
}
/* 7. 启动采集 */
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
xioctl(fd, VIDIOC_STREAMON, &type);
/* 8. 采集循环:DQBUF -> 处理 -> QBUF */
for (int n = 0; n < 100; n++) {
struct v4l2_buffer b = {0};
b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
b.memory = V4L2_MEMORY_MMAP;
xioctl(fd, VIDIOC_DQBUF, &b);
printf("frame %d: index=%u bytes=%u\n", n, b.index, b.bytesused);
/* process(bufs[b.index].start, b.bytesused); 显示/编码/保存 */
xioctl(fd, VIDIOC_QBUF, &b); /* 必须还回去 */
}
/* 9. 停止并释放 */
xioctl(fd, VIDIOC_STREAMOFF, &type);
for (int i = 0; i < BUF_CNT; i++)
munmap(bufs[i].start, bufs[i].length);
close(fd);
return 0;
}
编译运行:
bash
arm-linux-gnueabihf-gcc capture.c -o capture
./capture
七、像素格式怎么选:YUV / RGB / MJPEG
| 格式 | 四字符码 | 大小(一帧 640×480) | 特点 | 适用 |
|---|---|---|---|---|
| YUYV(YUV422) | V4L2_PIX_FMT_YUYV |
~600 KB | 传感器原生输出,每像素 2 字节 | 要自己做图像处理/算法 |
| RGB565 / RGB888 | V4L2_PIX_FMT_RGB565 等 |
~600 KB / ~900 KB | 可直接刷到 LCD framebuffer | 直接显示 |
| MJPEG | V4L2_PIX_FMT_MJPEG |
几十 KB(压缩) | 体积小,省带宽,需解码 | 网络传输、高帧率预览 |
- 板书结论:一般选 YUV(传感器原生、信息全),需要显示再转 RGB888
- YUV422 交错存储格式
Y0 U0 Y1 V1:两个 Y 共享一组 UV,人眼对亮度(Y)敏感、对色度(UV)不敏感,所以色度可以减半采样
八、调试与 FAQ
| 现象 | 可能原因 | 排查 |
|---|---|---|
/dev/video0 不存在 |
驱动没编/没加载,或摄像头没接 | `dmesg |
VIDIOC_QUERYCAP 失败 |
打开的不是 V4L2 设备 | v4l2-ctl --all 看设备信息 |
S_FMT 后宽高变了 |
传感器不支持该分辨率,内核自动就近调整 | 以 G_FMT 读回来的为准 |
DQBUF 一直阻塞 |
没入队足够的 buffer / 没 STREAMON / 数据线没接好 | 检查第 6、7 步顺序;量 MCLK 和 PCLK |
| 图像全绿/全灰 | 像素格式与实际不符(如 YUYV 当 RGB 显示) | 用 ENUM_FMT 确认格式;YUV 转 RGB 再显示 |
| 图像撕裂 | 处理太久还 buffer 不及时 | 把处理挪到另一线程;增加 buffer 数 |
| 花屏/错位 | bytesperline 没按 stride 处理 |
一行不是 width×2 就可能有对齐 padding |
| 没有摄像头想学流程 | 内核自带 vivid 虚拟摄像头驱动 | 加载 vivid 后同样有 /dev/video0,流程完全一致 |
无驱动的快速验证工具:
bash
v4l2-ctl --list-devices # 列出采集设备
v4l2-ctl -d /dev/video0 --all # 查看能力/格式
v4l2-ctl --list-formats-ext -d /dev/video0 # 枚举支持的格式和分辨率
dmesg | tail # 驱动加载日志
九、复习清单
- 能画出摄像头到 SoC 的两条路:控制走 I2C/SCCB,数据走 DVP/MIPI,并解释 MCLK 的作用
- 能对比 DVP 和 MIPI CSI(线数、差分、速率、场景)
- 能说出 V4L2 的三大基石:
videodev2.h、/dev/videoN、ioctl 万能接口 - 能默写采集九步:open → QUERYCAP → S_FMT → REQBUFS → QUERYBUF+mmap → QBUF → STREAMON → 循环(DQBUF→处理→QBUF) → STREAMOFF
- 能解释为什么申请 3~4 块 buffer(读写并行不丢帧)
- 能默写 mmap 六个参数及返回值,说清"零拷贝"省在哪
- 知道
v4l2_buffer的index / bytesused怎么用 - 知道 YUYV 的存储格式和 YUV→RGB 为什么要转
- 知道忘写 QBUF 会发生什么(队列耗尽、DQBUF 永久阻塞)
- 会用
v4l2-ctl在写代码前验证设备和格式
写在最后 :V4L2 的九步流程看似命令很多,其实只有三件事------谈好格式(S_FMT)、摆好缓冲区(REQBUFS/mmap/QBUF)、开始流水线(STREAMON + 循环) 。把它和之前写的字符设备驱动对照着看:ioctl 就是
file_operations.unlocked_ioctl的具体应用,buffer 队列就是内核与应用之间的生产者-消费者模型------新框架只是老知识的重新组合。
标签 :Linux V4L2 摄像头 驱动开发 嵌入式 IMX6ULL