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上的消耗,主要来自三个部分:
- 字体对象:加载字体文件后,FreeType会维护一个FT_Face对象,包含所有字形索引、度量信息。这个对象本身不大,几十KB到几百KB。
- 字形缓存:渲染过的字形会被缓存起来,避免重复渲染。缓存大小取决于你设置了多少个字形。
- 渲染缓冲区:每次渲染一个字符,都需要一个临时缓冲区来存放像素结果。这个缓冲区的大小 = 渲染宽度 × 渲染高度 × 每像素位数 / 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 我的经验:如何快速估算项目内存
每次开始一个新项目,我都会先做一件事:内存预算。具体步骤:
- 确定字符集:项目里到底需要多少个字符?从UI设计稿里提取,别猜。
- 确定最大字号:最大字号决定了渲染缓冲区的上限。
- 选择格式:Flash够大就上点阵,RAM够大就上矢量。
- 套公式计算:用上面的公式算一遍,留出30%余量。
我曾经在一个项目里,客户非要矢量字体,但MCU只有128KB RAM。我算了一下,光渲染缓冲区就要80KB,再加上系统其他开销,根本跑不起来。最后说服客户改用点阵,项目才顺利交付。
避坑指南:千万别只看Flash占用。矢量字体在Flash上可能只占200KB,但运行时RAM可能吃掉一半。我见过有人把8MB的完整字体文件塞进Flash,结果RAM直接爆了——因为FreeType加载时会把整个字体文件映射到RAM里。
3.6 知识结构图:字体内存占用全景
下面这张图,帮你把本章的核心逻辑串起来。从字体格式出发,分Flash和RAM两条线,最后落到选择策略上。
3.7 小结
这一章的核心就一句话:点阵吃Flash省RAM,矢量吃RAM省Flash。没有绝对的好坏,只有适不适合你的硬件。
我个人习惯是:如果Flash有1MB以上,优先用点阵,省心。如果Flash只有512KB,那就得上矢量子集化,但RAM一定要留够。你想想看,项目做到一半发现RAM不够,那才叫欲哭无泪。