3. 字体格式与资源管理

做嵌入式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里,用内存映射的方式访问,速度几乎没差别。

提示: 使用外部Flash时,务必确认你的MCU是否支持内存映射模式(如STM32的FMC或QSPI)。如果不支持,每次读取字体数据都要通过SPI命令,性能会大打折扣。

3.2 动态加载与预加载策略

字体全放Flash里,RAM就没事了吗?当然不是。TouchGFX在渲染文字时,需要把用到的字形(Glyph)从Flash读到RAM里的一个缓存区,这个缓存区就是字形缓存(Glyph Cache)。

这里就涉及到两种策略:动态加载和预加载。

  • 动态加载:用哪个字,就从Flash读哪个字到缓存。优点是RAM占用小,缺点是首次显示某个字时会有延迟。
  • 预加载:在初始化阶段,就把常用字全部读到缓存里。优点是显示速度快,缺点是RAM占用大。

怎么选?我建议分场景:

  1. 静态文本界面:比如菜单标题、按钮文字。这些字是固定的,可以用预加载。在TouchGFX Designer里,你可以为这些文本设置Cacheable属性为true,然后在初始化时调用TypedTextDatabase::setCache()来预填充缓存。
  2. 动态文本界面:比如用户输入、从传感器读取的数值。这些字不确定,用动态加载更合适。TouchGFX默认就是动态加载,你只需要保证缓存大小够用就行。
  3. 混合场景:大部分界面是静态的,但有个别区域显示动态数据。我一般会为静态文本做预加载,动态文本走动态加载。这样既保证了主要界面的流畅度,又不会浪费RAM。
核心原则: 预加载是拿空间换时间,动态加载是拿时间换空间。没有绝对的好坏,只有合不合适的场景。

3.3 字形缓存大小调优

字形缓存(Glyph Cache)是RAM里的一块缓冲区,专门用来存放渲染好的字形位图。这个缓存的大小,直接决定了字体渲染的性能和RAM占用。

缓存太小会怎样?频繁的换入换出,导致文字显示卡顿,甚至出现“缺字”现象。缓存太大呢?RAM被白白占用,其他功能可能就没内存可用了。

那到底设多大合适?我一般按这个步骤来调:

  1. 先估算理论最小值:找出你项目中字号最大的字体,计算单个字形位图的大小。比如一个32x32像素的汉字,每个像素用4位(抗锯齿),那就是32*32*4/8 = 512字节。再乘以你预计同时显示的最大不重复字数,比如20个,那就是10KB。
  2. 再考虑实际场景:理论值往往偏小。因为TouchGFX的缓存管理有开销,而且不同字体的字形大小不一样。我习惯在理论值基础上翻一倍,作为初始值。
  3. 跑起来看效果:在代码里加个调试输出,打印缓存的命中率。如果命中率低于90%,说明缓存太小了,需要加大。如果命中率接近100%,而且RAM还有富余,可以适当减小。
避坑指南: 我曾经在一个项目里,把字形缓存设得特别大,结果系统启动时直接HardFault。后来查了半天,发现是缓存数组定义在了栈上,栈溢出了。记住,字形缓存一定要定义在全局区或者堆区,用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 整体策略框架图

说了这么多,我画了一张图,把整个字体内存优化的流程串起来,方便你理解:

字体内存优化策略框架图 字体内存优化策略框架 1. 存储分配决策 Flash:存放原始字体数据(点阵/矢量) RAM:存放运行时字形缓存(Glyph Cache) 2. 加载策略选择 预加载:静态文本,RAM换速度 动态加载:动态文本,速度换RAM 3. 字形缓存大小调优 估算理论最小值 → 翻倍初始值 → 监控命中率 → 迭代调整 最终目标:流畅显示 + 最小内存占用 注:箭头表示决策流程

嗯,这张图把整个优化流程串起来了。从最底层的存储分配,到上层的加载策略,再到最后的缓存调优,每一步都环环相扣。你实际做项目时,就按这个顺序来排查和优化,基本不会出大问题。

最后再啰嗦一句:内存优化没有银弹。每个项目的硬件资源、UI复杂度都不一样,最好的方法就是动手测。把缓存命中率打印出来,把Flash和RAM的占用率监控起来,数据会告诉你最准确的答案。