1. 字体基础概念:点阵字体与矢量字体的区别、字库结构、字符编码
做嵌入式GUI,绕不开的第一个问题就是:字从哪来?
我刚开始搞嵌入式显示那会儿,以为字体就是一堆像素点。后来踩了不少坑才明白——字体这事儿,往浅了说是个显示问题,往深了说,它牵扯到编码、存储、渲染效率,甚至直接影响整个GUI框架的架构设计。
这一节,咱们就把最基础的东西捋清楚。
1.1 点阵字体 vs 矢量字体
先说结论:点阵字体是“画好的图”,矢量字体是“算出来的形”。
核心区别一句话:点阵字体存的是每个字符的像素矩阵;矢量字体存的是字符轮廓的数学描述(贝塞尔曲线、直线段)。
点阵字体
每个字符对应一个固定大小的位图。比如16×16的点阵,一个字就是32字节(16行×2字节/行)。
- 优点:渲染极快——直接把像素数据拷贝到显存就行,不需要任何计算。
- 缺点:放大就糊,缩小丢细节;每个字号都要单独存一套字库。
- 典型场景:低端MCU、OLED屏、段码屏、资源受限的嵌入式设备。
我的经验:在STM32F103上做菜单显示,我用12×12点阵,一屏显示8行汉字,帧率能跑到60fps。换成矢量字体渲染,同样的MCU直接掉到15fps——所以别盲目追求“高级”,得看硬件吃饭。
矢量字体
用数学曲线描述字形轮廓。TrueType(.ttf)用二次贝塞尔曲线,OpenType(.otf)用三次贝塞尔曲线。
- 优点:无限缩放不失真;一套字库覆盖所有字号。
- 缺点:渲染需要计算轮廓、填充、抗锯齿,CPU开销大。
- 典型场景:带GPU的嵌入式平台(如全志、瑞芯微方案)、高端HMI、智能家居面板。
| 对比维度 | 点阵字体 | 矢量字体 |
|---|---|---|
| 存储方式 | 像素矩阵 | 数学曲线 |
| 缩放质量 | 差(锯齿明显) | 好(平滑缩放) |
| 渲染速度 | 极快(内存拷贝) | 较慢(需计算) |
| 字库体积 | 大(每字号一套) | 小(一套覆盖所有字号) |
| 适用MCU | Cortex-M0/M3/M4 | Cortex-A系列/带FPU |
避坑指南:我曾经在一个项目里,为了“好看”选了矢量字体,结果跑在GD32F450上,每次界面切换都要卡半秒。后来换成预渲染的点阵缓存,流畅度直接起飞。记住:嵌入式里,没有最好的技术,只有最合适的方案。
1.2 字库结构
字库说白了就是一张“字符→字形数据”的映射表。嵌入式里常见的字库结构有三种:
1. 整体字库(全字库)
把所有字符的字形数据按编码顺序连续存放。访问时直接通过偏移地址 = 字符编码 × 单个字形大小计算。
// 16×16点阵全字库访问示例
#define FONT_WIDTH 16
#define FONT_HEIGHT 16
#define BYTES_PER_CHAR (FONT_WIDTH * FONT_HEIGHT / 8) // 32字节
const uint8_t font_data[] = {
// 0x20 空格
0x00, 0x00, 0x00, 0x00, ...,
// 0x21 '!'
0x00, 0x00, 0x1F, 0x00, ...,
// ... 按ASCII顺序排列
};
uint8_t* get_glyph(uint8_t ascii) {
return (uint8_t*)&font_data[ascii * BYTES_PER_CHAR];
}
2. 索引字库(分段字库)
用索引表记录每个字符的起始偏移。适合不连续编码的字符集(比如GB2312)。
// 索引表结构
typedef struct {
uint16_t code; // 字符编码
uint32_t offset; // 在字库文件中的偏移
uint16_t size; // 字形数据大小(字节)
} FontIndexEntry;
// 查找时用二分法
FontIndexEntry* find_glyph(uint16_t code) {
int lo = 0, hi = index_count - 1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (index[mid].code == code) return &index[mid];
if (index[mid].code < code) lo = mid + 1;
else hi = mid - 1;
}
return NULL; // 未找到
}
3. 压缩字库
对字形数据进行RLE(游程编码)或LZ77压缩,运行时解压。适合Flash空间紧张的场景。
我建议:如果Flash在1MB以上,直接用全字库,省心。如果Flash只有256KB,用索引字库+只包含常用汉字(GB2312一级汉字3755个),能省一半空间。
1.3 字符编码
编码这事儿,说白了就是给每个字符一个身份证号。嵌入式里常见的编码有下面几种:
ASCII
7位编码,0x00-0x7F,共128个字符。包含英文字母、数字、标点、控制字符。
- 一个字符占1字节
- 最高位始终为0
- 嵌入式里最常用的编码——简单、省空间
GB2312
中国国家标准,收录6763个汉字。采用双字节编码:
- 区码(0xA1-0xF7)+ 位码(0xA1-0xFE)
- 兼容ASCII(单字节0x00-0x7F表示英文字符)
- 一个汉字占2字节
我曾经踩过的坑:在串口屏项目里,PC端用GB2312发送汉字,MCU端用Unicode解析,结果所有中文都显示成乱码。排查了两天才发现是编码不匹配。记住:发送端和接收端的编码必须一致,否则就是灾难。
Unicode
全球统一编码,给每个字符分配唯一的码点(Code Point)。常见形式:
- UCS-2:固定2字节,可表示BMP(基本多语言平面)内的65536个字符
- UCS-4:固定4字节,可表示所有Unicode字符
UTF-8
Unicode的一种变长编码方案,嵌入式里最推荐使用。
| Unicode范围 | UTF-8编码(二进制) | 占用字节 |
|---|---|---|
| U+0000 ~ U+007F | 0xxxxxxx | 1字节 |
| U+0080 ~ U+07FF | 110xxxxx 10xxxxxx | 2字节 |
| U+0800 ~ U+FFFF | 1110xxxx 10xxxxxx 10xxxxxx | 3字节 |
| U+10000 ~ U+10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 4字节 |
为什么嵌入式推荐UTF-8?
- 兼容ASCII——纯英文文本和ASCII编码完全一样
- 无字节序问题——不像UCS-2需要区分大小端
- 节省空间——英文占1字节,中文占3字节,比UCS-2的2字节略多,但比UCS-4省一半
- 自同步——可以从任意位置开始解析,不会错位
1.4 知识体系总览
下面这张图,把这一节的核心逻辑串起来了: