当前位置: 首页 > news >正文

嵌入式 Linux V4L2 摄像头采集编程(MMAP 方式)(四)—— 从零到一,含全部宏详解与框架图

嵌入式 Linux V4L2 摄像头采集编程(MMAP 方式)(四)—— 从零到一,含全部宏详解与框架图

适用平台:IMX6ULL / 任何支持 V4L2 的嵌入式板卡
编译器:arm-buildroot-linux-gnueabihf-gcc
摄像头:USB 或 CSI,支持 MJPEG / YUYV


1. 前言:你为什么需要这篇博客?

很多人在写 V4L2 程序时,只是抄一段代码,却搞不懂VIDIOC_QUERYCAP的作用,不知道cap.capabilities为什么要用&运算,不理解mmap的 offset 从哪里来,甚至不知道ioctl成功返回 0、失败返回 -1。
这篇博客将逐行解释代码纠正常见误解,让你不仅能运行,还能在面试中从容应答。


图1:初始化阶段

图2:采集循环阶段

图3:清理阶段

2. V4L2 完整框架图(主干流程)

text

+-------------------+ | open(/dev/videoX) | +-------------------+ ↓ +-------------------------------+ | VIDIOC_QUERYCAP | → 查询驱动能力 | (检查 V4L2_CAP_VIDEO_CAPTURE | 是否支持捕获 | 和 V4L2_CAP_STREAMING) | +-------------------------------+ ↓ +-------------------------------+ | VIDIOC_ENUM_FMT (可选) | → 枚举所有像素格式 | └─ 每个格式再 VIDIOC_ENUM_ | 每种格式支持的分辨率 | FRAMESIZES | +-------------------------------+ ↓ +-------------------------------+ | VIDIOC_S_FMT | → 设置最终格式 (宽、高、像素格式) | (实际宽高可能被驱动调整) | +-------------------------------+ ↓ +-------------------------------+ | VIDIOC_REQBUFS | → 申请缓冲区 (count 个) | (memory = V4L2_MEMORY_MMAP) | +-------------------------------+ ↓ +-------------------------------+ | 循环: i=0..rb.count-1 | | ├─ VIDIOC_QUERYBUF | → 获取每个 buffer 的长度和 offset | └─ mmap(...) | → 映射到用户空间 +-------------------------------+ ↓ +-------------------------------+ | 循环: i=0..rb.count-1 | | └─ VIDIOC_QBUF | → 所有 buffer 入队 (放入空闲链表) +-------------------------------+ ↓ +-------------------------------+ | VIDIOC_STREAMON | → 启动视频流 +-------------------------------+ ↓ +-----------------+ | while(1) 循环 | +-----------------+ ↓ +-------------------------------+ | poll() 等待 POLLIN | → 等待数据可读 | (也可用 select) | +-------------------------------+ ↓ +-------------------------------+ | VIDIOC_DQBUF | → 从完成链表取出一个填好数据的 buffer | (获得 index 和 bytesused) | +-------------------------------+ ↓ +-------------------------------+ | 处理数据:写入文件等 | → 对于 MJPEG,直接存 .jpg +-------------------------------+ ↓ +-------------------------------+ | VIDIOC_QBUF (重新入队) | → 将该 buffer 放回空闲链表 +-------------------------------+ ↓ (循环) ↓ +-------------------------------+ | VIDIOC_STREAMOFF | → 停止流 +-------------------------------+ ↓ +-------------------------------+ | munmap() + close() | → 释放资源 +-------------------------------+

3. 完整代码(带极细注释)

c

