ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

i.MX6ULL实时显示OV5640:QT和V4L2完整链路实战指南

i.MX6ULL实时显示OV5640:QT和V4L2完整链路实战指南 用QT在i.MX6ULL上实时显示OV5640摄像头画面这个需求我在正点原子开发板上反复折腾过好几次。前两天又有朋友来问说网上教程要么只讲驱动设备树配置要么只把V4L2采集讲完就收工要么只演示QT界面怎么画控件就是没人把这三件事串成一条完整的链路。自己拿回去拼的时候光是摄像头数据怎么进入QT界面这一步就能卡住一整天。正好我手头这套环境还没拆干脆把整个流程重新走了一遍从交叉编译环境到V4L2采集再到QT界面上把画面刷出来每一步踩过的坑都整理在下面。这篇文章适合手里有正点原子i.MX6ULL开发板和OV5640摄像头模块、正在做嵌入式Linux图像采集项目的人也适合刚入门嵌入式开发、想搞明白Linux视频采集和GUI显示怎么配合的读者。项目本身不复杂但涉及内核驱动、用户态采集、图像格式转换、Qt跨平台显示多个环节任何一个环节出问题都会直接表现为黑屏、花屏或者帧率惨不忍睹。我尽量把每一步的原理和实际操作都讲透让你照着做就能把画面跑起来。1. 方案选型与整体架构为什么是QTV4L21.1 硬件平台与软件栈盘点i.MX6ULL是NXP基于Cortex-A7内核的单核处理器主频800MHz没有GPU也没有硬核视频编解码器但片上有摄像头接口CSI可以接DVP并口或者通过转换芯片接MIPI信号。正点原子的i.MX6ULL开发板型号比较多我手头这块是ALPHA/Mini常见型号板载OV5640模块通过CSI排线连接。OV5640是OmniVision的500万像素传感器输出格式支持YUV422YUYV、RGB565、JPEG等最高分辨率2592x1944在640x480下可以跑到较高帧率做实时显示绰绰有余。软件栈上Linux内核负责把OV5640驱动起来通过V4L2框架向用户空间提供/dev/video0设备节点。用户空间这边我们用一个QT应用程序打开这个节点配置采集格式然后把每一帧图像数据转换成QImage最终画到界面上。整条链路是OV5640传感器 - CSI控制器 - 内核驱动 -/dev/video0- V4L2 mmap - 用户态帧缓冲 - QImage - QWidget/QLabel显示。1.2 为什么不用GStreamer直接V4L2更可控很多人上来就建议用GStreamer说一条pipeline就能搞定采集和显示。但我的看法是在i.MX6ULL这种低配板子上GStreamer并不一定是首选。一是交叉编译GStreamer以及它的各种插件是个不小的工程buildroot里虽然有现成的但版本和插件裁剪未必符合你的需求二是GStreamer封装层次高出了问题不好定位对学习底层原理也没什么帮助。直接用V4L2写采集代码代码量不大也就几十行而且每一行都对应着内核里实实在在的操作排查问题的时候思路非常清晰。QT这边同理很多人觉得嵌入式显示用LVGL就够了但LVGL做小屏仪表盘确实轻量到了需要显示摄像头视频流这个场景它的图像处理能力和控件体系就不够用了。QT的QImage可以直接用外部的RGB数据构造不需要额外做一次像素拷贝配合linuxfb平台插件在没有GPU的情况下也能把画面刷出来。而且你以后要是换到RK3588这类带GPU的板子同一套QT代码可以平滑迁移到eglfs或者xcb后端不用重写界面逻辑。1.3 整体架构里的数据流设计整个项目的关键点在于数据流怎么设计。摄像头每一帧数据到了用户空间之后怎么在尽量少的拷贝次数下变成QT界面上的像素这是性能优化的核心。我最终的方案是V4L2用mmap方式把内核缓冲区映射到用户空间采集线程拿到一帧后根据像素格式做一次转换YUYV转RGB888转换后的数据直接构造QImage再通过信号槽或者直接在主线程paintEvent里绘制。全程尽量避免把整帧数据从QImage再拷贝到QPixmap因为QPixmap在linuxfb下最终还是写到framebuffer多一次拷贝就多一次性能损耗。2. 环境准备搭建QT 5.15的交叉编译环境2.1 板子自带系统 vs 自己动手编译如果你手里的开发板是正点原子出厂系统那系统里大概率已经帮你编译好了QT你只需要在板子上写QT程序然后交叉编译好丢进去跑就行。但如果你想自己控制QT的模块和版本或者想搞清楚QT到底是怎么在ARM Linux上跑起来的那建议自己编译一遍。我这里用的是QT 5.15.2因为它是5.x分支里最后一个长期支持版本资料多、踩坑案例也多比6.x在嵌入式交叉编译上更成熟。在动手之前建议先确认几个东西。第一板子上有没有/dev/video0节点如果没有先解决内核驱动和设备树的问题这部分后面单独说。第二板子的framebuffer设备/dev/fb0是否存在QT在linuxfb模式下会直接往这个设备写像素。第三交叉编译工具链是否可用正点原子的buildroot会生成arm-linux-gnueabihf-gcc也可以用Linaro的GCC工具链我用的是7.5版本编译QT 5.15.2没有问题。2.2 交叉编译QT 5.15.2的关键步骤下载QT 5.15.2源码包解压之后在源码根目录执行configure。以下是我实测可用的配置参数./configure -prefix /opt/qt5.15.2-arm \ -release -opensource -confirm-license \ -xplatform linux-arm-gnueabi-g \ -no-opengl -no-gtk -linuxfb \ -qt-zlib -qt-libpng -qt-libjpeg \ -no-xcb -no-cups -no-tslib \ -skip qtmultimedia -skip qtsensors -skip qtwebengine \ -skip qtdeclarative -skip qtscript \ -nomake examples -nomake tests这里要解释几个关键参数。-xplatform linux-arm-gnueabi-g告诉qmake使用ARM交叉编译配置实际上正点原子buildroot的工具链前缀通常是arm-linux-gnueabihf-你需要先确认arm-linux-gnueabihf-gcc在PATH里如果工具链前缀不同可以在qtbase/mkspecs/devices/linux-arm-gnueabi-g里修改编译器名称。-no-opengl是因为i.MX6ULL没有GPUOpenGL在软件渲染下性能太差没有实际意义。-linuxfb启用linuxfb平台插件这是无GPU环境下QT在framebuffer上显示的基础。-no-xcb表示不需要X11支持因为板子上没有跑X Server。-skip后面跟的模块都是我们用不到的跳过能大幅缩短编译时间。configure通过后执行make -j4树莓派上编译大约需要几十分钟到两个小时不等取决于你的主机性能。编译完成后make install会把QT安装到/opt/qt5.15.2-arm然后把整个目录打包拷贝到板子的/opt下之后把/opt/qt5.15.2-arm/lib加入板子的LD_LIBRARY_PATH环境变量。2.3 板端运行环境配置拷贝完成后在板子上设置环境变量通常写在/etc/profile里export QTDIR/opt/qt5.15.2-arm export LD_LIBRARY_PATH$QTDIR/lib:$LD_LIBRARY_PATH export QT_QPA_PLATFORMlinuxfb export QT_QPA_FB_DRM1 # 如果有需要可以指定fb设备这里最容易踩的坑是QT_QPA_PLATFORM没设置或者设置错误。QT 5.x默认会尝试xcb后端板子上没有X Server如果不指定linuxfb程序启动会报错。另外如果你的板子有多个framebuffer设备比如HDMI和RGB LCD各占一个/dev/fb0、/dev/fb1需要确认QT用的是哪个可以通过export QT_QPA_FB_DRM/dev/fb0强制指定。我建议你先写一个最简单的QT程序只显示一个QLabel在板子上跑通确认QT环境没有问题再开始搞摄像头。不然到时候又是摄像头又是QT问题混在一起很难定位。3. OV5640驱动确认与V4L2采集3.1 先确认摄像头驱动活着板子上电后第一步是确认内核已经识别到OV5640。执行下面的命令ls /dev/video* v4l2-ctl --list-devices media-ctl -p/dev/video0存在说明V4L2主设备已经被创建。v4l2-ctl --list-devices会告诉你这个节点对应什么名字如果是ov5640或者csi相关说明驱动注册成功。media-ctl -p可以查看media controller链路这个在CSI接口下很关键因为OV5640和CSI控制器之间可能有多个子设备节点链路没有配对好画面是出不来的。在正点原子出厂系统里这条链路通常已经配置好了。但如果你是自己移植的内核很可能出现/dev/video0存在但采集不到数据的情况这时候就要用media-ctl手动把数据链路打通具体命令类似media-ctl -l ov5640 1-003c:0 - csi:0[1]这个命令的意思是把I2C地址0x3C上的OV5640子设备的pad0输出连接到csi实体的pad0输入。[1]表示启用这条link。链路不通时VIDIOC_STREAMON可能直接返回错误这个排查点特别容易被忽略。3.2 用v4l2-ctl直接验证采集在写QT代码之前强烈建议先用系统的v4l2-ctl命令验证摄像头能不能出图。命令如下v4l2-ctl -d /dev/video0 --set-fmt-videowidth640,height480,pixelformatYUYV --stream-mmap --stream-count1 --stream-to/tmp/test.yuv这条命令指定以640x480分辨率、YUYV像素格式采集一帧保存到/tmp/test.yuv。执行成功后用ls -l /tmp/test.yuv查看文件大小640x480的YCbCr422一帧大小是614400字节如果文件大小对得上说明驱动和数据链路没有问题可以放心进入应用层开发。如果这里就报错先别急着写QT回头查设备树、查驱动。3.3 V4L2应用层采集流程拆解V4L2采集的标准流程可以概括为打开设备 - 设置格式 - 申请缓冲区 - 映射到用户空间 - 入队 - 启动流 - 循环出队/入队。我把核心代码贴出来每一段都加了注释照着抄就能用。#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include sys/mman.h #include linux/videodev2.h #define WIDTH 640 #define HEIGHT 480 #define BUFFER_COUNT 4 struct buffer_info { void *start; size_t length; }; int main(void) { int fd open(/dev/video0, O_RDWR); if (fd 0) { perror(open /dev/video0); return -1; } // 1. 查询设备能力 struct v4l2_capability cap; ioctl(fd, VIDIOC_QUERYCAP, cap); if (!(cap.capabilities V4L2_CAP_VIDEO_CAPTURE)) { fprintf(stderr, not a video capture device\n); return -1; } // 2. 设置采集格式 struct v4l2_format fmt {0}; fmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width WIDTH; fmt.fmt.pix.height HEIGHT; fmt.fmt.pix.pixelformat V4L2_PIX_FMT_YUYV; // 驱动输出格式 fmt.fmt.pix.field V4L2_FIELD_NONE; if (ioctl(fd, VIDIOC_S_FMT, fmt) 0) { perror(VIDIOC_S_FMT); return -1; } // 3. 申请帧缓冲 struct v4l2_requestbuffers req {0}; req.count BUFFER_COUNT; req.type V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_REQBUFS, req) 0) { perror(VIDIOC_REQBUFS); return -1; } // 4. 映射缓冲区 struct buffer_info buffers[BUFFER_COUNT]; for (int i 0; i BUFFER_COUNT; i) { struct v4l2_buffer buf {0}; buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; buf.index i; if (ioctl(fd, VIDIOC_QUERYBUF, buf) 0) { perror(VIDIOC_QUERYBUF); return -1; } buffers[i].length buf.length; buffers[i].start mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if (buffers[i].start MAP_FAILED) { perror(mmap); return -1; } } // 5. 所有缓冲区入队 for (int i 0; i BUFFER_COUNT; 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); } // 6. 启动采集 enum v4l2_buf_type type V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(fd, VIDIOC_STREAMON, type); // 7. 循环取帧 for (int i 0; i 30; i) { // 采集30帧示例 struct v4l2_buffer buf {0}; buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_DQBUF, buf) 0) { perror(VIDIOC_DQBUF); break; } // 此时 buffers[buf.index].start 指向这一帧数据 // 在这里做图像处理 / 转换 / 显示 // 处理完之后把缓冲区重新入队 ioctl(fd, VIDIOC_QBUF, buf); } // 8. 停止采集并清理 ioctl(fd, VIDIOC_STREAMOFF, type); for (int i 0; i BUFFER_COUNT; i) { munmap(buffers[i].start, buffers[i].length); } close(fd); return 0; }这个流程里最关键的一点是DQBUF和QBUF的循环。可以把缓冲区想象成四个盘子摄像头持续装菜装好一个就放到队列里应用层从队列里取走一个DQBUF处理完了再放回队列QBUF。如果应用层处理太慢四个盘子都用完了还在手里摄像头就只能丢帧表现出来的就是画面卡顿或者延迟。所以在实际项目中缓冲区的数量要结合处理耗时来定一般4个就够用太少容易丢帧太多会增加内存占用和延迟。4. QT界面显示与帧数据处理4.1 从V4L2帧缓冲到QImage摄像头采集到的YUYV数据不能直接被QImage显示QImage支持RGB32、RGB565、ARGB32等格式但就是没有YUYV。所以必须先做一次颜色空间转换。转换的数学原理不复杂YCbCr到RGB是一个线性变换网上查一下标准公式就有。但在嵌入式平台上浮点运算成本比较高我用的是整数定点运算把浮点系数乘以1024再取整最后右移10位实测在i.MX6ULL上性能提升明显。void yuyv_to_rgb565(const unsigned char *yuyv, unsigned char *rgb565, int width, int height) { int pixel_count width * height / 2; for (int i 0; i pixel_count; i) { int y0 yuyv[i * 4 0]; int u yuyv[i * 4 1] - 128; int y1 yuyv[i * 4 2]; int v yuyv[i * 4 3] - 128; // 像素0 int r0 (y0 * 1024 1402 * v) 10; int g0 (y0 * 1024 - 344 * u - 714 * v) 10; int b0 (y0 * 1024 1772 * u) 10; // 像素1 int r1 (y1 * 1024 1402 * v) 10; int g1 (y1 * 1024 - 344 * u - 714 * v) 10; int b1 (y1 * 1024 1772 * u) 10; // 限定范围并合成RGB565 r0 r0 0 ? 0 : (r0 255 ? 255 : r0); g0 g0 0 ? 0 : (g0 255 ? 255 : g0); b0 b0 0 ? 0 : (b0 255 ? 255 : b0); r1 r1 0 ? 0 : (r1 255 ? 255 : r1); g1 g1 0 ? 0 : (g1 255 ? 255 : g1); b1 b1 0 ? 0 : (b1 255 ? 255 : b1); unsigned short pixel0 ((r0 0xF8) 8) | ((g0 0xFC) 3) | (b0 3); unsigned short pixel1 ((r1 0xF8) 8) | ((g1 0xFC) 3) | (b1 3); ((unsigned short *)rgb565)[i * 2] pixel0; ((unsigned short *)rgb565)[i * 2 1] pixel1; } }转换成RGB565之后构造QImage非常方便因为QImage原生支持Format_RGB565QImage image((const uchar *)rgb565_buffer, WIDTH, HEIGHT, QImage::Format_RGB565);这里有个特别要注意的点QImage构造函数默认不会拷贝buffer数据它只是持有一个指针。如果buffer生命周期结束这个QImage就变成了野指针。所以如果你的采集线程释放buffer之后主线程还要用这个QImage必须在构造之后主动调用一次image.copy()或者用detach()强制分离数据。我在第一次做的时候没注意这个问题画面时不时花屏、崩溃排查了很久才发现是共享内存指针失效导致的。4.2 采集线程与界面刷新的配合方式在QT里做实时显示有两套方案。方案一是主线程用QTimer定时调用采集函数代码简单但DQBUF在没有新帧的时候会阻塞整个UI线程界面一旦在等帧就卡死了。方案二是单独开一个采集线程线程里循环DQBUF和转换转换完成后通过信号把QImage发到主线程主线程的槽函数里更新显示。后者更合理不会阻塞界面。我用的是第二种。核心代码如下思路是定义一个工作类继承QThread在run()里面持续采集和转换然后发射frameReady(QImage)信号class CaptureThread : public QThread { Q_OBJECT public: CaptureThread(QObject *parent nullptr) : QThread(parent), m_stop(false) {} void stop() { m_stop true; } signals: void frameReady(const QImage image); protected: void run() override { // 在这里执行V4L2打开、设置格式、mmap等初始化 // 然后进入while(!m_stop)循环 while (!m_stop) { // DQBUF拿帧 // yuyv转rgb565 QImage image((const uchar *)rgb565_buffer, WIDTH, HEIGHT, QImage::Format_RGB565); emit frameReady(image.copy()); // 注意深拷贝 // QBUF归还缓冲区 } // 停止采集清理资源 } private: volatile bool m_stop; };主线程里连接信号connect(m_captureThread, CaptureThread::frameReady, this, [this](const QImage image) { m_currentImage image.copy(); // 再存一份用于paintEvent update(); // 触发重绘 });这里的image.copy()挺关键发送信号时QImage已经构造好了如果不调用copy信号传递过程中可能因为线程间共享数据的时序问题导致图像数据被篡改。而且发射信号本身可能跨线程排队等到主线程收到的时候V4L2的缓冲区可能已经被硬件重新写入新帧了。所以无论发送端还是接收端该深拷贝的时候一定不能省。4.3 用paintEvent画图而不是QLabel.setPixmap很多人习惯用一个QLabel来显示图像直接调用label-setPixmap(QPixmap::fromImage(image))。在PC上这样写一点问题没有但在i.MX6ULL这种无GPU平台上QPixmap::fromImage会做一次深层转换和拷贝一帧640x480的RGB565图像大约是614KB每秒钟30帧就是18MB的内存流量再加上linuxfb的刷新CPU直接被吃满。更好的做法是自定义一个QWidget重写paintEvent直接把QImage画到控件上void VideoWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); if (!m_image.isNull()) { painter.drawImage(this-rect(), m_image); } }drawImage在linuxfb下会直接把QImage像素写到framebuffer对应的内存区域省掉了中间的QPixmap转换。实测下来同样的640x48030fps画面用QLabel方案CPU占用大概60%到70%用paintEvent方案可以降到40%左右。界面如果还需要叠加文字、画框也在paintEvent里用QPainter统一画效果比多层控件叠加强得多。4.4 帧率控制与性能陷阱OV5640在640x480YUYV下可以跑到60fps甚至更高但QT界面不需要这么高的刷新率30fps足够流畅了。帧率控制可以放在采集线程里用一个简单的定时器实现QElapsedTimer timer; timer.start(); while (!m_stop) { // 采集一帧 // 转换、发信号 int elapsed timer.restart(); int sleepMs 33 - elapsed; // 目标33ms一帧约30fps if (sleepMs 0) msleep(sleepMs); }这个实现不精确但够用。更精确的可以用QTimer配合wait condition但嵌入式项目里没必要搞那么复杂。注意不要每帧都去usleep(33000)因为采集和转换本身也要时间数据刷新实际应该以帧就绪为准而不是固定sleep。从用户角度来说30fps已经非常流畅了再往上增加帧率对体验提升有限反而白白占用CPU。还有一个坑是linuxfb模式下的刷新机制。QT在linuxfb下收到update()后会刷新整个控件区域到framebuffer刷新过程中如果摄像头同时往内存里写新帧画面可能出现撕裂。解决思路是双缓冲QImage构造的时候不要直接指向V4L2 buffer而是自己维护一块内存DQBUF取帧后先拷贝到这块内存再构造QImage这样framebuffer刷新和摄像头DMA写入就不会冲突。5. 常见问题与排查技巧实录5.1 黑屏链路配置、分辨率和像素格式逐个查黑屏是V4L2摄像头项目里最常见的问题排查顺序很重要。先确认/dev/video0存在然后执行v4l2-ctl --set-fmt-videowidth640,height480,pixelformatYUYV --stream-mmap --stream-count1如果这条命令报错那问题在内核驱动和链路配置QT代码写得再对也没用。如果命令执行成功能抓到帧但QT界面还是黑的那问题出在用户态。用户态黑屏首先要检查像素格式。OV5640驱动可能默认输出YUYV但你用RGB565去解释图像会出现严重的色偏或全黑。先通过v4l2-ctl --get-fmt-video查看当前格式然后确认你的转换代码和S_FMT配置一致。其次是分辨率不匹配V4L2的VIDIOC_S_FMT并不一定会完全满足你设置的参数驱动可能会自动调整到最接近的格式所以S_FMT之后最好再调用一次VIDIOC_G_FMT看看实际生效的宽高和像素格式到底是什么。5.2 花屏或图像错位花屏的原因通常有两个。一个是像素格式解释错误比如驱动输出NV12但你按YUYV转换数据错位会导致整个画面出现规律的彩色条纹。另一个是CSI/DVP数据线上的采样时序问题这种问题在内核驱动和设备树里排查因为DVP并行接口的时钟极性、采样沿配置不对会导致每行像素错位图像看起来像被撕开一样。我在调试时遇到过一次特别诡异的花屏画面垂直方向分成上下两半上半部分正常下半部分完全错位。后来发现是V4L2缓冲区内存的stride行字节数不等于图像宽度乘以每像素字节数。OV5640驱动在DVP模式下可能会对行做对齐比如宽度640、RGB565格式实际每行可能是1280字节而图像数据里有额外的padding。这种情况下直接用width * bytes_per_pixel去计算行偏移就会错位。解决方法是用VIDIOC_G_FMT查询fmt.fmt.pix.bytesperline用这个值作为QImage的bytesPerLine参数QImage image(buffer, width, height, bytesperline, QImage::Format_RGB565);这个坑非常隐蔽我当初查了好几个小时才定位到分享出来希望大家别走弯路。5.3 QT缺少serialport模块报错虽然摄像头显示项目不直接涉及串口但很多开发者在同一个QT工程里既要做摄像头又要做串口通信编译时经常遇到s:-1: error: unknown module(s) in qt: serialport。这个报错的意思是交叉编译的QT库里没有编译QSerialPort模块。解决办法有两个一个是在QT源码configure的时候不要-skip qtserialport重新编译QT让它带上这个模块另一个是单独下载qtserialport源码和QT版本匹配后用同样的交叉工具链编译安装。不想重新编QT的话也可以用系统自带的POSIX串口API直接操作/dev/ttySx或者/dev/ttymxcx代码量也不大。如果你是在正点原子出厂系统上开发系统自带的QT一般已经包含serialport模块。如果还报错多半是你自己在PC上用了Qt Creator交叉编译qmake指向的是没有serialport的普通桌面QT检查一下qmake -v输出对应的路径是不是ARM版。5.4 跑QT程序提示找不到Qt库交叉编译的QT程序拷贝到板子上执行时提示error while loading shared libraries: libQt5Core.so.5: cannot open shared object file。这是因为板子上没有安装对应的QT运行库或者LD_LIBRARY_PATH没有设置。排查方法是先看程序依赖哪些库arm-linux-gnueabihf-readelf -d your_program | grep NEEDED然后在板子上用ldd或者直接搜索确认这些库是否存在。我习惯把/opt/qt5.15.2-arm/lib整个目录拷贝到板子写进/etc/profile这样所有QT程序都能找到。如果不想改动全局环境也可以在启动脚本里临时设置。还有一个容易忽略的地方是linuxfb插件本身是一个动态库在/opt/qt5.15.2-arm/plugins/platforms目录下。QT_QPA_PLATFORMlinuxfb只是设置名字加载这个插件还是靠libQt5PlatformSupport等库。如果只拷了lib没拷plugins程序会提示找不到linuxfb解决办法是把整个/opt/qt5.15.2-arm目录完整拷贝到板子上。5.5 画面延迟大、帧率低画面延迟大首先确认是不是采集本身的问题。可以用v4l2-ctl --stream-mmap --stream-count100采集100帧统计耗时算出实际帧率。如果V4L2层帧率正常那就是QT显示链路的问题。重点排查以下几点一是有没有做了不必要的像素拷贝二是update()触发的是局部重绘还是全屏重绘控制区域越小越省CPU三是采集线程和主线程之间信号传递是否有性能瓶颈QImage不要太大640x480够用就不要上1080P。我在板子上实测640x48030fpspaintEvent方式刷新CPU占用大约40%。如果你发现CPU占用特别高试着把QT_QPA_PLATFORMlinuxfb换成QT_QPA_PLATFORMlinuxfb:fb/dev/fb0指定fb设备有时候QT会默认扫描多个fb导致额外开销。6. 最后再分享一点实战体会这个项目做完之后我最大的感受是嵌入式视频显示链路的难点不在QT也不在V4L2而是在两者的交接处。V4L2侧要理解缓冲区循环、mmap语义和像素格式QT侧要理解QImage的内存所有权和linuxfb的刷新机制把这两套思维模型接上整个项目就通透了。之前我一直觉得直接指向V4L2 buffer构造QImage是最快的方案但实际测试下来除非你能精确控制缓冲区的生命周期否则一次深拷贝换来的稳定性和头皮安宁非常值得。另外建议大家在做这个项目之前先准备好三件工具v4l2-ctl、media-ctl和gdb。前两个能帮你快速确认摄像头链路是否正常把QT从排查范围里摘出去gdb用于处理段错误和野指针问题尤其是QImage跨线程传递时的崩溃用gdb看backtrace可以快速定位是哪个函数在访问非法内存。我见过太多人一黑屏就怀疑QT代码结果摄像头驱动压根没配置好白白浪费一天时间。先分层验证再逐层联调这套方法论在这个项目里帮了我大忙。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进