
1. 从一次选型争论说起MCU 上跑 Qt 到底是不是伪命题去年底跟几个做工业 HMI 的朋友吃饭席间吵起来一个话题一块十几块钱的 MCU到底该不该上图形框架。一派认为老老实实裸机刷屏、状态机切页面就够了上框架纯属给自己找麻烦另一派则被产品经理反复改 UI 的需求折磨得够呛恨不得把 Qt 搬上去。当时谁也没说服谁但今年 Qt for MCUs 2.11 LTS 和 Qt 5.15.19 这两个版本一前一后落地我觉得这场争论可以有个阶段性的答案了。先把结论摆前面Qt for MCUs 2.11 LTS 是给资源受限但 UI 需求不简单的设备准备的典型代表就是 ESP32-S3 和瑞萨 RA8D1 这类带显示能力、内存又卡得死死的芯片。而Qt 5.15.19 作为 Qt 5 系列的最终版本意义完全不同——它是给那些还压在 Qt 5 上、短期不打算迁 Qt 6 的老项目一个封版安心丸。这两件事放在一起看其实勾勒出了嵌入式 GUI 领域两条并行的路线一条往极轻量走一条往存量维护走。这篇东西我打算按实际做项目的思路来写先讲清楚 Qt for MCUs 这套东西在 MCU 上到底怎么跑起来的再拆 ESP32-S3 和 RA8D1 这两个平台各自的坑然后聊地图渲染这种看起来不该出现在 MCU 上的功能是怎么实现的最后说说 Qt 5.15.19 这个最终版对存量项目意味着什么。如果你正在纠结 HMI 方案选型或者手上有个 Qt 5 老项目不知道该不该动应该能捞到点有用的东西。2. Qt for MCUs 的运行模型它和桌面 Qt 根本不是一回事很多人第一次接触 Qt for MCUs会下意识拿它跟桌面 Qt 对比然后得出阉割版的结论。这个理解方向就偏了。Qt for MCUs 不是把桌面 Qt 裁剪一下塞进 MCU而是一套重新设计的、面向无操作系统或 RTOS 环境的渲染与声明式 UI 运行时。搞不清这一点后面所有的性能预期和调试思路都会跑偏。2.1 QML 在 MCU 上是怎么被编译掉的桌面 Qt 跑 QML 靠的是 QML 引擎在运行时解析、绑定、求值这套机制灵活但吃内存吃 CPU。Qt for MCUs 走的是另一条路QML 在构建阶段就被 QUL 工具链Qt Quick Ultralite转译成 C 代码运行时几乎没有解释器开销。具体流程大致是这样你写的.qml文件先经过qmltocpp之类的工具生成对应的 C 类和属性绑定代码然后跟 QUL 运行时库一起编译进固件。属性绑定不再是运行时反射而是编译期生成的直接函数调用。这意味着什么意味着绑定表达式的复杂度在编译期就基本定型了运行时改不了结构只能改值。这个设计带来的直接后果是你不能像桌面那样动态Qt.createComponent()或者运行时加载 QML 字符串。所有界面结构在编译时就固定了。对做惯了桌面 Qt 的人来说这是束缚但对 MCU 来说这是必须的——没有 MMU、没有虚拟内存、堆只有几十 KB任何运行时动态分配都是风险。提示QUL 工具链对 QML 语法的支持是子集不是全集。动画、状态机、部分 JavaScript 表达式支持但像eval、动态属性、复杂的 JS 闭包基本别想。写之前先翻一遍官方支持的 QML 子集列表能省掉大量编译过了但跑不起来的时间。2.2 渲染管线为什么 MCU 也能跑出 60fpsMCU 没有 GPU这是常识。但 Qt for MCUs 依然能在 ESP32-S3 这种芯片上跑出流畅动画靠的是分层渲染 脏矩形更新 硬件加速的 2D 位块搬运。它的渲染模型大致分三层背景层、内容层、覆盖层。每层独立维护自己的缓冲区只有发生变化的区域才重新绘制并合成。ESP32-S3 带 LCD_CAM 外设和 PSRAM可以把帧缓冲放在外部 PSRAM 里CPU 只负责往脏区域写像素剩下的搬运交给 DMA。RA8D1 那边更舒服它自带 2D 图形加速引擎DRW画线、填充、alpha 混合都能硬件做CPU 占用能压得很低。这里有个关键参数要算清楚帧缓冲大小 宽 × 高 × 每像素字节数。以 480×272 的 RGB565 屏为例单缓冲就是 480×272×2 ≈ 255KB。ESP32-S3 内部 SRAM 只有 512KB还要留给程序和数据所以帧缓冲基本必须放 PSRAM。而 PSRAM 的带宽是有限的如果每帧都全屏重绘带宽直接吃满帧率就崩了。这就是为什么脏矩形更新不是优化选项而是生存必需。2.3 内存账本先算清楚再动手我见过太多项目死在内存上所以这里把账算细一点。一个典型的 Qt for MCUs 应用内存开销主要来自四块内存区域典型占用说明QUL 运行时库80~150KB Flash取决于启用的模块生成的 UI 代码50~200KB Flash界面越复杂越大帧缓冲200KB~1MB放 PSRAM看分辨率和层数堆运行时对象20~64KB控件实例、字符串等Flash 那边 ESP32-S3 一般有 8MB 外部 Flash问题不大。真正的瓶颈是 RAM。内部 SRAM 要留给中断栈、DMA 描述符、WiFi 协议栈如果用的话能分给 GUI 的往往只有一两百 KB。所以帧缓冲放 PSRAM、堆尽量放内部 SRAM是常见做法——堆访问频繁放内部快帧缓冲访问模式是顺序大块PSRAM 带宽够用。3. ESP32-S3 平台实操从点亮屏幕到跑通第一个 QUL 界面ESP32-S3 是这两年被问得最多的 MCU 之一带 WiFi/BLE、带 LCD 接口、带 PSRAM 支持价格还便宜做带屏 IoT 设备几乎是默认选项。但它的坑也不少尤其是显示这块官方例程和实际项目之间隔着一条河。3.1 硬件连接RGB 接口和 SPI 接口的选择ESP32-S3 支持两种主流屏幕接口RGB 并口LCD_CAM 外设和 SPI。选哪个直接决定了你能跑多高的刷新率。RGB 并口走的是 16 位或 18 位并行数据线配合 PCLK、HSYNC、VSYNC、DE 几个同步信号可以直接对接 RGB 屏。优点是带宽高480×272 甚至 800×480 都能跑缺点是吃引脚16 位数据线加控制线二十多个 GPIO 就没了。ESP32-S3 的 GPIO 数量本来就紧张如果还要接 WiFi 天线、按键、传感器很容易不够用。SPI 屏省引脚四线 SPI 加几个控制脚就够但带宽是硬伤。以 40MHz SPI 算理论峰值 5MB/s刷 480×272 RGB565 全屏要 255KB也就是 20 帧/秒封顶实际还要打折。所以 SPI 屏适合小尺寸、低刷新率的场景比如 240×240 的状态显示。我的建议是如果 UI 有动画、有滑动列表咬牙上 RGB 并口如果只是静态参数显示、偶尔刷新SPI 够用还省事。选型阶段就把引脚规划做出来别等 PCB 画完了才发现 GPIO 不够。3.2 PSRAM 配置最容易翻车的一步ESP32-S3 的 PSRAM 配置有几个关键点配错了要么跑不起来要么性能惨不忍睹。第一PSRAM 的时钟频率和模式。ESP32-S3 支持 Quad SPI PSRAM 和 Octal SPI PSRAMOctal 带宽翻倍但引脚占用多。在 menuconfig 里要选对模式选错了轻则不识别重则启动就崩。第二PSRAM 的访问方式。默认情况下 PSRAM 是映射到地址空间的可以直接指针访问但通过 cache 访问和绕过 cache 访问性能差很多。帧缓冲这种顺序访问的场景走 cache 效率高但如果 DMA 要直接读 PSRAM就得考虑 cache 一致性问题可能需要手动 flush。第三堆的分配策略。ESP-IDF 里可以用heap_caps_malloc指定从哪块内存分配。QUL 的帧缓冲建议用MALLOC_CAP_SPIRAM运行时对象用MALLOC_CAP_INTERNAL。这个在 QUL 的移植层里要改对默认配置不一定合适。注意PSRAM 不是万能的。它的随机访问延迟比内部 SRAM 高一个数量级如果 QUL 的某些操作频繁随机访问帧缓冲比如 alpha 混合读改写性能会明显下降。实测下来把覆盖层放内部 SRAM、背景层放 PSRAM 是个折中方案。3.3 跑通第一个界面从官方 example 到自己的工程官方给的 ESP32-S3 例程一般是个简单的仪表盘或者按钮界面跑通它不难难的是把它改成自己的工程结构。我一般这么做先用官方例程确认硬件没问题屏幕能亮、能显示、触摸能响应。然后把 QUL 的运行时库和生成的 UI 代码抽出来做成独立的 component。最后把应用逻辑业务代码和 UI 代码分离UI 只负责显示和发信号业务逻辑在单独的 task 里跑。这里有个经验QUL 的 UI 更新最好放在单独的 task 里给它固定的栈和优先级。因为渲染是周期性的如果跟业务逻辑混在一个 task业务一忙 UI 就卡。ESP32-S3 是双核的可以把 UI task 绑到 core 1业务绑到 core 0互不干扰。另外QUL 的Qul::Application主循环需要定期调用一般是app.update()或者类似的接口。这个调用频率决定了 UI 的响应速度但也不能太频繁否则 CPU 全耗在渲染上。实测 30~60Hz 的更新频率比较合理具体看屏幕刷新率。4. RA8D1 平台当 MCU 自带 2D 加速引擎时该怎么用瑞萨 RA8D1 是另一条路线。它基于 Cortex-M85主频高还带了 DRW2D Drawing Engine和 GLCDCGraphics LCD Controller硬件底子比 ESP32-S3 厚实不少。但硬件强不代表就能随便造用不好照样卡。4.1 DRW 加速引擎的能力边界RA8D1 的 DRW 能做什么简单说画线、画矩形、填充、alpha 混合、图像旋转缩放、颜色格式转换。这些操作如果让 CPU 做480×272 全屏填充一次就是十几万次写操作DRW 几个周期就搞定。但 DRW 不是万能的。它不支持任意路径绘制、不支持复杂的混合模式、不支持着色器。QUL 的渲染器在 RA8D1 上会尽量把能交给 DRW 的操作交出去但像圆角矩形、渐变、阴影这些可能还是 CPU 软渲染。所以实际性能取决于你的 UI 用了多少DRW 友好的元素。我的经验是多用纯色块、直线、矩形、位图少用渐变和复杂圆角。如果设计稿里全是毛玻璃和阴影那 RA8D1 也救不了你得回去跟设计师聊聊。4.2 GLCDC 双缓冲与撕裂问题GLCDC 是 RA8D1 的 LCD 控制器支持双缓冲和图层混合。双缓冲的意义在于CPU/DRW 在后台缓冲绘制GLCDC 在前台缓冲扫描输出绘制完成后交换。这样就不会出现画到一半屏幕显示出来的撕裂现象。但双缓冲的代价是帧缓冲内存翻倍。480×272 RGB565 双缓冲就是 510KBRA8D1 的 SRAM 够不够要看具体型号。如果不够就得考虑单缓冲 垂直同步等待或者降低分辨率。撕裂问题在静态界面看不出来一旦有滑动、动画就非常明显。我建议只要做动画就上双缓冲这是体验的底线。内存实在紧张宁可降分辨率也别省这个。4.3 中断优先级与渲染任务的配合RA8D1 的 GLCDC 和 DRW 都会产生中断这些中断的优先级要设对。GLCDC 的垂直同步中断优先级要高因为它决定了帧的节奏DRW 的完成中断可以稍低但也不能太低否则绘制任务排队。QUL 在 RA8D1 上的移植层一般会封装这些中断处理。你要做的是确认渲染任务的优先级低于显示中断但高于普通业务任务。这样显示不卡业务也不至于饿死。5. MCU 上的地图渲染一个反常识功能的实现思路地图渲染出现在 MCU 上第一反应是疯了吧。桌面地图动辄几百 MB 瓦片数据MCU 那点 Flash 和 RAM 怎么扛但仔细想想很多场景其实不需要完整地图——工业设备的厂区平面图、车载的简易导航、手持设备的轨迹显示这些地图数据量小、交互简单完全可以在 MCU 上做。5.1 瓦片数据的裁剪与存储核心思路是只存需要的瓦片而且只存低精度的。比如一个厂区平面图切成 256×256 的瓦片总共可能就几十张每张用 RGB565 存压缩后也就几百 KB。放外部 Flash 完全没问题。存储格式上我倾向于预先把瓦片转成 QUL 能直接用的位图格式避免运行时解码。QUL 支持从 Flash 直接读位图资源配合Qul::Image使用。如果瓦片是 PNG/JPG构建阶段就转掉别留到运行时。5.2 视口计算与瓦片调度地图渲染的核心是根据当前视口位置决定加载哪些瓦片、画在什么位置。这个计算不复杂视口中心点坐标除以瓦片尺寸得到中心瓦片索引然后按视口大小向外扩展一圈就是需要渲染的瓦片集合。关键是调度策略。MCU 的 Flash 读取速度有限如果每帧都重新读瓦片带宽吃不消。所以要做缓存把最近用到的瓦片缓存在 PSRAM 里用 LRU 淘汰。缓存大小看 PSRAM 余量一般能放十几到几十张瓦片就够流畅了。5.3 缩放与平移的性能取舍地图交互无非缩放和平移。平移相对简单改视口偏移重新计算瓦片位置即可。缩放麻烦一些因为瓦片需要缩放绘制。QUL 在 RA8D1 上可以借助 DRW 做位图缩放性能还行ESP32-S3 上就得 CPU 软缩放480×272 全屏缩放一次可能就要十几毫秒帧率直接掉到 30 以下。所以ESP32-S3 上的地图缩放建议做限制要么只支持整数倍缩放2x、4x要么缩放时降低刷新率松手后再恢复。提示地图渲染最怕的是每帧全量重绘。一定要用脏矩形只重绘视口变化的区域。平移时其实只有边缘新进入的区域需要绘制中间大部分可以复用上一帧。6. Qt 5.15.19Qt 5 的最终版本对存量项目意味着什么聊完 MCU 那边再说说 Qt 5.15.19。这个版本号本身没什么惊喜但Qt 5 最终版本这个定位很关键。它意味着Qt 5 系列到此为止后续只有安全补丁不会再有新功能。6.1 为什么还有大量项目压在 Qt 5 上原因很现实迁移成本。Qt 5 到 Qt 6 不是小版本升级是架构级变动。QML 引擎换了、图形栈换了、构建系统从 qmake 转向 CMake、很多模块被拆分或废弃。一个中型项目迁移几个月起步还得重新做兼容性测试。对于工业设备、医疗仪器这类产品生命周期长、认证成本高的领域迁移意味着重新认证代价太大。所以很多团队的选择是留在 Qt 5锁死版本只打安全补丁。Qt 5.15.19 就是给这些团队准备的。6.2 锁版本的正确姿势锁版本不是简单地把版本号写死就完事。我建议做这几件事把 Qt 安装包归档。官方下载链接以后可能失效自己存一份离线安装包。依赖库也锁死。Qt 5.15.19 依赖的 OpenSSL、字体库、图形驱动版本都要记录清楚。构建环境容器化。用 Docker 把编译环境固化避免换台机器就编不过。补丁管理流程。后续如果有安全补丁要有渠道获取并验证。这些事听起来琐碎但真到了需要重新构建的时候能救命。6.3 什么时候该考虑迁移我的判断标准是如果项目还在活跃开发、还要加新功能、还要支持新硬件那就该规划迁移。Qt 5 封版意味着新硬件适配、新系统支持都会越来越难。反之如果项目已经进入维护期只修 bug 不加功能那锁在 Qt 5.15.19 上完全合理。迁移也不是一步到位可以先迁构建系统qmake 转 CMake再迁 QML 代码最后迁图形栈分阶段降低风险。7. 两个版本放在一起看嵌入式 GUI 的路线选择把 Qt for MCUs 2.11 LTS 和 Qt 5.15.19 放一起其实能看到嵌入式 GUI 的两条清晰路线。路线一极致轻量。用 Qt for MCUs跑在 ESP32-S3、RA8D1 这类 MCU 上无 OS 或 RTOS内存以 KB 计UI 用 QML 子集描述编译期定型。适合成本敏感、量大、UI 相对固定的产品。路线二存量维护。用 Qt 5.15.19跑在带 Linux 的应用处理器上内存以百 MB 计UI 功能完整。适合已经量产、进入维护期的产品。这两条路线不是对立的很多公司同时有这两类产品。关键是别用错工具拿 Qt 5 去跑 MCU 是自找苦吃拿 Qt for MCUs 去做复杂桌面级 UI 也是缘木求鱼。选型的时候我一般会问三个问题屏幕多大、刷新率要求多少、UI 会不会频繁改。屏幕小、刷新率低、UI 固定MCU 方案屏幕大、要动画、UI 常改上应用处理器。中间地带就看成本能不能接受没有标准答案。8. 实操中踩过的几个坑和对应的解法最后分享几个实际项目里踩过的坑都是文档里不太会写、但真能卡住人的。坑一QUL 的字体渲染吃 RAM。中文字体动辄几 MBQUL 支持字体子集化但子集化工具用不对要么缺字要么体积没降下来。我的做法是先用工具统计界面实际用到的字符生成精确子集别偷懒用常用字表。坑二ESP32-S3 的 PSRAM 和 WiFi 抢带宽。WiFi 工作时会占用 PSRAM 带宽如果帧缓冲也在 PSRAMUI 会明显卡顿。解法是UI 刷新和 WiFi 传输错峰或者把关键帧缓冲放内部 SRAM。坑三RA8D1 的 DRW 中断和 QUL 渲染任务死锁。DRW 完成中断里如果直接触发下一次绘制可能和渲染任务抢资源。正确做法是中断里只置标志实际绘制在任务里做。坑四Qt 5.15.19 的 OpenSSL 版本兼容。Qt 5.15 对 OpenSSL 1.1 和 3.0 的支持不一样锁版本时要把 OpenSSL 一起锁否则换个环境就握手失败。坑五地图瓦片的坐标精度。MCU 上浮点运算慢地图坐标尽量用整数缩放用定点数。我见过用 float 算瓦片位置的帧率直接腰斩。这些坑的共同点是都不是框架本身的 bug而是资源约束下的工程取舍。MCU 开发就是这样硬件资源摆在那你得在有限空间里做平衡。想清楚每一 KB 内存、每一个 CPU 周期花在哪比背 API 重要得多。