#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/ioctl.h> #include <unistd.h> #include <stdio.h> #include <string.h> #include <linux/videodev2.h> // V4L2 核心头文件,定义所有宏和结构体 #include <poll.h> // poll() 函数 #include <sys/mman.h> // mmap() 函数 /* 编译:arm-buildroot-linux-gnueabihf-gcc -o video_test video_test.c */ int main(int argc, char **argv) { // ---------- 变量声明 ---------- int fd; // 设备文件描述符 struct v4l2_capability cap; // 存储驱动能力 struct v4l2_fmtdesc fmtdesc; // 格式描述符,用于枚举格式 struct v4l2_frmsizeenum fsenum; // 帧大小枚举结构 struct v4l2_format fmt; // 格式设置结构 struct v4l2_requestbuffers rb; // 申请缓冲区参数 struct v4l2_buffer buf; // 单个缓冲区信息 struct pollfd fds[1]; // poll 监听的 fd 集合 void *buffers[32]; // 保存 mmap 映射的地址(最多32个) int buf_cnt; // 实际申请的 buffer 数量 char filename[32]; // 保存文件名 int file_cnt = 0; // 文件序号 int i; // ========================= 1. 检查命令行参数 ========================= if (argc != 2) { printf("Usage: %s <video device> (e.g. ./video_test /dev/video1)\n", argv[0]); return -1; } // ========================= 2. 打开设备 ========================= // O_RDWR:读写模式,V4L2 一般都要求读写权限 fd = open(argv[1], O_RDWR); if (fd < 0) { perror("open device"); return -1; } // ========================= 3. 查询能力 (VIDIOC_QUERYCAP) ========================= memset(&cap, 0, sizeof(cap)); // 必须清零,否则可能残留垃圾数据导致 ioctl 失败 // ioctl 成功返回 0,失败返回 -1 (并设置 errno) if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) { perror("VIDIOC_QUERYCAP"); close(fd); return -1; } // 【纠正常见误解】cap.capabilities 是一个位掩码,不是整数等于某个值 // 要用按位与 (&) 检查特定标志位,而不是 if(cap.capabilities == 0) if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { fprintf(stderr, "Error: %s does not support video capture.\n", argv[1]); close(fd); return -1; } // 检查是否支持流式 I/O (即 MMAP 方式) if (!(cap.capabilities & V4L2_CAP_STREAMING)) { fprintf(stderr, "Error: %s does not support streaming I/O.\n", argv[1]); close(fd); return -1; } printf("Device: %s\n", cap.card); // 设备名称 printf("Driver: %s\n", cap.driver); // 驱动名称 // ========================= 4. 枚举所有支持的格式和分辨率(调试用,非必须) ========================= // 结构体 v4l2_fmtdesc 需要设置 type 为 V4L2_BUF_TYPE_VIDEO_CAPTURE fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 从 index=0 开始,逐一枚举,直到 ioctl 返回 -1(通常 errno == EINVAL 表示结束) for (fmtdesc.index = 0; ; fmtdesc.index++) { if (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) < 0) break; // 枚举结束,正常退出循环 printf("format %s, fourcc=0x%x\n", fmtdesc.description, fmtdesc.pixelformat); // 对于每一张格式,枚举其支持的分辨率 fsenum.pixel_format = fmtdesc.pixelformat; for (fsenum.index = 0; ; fsenum.index++) { if (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &fsenum) < 0) break; // fsenum.discrete.width 和 height 只有在 type 为 V4L2_FRMSIZE_TYPE_DISCRETE 时才有效 // 大多数摄像头都是离散分辨率,这里假设就是离散类型 printf(" framesize %d: %d x %d\n", fsenum.index, fsenum.discrete.width, fsenum.discrete.height); } } // ========================= 5. 设置格式 (VIDIOC_S_FMT) ========================= memset(&fmt, 0, sizeof(fmt)); fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = 1280; // 期望宽度 fmt.fmt.pix.height = 720; // 期望高度 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; // 期望像素格式 (FourCC 码) fmt.fmt.pix.field = V4L2_FIELD_ANY; // 场序:任意,一般摄像头都是逐行 if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) { perror("VIDIOC_S_FMT"); close(fd); return -1; } // 【重要】调用返回后,fmt 结构体中的值可能被驱动修改! // 例如:摄像头不支持 1280x720 可能会改成 800x600,不支持 MJPEG 可能改回 YUYV printf("set format ok: %d x %d, fourcc=0x%x\n", fmt.fmt.pix.width, fmt.fmt.pix.height, fmt.fmt.pix.pixelformat); // ========================= 6. 申请缓冲区 (VIDIOC_REQBUFS) ========================= memset(&rb, 0, sizeof(rb)); rb.count = 32; // 想要申请多少个 buffer rb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; rb.memory = V4L2_MEMORY_MMAP; // 使用内存映射方式 if (ioctl(fd, VIDIOC_REQBUFS, &rb) < 0) { perror("VIDIOC_REQBUFS"); close(fd); return -1; } // 驱动可能无法满足 count 的数量,会修改 rb.count 为实际分配的数量 buf_cnt = rb.count; printf("requested %d buffers, got %d\n", 32, buf_cnt); // ========================= 7. 查询每个 buffer 并 mmap 映射 ========================= for (i = 0; i < buf_cnt; i++) { memset(&buf, 0, sizeof(buf)); buf.index = i; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; // 查询 buffer 信息:获得长度 length 和物理偏移 offset if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) { perror("VIDIOC_QUERYBUF"); // 出错需要释放已经 mmap 的内存,为简明暂略 close(fd); return -1; } // mmap 映射:将内核空间的 buffer 映射到用户空间 // 参数:NULL(自动选择地址), buf.length(长度), PROT_READ|PROT_WRITE(可读可写), // MAP_SHARED(共享,其他进程可见), fd, buf.m.offset(物理偏移) buffers[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if (buffers[i] == MAP_FAILED) { perror("mmap"); close(fd); return -1; } printf("buffer %d mapped, length=%d\n", i, buf.length); } printf("all buffers mapped successfully\n"); // ========================= 8. 将所有 buffer 放入输入队列 (VIDIOC_QBUF) ========================= for (i = 0; i < buf_cnt; i++) { memset(&buf, 0, sizeof(buf)); buf.index = i; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; // 入队:告诉驱动可以往这个 buffer 里填数据 if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) { perror("VIDIOC_QBUF"); close(fd); return -1; } } printf("all buffers queued\n"); // ========================= 9. 启动视频流 (VIDIOC_STREAMON) ========================= int type_capture = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (ioctl(fd, VIDIOC_STREAMON, &type_capture) < 0) { perror("VIDIOC_STREAMON"); return -1; } printf("stream started, capturing...\n"); // ========================= 10. 循环采集数据 ========================= while (1) { // 使用 poll 等待设备有数据可读 fds[0].fd = fd; fds[0].events = POLLIN; // 等待可读事件 // 第3个参数 -1 表示无限等待,也可设置超时(毫秒) if (poll(fds, 1, -1) < 0) { perror("poll"); break; } // 【纠正】poll 返回后必须检查 revents 是否为 POLLIN,避免错误事件 if (fds[0].revents & POLLIN) { // 取出一个已经填好数据的 buffer memset(&buf, 0, sizeof(buf)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) { perror("VIDIOC_DQBUF"); break; } // 此时 buf.index 是哪个 buffer 有数据,buf.bytesused 是有效数据长度 sprintf(filename, "video_raw_data_%04d.jpg", file_cnt++); int fd_file = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd_file < 0) { perror("create file"); } else { // 将映射地址中的数据写入文件,长度是 buf.bytesused write(fd_file, buffers[buf.index], buf.bytesused); close(fd_file); printf("captured %s, size=%d bytes\n", filename, buf.bytesused); } // 将该 buffer 重新放入输入队列,以便驱动再次使用 if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) { perror("VIDIOC_QBUF (re-queue)"); break; } } } // ========================= 11. 停止流并释放资源 ========================= ioctl(fd, VIDIOC_STREAMOFF, &type_capture); for (i = 0; i < buf_cnt; i++) { // 需要知道每个 buffer 的长度,简单的做法是保存每个 buffer 的 length // 这里仅示意,实际项目中需保存每个 buffer 的 length 数组 // munmap(buffers[i], length_array[i]); } close(fd); return 0; }

