ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

V4L2+Qt在IMX6ULL上实现OV5640摄像头实时预览的完整方案

V4L2+Qt在IMX6ULL上实现OV5640摄像头实时预览的完整方案 OV5640模块插上IMX6ULL开发板的CSI接口开机屏幕却没有画面——这是很多第一次在正点原子开发板上玩摄像头的朋友都会碰到的事。厂家的测试程序能出画面网上也能找到一堆sensor寄存器配置的片段但真想把这些帧收进自己的Qt界面里实时显示能直接照抄的完整工程反而很少。这篇文章就把我调试这套环境时的完整思路、可运行代码和踩坑记录整理出来基于正点原子IMX6ULL开发板、OV5640模块和Linux系统用最直接的方式把实时预览跑起来。最终效果很明确开发板的LCD屏幕上显示摄像头实时画面窗口里能叠加自己的控件代码自己维护、自己编译不依赖厂家GUI。这套方案适合刚接触嵌入式Linux和Qt开发、想自己做图像采集与界面显示的工程师也适合准备把摄像头做成产品原型的学生。你看完不只拿到代码还会明白每一步为什么这么选。1. 为什么我选中了V4L2Qt这条路线1.1 三种主流实现方案的横向对比在IMX6ULL这种单核A7平台上做摄像头实时显示可选的路子其实就那么几条每一条都有明显的取舍我先把对比放在这里。第一条是直接操作framebuffer也就是打开/dev/fb0把摄像头数据填到显存里。这个方案速度确实快代码量也最小但问题也很致命你没有窗口系统任何文字、图标、菜单都需要自己画一旦界面复杂起来基本就是在裸写GUI开发和维护成本极高而且和Qt生态完全脱节。第二条是走完整的GStreamer管线用gst-launch配合v4l2src和waylandsink之类的插件来出图。GStreamer功能确实强抓流、编码、推流一条龙但在IMX6ULL上有个现实问题板子没有GPU也没有VPU视频链路完全靠CPU软解GStreamer的插件链一旦拉长CPU占用就蹭蹭涨。而且交叉编译GStreamer插件尤其是带各种编码器扩展的版本依赖关系能让新手直接放弃。第三条就是本文采用的V4L2采集Qt软件渲染。V4L2是Linux内核给视频设备的标准接口驱动的事情让驱动去做应用层只管拿帧逻辑非常简单。Qt这边用QImage承载帧数据再扔给QLabel或者自绘控件显示不需要额外的显示服务。整条链路依赖少、代码自包含、UI扩展又方便是嵌入式Linux下做摄像头显示最平衡的选择。从产品开发的角度看第三条路线还有个隐性优势——问题好排查。链路就三段设备节点、采集循环、Qt显示哪一段出问题都很容易通过日志定位。前两条路线要么代码量大要么黑盒环节多出了问题很难快速收敛。1.2 目标板卡资源盘点与预期性能动手之前先对硬件资源有个清醒认知。IMX6ULL是NXP的Cortex-A7单核处理器主频528MHz内部集成了LCD控制器和CSI摄像头接口但要注意它没有GPU也没有VPU硬件编解码器更没有ISP图像处理单元。这意味着所有图像处理、像素格式转换、UI绘制全靠这一个A7核用软件扛。所以性能预期必须合理。我在这个平台上实测下来640x480分辨率、YUYV格式、30帧的采集频率下纯软件做YUYV转RGB888的像素转换一帧大约需要20到30毫秒再加上Qt的软件渲染整体帧率能稳定在15到25帧已经算很好了。如果你的应用还要同时跑网络、数据库之类的任务建议把采集分辨率降到320x240或者把显示帧率限制在15帧CPU占用会明显下降。内存方面IMX6ULL的开发板一般配256MB到512MB DDR3Qt核心库加一个显示窗口大约占30到50MB预留4个V4L2采集缓冲区又占几MB完全够用。真正要留意的是内存带宽DMA写采集buffer的同时CPU还在做像素转换和Qt绘制总线竞争激烈时帧率会掉。后面第6章我会给具体的优化手段。2. OV5640硬件链路检查先让摄像头“活过来”2.1 CSI接口与设备节点之间的关系很多人在Qt里写代码半天发现open(/dev/video0)失败然后就开始怀疑程序其实问题往往出在更底层摄像头根本没有被系统识别。OV5640这颗500万像素sensor在IMX6ULL开发板上走的是DVP并口sensor通过一组并行数据线和行场同步信号把图像传给SoC的CSI控制器同时通过I2C接口交换控制命令。这里要建立一个基本认知OV5640不是USB摄像头不会即插即用。Linux内核需要先通过I2C探测到sensor正确初始化寄存器然后才能注册出/dev/video0这个V4L2设备节点。正点原子的出厂系统里设备树已经做了相关配置通常启动日志里能看到类似ov5640 2-003c: Detected OV5640 sensor的信息这时/dev/video0就存在了。检查设备节点就用最直接的三板斧ls -l /dev/video* cat /sys/class/video4linux/video0/name dmesg | grep -i ov5640如果name显示的是ov5640或者包含camera字样说明设备节点已经注册成功。如果/dev/video*下面什么都没有说明sensor没有被内核识别这时候直接去应用层写代码纯属白费功夫。2.2 验证摄像头是否真的在出数据设备节点存在不代表sensor真的在往外吐数据。我最常碰到的坑是节点在但打开后永远读不到帧或者报select超时。所以在Qt工程开工前我会先做一个最简单的链路验证用v4l2-ctl直接看sensor支持什么格式只要这一步数据能出来后面所有事都好说。v4l2-ctl --list-formats-ext -d /dev/video0正常的话你会看到OV5640支持的格式列表常见有YUYV 4:2:2和RGB565分辨率从640x480到2592x1944都有。这里有一点要注意OV5640最大支持500万像素但IMX6ULL的CSI接口带宽有限实际能稳定跑到的分辨率一般不超过1280x720我通常用640x480。如果系统里没有v4l2-ctl这个工具也可以直接写一段最小验证程序来抓一帧下面第4章的工程代码本身就是一个最好的验证工具。我的建议是先编译第4章的工程跑一次能出画面说明驱动和应用层都通不能出画面再回头查驱动和硬件这样可以避免在错误的方向上纠结。2.3 设备节点不存在时的排查思路如果你的板子上没有/dev/video0请按这个顺序排查先检查硬件连接。OV5640模块有没有插到正确的CSI排针上正点原子底板上有专门的摄像头接口注意金手指方向插反了大概率识别不到有些底板还有摄像头电源使能跳线没供电sensor自然不工作。然后看内核日志。dmesg | tail -50重点是找ov5640相关的错误常见的有failed to probe、failed to read chip id、timeout waiting for sensor之类。如果看到的是I2C通信失败的报错多半是sensor的I2C地址不对、复位引脚没有拉高、或者时钟没给上。最后查设备树。如果你自己改过设备树确认ov5640节点挂在正确的I2C总线上正点原子资料里通常已经写好了不需要动。如果出厂镜像本身就识别不了优先怀疑硬件接触和模块是不是坏了而不是去调内核。3. V4L2采集流程与Qt集成方案拆解3.1 V4L2采集的完整时序V4L2的采集流程说白了就是“向驱动要缓冲区、把缓冲区送进采集队列、再从队列里把装满数据的缓冲区拿回来”。整个时序可以概括成下面这串操作open()打开设备节点VIDIOC_QUERYCAP查询设备能力确认支持视频采集和内存映射VIDIOC_S_FMT设置采集格式宽、高、像素格式VIDIOC_REQBUFS申请缓冲区指定数量和使用V4L2_MEMORY_MMAP方式VIDIOC_QUERYBUF查询每个缓冲区的物理偏移然后mmap()映射到用户空间VIDIOC_QBUF把所有缓冲区放回驱动队列VIDIOC_STREAMON开始采集循环执行用select()或poll()等待数据就绪然后VIDIOC_DQBUF取出一个装满数据的缓冲区处理完数据后立刻VIDIOC_QBUF还回去VIDIOC_STREAMOFF停止采集munmap()解除映射close()关闭设备很多人第一次写V4L2程序时容易把DQBUF和QBUF的顺序搞反。记住一个原则驱动采集到一帧后写入队列中某个缓冲区应用层用DQBUF把它取走应用层处理完数据用QBUF把这块缓冲区还回队列让驱动继续往里写。缓冲区在“用户手里”和“驱动手里”之间反复流转这就是V4L2 mmap方式的核心模型。注意VIDIOC_S_FMT这一步。虽然驱动会尽量满足你设置的宽高和像素格式但有些sensor驱动并不支持所有参数组合你设640x480时驱动可能返回一个接近的分辨率。所以S_FMT调用之后应该再回读一次fmt.fmt.pix确认驱动实际采用的值然后以这个实际值作为后面转换和显示的基准否则容易出现画面撕裂。3.2 Qt层集成方案对比V4L2采集流程搞清楚之后下一个问题就是怎么把数据接到Qt界面里。常见做法有两种。方案一在Qt的GUI线程里放一个QTimer每隔30毫秒去执行一次DQBUF、转换、显示。这个方案代码看起来简单但有个严重问题GUI线程被摄像头读取和像素转换阻塞一旦采集卡顿整个界面就会无响应按钮点不了、窗口拖不动。而且select()等待数据的时候会把GUI事件循环卡住体验很差不推荐。方案二开一个独立的QThread采集线程循环做DQBUF、格式转换转换完毕后通过信号槽把QImage发回GUI线程GUI线程只干一件事——用QLabel::setPixmap()更新画面。这个方案把耗时操作全部挪出GUI线程界面始终流畅是实战中最常用的模式也是我最终采用的方案。跨线程用信号槽传QImage完全没问题。Qt的信号槽机制会检测到发射者和接收者不在同一个线程自动使用Qt::QueuedConnection把QImage通过事件队列投递给GUI线程线程安全由Qt框架保证。你只需要在connect()时不要强制指定Qt::DirectConnection就行。3.3 缓冲区数量与格式选择的考量V4L2的缓冲区数量在小内存嵌入式设备上是个需要权衡的设计点。设2个缓冲区内存占用最小但风险也很明显如果应用层处理一帧的时间太长两个缓冲区都还在用户手里驱动就无处写新帧要么丢帧要么阻塞。设4个缓冲区是我在IMX6ULL上实测后推荐的经验值既能容忍几帧的处理延迟内存占用也不大。有些驱动REQBUFS会直接给你4个你请求多少它不一定全给但总要实际读一下返回数量。至于像素格式OV5640在IMX6ULL CSI驱动下最常用的是YUYV和RGB565。YUYV是YUV422排列每个像素2字节RGB565也是每个像素2字节。我在工程里默认用YUYV原因是这类sensor的YUYV输出最稳定色彩也准一些。如果你用v4l2-ctl查过驱动明确支持RGB565也可以把工程里的格式改成V4L2_PIX_FMT_RGB565那样连YUV转RGB的步骤都能省掉直接把数据包装成QImage::Format_RGB565显示CPU占用还能再降一截。4. 可复制的工程代码线程采集、格式转换与界面刷新4.1 工程文件与类结构整个工程由4个文件组成cam_show.pro工程文件、main.cpp入口、mainwindow界面类、camerathread采集线程类。类职责划分得很清楚CameraThread负责打开设备、初始化V4L2、采集循环、像素格式转换然后把QImage通过frameReady信号发出去。MainWindow负责创建界面、启动采集线程、接收帧并显示。如果以后要扩展拍照、录像或者加OSD文字直接在这两个类里加逻辑就行结构不用大改。下面是完整的.pro文件QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET cam_show TEMPLATE app CONFIG c11 SOURCES \ main.cpp \ mainwindow.cpp \ camerathread.cpp HEADERS \ mainwindow.h \ camerathread.hCONFIG c11必须加因为代码里用到了QAtomicInt和lambda表达式之类的C11特性平台默认编译标准较老时不开会有问题。4.2 采集线程实现细节线程类的接口设计如下头文件camerathread.h#ifndef CAMERATHREAD_H #define CAMERATHREAD_H #include QThread #include QImage #include QAtomicInt class CameraThread : public QThread { Q_OBJECT public: explicit CameraThread(QObject *parent nullptr); ~CameraThread(); void setDevice(const QString dev); void setPixelFormat(unsigned int fmt); void setSize(int width, int height); void stop(); signals: void frameReady(const QImage frame); protected: void run() override; private: bool openDevice(); bool initV4L2(); void closeDevice(); QString m_device; unsigned int m_pixelFormat; int m_width; int m_height; QAtomicInt m_running; int m_fd; int m_bufferCount; void *m_buffers[4]; size_t m_bufferLength[4]; }; #endif // CAMERATHREAD_HQAtomicInt用来做停止标志跨线程访问没有任何风险比普通的bool加锁更轻量。m_buffers数组我固定给4个指针对应最多4个V4L2缓冲区如果你的驱动返回的缓冲数量超过4再做动态扩展。接下来是核心实现camerathread.cpp里的初始化流程。设备打开之后先查能力再设格式这是V4L2标准流程bool CameraThread::openDevice() { m_fd ::open(m_device.toLocal8Bit().constData(), O_RDWR | O_NONBLOCK); if (m_fd 0) { qCritical(open %s failed: %s, qPrintable(m_device), strerror(errno)); return false; } struct v4l2_capability cap; memset(cap, 0, sizeof(cap)); if (ioctl(m_fd, VIDIOC_QUERYCAP, cap) 0) { qCritical(VIDIOC_QUERYCAP failed: %s, strerror(errno)); return false; } if (!(cap.capabilities V4L2_CAP_VIDEO_CAPTURE)) { qCritical(device %s is not capture device, qPrintable(m_device)); return false; } return true; }这里我用O_NONBLOCK打开设备配合后面循环里的select()等待避免DQBUF在无数据时死等。V4L2设备用阻塞模式也能配合select使用但非阻塞模式能让超时和错误处理更干净属于久经考验的写法。初始化采集参数这一步是整套代码的重点。把格式设置、缓冲区申请、mmap映射、入队、启动采集全部串起来bool CameraThread::initV4L2() { struct v4l2_format fmt; memset(fmt, 0, sizeof(fmt)); fmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width m_width; fmt.fmt.pix.height m_height; fmt.fmt.pix.pixelformat m_pixelFormat; fmt.fmt.pix.field V4L2_FIELD_NONE; if (ioctl(m_fd, VIDIOC_S_FMT, fmt) 0) { qCritical(VIDIOC_S_FMT failed: %s, strerror(errno)); return false; } m_width fmt.fmt.pix.width; m_height fmt.fmt.pix.height; struct v4l2_requestbuffers req; memset(req, 0, sizeof(req)); req.count m_bufferCount; req.type V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory V4L2_MEMORY_MMAP; if (ioctl(m_fd, VIDIOC_REQBUFS, req) 0) { qCritical(VIDIOC_REQBUFS failed: %s, strerror(errno)); return false; } m_bufferCount req.count; for (int i 0; i m_bufferCount; i) { struct v4l2_buffer buf; memset(buf, 0, sizeof(buf)); buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; buf.index i; if (ioctl(m_fd, VIDIOC_QUERYBUF, buf) 0) { qCritical(VIDIOC_QUERYBUF failed: %s, strerror(errno)); return false; } m_buffers[i] mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, buf.m.offset); if (m_buffers[i] MAP_FAILED) { qCritical(mmap failed: %s, strerror(errno)); return false; } m_bufferLength[i] buf.length; if (ioctl(m_fd, VIDIOC_QBUF, buf) 0) { qCritical(VIDIOC_QBUF failed: %s, strerror(errno)); return false; } } enum v4l2_buf_type type V4L2_BUF_TYPE_VIDEO_CAPTURE; if (ioctl(m_fd, VIDIOC_STREAMON, type) 0) { qCritical(VIDIOC_STREAMON failed: %s, strerror(errno)); return false; } return true; }有个细节很多人忽视VIDIOC_S_FMT之后要用返回的fmt.fmt.pix.width和fmt.fmt.pix.height回读实际分辨率。因为驱动可能不接受你原始请求的值而是调整到最接近且支持的分辨率。我没用几个临时变量去接收而是直接覆盖了m_width和m_height保证后续所有处理跟实际采集分辨率保持一致这个细节能避免很多花屏和错位问题。采集线程的run()函数是全程跑在后台线程的里面是一个无限循环用select()等待数据然后DQBUF取帧void CameraThread::run() { if (!openDevice() || !initV4L2()) { closeDevice(); return; } while (m_running.load() 0) { fd_set fds; FD_ZERO(fds); FD_SET(m_fd, fds); struct timeval timeout; timeout.tv_sec 2; timeout.tv_usec 0; int ret select(m_fd 1, fds, NULL, NULL, timeout); if (ret 0) { if (errno EINTR) continue; qCritical(select error: %s, strerror(errno)); break; } if (ret 0) { qWarning(select timeout, no frame); continue; } struct v4l2_buffer buf; memset(buf, 0, sizeof(buf)); buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; if (ioctl(m_fd, VIDIOC_DQBUF, buf) 0) { if (errno EAGAIN) continue; qCritical(VIDIOC_DQBUF failed: %s, strerror(errno)); break; } QImage frame convertYUYVToRGB888( static_castconst unsigned char *(m_buffers[buf.index]), m_width, m_height); ioctl(m_fd, VIDIOC_QBUF, buf); emit frameReady(frame); } closeDevice(); }注意这个顺序convertYUYVToRGB888执行完之后才QBUF还缓冲区。这是因为缓冲区里存的是YUYV原始数据转换函数是基于它生成一幅全新的QImage源数据必须在这段时间内保持不被驱动覆盖。QBUF之后驱动的DMA随时可能往这块内存写数据所以决不能提前还。像素转换函数我用的是C定点整数算法避免浮点运算在无FPU的平台上省下大量时间static QImage convertYUYVToRGB888(const unsigned char *src, int width, int height) { QImage image(width, height, QImage::Format_RGB888); unsigned char *dst image.bits(); int pixelCount width * height; for (int i 0; i pixelCount / 2; i) { unsigned char y0 src[0]; unsigned char u src[1]; unsigned char y1 src[2]; unsigned char v src[3]; src 4; int c0 y0 - 16; int c1 y1 - 16; int d u - 128; int e v - 128; int r0 (298 * c0 409 * e 128) 8; int g0 (298 * c0 - 100 * d - 208 * e 128) 8; int b0 (298 * c0 516 * d 128) 8; int r1 (298 * c1 409 * e 128) 8; int g1 (298 * c1 - 100 * d - 208 * e 128) 8; int b1 (298 * c1 516 * d 128) 8; r0 r0 255 ? 255 : (r0 0 ? 0 : r0); g0 g0 255 ? 255 : (g0 0 ? 0 : g0); b0 b0 255 ? 255 : (b0 0 ? 0 : b0); r1 r1 255 ? 255 : (r1 0 ? 0 : r1); g1 g1 255 ? 255 : (g1 0 ? 0 : g1); b1 b1 255 ? 255 : (b1 0 ? 0 : b1); *dst r0; *dst g0; *dst b0; *dst r1; *dst g1; *dst b1; } return image; }这是标准的BT.601满范围转RGB公式系数和偏移参考了行业通用的整数近似表色偏控制得不错。IMX6ULL没有硬浮点单元整数运算比浮点快得多这一版转换在640x480分辨率下每帧省下不少时间。4.3 界面刷新与线程通信界面类mainwindow职责非常简单。一个QLabel铺满窗口一个采集线程指针一个槽函数接收帧// mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow class QLabel; class CameraThread; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); ~MainWindow(); private slots: void showFrame(const QImage frame); private: QLabel *m_label; CameraThread *m_cameraThread; }; #endif // MAINWINDOW_H实现文件里关键就两个地方。构造函数里创建界面并启动线程槽函数里只做显示#include mainwindow.h #include camerathread.h #include QLabel MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { m_label new QLabel(this); m_label-setAlignment(Qt::AlignCenter); setCentralWidget(m_label); resize(800, 480); m_cameraThread new CameraThread(this); connect(m_cameraThread, CameraThread::frameReady, this, MainWindow::showFrame, Qt::QueuedConnection); m_cameraThread-setDevice(/dev/video0); m_cameraThread-setPixelFormat(V4L2_PIX_FMT_YUYV); m_cameraThread-setSize(640, 480); m_cameraThread-start(); } MainWindow::~MainWindow() { if (m_cameraThread) { m_cameraThread-stop(); m_cameraThread-wait(2000); } } void MainWindow::showFrame(const QImage frame) { m_label-setPixmap(QPixmap::fromImage(frame)); }connect的第五个参数Qt::QueuedConnection在这里可以省略因为Qt会自动判断跨线程但显式写出来更清晰。帧数据以值方式传递QImageQt内部有隐式共享机制并不会发生深拷贝性能开销很小。wait(2000)这一行容易被人遗漏。窗口关闭时如果采集线程还在跑、还在emit信号界面对象已经销毁程序直接崩溃。这里先调stop()设置原子标志位让循环退出然后wait(2000)等待线程真正结束确保了安全退出。析构函数里这段代码是嵌入式Qt程序活得长久的秘诀之一。5. 交叉编译、部署与首屏验证5.1 准备交叉编译环境代码写好了下一步是在PC上交叉编译出能在IMX6ULL板上运行的ARM版本。这里有个很容易踩的坑很多人直接用开发板自带的源码包里的Qt却用PC上的qmake去编译最终链接到了x86体系结构的Qt库拷到板子上跑起来直接段错误。所以第一步必须是确认qmake是ARM版的。正点原子资料包的“开发工具”目录里通常会提供交叉编译器比如arm-linux-gnueabihf-gcc一族出厂系统镜像里如果有现成的Qt库也一定会带对应的交叉qmake。我习惯把工具链和Qt库统一放在/opt下面然后这样定位source /opt/qt5.12.9-arm/environment-setup-armv7ahf-neon-poky-linux-gnueabi qmake -v执行qmake -v后如果输出里出现QMake version 3.1 Using Qt version 5.12.9 in /opt/qt5.12.9-arm/lib这样的信息就说明已经切到了ARM版。千万别在一个终端里混用PC版和ARM版qmake最好为嵌入式Qt单独开一个构建目录避免Makefile污染。工程文件所在目录下执行qmake cam_show.pro make -j4编译完用file命令看产物类型file cam_show输出应该包含ARM, EABI或者32-bit ARM字样看到x86-64说明你还在用PC版工具链需要重新配置。5.2 部署到开发板并运行假设开发板的IP是192.168.1.10用户是root用scp把二进制拷到板子上scp cam_show root192.168.1.10:/root/然后登录板子启动前设置Qt运行环境。在linuxfb插件下Qt不会自动去找帧缓冲设备必须显式告诉它用哪个fb设备export QT_QPA_PLATFORMlinuxfb:fb/dev/fb0:rotation0 export QT_QPA_FONTDIR/usr/share/fonts export LD_LIBRARY_PATH/usr/lib:/usr/local/lib ./cam_showLCD屏不同fb设备节点也可能不同。用cat /sys/class/graphics/fb0/name确认你的屏幕对应的是fb0还是fb1弄错了画面要么黑屏要么花屏。QT_QPA_FONTDIR这个环境变量不能省。如果板端没有/usr/share/fonts目录Qt在显示文字时会找不到字体严重时直接崩溃。你可以用ls /usr/share/fonts确认一下没有就去正点原子镜像里拷贝一份字体目录过去。5.3 拿到清晰画面的判断标准程序跑起来后屏幕中央应该出现摄像头实时画面且画面方向和摄像头朝向一致颜色正常没有明显花屏、撕裂。判断链路是否健康的几个标志控制台没有任何错误输出特别是没有DQBUF failed、select timeout之类的刷屏画面连贯性稳定左右晃动摄像头时没有明显的跳动或绿色重影窗口拖动或叠加控件时采集画面依然在刷新说明GUI线程没有被卡住如果画面出来了但颜色整体发绿或者有明显斜向条纹基本就是驱动实际输出格式和你的设置不一致。这时候先执行v4l2-ctl --list-formats-ext看驱动支持的格式清单把代码里的像素格式改成驱动确认支持的那一个重新编译。我见过好几次出厂驱动默认输出的是RGB565代码却按YUYV解析出来的画面那是相当的“环保”。6. 实测踩坑记录与性能优化方向6.1 我遇到过的5个常见问题把测试过程中的高频问题整理成一张表每一条都是真实踩过的排查思路也一并写上现象根本原因解决办法open /dev/video0 failed设备节点不存在检查摄像头连接、dmesg查驱动确认/dev/video0存在select timeout一直刷屏sensor没有正常出数据检查供电跳线、复位引脚用厂家的摄像头测试程序交叉验证画面表现为绿屏或花屏像素格式不匹配用v4l2-ctl --list-formats-ext确认驱动实际输出格式改代码里pixelFormat画面清晰但出现撕裂分辨率与实际设置不一致回读S_FMT之后的实际宽高用实际值做转换和显示退出程序时崩溃线程还在emit信号窗口已销毁析构函数先stop()再wait(2000)确保线程完全停止选超时的排查有一点要提醒IMX6ULL的CSI接口对电源纹波很敏感OV5640的供电质量差会表现为“设备节点正常但就是没有帧”。正点原子底板上摄像头接口的供电由某路电源控制如果你同时接了其他大功率外设拉低了电压就会出现这种诡异故障。排查时优先把无关外设拔掉再试。关于绿屏再多说两句。如果你确认驱动输出是YUYV还是偏绿那多半是OV5640的AEC/AGC没有收敛。在摄像头正对较亮光源后一秒钟内颜色会恢复正常这是sensor自动曝光的正常过程。但长时间不恢复就要检查I2C通信稳定性可以在dmesg里看有没有I2C传输错误。6.2 性能优化与CPU占用控制IMX6ULL毕竟只是单核A7优化思路不是“让程序跑更快”而是“别做无谓的事”。第一控制采集分辨率。640x480的YUYV数据每次QImage构造加格式转换大约要20毫秒320x240则大幅下降到5毫秒以内。如果你的应用只是监控预览320x240完全够用需要看清细节时再把分辨率提升。分辨率的选择直接决定了系统的整体余量。第二减少不必要的拷贝。当前代码里convertYUYVToRGB888返回的QImage通过信号槽发送时会走QPixmap转换这一步其实会复制一次图像数据。在600x480这个量级还能接受如果你想进一步优化可以在MainWindow里直接保存一份固定的QImage用于缓存缩放只在帧尺寸不变时复用。不过这个优化优先级不高数据量不大时显卡瓶颈不明显。第三降低Qt刷新频率。摄像头输入可能是30帧但Qt界面在linuxfb插件下每帧都是全屏重绘30帧的软件渲染对A7来说负担不小。一个常用的技巧是在采集线程里做帧率控制只每隔一帧发一次frameReady信号让界面刷新频率保持在15帧左右人眼几乎看不出差别CPU占用却能降一半。第四考虑NEON优化。如果你熟悉ARM NEON指令集YUYV转RGB这个操作特别适合用SIMD向量化能提速3到5倍。但NEON对编译器和代码结构有要求现阶段可以先留着等产品功能稳定后再做避免过早优化增加代码复杂度。6.3 更进一步的玩法工程跑通以后可以按自己的需求扩展。我比较推荐的三个方向都是从这段基础代码再往前走一步就能接上的。方向一是拍照。给MainWindow加一个QShortcut捕获当前显示的QImage直接save(/home/root/snapshot.jpg)。注意往SD卡或Flash写文件有时延最好在采集线程之外做免得把显示卡顿。方向二是叠加OSD。在QLabel上层加一个透明子控件重写paintEvent绘制时间戳、帧率或者设备状态摄像头显示加UI图层的能力是Qt方案相对于裸framebuffer最大的价值所在。方向三是网络推流。把采集到的图像数据交给RTP或者RTSP模块送出去可以实现远程监控但IMX6ULL没有VPUH.264软编码720p跑不动比较现实的目标是480p或QVGA加低码率。这几个方向都不是贴上去就能用的功能但基础链路已经完全打通剩下的就是数据获取和分发层面的工作量。对于需要更多图像处理的项目也可以考虑在采集线程里加OpenCV的接口把帧转成cv::Mat做OpenCV处理再送回Qt显示——那是另一个相对长的故事了。最后说点我个人的实际体会。嵌入式Linux摄像头显示这个需求说难不难说简单也不简单最容易翻车的其实是把问题定位到错误的层。我这几次调试下来最大的经验就是严格按“先驱动、再采集、后UI”的顺序排查先用dmesg和v4l2-ctl确认驱动和sensor活得好好的再用一段不涉及Qt的最小代码验证能拿到帧最后才打开界面工程调显示。直接拿着Qt程序里的报错去猜底层原因十有八九会绕远路。这套V4L2加Qt的组合对IMX6ULL这种没有GPU、没有VPU、没有ISP的“三无”平台来说已经是软件层面最省力又最通用的实现了。按这篇文章的步骤走你应该很快能看到摄像头画面出现在自己的Qt窗口里。
返回列表