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就是暗。这就是最原始的字体数据——点阵字模。
点阵字模有几个关键参数:
| 参数 | 说明 | 常见值 |
|---|---|---|
| 宽度 | 每个字符占的像素列数 | 8、12、16 |
| 高度 | 每个字符占的像素行数 | 8、12、16 |
| 字节对齐 | 每行数据按 8bit 对齐 | 1、2、4 字节 |
| 存储顺序 | 逐行还是逐列 | 行优先(常用) |
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
1.2.2 GB2312——中文的「国标」
GB2312 是中国国家标准,1980 年发布。它用两个字节编码一个汉字,理论上能编 256×256=65536 个字符。实际收录了 6763 个汉字和 682 个其他符号。
它的编码规则是这样的:
- 区码:第一个字节,从 0xA1 到 0xF7(87 个区)
- 位码:第二个字节,从 0xA1 到 0xFE(94 个位)
- 每个区有 94 个汉字/符号
举个例子,「啊」字在 16 区 01 位,编码就是 0xB0A1。
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 字节,简单但浪费空间 | 极少 |
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 文件太费资源了。一般做法是:
- 在 PC 上用工具把 TTF 渲染成点阵
- 提取需要的字符生成点阵字库
- 或者用 FreeType 库在运行时渲染
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 本章知识体系总览
说了这么多,我把这一章的核心逻辑画成一张图,方便你整体把握:
这张图把本章的三个核心模块串起来了。从左到右看:像素和点阵解决「字怎么画」,字符编码解决「字怎么编号」,字体文件解决「数据存在哪」。三者结合,才能让屏幕正确显示你想要的文字。