3. 内存占用实测对比:Flash与RAM消耗,以STM32F429为例

做嵌入式GUI,内存就是真金白银。尤其像STM32F429这种片子,虽然带TFT控制器和2D加速器,但内部Flash只有2MB,RAM也就256KB+64KB CCM。你想想看,跑个复杂的仪表盘,UI框架本身吃掉多少资源?剩下的还能不能塞下业务逻辑?

这一章,我就拿STM32F429这块板子,把TouchGFX和LVGL的内存占用扒开来看看。不搞理论推算,直接上编译器生成的map文件,看实际烧进去的数据。

测试环境说明:

  • MCU:STM32F429ZIT6(Cortex-M4,180MHz)
  • Flash:2MB,RAM:256KB + 64KB CCM
  • 屏幕:480×272,16位色,RGB565
  • TouchGFX版本:4.22,LVGL版本:8.3
  • 编译器:ARM GCC 10.3,-O2优化
  • RTOS:FreeRTOS 10.4(两个框架都跑在同一个OS上)

3.1 测试方法:怎么测才公平?

我个人的习惯是,对比框架不能只跑空工程。空工程只能看到框架本身的最小开销,但实际项目里你总要放几个控件吧?所以这次我设计了三个测试用例:

  1. 空工程:只有初始化、屏幕清空、无任何控件。测框架裸开销。
  2. 基础控件页:放10个按钮、5个标签、2个进度条、1个列表。模拟简单HMI。
  3. 复杂仪表盘:包含1个实时曲线图、1个圆形仪表、3个数字显示框、背景图片(480×272,JPEG压缩)。模拟中等复杂度场景。

每个用例编译后,我直接从.map文件里摘出.text(Flash)、.data(已初始化RAM)、.bss(未初始化RAM)三个段的大小。堆和栈统一配置:堆32KB,栈4KB。

注意: 堆栈大小会影响RAM统计。我两个框架的堆栈配置完全一致,FreeRTOS的任务栈也统一为1024字。这样对比才有意义。

3.2 Flash占用对比:谁更省空间?

先看空工程。TouchGFX空工程编译出来,Flash占用大约186KB。LVGL呢?只有92KB。差了一倍。为什么会这样?

TouchGFX的架构里,有一部分代码是C++模板和虚函数表,编译器很难完全优化掉。而且它内置了完整的字体渲染引擎和图片解码器,即使你没用,链接器也不会全裁掉。LVGL是纯C写的,函数粒度更细,链接器能更激进地做-ffunction-sections -fdata-sections裁剪。

但别急着下结论。加上基础控件后,差距缩小了:

测试用例 TouchGFX Flash LVGL Flash 差值
空工程 186 KB 92 KB +94 KB
基础控件页 224 KB 148 KB +76 KB
复杂仪表盘 412 KB 336 KB +76 KB

看到没?随着控件增多,LVGL的Flash增长更快。因为LVGL每个控件都是独立的.c文件,你每加一个控件类型,就多拉进来一堆函数。TouchGFX的控件很多是模板实例化,编译器会复用代码段。

我曾经在一个项目里,用LVGL做了个带30多种控件的复杂界面,最后Flash飙到了580KB。换成TouchGFX,同样的界面只用了460KB。所以,小项目LVGL省Flash,大项目TouchGFX反而更优。

3.3 RAM占用对比:运行时谁更吃内存?

RAM才是嵌入式GUI的命门。Flash不够还能外挂,RAM不够就只能砍功能了。

空工程下,TouchGFX的RAM占用约48KB,LVGL约32KB。这16KB的差距主要来自TouchGFX的帧缓冲管理——它默认会分配两个全屏缓冲区用于双缓冲,而LVGL默认只用一个。

但基础控件页的结果让我有点意外:

测试用例 TouchGFX RAM LVGL RAM 差值
空工程 48 KB 32 KB +16 KB
基础控件页 76 KB 68 KB +8 KB
复杂仪表盘 184 KB 212 KB -28 KB

复杂仪表盘下,LVGL的RAM反而超过了TouchGFX。为什么?因为LVGL的控件对象是动态分配的,每个控件都带着完整的样式结构体和事件回调链表。TouchGFX的控件是静态声明在栈上或全局区的,而且它的样式系统是扁平化的,不搞深拷贝。

