4. 点阵字体显示:单色点阵字体的显示算法、8x8/16x16点阵的C语言实现、字模数据在内存中的排列方式
点阵字体,说白了就是用一个网格来画字。每个格子要么亮(1),要么灭(0)。你想想看,LED广告牌、老式手机屏幕、电子价签,这些资源受限的设备上,点阵字体依然是主力。
我刚开始做嵌入式GUI时,总觉得用字库芯片或者矢量字体才够专业。后来做一个温控器项目,MCU只有2KB RAM,Flash也紧巴巴的。嗯,最后还是老老实实回到点阵字体。效果其实不差,关键是心里有底——我知道每个像素是怎么来的。
4.1 单色点阵字体的显示原理
单色点阵的核心就一句话:用二进制位控制像素的亮灭。
一个8x8的点阵,就是64个像素点。在内存里,我们用8个字节来表示。每个字节的8个bit,对应一列(或一行)的8个像素。
关键公式:
点阵大小 = 宽度(像素) × 高度(像素) / 8 (字节)
例如:16x16点阵 = 16 × 16 / 8 = 32字节
显示算法其实不复杂。逐行扫描,判断每个bit是1还是0。是1就画点,是0就跳过。我在一个电梯楼层显示项目里,就是用这个逻辑驱动128x64的OLED屏,刷新率能做到60fps。
4.2 字模数据在内存中的排列方式
这里有个坑,我当年栽过。字模的排列方式,不同厂家的工具生成的结果可能完全不同。
常见的排列方式有两种:
| 排列方式 | 说明 | 常见场景 |
|---|---|---|
| 列行式(Column-Major) | 按列存储,每列一个字节,从上到下 | 大多数取模软件默认方式 |
| 行列式(Row-Major) | 按行存储,每行一个字节,从左到右 | 某些LCD控制器硬件要求 |
我习惯用列行式。为什么?因为很多点阵屏的驱动芯片,数据写入顺序就是按列来的。你按行列式存,显示的时候还得转置,多一道工序就多一个bug源。
举个例子,一个8x8点阵的字母'A',列行式存储可能是这样的:
// 8x8 点阵 'A' - 列行式排列
// 每列一个字节,从上到下
const uint8_t font_A[8] = {
0x00, // 第0列:全灭
0x18, // 第1列:00011000
0x24, // 第2列:00100100
0x42, // 第3列:01000010
0x7E, // 第4列:01111110
0x42, // 第5列:01000010
0x42, // 第6列:01000010
0x00 // 第7列:全灭
};
你看,第4列是0x7E,也就是中间那一横。这就是'A'的骨架。
我曾经踩过的坑:用某款取模软件生成16x16中文字模,默认是行列式。我直接烧进去,显示出来的字是躺倒的。后来排查了半天,才发现是排列方式不匹配。从那以后,我每次拿到字模数据,第一件事就是确认排列方式。
4.3 8x8点阵的C语言实现
8x8点阵适合显示ASCII字符。实现起来很直接。
先定义一个字体表:
// 8x8 ASCII字体表(部分)
// 列行式排列
const uint8_t ascii_font[95][8] = {
// 空格 ' ' (0x20)
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
// '!' (0x21)
{0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x18, 0x00},
// '"' (0x22)
{0x24, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
// ... 中间省略 ...
// 'A' (0x41)
{0x00, 0x18, 0x24, 0x42, 0x7E, 0x42, 0x42, 0x00},
// ... 后续字符 ...
};
显示函数的核心逻辑:
/**
* @brief 在指定位置显示一个8x8字符
* @param x, y 屏幕坐标(左上角)
* @param ch 要显示的ASCII字符
*/
void draw_char_8x8(uint16_t x, uint16_t y, char ch) {
uint8_t idx = ch - 0x20; // 字符在字体表中的索引
if (idx > 94) return; // 超出范围
for (uint8_t col = 0; col < 8; col++) {
uint8_t data = ascii_font[idx][col];
for (uint8_t row = 0; row < 8; row++) {
if (data & (0x80 >> row)) {
// 该像素点亮
set_pixel(x + col, y + row, 1);
} else {
// 该像素熄灭
set_pixel(x + col, y + row, 0);
}
}
}
}
这里有个细节:0x80 >> row 是从高位到低位扫描。因为字模数据的高bit对应点阵的上方。你要是搞反了,字就上下颠倒了。
4.4 16x16点阵的C语言实现
16x16点阵主要用来显示中文。一个汉字需要32个字节。
16x16的显示逻辑和8x8类似,但要注意:每个字节只表示8个像素,所以一列需要2个字节。
/**
* @brief 在指定位置显示一个16x16汉字
* @param x, y 屏幕坐标(左上角)
* @param font 字模数据指针(32字节)
*/
void draw_chinese_16x16(uint16_t x, uint16_t y, const uint8_t *font) {
for (uint8_t col = 0; col < 16; col++) {
// 每列需要2个字节:上半部分和下半部分
uint8_t data_high = font[col * 2]; // 上半部分
uint8_t data_low = font[col * 2 + 1]; // 下半部分
// 绘制上半部分(第0-7行)
for (uint8_t row = 0; row < 8; row++) {
if (data_high & (0x80 >> row)) {
set_pixel(x + col, y + row, 1);
}
}
// 绘制下半部分(第8-15行)
for (uint8_t row = 0; row < 8; row++) {
if (data_low & (0x80 >> row)) {
set_pixel(x + col, y + 8 + row, 1);
}
}
}
}
我个人的优化习惯:如果MCU性能允许,我会把内层循环展开。8次循环的展开,在72MHz的Cortex-M3上能省下大约2μs。别小看这点时间,显示一整屏汉字时,差距就出来了。
4.5 点阵字体的显示算法流程
我把整个流程画成了一张图,方便你理解数据是怎么从字模变成屏幕上的像素的。
4.6 性能优化与避坑
实际项目中,点阵显示的性能瓶颈往往不在计算,而在像素写入速度。我做过一个测试:
| 操作 | 耗时(72MHz Cortex-M3) | 说明 |
|---|---|---|
| 逐像素写入(8x8) | ~12μs | 每次调用set_pixel |
| 整列写入(8x8) | ~3μs | 一次写入8个像素 |
| DMA批量写入(16x16) | ~8μs | 整字模一次性传输 |
你看,整列写入比逐像素快了4倍。如果你的屏幕驱动支持列写入模式,一定要用。
我曾经遇到的一个诡异问题:在ST7789驱动的屏幕上显示16x16汉字,边缘总有毛刺。排查到最后,发现是字模数据里包含了非预期的bit。有些取模软件会在字节末尾补0,但补的位置不对。解决方案很简单:显示前做一次掩码操作,只取有效的8个bit。
4.7 字模数据的生成与管理
实际项目中,字模数据通常用工具生成。我个人推荐两种方式:
- PC端取模软件:如PCtoLCD2002、FontCreator。设置好点阵大小和排列方式,一键导出C数组。
- Python脚本生成:用PIL库读取系统字体,渲染成点阵。适合需要动态生成字模的场景。
不管用哪种方式,一定要在代码里加上注释,标明排列方式。我见过太多项目,换了一个人维护,字模排列方式搞混了,整个显示系统崩掉。
// 字模数据示例 - 16x16汉字"中"
// 排列方式:列行式,高位在上
// 生成工具:PCtoLCD2002 v2.0
const uint8_t font_zhong[32] = {
0x00, 0x00, // 第0列
0x00, 0x00, // 第1列
0x00, 0x00, // 第2列
0x7F, 0xFC, // 第3列:11111111 11111100
0x08, 0x04, // 第4列
0x08, 0x04, // 第5列
0x08, 0x04, // 第6列
0x7F, 0xFC, // 第7列
0x08, 0x04, // 第8列
0x08, 0x04, // 第9列
0x08, 0x04, // 第10列
0x7F, 0xFC, // 第11列
0x00, 0x00, // 第12列
0x00, 0x00, // 第13列
0x00, 0x00, // 第14列
0x00, 0x00 // 第15列
};
嗯,看到第3列和第7列的0x7F、0xFC了吗?那就是"中"字的那一竖和一横。点阵字体就是这么直观——你看到数据,就能在脑子里画出字形。
做嵌入式GUI,点阵字体是基本功。别看现在各种高级字体渲染库满天飞,真正到了资源受限的场合,点阵字体依然是那个最可靠的选择。把这一章的内容吃透,后面讲抗锯齿、灰度渲染的时候,你才能理解那些优化到底在优化什么。