ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

U8G2显示中文方案:字模裁剪与GB2312映射实战

2026/9/7 6:36:03 拓冰建站 浏览量
U8G2显示中文方案:字模裁剪与GB2312映射实战 简介针对Arduino平台下u8g2驱动库默认字体不含中文、显示中文困难的问题这份资料包汇总了自定义中文字体所需的完整工具链适合需要在OLED/LCD屏上呈现中文的开发者、电子爱好者与嵌入式初学者。压缩包体积约54.68MB内部以GUITool字体制作工具和u8g2-master库源码为核心GUITool用于字形设计与点阵提取u8g2-master则提供驱动与绘制接口两者配合即可生成自定义中文点阵字体并集成到Arduino工程中。资源目前已有2238人学习下载反映了该方案在u8g2中文字体需求中的实用价值。对于希望摆脱开源字体数量限制、追求个性化中文显示的开发者这份资料能帮助快速搭建起自己的字体制作流程减少在工具下载、库版本配对上的时间消耗值得作为中文字体二次开发的基础参考。 老实说我第一次把u8g2.drawStr()里的字符串换成中文屏幕上一排整整齐齐的方块我盯着那块 0.96 寸 OLED 看了半天脑子里只有一个念头U8G2 不是号称什么字体都能画吗怎么到了中文这里就歇菜了。后来把 U8G2 里跟中文有关的文档、字体文件、示例工程来回翻了几遍才算彻底搞明白问题不在库本身而在字库体制上。U8G2 确实带了几套中文字体但要么体积大到单片机 Flash 直接装不下要么只覆盖了几百个常用字产品文案里稍微出现一个生僻字就露馅。我最后用的方案就是这次要聊的 U8G2ChineseFont 思路自己裁剪字模、按需挂载、用查找表把 GB2312 码映射到字模数据上。这套东西做完之后我在好几个项目里都直接复用包括 128x64 的 OLED 和 320x240 的 LCD效果都比较稳。如果你最近也被 U8G2 显示中文的乱码、空白、方块折磨过这篇文章会把你踩过的坑和还没踩的坑都摊开讲清楚。1. 先搞清楚U8G2 的中文字体到底卡在哪1.1 U8G2 内置字体的真实现状很多教程里说“U8G2 不支持中文”这个说法其实不严谨。U8G2 官方库里的确带了几套中文字体比如u8g2_font_wqy12_t_gb2312、u8g2_font_wqy16_t_gb2312还有u8g2_font_unifont_t_chinese1、chinese2、chinese3这类。它们能显示中文但问题恰恰也出在“有没有”和“能不能用”之间的差距上。字体名覆盖范围16px 字模体积约实际问题wqy12_t_gb2312GB2312 全部 6763 字600KBSTM32F103C8T6 的 64KB Flash 连零头都装不下wqy16_t_gb2312GB2312 全部 6763 字1MB只有带外部 Flash 的开发板才敢想unifont_t_chinese1部分常用汉字约 1000 字200KB覆盖不全产品里出现“睿”“曦”这类字直接空白unifont_t_chinese2/3扩展区部分汉字更大体积问题依旧且实际项目根本用不到那么多字当时我手里是一块 STM32F103C8T6Flash 总共 64KB还要放 U8G2 库本身、驱动代码和业务逻辑。把wqy12_t_gb2312链接进去编译器直接报 region overflow根本没法用。退一步用unifont_t_chinese1体积倒是勉强能塞但一测试就发现“传感正常采集完成”这些话里有好几个字根本不在这套字体里。1.2 双字节编码与单字节字体机制之间的错位更隐蔽的问题出在编码机制上。U8G2 原生的字体系统是为单字节字符设计的drawStr()按字节流解析字符串遇到 ASCII 码直接查字体表里对应的字形。英文字母加数字一共才 62 个可见字符再加标点符号撑死一百来个内存占用几乎可以忽略。中文不一样。GB2312 编码下一个汉字占两个字节而且两个字节都大于 0xA1。U8G2 如果不认识你传入的字节序列它就不知道这两个字节代表的是一个完整的汉字还是两个残缺的 ASCII 字符。就算库内部有 UTF-8 解码逻辑部分带_t_后缀的字体确实支持前提也是字体文件里包含那个 Unicode 码位对应的字形数据——这就又绕回到了体积问题。所以我当时得出的结论是在资源受限的单片机上直接从源头上回避全字库走“按需裁剪 自建字模表”这条路才是真正能在 64KB Flash 上跑中文显示的策略。2. U8G2ChineseFont 的设计思路用小字模表替代全字库2.1 为什么不改 U8G2 库源码最初我确实动过改 U8G2 源码的念头想给它加一套中文字体解析机制让drawStr()直接识别 GB2312 双字节。但翻了源码之后我放弃了原因有三点第一U8G2 的字体渲染链路牵扯到字宽表、字高表、跨平台的裁剪逻辑改动面很大第二每次 U8G2 库升级我的补丁都要重新适配维护成本太高第三也是最关键的直接改库会让项目没法很方便地在 Arduino、STM32、ESP32 多个平台之间迁移。U8G2ChineseFont 用的思路完全相反库原封不动展示中文字形的逻辑放在 U8G2 外面用u8g2.drawXBM()这个原生 API 把点阵数据直接画到缓冲区里。U8G2 只管像素不管这个像素点阵是谁提供的、代表什么字符。这样既绕开了字体表的限制又没侵入库的源码。2.2 字模表的结构整套方案的核心是两张表一张是字模数据表一张是编码映射表。字模数据表就是一个const uint8_t数组里面按固定顺序存放每个汉字的点阵字节。编码映射表则记录“这个 GB2312 编码对应数组里的哪一段”通常是结构体数组typedef struct { uint16_t gb_code; // GB2312 双字节编码例如 0xD6D0 代表“中” uint8_t width; // 字模宽度单位像素 uint8_t height; // 字模高度单位像素 const uint8_t *data; // 指向字模点阵数据 } ChineseCharInfo;单看结构可能觉得简单但这个设计的优越性在于它允许你把不同字体大小、不同风格的汉字混放在一起。比如标题用 24x24 的黑体正文用 16x16 的宋体同一个字符串里可以自由切换。这在产品 UI 上特别实用。2.3 绘制流程drawGlyph 与 drawXBM 的本质区别U8G2 自带的drawGlyph()是把字符编码交给字体系统由字体系统内部查表、解码、获取字形然后渲染到缓冲区。而drawXBM()是你直接把点阵位图交给 U8G2它只负责把位图“贴”到指定坐标。两个 API 的底层都是往缓冲区写像素区别在于字形的来源。U8G2ChineseFont 的字形来源是我们自己的字模表而不是 U8G2 字体系统。这意味着字体系统体积可以做到非常小只保留 ASCII 部分给数字、字母和符号用就够了。中文部分完全由扩展库接管两套逻辑互不干扰。3. 把方案接到工程里代码级拆解与实测3.1 裁剪后的字模数据结构我实际项目里用的字模表是按需生成的。比如一个恒温控制器界面需要显示的汉字只有“温度、湿度、设置、运行、停止、报警、正常、当前、上限、下限”这十个词去掉重复的字最终只有 15 个不同的汉字。每个 16x16 字模占 32 字节15 个字总共 480 字节对 Flash 毫无压力。以下是我实际用的查找函数const ChineseCharInfo* findChineseChar(uint16_t gb_code) { for (size_t i 0; i chinese_char_count; i) { if (chinese_font_table[i].gb_code gb_code) { return chinese_font_table[i]; } } return NULL; }如果项目需要的汉字数量在 50 个以内线性查找完全够用。查找函数每调用一次最多几十次比较微秒级耗时在 72MHz 的 Cortex-M3 上几乎可以忽略。但如果你的字模表膨胀到几百上千个字建议把chinese_font_table按 GB2312 编码排好序然后用二分查找性能会明显更好。3.2 混排绘制函数中英文和数字一起画实际产品界面上很少有纯中文的情况绝大多数是“温度 25℃”“湿度 45%”这种中英文和数字混排。所以我写了一个统一的绘制函数同时处理 ASCII 字符和中文void drawMixedString(U8G2 u8g2, int16_t x, int16_t y, const char* str) { while (*str) { uint8_t c (uint8_t)*str; if (c 0x80) { // ASCII 字符直接交给 U8G2 原生的 drawGlyph u8g2.drawGlyph(x, y, c); x getGfxWidth(u8g2, c); // 按具体字体取字符宽度 str; } else { // 双字节 GB2312 汉字 uint16_t gb_code ((uint16_t)c 8) | (uint8_t)str[1]; const ChineseCharInfo* info findChineseChar(gb_code); if (info) { // 注意 drawXBM 的 y 是位图左上角不是文字基线 u8g2.drawXBM(x, y - info-height, info-width, info-height, info-data); x info-width; } else { // 找不到字模时打印占位符方便调试时发现缺字 u8g2.drawGlyph(x, y, ?); x getGfxWidth(u8g2, ?); } str 2; } } }这里有个特别容易踩的坑drawXBM()的y坐标是位图左上角的位置而drawGlyph()的y坐标是文字基线。如果直接用同一个y绘制中文字形整体会比 ASCII 字符高出一截视觉上不在一条水平线上。正确做法是给drawXBM传入y - info-height让字形底边对齐基线。3.3 平台实测数据我在 STM32F103C8T6 SSD1306 0.96 寸 OLEDI2C 接口400kHz上做了实际测量项目数据15 个 16x16 汉字字模体积480 字节ASCII 字体u8g2_font_6x13_tr约 1.4KB一个完整中文字符的绘制耗时约 0.2ms一屏 8 行 x 8 列中文64 字刷新耗时约 85ms整个方案额外占用的 RAM几乎为 0字模全在 Flash 里也就是说在 64KB Flash 的芯片上中文显示方案只占用了不到 2KB 的 Flash 空间这对那些“想做中文界面但总被体积劝退”的项目来说是一个完全不同的起跑线。4. 字模生成与编码映射这一步最容易翻车4.1 该收哪些字进字模表裁剪字模最忌讳的一件事是把常用汉字表照单全收。我之前还真干过这种事把现代汉语常用 3500 字全部生成字模占用 112KB Flash也超了 64KB 的预算而且 90% 的字根本用不上。正确的收字方式是做字表分析第一步把产品界面所有文案整理成文档去重统计这是字模表的主体。第二步把动态单位词补进去“年月日时分秒”“温度湿度”“电压电流”“开机关机”“运行停止”“报警正常”。第三步如果还有余量补上 0-9 数字和“年月日”这类符号的配套单位。第四步宁可做多不做少我会额外加 20 个左右常用字兜底避免后期改需求时又要重新生成字模。4.2 从字体文件批量生成点阵我最初用 PCtoLCD2002 手动取模一个汉字一个汉字地点20 个字就点得眼睛发花。后来发现完全可以用脚本批量搞定。用 Python 做这事最方便思路是用 PIL 加载 TTF 字体把每个汉字渲染成指定像素尺寸的点阵from PIL import Image, ImageDraw, ImageFont def char_to_bitmap(char, font_path, size16): font ImageFont.truetype(font_path, size) img Image.new(1, (size, size), 0) draw ImageDraw.Draw(img) # 偏移参数需要微调不同字体的基线位置不一样 draw.text((2, -2), char, fontfont, fill1) data [] for y in range(size): byte 0 for x in range(size): if img.getpixel((x, y)): byte | (1 (7 - x % 8)) data.append(byte) return bytes(data)注意画布左上角的偏移量也就是代码里(2, -2)这组参数需要根据你用的字体反复调。不同字体的字形实际大小不一样同样 16 像素的宋体和黑体笔画的上下位置可能有 1 到 2 像素的偏差。如果不对齐渲染出来的文字在 OLED 上会有明显的“漂浮感”不是偏上就是偏下。这个只能实测微调没有捷径。4.3 GB2312 索引公式到底怎么算如果你的字模表是按“GB2312 区位码连续存放”的方式组织的那就可以用公式直接计算偏移。GB2312 汉字区从 0xB0A1 开始也就是“啊”字的编码。公式是uint16_t index ((gb_code 8) - 0xB0) * 94 ((gb_code 0xFF) - 0xA1);0xB0 是汉字区的起始区号0xA1 是起始位号94 是每个区的字符数。这个公式的成立前提是你的字模数组必须严格按照 GB2312 区位顺序连续存放中间不能跳字否则按公式算出来的偏移会指向错误的数据。我当时用的是结构体查找表方案因为项目只需要 15 个字没必要按全字库的方式铺开。如果哪天要做全字库产品我会用公式索引方案代码量更少、查找速度更快。但请注意两者不能混用要么全查表要么全公式混着用会把你自己绕晕。5. 用 u8g2 模拟器在电脑上把方案先跑通5.1 为什么建议先在模拟器上验证嵌入式开发最常见的烦恼是修改代码、烧录固件、上电看效果这个循环一次至少一两分钟。如果界面布局复杂一天下来光烧录就占了大半时间。U8G2 官方提供了模拟器可以在电脑上把 OLED 屏幕的内容直接渲染到 SDL 窗口里真正做到了“改了代码立刻看到效果不用碰硬件”。我实际用下来的感受是模拟器非常适合验证字库切字、字号调参、中文字间距、字符串换行这类问题。你甚至可以把 U8G2ChineseFont 的字模表和绘制函数直接放进模拟器工程里跑模拟器能实时渲染出中文字形在实际屏幕上的效果包括上面说的基线对齐问题都能直观看到。U8G2 模拟器在 Linux 下的编译思路是安装 SDL2 开发库进入 U8G2 仓库的sys/sdl目录运行make会生成一个模拟器可执行文件。它本质上就是把 U8G2 的U8G2_SSD1306_128X64_NONAME_1_HW_I2C改为U8G2_SSD1306_128X64_NONAME_1_SW_I2C然后通过 SDL 窗口模拟 I2C 总线和屏幕。5.2 模拟器里跑通中文的代码骨架模拟器工程里加一段这样的代码就能直接看到中文效果#include U8g2lib.h #include SDL.h // 模拟器用的 U8G2 驱动不依赖真实 I2C U8G2_SSD1306_128X64_NONAME_1_SW_I2C u8g2(U8G2_R0, /* clock*/ 0, /* data*/ 0, /* reset*/ U8X8_PIN_NONE); int main(void) { u8g2.begin(); while (1) { u8g2.firstPage(); do { u8g2.setFont(u8g2_font_6x13_tr); drawMixedString(u8g2, 4, 24, 温度正常 25℃); drawMixedString(u8g2, 4, 44, 湿度 45%); } while (u8g2.nextPage()); SDL_Delay(16); } }这里有个重要的模拟器特性它在 SDL 窗口里绘制的像素和真实 SSD1306 屏幕上的像素是一一对应的。你在模拟器里看到的乱码、错位、重叠烧到真实屏幕上大概率一模一样。所以布局调好后可以直接转移到固件工程不需要重新调坐标。5.3 模拟器测不出来的事情模拟器虽好但也有明显的边界。它测不出真实 I2C 总线的时序问题测不出低主频下的渲染耗时测不出 Flash 空间占用。我的习惯是模拟器只管“对不对”真实硬件只管“快不快”。先把布局逻辑在模拟器里调到满意再编译烧录到开发板测量耗时最后根据耗时决定要不要换 SPI 屏幕、降低刷新频率、或者把双缓冲改成单缓冲。6. 实测里的坑和让它跑得更快的技巧6.1 乱码的三种表现与根因猪肉吃多了之后各种奇怪的显示问题就冒出来了。我遇到的乱码问题基本可以归为三类全屏方块字模表里没有收录那个汉字查找函数返回 NULL绘制逻辑走到了占位符分支。解决方法是补字模或者确认字符串编码不是 UTF-8 导致解析错位。单个字偏上偏下字模生成时字体基线与画布边界没对齐或者drawXBM的 y 坐标没有减字高。这个在模拟器里调试最方便肉眼看到哪行不对直接改偏移量。字和字之间有空隙或重叠ASCII 字符宽度和中文宽度混用导致的。我见过有人给数字也走中文绘制路径结果每个数字宽度按 16px 算间距大得离谱。正确做法是数字和标点走drawGlyph的等宽字体路径汉字走drawXBM。6.2 速度优化一屏中文怎么从 85ms 降到 40ms如果你觉得 85ms 刷新一屏中文不够流畅优化手段有几个方向。最容易做的是把 I2C 速率从 400kHz 提到 800kHz 甚至 1MHz大部分 SSD1306 模组在 1MHz 下都能稳定工作刷新时间能缩短一半。其次是把字模按字复制到 RAM 的临时缓冲里再一次性绘制减少多次drawXBM调用的开销但这需要 RAM 空间在小芯片上可能得不偿失。更彻底的做法是换 SPI 接口的 OLED。SPI 可以直接挂到硬件 SPI 外设上DMA 搬运帧缓冲刷新一屏 128x64 的数据在 8MHz SPI 下只需要 1ms 左右CPU 几乎不参与。如果你的瓶颈真的是刷新速度硬件层面的选择往往比代码优化更有效。6.3 动态数字与中文字符串拼接产品界面经常要显示“温度25.3℃”这种动态内容。我最初的写法是分多次调用绘制函数每次 x 左边都手工加偏移结果数字位数一变整行文字就错位。后来我统一用snprintf先拼字符串再把拼接结果交给drawMixedStringchar line[32]; snprintf(line, sizeof(line), 温度:%.1f℃, temperature); drawMixedString(u8g2, 4, 24, line);这样无论是 25.3℃ 还是 9.8℃整行文字的起点始终固定不会出现数字一变就右移或左移的情况。这个技巧对做仪表类界面的同学尤其有用。另外%和℃这种特殊符号也要记得收进字模表或者单独用 ASCII 走宽度较小的路径否则显示效果很容易出现一个突兀的大间距。把这套流程跑通之后U8G2 显示中文在我这边就从“不可行”变成了“按需定制”。现在每次新项目要加中文界面我第一个念头不是去下载大字号字体而是打开 Excel 把界面上所有文案列出来统计去重后生成一份字模表半小时之内就能把中文字库安排得明明白白。这比在通用方案里来回折腾要高效得多。本文还有配套的精品资源点击获取