点阵字体原理:存储结构与提取算法
做嵌入式显示驱动,绕不开点阵字体。说白了,点阵字体就是把每个字符画在一个网格里——有像素的地方填1,没有的地方填0。这个道理很简单,但实际用起来,坑不少。
我记得刚入行那会儿,接手一个LED点阵屏项目。屏幕能显示,但字符全是歪的。查了两天才发现,是字模的存储顺序搞反了。从那以后,我对点阵字体的存储结构就特别敏感。
点阵字模的存储结构
一个字符的点阵数据,我们叫它「字模」。字模怎么存?最常见的方式是按字节存储,逐行排列。
举个例子,一个8x8的点阵,每行8个像素,刚好占1个字节。8行就是8个字节。存储时,第1个字节对应第1行,第2个字节对应第2行,以此类推。
核心概念:点阵字模 = 二维像素矩阵 → 按行压缩为一维字节数组。
这里有个关键点:字节内的位顺序。是从高位到低位对应从左到右,还是反过来?不同的字库厂商习惯不同。我建议你在项目一开始就确认清楚,否则后面调起来很痛苦。
常用点阵尺寸
嵌入式系统里,最常见的点阵尺寸就三种:
| 尺寸 | 每字字节数 | 适用场景 | 我的经验 |
|---|---|---|---|
| 8x8 | 8 字节 | 简单图标、状态指示 | 显示英文字母勉强够用,中文就别想了 |
| 8x16 | 16 字节 | 英文字符、数字 | 做菜单显示最常用,省空间又清晰 |
| 16x16 | 32 字节 | 中文汉字 | 显示宋体16x16,基本能认出大部分汉字 |
你想想看,一个16x16的中文字模要32个字节。如果存一二级汉字共6763个,光字库就要216KB。这在资源紧张的MCU上,是个不小的开销。
小技巧:如果Flash空间吃紧,可以考虑只存常用汉字(比如GB2312中的一级汉字3755个),或者用压缩算法。我曾经在一个4KB RAM的单片机上做过汉字显示,最后用了半字节压缩,字库从216KB压到了140KB左右。
字模提取算法
有了字模数据,怎么把某个字符的字模取出来?这就涉及到字模提取算法。
最直接的方法是用「索引表」。每个字符对应一个固定的偏移地址。提取时,用字符编码算出偏移,然后从字库中读取对应字节。
以16x16点阵为例,每个字模32字节。假设字库从地址 FONT_BASE 开始,字符'中'的内码是 0xD6D0,它在字库中的偏移怎么算?
// 伪代码:从GB2312字库中提取16x16字模
#define FONT_BASE 0x08020000 // 字库起始地址
#define FONT_SIZE 32 // 每个字模32字节
uint8_t font_buffer[32];
void get_gb2312_font(uint16_t gb_code, uint8_t *buffer) {
uint8_t qh, ql;
uint32_t offset;
// 区码和位码
qh = (gb_code >> 8) - 0xA0; // 区码
ql = gb_code & 0xFF - 0xA0; // 位码
// 计算偏移
offset = ((qh - 1) * 94 + (ql - 1)) * FONT_SIZE;
// 从Flash读取字模
memcpy(buffer, (uint8_t*)(FONT_BASE + offset), FONT_SIZE);
}
这段代码看着简单,但有个坑:GB2312的编码范围是 0xA1A1 到 0xFEFE,区码和位码都是从1开始的。计算偏移时一定要减1,否则取出来的字模是错的。
注意:不同编码体系的索引算法不同。GB2312、GBK、Unicode、ASCII,每种都有自己的偏移计算方式。我曾经在项目里混用了GBK和GB2312的索引算法,结果显示出来的汉字全乱了。后来花了半天才定位到问题。
字模提取的完整流程
我把整个流程画了张图,方便你理解:
实际项目中的避坑指南
做点阵字体驱动,有几个坑我踩过,分享给你:
- 字节序问题:有些字库是高位在前,有些是低位在前。读取后如果不做转换,显示出来的字符是镜像的。我曾经在STM32上遇到过,后来加了个字节反转函数才解决。
- 字库对齐:如果字库放在外部Flash,读取时要注意地址对齐。有些MCU不支持非对齐访问,强行读会触发硬件错误。
- 缓存策略:频繁从Flash读字模会影响性能。我习惯在RAM里开一个小的字模缓存,把最近用过的几个字模存起来。显示滚动菜单时,速度能快不少。
经验之谈:如果你用的是带FPU的MCU,可以考虑把字模提取做成查表+位运算的形式。不要用除法,用移位和加法。我在Cortex-M4上做过测试,优化后的提取速度能快3倍以上。
小结
点阵字体的核心就三件事:存储结构、尺寸选择、提取算法。存储结构决定了你怎么组织数据,尺寸选择影响显示效果和资源占用,提取算法则直接关系到运行效率。