4. 运行结果展示

说明

  • 枚举结果中1448695129是 FourCC'YUYV'的十进制表示,1196444237'MJPEG'
  • 设置格式时我们要求 1280×720 MJPEG,驱动返回成功并确认格式为 1280×720(说明摄像头支持该分辨率)。
  • 采集的图片直接是 JPEG 文件,可以复制到电脑查看。

5. 常见误区纠正(结合你之前的口述)

你的口述(有误)正确理解
“ioctl 返回不等于0表示有错误”错误。ioctl成功返回0,失败返回 -1。所以应该用if(ioctl(...) < 0)判断失败。
“检查 cap.capabilities 是否等于 0 来判断不支持”错误。cap.capabilities 是位掩码,要用&检查特定 bit,例如cap.capabilities & V4L2_CAP_VIDEO_CAPTURE
“枚举格式时 while 循环里 ioctl 返回不为0就表示函数没执行”不准确。返回 -1 且 errno==EINVAL 是正常结束,不是错误。应作为循环结束条件。
“frame_index 清零是防止信息混乱”本质是对每种格式重新从分辨率 0 开始枚举,没错,但更精确说是“重新开始查询该格式的第一个分辨率”。
“申请 buffer 后 buf_cnt = rb.count,但忘记 rb.count 可能被驱动修改”正确意识:必须使用返回后的 rb.count,不能假设等于请求的 count。
“poll 返回 1 就直接 DQBUF,没有检查 revents”危险!必须检查fds[0].revents & POLLIN,否则可能错误事件导致程序崩溃。
“write(fd_file, bufs[buf.index], buf.bytesused) 不知道为什么那样写”解释:bufs[] 是 mmap 映射的用户态地址,buf.bytesused 是驱动填写的实际数据长度,直接写入文件即可。
“设置格式时如果 ioctl 返回 0 就认为完全成功”忽略驱动可能修改 width/height/pixelformat。必须读取返回后的 fmt 结构体,以实际值为准。

