3. 内存优化策略:Flash与RAM的字体存储分配、动态加载与预加载策略、字形缓存大小调优
做嵌入式UI,说白了就是跟内存较劲。尤其是字体,看着不起眼,吃起资源来一点不含糊。我见过不少项目,界面逻辑都写好了,一跑起来发现Flash爆了,或者RAM撑不住,最后查下来,罪魁祸首往往是字体。
这一章,我们就来聊聊怎么把字体这块的内存管好。我会结合我自己的项目经验,把Flash和RAM的分配、动态加载、还有字形缓存调优这几个关键点掰开揉碎了讲。
3.1 Flash与RAM的字体存储分配
字体数据放哪儿,这是个根本问题。TouchGFX里,字体数据可以存在Flash里,也可以存在RAM里。但这两者的特性完全不同。
| 存储介质 | 优点 | 缺点 | 典型用途 |
|---|---|---|---|
| Flash | 容量大,掉电不丢失,读取快 | 写入慢(运行时基本不写),不能修改 | 存放静态字体数据,如界面固定文本 |
| RAM | 读写速度快,可动态修改 | 容量小,掉电丢失 | 存放动态加载的字形缓存、临时渲染数据 |
我个人习惯,是把所有字体的原始点阵数据或者矢量轮廓数据,都放在Flash里。为什么?因为Flash便宜,容量大。你想想看,一个完整的GB2312字库,少说几百KB,放RAM里太奢侈了。
但这里有个坑。TouchGFX默认会把字体数据编译到Flash的某个段里。如果你的MCU内部Flash不够,就得考虑外挂SPI Flash或者QSPI Flash。我记得在做一个带全字库的工业平板项目时,内部Flash只有1MB,光字体就占了600多KB,剩下的代码和图片根本塞不下。后来我们把字体全部挪到了外部QSPI Flash里,用内存映射的方式访问,速度几乎没差别。
3.2 动态加载与预加载策略
字体全放Flash里,RAM就没事了吗?当然不是。TouchGFX在渲染文字时,需要把用到的字形(Glyph)从Flash读到RAM里的一个缓存区,这个缓存区就是字形缓存(Glyph Cache)。
这里就涉及到两种策略:动态加载和预加载。
- 动态加载:用哪个字,就从Flash读哪个字到缓存。优点是RAM占用小,缺点是首次显示某个字时会有延迟。
- 预加载:在初始化阶段,就把常用字全部读到缓存里。优点是显示速度快,缺点是RAM占用大。
怎么选?我建议分场景:
- 静态文本界面:比如菜单标题、按钮文字。这些字是固定的,可以用预加载。在TouchGFX Designer里,你可以为这些文本设置
Cacheable属性为true,然后在初始化时调用TypedTextDatabase::setCache()来预填充缓存。 - 动态文本界面:比如用户输入、从传感器读取的数值。这些字不确定,用动态加载更合适。TouchGFX默认就是动态加载,你只需要保证缓存大小够用就行。
- 混合场景:大部分界面是静态的,但有个别区域显示动态数据。我一般会为静态文本做预加载,动态文本走动态加载。这样既保证了主要界面的流畅度,又不会浪费RAM。
3.3 字形缓存大小调优
字形缓存(Glyph Cache)是RAM里的一块缓冲区,专门用来存放渲染好的字形位图。这个缓存的大小,直接决定了字体渲染的性能和RAM占用。
缓存太小会怎样?频繁的换入换出,导致文字显示卡顿,甚至出现“缺字”现象。缓存太大呢?RAM被白白占用,其他功能可能就没内存可用了。
那到底设多大合适?我一般按这个步骤来调:
- 先估算理论最小值:找出你项目中字号最大的字体,计算单个字形位图的大小。比如一个32x32像素的汉字,每个像素用4位(抗锯齿),那就是32*32*4/8 = 512字节。再乘以你预计同时显示的最大不重复字数,比如20个,那就是10KB。
- 再考虑实际场景:理论值往往偏小。因为TouchGFX的缓存管理有开销,而且不同字体的字形大小不一样。我习惯在理论值基础上翻一倍,作为初始值。
- 跑起来看效果:在代码里加个调试输出,打印缓存的命中率。如果命中率低于90%,说明缓存太小了,需要加大。如果命中率接近100%,而且RAM还有富余,可以适当减小。
static或者malloc分配。
下面是一个典型的缓存配置代码示例,在TouchGFXConfiguration.cpp里:
// 定义字形缓存大小,单位是字节
// 根据你的实际需求调整,这里设为 64KB
static const uint16_t GLYPH_CACHE_SIZE = 64 * 1024;
// 分配缓存空间,注意要放在全局区
static uint8_t glyphCache[GLYPH_CACHE_SIZE] __attribute__((section(".ram_d2")));
// 在初始化时注册缓存
void touchgfx_init()
{
// ... 其他初始化代码 ...
TypedTextDatabase::setCache(glyphCache, GLYPH_CACHE_SIZE, 0);
// 第三个参数是字体位图格式,0表示自动检测
}
这里有个小技巧:__attribute__((section(".ram_d2"))) 是把缓存放到特定的RAM区域,比如STM32H7系列的D2域,这样可以避免跟DMA访问冲突,提升性能。
3.4 整体策略框架图
说了这么多,我画了一张图,把整个字体内存优化的流程串起来,方便你理解:
嗯,这张图把整个优化流程串起来了。从最底层的存储分配,到上层的加载策略,再到最后的缓存调优,每一步都环环相扣。你实际做项目时,就按这个顺序来排查和优化,基本不会出大问题。
最后再啰嗦一句:内存优化没有银弹。每个项目的硬件资源、UI复杂度都不一样,最好的方法就是动手测。把缓存命中率打印出来,把Flash和RAM的占用率监控起来,数据会告诉你最准确的答案。