1. 字体基础概念:点阵字体与矢量字体的区别、字库结构、字符编码

做嵌入式GUI,绕不开的第一个问题就是:字从哪来?

我刚开始搞嵌入式显示那会儿,以为字体就是一堆像素点。后来踩了不少坑才明白——字体这事儿,往浅了说是个显示问题,往深了说,它牵扯到编码、存储、渲染效率,甚至直接影响整个GUI框架的架构设计。

这一节,咱们就把最基础的东西捋清楚。

1.1 点阵字体 vs 矢量字体

先说结论:点阵字体是“画好的图”,矢量字体是“算出来的形”。

核心区别一句话:点阵字体存的是每个字符的像素矩阵;矢量字体存的是字符轮廓的数学描述(贝塞尔曲线、直线段)。

点阵字体

每个字符对应一个固定大小的位图。比如16×16的点阵,一个字就是32字节(16行×2字节/行)。

  • 优点:渲染极快——直接把像素数据拷贝到显存就行,不需要任何计算。
  • 缺点:放大就糊,缩小丢细节;每个字号都要单独存一套字库。
  • 典型场景:低端MCU、OLED屏、段码屏、资源受限的嵌入式设备。

我的经验:在STM32F103上做菜单显示,我用12×12点阵,一屏显示8行汉字,帧率能跑到60fps。换成矢量字体渲染,同样的MCU直接掉到15fps——所以别盲目追求“高级”,得看硬件吃饭。

矢量字体

用数学曲线描述字形轮廓。TrueType(.ttf)用二次贝塞尔曲线,OpenType(.otf)用三次贝塞尔曲线。

  • 优点:无限缩放不失真;一套字库覆盖所有字号。
  • 缺点:渲染需要计算轮廓、填充、抗锯齿,CPU开销大。
  • 典型场景:带GPU的嵌入式平台(如全志、瑞芯微方案)、高端HMI、智能家居面板。
对比维度 点阵字体 矢量字体
存储方式 像素矩阵 数学曲线
缩放质量 差(锯齿明显) 好(平滑缩放)
渲染速度 极快(内存拷贝) 较慢(需计算)
字库体积 大(每字号一套) 小(一套覆盖所有字号)
适用MCU Cortex-M0/M3/M4 Cortex-A系列/带FPU

避坑指南:我曾经在一个项目里,为了“好看”选了矢量字体,结果跑在GD32F450上,每次界面切换都要卡半秒。后来换成预渲染的点阵缓存,流畅度直接起飞。记住:嵌入式里,没有最好的技术,只有最合适的方案。

1.2 字库结构

字库说白了就是一张“字符→字形数据”的映射表。嵌入式里常见的字库结构有三种:

1. 整体字库(全字库)

把所有字符的字形数据按编码顺序连续存放。访问时直接通过偏移地址 = 字符编码 × 单个字形大小计算。

// 16×16点阵全字库访问示例
#define FONT_WIDTH  16
#define FONT_HEIGHT 16
#define BYTES_PER_CHAR (FONT_WIDTH * FONT_HEIGHT / 8)  // 32字节

const uint8_t font_data[] = {
    // 0x20 空格
    0x00, 0x00, 0x00, 0x00, ...,
    // 0x21 '!'
    0x00, 0x00, 0x1F, 0x00, ...,
    // ... 按ASCII顺序排列
};

uint8_t* get_glyph(uint8_t ascii) {
    return (uint8_t*)&font_data[ascii * BYTES_PER_CHAR];
}

2. 索引字库(分段字库)

用索引表记录每个字符的起始偏移。适合不连续编码的字符集(比如GB2312)。

// 索引表结构
typedef struct {
    uint16_t code;      // 字符编码
    uint32_t offset;    // 在字库文件中的偏移
    uint16_t size;      // 字形数据大小(字节)
} FontIndexEntry;

// 查找时用二分法
FontIndexEntry* find_glyph(uint16_t code) {
    int lo = 0, hi = index_count - 1;
    while (lo <= hi) {
        int mid = (lo + hi) / 2;
        if (index[mid].code == code) return &index[mid];
        if (index[mid].code < code) lo = mid + 1;
        else hi = mid - 1;
    }
    return NULL;  // 未找到
}

3. 压缩字库

对字形数据进行RLE(游程编码)或LZ77压缩,运行时解压。适合Flash空间紧张的场景。

我建议:如果Flash在1MB以上,直接用全字库,省心。如果Flash只有256KB,用索引字库+只包含常用汉字(GB2312一级汉字3755个),能省一半空间。

1.3 字符编码

编码这事儿,说白了就是给每个字符一个身份证号。嵌入式里常见的编码有下面几种:

ASCII

7位编码,0x00-0x7F,共128个字符。包含英文字母、数字、标点、控制字符。

  • 一个字符占1字节
  • 最高位始终为0
  • 嵌入式里最常用的编码——简单、省空间

GB2312

中国国家标准,收录6763个汉字。采用双字节编码:

  • 区码(0xA1-0xF7)+ 位码(0xA1-0xFE)
  • 兼容ASCII(单字节0x00-0x7F表示英文字符)
  • 一个汉字占2字节

我曾经踩过的坑:在串口屏项目里,PC端用GB2312发送汉字,MCU端用Unicode解析,结果所有中文都显示成乱码。排查了两天才发现是编码不匹配。记住:发送端和接收端的编码必须一致,否则就是灾难。

Unicode

全球统一编码,给每个字符分配唯一的码点(Code Point)。常见形式:

  • UCS-2:固定2字节,可表示BMP(基本多语言平面)内的65536个字符
  • UCS-4:固定4字节,可表示所有Unicode字符

UTF-8

Unicode的一种变长编码方案,嵌入式里最推荐使用。

Unicode范围 UTF-8编码(二进制) 占用字节
U+0000 ~ U+007F 0xxxxxxx 1字节
U+0080 ~ U+07FF 110xxxxx 10xxxxxx 2字节
U+0800 ~ U+FFFF 1110xxxx 10xxxxxx 10xxxxxx 3字节
U+10000 ~ U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 4字节

为什么嵌入式推荐UTF-8?

  • 兼容ASCII——纯英文文本和ASCII编码完全一样
  • 无字节序问题——不像UCS-2需要区分大小端
  • 节省空间——英文占1字节,中文占3字节,比UCS-2的2字节略多,但比UCS-4省一半
  • 自同步——可以从任意位置开始解析,不会错位

1.4 知识体系总览

下面这张图,把这一节的核心逻辑串起来了:

嵌入式GUI字体显示基础 · 知识体系 字体显示基础 字体类型 点阵字体 矢量字体 像素矩阵 vs 数学曲线 字库结构 全字库(连续存储) 索引字库(二分查找) 压缩字库(RLE/LZ77) 空间 vs 速度 权衡 字符编码 ASCII(1字节) GB2312(2字节) Unicode(UCS-2/4) UTF-8(变长编码) 推荐嵌入式使用 UTF-8 选型原则:硬件资源 → 显示需求 → 编码方案 → 字库结构