Qt嵌入式从零基础到精通(01):Qt嵌入式开发全景图:从桌面Qt到ARM Linux
系列定位:Qt 5.15.2 + C++ + qmake + Embedded Linux,兼顾 QWidget/QML,面向希望从桌面 Qt 进入 ARM Linux 嵌入式开发的工程师。
文章目录
- [Qt嵌入式从零基础到精通(01):Qt嵌入式开发全景图:从桌面Qt到ARM Linux](#Qt嵌入式从零基础到精通(01):Qt嵌入式开发全景图:从桌面Qt到ARM Linux)
-
- 本章学习目标
- 前言
- [1. Qt 嵌入式到底在解决什么问题](#1. Qt 嵌入式到底在解决什么问题)
- [2. 桌面 Qt 与嵌入式 Qt 的真正差异](#2. 桌面 Qt 与嵌入式 Qt 的真正差异)
- [3. 一条完整的数据与运行链路](#3. 一条完整的数据与运行链路)
- [4. QWidget 与 QML 的定位](#4. QWidget 与 QML 的定位)
- [5. 这套系列会学到什么](#5. 这套系列会学到什么)
- [本章书稿级扩展:建立完整的 Qt 嵌入式工程认知](#本章书稿级扩展:建立完整的 Qt 嵌入式工程认知)
-
- [1. 从"能运行"到"能交付"是两回事](#1. 从“能运行”到“能交付”是两回事)
- [2. Qt 在整个系统中的位置](#2. Qt 在整个系统中的位置)
- [3. 一个工业 Qt 应用的典型模块](#3. 一个工业 Qt 应用的典型模块)
- [4. 嵌入式 Qt 的四种时间尺度](#4. 嵌入式 Qt 的四种时间尺度)
- [5. 跨平台代码应该怎么写](#5. 跨平台代码应该怎么写)
- [6. 建立"数据链路图"习惯](#6. 建立“数据链路图”习惯)
- [7. 一个最低限度的产品基线](#7. 一个最低限度的产品基线)
- [8. 本章工程练习](#8. 本章工程练习)
- 常见问题与排查表
- 动手实验与验收
- 案例推演:从现象到根因
-
- [ARM 工业屏"界面能开但设备数据全为空"](#ARM 工业屏“界面能开但设备数据全为空”)
- API、命令与工程要点速查
- 端到端实战任务
- 工程决策原则
- 章节复习题
- 工程检查清单
- 本篇小结
- 系列导航
本章学习目标
完成本章后,你不仅应该"知道是什么",还应能够:
- 解释关键机制为什么这样设计;
- 根据日志、调用链和系统状态判断故障属于哪一层;
- 写出一个可运行、可验证、可退出的小型工程;
- 把本文方法迁移到真实 ARM Linux 项目。
前言
很多 Qt 开发者第一次接触"嵌入式 Qt"时,会误以为需要重新学一套 Qt。实际上,Qt 的 API 大部分仍然是熟悉的 QObject、信号槽、QWidget/QML、QThread、QNetwork、QSerialPort;真正新增的是 CPU 架构、Linux 运行环境、交叉编译、目标板图形栈和硬件通信。
这一篇不急着写代码,而是先把整个技术栈串起来。后面 35 篇文章,基本都可以挂到这张"全景图"上。
1. Qt 嵌入式到底在解决什么问题
典型设备包括工业控制屏、采集终端、检测设备、车载屏、医疗设备、广告机、机器人控制面板等。
可以把系统分为五层:
text
┌────────────────────────────┐
│ Qt Application │ QWidget / QML / C++
├────────────────────────────┤
│ Qt Framework │ Core / Gui / Widgets / Network
├────────────────────────────┤
│ QPA / Graphics Backend │ EGLFS / LinuxFB / Wayland / XCB
├────────────────────────────┤
│ Embedded Linux │ glibc / systemd / device node
├────────────────────────────┤
│ Hardware │ ARM / GPU / LCD / UART / CAN / USB
└────────────────────────────┘
Qt 主要帮助我们解决"应用层跨平台开发"和"图形 UI + 事件驱动 + 通信"的问题;驱动、内核、BSP 仍然属于 Linux/芯片平台范畴。
2. 桌面 Qt 与嵌入式 Qt 的真正差异
桌面 Windows 上:
text
Qt Creator → MSVC → x86_64 exe → Windows
嵌入式 Linux 上:
text
Qt Creator → ARM 交叉编译器 → ARM ELF → Linux 开发板
代码层面可能一模一样:
cpp
auto *button = new QPushButton(tr("启动"), this);
connect(button, &QPushButton::clicked, this, &MainWindow::startDevice);
但构建产物完全不同。最常见的新手错误就是:拿 x86_64 程序复制到 ARM 板上运行 ,最后得到 cannot execute binary file。
3. 一条完整的数据与运行链路
把"写代码到设备运行"完整展开:
text
源码
↓
qmake / CMake
↓
ARM GCC/G++
↓
链接 ARM Qt + ARM 第三方库
↓
生成 ARM ELF
↓
scp / rsync / OTA
↓
开发板
↓
动态链接器加载 Qt .so
↓
QPA 选择 eglfs/linuxfb/wayland/xcb
↓
GPU/DRM/FB 驱动
↓
LCD
任何一层不匹配,都可能导致程序"编译成功但运行失败"。因此嵌入式调试的核心不是猜,而是逐层验证。
4. QWidget 与 QML 的定位
工业项目中 QWidget 依然大量存在,优势是 C++ 绑定直接、控件成熟、维护成本低。
QML/Qt Quick 更适合:
- 动画丰富
- 触控交互
- 车载/消费电子
- 需要 GPU 场景图加速的 UI
常见工程边界:
text
QML / QWidget:展示层
↓
Controller / ViewModel
↓
Service
↓
Protocol / Device
↓
Linux / Hardware
不要因为用了 QML 就把串口协议、业务状态机全部写进 QML。
5. 这套系列会学到什么
后续内容分成七段:
- Linux、ARM、ABI、交叉编译、Sysroot
- Qt 5.15.2 交叉编译与 Qt Creator Kit
- QObject、信号槽、事件循环、QThread
- 串口、TCP、UDP、CAN、GPIO、I2C、SPI、USB
- 配置、日志、SQLite、WIdget/QML
- EGLFS/LinuxFB/Wayland、部署、systemd、性能、崩溃
- 架构、综合项目、发布检查与进阶路线
目标不是"看完 36 篇",而是具备独立定位 Qt 嵌入式问题的能力。
本章书稿级扩展:建立完整的 Qt 嵌入式工程认知
1. 从"能运行"到"能交付"是两回事
刚开始学习时,我们通常把目标定义为:把一个带按钮的 Qt 界面运行到开发板上。这个目标很重要,但只完成了产品工程的一小部分。真正能交付的嵌入式 Qt 软件还需要同时满足:
text
可启动
可通信
可恢复
可诊断
可升级
可长期运行
例如串口设备被拔掉以后,程序是崩溃、卡住,还是进入 Offline 并自动恢复;服务器关闭以后,TCP 是否无限高频重连;数据库所在磁盘写满以后,程序是继续吃内存还是进入可控降级。这些才是产品级软件和 Demo 的区别。
2. Qt 在整个系统中的位置
不要把 Qt 想象成一个"操作硬件的万能框架"。Qt 主要位于用户空间:
text
业务逻辑 / UI
↓
Qt Core / Gui / Widgets / Quick / Network
↓
Linux libc / socket / ioctl / epoll / filesystem
↓
Linux Kernel Driver
↓
UART / CAN / USB / GPU / LCD / GPIO
因此,当 QSerialPort::open() 失败时,必须继续向下看:设备节点是否存在、权限是否正确、驱动是否加载、USB 是否枚举成功。反过来,当 /dev/ttyUSB0 正常,而 UI 仍无数据显示,就应该向上检查协议拆包、线程信号槽和 UI 刷新。
3. 一个工业 Qt 应用的典型模块
建议从第一天就把系统理解为模块,而不是把所有逻辑堆到 MainWindow:
text
Application
├── UI
│ ├── MainWindow
│ ├── DevicePage
│ └── SettingsPage
├── Device
│ ├── DeviceService
│ ├── SerialWorker
│ └── ProtocolCodec
├── Network
│ └── TcpClient
├── Storage
│ ├── Repository
│ └── DbWorker
├── Config
│ └── ConfigManager
└── Diagnostics
└── LogManager
这里最重要的不是目录名字,而是依赖方向:UI 可以依赖 Service,但 Protocol 不应该反过来依赖 QWidget。
4. 嵌入式 Qt 的四种时间尺度
开发时很容易只关注"当前点击有没有反应",但产品运行涉及不同时间尺度:
- 毫秒级:串口收包、UI 刷新、事件循环。
- 秒级:心跳、超时、重连。
- 小时级:日志滚动、缓存增长。
- 天/月级:内存泄漏、数据库膨胀、FD 泄漏、升级兼容。
所以一个设计看起来在 10 分钟内完全正常,并不能说明它适合设备长期运行。
5. 跨平台代码应该怎么写
不要在业务层到处这样写:
cpp
#ifdef Q_OS_WIN
// 一套逻辑
#else
// 另一套逻辑
#endif
更好的方式是封装平台能力:
cpp
class IDeviceEnumerator
{
public:
virtual ~IDeviceEnumerator() = default;
virtual QStringList enumerate() = 0;
};
Windows、Linux 分别提供实现。业务层只面向接口。
6. 建立"数据链路图"习惯
任何功能都可以画成数据链路。例如设备温度显示:
text
传感器
→ UART
→ Linux tty driver
→ QSerialPort::readyRead
→ rxBuffer
→ ProtocolCodec
→ DeviceFrame.temperature
→ DeviceService::temperatureChanged
→ MainWindow slot
→ QLabel
出现"温度不更新",沿这条链逐点加证据即可。这个方法比在 UI 层反复修改代码有效得多。
7. 一个最低限度的产品基线
第一版产品至少应该具备:
- 启动日志:版本、Qt 版本、编译时间、目标架构。
- 通信状态:串口/TCP 在线与离线原因。
- 所有耗时任务不阻塞 UI。
- 关键缓存有最大上限。
- 线程和 QObject 有明确销毁路径。
- 设备断开和网络断开可以恢复。
- 可收集日志、配置和 Core Dump。
这些内容会贯穿整套系列。
8. 本章工程练习
建立一个 HelloEmbeddedQt 工程,不需要真实硬件,但要求包含:
text
MainWindow
DeviceService(Mock)
QTimer 模拟设备数据
状态栏显示 Online/Offline
日志输出版本和线程 ID
每 100ms 生成数据,但 UI 只每 500ms 刷新一次。通过这个小工程提前理解"采集频率"和"UI 刷新频率"为什么应该分离。
常见问题与排查表
| 现象 | 优先排查 |
|---|---|
| ARM板提示 cannot execute binary file | 先用 file MyApp 检查是否误编成 x86_64;再用 uname -m 检查目标 CPU。 |
| 程序架构正确但启动失败 | 继续检查动态链接器、Qt 动态库和 QPA,不要再纠结 CPU。 |
| 同一套源码 Windows 能跑、Linux 不能跑 | 从平台相关代码、文件路径、大小写、权限、动态库和 QPA 分层排查。 |
动手实验与验收
建议不要只阅读,本篇至少完成下面的实验:
- 在 PC 上分别记录
uname -m与一个 Qt 可执行文件的file输出。 - 画出自己的目标项目从 UI 到硬件的五层架构图。
- 列出项目里 3 个属于 Qt 问题、3 个属于 Linux/驱动问题的例子。
验收标准: 完成实验后,不看文章也能解释关键调用链、失败时的排查顺序,以及哪些问题属于 Qt 层、Linux 层或硬件层。
案例推演:从现象到根因
ARM 工业屏"界面能开但设备数据全为空"
现场现象
程序启动正常,UI 可操作,但温度、电压始终显示"-"。
证据收集与分析
先确认 /dev 设备节点与驱动,再确认 SerialWorker 是否收到字节,随后检查 ProtocolCodec 是否输出业务 Frame,最后确认跨线程 signal 是否到达 UI。
根因
最终发现设备串口节点从 ttyUSB0 变成 ttyUSB1,而路径被 UI 代码写死。
修复与验证
将设备枚举集中到 DeviceService,UI 不再直接持有设备路径;拔插 20 次均可恢复。
这个案例体现了本系列反复强调的方法:不要从"最像的原因"开始改代码,而要沿数据链、线程链或系统链逐层排除,并把验证结果写进日志或测试记录。
API、命令与工程要点速查
| 名称 | 本章用途 |
|---|---|
QApplication |
本章核心工具/API;建议在实验中实际使用并记录结果。 |
QObject |
本章核心工具/API;建议在实验中实际使用并记录结果。 |
QWidget/QML |
本章核心工具/API;建议在实验中实际使用并记录结果。 |
QThread |
本章核心工具/API;建议在实验中实际使用并记录结果。 |
QSerialPort |
本章核心工具/API;建议在实验中实际使用并记录结果。 |
QTcpSocket |
本章核心工具/API;建议在实验中实际使用并记录结果。 |
Linux QPA |
本章核心工具/API;建议在实验中实际使用并记录结果。 |
systemd |
本章核心工具/API;建议在实验中实际使用并记录结果。 |
端到端实战任务
建立一个 Mock 设备监控程序:100ms 生成数据、500ms 刷新 UI,显示 Online/Offline,并输出版本和线程信息。
建议按下面的工程流程完成,而不是只复制代码:
- 先写出输入、输出、线程和数据链路。
- 只实现最小成功路径并编译通过。
- 增加日志,能够看到每个关键状态变化。
- 主动制造至少两种失败条件。
- 观察 CPU、内存、线程和 FD 是否符合预期。
- 记录人工验证步骤与预期现象。
- 保存最终代码、日志和结论,作为后续章节的基线。
工程决策原则
- 先分层再修复: 先判断属于 UI、Qt 机制、Linux、协议还是硬件层。
- 先证据再结论: 使用日志、系统命令、调用栈或抓包验证,不靠猜测。
- 先最小改动再扩展: 能在局部解决的问题,不做无关重构。
- 先长期边界再上线: 缓存、线程、FD、日志和数据库都必须有上限或回收策略。
章节复习题
- 为什么嵌入式 Qt 不是另一套 Qt?
- QPA 位于哪一层?
- 为什么 UI 与硬件访问要分层?
- 长期运行与普通桌面 Demo 的质量标准有什么差异?
- 遇到无数据显示时如何画数据链路?
建议在不看正文的情况下口头回答。如果无法解释"为什么",说明还停留在 API 记忆阶段,需要重新完成本章实验。
工程检查清单
- 能解释 x86_64 程序为什么不能直接运行在 ARM64
- 知道 Qt 应用、Qt 库、第三方库必须与目标 ABI 匹配
- 能说清 EGLFS/LinuxFB/Wayland/XCB 属于哪一层
- 明确 QWidget/QML 与硬件驱动不是同一层问题
本篇小结
Qt 嵌入式不是另一套 Qt,而是 Qt 运行环境从桌面 OS 变成了 Embedded Linux。真正新增的能力是交叉编译、系统环境、硬件通信和长期稳定性。
系列导航
- 上一篇:无(系列第一篇)
- 下一篇:嵌入式Linux基础:Qt开发者必须掌握的命令与排查思路
建议:真正掌握本篇内容的标准不是"看懂",而是能在自己的开发板、虚拟机或测试工程中复现关键流程,并能解释失败时该从哪一层排查。