第二课:LVGL配置裁剪艺术——从源头掐掉内存浪费

好,咱们接着聊。上一课我讲了LVGL的内存模型,说白了就是让你知道内存到底花在哪了。这一课更实在——怎么通过lv_conf.h这个配置文件,把不需要的东西统统砍掉。

你想想看,LVGL默认配置是给啥平台用的?是给那些动不动几百兆RAM、带GPU的板子用的。你一个Cortex-M4,总共就64KB RAM,还开着所有特效、所有字体、所有图片格式……那不完蛋了吗?

所以,裁剪是第一课。而且我告诉你,这一步做得好,内存能省掉50%以上。我在一个STM32F429的项目上,就是靠裁剪配置,硬是把LVGL塞进了原本不够用的32KB RAM里。

2.1 lv_conf.h 这个文件,到底在管什么?

先说说这个文件的位置。你从GitHub拉下来LVGL源码后,会看到一个lv_conf_template.h。把它复制到工程目录,改名为lv_conf.h,然后开始改。

这个文件里,全是宏定义。每个宏控制一个功能模块的开关。说白了,这就是LVGL的“总闸”。

我个人习惯,拿到一个新板子,第一件事就是打开这个文件,从头到尾扫一遍。把那些明显用不上的功能,直接关掉。嗯,这里要注意:不是所有宏都标了“关掉能省多少内存”,有些你得自己掂量。

核心原则:用不到的,一律关。犹豫的,先关。等需要了再开,不迟。

2.2 组件裁剪:砍掉你不需要的控件

LVGL的组件(widgets)有几十个。你一个温湿度计界面,需要用到lv_chart(图表)吗?需要lv_calendar(日历)吗?大概率不需要。

每个组件都会占用一定的代码空间(Flash)和运行时内存(RAM)。关掉它们,立竿见影。

lv_conf.h里,找到这一段:

/*===================
  开启/关闭 组件
 *===================*/
#define LV_USE_BTN           1
#define LV_USE_LABEL         1
#define LV_USE_SLIDER        1
#define LV_USE_CHART         0  // 关掉图表
#define LV_USE_CALENDAR      0  // 关掉日历
#define LV_USE_DROPDOWN      1
#define LV_USE_METER         0  // 关掉仪表盘
// ... 以此类推

我建议你这样做:先列一个清单,你的界面到底用了哪些控件。然后对照着lv_conf.h,把没出现的控件全部设为0。

我曾经在一个项目里,界面只用到了按钮、标签和滑块。结果我发现默认配置里开了二十多个组件。关掉之后,Flash占用直接少了30KB。你想想看,30KB对于单片机来说,能多放多少业务代码?

小技巧:如果你不确定某个控件是否被用到,可以先搜索整个工程,看看有没有引用它的头文件或函数。没有引用,就关掉。

2.3 字体裁剪:别让字体吃掉你的RAM

字体是内存大户。尤其是中文字体,一个GB2312的16x16点阵字库,就要200多KB。你如果把它全部加载到RAM里,那基本不用干别的了。

LVGL支持多种字体格式,包括内置的ASCII字体、FreeType渲染的矢量字体、以及自定义的点阵字体。在lv_conf.h里,字体相关的配置长这样:

/*===================
  字体配置
 *===================*/
#define LV_FONT_MONTSERRAT_8     1
#define LV_FONT_MONTSERRAT_12    1
#define LV_FONT_MONTSERRAT_16    1
#define LV_FONT_MONTSERRAT_20    0  // 用不到20号字,关掉
#define LV_FONT_MONTSERRAT_24    0
#define LV_FONT_MONTSERRAT_28    0
#define LV_FONT_MONTSERRAT_32    0

/* 中文字体 */
#define LV_FONT_SIMSUN_16_CJK    0  // 如果你不用中文,关掉
#define LV_FONT_SIMSUN_16_CJK    1  // 如果你只用少量中文,考虑自定义字库

这里有个坑,我踩过。LVGL默认会把所有开启的字体都编译进去。你如果只用了12号字,但开了8、12、16三个字体,那8和16也会占用Flash空间。所以,只保留你实际用到的字号。

对于中文,我的建议是:

  • 如果只显示几个固定汉字(比如“温度”、“湿度”),用LVGL的字体工具生成一个自定义字库,只包含这几个字。这样可能只需要几百字节。
  • 如果需要显示大量中文,考虑使用外部Flash存储字库,或者用LVGL的“字体缓存”机制,只加载当前屏幕用到的字。
  • 如果完全不需要中文,那太好了,直接把所有CJK字体关掉,能省一大块。

注意:LVGL 8.x之后,字体默认是编译到代码段(Flash)的,不会占用RAM。但如果你用了FreeType或者字体缓存,那就要小心RAM消耗了。

2.4 图片格式裁剪:别让解码器闲着

LVGL支持多种图片格式:PNG、JPG、BMP、GIF、以及它自己的LVGL图片格式(二进制)。每种格式都需要一个解码器。解码器本身占Flash,解码过程中还需要临时缓冲区,占RAM。

lv_conf.h里,图片格式的开关在这里:

/*===================
  图片解码器
 *===================*/
#define LV_USE_PNG            0  // 不用PNG,关掉
#define LV_USE_BMP            0  // 不用BMP,关掉
#define LV_USE_SJPG           0  // 不用JPG,关掉
#define LV_USE_GIF            0  // 不用GIF,关掉
#define LV_USE_QRCODE         0  // 不用二维码,关掉

