3. 字体格式与原理

做嵌入式GUI,说白了就是跟内存较劲。Flash空间有限,RAM更是金贵。字体作为GUI里最占地方的资源之一,选错了格式,可能直接导致项目流产。我见过不少团队,功能都调通了,最后发现字库存放不下,只能从头再来。

这一章,咱们就掰开揉碎,看看不同字体格式到底吃了多少内存。我会给出具体的计算公式,你拿着计算器就能估个八九不离十。

3.1 字体占用的两个维度:Flash与RAM

先搞清楚一件事:字体在Flash里存着,在RAM里跑着。两个地方占的空间,计算方式完全不同。

  • Flash占用:字库的原始数据。烧进去多大,就占多大。这部分是静态的,不会变。
  • RAM占用:运行时需要的内存。包括缓存、渲染缓冲区、字模临时存储等。这部分是动态的,跟你的使用方式强相关。

我习惯把Flash占用叫“死空间”,RAM占用叫“活空间”。死空间好算,活空间容易翻车。

3.2 点阵字体的内存模型

点阵字体,说白了就是每个字符存一张像素图。简单粗暴,但内存开销一目了然。

3.2.1 Flash占用计算

公式很简单:

Flash_Size = 字符数 × 字模宽度(像素) × 字模高度(像素) × 每像素位数 / 8

举个例子,一个16×16的中文字库,包含GB2312的6763个汉字,用1bpp(每个像素1bit)存储:

Flash_Size = 6763 × 16 × 16 × 1 / 8 = 6763 × 32 = 216,416 字节 ≈ 211 KB

嗯,211KB,看起来不大。但如果你用4bpp抗锯齿呢?

Flash_Size = 6763 × 16 × 16 × 4 / 8 = 6763 × 128 = 865,664 字节 ≈ 845 KB

直接翻了4倍。我在一个STM32F4项目里就吃过这个亏,选了4bpp抗锯齿点阵,结果Flash快塞满了,最后只能砍掉生僻字。

关键点:点阵字体的Flash占用与字符数、字号、位深成正比。想省空间,要么减少字符集,要么降低位深。

3.2.2 RAM占用计算

点阵字体的RAM占用主要来自缓存。如果你做全字库加载,RAM占用就等于Flash占用。但嵌入式里很少这么干,一般都是按需加载。

常见的做法是开一个“字模缓存池”,大小取决于你同时显示多少个字符:

RAM_Cache = 缓存行数 × 字模宽度 × 字模高度 × 每像素位数 / 8

比如你开10行缓存,每行存一个16×16的1bpp字模:

RAM_Cache = 10 × 16 × 16 × 1 / 8 = 320 字节

320字节,几乎可以忽略。但如果你用4bpp抗锯齿,同样10行:

RAM_Cache = 10 × 16 × 16 × 4 / 8 = 1280 字节

也才1.25KB。所以点阵字体在RAM上其实很省,真正的压力在Flash。

注意:有些GUI库会为每个字符分配一个“渲染缓冲区”,用于临时存放字模。这个缓冲区大小等于单个字模的大小。如果你同时渲染多个字符,缓冲区会叠加。我曾经遇到一个bug,就是因为缓冲区开小了,导致字符显示不全。

3.3 矢量字体的内存模型

矢量字体(比如FreeType用的TrueType/OpenType)就复杂多了。它存的不是像素,而是数学曲线。好处是缩放不变形,坏处是——吃RAM大户。

3.3.1 Flash占用计算

矢量字体的Flash占用取决于字体文件本身。一个完整的TrueType中文字体文件,通常在3MB到10MB之间。这还没算上Hinting(微调)数据。

但嵌入式里不会用完整字体文件。常用的做法是:

  • 子集化:只提取需要的字符。比如只提取ASCII和常用汉字,文件能压缩到几百KB。
  • 压缩存储:用LZ4或zstd压缩字体文件,运行时解压。压缩比通常在2:1到4:1之间。

我做过一个项目,原版字体文件8MB,子集化后只剩600KB,再用LZ4压缩到200KB。嗯,代价是启动时多花几百毫秒解压。

技巧:子集化工具推荐fonttools(Python库),可以精确控制保留哪些字符。我一般会保留ASCII、数字、标点,再加上项目里实际用到的汉字,从日志里提取就行。

