摄像头模块(九)编写 V4L2 数据传输函数前置必须理解摄像头模块(六)结构体抽象与设备管理、 摄像头模块(七)编写 V4L2 设备框架、摄像头模块(八)编写 V4L2 初始化函数深度解析本文档专为初学者和需要深入理解 V4L2 数据传输的同学准备包含你的常见错误、纠正、完整代码及逐行解释。文章目录摄像头模块(九)编写 V4L2 数据传输函数 本节核心目标 本节涉及的文件 V4L2 数据传输的两大模式回顾 数据传输的四个核心函数完整代码补充传统 read/write 模式简单对比 逐函数精讲包含常见错误与纠正1. V4l2StartDevice — 启动视频流2. V4l2StopDevice — 停止视频流3. V4l2GetFrameForStreaming — 获取一帧3.1 poll 等待数据3.2 DQBUF 取出缓冲区3.3 填充输出结构体 PT_VideoBuf4. V4l2PutFrameForStreaming — 归还缓冲区 主循环中的数据流结合 main.c 常见错误及排查 自测题检验是否掌握1. VIDIOC_STREAMON 之前至少要执行哪个 ioctl为什么2. poll 函数在这里的作用是什么如果不使用 poll如何实现等待3. DQBUF 返回后为什么必须保存 index4. PutFrame 中使用的 index 来自哪里5. 假设我们忘记调用 PutFrame程序运行一段时间后会发生什么6. 为什么 iTotalBytes 要用 bytesused 而不是 length7. 如果把 PutFrame 放在 GetFrame 之后、处理数据之前会有什么后果8. 描述一帧数据从摄像头硬件到应用程序内存的完整路径 总结 本节核心目标实现视频采集的数据传输循环即启动视频流VIDIOC_STREAMON循环获取一帧数据pollVIDIOC_DQBUF处理完成后归还缓冲区VIDIOC_QBUF停止视频流VIDIOC_STREAMOFF学完本节你将彻底理解 V4L2 的内存映射mmap流式 I/O模型能够独立写出摄像头采集循环。 本节涉及的文件文件函数video/v4l2.cV4l2StartDevice,V4l2StopDevice,V4l2GetFrameForStreaming,V4l2PutFrameForStreaming以及可选的V4l2GetFrameForReadWrite/V4l2PutFrameForReadWrite仅作为对比 V4L2 数据传输的两大模式回顾在 08.2 初始化时我们查询了设备能力V4L2_CAP_STREAMING支持内存映射mmap多缓冲高效本课程使用此模式。V4L2_CAP_READWRITE支持传统的read()系统调用简单但低效。项目代码中根据能力动态选择GetFrame/PutFrame函数指针。我们只深入讲解streaming 模式。 数据传输的四个核心函数完整代码c/* 1. 启动视频流 */ static int V4l2StartDevice(PT_VideoDevice ptVideoDevice) { int iType V4L2_BUF_TYPE_VIDEO_CAPTURE; int iError ioctl(ptVideoDevice-iFd, VIDIOC_STREAMON, iType); if (iError) { DBG_PRINTF(Unable to start capture.\n); return -1; } return 0; } /* 2. 停止视频流 */ static int V4l2StopDevice(PT_VideoDevice ptVideoDevice) { int iType V4L2_BUF_TYPE_VIDEO_CAPTURE; int iError ioctl(ptVideoDevice-iFd, VIDIOC_STREAMOFF, iType); if (iError) { DBG_PRINTF(Unable to stop capture.\n); return -1; } return 0; } /* 3. 获取一帧streaming 模式 */ static int V4l2GetFrameForStreaming(PT_VideoDevice ptVideoDevice, PT_VideoBuf ptVideoBuf) { struct pollfd tFds[1]; struct v4l2_buffer tV4l2Buf; int iRet; /* 等待数据可读 */ tFds[0].fd ptVideoDevice-iFd; tFds[0].events POLLIN; iRet poll(tFds, 1, -1); if (iRet 0) { DBG_PRINTF(poll error\n); return -1; } /* 取出已填满的缓冲区 */ memset(tV4l2Buf, 0, sizeof(tV4l2Buf)); tV4l2Buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; tV4l2Buf.memory V4L2_MEMORY_MMAP; iRet ioctl(ptVideoDevice-iFd, VIDIOC_DQBUF, tV4l2Buf); if (iRet 0) { DBG_PRINTF(Unable to dequeue buffer\n); return -1; } ptVideoDevice-iVideoBufCurIndex tV4l2Buf.index; /* 填充输出结构体 */ ptVideoBuf-iPixelFormat ptVideoDevice-iPixelFormat; ptVideoBuf-tPixelDatas.iWidth ptVideoDevice-iWidth; ptVideoBuf-tPixelDatas.iHeight ptVideoDevice-iHeight; /* 根据像素格式确定每个像素的位数bpp */ if (ptVideoDevice-iPixelFormat V4L2_PIX_FMT_YUYV) ptVideoBuf-tPixelDatas.iBpp 16; else if (ptVideoDevice-iPixelFormat V4L2_PIX_FMT_MJPEG) ptVideoBuf-tPixelDatas.iBpp 0; // MJPEG 是压缩格式bpp 无意义 else if (ptVideoDevice-iPixelFormat V4L2_PIX_FMT_RGB565) ptVideoBuf-tPixelDatas.iBpp 16; else ptVideoBuf-tPixelDatas.iBpp 0; ptVideoBuf-tPixelDatas.iLineBytes ptVideoDevice-iWidth * ptVideoBuf-tPixelDatas.iBpp / 8; ptVideoBuf-tPixelDatas.iTotalBytes tV4l2Buf.bytesused; // 实际数据长度 ptVideoBuf-tPixelDatas.aucPixelDatas ptVideoDevice-pucVideBuf[tV4l2Buf.index]; return 0; } /* 4. 归还缓冲区streaming 模式 */ static int V4l2PutFrameForStreaming(PT_VideoDevice ptVideoDevice, PT_VideoBuf ptVideoBuf) { struct v4l2_buffer tV4l2Buf; memset(tV4l2Buf, 0, sizeof(tV4l2Buf)); tV4l2Buf.index ptVideoDevice-iVideoBufCurIndex; // 使用之前保存的索引 tV4l2Buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; tV4l2Buf.memory V4L2_MEMORY_MMAP; int iError ioctl(ptVideoDevice-iFd, VIDIOC_QBUF, tV4l2Buf); if (iError) { DBG_PRINTF(Unable to queue buffer\n); return -1; } return 0; }补充传统 read/write 模式简单对比cstatic int V4l2GetFrameForReadWrite(PT_VideoDevice ptVideoDevice, PT_VideoBuf ptVideoBuf) { int iRet read(ptVideoDevice-iFd, ptVideoDevice-pucVideBuf[0], ptVideoDevice-iVideoBufMaxLen); if (iRet 0) return -1; // 填充 ptVideoBuf 类似略... return 0; } static int V4l2PutFrameForReadWrite(PT_VideoDevice ptVideoDevice, PT_VideoBuf ptVideoBuf) { return 0; // read 模式无需归还 } 逐函数精讲包含常见错误与纠正1.V4l2StartDevice— 启动视频流cint iType V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(fd, VIDIOC_STREAMON, iType);作用通知驱动开始采集驱动会开始往已入队的缓冲区中填充图像数据。为什么必须先调用它在STREAMON之前驱动不会采集数据。STREAMON后驱动才启动 DMA 或 USB 传输。前提条件已经调用VIDIOC_QBUF至少一次否则驱动没有可用缓冲区STREAMON会返回错误。❌你的疑问“如果在调用 StartDevice 之前没有执行过 VIDIOC_QBUF入队缓冲区会发生什么”✅正确答案STREAMON会失败返回-1。驱动需要至少一个空缓冲区来放置第一帧。2.V4l2StopDevice— 停止视频流cioctl(fd, VIDIOC_STREAMOFF, iType);作用停止采集驱动停止填充缓冲区。为什么需要它释放资源前必须停止流否则close(fd)可能卡住或导致内核警告。注意停止后之前已DQBUF但未QBUF的缓冲区会怎样驱动会内部重置但应用程序最好在停止前归还所有缓冲区。3.V4l2GetFrameForStreaming— 获取一帧3.1poll等待数据cstruct pollfd tFds[1]; tFds[0].fd ptVideoDevice-iFd; tFds[0].events POLLIN; poll(tFds, 1, -1);pollfd结构体初学者常见盲点cstruct pollfd { int fd; // 要监视的文件描述符 short events; // 请求的事件如 POLLIN 表示可读 short revents; // 返回的事件内核填充 };POLLIN表示文件描述符有数据可读对于摄像头就是有一帧准备好了。poll返回值0有事件0超时0错误。这里第三个参数-1表示无限等待。❓ 如果不使用poll直接调用DQBUF会怎样如果当前没有缓冲区已填满DQBUF会立即失败返回EAGAIN你需要循环不断重试浪费 CPU。poll让进程睡眠直到有数据才唤醒这是高效的做法。3.2DQBUF取出缓冲区cstruct v4l2_buffer tV4l2Buf; memset(tV4l2Buf, 0, sizeof(tV4l2Buf)); tV4l2Buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; tV4l2Buf.memory V4L2_MEMORY_MMAP; ioctl(fd, VIDIOC_DQBUF, tV4l2Buf);VIDIOC_DQBUF从驱动的已完成队列中取出一个缓冲区该缓冲区已被驱动填满图像数据。返回后tV4l2Buf.index指示是哪个缓冲区0 ~ n-1。tV4l2Buf.bytesused是实际数据长度对于 MJPEG 等压缩格式每帧大小不同。为什么保存iVideoBufCurIndex因为PutFrame时需要知道归还哪个缓冲区。你之前回答“不知道”现在要记住索引必须保存否则无法正确归还。3.3 填充输出结构体PT_VideoBufcptVideoBuf-tPixelDatas.aucPixelDatas ptVideoDevice-pucVideBuf[tV4l2Buf.index];重要这里没有分配新内存直接指向已映射的缓冲区地址零拷贝。后续处理转换、缩放将直接修改或读取这块内存。iTotalBytes使用bytesused对于压缩格式固定缓冲区长度可能更大bytesused才是真实有效数据长度。如果使用全长处理转换时会包含垃圾数据。❌你之前问“为什么iTotalBytes用的是bytesused而不是固定长度”✅正确答案对于 MJPEG一帧可能是 10KB但缓冲区长度是 1MB。如果使用固定长度转换模块会尝试处理大量无用数据效率低下且可能出错。bytesused是驱动实际写入的字节数必须用它。4.V4l2PutFrameForStreaming— 归还缓冲区ctV4l2Buf.index ptVideoDevice-iVideoBufCurIndex; ioctl(fd, VIDIOC_QBUF, tV4l2Buf);VIDIOC_QBUF将缓冲区归还给驱动重新放入空闲队列驱动会再次用它填充新数据。为什么必须配对DQBUF取出的缓冲区在驱动程序眼中被“借出”给应用程序。如果不QBUF驱动会认为那个缓冲区还在应用中可用缓冲区数量减少。最终所有缓冲区都被借出驱动没有缓冲区可用poll可能永不返回或DQBUF失败。❓ 如果忘记调用PutFrame会怎样采集几帧后所有缓冲区都被应用程序占用驱动无法继续采集程序可能卡在poll或DQBUF返回EAGAIN。必须归还。 主循环中的数据流结合 main.ccwhile (1) { GetFrame(...); // 等待并取出已填满的缓冲区 处理数据转换/缩放/合并; // 直接操作 aucPixelDatas 指向的内存 PutFrame(...); // 归还缓冲区 }为什么PutFrame必须放在处理之后因为缓冲区内存正在被应用程序读取/修改。如果在处理之前就归还驱动可能会立即在该缓冲区中写入新数据导致处理的数据被覆盖出现图像撕裂或错误。能否提前归还不能。必须等处理完成确保不再需要该缓冲区内容。 常见错误及排查现象可能原因解决方法poll立即返回且revents无POLLIN驱动未启动流或没有缓冲区入队检查是否调用了STREAMON和QBUFDQBUF返回EAGAIN当前没有缓冲区已填满但使用了非阻塞应用层应使用poll或select等待图像数据错乱或部分更新未等处理完就归还缓冲区确保PutFrame在数据处理之后采集几帧后停止poll永不返回忘记PutFrame所有缓冲区都被占用检查PutFrame是否被调用内存越界错误使用了固定的缓冲区长度而不是bytesused使用bytesused限定处理范围MJPEG 图像解压失败可能bytesused被误设为缓冲区全长打印bytesused与缓冲区长度对比确认正确 自测题检验是否掌握1.VIDIOC_STREAMON之前至少要执行哪个 ioctl为什么回答至少必须执行VIDIOC_QBUF缓冲区入队。因为STREAMON是启动视频流采集驱动必须先拿到至少一个空闲缓冲区才能开始填数据如果没有缓冲区入队驱动无内存可写启动会直接失败。补充实际工程中还需要REQBUFSS_FMT但最小必须是 QBUF2.poll函数在这里的作用是什么如果不使用poll如何实现等待回答poll的作用是阻塞等待摄像头帧就绪POLLIN 事件避免 CPU 空转只有帧准备好了才去取帧。如果不用poll可以直接阻塞调用 DQBUF(取出一帧已采集完成的缓冲区)默认模式下 DQBUF 会阻塞到有帧循环忙等 DQBUF判断 EAGAIN 并重试但不推荐因为会大幅浪费 CPU。3.DQBUF返回后为什么必须保存index回答因为DQBUF会由驱动返回当前就绪的缓冲区编号应用必须保存这个index才能找到对应帧的数据内存地址后续调用QBUF把缓冲区正确归还给驱动不保存 index就无法循环采集程序会卡死或报错。4.PutFrame中使用的index来自哪里回答来自GetFrame 时 DQBUF 返回的 index并保存在设备结构体iVideoBufCurIndex中。PutFrame 只是把这个保存的 index 取出来填回 v4l2_buffer 用于入队。5. 假设我们忘记调用PutFrame程序运行一段时间后会发生什么回答缓冲区会全部被取走但不归还驱动的空闲队列变空最终摄像头无法继续采集DQBUF 会一直阻塞或返回 EAGAIN程序不再收到新帧画面卡死、不再更新。6. 为什么iTotalBytes要用bytesused而不是length回答length是缓冲区总大小固定值bytesused是这一帧实际使用的字节数真实有效数据长度特别是 MJPEG 压缩帧每帧大小不一样必须用bytesused才能拿到正确长度。7. 如果把PutFrame放在GetFrame之后、处理数据之前会有什么后果回答会导致数据被覆盖、画面花屏、数据错乱。因为你还没处理数据就把缓冲区还给驱动驱动会立即写入新数据把你还没读取的内容覆盖掉。8. 描述一帧数据从摄像头硬件到应用程序内存的完整路径回答摄像头硬件通过 DMA 把图像写入内核缓冲区驱动将缓冲区标记为就绪触发POLLIN应用调用DQBUF从内核取出该缓冲区通过MMAP 映射应用直接访问内核物理内存应用处理完后调用QBUF把缓冲区归还驱动驱动重新放入队列等待下一帧写入全程零拷贝数据不复制直接从内核映射到用户空间。 总结课程的核心是掌握poll DQBUF 处理 QBUF的循环模型。Start/Stop控制流的启停。GetFrame等待并取出数据返回指向共享内存的指针零拷贝。PutFrame归还缓冲区允许驱动复用。理解了这个模型你就掌握了 V4L2 高效采集的精髓。后续的转换模块,将基于这些原始数据帧进行格式转换。下一节转换模块_结构体抽象与管理你会看到类似摄像头模块的设计模式。 本文档包含了你在学习V4l2GetFrameForStreaming过程中提出的所有疑问及正确答案。反复阅读直到你能独立写出这些函数。