1. 字体显示基础:像素、点阵与字符编码

做嵌入式显示驱动这么多年,我始终觉得字体显示是「看起来简单,做起来全是坑」的领域。很多新手一上来就调像素、刷显存,结果显示出来的字要么缺胳膊少腿,要么乱码一堆。说白了,你得先搞清楚三个基础问题:字长什么样?字怎么编号?字存在哪里?

这一章我们就从最底层开始,把像素、点阵、字符编码和字体文件格式这几个概念彻底捋清楚。嗯,这些都是后面实战的基石,绕不过去的。

1.1 像素与点阵——字是怎么「画」出来的

先问个问题:你在屏幕上看到的「A」,本质是什么?

答案是——一堆亮和不亮的小格子。每个格子就是一个像素(Pixel)。把一堆像素按矩阵排列,亮代表1,不亮代表0,这就构成了点阵(Dot Matrix)。

举个例子,一个 8×8 的点阵可以显示简单的英文字母:

// 字符 'A' 的 8×8 点阵数据(逐行排列)
0x00, // 00000000
0x18, // 00011000
0x24, // 00100100
0x42, // 01000010
0x7E, // 01111110
0x42, // 01000010
0x42, // 01000010
0x00  // 00000000

你看,每一行用一个字节表示,8个bit对应8个像素。1就是亮,0就是暗。这就是最原始的字体数据——点阵字模。

我的习惯:调试阶段我经常用 Excel 画格子,手动填 0/1 来验证点阵数据对不对。比盯着十六进制数猜形状快多了。

点阵字模有几个关键参数:

参数 说明 常见值
宽度 每个字符占的像素列数 8、12、16
高度 每个字符占的像素行数 8、12、16
字节对齐 每行数据按 8bit 对齐 1、2、4 字节
存储顺序 逐行还是逐列 行优先(常用)
我曾经踩过的坑:有一次从网上下了一个 16×16 的汉字字库,结果显示出来全是斜的。查了半天才发现——人家是按列存储的,我按行解析了。字节对齐和存储顺序,这两个参数搞错一个字都别想显示对。

1.2 字符编码基础——字是怎么「编号」的

点阵数据有了,但你怎么告诉屏幕「我要显示字母A」?你得有个编号系统。这就是字符编码(Character Encoding)。

我刚开始做驱动时,觉得编码就是查表,没什么好学的。直到有一次客户说「日文显示乱码」……嗯,从那以后我老老实实把编码体系学了一遍。

1.2.1 ASCII——最基础的编码

ASCII(American Standard Code for Information Interchange)用 7 个 bit 编码 128 个字符。包括:

  • 控制字符(0x00-0x1F):换行、回车、退格等
  • 可打印字符(0x20-0x7E):数字、字母、标点
  • DEL(0x7F):删除

你想想看,一个字节 8 位,ASCII 只用 7 位,剩下 1 位干嘛?扩展呗。于是就有了各种扩展 ASCII,比如把 0x80-0xFF 用来编码西欧语言的带音符字母。

// ASCII 编码示例
'A' = 0x41 = 65
'a' = 0x61 = 97
'0' = 0x30 = 48
'\n' = 0x0A = 10
关键点:ASCII 只适合英文。中文怎么办?一个字节 256 种组合,根本不够用。所以中文必须用多字节编码。

1.2.2 GB2312——中文的「国标」

GB2312 是中国国家标准,1980 年发布。它用两个字节编码一个汉字,理论上能编 256×256=65536 个字符。实际收录了 6763 个汉字和 682 个其他符号。

它的编码规则是这样的:

  • 区码:第一个字节,从 0xA1 到 0xF7(87 个区)
  • 位码:第二个字节,从 0xA1 到 0xFE(94 个位)
  • 每个区有 94 个汉字/符号

举个例子,「啊」字在 16 区 01 位,编码就是 0xB0A1。

我建议:做嵌入式字库时,GB2312 的区码和位码可以直接用来计算字模在字库文件中的偏移地址。公式很简单:offset = ((区码 - 0xA1) * 94 + (位码 - 0xA1)) * 字模大小。这个公式我用了十几年,从来没出过问题。

1.2.3 Unicode——一统天下的编码

GB2312 只支持简体中文,那繁体、日文、韩文怎么办?不同国家各搞一套,乱码就成了家常便饭。Unicode 的目标就是——一个编码对应一个字符,全世界的字符都放在一个表里。

Unicode 的编码空间有 0x0000-0x10FFFF,超过 110 万个码位。实际常用的也就十几万。

