嵌入式 Linux V4L2 摄像头采集编程MMAP 方式(四)—— 从零到一含全部宏详解与框架图适用平台IMX6ULL / 任何支持 V4L2 的嵌入式板卡编译器arm-buildroot-linux-gnueabihf-gcc摄像头USB 或 CSI支持 MJPEG / YUYV1. 前言你为什么需要这篇博客很多人在写 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) | ------------------------------- ↓ ------------------------------- | 循环: i0..rb.count-1 | | ├─ VIDIOC_QUERYBUF | → 获取每个 buffer 的长度和 offset | └─ mmap(...) | → 映射到用户空间 ------------------------------- ↓ ------------------------------- | 循环: i0..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; // 从 index0 开始逐一枚举直到 ioctl 返回 -1通常 errno EINVAL 表示结束 for (fmtdesc.index 0; ; fmtdesc.index) { if (ioctl(fd, VIDIOC_ENUM_FMT, fmtdesc) 0) break; // 枚举结束正常退出循环 printf(format %s, fourcc0x%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, fourcc0x%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是 FourCCYUYV的十进制表示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 且 errnoEINVAL 是正常结束不是错误。应作为循环结束条件。“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. 面试自测题一问一答Q1VIDIOC_QUERYCAP的作用是什么如何正确检查摄像头是否支持视频捕获A查询驱动能力填充struct v4l2_capability。正确检查是if (!(cap.capabilities V4L2_CAP_VIDEO_CAPTURE))而不是if (cap.capabilities 0)。Q2VIDIOC_S_FMT调用后为什么要重新读取fmt.fmt.pix.width和pixelformatA因为驱动可能调整参数为硬件支持的值实际使用的宽高和格式可能跟请求的不同后续申请 buffer 必须用实际值。Q3mmap映射中的offset参数从哪里获得A通过VIDIOC_QUERYBUF获得的buf.m.offset。这个 offset 是物理偏移不是文件偏移。Q4VIDIOC_REQBUFS的count为什么有时申请 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 编码。Q7poll返回正数后为什么要检查revents POLLINApoll可能因错误POLLERR或挂断POLLHUP返回此时不应进行DQBUF否则会导致程序异常。Q8VIDIOC_QBUF和VIDIOC_DQBUF如何协同工作A初始所有 buffer 通过QBUF放入输入队列驱动依次填充完成后移入完成队列用户调用DQBUF取出处理后再QBUF放回输入队列形成循环。Q9为什么申请 buffer 后需要调用VIDIOC_QUERYBUF才能 mmapAQUERYBUF返回每个 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 格式我可以再补充。