3.3.2 RAM占用计算——这才是大头

矢量字体在RAM上的消耗,主要来自三个部分:

  1. 字体对象:加载字体文件后,FreeType会维护一个FT_Face对象,包含所有字形索引、度量信息。这个对象本身不大,几十KB到几百KB。
  2. 字形缓存:渲染过的字形会被缓存起来,避免重复渲染。缓存大小取决于你设置了多少个字形。
  3. 渲染缓冲区:每次渲染一个字符,都需要一个临时缓冲区来存放像素结果。这个缓冲区的大小 = 渲染宽度 × 渲染高度 × 每像素位数 / 8。

咱们重点算渲染缓冲区。假设你要渲染一个100×100的字符,用8bpp灰度抗锯齿:

Buffer_Size = 100 × 100 × 8 / 8 = 10,000 字节 ≈ 10 KB

一个字符10KB,看起来还行。但如果你同时渲染10个字符呢?

Total_Buffer = 10 × 10 KB = 100 KB

100KB,对于只有256KB RAM的MCU来说,已经占了40%。这还没算其他开销。

核心结论:矢量字体的RAM占用,主要取决于你同时渲染多少个字符,以及渲染尺寸。想省RAM,就减小渲染缓冲区,或者用1bpp(但会失去抗锯齿效果)。

3.4 对比表格:三种主流格式的内存占用

为了让你看得更清楚,我整理了一个对比表。假设字符集为GB2312(6763汉字+ASCII),字号16×16:

字体格式 Flash占用(典型值) RAM占用(典型值) 特点
点阵(1bpp) ~211 KB ~320 B(10行缓存) Flash省,RAM极省,不可缩放
点阵(4bpp抗锯齿) ~845 KB ~1.25 KB(10行缓存) Flash翻4倍,RAM略增,效果好
矢量(子集化+压缩) ~200 KB(压缩后) ~100 KB(10个字符渲染) Flash省,RAM大户,可任意缩放
矢量(完整字体) 3~10 MB ~500 KB+ Flash巨大,RAM巨大,一般不用于MCU

你看,点阵和矢量在内存占用上完全是两个极端。选哪个,取决于你的硬件资源。

3.5 我的经验:如何快速估算项目内存

每次开始一个新项目,我都会先做一件事:内存预算。具体步骤:

  1. 确定字符集:项目里到底需要多少个字符?从UI设计稿里提取,别猜。
  2. 确定最大字号:最大字号决定了渲染缓冲区的上限。
  3. 选择格式:Flash够大就上点阵,RAM够大就上矢量。
  4. 套公式计算:用上面的公式算一遍,留出30%余量。

我曾经在一个项目里,客户非要矢量字体,但MCU只有128KB RAM。我算了一下,光渲染缓冲区就要80KB,再加上系统其他开销,根本跑不起来。最后说服客户改用点阵,项目才顺利交付。

避坑指南:千万别只看Flash占用。矢量字体在Flash上可能只占200KB,但运行时RAM可能吃掉一半。我见过有人把8MB的完整字体文件塞进Flash,结果RAM直接爆了——因为FreeType加载时会把整个字体文件映射到RAM里。

3.6 知识结构图:字体内存占用全景

下面这张图,帮你把本章的核心逻辑串起来。从字体格式出发,分Flash和RAM两条线,最后落到选择策略上。

字体内存占用分析全景图 字体格式选择 点阵字体 矢量字体 Flash: 字符数×字模大小 RAM: 缓存行数×字模大小 Flash: 字体文件(子集化+压缩) RAM: 渲染缓冲区+字形缓存 选择策略:Flash vs RAM 权衡 💡 核心原则: Flash充裕 → 优先点阵(RAM省,性能好) RAM充裕 → 可选矢量(灵活,可缩放)

3.7 小结

这一章的核心就一句话:点阵吃Flash省RAM,矢量吃RAM省Flash。没有绝对的好坏,只有适不适合你的硬件。

我个人习惯是:如果Flash有1MB以上,优先用点阵,省心。如果Flash只有512KB,那就得上矢量子集化,但RAM一定要留够。你想想看,项目做到一半发现RAM不够,那才叫欲哭无泪。