【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 是什么:先看清整条数据通路
- 二、采集九步流程总览
- 三、逐步拆解(含结构体)
- 四、缓冲区轮转:为什么是 4 块 buffer
- 五、mmap 内存映射详解
- 六、完整示例代码
- 七、像素格式怎么选:YUV / RGB / MJPEG
- 八、调试与 FAQ
- 九、复习清单
一、V4L2 是什么:先看清整条数据通路
V4L2 是内核的视频采集框架:向上给应用提供统一的/dev/videoN接口(open/read/ioctl),向下管理各种摄像头驱动。先看硬件通路,再谈软件流程。
: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 打开设备 + 查询能力
#include<linux/videodev2.h>intfd=open("/dev/video0",O_RDWR);structv4l2_capabilitycap;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)
structv4l2_formatfmt={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)
structv4l2_requestbuffersreq={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)
structbuffer{void*start;size_tlength;}bufs[4];for(inti=0;i<4;i++){structv4l2_bufferbuf={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)
for(inti=0;i<4;i++){structv4l2_bufferbuf={0};buf.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory=V4L2_MEMORY_MMAP;buf.index=i;ioctl(fd,VIDIOC_QBUF,&buf);/* 全部交给驱动 */}enumv4l2_buf_typetype=V4L2_BUF_TYPE_VIDEO_CAPTURE;ioctl(fd,VIDIOC_STREAMON,&type);/* 开始采集 */3.6 采集循环(DQBUF → 处理 → QBUF)
for(;;){structv4l2_bufferbuf={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 直接映射进应用的虚拟地址空间,两边看到的是同一块物理内存,零拷贝。
函数原型:
void*mmap(void*addr,size_tlength,intprot,intflags,intfd,off_toffset);| 参数 | 传什么 | 说明 |
|---|---|---|
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)逐块解除映射
六、完整示例代码
把九步串起来,就是一段可以直接编译跑的采集骨架:
#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>#defineBUF_CNT4#defineDEV"/dev/video0"structbuffer{void*start;size_tlength;};staticstructbufferbufs[BUF_CNT];staticvoidxioctl(intfd,unsignedlongcmd,void*arg){if(ioctl(fd,cmd,arg)<0){perror("ioctl");exit(1);}}intmain(void){/* 1. 打开设备 */intfd=open(DEV,O_RDWR);/* 2. 查询能力 */structv4l2_capabilitycap;xioctl(fd,VIDIOC_QUERYCAP,&cap);printf("driver=%s card=%s\n",cap.driver,cap.card);/* 3. 设置格式 */structv4l2_formatfmt={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. 申请缓冲区 */structv4l2_requestbuffersreq={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(inti=0;i<BUF_CNT;i++){structv4l2_bufferb={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");return1;}xioctl(fd,VIDIOC_QBUF,&b);}/* 7. 启动采集 */enumv4l2_buf_typetype=V4L2_BUF_TYPE_VIDEO_CAPTURE;xioctl(fd,VIDIOC_STREAMON,&type);/* 8. 采集循环:DQBUF -> 处理 -> QBUF */for(intn=0;n<100;n++){structv4l2_bufferb={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(inti=0;i<BUF_CNT;i++)munmap(bufs[i].start,bufs[i].length);close(fd);return0;}编译运行:
arm-linux-gnueabihf-gcc capture.c-ocapture ./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,流程完全一致 |
无驱动的快速验证工具:
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 队列就是内核与应用之间的生产者-消费者模型——新框架只是老知识的重新组合。
标签:LinuxV4L2摄像头驱动开发嵌入式IMX6ULL