6. 面试自测题(一问一答)

Q1:VIDIOC_QUERYCAP的作用是什么?如何正确检查摄像头是否支持视频捕获?
A:查询驱动能力,填充struct v4l2_capability。正确检查是:
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)),而不是if (cap.capabilities == 0)

Q2:VIDIOC_S_FMT调用后,为什么要重新读取fmt.fmt.pix.widthpixelformat
A:因为驱动可能调整参数为硬件支持的值,实际使用的宽高和格式可能跟请求的不同,后续申请 buffer 必须用实际值。

Q3:mmap映射中的offset参数从哪里获得?
A:通过VIDIOC_QUERYBUF获得的buf.m.offset。这个 offset 是物理偏移,不是文件偏移。

Q4:VIDIOC_REQBUFScount为什么有时申请 4 个,有时申请 32 个?
A:更多 buffer 可减少丢帧风险,但占用内存更大。根据应用场景调整,一般 4~8 足够,演示用 32 展示扩展性。

Q5:什么情况下VIDIOC_DQBUF会阻塞?如何避免?
A:如果没有 buffer 处于完成状态,DQBUF阻塞。避免方法:使用O_NONBLOCK打开设备,或用poll/select检测可读后再调用。

Q6:如果摄像头输出 YUYV 格式,而你保存为.jpg,会怎样?
A:文件内容是原始 YUYV 数据,不是 JPEG,无法直接查看。需要检查实际格式,若不是 MJPEG,则要进行格式转换(如用 libjpeg 编码)。

Q7:poll返回正数后,为什么要检查revents & POLLIN
A:poll可能因错误(POLLERR)或挂断(POLLHUP)返回,此时不应进行DQBUF,否则会导致程序异常。

Q8:VIDIOC_QBUFVIDIOC_DQBUF如何协同工作?
A:初始所有 buffer 通过QBUF放入输入队列,驱动依次填充,完成后移入完成队列;用户调用DQBUF取出,处理后再QBUF放回输入队列,形成循环。

Q9:为什么申请 buffer 后,需要调用VIDIOC_QUERYBUF才能 mmap?
A:QUERYBUF返回每个 buffer 的长度和物理偏移,这是 mmap 必须的参数。没有这些信息无法建立映射。

Q10:如果程序运行一段时间后采集变慢或卡死,可能原因是什么?
A:可能忘记在DQBUF之后重新QBUF,导致所有 buffer 都进入“完成队列”,输入队列为空,驱动无法填充数据。检查循环中是否每个DQBUF都配对了QBUF


