嵌入式Linux开发里,把Qt程序从PC搬到开发板上跑,是很多人绕不开的一道坎。我前后在RV1109和RV1126这两颗芯片上做过好几个带界面的项目,从最早的“编译报错一整天”到后来能稳定量产,中间踩的坑足够写一本小册子。这篇就把整套流程摊开讲清楚:怎么选交叉编译工具链、怎么配Qt的编译参数、怎么把程序丢到开发板上跑起来、以及那些文档里不会写的坑。不管你是刚拿到RV1126开发板的新手,还是已经能编译但总在部署环节翻车的老手,下面这些内容应该都能帮你省下不少时间。
1. 先搞清楚RV1109/RV1126这颗芯片到底适合跑什么Qt
很多人一上来就问“Qt怎么交叉编译”,但更该先问的是“我这块板子到底该跑Qt的哪个版本、用哪种渲染后端”。RV1109和RV1126是瑞芯微面向IPC和边缘视觉场景的SoC,CPU部分是Cortex-A7双核(RV1109)或四核(RV1126),主频在1.2GHz到1.5GHz之间,内存通常配512MB到1GB。这个配置跑Qt完全没问题,但前提是你得选对版本和配置,否则要么编译不过,要么跑起来卡成幻灯片。
1.1 为什么是Qt 5.12而不是Qt 5.15或Qt 6
网上关于Qt版本的选择吵得很凶,我实测下来的结论是:RV1109/RV1126优先用Qt 5.12.10或5.12.12。原因有三点。
第一,瑞芯微官方SDK里自带的Qt版本就是5.12系列,buildroot配置里直接能勾选,工具链、依赖库、字体、插件都是配套验证过的。你换成5.15.2,理论上能编,但会遇到一堆依赖库版本对不上的问题,比如libinput、libxkbcommon的API变动,改起来很烦。
第二,Qt 5.15对C++标准的要求更高,部分模块默认用C++17编译,而RV1109/RV1126的交叉工具链(通常是gcc 8.3或9.3)虽然支持,但配合glibc版本容易出链接错误。Qt 5.12对C++11的支持更稳,编译成功率明显更高。
第三,Qt 6直接不用考虑。Qt 6的图形栈依赖OpenGL ES 3.0以上和更新的CMake构建体系,RV1109/RV1126的GPU是Mali-G52(RV1126)或Mali-G31(RV1109),驱动对Qt 6的EGL集成支持不完善,跑起来大概率黑屏。
提示:如果你非要用Qt 5.15,建议用5.15.2这个版本,它是5.15系列里相对稳定的LTS版本,社区里交叉编译成功的案例也最多。但要做好改配置文件的准备。
1.2 渲染后端选EGLFS还是LinuxFB
这是新手最容易忽略、但直接影响程序能不能显示的问题。Qt在嵌入式Linux上有几种平台插件(platform plugin),常用的两种是EGLFS和LinuxFB。
EGLFS走的是GPU加速路径,通过EGL和OpenGL ES渲染,性能好,适合有动画、视频、复杂界面的场景。RV1126带Mali-G52 GPU,用EGLFS能发挥硬件性能。但EGLFS对驱动要求高,需要内核里DRM(Direct Rendering Manager)配置正确,否则启动就报“EGL not initialized”。
LinuxFB是纯软件渲染,直接写framebuffer,不依赖GPU。性能差一些,但兼容性极好,几乎不会因为驱动问题跑不起来。如果你的界面比较简单(按钮、文本、少量图片),LinuxFB完全够用,而且调试省心。
我的建议是:先用LinuxFB把流程跑通,确认Qt程序能在板子上正常显示后,再切换到EGLFS做性能优化。这样出问题时你能快速定位是Qt配置的问题还是GPU驱动的问题。
配置方式是在运行程序时指定平台插件:
# 用LinuxFB ./myapp -platform linuxfb # 用EGLFS ./myapp -platform eglfs或者在编译Qt时通过-qpa参数指定默认插件。交叉编译Qt时,./configure里加-qpa linuxfb或-qpa eglfs。
1.3 触摸屏和输入设备的前置确认
界面能显示只是第一步,触摸能不能用是另一回事。RV1109/RV1126的开发板通常配电容触摸屏,通过I2C接口连接,内核里需要加载对应的触摸驱动(比如gt9xx、gt1x系列)。
在部署Qt程序之前,先在板子上确认触摸设备节点存在:
ls /dev/input/ # 应该能看到event0、event1之类的节点 cat /proc/bus/input/devices # 查看触摸设备是否被识别然后用evtest工具测试触摸事件是否正常上报。如果触摸设备节点存在但Qt程序没反应,多半是环境变量没配对:
export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event1 export QT_QPA_GENERIC_PLUGINS=evdevtouch这两个变量告诉Qt去哪里找触摸设备。event1要换成你板子上实际的触摸设备节点,具体是哪个用evtest试出来。
2. 交叉编译工具链的选型与验证
工具链选错,后面全是白费功夫。RV1109/RV1126的官方SDK里自带交叉编译工具链,位置一般在prebuilts/gcc/linux-x86/arm/目录下,名字类似arm-rockchip830-linux-uclibcgnueabihf或arm-rockchip830-linux-gnueabihf。这两个的区别在于C库:uclibc版本体积小,适合资源紧张的场景;gnueabihf版本用glibc,兼容性好,适合跑Qt这种依赖较多的程序。
2.1 工具链的获取与安装
如果你用的是官方SDK,工具链直接就有,不用额外下载。如果是自己搭环境,可以从瑞芯微的开发者网站下载对应的工具链包。安装就是解压到某个目录,然后加到PATH里:
# 解压工具链 tar -xzf arm-rockchip830-linux-gnueabihf.tar.gz -C /opt/ # 加到环境变量 export PATH=/opt/arm-rockchip830-linux-gnueabihf/bin:$PATH # 验证 arm-rockchip830-linux-gnueabihf-gcc -v能打印出gcc版本信息就说明装好了。注意工具链的架构要和你的目标板匹配:RV1109/RV1126都是32位ARM Cortex-A7,所以用arm-linux-gnueabihf,不要用aarch64的。
2.2 用一个小程序验证工具链是否可用
装完工具链别急着编Qt,先写个hello world验证一下:
// test.c #include <stdio.h> int main() { printf("hello rv1126\n"); return 0; }编译:
arm-rockchip830-linux-gnueabihf-gcc test.c -o test file test # 应该显示: ELF 32-bit LSB executable, ARM, EABI5把test拷到开发板上运行,能打印出内容就说明工具链没问题。这一步看着简单,但能帮你排除掉“工具链本身有问题”这个变量,后面出问题时就少一个排查方向。
2.3 sysroot的配置:交叉编译最容易翻车的地方
交叉编译Qt时,sysroot是最关键也最容易出错的配置。sysroot本质上是目标板根文件系统的一个副本,编译时编译器会从这里找头文件和库。
官方SDK里通常有一个sysroot目录,路径类似buildroot/output/rockchip_rv1126_rv1109/host/arm-buildroot-linux-gnueabihf/sysroot。配置Qt的./configure时要指定:
--sysroot=/path/to/sysroot如果sysroot路径不对,或者里面缺库,编译时会报“cannot find -lxxx”或者“header not found”。我的经验是:编译Qt之前,先确认sysroot里有Qt需要的所有依赖库,包括:
- libz(zlib)
- libpng、libjpeg(图片格式支持)
- freetype(字体渲染)
- libinput或tslib(输入设备)
- libEGL、libGLESv2(如果用EGLFS)
这些库在buildroot配置里勾选后会自动编进sysroot。如果缺,要么在buildroot里补上重新编译,要么自己交叉编译后手动拷进sysroot。
注意:sysroot里的库版本要和开发板上实际运行的库版本一致。我遇到过一次,sysroot里的libpng是1.6.37,板子上是1.6.40,编译链接没问题,但运行时符号找不到直接崩溃。所以部署前一定要核对版本。
3. Qt源码交叉编译的完整配置流程
这部分是重头戏。Qt的交叉编译配置项很多,配错一个就可能编不过或者编出来跑不了。我下面给出一套在RV1126上实测通过的配置,并解释每个关键参数的作用。
3.1 下载Qt源码与准备编译目录
Qt 5.12.10的源码可以从Qt官方归档下载,国内建议用镜像站加速。下载qt-everywhere-src-5.12.10.tar.xz后解压:
tar -xf qt-everywhere-src-5.12.10.tar.xz cd qt-everywhere-src-5.12.10Qt源码体积很大,解压后有好几个G,确保磁盘空间充足。编译过程建议在单独的build目录里进行,不要污染源码目录:
mkdir build-rv1126 cd build-rv11263.2 configure参数逐项拆解
下面是我在RV1126上用的configure命令,逐项解释:
../configure \ -prefix /opt/qt5.12.10-rv1126 \ -opensource -confirm-license \ -release \ -static \ -no-pch \ -xplatform linux-arm-gnueabi-g++ \ -sysroot /path/to/sysroot \ -no-opengl \ -linuxfb \ -no-xcb \ -no-gtk \ -no-cups \ -no-iconv \ -qt-zlib \ -qt-libpng \ -qt-libjpeg \ -qt-freetype \ -nomake examples \ -nomake tests \ -skip qtwebengine \ -skip qtquickcontrols \ -skip qtlocation \ -v逐项说明:
-prefix:安装路径,编译完成后make install会装到这里。-static:静态编译。嵌入式场景推荐静态编译,生成的程序不依赖板子上的Qt库,部署简单。缺点是程序体积大,编译时间长。如果你板子存储空间紧张,可以用动态编译,但要把Qt库也拷到板子上。-xplatform linux-arm-gnueabi-g++:指定目标平台。这个对应qtbase/mkspecs/linux-arm-gnueabi-g++/qmake.conf文件,你需要修改这个文件里的编译器和sysroot路径。-no-opengl:如果不用EGLFS,直接关掉OpenGL,减少依赖。用EGLFS的话要改成-opengl es2。-linuxfb:启用LinuxFB平台插件。-no-xcb:禁用X11相关,嵌入式不需要。-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype:用Qt自带的这些库,避免依赖sysroot里的版本。这样编译出来的程序更独立。-skip qtwebengine:跳过WebEngine模块,这个模块编译极其耗时且RV1126跑不动。-nomake examples -nomake tests:不编译示例和测试,节省大量时间。
3.3 修改mkspecs文件适配工具链
-xplatform linux-arm-gnueabi-g++指向的是qtbase/mkspecs/linux-arm-gnueabi-g++/qmake.conf。你需要编辑这个文件,把里面的编译器改成你的工具链:
# qmake.conf MAKEFILE_GENERATOR = UNIX CONFIG += incremental QMAKE_INCREMENTAL_STYLE = sublib include(../common/linux.conf) include(../common/gcc-base-unix.conf) include(../common/g++-unix.conf) # 修改这里 QMAKE_CC = arm-rockchip830-linux-gnueabihf-gcc QMAKE_CXX = arm-rockchip830-linux-gnueabihf-g++ QMAKE_LINK = arm-rockchip830-linux-gnueabihf-g++ QMAKE_LINK_SHLIB = arm-rockchip830-linux-gnueabihf-g++ QMAKE_AR = arm-rockchip830-linux-gnueabihf-ar cqs QMAKE_OBJCOPY = arm-rockchip830-linux-gnueabihf-objcopy QMAKE_NM = arm-rockchip830-linux-gnueabihf-nm -P QMAKE_STRIP = arm-rockchip830-linux-gnueabihf-strip load(qt_config)改完保存,再执行configure。这一步如果没改对,configure会报“compiler not found”。
3.4 编译过程中的常见报错与处理
Qt编译时间长,中途报错很常见。我列几个遇到过的:
报错一:Project ERROR: Unknown module(s) in QT: serialport
这个是因为Qt的serialport模块没有编译。如果你程序里用了QSerialPort,需要在configure时确保serialport模块被包含。默认情况下Qt 5.12会编译serialport,但如果sysroot里缺libudev,这个模块会被跳过。解决办法是在buildroot里勾选libudev,或者手动交叉编译libudev后放进sysroot。
报错二:cannot find -lGLESv2
用EGLFS时需要链接GLESv2库。如果sysroot里没有,要么补上,要么改用LinuxFB。
报错三:编译到qtdeclarative时内存不足
Qt的QML模块编译很吃内存,如果编译机内存小于8GB,可能会被OOM kill。解决办法是减少并行编译任务数:
make -j4 # 而不是 -j8 或 -j16或者加swap。
报错四:fatal error: bits/libc-header-start.h: No such file or directory
这是sysroot路径配错或者工具链和sysroot不匹配。检查--sysroot路径是否正确,以及工具链的C库类型(uclibc还是glibc)是否和sysroot一致。
编译完成后:
make -j4 make install安装到/opt/qt5.12.10-rv1126,里面会有bin、lib、include等目录。
4. 用qmake构建你的Qt应用并部署到开发板
Qt编译好了,接下来是编你自己的程序。这里有个关键点:必须用交叉编译出来的qmake,不能用PC上的qmake。
4.1 配置Qt Creator的交叉编译套件
如果你用Qt Creator开发,需要在“工具-选项-套件”里配置交叉编译套件:
- 编译器:添加C和C++编译器,指向工具链的gcc和g++
- Qt版本:添加qmake,指向
/opt/qt5.12.10-rv1126/bin/qmake - 设备:配置开发板的SSH连接(如果支持网络部署)
配置好后,选择这个套件编译,生成的程序就是ARM架构的。
如果不用Qt Creator,直接用命令行:
/opt/qt5.12.10-rv1126/bin/qmake myproject.pro make -j4编译出来的可执行文件用file命令确认是ARM架构。
4.2 静态编译与动态编译的部署差异
静态编译的程序体积大(几十MB很正常),但部署简单,直接拷一个文件到板子上就能跑:
scp myapp root@192.168.1.100:/userdata/动态编译的程序体积小,但需要把依赖的Qt库也拷到板子上。用ldd查看依赖:
arm-rockchip830-linux-gnueabihf-ldd myapp然后把对应的.so文件拷到板子的/usr/lib或程序同目录下,并设置LD_LIBRARY_PATH:
export LD_LIBRARY_PATH=/userdata/lib:$LD_LIBRARY_PATH我的建议是:开发阶段用动态编译,改代码后重新编译快,部署也快(只传程序本身);量产阶段用静态编译,避免库版本不一致导致的问题。
4.3 开发板上的运行环境配置
程序拷到板子上后,直接运行可能报错。需要配置几个环境变量:
# 指定平台插件 export QT_QPA_PLATFORM=linuxfb # 指定触摸设备 export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event1 # 指定字体目录(如果程序用了自定义字体) export QT_QPA_FONTDIR=/usr/share/fonts # 指定插件路径(动态编译时需要) export QT_PLUGIN_PATH=/userdata/plugins这些变量可以写进/etc/profile或程序启动脚本里,避免每次手动设置。
如果程序启动后黑屏,先检查QT_QPA_PLATFORM是否设置正确,再用strace跟踪:
strace -f ./myapp 2>&1 | grep -i "open\|error"看是哪个文件或设备打不开。
4.4 开机自启动的配置
产品化时程序要开机自启动。RV1109/RV1126的buildroot系统用init.d或systemd管理启动项。最简单的方式是在/etc/init.d/下加一个启动脚本:
#!/bin/sh case "$1" in start) export QT_QPA_PLATFORM=linuxfb export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event1 /userdata/myapp & ;; stop) killall myapp ;; esac然后创建软链接到启动目录:
ln -s /etc/init.d/myapp /etc/rc3.d/S99myapp注意S99的编号要足够大,确保在网络、显示等基础服务启动后再启动你的程序。
5. 那些文档里不会写的踩坑记录
这部分是我觉得最有价值的内容。下面这些坑,每一个都让我浪费过半天以上的时间。
5.1 触摸坐标偏移和镜像问题
触摸屏能用,但点击位置和实际显示位置对不上,这是很常见的问题。原因通常是触摸设备的坐标范围和屏幕分辨率不匹配,或者触摸屏装反了。
解决办法是通过环境变量做坐标变换:
# 交换X和Y轴 export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event1:swapxy # 反转X轴 export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event1:invertx # 反转Y轴 export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event1:inverty # 组合使用 export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event1:swapxy:invertx具体用哪个组合,要看你屏幕的实际安装方向。我的经验是:先试swapxy,如果X轴对了Y轴反了,再加inverty,一般两三次就能试对。
5.2 中文字体显示方块
Qt程序在板子上跑起来,中文全变成方块,这是因为板子上没有中文字体。解决办法有两个:
一是把中文字体文件(比如wqy-zenhei.ttc或NotoSansCJK.ttc)拷到板子的字体目录:
mkdir -p /usr/share/fonts cp wqy-zenhei.ttc /usr/share/fonts/ export QT_QPA_FONTDIR=/usr/share/fonts二是在程序里用QFontDatabase::addApplicationFont()加载字体文件:
int fontId = QFontDatabase::addApplicationFont("/userdata/fonts/wqy-zenhei.ttc"); QStringList fontFamilies = QFontDatabase::applicationFontFamilies(fontId); if (!fontFamilies.isEmpty()) { QFont font(fontFamilies.at(0)); qApp->setFont(font); }第二种方式更可靠,因为字体文件跟着程序走,不依赖板子上的字体配置。
5.3 程序崩溃但没有任何输出
程序在板子上跑起来就崩,终端也没打印错误信息。这种情况通常是动态库加载失败或者段错误。排查步骤:
第一步,用ldd确认所有依赖库都能找到:
arm-rockchip830-linux-gnueabihf-ldd myapp # 如果有 "not found",说明缺库第二步,如果库都在,用gdb调试:
gdb ./myapp (gdb) run # 崩溃后 (gdb) bt看backtrace定位崩溃位置。如果板子上没有gdb,可以在PC上用gdbserver远程调试。
第三步,检查是不是内存不足。RV1109通常只有512MB内存,如果程序加载了大量图片或数据,可能OOM。用free和top查看内存使用。
5.4 静态编译后程序体积过大
静态编译的Qt程序动辄50MB以上,如果板子存储空间紧张,需要做裁剪。几个有效的手段:
- configure时加
-no-feature-xxx禁用不需要的特性 - 用
strip去掉符号表:arm-rockchip830-linux-gnueabihf-strip myapp - 用
upx压缩(但嵌入式上upx可能不兼容,慎用) - 只编译用到的Qt模块,
-skip掉其他模块
我做过一个项目,通过裁剪把程序从60MB压到了18MB,主要靠禁用WebEngine、Multimedia、3D等模块,以及strip符号表。
5.5 屏幕旋转后触摸不跟随
如果屏幕做了90度旋转(比如竖屏应用),触摸坐标也要跟着旋转。Qt本身不自动处理这个,需要手动配置:
# 屏幕旋转90度 export QT_QPA_EGLFS_ROTATION=90 # 触摸也要相应旋转 export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event1:rotate=90注意rotate参数的值要和屏幕旋转角度一致,否则触摸会错位。这个参数在Qt 5.12里支持,但不同小版本行为可能有差异,建议实测确认。
6. 性能调优与稳定性加固
程序能跑起来只是及格,要跑得流畅、跑得稳,还需要做一些优化。
6.1 减少界面重绘提升流畅度
RV1109/RV1126的CPU性能有限,如果界面频繁重绘,会明显卡顿。几个优化手段:
- 用
QWidget::setAttribute(Qt::WA_OpaquePaintEvent)告诉Qt这个控件不透明,减少背景重绘 - 避免在
paintEvent里做耗时操作,把计算逻辑放到后台线程 - 用
QStaticText代替QPainter::drawText绘制静态文本 - 图片预缩放到目标尺寸,避免运行时缩放
如果用了QML,开启QSG_RENDER_LOOP=basic可以降低渲染线程的复杂度,在低性能设备上反而更流畅。
6.2 用EGLFS提升渲染性能
如果界面有动画或视频,LinuxFB的软件渲染会力不从心,这时候要切到EGLFS。切换步骤:
第一步,重新编译Qt,configure时把-no-opengl -linuxfb改成-opengl es2 -eglfs。
第二步,确认板子内核里DRM配置正确,/dev/dri/card0存在。
第三步,运行时指定:
export QT_QPA_PLATFORM=eglfs export QT_QPA_EGLFS_INTEGRATION=eglfs_kms ./myappeglfs_kms是RV1126上验证过的集成方式,通过KMS/DRM直接管理显示。
6.3 看门狗与异常重启
产品化场景下,程序崩溃后要能自动重启。可以用一个简单的守护脚本:
#!/bin/sh while true; do /userdata/myapp echo "myapp exited, restarting..." sleep 1 done配合硬件看门狗(RV1109/RV1126内置WDT),在程序里定期喂狗,如果程序卡死,看门狗会复位系统。
6.4 日志输出与远程调试
开发阶段建议把日志输出到文件,方便排查:
./myapp > /userdata/myapp.log 2>&1 &Qt的日志可以通过qInstallMessageHandler自定义,把qDebug、qWarning等输出重定向到文件或网络。量产版本可以关掉调试日志,减少IO开销。
远程调试用gdbserver:
# 板子上 gdbserver :2345 ./myapp # PC上 arm-rockchip830-linux-gnueabihf-gdb ./myapp (gdb) target remote 192.168.1.100:2345这样可以在PC上单步调试板子上的程序,比在板子上直接调试方便得多。
7. 从开发到量产的几个关键决策
最后聊几个实际项目里会遇到的决策点,这些没有标准答案,但我的经验可以给你参考。
7.1 静态编译还是动态编译
前面提过,开发阶段动态、量产阶段静态。但还有一个考量:如果你的产品需要OTA升级,动态编译更合适,因为升级时只需要替换程序本身,不用重新烧录整个固件。静态编译的程序虽然部署简单,但每次升级都要传大文件。
我的做法是:核心程序动态编译,把Qt库打包进固件,程序通过OTA单独升级。这样兼顾了升级灵活性和部署便利性。
7.2 用buildroot还是自己搭根文件系统
瑞芯微官方SDK用的是buildroot,配置好之后一键编译整个系统,包括内核、根文件系统、Qt库。这是最省事的方式,推荐新手直接用。
如果你需要更精细的控制(比如裁剪掉不需要的组件、集成特定的库),可以自己用busybox搭根文件系统,但工作量大很多。我的建议是:先用buildroot跑通,遇到buildroot解决不了的问题再考虑自己搭。
7.3 Qt版本锁定与升级策略
项目一旦选定Qt版本,就不要轻易升级。Qt 5.12.10和5.12.12之间虽然是小版本差异,但交叉编译出来的库可能有ABI不兼容。如果团队多人开发,一定要统一Qt版本和工具链版本,最好把工具链和Qt库都放进版本控制或者内部服务器,避免“我这里能编你那里编不过”的问题。
升级Qt版本时,先在独立分支上验证,确认所有功能正常、性能没有退化后再合并。我见过因为升级Qt导致触摸驱动不兼容、界面卡顿的案例,回滚花了不少时间。
7.4 开发板与量产板的差异处理
开发板通常配置高(1GB内存、16GB存储),量产板可能缩水(512MB内存、4GB存储)。在开发板上跑得好好的程序,到量产板上可能因为内存不足崩溃。所以尽早拿到量产板做验证,不要等到最后才发现问题。
另外,量产板的屏幕、触摸芯片可能和开发板不同,驱动和设备节点都会变。部署前用evtest和ls /dev/input/重新确认设备节点,更新环境变量配置。
这套流程我在RV1109和RV1126上都跑通过,核心步骤是一样的,差异主要在GPU驱动和内存配置上。RV1126性能更强,可以开EGLFS跑复杂界面;RV1109建议用LinuxFB,界面做简单些。实际做项目时,先把最小系统跑通(一个空白窗口能显示、能响应触摸),再逐步加功能,这样出问题容易定位。交叉编译这事,坑是躲不完的,但把上面这些关键点抓住,能省下大部分折腾的时间。