交叉编译与部署
# 第 12 章 交叉编译与部署
本章定位:从 x86 开发机到 ARM 设备点亮屏幕。x86 上编译通过的代码拷贝到 ARM 板上黑屏无反应——90% 是交叉编译链没搭对。本章覆盖 toolchain 搭建、Qt 源码交叉编译、CMake 嵌入式配置、sysroot 构建与最小依赖部署,以及 OTA 热更新——让 QML 应用从"能编译"到"能部署在产线上"。
# 目录介绍
# 12.1 案例引入
# 12.1.1 ARM 黑屏
某车机工程师在 Ubuntu x86 上开发了一个仪表盘 QML 应用——运行完美,FPS 60。拷贝到 ARM Cortex-A53(Raspberry Pi 4)上,满心期待地敲下 ./myapp -platform eglfs,结果是:
-bash: ./myapp: No such file or directory
但 ls -la ./myapp 显示文件确实存在:
-rwxr-xr-x 1 root root 245K Jun 24 10:00 ./myapp
三连排查:
| 错误信息 | 根因 | 修复 |
|---|---|---|
No such file or directory(文件存在) | ELF 的 interpreter(动态链接器)路径在 ARM 上不存在:/lib/ld-linux-armhf.so.3 | 在 sysroot 中安装匹配的 glibc |
error while loading shared libraries: libQt6Quick.so.6 | 未部署 Qt .so 库 | 拷贝 lib/ 目录 |
QQmlApplicationEngine failed to load component | QML plugins 缺失:platforms/libqeglfs.so 未部署 | 拷贝 plugins/ 目录 |
# 12.1.2 根因分析
每条错误对应一个部署缺口:
① "No such file or directory"(文件存在):
x86 ELF: INTERP /lib64/ld-linux-x86-64.so.2
ARM ELF: INTERP /lib/ld-linux-armhf.so.3
→ ARM 板上没有 /lib/ld-linux-armhf.so.3 → bash 报"文件不存在"
② "libQt6Quick.so.6 not found":
ldd ./myapp → 列出 50+ 个 .so 依赖
→ 部署时忘了拷贝 Qt 的运行库
③ "QQmlApplicationEngine failed":
QML 依赖的 QPA 插件不在部署目录
→ 部署时忘了 plugins/platforms/libqeglfs.so
# 12.1.3 三大问题
| 问题 | 在哪节回答 |
|---|---|
| 怎么在 x86 上编译出 ARM 二进制?sysroot 是什么? | §12.2 + §12.3 |
| 编译完后拷贝哪些文件到 ARM 板上才能跑? | §12.4 |
| 已经部署到产线的设备怎么静默升级 QML? | §12.5 |
# 12.1.4 ELF 探秘
"文件存在但 bash 报 No such file or directory"——这句话背后是 ELF 文件格式和内核加载流程。
ELF Header 里的 INTERP 字段
每个动态链接的 ELF 可执行文件里嵌了一个解释器路径,告诉内核"用这个程序来加载我":
# 查看 x86 和 ARM 二进制的 INTERP 差异
readelf -l build-x86/dashboard | grep INTERP
# INTERP /lib64/ld-linux-x86-64.so.2 ← x86 的动态链接器
aarch64-linux-gnu-readelf -l build-arm/dashboard | grep INTERP
# INTERP /lib/ld-linux-aarch64.so.1 ← ARM64 的动态链接器
内核加载 ELF 的三步流程:
execve("./myapp") → 内核:
1. 读 ELF Header → 找到 INTERP: /lib/ld-linux-aarch64.so.1
2. 尝试打开 /lib/ld-linux-aarch64.so.1 ← ARM 板上这个文件存在吗?
├── 存在 → 加载 ld.so → ld.so 再加载 libc, libQt6*.so ...
└── 不存在 → 返回 ENOENT ("No such file or directory")
↑ 这个错误消息说的是 解释器 不存在,
不是说 可执行文件 不存在!
为什么 bash 的报错如此误导?
bash 调用 execve() 失败后,它只知道系统返回了 ENOENT——但 execve 不区分"可执行文件没找到"和"解释器没找到"。两者返回同一个错误码。bash 别无选择,只能说 No such file or directory。
RPATH 与 RUNPATH——内嵌的库搜索路径
ELF 里除了 INTERP,还可以嵌入库搜索路径,让部署不依赖 LD_LIBRARY_PATH:
# 查看当前二进制的 RPATH/RUNPATH
readelf -d build-arm/dashboard | grep -E "RPATH|RUNPATH"
# 编译时设置——让 Qt .so 的搜索路径内嵌在 ELF 里
cmake -B build-arm \
-DCMAKE_INSTALL_RPATH="/opt/dashboard/lib" \ # 内嵌搜索路径
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON
# 效果: 不需要 start.sh 设 LD_LIBRARY_PATH,直接 ./myapp 就能跑
| 搜索机制 | 设置方式 | 优先级 | 是否绕过 |
|---|---|---|---|
DT_RPATH | 编译时 -Wl,-rpath,/opt/app/lib | 高 | 需要用 chrpath 修改 |
DT_RUNPATH | CMake INSTALL_RPATH | 中 | LD_LIBRARY_PATH 可覆盖 |
LD_LIBRARY_PATH | 运行时环境变量 | 中高 | 受 secure-execution 限制 |
/etc/ld.so.conf | 系统配置 | 低 | 仅系统目录 |
# 12.2 交叉编译原理
# 12.2.1 编译链路
x86 开发机 (Host) ARM 设备 (Target)
┌─────────────────────────┐ ┌─────────────────────────┐
│ gcc → ELF (x86-64) │ │ 需要 ARM aarch64 ELF │
│ × 不能在 ARM 跑 │ │ 从 Host 拷贝过来 │
└─────────────────────────┘ └─────────────────────────┘
↓
arm-linux-gnueabihf-gcc ← 交叉编译器
(或 aarch64-linux-gnu-gcc)
↓
生成 ARM ELF → 拷贝到 ARM 板 → ./myapp
交叉编译的三要素:
| 要素 | 说明 | 例子 |
|---|---|---|
| 交叉编译器 | 运行在 x86、输出 ARM 指令的工具链 | arm-linux-gnueabihf-gcc / aarch64-linux-gnu-gcc |
| sysroot | ARM 根文件系统的镜像——含所有头文件和 .so | /home/user/raspi-sysroot/ |
| 目标设备配置 | Qt 的 -device 参数——对应 ARM 板的 mkspecs | linux-rasp-pi4-g++ / linux-imx8-g++ |
sysroot 的本质——它是 ARM 板的完整文件系统副本:
sysroot/
├── usr/
│ ├── include/ ← 交叉编译时 `#include <xxx>` 从这里找
│ │ ├── stdio.h
│ │ ├── GLES2/gl2.h
│ │ └── ...
│ └── lib/
│ ├── arm-linux-gnueabihf/
│ │ ├── libc.so.6 ← 交叉链接时从这里找
│ │ ├── libGLESv2.so
│ │ └── ...
│ └── ...
├── lib/
│ └── ld-linux-armhf.so.3 ← ARM 的动态链接器
└── opt/vc/ ← Raspberry Pi 专有:GPU 库
├── include/
└── lib/
# 12.2.2 Qt 特殊性
Qt 编译过程除了 gcc/g++,还依赖 Host 端工具——它们运行在 x86 上、但为 ARM 目标生成代码:
| 工具 | 运行位置 | 作用 |
|---|---|---|
moc | Host(x86) | 扫描 Q_OBJECT 宏 → 生成 moc_*.cpp |
rcc | Host(x86) | 编译 .qrc 资源文件 → qrc_*.cpp |
qmlcachegen | Host(x86) | 预编译 .qml → .qmlc 字节码 |
uic | Host(x86) | .ui → C++ 代码 |
结论:编译 Qt 时需要两套工具——Host 工具用 x86 编译器、Target 代码用 ARM 交叉编译器。CMake 的 CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER 正是告诉构建系统"程序在 Host 找,库在 Target sysroot 找"。
# 12.2.3 工具链内幕
把 aarch64-linux-gnu-gcc 的编译过程拆开,可以看到 sysroot 是如何被注入的:
# 交叉编译器实际上是一组具有相同前缀的程序
ls /usr/bin/aarch64-linux-gnu-*
# aarch64-linux-gnu-gcc ← C 编译器
# aarch64-linux-gnu-g++ ← C++ 编译器
# aarch64-linux-gnu-ld ← 链接器
# aarch64-linux-gnu-objdump ← 反汇编
# ...
# 编译一个 .cpp 时的实际步骤
aarch64-linux-gnu-g++ -c main.cpp -o main.o \
--sysroot=/home/user/raspi-sysroot \ # ← gcc 内部参数——所有路径前加 sysroot
-I/usr/include # → 实际找 /home/user/raspi-sysroot/usr/include
aarch64-linux-gnu-ld main.o -o myapp \
--sysroot=/home/user/raspi-sysroot \
-L/usr/lib/aarch64-linux-gnu # → /home/user/raspi-sysroot/usr/lib/aarch64-linux-gnu
-L/usr/lib # → /home/user/raspi-sysroot/usr/lib
-lQt6Core
sysroot 下的多架构路径陷阱
ARM64 Debian/Ubuntu 系统使用 multiarch 路径——库不在 usr/lib/ 下,而是 usr/lib/aarch64-linux-gnu/:
rsync ARM板 → 本地 sysroot 后:
/home/user/raspi-sysroot/usr/lib/ ← 没有 .so!
/home/user/raspi-sysroot/usr/lib/aarch64-linux-gnu/ ← .so 都在这里
如果编译时报 cannot find -lGLESv2,不是库不存在——是 gcc 没找到 multiarch 子目录。需要显式指定:
./configure -sysroot /home/user/raspi-sysroot \
-L /home/user/raspi-sysroot/usr/lib/aarch64-linux-gnu \ # ← 显式添加
-I /home/user/raspi-sysroot/usr/include/aarch64-linux-gnu
工具链的三层库搜索优先级:
1. --sysroot 下的路径(交叉编译专属)
/home/user/raspi-sysroot/usr/lib/aarch64-linux-gnu/
/home/user/raspi-sysroot/usr/lib/
/home/user/raspi-sysroot/lib/
2. gcc 内部 multilib 路径(编译时嵌入的工具链默认路径)
/usr/lib/gcc-cross/aarch64-linux-gnu/12/
3. Host 系统路径(如果 CMAKE_FIND_ROOT_PATH_MODE_LIBRARY 设为 BOTH)
/usr/lib/x86_64-linux-gnu/ ← 危险:x86 的库链接到 ARM 二进制!
# 12.3 构建 Qt
# 12.3.1 sysroot 准备
方案 A:从 ARM 板 rsync 完整根文件系统:
# 在 x86 开发机上
mkdir -p ~/raspi-sysroot
rsync -avz pi@192.168.1.100:/lib ~/raspi-sysroot/
rsync -avz pi@192.168.1.100:/usr/lib ~/raspi-sysroot/usr/
rsync -avz pi@192.168.1.100:/usr/include ~/raspi-sysroot/usr/
rsync -avz pi@192.168.1.100:/opt/vc ~/raspi-sysroot/opt/ # 树莓派专有 GPU 库
# 修正符号链接——rsync 之后可能有绝对路径链接指向 /
# 需要用脚本把它们改为相对路径
wget https://raw.githubusercontent.com/riscv/riscv-poky/master/scripts/sysroot-relativelinks.py
python3 sysroot-relativelinks.py ~/raspi-sysroot
方案 B:用预构建的交叉编译工具链 + sysroot(各厂商提供):
| 平台 | 工具链 |
|---|---|
| Raspberry Pi 4 | arm-linux-gnueabihf (32-bit) / aarch64-linux-gnu (64-bit) |
| i.MX 6/8 | NXP 官方 Yocto SDK 中的 aarch64-poky-linux-gcc |
| STM32MP1 | ST 官方 OpenSTLinux SDK |
| 通用 ARM | Linaro 或 bootlin 预编译工具链 |
# 12.3.2 设备编译
# ① 下载 Qt 源码
wget https://download.qt.io/official_releases/qt/6.5/6.5.3/single/qt-everywhere-src-6.5.3.tar.xz
tar xf qt-everywhere-src-6.5.3.tar.xz && cd qt-everywhere-src-6.5.3
# ② 配置——指定 device + sysroot + 交叉编译器
./configure \
-release \
-opengl es2 \
-eglfs \
-device linux-rasp-pi4-g++ \ # 设备 mkspec
-device-option CROSS_COMPILE=aarch64-linux-gnu- \ # 交叉编译器前缀
-sysroot /home/user/raspi-sysroot \ # ARM 根文件系统
-prefix /usr/local/qt6-arm \ # 安装路径(在 ARM 板上)
-extprefix /home/user/qt6-arm-host \ # Host 上的暂存路径
-nomake examples -nomake tests \ # 不编译示例/测试
-skip qtwebengine \ # 嵌入式不需要 WebEngine
-no-feature-wayland-server
# ③ 编译
make -j$(nproc) # 约 40 分钟(取决于 CPU)
make install
# ④ 验证——查看生成的文件
file /home/user/qt6-arm-host/lib/libQt6Core.so
# → ELF 64-bit LSB shared object, ARM aarch64, ...
常用设备配置:
| 设备 | -device 参数 | 编译器前缀 |
|---|---|---|
| Raspberry Pi 4 (64-bit) | linux-rasp-pi4-g++ | aarch64-linux-gnu- |
| Raspberry Pi 3 (32-bit) | linux-rasp-pi3-g++ | arm-linux-gnueabihf- |
| i.MX8 | linux-imx8-g++ | aarch64-poky-linux- |
| STM32MP1 | linux-stm32mp1-g++ | arm-ostl-linux-gnueabi- |
# 12.3.3 CMake 集成
# toolchain-arm64.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
set(CMAKE_SYSROOT /home/user/raspi-sysroot)
# 程序(moc/rcc)在 Host 找
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# 库在 Target sysroot 找
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
# 头文件在 Target sysroot 找
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(DashboardApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
# Qt6 路径——交叉编译时指向 ARM 版的 Qt
set(CMAKE_PREFIX_PATH /home/user/qt6-arm-host)
find_package(Qt6 REQUIRED COMPONENTS Quick QuickControls2)
qt_add_executable(dashboard main.cpp)
qt_add_qml_module(dashboard
URI Dashboard
VERSION 1.0
QML_FILES main.qml SpeedGauge.qml RPMGauge.qml
RESOURCES resources.qrc
)
# 部署 QML 运行时文件
qt_generate_deploy_qml_app_script(
TARGET dashboard
OUTPUT_SCRIPT deploy_script
)
install(SCRIPT ${deploy_script})
编译三连:
# x86 开发调试
cmake -B build-x86 -DCMAKE_PREFIX_PATH=/usr/lib/qt6
cmake --build build-x86
# ARM 交叉编译
cmake -B build-arm \
-DCMAKE_TOOLCHAIN_FILE=toolchain-arm64.cmake \
-DCMAKE_PREFIX_PATH=/home/user/qt6-arm-host
cmake --build build-arm -j$(nproc)
# 打包部署
cmake --install build-arm --prefix /tmp/dashboard-package
# 12.4 设备部署
# 12.4.1 最小依赖检查
# 在 x86 开发机上用交叉 ldd 检查 ARM 二进制的依赖
aarch64-linux-gnu-readelf -d build-arm/dashboard | grep NEEDED
# → libQt6Quick.so.6, libQt6Qml.so.6, libQt6Core.so.6, ...
# 或用交叉编译工具链的 ldd
aarch64-linux-gnu-ldd build-arm/dashboard
部署文件的三类来源:
| 来源 | 文件 | 部署到 |
|---|---|---|
| Qt 安装目录(交叉编译产物) | lib/libQt6*.so | /opt/dashboard/lib/ |
| Qt plugins | plugins/platforms/libqeglfs.so | /opt/dashboard/plugins/platforms/ |
| ARM sysroot | lib/ld-linux-aarch64.so.1、libc.so.6 等系统库 | ARM 板已自带,不需部署 |
# 12.4.2 Qt 插件部署
QML 应用依赖以下插件,任何一个缺失都会导致运行时崩溃:
/opt/myapp/
├── bin/myapp ← 可执行文件
├── plugins/
│ ├── platforms/
│ │ └── libqeglfs.so ← QPA 插件(EGLFS 后端)——必须
│ ├── imageformats/
│ │ ├── libqjpeg.so ← JPEG 图片解码——如果有 Image
│ │ ├── libqpng.so ← PNG 图片解码
│ │ └── libqsvg.so ← SVG 图标——如果有
│ ├── egldeviceintegrations/
│ │ └── libqeglfs-kms-integration.so ← eglfs KMS 集成
│ └── qmltooling/
│ └── libqmldbg_*.so ← 调试工具——生产环境可省略
├── qml/
│ ├── QtQuick/ ← QML 模块
│ ├── QtQuick/Controls/ ← Controls 模块
│ ├── QtQuick/Layouts/ ← Layouts 模块
│ └── QtQuick/Shapes/ ← Shapes 模块(如果用了)
├── lib/
│ ├── libQt6Core.so.6
│ ├── libQt6Gui.so.6
│ ├── libQt6Quick.so.6
│ ├── libQt6Qml.so.6
│ ├── libQt6QuickControls2.so.6
│ └── ...
└── qml/ ← 应用的 QML 文件 + .qmlc 缓存
├── main.qml
├── SpeedGauge.qml
└── ...
Qt 6 的自动部署脚本——qt_generate_deploy_qml_app_script 自动生成部署脚本,拷贝所有需要的 QML 模块:
# 运行 CMake 生成的部署脚本
cmake --install build-arm --prefix /tmp/package
# 或手动执行
cd build-arm
./.qt/deploy_qml_imports.sh /tmp/package/qml
# 12.4.3 自包含打包
方案 A:LD_LIBRARY_PATH + 启动脚本:
#!/bin/sh
# start.sh
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
export LD_LIBRARY_PATH=$SCRIPT_DIR/lib:$LD_LIBRARY_PATH
export QT_PLUGIN_PATH=$SCRIPT_DIR/plugins
export QML_IMPORT_PATH=$SCRIPT_DIR/qml
export QT_QPA_PLATFORM=eglfs
exec $SCRIPT_DIR/bin/myapp "$@"
方案 B:AppImage 自包含——整个应用 + Qt 库打包成一个文件:
# ① 用 linuxdeployqt 打包
linuxdeployqt ./bin/myapp \
-qmldir=./qml \
-bundle-non-qt-libs \
-extra-plugins=platforms/libqeglfs.so
# ② 生成 .AppImage(可选)
appimagetool ./AppDir MyApp.arm64.AppImage
方案 C:静态链接——编译 Qt 时加 -static,应用二进制包含所有 Qt 代码:
./configure -static -release -opengl es2 -eglfs ...
# 编译产物:myapp 一个文件 = 可执行文件 + Qt + QML 引擎
# 缺点:文件大(20-50 MB)、不能热更新 QML 模块
# 12.4.4 部署验证
部署到 ARM 板后、首次启动前,用 五步验证法 避免"盲启动→崩溃→猜原因"的循环:
# Step 1: 验证 INTERP——解释器路径在 ARM 板上存在吗?
ssh root@192.168.1.100 'ls -la /lib/ld-linux-aarch64.so.1'
# → 不存在?→ 说明 glibc 未安装或 sysroot 路径不匹配
# → readelf -l myapp | grep INTERP 确认二进制里写的路径
# → 路径 /lib/ld-linux-aarch64.so.1 vs 实际 /lib/ld-linux-armhf.so.3?
# Step 2: 验证 NEEDED——所有 .so 依赖都在部署目录里吗?
ssh root@192.168.1.100 << 'EOF'
for lib in $(readelf -d /opt/dashboard/bin/dashboard | grep 'Shared lib' | awk '{print $NF}' | tr -d '[]'); do
found=$(find /opt/dashboard/lib -name "$lib" 2>/dev/null)
if [ -z "$found" ]; then
echo "❌ 缺失: $lib"
fi
done
EOF
# → 有输出?→ 部署目录少了某个 .so → 补全
# Step 3: 验证 QPA 插件——EGLFS 后端存在吗?
ssh root@192.168.1.100 'ls /opt/dashboard/plugins/platforms/libqeglfs.so'
# → 不存在?→ 严格按 §12.4.2 的目录树补全 plugins/
# Step 4: 验证 GPU 设备权限——QML 能打开 /dev/dri/card0 吗?
ssh root@192.168.1.100 'ls -la /dev/dri/card0'
# → crw-rw---- 1 root video?→ sudo usermod -aG video myapp-user
# Step 5: 验证 QML 模块——import 路径完整吗?
ssh root@192.168.1.100 'find /opt/dashboard/qml -name "qmldir"'
# → 应该看到 QtQuick/qmldir, QtQuick/Controls/qmldir 等
# → 缺失?→ 重新运行 qt_generate_deploy_qml_app_script
strace 终极法——当以上都通过但仍崩溃时
ssh root@192.168.1.100 << 'EOF'
strace -f -e trace=openat,open,stat -o /tmp/start.log \
/opt/dashboard/start.sh
# 在日志里搜索 ENOENT——找到第一个"文件不存在"的错误
grep ENOENT /tmp/start.log | head -5
EOF
# → openat(AT_FDCWD, "libQt6Core.so.6", ...) = -1 ENOENT
# → ld.so 在搜索 libQt6Core.so.6 但没找到——库里部署了但路径不对
# → 检查 LD_LIBRARY_PATH 是否设对了
# 12.5 OTA 远程更新
增量更新策略——只更新 QML 文件 + .qmlc 缓存,不动 C++ 二进制:
# 服务端:准备更新包
tar czf update.tar.gz \
main.qml \
SpeedGauge.qml \
RPMGauge.qml \
*.qmlc
# 客户端:OTA 守护进程
#!/bin/sh
# ota_update.sh
wget -O /tmp/update.tar.gz https://ota.example.com/v1.2.3/update.tar.gz
sha256=$(sha256sum /tmp/update.tar.gz | awk '{print $1}')
expected=$(curl -s https://ota.example.com/v1.2.3/sha256.txt)
if [ "$sha256" != "$expected" ]; then
echo "校验失败,放弃更新"
exit 1
fi
# 备份旧版本
cp -r /opt/myapp/qml /opt/myapp/qml.bak
# 解压新版本
tar xzf /tmp/update.tar.gz -C /opt/myapp/qml/
# 重启应用
systemctl restart myapp.service
# 如果应用启动失败——回滚
if ! systemctl is-active --quiet myapp.service; then
rm -rf /opt/myapp/qml
mv /opt/myapp/qml.bak /opt/myapp/qml
systemctl restart myapp.service
fi
完整 OTA 更新架构:
云端 OTA Server
├── /v1.2.3/
│ ├── update.tar.gz ← QML + .qmlc 更新包
│ ├── sha256.txt ← 校验和
│ └── changelog.txt
└── /v1.2.2/ (上一版本——用于回滚)
车载 ARM 设备
├── ota_client.service ← systemd 定时任务
│ └── 每小时检查一次 OTA 服务器
├── ota_update.sh ← 下载 → 校验 → 更新 → 回滚
└── myapp.service ← QML 应用
OTA 安全三件套:
| 安全措施 | 实现 |
|---|---|
| 签名验证 | gpg --verify update.tar.gz.sig update.tar.gz |
| 加密传输 | HTTPS + mTLS 双向认证 |
| 完整性校验 | sha256sum 逐包校验 |
| 原子替换 | mv 是原子操作——不出现"一半旧一半新"的状态 |
| 故障回滚 | 双槽位 A/B 分区更新——一个分区失败立刻切到另一个 |
A/B 分区原子切换——零停机更新
shell 脚本的 mv 回滚有一个致命缺陷:如果应用在启动过程中崩了,systemd 会无限重启它——不是"回滚到旧版",而是"反复崩溃"。A/B 分区从根本上解决:
设备 eMMC 分区表:
/dev/mmcblk0p1 → boot (bootloader)
/dev/mmcblk0p2 → slot_a (/opt/dashboard_a) ← 当前运行版本 v1.2.2
/dev/mmcblk0p3 → slot_b (/opt/dashboard_b) ← 空闲槽位
/dev/mmcblk0p4 → data (用户数据——不随 OTA 改变)
OTA 更新流程(双槽位):
1. 下载 v1.2.3 → 解压到 /opt/dashboard_b (不在当前运行的槽位)
2. 校验完整性(sha256 + 签名)
3. 原子切换:
# bootloader 环境变量
fw_setenv boot_slot b ← 写 U-Boot 环境变量
fw_setenv boot_attempts 3 ← 最多尝试 3 次启动
sync && reboot
4. 设备重启 → U-Boot 读 boot_slot → 挂载 slot_b
5. 启动 v1.2.3:
├── 启动成功 → systemd 通知 bootloader → boot_attempts = 0 (确认成功)
└── 启动失败(3次内)→ bootloader 自动切回 slot_a → 启动旧版 v1.2.2
A/B 切换的 systemd 集成:
# /etc/systemd/system/dashboard.service
[Unit]
Description=Dashboard HMI
After=multi-user.target
OnFailure=dashboard-fallback.service ← 崩溃时触发回滚
[Service]
ExecStart=/opt/dashboard/start.sh
Restart=on-failure
RestartSec=2s
# 健康检查——启动 30 秒后验证
ExecStartPost=/usr/bin/health-check.sh
# health-check.sh:
# sleep 30
# if ! pgrep dashboard; then
# fw_setenv boot_slot a && reboot # 切回旧槽位
# fi
| 方案 | 回滚速度 | 回滚可靠性 | 实现复杂度 |
|---|---|---|---|
mv 回滚 | < 1s(应用重启) | ⚠️ 应用崩在 systemd 循环里无效 | 低 |
| A/B 分区 | ~5s(reboot) | ✅ bootloader 级保证 | 中(需分区 + U-Boot) |
完整操作脚本——从交叉编译到 ARM 板点亮屏幕:
#!/bin/bash
# deploy-to-pi4.sh — Raspberry Pi 4 一键部署
set -e
PI_IP=192.168.1.100
PI_USER=pi
echo "=== Step 1: 交叉编译 ==="
cmake -B build-arm \
-DCMAKE_TOOLCHAIN_FILE=toolchain-arm64.cmake \
-DCMAKE_PREFIX_PATH=/home/user/qt6-arm-host
cmake --build build-arm -j4
echo "=== Step 2: 安装到临时目录 ==="
cmake --install build-arm --prefix /tmp/dashboard-pkg
echo "=== Step 3: 拷贝 Qt 运行库 ==="
cp -r /home/user/qt6-arm-host/lib/libQt6*.so* /tmp/dashboard-pkg/lib/
cp -r /home/user/qt6-arm-host/plugins /tmp/dashboard-pkg/
cp -r /home/user/qt6-arm-host/qml /tmp/dashboard-pkg/
echo "=== Step 4: 创建启动脚本 ==="
cat > /tmp/dashboard-pkg/start.sh << 'EOF'
#!/bin/sh
DIR=$(dirname "$(readlink -f "$0")")
export LD_LIBRARY_PATH=$DIR/lib
export QT_PLUGIN_PATH=$DIR/plugins
export QML_IMPORT_PATH=$DIR/qml
export QT_QPA_PLATFORM=eglfs
export QT_QPA_EGLFS_HIDECURSOR=1
exec $DIR/bin/dashboard
EOF
chmod +x /tmp/dashboard-pkg/start.sh
echo "=== Step 5: 上传到 ARM 板 ==="
rsync -avz /tmp/dashboard-pkg/ $PI_USER@$PI_IP:/opt/dashboard/
echo "=== Step 6: 在 ARM 板上启动 ==="
ssh $PI_USER@$PI_IP "sudo systemctl stop dashboard; sleep 1"
ssh $PI_USER@$PI_IP "/opt/dashboard/start.sh &"
echo "✅ 部署完成——检查仪表盘是否点亮屏幕"
跨章知识串连——这个部署脚本看似简单,背后串起了全书关键点:
| 脚本步骤 | 背后原理 | 参考章节 |
|---|---|---|
-DCMAKE_TOOLCHAIN_FILE=toolchain-arm64.cmake | 交叉编译 toolchain 的 sysroot + 编译器前缀 | §12.2 + §12.3 |
plugins/libqeglfs.so 部署 | EGLFS 后端——ARM 无桌面环境的唯一渲染路径 | §13.3 EGLFS |
QT_QPA_PLATFORM=eglfs | QPA 平台抽象层——跳过 X11/Wayland,直连 DRM/KMS | §13.2.2 QPA层 |
QT_QPA_EGLFS_HIDECURSOR=1 | 嵌入式默认无鼠标——隐藏光标 | §13.3.3 输入设备 |
LD_LIBRARY_PATH | 动态链接器搜索路径——内嵌 RPATH 更优但需编译时设 | §12.1.4 ELF探秘 |
rsync 同步部署 | sysroot 的来源就是 ARM 板的 rsync 镜像 | §12.3.1 sysroot |
部署失败的"五分钟排障法"
当 6 步脚本全部顺利执行但屏幕仍然黑屏——按这个顺序逐个排查,不跳步骤:
Step 1: 确认二进制是 ARM ELF
file /opt/dashboard/bin/dashboard
→ 必须是 "ELF 64-bit LSB executable, ARM aarch64" 而不是 "x86-64"
→ 如果看到 x86-64 → toolchain 文件没生效,CMake 用了 Host 编译器
Step 2: 确认 EGLFS 可以初始化 GPU
QT_LOGGING_RULES="qt.qpa.eglfs*=true" ./start.sh
→ 日志里有 "EGL display opened" → GPU OK
→ 日志里有 "Could not open egl display" → GPU 驱动问题 → §13.6.3
Step 3: 确认 QML engine 可以加载主文件
QML_IMPORT_TRACE=1 ./start.sh
→ 看到 "loaded main.qml" → QML OK
→ 看到 "import QtQuick not found" → qml/ 目录不完整 → §12.4.2
Step 4: strace 跟踪文件打开
strace -f -e openat ./start.sh 2>&1 | grep -E "ENOENT|qml|so"
→ 搜索第一个 ENOENT——就是缺失的文件
→ 补全 → 重新验证
# 12.7 新手陷阱
| # | 陷阱 | 说明 | 修复 |
|---|---|---|---|
| 1 | 用 x86 gcc 编译的二进制直接拷到 ARM | ELF 格式不兼容——No such file or directory | 必须用交叉编译器 |
| 2 | sysroot 没做 relativelinks | 链接指向 /lib/...(ARM 板绝对路径)→ Host 上找不到 | sysroot-relativelinks.py 脚本修正 |
| 3 | 部署时忘记 plugins/platforms/libqeglfs.so | QML 加载时找不到 QPA 插件→启动崩溃 | 部署脚本中显式列出 plugins 依赖 |
| 4 | qml/ 目录下的 QML 模块没拷贝 | QML import 路径缺失→运行时模块加载失败 | 用 qt_generate_deploy_qml_app_script 自动生成 |
| 5 | OTA 更新没有备份+回滚 | 新 QML 有 bug → 设备变砖 | 双槽位更新 + systemd 健康检查 |
# 12.8 训练题
写一个 Raspberry Pi 4 的 toolchain 文件
要求:aarch64 架构,sysroot 在 /opt/raspi-sysroot,编译器前缀 aarch64-linux-gnu-。
参考答案
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
set(CMAKE_SYSROOT /opt/raspi-sysroot)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# 12.9 速查表
| 概念 | 一句话 |
|---|---|
| 交叉编译器 | 运行在 x86、输出 ARM 指令:aarch64-linux-gnu-gcc |
| sysroot | ARM 根文件系统镜像——含所有头文件和 .so |
-device | Qt configure 的设备参数——linux-rasp-pi4-g++ |
CMAKE_TOOLCHAIN_FILE | CMake 交叉编译配置——指定编译器 + sysroot |
CMAKE_FIND_ROOT_PATH_MODE | 程序/库/头文件的查找范围控制 |
ldd / readelf -d | ARM 二进制依赖检查 |
qt_generate_deploy_qml_app_script | Qt 6 自动生成部署脚本——拷贝 QML 模块 |
rsync | 文件同步——sysroot 构建 + 部署上传 |
LD_LIBRARY_PATH | 运行时库搜索路径——嵌入式必备 |
| OTA | 远程静默升级——QML 热更新 + A/B 槽位回滚 |
核心哲学:
交叉编译 = 正确的 toolchain + 完整的 sysroot + 匹配的 device mkspec。
部署 = ldd 检查 + plugins 全部拷贝 + 启动脚本设 LD_LIBRARY_PATH。
缺任何一环 = ARM 板上的"No such file or directory"。