Zynq UltraScale+ MPSoC 的 Qt 程序编译,是很多人第一次拿到这块芯片时的第一道坎。它不像单片机那样丢个交叉编译器就完事,也不像普通 ARM Linux 那样在板子上apt install qtbase5-dev就能跑。MPSoC 里同时存在 A53 应用核、GPU、显示控制器和 FPGA 逻辑,Qt 程序跑在 A53 上,渲染要跟 GPU 驱动、DRM 显示栈打交道,所以整个过程涉及交叉编译工具链、sysroot、Qt 库源码配置、平台插件部署、运行时环境变量等一堆环节。这篇文章就是把整个流程从头到尾拆开讲一遍,包含我实际调试中反复踩过的问题和最终采用的方案,适合刚拿到 Zynq UltraScale+ 板子、准备做 HMI 或者图像显示交互的开发者参考。
我默认你的目标环境是:Zynq UltraScale+ 运行 PetaLinux 或基于 Yocto 的 Linux,Qt 应用最终以 eglfs 方式跑在 A53 核上。如果你的项目使用 Wayland 或者 X11,部分配置参数和应用场景需要微调,但交叉编译的思路完全一样。
1. 动手之前,先想清楚:交叉编译还是板上编译
1.1 MPSoC 上跑 Qt 的硬件视角
Zynq UltraScale+ MPSoC 不是一颗纯 FPGA,也不只是一颗 ARM 芯片。它的应用处理单元里放了四核 Cortex-A53,旁边还有 Cortex-R5、Mali GPU、视频编解码单元,以及一大片可编程逻辑。Qt 程序最终是跑在 A53 上的 Linux 用户空间里,但它要不要调用 GPU,直接决定了编译库时的依赖关系。
如果你的界面是传统 QWidget,绝大部分绘制走 CPU,用 raster 后端就够了,对 EGL/OpenGL ES 的依赖很弱;但只要你用 Qt Quick、QML 动画、或者希望在界面上叠加缩放/旋转/半透明效果,就一定会用到 GPU。Qt 在嵌入式 Linux 上的 EGLFS 插件正是通过 EGL/GBM 和 DRM 把渲染结果直接送到底层显示控制器。换句话说,编译 Qt 库之前你得先搞清楚目标平台提供了哪些 GPU 驱动头文件和用户态库,不然 configure 阶段就会把 OpenGL ES 支持检测成 no。
另一个需要重视的是,MPSoC 的显示通路在不同板卡上的配置差异很大。有的板子从 DisplayPort 输出,有的走 HDMI,有的通过 FPGA 逻辑把 AXI 总线的显示数据搬到屏上。即使 Qt 库编译完全正确,只要 DRM 设备路径或显示时序不对,运行起来照样黑屏。所以“编译”这件事,从一开始就不是编译器的独立动作,而是整个显示链路的一部分。
1.2 本地编译与交叉编译的取舍
在动手配置环境前,先回答一个问题:你到底是交叉编译,还是把整个 Qt 源码拿到板子上本地编译?
我见过有人把所有依赖都放到板子上,然后用apt装完 Qt,再直接在板子上qmake && make编译自己的工程。这种做法的好处是库和工具链完全匹配,不用折腾 sysroot,调试小型工程比较方便。但坏处也很明显:A53 的处理能力有限,编译一个完整 Qt 库往往要几个小时,而且反复清理重新编译会让人崩溃。更重要的是,产品交付阶段需要可复现的构建脚本,不可能每次都依赖一块板子的本地环境。
交叉编译的优势在于速度和可控性。你可以在高性能主机上编译整个 Qt 库和应用程序,产出二进制后统一部署到目标板。代价是要维护一套完整的交叉编译环境,包括工具链、sysroot、pkg-config 路径、目标设备依赖库。我的建议很简单:只要你不是只做几分钟的小验证,一律走交叉编译;板上本地编译只用来验证某个运行时小问题,不要作为主力构建方式。
1.3 为什么显示相关选型会影响编译参数
我在实际项目中见过太多“能编过,但跑不起来”的例子,根源都在于编译 Qt 的时候没有把目标显示后端考虑进去。Qt 源码编译时,configure 脚本会自动检测 sysroot 里的 EGL、OpenGL ES、Fontconfig、DBus 等库。检测不到,它就会静默关闭相关功能,最后你得到的 Qt 只是一个“功能性残疾”的版本。
比如你只用了-linuxfb,没有开-eglfs,程序在板子上运行时只能走纯 framebuffer,不仅刷新慢,而且根本没法发挥 GPU 能力;反过来,如果你开了-eglfs但 sysroot 里没有匹配的 Mali EGL 头文件和 libmali 库,配置阶段就会把 EGLFS 标记为 no,让你误以为是自己参数写错了。所以我一直建议,交叉编译环境搭建阶段,先花十分钟确认目标板 rootfs 里的 GPU 用户态库是什么、头文件在哪、DRM 设备节点是否存在,再往下走。
2. 搭建交叉编译环境:sysroot 是真正的核心
2.1 用官方 SDK 还是手工做 sysroot
交叉编译最容易搞错的不是编译器本身,而是 sysroot。所谓 sysroot,就是目标板 rootfs 的一个“快照”,里面包含目标系统上的/usr/include、/usr/lib、/lib等目录。交叉编译时,头文件和库都从这个 sysroot 里找,而不是从主机的/usr/include找。如果你直接用 Ubuntu 的库去交叉编译,链接阶段会分分钟报出各种架构不兼容错误。
最稳妥的做法是直接从 PetaLinux 导出 SDK。在 PetaLinux 工程目录下执行:
petalinux-build --sdk生成的 SDK 安装脚本安装完成后,用source进入环境,会得到$SDKTARGETSYSROOT这个关键变量。sysroot 里已经包含与目标 rootfs 一致的头文件、库和 pkg-config 文件,这是我要强调的第一条经验:优先用官方 SDK 提供的 sysroot,不要手工从板子上拷目录。手工拷不仅容易漏掉符号链接,还会把一些板子上没有的 debug 文件带进来,后面排查问题会被引入歧途。
如果实在需要手工制作 sysroot,至少要注意保留/lib和/usr/lib下的符号链接,不能只拷贝实体文件。检查 sysroot 是否正常,可以用:
file $SDKTARGETSYSROOT/usr/lib/libc.so输出里应该明确包含aarch64字样的架构信息,而不是x86-64。
2.2 准备 Qt 的系统依赖
Qt 编译时并不是只编译 Qt 自己的源码,它会检测一堆外部依赖。常见的有zlib、libpng、freetype、fontconfig、glib、dbus、libinput、tslib。这些依赖必须存在于 sysroot 中,而且必须是目标架构 aarch64 的版本,不能拿主机上的 x86 库凑数。
在开始编译 Qt 前,先检查 sysroot 里有哪些 pkg-config 文件:
ls $SDKTARGETSYSROOT/usr/lib/pkgconfig | grep -E "freetype|fontconfig|libpng|glib|dbus"如果你发现缺了几个,优先去 rootfs 对应的包管理源里找 dev 包,而不是自己用主机上的源码编完再拷进去。手工往 sysroot 里补库虽然可行,但版本匹配很难控制。另外,pkg-config 的路径必须重定向到 sysroot,否则 configure 脚本会使用主机上的.pc文件,导致检测结果完全不准确。可以这样设置:
export PKG_CONFIG_SYSROOT_DIR=$SDKTARGETSYSROOT export PKG_CONFIG_PATH=$SDKTARGETSYSROOT/usr/lib/pkgconfig:$SDKTARGETSYSROOT/usr/share/pkgconfig这一步不设置的话,你后续看 configure 输出会一脸疑惑:为什么明明装了库,检测结果还是 no。
2.3 选择 Qt 版本:别急着上最新版
Zynq UltraScale+ MPSoC 上最常见的 Qt 版本是 5.15 LTS 分支。原因很简单,嵌入式 Linux 的 Qt 支持里,Qt 5.15 的 EGLFS 插件非常稳定,社区资料也多,遇到问题很容易搜到解决方案。Qt 6 当然可以跑,但它对 OpenGL ES 的版本要求更高,CMake 集成的变化也很大,如果板卡 BSP 比较老,会额外增加很多适配工作。
我个人的建议是:如果新项目没有必须用 Qt 6 新特性的需求,优先选 Qt 5.15.2 版本。这个版本在 ARM Linux 生态里已经过长期验证,坑基本都被踩平了。另外要注意区分“只编译应用”和“重编译 Qt 库”。如果目标 rootfs 里已经带了 Qt 库,你可以直接在应用工程里链接它,根本不需要自己编整个 Qt。只有在需要修改 Qt 源码、裁剪模块、或者 rootfs 里的 Qt 版本太老无法满足需求时,才值得走一遍 Qt 源码编译流程。
3. 编译 Qt 库:configure 参数决定能不能点亮屏幕
3.1 一份可复用的 configure 示例
如果你确定要自己编译 Qt 库,configure 参数就必须仔细斟酌。下面是我在 MPSoC 上用过的一套比较干净的配置:
./configure -opensource -confirm-license \ -release \ -prefix /opt/Qt \ -sysroot $SDKTARGETSYSROOT \ -xplatform linux-aarch64-gnu-g++ \ -eglfs -linuxfb \ -opengl es2 \ -fontconfig -system-freetype \ -dbus -no-pch \ -nomake examples -nomake tests \ -no-gtk -no-xcb这里逐个说为什么这么写。-xplatform linux-aarch64-gnu-g++是 Qt 5.15 自带的 AArch64 目标平台描述文件,它会告诉 configure 使用aarch64-linux-gnu-g++编译器。-sysroot指定 SDK 环境里的 sysroot,所有头文件和库的查找都会限制在这个目录内。-eglfs和-opengl es2是为了让 Qt 能通过 GPU 渲染并直接输出到 DRM 显示设备。-linuxfb保留一个 framebuffer 后端作为调试兜底,万一 eglfs 起不来还能退回 LinuxFB 排查。-no-xcb是因为一般的 MPSoC 镜像不会装 X11 服务,编译 xcb 插件纯粹浪费时间。-no-pch在交叉编译时经常能避免一些莫名其妙的预编译头文件报错。
如果你的目标系统走 Wayland,可以把-no-xcb改成-wayland,但需要确保 sysroot 里有 libwayland-dev。没有 Wayland 需求的情况下,不建议为它增加依赖。
3.2 configure 输出怎么读:模块列表和 QPA
configure 结束时会打印一个庞大的摘要,很多人直接忽略它,这是错误的。你需要重点检查几行:
EGLFS ...................... yes OpenGL ES 2.0 .............. yes Fontconfig ................. yes LinuxFB .................... yes如果这里显示 no,不要继续make,否则编出来的 Qt 会缺失核心能力。EGLFS 显示 no 时,优先检查 sysroot 里有没有EGL/egl.h和GLES2/gl2.h。你可以在板卡 BSP 的 GPU 用户态库安装包里找到这些头文件。把它们放进 sysroot 之后,重新运行 configure。
有时候eglfs显示 yes 但运行时却崩溃,这种情况大多不是 Qt 编译参数的问题,而是 libmali 和 Qt 编译时看到的 EGL 头文件版本不一致。所以我会在编译 Qt 之前先跑通一个最小的 EGL 测试,比如eglinfo,确认板子上 EGL 初始化正常,再继续 Qt 编译流程。
3.3 编完后的产物检查
make -j$(nproc)完成后,不要急着make install,先在构建目录里检查产物:
find plugins/platforms -name "*.so"你至少应该看到libqeglfs.so和libqlinuxfb.so,前者是 GPU 渲染的直接输出插件,后者是 framebuffer 兜底插件。再检查lib/libQt5EglFs.so是否存在,这个库是 EGLFS 插件高度依赖的运行时模块。如果没有它,部署到板上十有八九会报“找不到平台插件”。
make install之后,用file检查安装后的库架构:
file /opt/Qt/lib/libQt5Core.so输出必须包含ARM aarch64或类似的 AArch64 架构描述。如果你发现这里显示是 x86 架构,那就是 sysroot 路径设置错了,趁早回头找原因,不要往下走。
4. 编译自己的 Qt 工程:qmake 和 CMake 都要会一点
4.1 用目标 qmake 构建工程
Qt 库交叉编译完成后,目标 Qt 目录下的bin/qmake就是你所有工程都要用的构建工具。直接在工程目录里运行:
/opt/Qt/bin/qmake 工程.pro make -j8这里有一个非常隐蔽的坑:如果你在普通终端里运行qmake,很可能调用的是宿主机/usr/bin/qmake,它负责的是 x86 版 Qt,编译出来的 Makefile 会把你的代码连到错误的目标库上。所以要么在构建脚本里写死绝对路径,要么在编译每个工程前用which qmake确认路径。
在 Qt Creator 里也可以配一套交叉编译 Kit:编译器选择aarch64-linux-gnu-gcc/g++,Qt Version 指向/opt/Qt/bin/qmake,Sysroot 指向$SDKTARGETSYSROOT。这样你在 IDE 里按一下构建,它就会用目标 Qt 的 qmake 生成 Makefile,再调用交叉编译器产出 aarch64 的二进制。工程里如果链接 OpenCV 之类的第三方库,注意INCLUDEPATH和LIBS必须指向 aarch64 版本的库路径,不能把主机/usr/lib/x86_64-linux-gnu里那套拷过去。
4.2 CMake 工具链文件的写法
现在越来越多的 Qt 工程用 CMake 而不是 qmake 构建,尤其新项目。你需要写一个toolchain.cmake:
set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(CROSS_COMPILE aarch64-linux-gnu-) set(CMAKE_C_COMPILER ${CROSS_COMPILE}gcc) set(CMAKE_CXX_COMPILER ${CROSS_COMPILE}g++) set(CMAKE_SYSROOT $ENV{SDKTARGETSYSROOT}) set(CMAKE_FIND_ROOT_PATH $ENV{SDKTARGETSYSROOT} /opt/Qt) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)构建时的命令是:
cmake -DCMAKE_TOOLCHAIN_FILE=./toolchain.cmake -DCMAKE_PREFIX_PATH=/opt/Qt/lib/cmake .. make -j8CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER是为了让 CMake 在找 pkg-config、编译工具等程序时使用主机的程序,而不会去 sysroot 里找一个无法执行的目标版本。LIBRARY和INCLUDE模式设为ONLY,则是强制所有库和头文件的查找只在 sysroot 和 /opt/Qt 里进行,杜绝误入主机路径。
如果你在 CMake 构建时看到/usr/include/...的 header 出现在编译命令中,几乎可以断定是CMAKE_FIND_ROOT_PATH_MODE_INCLUDE没设置成ONLY,或者CMAKE_SYSROOT设置无效。这种问题排查起来特别恶心,因为报错往往很随机,比如某一次编译找不到某个头文件,下一次换了依赖顺序又好了。
4.3 部署目录规划
编译通过只完成了第一步。把二进制部署到板子上时,不能只拷app可执行文件。Qt 程序运行时需要库、平台插件、QML 模块等一大堆文件。我的部署目录通常是这样:
/opt/app/bin/app /opt/Qt/lib/libQt5Core.so.5 /opt/Qt/lib/libQt5Gui.so.5 /opt/Qt/lib/libQt5Widgets.so.5 /opt/Qt/plugins/platforms/libqeglfs.so /opt/Qt/plugins/egldeviceintegrations/libqeglfs-kms-integration.so为了搞清楚可执行文件到底依赖了哪些 Qt 库,在交叉编译环境的 sysroot 下使用:
aarch64-linux-gnu-readelf -d app | grep NEEDED这样能看到它链接的所有动态库名称。不要直接使用宿主机的ldd,因为它会试图在 x86 下解析 aarch64 二进制,结果基本不可靠。把需要的库和插件都放进 rootfs,再部署到目标板。
5. 部署运行时的坑:编译过了不等于能跑
5.1 找不到平台插件
最经典的报错是这样的:
qt.qpa.plugin: Could not find the Qt platform plugin "eglfs"很多时候第一反应是插件没拷贝过去,但实际上可能是动态库路径不对。Qt 程序启动时,先要加载libQt5EglFs.so,然后再去plugins/platforms/下找libqeglfs.so。如果LD_LIBRARY_PATH没有包含/opt/Qt/lib,动态链接器连前者都找不到,表现就是“找不到平台插件”。所以在板子上先设置一套完整的运行环境:
export QT_QPA_PLATFORM=eglfs export QT_QPA_PLATFORM_PLUGIN_PATH=/opt/Qt/plugins/platforms export LD_LIBRARY_PATH=/opt/Qt/lib如果还找不到,可以用LD_DEBUG=libs临时打开动态链接调试日志,看看它到底加载到哪一步失败了,排查完再关闭。这个变量在交叉环境里尤其有用,能直接定位是插件缺失还是插件依赖的某个libQt5Core没找到。
5.2 eglfs 黑屏或崩溃
程序起来了,但屏幕黑着,或者进程一启动就 Segmentation fault。这个问题大概率不在 Qt 库本身,而在 DRM/EGL 设备互通。
先检查/dev/dri节点:
ls -l /dev/dri/card*如果设备权限是0660,而你启动程序的用户不在 video 组,就会打不开设备。最直接的规避方式是用 root 用户跑,或者写一条 udev 规则:
KERNEL=="card*", SUBSYSTEM=="drm", GROUP="video", MODE="0660"放在/etc/udev/rules.d/99-gpu.rules下重启即可。另外有些板卡有多个 DRM 设备,Qt 默认选择的 card 不一定对应实际 HDMI/DP 接口。这时可以强制指定:
./app -platform eglfs:fb=/dev/dri/card0如果手头有多个显示输出,先去/sys/class/drm/下面看看每个 card 对应的是哪个接口,再决定用哪个。
黑屏还有一种常见原因:libmali用户态库和 Qt 源码编译时用的 EGL 头文件版本不匹配。这种问题比较隐蔽,因为编译和启动都不报错,只是显示不出来。解决办法是直接换用 rootfs 里带的 Qt 版本,或者将 BSP 中的 libmali 替换为与 Qt 编译时匹配的版本。先跑一个最小的eglinfo或 Qt 的 hellowindow 例子,就能快速确认是不是这个层面的问题。
5.3 字体、触摸输入和运行时目录
界面跑起来之后,紧接着遇到的一般是字体方块。Qt 在 eglfs 模式下不会主动搜索所有系统字体目录,需要手动指定。把中文字体,比如文泉驿或者思源黑体的 ttc/ttf 文件,放到/usr/share/fonts/truetype/,然后在启动脚本里设置:
export QT_QPA_FONTDIR=/usr/share/fonts/truetype如果还是不生效,检查 sysroot 里的 fontconfig 库是否在编译 Qt 时被检测到。如果没检测到,Qt 就会忽略系统字体配置。
触摸屏调试也要单独处理。先在板子上运行:
cat /proc/bus/input/devices找到触摸设备的/dev/input/eventX,然后在启动脚本里给 Qt 指定设备参数:
export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event1如果你的 BSP 使用 libinput 管理输入设备,可能还需要确认 Qt 编译时启用了 libinput 插件。否则会出现鼠标能动、触摸没反应,或者触摸坐标完全错乱的问题。
再看一个小毛病:Qt 程序启动时经常有这种警告:
QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-root'它不影响大多数程序运行,但如果你做的是需要保存配置文件或缓存的工程,最好提前创建这个目录并设置好权限:
mkdir -p /tmp/runtime-root chmod 700 /tmp/runtime-root export XDG_RUNTIME_DIR=/tmp/runtime-root5.4 一个可以直接用的启动脚本模板
把前面所有经验集中到一个脚本里,可以少踩很多坑:
#!/bin/sh export QT_QPA_PLATFORM=eglfs export QT_QPA_PLATFORM_PLUGIN_PATH=/opt/Qt/plugins/platforms export LD_LIBRARY_PATH=/opt/Qt/lib export QT_QPA_FONTDIR=/usr/share/fonts/truetype export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event1 export XDG_RUNTIME_DIR=/tmp/runtime-root mkdir -p $XDG_RUNTIME_DIR chmod 700 $XDG_RUNTIME_DIR exec /opt/app/bin/app "$@"注意脚本里没有在exec一行后面再加-platform eglfs参数,因为环境变量已经指定了,保持一个入口更干净。如果你的板卡 DRM 设备比较特殊,在这条 exec 的参数里追加-platform eglfs:fb=/dev/dri/card0也是一种办法,但我不建议同时用环境和参数两种方式指定,容易混乱。
6. 我踩过坑之后的几条实在体会
6.1 版本匹配是第一个大坑
工具链版本、sysroot、BSP 里的 GPU 驱动、Qt 源码版本,这四个东西必须锁成一个组合。我最初在某块板子上用较新的 Qt 5.15 源码去配一套旧 BSP,编译完全通过,部署上去后 eglfs 也能点亮,但运行十几分钟就随机崩溃。后来换了 rootfs 里自带的 Qt 版本,同一份应用代码就稳定了。问题根源就是用户态 libmali 和 Qt 编译时看到的 EGL 头文件之间存在细微差异。这类问题靠读日志很难直接定位,最好的办法是保持整体版本一致性,不要随意升级其中的某一环。
6.2 能用现成包就别自己编
如果 PetaLinux 配置里已经包含了qtbase、qtdeclarative、qtsvg这些模块,直接启用它们,然后只交叉编译自己的应用工程,是最省心的路径。自己从头编 Qt 意味着你以后要长期维护一套 configure 参数,还要在 PetaLinux 版本升级时处理各种依赖变化。只有在需要修改 Qt 源码、定制底层模块、或者 rootfs 里的 Qt 无法满足功能需求时,才值得走完整源码编译流程。这不是懒,而是把精力留给真正需要解决的问题。
6.3 先从最小显示 Demo 开始
不要第一次就在板子上部署一个完整的大型 Qt 程序。先用 Qt 源码里自带的hellowindow示例交叉编译并部署,确认窗口能正常点亮屏幕,再逐层加入控件、动画、业务代码。这一步能过滤掉 80% 的环境问题。如果 hellowindow 能正常跑,你后续遇到问题就基本可以集中在应用代码层面;如果 hellowindow 都黑屏,那就要回头查 sysroot、GPU 驱动和平台插件,不要带着一个庞大的工程去排查环境问题。
最后再分享一个实战小技巧:当显示输出不对时,优先怀疑QT_QPA_PLATFORM=eglfs后没有指定 DRM 设备,其次怀疑LD_LIBRARY_PATH把系统库路径给盖掉了。把所有 Qt 相关库集中到一个专用目录,把这个目录作为唯一的运行时库路径,不要到处叠加多个版本目录,能避免很多诡异的加载冲突。这套流程虽然看起来繁琐,但只要环境理顺过一次,后续换板子、换 Qt 版本时就会顺畅很多。