中文字库核心概念
在嵌入式项目中显示汉字,需解决 “计算机如何存储和识别汉字” 的问题 —— 早期 ANSI 字符集仅收录 256 个字符(无中文),因此中国制定了GB2312 简体中文字符集,成为嵌入式中文显示的核心标准。
从 ANSI 到 GB2312 的演进
- ANSI 字符集局限:仅包含英文、数字和少量符号(共 256 个),无法满足中文存储需求。
- GB2312 字符集(GB2312-80):
- 全称《信息交换用汉字编码字符集・基本集》,1981 年实施,覆盖 99.75% 的常用汉字,满足主流场景。
- 收录内容:共 7445 个图形字符,包括:
- 6763 个汉字(一级汉字 3755 个,按拼音排序;二级汉字 3008 个,按部首 / 笔画排序);
- 682 个全角字符(拉丁字母、希腊字母、日文假名、俄文字母等)。
GB2312 的分区规则(关键!定位汉字的基础)
GB2312 将所有字符按 “区 - 位” 划分,每个区含 94 个字符(位),共 94 个区,分区规则决定了汉字的存储位置:
| 区号范围 | 字符类型 | 说明 |
|---|---|---|
| 01-09 | 特殊符号 | 标点、序号、数字、拼音符号等 |
| 10-15 | 未编码(空区) | 预留区域 |
| 16-55 | 一级汉字(常用字) | 按拼音字母顺序排列(如 “啊” 在 16 区 01 位) |
| 56-87 | 二级汉字(生僻字) | 按部首和笔画数排序 |
| 88-94 | 未编码(空区) | 预留区域 |
注意:只有 “区 + 位” 的组合(即区位码),才能唯一确定一个字符,与汉字的显示大小无关。
汉字编码与字库定位
GB2312 通过双字节编码存储汉字,且通过 “区码 + 位码” 定位汉字在字库中的位置,这是中文显示的核心逻辑。
双字节编码规则(区码与位码)
GB2312 汉字用 2 个字节表示(高字节 = 区码,低字节 = 位码),编码计算规则如下:
- 区号:对应字符所在的 “区”(1-94),转换为区码(高字节):
区码 = 0xA0 + 区号; - 位号:对应字符在区内的 “位”(1-94),转换为位码(低字节):
位码 = 0xA0 + 位号;- 原因:0x00~0xFF为ASCII码,避免和 ASCII 码冲突
0x00~0x1F:不可见控制字符(如换行、回车);0x20~0x7E:可见字符(如字母A是0x41、数字0是0x30);0x80~0xFF:ASCII 码未定义的 “扩展区域”(早期未被标准化使用)。
- 最终汉字编码 = 区码(高字节) + 位码(低字节)(十六进制)。
示例:“啊” 字的编码计算
- “啊” 是一级汉字,位于 16 区 01 位(区号 16,位号 01);
- 区码 = 0xA0 + 16 = 0xB0(十六进制);
- 位码 = 0xA0 + 1 = 0xA1(十六进制);
- 因此 “啊” 的 GB2312 编码为
0xB0A1(双字节:高字节 0xB0,低字节 0xA1)。
编码验证代码
GB2312 汉字是双字节,需 2 个字节存储,数组大小应为 2,以下验证代码(需确保文件编码为 GB2312):
#include <stdio.h>
int main(int argc, char const *argv[]) {// GB2312汉字为双字节,数组大小设为2(无需额外存'\0',因不是字符串)char font_buf[2] = "啊"; // 正确:2字节存储双字节编码// 输出高字节(区码)和低字节(位码)printf("高字节(区码):%#x\n", (unsigned char)font_buf[0]); // 输出0xB0(正确)printf("低字节(位码):%#x\n", (unsigned char)font_buf[1]); // 输出0xA1(正确)return 0;
}
编译运行注意:
- 若编译器默认编码为 UTF-8,需手动设置文件编码为 GB2312(如 VS Code 右下角切换);
- 运行结果应为
0xb0和0xa1,与 “啊” 的编码一致,若输出其他值,需检查文件编码。
汉字在字库中的位置定位
汉字在字库中的存储位置由区号和位号决定(与汉字大小无关),位置计算分两步:
确定汉字在字库中的索引:每个区有 94 个汉字,因此索引 =(区号 - 1)× 94 +(位号 - 1);
- 示例:“啊”(区号 16,位号 1)的索引 =(16-1)×94 +(1-1)= 15×94 = 1410(第 1411 个汉字,从 0 开始)。
计算字库中的偏移量:偏移量 = 索引 × 单个汉字占用字节数;
- 单个汉字占用字节数由显示分辨率决定(点阵字库),公式:
字节数 = (宽度 / 8) × 高度(1 字节 = 8 像素)。
点阵汉字占用字节计算(关键公式)
嵌入式常用点阵字库(如 16×16、24×24),每个汉字的占用字节数由分辨率决定,示例如下:
| 汉字分辨率 | 宽度(像素) | 高度(像素) | 每行列数(字节)= 宽度 / 8 | 总字节数 = 每行列数 × 高度 |
|---|---|---|---|---|
| 16×16 | 16 | 16 | 16/8 = 2 | 2×16 = 32 |
| 24×24 | 24 | 24 | 24/8 = 3 | 3×24 = 72 |
| 32×32 | 32 | 32 | 32/8 = 4 | 4×32 = 128 |
示例:16×16 的 “你” 字,共占用 32 字节,点阵数据如下(每行 2 字节,16 行):

