u8g2与Adafruit_GFX实战:为嵌入式显示定制精简中文字库
1. 为什么你的嵌入式屏幕显示不了中文很多朋友玩ESP32、Arduino或者树莓派Pico接上OLED或者LCD屏幕用上Adafruit_GFX或者u8g2库显示英文和图形都挺溜的。但一到要显示中文比如做个带中文菜单的温湿度计或者显示个中文的设备状态立马就卡壳了。屏幕上要么是空白要么是一堆乱码或者干脆编译都通不过提示存储空间不足。这背后的原因其实很简单但也很“要命”字库太大了。一个完整的、能显示几千个常用汉字的点阵字库体积轻轻松松就能达到几百KB甚至几MB。而像ESP32-C3、Arduino Uno这类常见的嵌入式开发板其Flash存储空间可能总共也就4MB、16MB程序代码、Wi-Fi固件、文件系统一瓜分留给字库的空间就非常拮据了。直接把一个完整字库塞进去不仅浪费宝贵的存储空间还会拖慢程序的启动和运行速度。所以在嵌入式世界里搞中文显示核心思路不是“全都要”而是“按需索取”。你的产品界面上总共就显示“温度”、“湿度”、“设置”、“确定”、“取消”这几十个汉字为什么要把一本《新华字典》都背在身上呢这就是我们今天要解决的痛点如何为你的嵌入式项目量身定制一个只包含必需字符的、体积超小的精简中文字库。我会手把手带你走通整个流程从找字体、用工具生成再到集成到Adafruit_GFX库中显示出来。整个过程就像给项目做一件合身的“衣服”而不是套一件宽大的“袍子”。我们将使用一个非常强大的工具——u8g2字体生成工具。它本身是u8g2图形库生态的一部分但生成的字体可以被Adafruit_GFX库完美使用。这个工具能让我们从一个漂亮的TrueType字体.ttf文件出发精确地“裁剪”出我们需要的汉字和符号生成一个高度优化的C语言源文件最终集成到我们的Arduino或PlatformIO项目里。2. 准备工作获取你的“武器库”工欲善其事必先利其器。在开始裁剪字库之前我们需要准备好两样关键的东西字体生成工具和心仪的字体文件。2.1 获取u8g2字体生成工具这个工具是开源项目u8g2的一部分。最稳妥的获取方式是访问其GitHub仓库。你可以直接在GitHub上搜索“olikraus/u8g2”进入仓库后找到并下载最新的发布版本Release。在发布包的附件中通常会有一个名为u8g2/tools/font/build相关的压缩包或者一个独立的bdfconv工具这是核心的转换工具。不过对于新手来说网上也有一些热心开发者打包好的、带图形界面的工具版本使用起来更直观。这里我分享一个我常用的方法直接搜索“u8g2 font tool”或“u8g2字体转换工具”可以找到一些整合好的可执行文件或Python脚本包。下载后你会得到一个包含bdfconv可执行文件或main.pyPython脚本以及一些辅助文件的文件夹。一个至关重要的提醒解压这个工具的路径绝对不要包含任何中文字符。最好把它放在像D:\u8g2_tools或C:\Users\YourName\Documents\u8g2这样的纯英文路径下。这是避免后续转换过程中出现各种奇怪错误的第一步。2.2 挑选一款好看的免费字体字库的灵魂在于字体。一款好看的字体能让你的嵌入式设备界面瞬间提升档次。我们需要的字体文件格式是.ttf (TrueType Font)或.otf (OpenType Font)。去哪里找免费的商用字体呢国内有很多优秀的字体平台比如“字体天下”、“字魂网”、“站酷字库”等它们都提供了大量免费可商用的字体资源。这里我以“优设标题圆”这款字体为例因为它圆润可爱在很多智能硬件的小屏幕上显示效果非常清晰友好。在字体网站找到心仪的字体后下载它的 .ttf 格式文件。同样下载后建议先将字体文件复制到我们之前解压的工具文件夹内并重命名为一个简单的英文名比如youshe.ttf。这样做同样是为了避免路径和文件名中的中文字符引发问题。3. 核心操作用工具“裁剪”你的专属字库准备工作做完接下来就是最核心的步骤——使用工具生成字库。这里我以使用bdfconv命令行工具和带界面的main.py脚本两种方式来讲解让你理解背后的原理。3.1 理解转换原理从TTF到C数组工具的本质工作是一个“翻译裁剪”的过程翻译读取你提供的.ttf矢量字体文件根据你指定的字体大小例如12像素高将每个字符的轮廓信息“渲染”或“栅格化”成对应的点阵位图。这个位图信息会被转换成一种叫BDF的中间格式。裁剪工具不会转换整个字体文件里的所有字符那可能有数万个。它只转换你明确告诉它需要的字符。这些字符的列表就是你提供的“字符集文件”或直接在命令行/界面中输入的文字。编码最后工具将这些点阵数据按照u8g2库规定的格式编码成一个巨大的C语言数组并生成一个.c和.h文件。这个数组就是你的字库。3.2 实战转换步骤详解假设你已经把重命名后的字体文件如youshe.ttf放到了工具文件夹里。我们分别看两种操作方式。方式一使用图形界面main.py如果你下载的工具包里有main.py那么操作会非常直观。打开命令行终端进入工具所在目录。运行python main.py确保你安装了Python环境。通常界面会引导你进行以下配置输入要生成的汉字这是最关键的一步仔细想想你的项目所有界面需要显示哪些中文。比如“温度湿度设置确定取消返回开关”。务必一次性输入全中间不要加空格或换行。你也可以从一个文本文件里复制粘贴进来。字库命名给你的字库起个英文名比如myFont。这将是后续代码中使用的字体对象名。选择字体路径浏览并选中文件夹里的youshe.ttf。选择字体大小根据你的屏幕分辨率和显示需求选择。对于128x64的OLED12或16像素是比较常用的尺寸。大小决定了字体的清晰度和占用空间。点击生成或确认。工具会在core或output文件夹下生成myFont.c和myFont.h文件。方式二使用命令行bdfconv命令行方式更灵活适合批量处理或集成到脚本中。一个典型的命令如下./bdfconv -v -f 1 -m 32-126, 28246-28246, 28248-28248 -n myFont -o myFont.c ./youshe.ttf我来解释一下关键参数-f 1指定字体格式1通常代表u8g2格式。-m这是“字符映射”参数定义了要转换的字符范围。32-126表示ASCII码从32空格到126~的所有字符即基本的英文、数字和符号。后面跟着的28246-28246这样的数字是某个特定汉字的Unicode码点。你需要把你需要的汉字转换成对应的Unicode码点范围。获取汉字Unicode的一个简单方法是在Python交互环境里输入ord(温)它会返回28246。-n myFont指定生成字体的名称。-o myFont.c指定输出的C文件名。最后是输入的ttf文件路径。无论用哪种方式请牢记这个黄金法则生成的字体文件只包含你配置时输入的文本和基本的ASCII字符集。如果你的产品后期需要显示一个新的汉字你必须重新运行工具把旧的汉字加上这个新的汉字一起作为新的字符集输入重新生成整个字库文件。不能简单地追加。4. 集成到Adafruit_GFX项目并显示字库文件.c和.h生成了现在我们要把它“安装”到我们的Arduino项目中并用Adafruit_GFX库把它画到屏幕上。4.1 将字体文件加入工程在你的Arduino项目文件夹或者PlatformIO项目的src目录下创建一个新的文件夹例如叫做Fonts。将生成的myFont.c和myFont.h文件复制进去。在Arduino IDE中如果你的项目结构支持可以直接将Fonts文件夹拖放到工程窗口中。在PlatformIO中文件放入src后会自动被识别。关键的一步在你的主程序文件通常是.ino或.cpp的开头包含生成的字库头文件#include Fonts/myFont.h4.2 在代码中调用与显示Adafruit_GFX库本身不直接处理u8g2格式的字体但我们可以通过一个“桥接”的方式来使用。u8g2字体生成工具产生的.h文件里实际上已经定义了一个符合Adafruit_GFX库要求的字体结构体变量。在你的setup()或loop()函数中当你需要显示中文时需要做以下几步设置字体使用setFont()函数指向我们导入的字体。display.setFont(myFont); // 假设你的显示对象叫 display设置光标位置指定文本开始显示的坐标左上角为原点。display.setCursor(0, 16); // 在坐标(0, 16)处开始打印打印文本使用print()或println()函数输出字符串。这里输入的字符串必须完全由你生成字库时包含的字符组成。display.print(温度:25℃); // 完美显示 // display.print(湿度计); // 如果“计”字不在字库里这里可能显示为空白或乱码恢复默认字体可选显示完中文后如果你后续要显示默认的英文字体可以调用setFont()并传入NULL或默认字体。display.setFont(NULL); // 恢复Adafruit_GFX内置的默认字体4.3 一个完整的示例代码片段假设我们使用SSD1306 OLED屏幕128x64并已通过Adafruit_SSD1306库初始化了名为display的对象。#include Adafruit_GFX.h #include Adafruit_SSD1306.h #include Fonts/myFont.h // 导入我们自定义的字库 #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, Wire, -1); void setup() { Serial.begin(115200); if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println(F(SSD1306 allocation failed)); for(;;); } display.clearDisplay(); // 使用自定义中文字体 display.setFont(myFont); display.setTextSize(1); // 注意这里TextSize是相对于字体本身的缩放通常设为1 display.setTextColor(SSD1306_WHITE); // 显示标题 display.setCursor(0, 0); display.println(智能温室系统); // 这些字必须都在myFont字库里 // 显示数据 display.setCursor(0, 20); display.print(温度:); display.print(25.5); display.println(℃); // 摄氏度符号确保它在ASCII或自定义字库中 display.setCursor(0, 36); display.print(湿度:); display.print(65); display.println(%); // 显示操作提示使用部分汉字 display.setFont(myFont); // 确保字体已设置 display.setCursor(0, 52); display.print(设置 确定 返回); display.display(); // 将缓存内容刷到屏幕上 } void loop() { // 主循环 }5. 避坑指南与高级技巧走通基本流程后你可能还会遇到一些“坑”。这里分享几个我实战中总结的经验。5.1 常见问题与解决**编译错误“undefined reference tomyFont”**这通常意味着.c文件没有被正确编译。在Arduino IDE中确保.c文件在正确的文件夹并被项目包含。在PlatformIO中检查platformio.ini确保没有排除该文件。有时需要重启一下IDE。屏幕上中文显示为乱码或空白首要检查你代码里print的字符串是否每一个汉字都出现在你生成字库时输入的字符集中一个都不能少。编码问题确保你的Arduino IDE或代码编辑器的文件编码是UTF-8 without BOM。在Windows的记事本里另存为时可以选。带BOM的UTF-8有时会导致第一个字符显示异常。字体设置时机在每次调用print之前是否都正确执行了setFont(myFont)如果你在显示中文和英文间切换忘记设置回来会导致后续英文显示异常。字体太大Flash空间不足这是定制字库要解决的核心问题。如果还太大尝试进一步精简汉字去掉任何可能用不到的字符。减小字体大小像素高度。12px通常比16px节省不少空间。检查是否无意中包含了整个ASCII扩展字符集。在工具配置中确认只包含了32-126这个基本范围。5.2 优化策略让字库更小、更高效分场景使用多个微型字库如果你的设备有不同界面如主界面、设置界面可以考虑为每个界面生成一个独立的、只包含该界面所需汉字的小字库。在切换界面时在代码中动态切换使用的字体变量。虽然管理稍复杂但能最大程度节约空间。利用外部存储如果板子支持SPIFFSESP32或SD卡可以将字库文件放在文件系统中启动时加载到内存如果内存足够大。但这会增加代码复杂度和启动时间。压缩字体一些高级的字体工具支持对点阵数据进行简单的游程编码RLE压缩。u8g2的bdfconv工具本身就有一些压缩选项如-b参数可以在生成时尝试使用但要注意可能会增加一点解压显示时的CPU开销。5.3 与u8g2库本身的对比你可能会问既然用了u8g2的工具为什么不直接用u8g2库Adafruit_GFX和u8g2都是优秀的图形库各有侧重。Adafruit_GFX生态极好与Adafruit的大量硬件驱动屏幕、传感器集成无缝API相对高层、易用社区资源丰富。u8g2在单色、低分辨率OLED驱动上极其高效和轻量内置了海量的字体和驱动支持但API风格更底层一些。我们的方案结合了二者的优点用u8g2强大的字体工具生成字库享受Adafruit_GFX友好的开发体验。这对于已经基于Adafruit生态进行开发又急需中文显示的项目来说是一条非常平滑的升级路径。整个流程走下来从几MB的完整字库到可能只有几KB的定制字库这种存储空间的释放是立竿见影的。更重要的是你获得了对项目资源的精确控制权。下次当你的嵌入式设备需要显示一句“欢迎使用”时你不会再为庞大的字库发愁而是从容地打开工具输入这四个字生成一个专属于它的、小巧精致的字库文件。这种“量体裁衣”的乐趣正是嵌入式开发的魅力所在。