我建议你注意一点:LVGL的RAM消耗跟控件数量几乎线性相关。如果你有100个按钮,每个按钮的样式对象、状态标志、回调指针加起来,轻松吃掉十几KB。TouchGFX在这方面更克制,它的控件对象更紧凑。

个人经验: 如果你的RAM预算在128KB以内,LVGL的初始开销更低,适合做简单界面。但一旦界面复杂度上去(超过50个控件),TouchGFX的RAM增长曲线更平缓。我在一个128KB RAM的项目里,用LVGL做到80个控件时RAM报警,换成TouchGFX后稳住了。

3.4 帧缓冲的隐藏开销

嗯,这里要单独拎出来说。帧缓冲是RAM消耗的大头,而且两个框架的策略完全不同。

TouchGFX默认使用双缓冲:两个全屏缓冲区,每个480×272×2字节 = 261KB。两个就是522KB!但STM32F429总共才256KB RAM,这怎么跑?

实际上,TouchGFX在F429上会启用部分缓冲(Partial Buffer)模式。它只分配几个行缓冲(比如16行),每次渲染一小块区域,然后通过LTDC的DMA2D搬运。这样RAM占用可以降到20KB以下。

LVGL默认是单缓冲,一个全屏缓冲区261KB。但它也支持部分刷新,你可以把缓冲区设成1/10屏幕大小,大约26KB。

我实测下来,在F429上两个框架都配置为部分缓冲模式:

  • TouchGFX:行缓冲16行,RAM占用约15KB
  • LVGL:缓冲区1/10屏幕,RAM占用约26KB

TouchGFX更省,因为它的渲染引擎对部分缓冲做了深度优化,每次只刷新脏区域。LVGL的部分刷新机制相对粗糙,缓冲区设太小会导致频繁刷新,CPU占用飙升。

3.5 一张图看懂内存分布

下面这张SVG图,是我根据实测数据画的。它展示了两个框架在STM32F429上的内存分布对比,你一眼就能看出谁占什么。

TouchGFX与LVGL内存占用对比(STM32F429) TouchGFX vs LVGL 内存占用对比(复杂仪表盘) TouchGFX Flash: 412 KB 代码 + 字体 + 图片资源 RAM: 184 KB 帧缓冲 + 控件对象 + 堆栈 帧缓冲:部分缓冲模式 行缓冲 16 行 ≈ 15 KB 控件对象:静态分配 紧凑结构,无动态开销 LVGL Flash: 336 KB 代码 + 字体 + 图片资源 RAM: 212 KB 帧缓冲 + 控件对象 + 堆栈 帧缓冲:部分刷新模式 1/10 屏幕 ≈ 26 KB 控件对象:动态分配 每个控件独立 malloc 注:数据基于 STM32F429,480×272 屏幕,复杂仪表盘场景实测

3.6 避坑指南:我踩过的内存坑

最后分享几个实战中遇到的坑,希望能帮你省点调试时间。

坑一:LVGL动态分配导致碎片化

我曾经在一个需要长期运行的项目里,用LVGL频繁创建和销毁控件。跑了三天后,malloc开始返回NULL。排查发现,LVGL默认的内存分配器(lv_mem)是简单链表分配,频繁分配释放会产生大量碎片。后来我换成tlsf分配器才解决。如果你用LVGL做动态界面,记得换分配器。

坑二:TouchGFX的图片解码器吃Flash

TouchGFX内置了JPEG和PNG解码器,但这两个解码器加起来要占80KB Flash。如果你只用JPEG,可以在配置里关掉PNG支持。我一开始没注意,白白浪费了40KB。在TouchGFXConfiguration.hpp里可以设置。

坑三:帧缓冲对齐要求

STM32F429的LTDC要求帧缓冲地址32字节对齐。如果你用LVGL的动态分配,malloc返回的地址不一定对齐。我遇到过屏幕显示错位,查了半天发现是缓冲区地址没对齐。解决办法是用memalign或者自己维护一个对齐的内存池。

3.7 小结:选哪个看你的内存预算

好了,数据摆在这里。我的建议是:

  • Flash紧张(< 256KB):选LVGL,空工程省一半Flash。但注意控件多了之后Flash增长快。
  • RAM紧张(< 128KB):选TouchGFX,尤其界面复杂时RAM增长更平缓。
  • 两者都充裕:看团队技术栈。熟悉C++用TouchGFX,熟悉C用LVGL。