2. 点阵字体的原理:像素矩阵、字模提取、BDF格式解析、自建点阵字库
说到嵌入式GUI的字体显示,点阵字体是绕不开的基础。说白了,它就是用一个像素矩阵来记录每个字符的形状。我刚开始做GUI时,觉得矢量字体才是未来,点阵字体太土了。直到有一次在STM32F103上跑一个菜单系统,矢量字库把Flash撑爆了,我才老老实实回头研究点阵。
你想想看,在资源受限的MCU上,点阵字体就是最实在的方案。没有复杂的数学运算,没有内存碎片,一个字模就是一个二维数组。今天我们就把它彻底讲透。
2.1 像素矩阵:一个字就是一张小图
点阵字体的核心思想很简单:每个字符被定义在一个固定大小的网格里。比如16×16点阵,就是16行、16列,总共256个像素点。每个点要么亮(1),要么灭(0)。
举个例子,英文字母'A'在8×8点阵里长这样:
列: 0 1 2 3 4 5 6 7
行0: 0 0 0 1 1 0 0 0
行1: 0 0 1 0 0 1 0 0
行2: 0 0 1 0 0 1 0 0
行3: 0 1 0 0 0 0 1 0
行4: 0 1 1 1 1 1 1 0
行5: 0 1 0 0 0 0 1 0
行6: 0 1 0 0 0 0 1 0
行7: 0 0 0 0 0 0 0 0
看到没?1的位置拼起来就是一个'A'的形状。这就是像素矩阵,最原始的字体数据。
关键参数:
- 点阵尺寸:常见的有8×8、12×12、16×16、24×24、32×32
- 字模大小 = 行数 × (列数 / 8),单位字节。16×16点阵就是16 × (16/8) = 32字节
- 存储格式:横向取模或纵向取模,决定了字节排列顺序
2.2 字模提取:从像素到字节
字模提取,就是把像素矩阵转换成二进制数据的过程。我个人习惯用横向取模,因为和LCD的显存排列方式一致,显示时不需要额外转换。
横向取模规则:
- 从左到右扫描每一行
- 每8个像素组成一个字节
- 高位在左,低位在右
- 行末不足8位时补0
拿上面那个8×8的'A'来说,横向取模的结果是:
// 8x8 点阵 'A' 字模(横向取模)
const uint8_t font_A[8] = {
0x18, // 0001 1000
0x24, // 0010 0100
0x24, // 0010 0100
0x42, // 0100 0010
0x7E, // 0111 1110
0x42, // 0100 0010
0x42, // 0100 0010
0x00 // 0000 0000
};
我的经验: 在项目中我常用一个Python脚本,把字体文件批量转换成C数组。写个循环遍历每个字符的像素,按横向取模规则打包成字节,直接生成.h文件。省时省力,还不会出错。
2.3 BDF格式解析:开源字体的通用语言
BDF(Bitmap Distribution Format)是Adobe定义的一种点阵字体格式。它是纯文本的,用眼睛就能看懂。我在一个开源项目里第一次接触BDF,当时觉得这格式太友好了,比那些二进制格式好解析一百倍。
BDF文件结构:
STARTFONT 2.1
FONT -Misc-Fixed-Medium-R-Normal--8-80-75-75-C-50-ISO10646-1
SIZE 8 75 75
FONTBOUNDINGBOX 8 8 0 -1
STARTPROPERTIES 2
FONT_ASCENT 7
FONT_DESCENT 1
ENDPROPERTIES
CHARS 256
STARTCHAR A
ENCODING 65
SWIDTH 500 0
DWIDTH 8 0
BBX 8 8 0 -1
BITMAP
18
24
24
42
7E
42
42
00
ENDCHAR
...
ENDFONT
关键字段解析:
| 字段 | 含义 | 说明 |
|---|---|---|
| STARTFONT | 版本号 | 通常为2.1 |
| FONTBOUNDINGBOX | 全局边界框 | 宽度 高度 X偏移 Y偏移 |
| STARTCHAR | 字符开始 | 后面跟字符名称 |
| ENCODING | 字符编码 | ASCII或Unicode码点 |
| BBX | 字符边界框 | 宽度 高度 X偏移 Y偏移 |
| BITMAP | 字模数据 | 十六进制,每行一个字节 |
注意: BDF中的Y偏移是相对于基线向上为正。我曾经在解析时搞反了符号,结果所有字符都往上飘了2个像素。排查了半天才发现是Y偏移处理错了。嗯,这里一定要小心。
2.4 自建点阵字库:从零到一
很多时候,现成的BDF字体要么太大,要么字符集不全。我建议自己建一个字库,只包含项目需要的字符。这样Flash占用最小,访问速度也最快。
自建字库的步骤:
- 确定需求:需要哪些字符?多大点阵?
- 获取源数据:从BDF文件提取,或用字模提取工具生成
- 设计数据结构:用数组还是链表?索引怎么查?
- 生成字库文件:输出为C头文件或二进制文件
- 编写访问接口:提供根据字符编码获取字模的函数
一个实用的字库结构:
// 字库索引表
typedef struct {
uint16_t code; // Unicode编码
uint16_t offset; // 字模在数据区中的偏移(字节)
uint8_t width; // 字符实际宽度(像素)
uint8_t height; // 字符实际高度(像素)
} FontIndex_t;
// 字库信息头
typedef struct {
uint8_t fontHeight; // 字体高度(像素)
uint8_t lineHeight; // 行高(像素)
uint16_t charCount; // 字符总数
const FontIndex_t *index; // 索引表指针
const uint8_t *data; // 字模数据指针
} FontInfo_t;
// 获取字模接口
const uint8_t* Font_GetGlyph(const FontInfo_t *font,
uint16_t code,
uint8_t *width,
uint8_t *height);
我的建议: 索引表用二分查找。把字符编码排序后,查找复杂度从O(n)降到O(log n)。对于几百个字符的字库,速度提升很明显。我在一个菜单系统里用了这个方法,切换页面时字体加载几乎感觉不到延迟。
2.5 点阵字体的显示流程
把字模显示到屏幕上,其实就三步:
- 根据字符编码查索引,找到字模数据
- 按点阵尺寸逐行读取字节
- 把每个bit映射到LCD的像素点
核心代码就几行:
void Font_DrawChar(int x, int y, uint16_t code, uint32_t color) {
uint8_t width, height;
const uint8_t *glyph = Font_GetGlyph(&myFont, code, &width, &height);
if (!glyph) return;
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
// 判断当前像素是否点亮
if (glyph[row * ((width + 7) / 8)] & (0x80 >> (col % 8))) {
LCD_DrawPixel(x + col, y + row, color);
}
}
}
}
性能优化要点:
- 预计算每行字节数,避免重复计算
- 用位运算代替取模和除法
- 如果LCD支持批量写,先缓存一行像素再一次性刷出
- 对于固定大小的字体,可以展开循环
点阵字体看着简单,但用好了效果一点不差。我在一个工业仪表项目里,用16×16点阵配合抗锯齿算法,显示效果几乎能和矢量字体媲美。关键是,整个字库只占了不到8KB的Flash。
记住一句话:在嵌入式世界里,简单就是美。点阵字体就是这种美的体现。