但 Unicode 有个问题:存储方式。一个字符可能用 1 个、2 个、3 个甚至 4 个字节表示。这就引出了几种实现方式:

编码方式 特点 嵌入式常用?
UTF-8 变长,兼容 ASCII,英文省空间 常用
UTF-16 变长,基本平面用 2 字节 较少
UTF-32 定长 4 字节,简单但浪费空间 极少
注意:UTF-8 的 BOM(Byte Order Mark)在嵌入式里经常惹麻烦。有些编辑器会在文件头加 3 个字节的 BOM(0xEF 0xBB 0xBF),你的解析程序如果不处理,第一个字符就会显示异常。我习惯在代码里加一段 BOM 检测和跳过逻辑。

1.3 字体文件格式概述——字模存在哪里

编码解决了「字怎么编号」,点阵解决了「字怎么画」。那这些数据存在哪里?——字体文件。

嵌入式领域常见的字体文件格式有这几种:

1.3.1 点阵字体文件(.fon, .dzk, .hzk)

这是最原始、最直接的方式。每个字符的字模数据按顺序排列,查表就能找到。比如:

  • HZK16:16×16 点阵汉字库,每个汉字 32 字节
  • ASC16:8×16 点阵 ASCII 字库,每个字符 16 字节
  • .fon:Windows 点阵字体,包含多种尺寸
// HZK16 字库中查找汉字 '啊' 的偏移
// 区码 = 0xB0 - 0xA1 = 15
// 位码 = 0xA1 - 0xA1 = 0
// 偏移 = (15 * 94 + 0) * 32 = 45120 字节

点阵字体的优点是简单、速度快、占用资源少。缺点是——放大就锯齿,缩小就糊。而且每个字号都要单独做一套字库。

1.3.2 矢量字体(TrueType, OpenType)

矢量字体用数学曲线(贝塞尔曲线)来描述字形轮廓。放大缩小都不失真。TrueType(.ttf)和 OpenType(.otf)是 PC 上的主流。

但在嵌入式里,直接解析 TTF 文件太费资源了。一般做法是:

  1. 在 PC 上用工具把 TTF 渲染成点阵
  2. 提取需要的字符生成点阵字库
  3. 或者用 FreeType 库在运行时渲染
我的经验:如果 MCU 有 512KB 以上的 Flash,我倾向于用 FreeType 动态渲染矢量字体。虽然代码量大一点,但一个 TTF 文件就能覆盖所有字号,维护起来省心多了。如果 Flash 只有 64KB,那就老老实实用点阵字库吧。

1.3.3 自定义二进制字库

很多嵌入式项目会自己定义字库格式。比如:

  • 文件头:包含字符总数、编码范围、字模大小等信息
  • 索引表:每个字符的编码和偏移地址
  • 字模数据:按索引表排列的点阵数据
// 自定义字库文件结构示例
typedef struct {
    uint16_t char_count;      // 字符总数
    uint8_t  char_width;      // 字符宽度(像素)
    uint8_t  char_height;     // 字符高度(像素)
    uint8_t  encoding_type;   // 编码类型:0=ASCII, 1=GB2312, 2=Unicode
    uint32_t index_offset;    // 索引表偏移
    uint32_t data_offset;     // 字模数据偏移
} font_header_t;

typedef struct {
    uint32_t char_code;       // 字符编码
    uint32_t data_offset;     // 字模数据偏移
    uint16_t data_size;       // 字模数据大小
} font_index_t;
核心思路:不管什么格式,最终目的都是——给定一个字符编码,快速找到对应的点阵数据。索引表的设计决定了查找速度。我一般用二分查找,如果字符集是连续的,直接用公式计算偏移更快。

1.4 本章知识体系总览

说了这么多,我把这一章的核心逻辑画成一张图,方便你整体把握:

字体显示基础知识体系 字体显示基础 · 知识体系 像素与点阵 字符编码 字体文件格式 像素定义 点阵矩阵 字模数据 字节对齐 ASCII GB2312 Unicode UTF-8/16/32 点阵字体 矢量字体 自定义字库 索引表 核心目标:给定字符编码 → 快速找到对应点阵数据 → 驱动屏幕显示 三个模块环环相扣,缺一不可 像素与点阵 字符编码 字体文件 核心目标

这张图把本章的三个核心模块串起来了。从左到右看:像素和点阵解决「字怎么画」,字符编码解决「字怎么编号」,字体文件解决「数据存在哪」。三者结合,才能让屏幕正确显示你想要的文字。