显示逻辑:将点阵数据的每一位(0/1)与 LCD 像素对应,1 表示显示前景色,0 表示背景色,逐行逐像素绘制。
字库类型与生成
嵌入式中文显示常用两种字库:点阵字库(固定分辨率)和TrueType 字库(矢量可缩放),需根据需求选择。
点阵字库(固定分辨率)
- 特点:基于像素点阵存储,每个汉字对应固定的像素数据,如 16×16、24×24;
- 优点:结构简单,解码快,占用内存小,适合资源有限的嵌入式设备(如 51 单片机、小型 ARM 开发板);
- 缺点:分辨率固定,放大后会出现锯齿(失真),不同分辨率需不同字库(如 16×16 和 24×24 需两个字库)。
- 生成工具:PC 端取模软件(如 PCtoLCD2002、字模大师),可自定义分辨率、取模方式(行列式、行优先)、颜色(16 色 / 256 色)。
- 使用场景:OLED 屏(小分辨率)、简单 LCD 显示(固定字体大小)。
TrueType 字库(矢量可缩放)
-
特点:基于数学矢量曲线描述字体轮廓,而非像素,文件格式为
.ttf(TrueType Font);- 优点:可任意缩放不失真,支持多种字体风格(宋体、黑体、楷体),一个字库支持所有分辨率;
- 缺点:解码需计算矢量曲线,占用 CPU 资源稍多,适合中高端嵌入式设备(如 ARM Cortex-A 系列开发板)。
-
系统存放路径:
-
Windows:
C:\Windows\Fonts(如simkai.ttf是楷体,simsun.ttf是宋体);
-
Linux:
/usr/share/fonts(系统默认路径,用户可添加自定义路径)。
-
-
开发板移植:将 Windows 的
.ttf文件(如simkai.ttf)通过scp拷贝到开发板的/usr/share/fonts目录,即可被程序加载使用:# 示例:将Windows的simkai.ttf拷贝到开发板(开发板IP:192.168.1.100) scp C:\Windows\Fonts\simkai.ttf root@192.168.1.100:/usr/share/fonts/
嵌入式中文字库调用(基于 TTF 字库)
嵌入式中调用 TTF 字库需依赖字体库接口(如简化版font接口或开源FreeType库)
font.h(头文件) libfont.a(字体库)
图解