7. 扩展建议

  • 错误处理完善:实际项目应使用goto统一释放资源(munmap、close)。
  • 保存实际格式:根据fmt.fmt.pix.pixelformat动态生成文件后缀(.yuv 或 .jpg)。
  • 非阻塞模式:可设置O_NONBLOCK,并用 poll 设置超时。
  • 多平面格式:对于 V4L2_PIX_FMT_NV12 等需要处理v4l2_plane

这篇博客已经完全覆盖了你要求的“极其详细、宏解释、框架图、纠正错误、面试题”。直接复制发布即可。如果你还需要我帮你画流程图(比如用 ASCII 或 Mermaid 格式),我可以再补充。

http://www.jsqmd.com/news/763465/

相关文章:

  • Windows更新卡住怎么办?3分钟快速修复终极指南
  • 在 Web 界面直接编辑 DESIGN.md:从思路到实现(二)
  • Webhook桥接器:解决内外网通信与格式转换的轻量级解决方案
  • 闲置沃尔玛购物卡别浪费!三大靠谱回收渠道实测,变现快还不踩坑 - 京回收小程序
  • AI短剧一站式平台与普通AI平台有什么区别? - Pixmax-AI短剧/漫剧
  • 在 Node.js 服务中接入 Taotoken 实现异步聊天补全功能
  • 开源AI产品经理Vibe-PM:三阶段对话生成PRD,重塑产品工作流
  • 四川盛世钢联国际贸易有限公司2026年5月6日成都钢材现货今日价格 - 四川盛世钢联营销中心
  • 月烧 400 刀到不到 20 刀:我是怎么把 OpenClaw 的 Token 账单砍掉 95% 的
  • OpenClaw集成DeepSeek V3:低成本高性能AI智能体解决方案
  • Gather Statistics AUTO_INVALIDATE 减少db的 library cache lock
  • 2026年山西精准获客与GEO生成式引擎优化深度横评指南 - 企业名录优选推荐
  • ThingsBoard MQTT上传数据避坑指南:连接失败、JSON格式错误、时间戳处理全解析
  • 量子-经典混合神经网络硬件资源评估与优化
  • 2026年山西精准获客、太原短视频代运营与晋中手机号定向完全指南 - 企业名录优选推荐
  • 孩子厌学逃学干预哪家专业?九州金榜一站式青少年心理与家庭教育解决方案 - 品牌企业推荐师(官方)
  • 开发者软技能文档库:提升技术协作与职业竞争力的实践指南
  • 让 AI 不再按过期文档写代码:AgentLockDoc 开源了
  • 深入PX4 Bootloader:从源码编译到自定义配置,打造你的专属飞控启动器
  • 2026年山西精准获客与短视频代运营完全指南:手机号定向推广、GEO优化、本地门店引流一体化解决方案 - 企业名录优选推荐
  • 从“捡回来”到玩转:ESP-01刷机后,如何用串口助手74880波特率查看启动日志与芯片信息
  • 交互式视频超分辨率技术:关键帧与智能传播
  • 上海庭院设计景观公司排行:5家靠谱公司深度盘点 - 真知灼见33
  • 【ISO/SAE 21434合规加速器】:Docker 27轻量化27步法——通过ASAM OpenSCENARIO V2.3认证的最小可信运行时构建指南
  • 九江黄金回收实测:福正美到手价比同行高8%的秘密 - 福正美黄金回收
  • 2026年内蒙环境检测哪家好?如何破解水质检测与废气检测难题 - 深度智识库
  • 专业视觉设计神器 Photoshop 2026 (PS)破解版下载安装教程
  • 2026年选毛刷厂家,掌握这三点绝不出错 - 品牌企业推荐师(官方)
  • 2026年5月新发布:山东地区精密管、精密钢管、合金无缝钢管优质厂商推荐,认准聊城市国顺钢管制造有限公司 - 2026年企业推荐榜
  • 在Ubuntu 22.04上,用Python脚本打通ROS2 Humble与科大讯飞SDK的简易语音控制方案