1. 核心思路先搞清楚U8g2的中文困境到底是什么先说结论U8g2库本身是不带中文字库的它默认支持的字体基本以拉丁字符和少量符号为主。你直接调用u8g2.setFont(u8g2_font_unifont_t_chinese2)这类接口看似能用中文实际上那是U8g2内置的Unifont字体里覆盖到的部分汉字数量有限而且字形是16x16的等宽点阵显示效果说实话一般般尤其在小屏OLED上字一多就显得很挤。那问题来了如果我的项目需要显示特定的几十个汉字比如温湿度界面的“温度”、“湿度”、“正常”、“异常”或者菜单里的“设置”、“返回”、“确认”总不能为了显示几个字就把几千个常用汉字全塞进单片机里吧这就是我说的“中文字库困境”——资源不够、字形不满意、裁剪麻烦。这个方案的核心思路其实只有一句话按需提取做成自定义点阵字库让U8g2以最小代价显示中文。具体来说有两步第一步把你要用的汉字通过工具转成U8g2能识别的点阵数据格式做成一个极简的“字库头文件”第二步用U8g2的自定义字体回调机制在运行时按字符编码查表把点阵数据渲染到屏幕上。这样做的好处非常明显。首先是内存开销极小一个汉字16x16点阵全字模是32字节如果你只需要50个汉字总共才1600字节Flash完全无压力RAM更是可以做到几乎不占。其次是显示效果好你可以自由选择字体大小、字形风格甚至可以做32x32的大字完全不受U8g2内置字库的限制。最后是适配性强不管是Arduino Uno这种AVR小内存板子还是ESP32、STM32这类资源宽裕的平台这套方案都跑得动。如果你之前被U8g2的中文显示搞到头大或者项目里正好需要显示固定内容的中文菜单、状态提示这篇文章就是给你准备的。我会把工具链、数据格式、代码实现、踩坑记录全部拆开讲保证你照着做能一次点亮。2. 方案选型对比为什么是U8g2为什么不用全字库U8g2是目前Arduino生态里最主流的单色屏驱动库之一支持SSD1306、SH1106、ST7920、SSD1327等一大堆控制器SPI和I2C都能驱动覆盖面非常广。但它对中文的支持一直是社区里的老话题因为字体系统以Latin、Greek、Cyrillic等西文为主中文字形要么没有要么不完整。关于中文显示市面上大概有三条路方案占用资源显示灵活性实现难度适用场景U8g2内置Unifont字体字体文件几百KBFlash紧张16x16固定字形粗糙最低偶尔显示几个字不较真外挂SD卡/字库芯片几乎不占Flash高可换多字号中高需要大量中文、动态文本自定义裁剪字库极小几十个字只有1~2KB高自定义点阵大小中固定菜单、状态显示、小型仪表盘我个人更推荐第三种原因很实在大多数嵌入式项目的界面文本都是有限的、可枚举的。一个温湿度计需要显示的字数不超过50个一个小型气象站可能只需要30个。这种情况下你加载一个几百KB的全字库纯粹是在浪费Flash而且U8g2内置字体的字形质量也不尽如人意。还有一种做法是运行时从SD卡或者Flash芯片里读取字模这样确实能做到全字库但增加了硬件成本和代码复杂度还要处理FatFS或者SPI Flash驱动对小型项目来说属于“杀鸡用牛刀”。当然如果你的项目确实需要动态显示任意中文比如用户输入、网络获取的文字内容那就老老实实走全字库方案别折腾自定义裁剪了。但如果你仔细数一数项目里真实出现过的汉字大概率会发现自定义裁剪字库是性价比最高的选择。3. 核心原理U8g2自定义字体的工作机制要让U8g2支持自定义中文字库得先理解它的字体渲染机制。U8g2渲染字符时并不是直接拿字符编码去屏幕上画字形而是通过一个“字体回调函数”来获取字形数据。这个回调函数是U8g2内部的一个关键接口它接收一个编码值比如UTF-8编码的汉字然后返回该字符对应的点阵数据指针。U8g2把每个字符的显示信息抽象成了几个关键参数字符宽度这个字形横向占多少像素字符高度这个字形纵向占多少像素字形数据点阵的字节数组每个bit代表一个像素点基线偏移字形相对于当前绘制位置的垂直偏移量。对于自定义字体我们只需要实现一个回调函数告诉U8g2“这个字符对应哪个字模”剩下的定位、拼接、清屏等操作U8g2会替你完成。具体来说U8g2的自定义字体实现需要定义一个u8g2_uint_t类型的函数指针函数签名大致是uint8_t *u8g2_custom_font_cb(u8g2_uint_t encoding) { // 根据encoding查表返回对应字模数据 }然后把这个函数指针注册进字体表结构体U8g2在绘制时就会调用它。这里面有几个容易踩坑的细节。第一是编码格式U8g2内部默认使用UTF-8编码而汉字在UTF-8下是三字节的如果你直接用String或char[]存储中文字符串编译器会按UTF-8处理这时回调函数收到的encoding参数并不是汉字的Unicode码点而是UTF-8字节序列拼出来的一个“错误编码”。解决方法是自己写一个简单的UTF-8解码器把三字节的UTF-8序列还原成Unicode码点再拿这个码点去查自定义字库表。第二是字库表的组织方式最典型的做法是维护一个“编码-字模”的字典结构按Unicode码点排序查找时用二分法这样即使你的字库有几百个汉字搜索速度也很快。如果只有几十个字哪怕线性遍历也无所谓。第三是字模数据的格式U8g2支持多种位图格式最常见的是U8G2_FONT_BITMAP_TYPE_1BPP即每像素1比特字节按行排列高位在前。生成工具输出的数据如果不符合这个格式显示就会花屏、错位。理解了这三个关键点你就能明白自定义中文字库的核心逻辑不是让U8g2去理解中文而是你替它把中文翻译成它能理解的点阵数据。4. 实操过程把汉字变成可用的字库头文件4.1 准备工具链要把汉字转成点阵数据我推荐用两个方案一个是在线工具一个是在线本地脚本混合。在线工具方面我实测好用的是一个叫“PCtoLCD2002”的老牌软体界面虽然复古但功能扎实支持输出C语言数组格式可以直接复制进Arduino代码。如果你不想装Windows软件也可以用网页版的取模工具比如“点阵字模生成器”这类的在线服务操作逻辑差不多。取模的关键参数一定要设置对否则后面显示会乱。以下是我踩过坑之后总结的参数清单取模方式逐行式水平扫描每行从左到右从上到下每字节数据方向高位在前MSB first像素位排列1表示亮0表示灭输出格式C语言数组const unsigned char[]字模大小16x16或你需要的任意尺寸。这里我多说一句为什么必须是“高位在前”。U8g2的1BPP位图格式约定每个字节的最高位对应最左边的像素如果你按低位在前取模整个字形会左右镜像看起来就像镜子里的字排查起来非常令人崩溃。4.2 把取模数据整理成字库表假设我们需要显示“温”、“湿”、“度”三个字取模工具会生成三个数组const unsigned char utf8_16_16_wen[] {0x10, 0x80, ...}; const unsigned char utf8_16_16_shi[] {0x00, 0x00, ...}; const unsigned char utf8_16_16_du[] {0x00, 0x00, ...};接下来需要在Arduino代码里建立一个“字典表”把每个字符的Unicode码点注意是Unicode码点不是UTF-8字节和对应的字模数组关联起来。typedef struct { uint16_t unicode; // 汉字的Unicode码点 const unsigned char *bitmap; // 对应点阵数据 } ChineseGlyph; const ChineseGlyph chinese_glyphs[] { {0x6E29, utf8_16_16_wen}, // 温 {0x6E7F, utf8_16_16_shi}, // 湿 {0x5EA6, utf8_16_16_du}, // 度 }; const int glyph_count sizeof(chinese_glyphs) / sizeof(chinese_glyphs[0]);这里有个很关键的“坑”要提醒你Arduino IDE默认的源文件编码是UTF-8你在代码里直接写0x6E29这种数字没问题但如果直接用字符串字面量温去匹配编译器在存储时会按UTF-8编码存储也就是三字节0xE6 0xB8 0xA9如果你拿这个去和0x6E29比较永远匹配不上。所以要么统一用Unicode码点要么写一个UTF-8解码函数先转换再比较。4.3 实现UTF-8解码和查找函数查找函数的核心逻辑是把传入的UTF-8编码解码成Unicode码点然后在字库表中查找。uint16_t utf8_decode(const uint8_t *s, uint8_t *len) { uint8_t c s[0]; if (c 0x80) { *len 1; return c; } else if ((c 5) 0x06) { *len 2; return ((c 0x1F) 6) | (s[1] 0x3F); } else if ((c 4) 0x0E) { *len 3; return ((c 0x0F) 12) | ((s[1] 0x3F) 6) | (s[2] 0x3F); } return 0; }然后在自定义字体回调函数里调用这个解码函数const unsigned char *find_glyph(uint16_t unicode) { for (int i 0; i glyph_count; i) { if (chinese_glyphs[i].unicode unicode) { return chinese_glyphs[i].bitmap; } } return NULL; // 未找到返回空指针 }这个方案虽然用的是线性查找但字库表一般只有几十个条目单次查找耗时在微秒级完全够用。如果你的字库规模到了几百上千再把线性查找换成二分法性能才会明显提升。4.4 注册自定义字体到U8g2U8g2的自定义字体注册方式比较隐蔽很多初学者卡在这一步。你需要定义一个u8g2_font_t结构体然后手动填充它的回调函数指针uint8_t *custom_font_cb(u8g2_uint_t encoding, uint8_t *bitmap, uint8_t *width, uint8_t *height, int16_t *x_offset, int16_t *y_offset) { uint8_t len 0; uint16_t unicode utf8_decode((const uint8_t *)encoding, len); const unsigned char *glyph find_glyph(unicode); if (glyph NULL) return NULL; // 注意这里的宽度高度、偏移量都可以自定义 *width 16; *height 16; *x_offset 0; *y_offset 0; *bitmap (uint8_t *)glyph; // 返回字模指针 return (uint8_t *)glyph; } u8g2_font_t custom_font { 16, // glyph height 16, // glyph width 0, // reference char 0, // start encoding 255, // end encoding custom_font_cb }; u8g2.setFont(custom_font);这段代码里不少朋友会疑问encoding参数类型到底该是什么U8g2库在不同版本里对回调函数的定义略有不同有的版本encoding是uint16_t有的是直接传入字符编码值。我的建议是直接查看你安装的U8g2库源码找到u8g2_font_t的定义照着它的函数指针签名来写避免版本差异导致的编译错误。4.5 显示字符串时的注意事项一旦自定义字体注册成功u8g2.drawStr()就能正常绘制中文了。但这里有一个隐蔽的“坑”drawStr本身不负责解码它只是把字符串的字节逐个传给回调函数。对于UTF-8编码的中文每次传入的只是三字节中的第一个字节如果你不处理“未完待续”的字节就会出现乱码。我常用的处理方式有两个思路。第一种是在自定义字体回调函数里自己维护一个“状态机”记录是否正在接收多字节字符第二种更简单直接修改drawStr逻辑改为遍历整个字符串先做UTF-8解码再逐字绘制。第二种思路更直观代码也不复杂void drawChineseString(u8g2_t *u8g2, int x, int y, const char *s) { while (*s) { uint8_t len 0; uint16_t unicode utf8_decode((const uint8_t *)s, len); const unsigned char *glyph find_glyph(unicode); if (glyph ! NULL) { u8g2_DrawXBM(u8g2, x, y, 16, 16, glyph); } x 16; // 每个字占16像素宽 s len; } }这个自定义绘制函数的灵活度更高你可以自由控制字间距、对齐方式、甚至做到同一行里混排不同大小的中英文。5. 常见问题与排查技巧实录5.1 显示花屏、错位花屏大概率是字模取模方式不对。检查两点一是取模方式是否为“逐行式”二是字节内像素顺序是否为高位在前。还有一个容易忽略的点就是字模的宽度是否是8的整数倍。16x16没问题但如果你做了24x24的字模横向宽度是24像素不是字节对齐的取模工具会默认补一个字节的空白位如果你的U8g2绘制代码里没有处理这种补齐字形就会错位。5.2 中文全显示成方框或乱码这个情况通常是回调函数的查找逻辑没有生效。可以先做一个最简单的测试在find_glyph函数里加一个串口打印看看实际传入的unicode是什么值再对比一下字库表里定义的码点。如果发现传入值是0xE6这类超过两字节的值说明UTF-8解码没做好检查utf8_decode的字节长度判断条件是不是写错了。5.3 编译报错函数指针类型不匹配U8g2库的版本非常多不同版本的自定义字体回调函数签名有不少差异。最稳妥的办法是打开库源码搜索u8g2_font_t的定义然后把函数签名逐个比对。不要靠记忆写回调每个副版本之间的改动都可能不一样。5.4 内存不够如果你用的是Arduino UnoATmega328P仅有2KB RAM特殊情况下尝试把大数组用PROGMEM存放到Flash。Arduino IDE里可以直接用const关键字但要注意在AVR平台上直接访问PROGMEM数组需要pgm_read_byte之类的宏U8g2内部对字模数据的读取已经做了处理但你自己写的find_glyph里如果直接索引数组AVR上会读不到正确数据。这是个非常隐蔽的内存问题排查时要格外小心。5.5 中英混排中文字和英文字宽不一致混排时会出现间距忽大忽小。解决思路是用“半角自适应”英文字符用U8g2内置字号如6x12或8x13中文字符用自定义16x16字模绘制前先判断当前字节是否为ASCII小于0x80是则用内置字体否则用自定义中文字库。代码实现时注意设置字体后要恢复否则下一个字符会串字体。最后分享一个我自己常用的调试技巧先把所有字模数据串口打印出来用串口监视器肉眼检查点阵是否正确。虽然土但对付取模方向错了这类问题比任何调试器都来得快。如果你在PCtoLCD2002里看到的是“镜像字”那基本就是高低位反了不用怀疑其它原因。这套自定义中文字库的方案我在多个实际项目里用过包括一个小型家庭环境监测站一个桌面迷你时钟还有一个简单的OLED菜单系统运行都很稳定。只要把字库维护好后续加字只是添一个数组、加一行映射的事前期的麻烦换来的是长期的高效。