1. 字体基础概念:点阵与矢量,编码与结构
做嵌入式GUI,说白了就是跟“字”打交道。你想想看,一个屏幕亮起来,上面总得显示点什么吧?是菜单、是数据、还是状态提示,最终都落到一个个字符上。我刚开始做嵌入式产品那会儿,觉得字体嘛,不就是把字画上去吗?后来踩了不少坑才明白——字体选不对,整个UI的体验和性能都会崩。
这一章,咱们就把字体最基础的东西捋清楚。点阵和矢量到底差在哪?字库在芯片里是怎么存的?ASCII、GB2312、Unicode这些编码又是怎么回事?搞懂了这些,后面做字体采集和处理你才不会发懵。
1.1 点阵字体 vs 矢量字体
先说点阵字体。它就像一张格子纸,每个格子要么涂黑要么留白。一个“中”字,在16×16的格子里,哪些格子涂黑是固定的。说白了,它就是一张位图。
点阵字体的核心特点:
- 每个字符对应一个固定大小的像素矩阵(如8×8、16×16、24×24)
- 显示速度快,直接拷贝像素数据到显存就行
- 放大后锯齿严重,边缘像楼梯一样
- 存储空间固定,一个16×16的点阵字模占32字节
矢量字体就不一样了。它存的是轮廓——用贝塞尔曲线描述每个笔画的边缘。显示的时候再根据需要的尺寸去“渲染”成像素。TTF、OTF这些格式都是矢量字体。
我个人习惯,在资源紧张的MCU上优先用点阵。为什么?因为矢量字体需要渲染引擎,还要做抗锯齿,对CPU和RAM的要求高不少。我记得有一次在Cortex-M3上跑矢量字体渲染,帧率直接掉到个位数,后来换成预先生成的点阵字库,问题就解决了。
| 对比项 | 点阵字体 | 矢量字体 |
|---|---|---|
| 存储方式 | 像素矩阵 | 轮廓曲线方程 |
| 缩放效果 | 锯齿明显 | 平滑无损 |
| 显示速度 | 极快(直接拷贝) | 较慢(需渲染) |
| 存储空间 | 固定,与字符数成正比 | 较小,但需额外引擎 |
| 适用场景 | 低资源MCU、固定字号 | 高分辨率屏、可变字号 |
避坑提醒:我曾经在一个项目里直接用16×16点阵显示小字号英文,结果字母“i”和“l”几乎分不清。后来才发现,英文字体在点阵里需要做hinting(微调),否则小字号下笔画会糊在一起。如果你做多语言界面,务必检查每种字符的最小可读尺寸。
1.2 字库结构:芯片里怎么存字?
字库在嵌入式系统里不是随便扔一堆数据就完事的。你得有个索引,不然CPU怎么知道“啊”字的点阵存在哪个地址?
常见的字库结构分两部分:索引表和字模数据区。
- 索引表:每个字符对应一个偏移地址或序号。比如GB2312里的汉字,按区号和位号排好,查表就能算出字模位置。
- 字模数据区:连续存储所有字符的点阵数据。每个字模大小固定(比如16×16点阵就是32字节),所以地址计算很简单:
地址 = 基址 + 字符序号 × 字模大小。
举个例子,一个16×16点阵的“中”字,它的字模数据长这样(十六进制):
// 16x16 点阵 "中" 字(部分数据)
0x00, 0x00, 0x7F, 0xFC, 0x08, 0x20, 0x08, 0x20,
0x08, 0x20, 0x0F, 0xE0, 0x08, 0x20, 0x08, 0x20,
0x08, 0x20, 0x7F, 0xFC, 0x08, 0x20, 0x08, 0x20,
0x08, 0x20, 0x0F, 0xE0, 0x08, 0x20, 0x00, 0x00
你看,每两个字节代表一行16个像素。显示的时候,把这32个字节按顺序刷到屏幕上对应的位置就行。
我的经验:实际项目中,我习惯把字库放在外部Flash里,用内存映射或者SPI读取。索引表放在RAM里做缓存,这样查表快。如果字库太大(比如全GB2312的24×24点阵),还可以做按需加载——只把当前界面用到的字符索引加载到RAM里。
1.3 字符编码:ASCII、GB2312、Unicode、UTF-8
编码这事儿,说白了就是给每个字符一个唯一的数字编号。但不同编码体系给的编号规则不一样,这就容易出乱子。
ASCII
最基础的编码,7位表示一个字符,共128个。包括英文字母、数字、标点和控制字符。一个字节存一个ASCII字符,最高位是0。在嵌入式里,处理英文字符串时ASCII够用,但遇到中文就抓瞎了。
GB2312
国标码,收录了6763个汉字和682个符号。它用两个字节表示一个汉字,每个字节的最高位都是1(区别于ASCII)。GB2312把汉字按区号和位号排列,比如“啊”字在16区01位。
我遇到过一个问题:有些老设备只支持GB2312,但用户输入了一个GBK(GB2312的扩展)里的生僻字,结果显示成乱码。所以做产品时,一定要明确你的字库覆盖了哪些编码范围。
Unicode
Unicode想一统天下,给全世界所有字符一个唯一的编号。目前已经超过14万个字符。但Unicode只是编号,不规定怎么存储。比如“中”字的Unicode编号是U+4E2D。
UTF-8
UTF-8是Unicode的一种存储方式,变长编码。一个字符用1到4个字节表示。ASCII字符用1个字节(兼容ASCII),拉丁字母用2个字节,常用汉字用3个字节,一些生僻字用4个字节。
| 编码 | 字节数 | 字符集 | 嵌入式常见问题 |
|---|---|---|---|
| ASCII | 1字节/字符 | 128个英文字符 | 不支持中文 |
| GB2312 | 2字节/汉字 | 6763汉字+符号 | 与ASCII混用时需区分高位 |
| Unicode | 2或4字节/字符 | 全球字符 | 存储空间大,解析复杂 |
| UTF-8 | 1~4字节/字符 | 全球字符 | 变长解析,需处理多字节序列 |
注意:在嵌入式系统里,UTF-8的变长特性会带来解析开销。如果你要遍历字符串找第N个字符,不能直接下标访问,必须从头扫描。我建议在资源受限的设备上,先把UTF-8转成固定长度的内部编码(比如UCS-2),再处理显示逻辑。
1.4 知识体系总览
下面这张图把本章的核心关系串起来了。你可以看到,从字符编码到字库结构,再到点阵/矢量字体的选择,最终都指向嵌入式GUI的显示输出。
从这张图你能看到,字符编码是源头,决定了你要显示哪些字符;字体类型决定了显示质量和资源消耗;字库结构决定了你怎么在芯片里组织和读取这些数据。这三层串起来,才构成一个完整的嵌入式字体方案。
我的建议:刚开始做嵌入式GUI时,别一上来就追求花哨的矢量字体。先把手头的点阵字库调通,把编码转换搞明白,再考虑升级。我见过太多团队在字体上过度设计,结果产品连基本的中文显示都跑不稳。稳扎稳打,比什么都重要。