第二课: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实现流畅的显示。到时候见。