核心函数详解(含参数 / 返回值注释)
假设使用嵌入式简化字体库(如基于FreeType封装),核心函数包括 “加载字库→设置字体大小→创建画布→绘制文字→显示到 LCD”,函数注释如下:
加载 TTF 字库文件
/*** @brief 加载TrueType字库文件(如simkai.ttf楷体)* @param fontPath TTF字库文件路径(例如"/usr/share/fonts/simkai.ttf")* @return 成功:指向font结构体的指针(后续字体操作依赖此指针);失败:NULL* @note 失败原因可能包括:路径错误、文件不存在、权限不足(建议chmod 644 simkai.ttf)*/
font* fontLoad(char* fontPath);
设置字体显示大小
/*** @brief 设置字体的显示高度(像素)* @param f 已加载的font指针(fontLoad返回值,不可为NULL)* @param pixels 字体高度(像素,如16、24、72,宽度按字体比例自动计算)* @note 无返回值,若f为NULL或pixels≤0,可能导致后续绘制异常* @example 显示大尺寸楷体:fontSetSize(f, 72); // 72像素高*/
void fontSetSize(font* f, s32 pixels);
创建带初始化的画布
/*** @brief 创建画布(存储文字像素数据)并初始化为指定背景色* @param width 画布宽度(像素,需≥文字总宽度,避免截断)* @param height 画布高度(像素,需≥字体高度,避免截断)* @param byteperpixel 位深(字节/像素,LCD通常为4字节ARGB)* @param c 背景色(ARGB格式,通过getColor(a,b,c,d)生成,如getColor(255,255,255,255)为白色)* @return 成功:指向bitmap结构体的指针;失败:NULL(内存分配失败)* @note 显示“晚安”(2个汉字)用72号楷体时,画布宽建议≥200(72×2 + 预留间距)*/
bitmap* createBitmapWithInit(u32 width, u32 height, u32 byteperpixel, color c);
将文字绘制到画布
/*** @brief 将字符串(支持中文楷体)绘制到画布指定位置* @param f 已加载的font指针(不可为NULL)* @param screen 目标画布指针(createBitmapWithInit返回值,不可为NULL)* @param x 文字在画布上的起始x坐标(左上角为(0,0))* @param y 文字在画布上的起始y坐标(建议设为字体高度,避免顶部截断)* @param text 待绘制字符串(GB2312编码,如"楷体测试")* @param c 文字颜色(ARGB格式,如getColor(255,255,0,0)为红色)* @param maxWidth 文字显示的最大宽度(像素,超过此宽度会截断)* @note 字符串需为GB2312编码,否则楷体显示乱码;maxWidth用于限制单行显示范围*/
void fontPrint(font* f, bitmap* screen, s32 x, s32 y, char* text, color c, s32 maxWidth);
将画布显示到 LCD
/*** @brief 将画布像素数据显示到LCD指定位置* @param p LCD内存映射首地址(mmap返回的unsigned int*指针)* @param px 画布在LCD上的起始x坐标(左上角为(0,0))* @param py 画布在LCD上的起始y坐标* @param bm 待显示的画布指针(不可为NULL)* @note 需确保px + bm->width ≤ LCD宽度,py + bm->height ≤ LCD高度,避免越界*/
void show_font_to_lcd(unsigned int* p, int px, int py, bitmap* bm);
基础调用示例(用楷体显示 “晚安” 到 LCD)
结合 LCD 初始化,使用simkai.ttf楷体显示文字的完整代码:
#include "font.h"
#include <unistd.h> // 包含sleep函数声明// 初始化LCD设备,增加错误信息输出
struct LcdDevice *init_lcd(const char *device) {if (device == NULL) {fprintf(stderr, "Error: device path is NULL\n");return NULL;}// 申请LCD设备结构体空间struct LcdDevice *lcd = malloc(sizeof(struct LcdDevice));if (lcd == NULL) {perror("malloc LcdDevice failed");return NULL;}// 打开LCD设备文件lcd->fd = open(device, O_RDWR);if (lcd->fd < 0) {perror("open lcd device failed");free(lcd);return NULL;}// 内存映射LCD显存(800×480分辨率,4字节ARGB)lcd->mp = mmap(NULL, 800 * 480 * 4, PROT_READ | PROT_WRITE, MAP_SHARED, lcd->fd, 0);if (lcd->mp == MAP_FAILED) {perror("mmap lcd failed");close(lcd->fd);free(lcd);return NULL;}return lcd;
}// 释放LCD设备资源
void release_lcd(struct LcdDevice *lcd) {if (lcd == NULL) return;// 解除内存映射if (lcd->mp != MAP_FAILED && lcd->mp != NULL) {munmap(lcd->mp, 800 * 480 * 4);lcd->mp = NULL;}// 关闭文件描述符if (lcd->fd >= 0) {close(lcd->fd);lcd->fd = -1;}// 释放结构体空间free(lcd);
}int main() {struct LcdDevice *lcd = NULL;font *f = NULL;bitmap *bm = NULL;// 初始化LCDlcd = init_lcd("/dev/fb0");if (lcd == NULL) {fprintf(stderr, "初始化LCD失败\n");goto exit; // 统一跳转到资源释放处}// 加载字体(确保字库文件存在且路径正确)f = fontLoad("/usr/share/fonts/simkai.ttf");if (f == NULL) {fprintf(stderr, "加载字体文件失败\n");goto exit;}// 设置字体大小(72像素高度)fontSetSize(f, 72);// 创建画布(200×200,4字节ARGB,透明背景)// 注意:getColor参数为ARGB,此处a=0表示完全透明bm = createBitmapWithInit(200, 200, 4, getColor(0, 255, 255, 255));if (bm == NULL) {fprintf(stderr, "创建画布失败\n");goto exit;}// 绘制中文字符(确保源文件编码为GB2312,与字库匹配)char text[] = "晚安";// 参数说明:x=0,y=72(基线位置),红色文字(ARGB:0,255,0,0),maxWidth=0表示不限制宽度fontPrint(f, bm, 0, 72, text, getColor(0, 255, 0, 0), 0);// 显示画布到LCD的(0,0)和(200,200)位置(确保不超出屏幕范围:800×480)show_font_to_lcd(lcd->mp, 0, 0, bm);show_font_to_lcd(lcd->mp, 200, 200, bm);// 停留5秒观察效果sleep(5);exit: // 统一释放所有资源destroyBitmap(bm); // 释放画布fontUnload(f); // 卸载字体release_lcd(lcd); // 释放LCD资源return 0;
}
#程序中使用了 floor、ceil 等数学函数,链接阶段需要关联数学库(libm)
arm-linux-gcc main.c -o main -L./ -lfont -lm
实战:用楷体实时显示系统时间(1 秒刷新)
需求:格式化时间为 “2024 年 5 月 15 日 15:00:04”,用楷体显示在 LCD 中央,每秒刷新(避免重叠)。
#include "font.h"
#include <time.h>
#include <unistd.h>
#include <string.h>// LCD分辨率配置(根据实际设备修改)
#define LCD_WIDTH 800
#define LCD_HEIGHT 480
#define LCD_SIZE (LCD_WIDTH * LCD_HEIGHT * 4) // 每个像素4字节(ARGB)// 时间显示参数配置
#define FONT_SIZE 48 // 字体大小
#define TIME_X 100 // 时间显示起始X坐标
#define TIME_Y 180 // 时间显示起始Y坐标
#define AREA_WIDTH 600 // 显示区域宽度
#define AREA_HEIGHT 120 // 显示区域高度// 初始化LCD设备
struct LcdDevice* lcdInit() {struct LcdDevice* lcd = malloc(sizeof(struct LcdDevice));if (!lcd) {perror("malloc lcd device failed");return NULL;}// 打开LCD设备lcd->fd = open("/dev/fb0", O_RDWR);if (lcd->fd == -1) {perror("open /dev/fb0 failed");free(lcd);return NULL;}// 内存映射LCD设备lcd->mp = mmap(NULL, LCD_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, lcd->fd, 0);if (lcd->mp == MAP_FAILED) {perror("mmap lcd failed");close(lcd->fd);free(lcd);return NULL;}return lcd;
}// 关闭LCD设备
void lcdClose(struct LcdDevice* lcd) {if (lcd) {munmap(lcd->mp, LCD_SIZE);close(lcd->fd);free(lcd);}
}int main() {// 初始化LCDstruct LcdDevice* lcd = lcdInit();if (!lcd) {return -1;}// 加载字体(请确保字体文件路径正确)font* f = fontLoad("/usr/share/fonts/simkai.ttf"); // 中文字体if (!f) {fprintf(stderr, "加载字体失败,请检查字体路径\n");lcdClose(lcd);return -1;}// 设置字体大小fontSetSize(f, FONT_SIZE);// 创建显示画布(黑色背景)bitmap* timeBmp = createBitmapWithInit(AREA_WIDTH, AREA_HEIGHT, 4, // 4字节/像素(ARGB)getColor(255, 0, 0, 0) // 黑色背景);if (!timeBmp) {fprintf(stderr, "创建画布失败\n");fontUnload(f);lcdClose(lcd);return -1;}// 循环显示时间while (1) {// 获取当前时间time_t now;struct tm* timeInfo;time(&now);timeInfo = localtime(&now);// 格式化时间字符串(包含年月日时分秒)char timeStr[64];strftime(timeStr, sizeof(timeStr), "%Y年%m月%d日 %H:%M:%S", timeInfo);// 清空画布(黑色填充)memset(timeBmp->map, 0, AREA_WIDTH * AREA_HEIGHT * 4);// 绘制文字到画布(白色文字)fontPrint(f, timeBmp, 10, // 文字在画布内的X偏移60, // 文字在画布内的Y偏移(基线位置)timeStr, getColor(255, 255, 255, 255), // 白色文字AREA_WIDTH - 20 // 最大显示宽度(留边距));// 将画布显示到LCDshow_font_to_lcd(lcd->mp, TIME_X, TIME_Y, timeBmp);// 1秒刷新一次sleep(1);}// 资源释放(实际运行时循环不会退出,此处为代码完整性)destroyBitmap(timeBmp);fontUnload(f);lcdClose(lcd);return 0;
}
关键说明与问题排查
- 楷体显示乱码
- 原因:字符串编码非 GB2312,或
simkai.ttf未正确加载。 - 解决:确保源文件用 GB2312 编码保存,检查字库路径(用
ls /usr/share/fonts/simkai.ttf验证)。
- 原因:字符串编码非 GB2312,或
- 文字重叠
- 原因:未清空画布或 LCD 区域的旧数据。
- 解决:每次刷新前用
memset(bm->map, 0, ...)清空画布,或调用lcd_clear_area清空 LCD 对应区域。
- 字体截断
- 原因:画布宽度不足,或
fontPrint的maxWidth设置过小。 - 解决:时间字符串约 20 字符,48 号楷体需画布宽≥48×20=960(或调大
maxWidth)。
- 原因:画布宽度不足,或
- 函数参数错误
fontSetSize无返回值,需确保font* f非空;show_font_to_lcd的第一个参数必须是unsigned int*(与mmap返回值类型一致)。
- 通过以上适配,可在嵌入式设备上使用楷体
simkai.ttf正确显示中文及时间,兼顾美观与稳定性。
扩展:点阵字库与 TTF 字库对比
| 对比维度 | 点阵字库(如 16×16) | TrueType 字库(.ttf) |
|---|---|---|
| 存储方式 | 像素点阵数据 | 矢量曲线公式 |
| 缩放效果 | 固定分辨率,放大失真(锯齿) | 任意缩放不失真 |
| 占用内存 | 小(16×16 字库约 6763×32 字节≈210KB) | 较大(单个.ttf 约 1-5MB) |
| 解码速度 | 快(直接映射像素) | 较慢(需计算矢量曲线) |
| 适用场景 | 小分辨率屏(OLED)、资源有限的嵌入式设备 | 大分辨率 LCD、需缩放字体的场景(如菜单) |
| 开发复杂度 | 简单(直接操作点阵) | 较复杂(需依赖字体库,如 FreeType) |
根据项目需求选择字库:若显示简单、分辨率固定,选点阵字库;若需缩放、美观,选 TTF 字库。