我个人建议:在低资源设备上,尽量使用LVGL自带的二进制图片格式。这种格式不需要解码器,直接按像素读取,速度最快,内存占用最小。你可以用LVGL的图片转换工具(lv_img_conv)把PNG/JPG转成这种格式。

我记得有一次,一个同事非要在STM32上显示一张JPEG图片。结果解码器占用了8KB RAM作为缓冲区,图片显示还卡顿。后来我帮他转成LVGL格式,RAM占用直接降到0,显示也流畅了。

经验之谈:在低资源设备上,图片格式只用LVGL原生格式。其他格式的解码器,统统关掉。如果你必须用外部图片,考虑用Flash直接存储LVGL格式的图片数据。

2.5 特效裁剪:动画和阴影是奢侈品

LVGL的特效包括:动画、阴影、圆角、透明度、渐变等。这些特效看起来很酷,但每个都需要额外的计算和内存。

lv_conf.h里,特效相关的配置:

/*===================
  特效配置
 *===================*/
#define LV_USE_ANIMATION      0  // 关掉动画
#define LV_USE_SHADOW         0  // 关掉阴影
#define LV_USE_OPA_SCALE      0  // 关掉透明度缩放
#define LV_USE_GRADIENT       0  // 关掉渐变

你想想看,一个嵌入式界面,真的需要按钮有阴影吗?真的需要页面切换有滑动动画吗?对于工业控制、仪器仪表这类应用,稳定和快速响应比花哨更重要。

我曾经在一个医疗设备项目上,把所有特效都关了。客户反馈说界面“很清晰、反应很快”。没人抱怨没有阴影。所以,特效这东西,能关就关。

如果你实在想要一点视觉反馈,可以用最简单的状态切换(比如按钮按下变颜色),这个不占多少资源。

折中方案:如果你必须保留动画,可以降低帧率。默认是30fps,改成15fps甚至10fps,CPU和内存压力都会小很多。

2.6 其他可以裁剪的“隐形”消耗

除了上面这些明显的模块,还有一些“隐形”的配置项,也会吃掉你的内存。我列个表,你对照着检查:

配置项 默认值 建议值 说明
LV_MEM_SIZE 32KB 根据实际调整 LVGL动态内存池大小,设太大浪费,设太小崩溃
LV_DPI_DEF 130 根据屏幕实际DPI 影响控件默认大小,设不对会导致布局异常
LV_DISP_DEF_REFR_PERIOD 30ms 50ms或100ms 刷新周期,越大CPU占用越低
LV_USE_LOG 1 0 调试日志,发布版本一定要关掉
LV_USE_ASSERT 1 0 断言检查,发布版本关掉
LV_USE_PERF_MONITOR 0 0 性能监视器,调试用,发布版本关掉

这里我要特别说一下LV_MEM_SIZE。这个值设多大,取决于你的界面复杂度。我一般这样估算:

  • 先设一个较大的值(比如16KB),跑起来看看。
  • 然后打开LVGL的内存监视功能,看看实际用了多少。
  • 最后根据峰值使用量,加上20%的余量,设定最终值。

我曾经犯过一个错误:把LV_MEM_SIZE设得太小,结果界面一复杂就崩溃。后来发现是内存池不够用,动态分配失败。所以,这个值不能拍脑袋,要实测。

重要提醒:每次修改lv_conf.h后,一定要重新编译整个工程。因为很多配置会影响头文件的包含和代码的编译。只改文件不编译,等于没改。

2.7 我的裁剪清单(供参考)

最后,我分享一下我在一个实际项目中的裁剪清单。这个项目用的是STM32G070,64KB Flash,16KB RAM。界面只有三个页面:主菜单、参数设置、数据展示。

/* 组件:只保留用到的 */
#define LV_USE_BTN          1
#define LV_USE_LABEL        1
#define LV_USE_DROPDOWN     1
#define LV_USE_SLIDER       1
#define LV_USE_SWITCH       1
#define LV_USE_TEXTAREA     0  // 不用
#define LV_USE_CHART        0  // 不用
#define LV_USE_CALENDAR     0  // 不用
// ... 其他全部设为0

/* 字体:只保留12号和16号 */
#define LV_FONT_MONTSERRAT_12   1
#define LV_FONT_MONTSERRAT_16   1
// 其他字号全部设为0

/* 图片格式:只用LVGL原生格式 */
#define LV_USE_PNG   0
#define LV_USE_BMP   0
#define LV_USE_SJPG  0

/* 特效:全关 */
#define LV_USE_ANIMATION   0
#define LV_USE_SHADOW      0
#define LV_USE_OPA_SCALE   0
#define LV_USE_GRADIENT    0

/* 其他 */
#define LV_MEM_SIZE        (4 * 1024)  // 4KB内存池,实测够用
#define LV_USE_LOG         0
#define LV_USE_ASSERT      0

最终效果:Flash占用约28KB,RAM占用约6KB(包括LVGL内存池和显示缓冲区)。整个系统跑得很稳。

所以,别怕裁剪。LVGL的设计哲学就是模块化,你尽管砍。砍完之后,你会发现,原来低资源设备也能跑出不错的界面。

下一课,我会讲显示缓冲区的优化——怎么用最小的RAM实现流畅的显示。到时候见。