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占用最小,访问速度也最快。

自建字库的步骤:

  1. 确定需求:需要哪些字符?多大点阵?
  2. 获取源数据:从BDF文件提取,或用字模提取工具生成
  3. 设计数据结构:用数组还是链表?索引怎么查?
  4. 生成字库文件:输出为C头文件或二进制文件
  5. 编写访问接口:提供根据字符编码获取字模的函数

一个实用的字库结构:

// 字库索引表
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 点阵字体的显示流程

把字模显示到屏幕上,其实就三步:

  1. 根据字符编码查索引,找到字模数据
  2. 按点阵尺寸逐行读取字节
  3. 把每个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支持批量写,先缓存一行像素再一次性刷出
  • 对于固定大小的字体,可以展开循环
点阵字体知识体系 点阵字体 像素矩阵 字模提取 BDF格式解析 自建点阵字库 像素矩阵 网格化存储 1表示点亮 0表示熄灭 字模提取 横向/纵向取模 每8像素1字节 生成C数组 BDF格式解析 纯文本格式 BBX边界框 BITMAP数据段 自建点阵字库 索引表+数据区 二分查找加速 按需裁剪字符集 显示流程:查索引 → 读字模 → 逐像素绘制

点阵字体看着简单,但用好了效果一点不差。我在一个工业仪表项目里,用16×16点阵配合抗锯齿算法,显示效果几乎能和矢量字体媲美。关键是,整个字库只占了不到8KB的Flash。

记住一句话:在嵌入式世界里,简单就是美。点阵字体就是这种美的体现。