简介:这份资源面向使用 Arduino 与 u8g2 库进行中文显示开发的嵌入式爱好者与工程师,解决 u8g2 默认字库不含中文字形、需要自行生成定制字体的实际问题。压缩包内包含 GUITool 字体取模工具与 u8g2-master 源码库两部分核心内容,前者用于将中文字符转换为 u8g2 可识别的点阵字模数据,后者提供库文件与示例参考,便于在项目中直接调用自定义中文字体。资源包整体约 54.68MB,文件类型以工具程序与源码文件为主,结构清晰,适合按需取用。目前已有 2242 人学习下载,读者可借助这套资料完成从取模、生成字库到在 Arduino 工程中加载显示中文的完整流程,减少自行摸索字库格式与配置参数的时间,对 OLED、LCD 等小屏中文界面开发具有直接参考价值。
1. 从 U8G2ChineseFont.zip 说起:嵌入式中文点阵字库到底怎么落地
如果你玩过 OLED 或 LCD 单色屏,大概率绕不开 U8G2 这个图形库。它轻量、跨平台、驱动覆盖广,但真正让国内开发者头疼的,从来不是画线画圆,而是在只有几十 KB RAM 的单片机上显示中文。U8G2ChineseFont.zip 这类资源包,本质上就是一套已经裁剪好的中文字库文件加配套取模配置,解决的就是“屏幕能亮、英文能显示、中文一写就乱码或编译爆 Flash”的经典问题。它适合做小家电面板、工控仪表、穿戴设备、电子价签的嵌入式工程师,也适合刚接触 U8G2、被字库格式劝退的新手。核心矛盾只有一个:中文字形数据太大,而单片机资源太小,必须靠取模裁剪和按需加载来平衡。
2. 中文字库在 U8G2 里的三种存法:选错了后面全是坑
2.1 全字库、子集字库和动态加载的取舍
U8G2 原生支持的中文字库调用方式,常见做法有三种。第一种是全字库编译进 Flash,比如把 GB2312 一二级字库全部转成 C 数组,优点是调用简单,缺点是 Flash 占用动辄几百 KB 到 1 MB 以上,STM32F103C8T6 这种 64 KB Flash 的芯片直接出局。第二种是子集字库,只提取项目实际用到的几十到几百个汉字,生成一个自定义小字库,这是 U8G2ChineseFont.zip 这类资源最典型的用法。第三种是外部 Flash 动态加载,字库放在 W25Q64 这类 SPI Flash 里,运行时按需读取字形点阵,RAM 和内部 Flash 都省,但需要自己写读取和缓存逻辑。
选型判断很简单:如果你的 MCU Flash 小于 128 KB,优先子集字库;如果产品需要显示用户输入或大量动态文本,考虑外部 Flash 方案;如果只是固定显示几十个菜单词,子集字库加U8G2_USE_FONT宏裁剪就够了。我一般会先统计 UI 上所有可能出现的汉字,去重后通常不超过 300 个,这样生成的字库体积可以压到 20 KB 以内。
2.2 用 bdfconv 把 TTF 转成 U8G2 可用的 C 数组
U8G2 官方工具链里有一个bdfconv工具,配合otf2bdf可以把 TTF/OTF 字体转成 U8G2 能识别的.bdf,再转成 C 数组。下面是我在 Linux 下常用的命令流程,Windows 下可以用 WSL 或 MSYS2 跑同样的命令。
# 第一步:把 TTF 转成 BDF,指定字号 16 像素 otf2bdf -p 16 -o myfont16.bdf SourceHanSansCN-Regular.ttf # 第二步:用 bdfconv 生成 U8G2 格式的 C 文件 # -f 1 表示输出格式为 U8G2 字体 # -m 0 表示不生成映射表,使用 Unicode 直接索引 # -n 指定字体变量名 # -o 输出文件名 # -b 0 表示从 0 开始编码 # -e 1 表示编码范围结束标志 bdfconv -f 1 -m 0 -n u8g2_font_myfont16 -o myfont16.c myfont16.bdf # 第三步:如果只需要部分汉字,用 -u 指定 Unicode 列表文件 # unicode_list.txt 每行一个十六进制码点,例如 4E2D 表示“中” bdfconv -f 1 -m 0 -n u8g2_font_subset16 -o subset16.c -u unicode_list.txt myfont16.bdf这里的关键参数是-m。-m 0表示不做映射表,U8G2 会按 Unicode 码点直接计算偏移,适合码点连续的 GB2312 子集;如果码点跳跃很大,建议用-m 1生成映射表,虽然多占一点空间,但查找速度稳定。-u指定的 Unicode 列表文件,就是你把 UI 上所有汉字去重后转成十六进制码点的结果,可以用 Python 脚本从字符串里自动提取。
2.3 在 U8G2 工程里注册自定义字库并调用
生成 C 文件后,把它加入工程,然后在调用u8g2_DrawStr之前设置字体。下面是一个 STM32 HAL + U8G2 的典型片段。
// 声明外部字库变量,变量名要和 bdfconv -n 参数一致 extern const uint8_t u8g2_font_subset16[]; // 初始化 U8G2 后,设置自定义中文字库 u8g2_Setup_ssd1306_i2c_128x64_noname_f( &u8g2, U8G2_R0, u8x8_byte_hw_i2c, u8x8_gpio_and_delay); u8g2_InitDisplay(&u8g2); u8g2_SetPowerSave(&u8g2, 0); // 关键一步:把字体设置为自定义子集字库 u8g2_SetFont(&u8g2, u8g2_font_subset16); // 绘制中文,注意字符串要用 UTF-8 编码 u8g2_DrawUTF8(&u8g2, 0, 20, "温度"); u8g2_DrawUTF8(&u8g2, 0, 40, "湿度"); u8g2_SendBuffer(&u8g2);逻辑说明:u8g2_SetFont只是切换当前字体指针,真正绘制中文要用u8g2_DrawUTF8而不是u8g2_DrawStr,因为DrawStr按单字节处理,遇到 UTF-8 多字节编码会直接乱码。参数上,u8g2_font_subset16这个变量名必须和bdfconv -n指定的一致,否则链接阶段会报未定义符号。另外注意u8g2_Setup_ssd1306_i2c_128x64_noname_f里的_f后缀表示全缓冲模式,RAM 占用 1 KB,如果 RAM 紧张可以换成_1或_2分页模式,但分页模式下DrawUTF8的坐标计算会更麻烦。
3. 裁剪与压缩:把中文字库塞进 64 KB Flash 的实操参数
3.1 按 Unicode 码点生成最小子集
子集字库的核心是“只取所需”。我一般用下面这个 Python 脚本从 UI 字符串里提取所有不重复汉字,输出 bdfconv 需要的 Unicode 列表文件。
# extract_unicode.py # 从文本文件中提取所有汉字,输出十六进制 Unicode 码点列表 import re def extract_chinese_chars(text): # 匹配 CJK 统一汉字基本区 pattern = re.compile(r'[\u4e00-\u9fff]') return set(pattern.findall(text)) def main(): with open('ui_strings.txt', 'r', encoding='utf-8') as f: content = f.read() chars = extract_chinese_chars(content) # 排序后输出,每行一个码点,格式如 4E2D with open('unicode_list.txt', 'w', encoding='utf-8') as f: for ch in sorted(chars): f.write(f'{ord(ch):04X}\n') print(f'共提取 {len(chars)} 个不重复汉字') if __name__ == '__main__': main()逻辑说明:ui_strings.txt里放你界面上所有可能出现的字符串,一行一个或直接一段文本都行。脚本用正则匹配\u4e00-\u9fff范围内的字符,去重后按码点排序输出。参数上,04X表示至少 4 位十六进制,不足补零,这是 bdfconv 能识别的格式。如果你的项目还包含标点符号和数字,需要额外匹配\u3000-\u303f和\uff00-\uffef区段。
3.2 用 RLE 压缩点阵数据再省一半空间
即使裁剪到 300 个汉字,16x16 点阵每个字 32 字节,总共也要 9.6 KB,加上 U8G2 的字体结构体开销,差不多 12 KB。如果 Flash 还是紧张,可以对点阵数据做游程编码(RLE)。U8G2 本身不直接支持 RLE 字库,但你可以自己写一个解压函数,在绘制前把字形解到 RAM 缓冲区。
// 简单的 RLE 解压:每个字节的高 4 位是重复次数,低 4 位是数据 // 注意:这是自定义格式,需要配合你自己的取模脚本生成 void rle_decode(const uint8_t *src, uint8_t *dst, uint16_t dst_len) { uint16_t di = 0; while (di < dst_len) { uint8_t count = (*src >> 4) + 1; // 重复次数 1-16 uint8_t value = *src & 0x0F; // 实际数据,这里只用了低 4 位 for (uint8_t i = 0; i < count && di < dst_len; i++) { dst[di++] = value; } src++; } }逻辑说明:这个 RLE 方案把每个字节拆成“重复次数 + 数据”,适合点阵里大量连续 0x00 或 0xFF 的情况。参数上,高 4 位最大表示 16 次重复,低 4 位表示数据值,实际使用中如果数据超过 4 位范围,需要改成整字节压缩方案。解压后的数据要按 U8G2 的字形格式重新组装,这部分需要你根据u8g2_font_decode的源码逻辑来适配,属于进阶操作,新手建议先用未压缩子集字库跑通。
3.3 外部 SPI Flash 方案的分区与读取参数
当子集字库也放不下时,外部 Flash 是唯一出路。以 W25Q64 为例,8 MB 空间可以放完整 GB2312 字库还有富余。我的做法是把字库按 4 KB 扇区对齐存放,每个汉字 32 字节,一个扇区存 128 个汉字。读取时根据 Unicode 码点计算扇区地址和偏移。
// 假设字库从外部 Flash 的 0x10000 地址开始存放 // 每个汉字 32 字节,按 Unicode 码点顺序排列 #define FONT_BASE_ADDR 0x10000 #define FONT_CHAR_SIZE 32 void read_font_from_flash(uint16_t unicode, uint8_t *buf) { // 计算偏移:只处理 CJK 基本区,码点减去 0x4E00 uint32_t offset = (unicode - 0x4E00) * FONT_CHAR_SIZE; uint32_t addr = FONT_BASE_ADDR + offset; // W25Q64 读取函数,每次最多读一页 256 字节 w25q64_read(addr, buf, FONT_CHAR_SIZE); }逻辑说明:FONT_BASE_ADDR是字库在外部 Flash 中的起始地址,需要根据你的分区表调整。FONT_CHAR_SIZE是单个汉字点阵的字节数,16x16 是 32 字节,24x24 是 72 字节。w25q64_read是底层 SPI 读取函数,注意 W25Q64 的页读取边界是 256 字节,跨页读取需要分两次。这个方案省内部 Flash,但每次绘制中文都要读 SPI,刷新率会下降,适合静态显示场景。
4. 避坑与排查:中文显示翻车的五个血泪现场
4.1 现象:中文显示为方块或问号
原因:字体没有正确设置,或者调用了u8g2_DrawStr而不是u8g2_DrawUTF8。DrawStr按单字节处理,UTF-8 编码的汉字会被拆成多个无效字节。解决:确认u8g2_SetFont设置的是中文字库,并且绘制函数用u8g2_DrawUTF8。另外检查源文件编码是否为 UTF-8,Keil 默认 GBK 编码会导致字符串常量本身就不对。
4.2 现象:编译报错 “undefined reference to u8g2_font_xxx”
原因:bdfconv 生成的 C 文件没有加入工程,或者变量名和-n参数不一致。解决:检查生成的.c文件是否在编译列表里,检查extern声明和实际变量名是否完全一致,包括大小写。如果用的是 C++ 工程,记得加extern "C"包裹。
4.3 现象:显示一半汉字,后面的乱码
原因:子集字库的 Unicode 列表不完整,或者 bdfconv 的-m映射模式选错。如果用了-m 0但码点不连续,U8G2 会按错误偏移查找。解决:重新生成 Unicode 列表,确保覆盖所有用到的汉字;如果码点跳跃大,改用-m 1生成映射表。另外检查unicode_list.txt里是否有重复或格式错误的行。
4.4 现象:Flash 占用远超预期
原因:bdfconv 默认会生成完整的字体结构体,包括所有字形的偏移表,即使子集很小,偏移表也可能占几百字节。另外如果-m 1映射表过大,也会增加体积。解决:用arm-none-eabi-size查看各段大小,确认字库确实在.rodata段。如果偏移表太大,可以手动裁剪,只保留实际用到的码点范围。我一般会把字库单独放一个.c文件,方便用size命令单独统计。
4.5 现象:外部 Flash 方案读取速度慢,界面卡顿
原因:每次绘制都直接读 SPI Flash,没有缓存机制。解决:在 RAM 里开一个字形缓存,比如缓存最近用到的 16 个汉字点阵,用简单的 LRU 淘汰。缓存大小根据 RAM 余量决定,16 个 16x16 汉字占 512 字节,对大多数 MCU 可以接受。另外把 SPI 时钟提高到 40 MHz 以上,W25Q64 支持 80 MHz 读取,但要注意 PCB 走线质量。
5. 进阶技巧:用 U8G2 的字体裁剪宏和运行时切换做多语言界面
U8G2 本身提供了一组编译期裁剪宏,比如U8G2_USE_FONT可以在u8g2.h里禁用不需要的字体,减少 Flash 占用。但很多人不知道的是,你可以在运行时动态切换字体指针,实现中英文混排或多语言界面。具体做法是定义多个字体变量,在界面切换时调用u8g2_SetFont切换。
// 定义中英文字体指针 extern const uint8_t u8g2_font_subset16[]; extern const uint8_t u8g2_font_helvB12_tf[]; // 根据语言设置切换字体 void set_language_font(u8g2_t *u8g2, uint8_t lang) { if (lang == LANG_CN) { u8g2_SetFont(u8g2, u8g2_font_subset16); } else { u8g2_SetFont(u8g2, u8g2_font_helvB12_tf); } } // 绘制时统一用 DrawUTF8,英文也能正常显示 u8g2_DrawUTF8(&u8g2, 0, 20, "温度: 25C");逻辑说明:u8g2_font_helvB12_tf是 U8G2 自带的英文字体,_tf后缀表示包含完整 ASCII 字符集。DrawUTF8对纯 ASCII 字符串同样有效,所以中英文混排不需要切换绘制函数。参数上,LANG_CN和LANG_EN是你自己定义的宏,根据界面状态或用户设置来切换。这个技巧在需要支持多国语言的产品里很实用,代价是同时链接两套字体,Flash 占用增加,但比全字库方案还是省很多。
另一个进阶点是字库的按需加载与释放。如果你的系统有外部 RAM 或足够大的内部 RAM,可以把整个子集字库在启动时从外部 Flash 读到 RAM 里,之后绘制直接读 RAM,速度提升明显。代价是启动时间增加几百毫秒,以及 RAM 占用。我一般会在产品启动时做一个进度条,把字库加载和界面初始化并行处理,用户感知不到延迟。
最后说一个我踩过的坑:不要用u8g2_DrawGlyph直接绘制中文。这个函数按字形索引查找,而中文字库的字形索引和 Unicode 码点不是一一对应关系,除非你完全清楚 bdfconv 生成的索引表结构,否则很容易画错字。老老实实用u8g2_DrawUTF8,让它内部去处理编码转换和字形查找。这个习惯帮我省了至少两天调试时间。希望帮到你。
本文还有配套的精品资源,点击获取