简介:U8g2 2.27.6是一份用于Arduino IDE的图形显示库压缩包,面向需要驱动OLED、LCD及电子纸屏幕的开发者,能统一解决不同屏幕接口和字体显示问题。包内共159个文件,以C语言源码、INO示例、H/CPP头文件为主,并附带TXT/MD文档、许可证及版本变更说明,压缩后约10MB,结构清晰,便于解压后直接引用。功能上,该版本内置上百种字体,支持I2C、SPI、并行接口和200余款显示屏,提供画点、线、图形、滚动、旋转等API,且针对Arduino有限内存做了优化,在UNO、ESP8266、ESP32上均可流畅运行。资源已有3296人学习下载,既能用于学习屏幕驱动原理,也能作为项目底包快速调用,减少底层驱动调试时间,快速实现界面显示。
1. 一块 128×64 的屏为什么值得专门装一个 u8g2 zip 包
拿到一块 128×64 的 OLED,很多人以为最难的部分是画曲线、做动画,实际上第一个晚上的大部分时间都花在“让屏幕上出现一个像素”上。u8g2 库做的是把 SSD1306、SH1106 这类单色显示控制器接到 Arduino IDE 这条路上最省事的一段,而 U8g2-2.27.6.zip 这个安装包,是这条路最常见的起点:下载、导入、编译,然后屏幕上终于有字。这篇笔记用一次完整的“安装到出图”过程,讲清楚 u8g2 怎么装、怎么接、怎么调,以及那些让新手反复翻车的细节。适合正在给 Arduino、ESP32 接单色屏的开发者,也适合把旧屏翻出来想复用的人。装一次库不难,难的是后续十几个问题的排查思路,这部分会按踩坑顺序写完整。
2. 把 U8g2-2.27.6.zip 装进 Arduino IDE:手动安装、验证与版本锁死
2.1 为什么不用库管理器:zip 安装的两个硬理由
Arduino IDE 自带的库管理器能搜到 u8g2,点一下 Install 就能用,看起来没有必要碰 zip。但实际工程里有两个场景会让库管理器变成黑匣子:第一,库管理器拉到的永远是当前渠道里最新的版本,你没法精确锁死“我用的就是 2.27.6”,同一个项目换一台电脑编译,库版本漂了,行为可能跟着漂;第二,很多开发环境是离线的,实验室、车间、工控现场没有外网,库管理器直接不可用。这两个理由凑在一起,“下载一个 zip 包,插到任何一台机器上都能复现”就变成了最稳的交付方式。
我一般会优先把 zip 包留一份在项目目录里,和源码放一起,并在 README 里写清楚用的版本。这样同事拿到项目,不需要先上网搜库,直接 Add .ZIP Library 就能编。版本锁死的价值在 u8g2 这种持续更新的库上尤其明显:构造宏、字体对象、方法名偶尔会调整,旧代码换新库不一定编译得过,就算编译得过,显示行为也可能变。
2.2 从 zip 到“库已导入”:Add .ZIP Library 与手工解压两条路
最常见做法是用 IDE 的导入功能,路径是 Sketch → Include Library → Add .ZIP Library,然后选中 U8g2-2.27.6.zip。IDE 会把 zip 里的内容解压到当前用户目录下的 libraries 文件夹,并在左下角状态区提示“Library added to your libraries”。注意这里有个隐藏要求:zip 解压后第一层必须直接是 U8g2 目录,目录里是 U8g2.h、library.properties、src 这些内容。如果你下载的 zip 带了一层外层文件夹,比如“U8g2-2.27.6/U8g2/U8g2.h”,IDE 会导入失败,或者导入进去但编译时找不到头文件。
我自己的习惯是两条路都准备:大多数时候用 Add .ZIP Library,如果 IDE 版本太老、或者批量部署到多台机器,就直接手工解压到 libraries 目录。手工解压的路径比较好定位,Windows 一般在“文档(我的文档)/Arduino/libraries”,macOS 在 ~/Documents/Arduino/libraries,Linux 在 ~/Arduino/libraries。解压完把文件夹名字改成 U8g2,确保里面没有嵌套的深层目录,这一步就完成了。
# 在 Arduino 的 libraries 目录下确认 u8g2 已经就位 cd ~/Documents/Arduino/libraries ls -d U8g2* # 期待输出: U8g2 test -f U8g2/U8g2.h && echo "header OK" test -f U8g2/library.properties && echo "metadata OK"这段命令做的事情很简单:先看目录里有没有 U8g2 文件夹,再检查两个关键文件是否存在。U8g2.h 是库的主头文件,编译时 include 的就是它;library.properties 是库管理器识别库用的元数据文件,IDE 判断“这个目录里装的是什么库、什么版本”全靠它。如果test -f输出的是 OK,说明目录结构是对的,可以往下走。
2.3 确认安装成功的三个检查点:目录、属性文件与编译日志
装完库,真正能确认“它已经准备好”的检查点有三个。第一个是目录结构,也就是上面命令已经验证过的:libraries 下有一个 U8g2 目录,U8g2.h 在目录根部。第二个是 library.properties 里的版本信息,用编辑器打开这个文件,能看到 name=U8g2、version=2.27.6 这样的字段。版本号这段我一般会对一下 zip 包名,如果包名是 U8g2-2.27.6.zip,properties 里也应该是 2.27.6,出入太大说明文件来源有问题。
第三个检查点是编译日志。新建一个空 sketch,写两行代码:
#include <U8g2lib.h> void setup() {} void loop() {}编译这个最小程序,如果 IDE 的编译输出里出现类似Sketch uses ... bytes的字样且没有报错,说明头文件已经找到,库能被编译。如果这里就报“U8g2lib.h: No such file or directory”,优先检查是不是把库解压到了错误目录,或者文件夹嵌套层数不对。安装阶段的问题九成出在这两个地方,跟代码本身没关系。
提示:导入 zip 之后最好重启一次 Arduino IDE 再编译。某些版本的 IDE 不会在导入后立刻刷新库索引,不重启可能报找不到库,容易误判成安装失败。
3. 跑通 u8g2 最小 Demo:接线、构造函数与两行绘图 API
3.1 接线先看引脚:Uno、Mega、ESP32 的 SDA/SCL 对照表
u8g2 支持很多接口,最常见的还是 I2C。I2C 只需要两根数据线,接线少,而且 u8g2 对 I2C 的支持最顺。先记住一个原则:SDA 是数据线,SCL 是时钟线,不同主控板上的引脚位置完全不同,不要照着网上某一张图就接。
| 主控板 | SDA | SCL | 供电 |
|---|---|---|---|
| Uno / Nano / Mini | A4 | A5 | 5V |
| Mega 2560 | 20 | 21 | 5V |
| ESP32 DevKit | GPIO21 | GPIO22 | 3.3V |
| ESP8266 NodeMCU | GPIO4(D2) | GPIO5(D1) | 3.3V |
OLED 模块的 VCC 和 GND 也要接对,很多模块上丝印写的是 VCC 和 GND,有的写着 3.3V 和 5V 双电源。屏幕的逻辑电压以模块上的稳压芯片为准,如果是纯 3.3V 模块,接到 5V 上运气好能亮,但长时间跑会发热,我建议直接按模块标注接。还有一个细节:I2C 总线需要上拉电阻,绝大多数成品模块上已经焊好了,不需要自己加;如果是裸屏接出来的,就要在 SDA 和 SCL 上各接一个 4.7kΩ 上拉到 VCC。
3.2 最小 sketch:begin() 之后只有 setFont 和 drawStr
接线完成,先把最简代码跑起来。下面这段就是 u8g2 的“Hello World”,所有功能都可以从这里扩展:
#include <U8g2lib.h> // 128x64 OLED, SSD1306 控制器, I2C 接口, 全缓冲(F)模式 U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); void setup(void) { u8g2.begin(); // 初始化屏幕, 必须先调用 u8g2.setFont(u8g2_font_ncenB08_tr); // 选择内置英文字体 u8g2.setFontPosTop(); // 字形按顶部对齐绘制 } void loop(void) { u8g2.firstPage(); // 开始第一页 do { u8g2.drawStr(0, 0, "Hello u8g2!"); // 在 (0,0) 处画字符串 u8g2.drawFrame(0, 12, 128, 20); // 画一个带边框的矩形 } while (u8g2.nextPage()); // 换下一页, 直到全部刷新完成 delay(200); }逻辑拆开看:构造函数的第一个参数 U8G2_R0 表示不做旋转,第二个参数 U8X8_PIN_NONE 表示不使用 Reset 引脚。setup 里的 begin() 完成屏幕时序初始化和清屏,必须在任何绘制之前调用;setFont 决定 drawStr 画出来的字长什么样;setFontPosTop 是可选的,它让文字顶部对齐,方便在固定坐标排布 UI。loop 里的 firstPage/nextPage 是 u8g2 最重要的循环模式:firstPage 进入绘制流程,nextPage 返回 true 就继续画,返回 false 表示这一帧画完,把缓冲内容一次性交给屏幕。
参数上,drawStr(0, 0, "Hello u8g2!") 的前两个数字是 x、y 坐标,左上角为原点,向右向下增加。drawFrame(0, 12, 128, 20) 四个参数分别是左上角 x、y、宽度、高度,画出来的是一个空心矩形。这行画在文字下方,用来确认绘图坐标没有问题。
3.3 看懂那个超长构造函数:型号、缓冲、接口三个字段拆解
u8g2 的构造函数一眼看过去很吓人,长这样:U8G2_SSD1306_128X64_NONAME_F_HW_I2C。拆开就是五段信息:控制器型号(SSD1306)+面板尺寸(128X64)+面板变体(NONAME)+缓冲模式(F 或 1)+接口类型(HW_I2C)。控制器型号必须和屏幕里的芯片一致,SSD1306 和 SH1106 的命令集有差异,混用会出现显示异常甚至完全不亮;面板尺寸要匹配,128X64 就是 128 列、64 行像素。
缓冲模式是另一个容易踩坑的地方:_F_表示 full buffer,也就是全缓冲,在 RAM 里保留整帧画面,绘制完一次性刷新;_1_表示 page buffer,也就是页缓冲,只留 1/8 屏的缓冲,u8g2 自动分页刷新。全缓冲画起来自由,但占用 RAM 大,AVR 上经常爆内存;页缓冲省内存,适合 Uno、Nano 这种 RAM 只有 2KB 的板子。接口类型 HW_I2C 表示硬件 I2C,走 Wire 库,接线只需要 SDA/SCL 两根线;SW_I2C 是软件模拟 I2C,可以任意指定引脚,但速度慢、占用 CPU。选型建议:板子有硬件 I2C 就用 HW_I2C,Uno、Mega、ESP32 都有,没必要用软件模拟。
构造函数实例化时的参数同样有讲究:U8g2(Rotation, Reset_pin)是标准写法,第一个参数 U8G2_R0 表示正常方向,不旋转;如果是竖屏使用,可以改成 U8G2_R2 或 U8G2_R1、U8G2_R3,对应 180°、90°、270° 旋转。第二个参数 U8X8_PIN_NONE 表示 Reset 脚不接。如果你的屏有 RES 引脚,强烈建议接上并在构造函数里填入实际引脚号,比如 U8G2(UNO, A3),这样上电时序更可靠,能减少很多花屏和初始化失败的问题。
4. 中文、字体与显存:u8g2 里最容易被卡住的三件事
4.1 中文为什么默认画不出来:字体匹配的真相
u8g2 和 PC 上的图形库有一个根本差异:它没有“系统字体”这个概念,所有字符都以字模数组的形式固化在 Flash 里,drawStr 拿到字符串后,按每个字节在字体表里查字形。默认选择的字体只覆盖 ASCII 字符集,drawStr(0, 0, "温度") 画出来的只会是乱码或空白,因为字体表里根本没有“温”这个字的字形索引。
这不是 u8g2 的缺陷,而是嵌入式环境的现实:中文字形数据量太大,不做选择就全塞进去,Flash 先扛不住。u8g2 把字体分成很多对象,每个对象对应一种字符集覆盖范围。英文字体一个字符只占几十字节,而一个 16×16 的汉字字模要 32 字节,常用汉字按 3000 个算就是近 100KB,8 位 AVR 显然装不下。所以 u8g2 的策略是“按需加载”:你画什么语言,setFont 就选什么字库。
4.2 字体选型与自建小字库:unifont 与 bdfconv 的取舍
如果你用的是 ESP32 或 STM32 这类 Flash 和 RAM 都充裕的平台,u8g2 自带一个覆盖中文的字体对象:u8g2_font_unifont_t_chinese。它基于 GNU Unifont,覆盖了 CJK 基本区,直接 setFont 就能画中文:
#include <U8g2lib.h> // 页缓冲模式, 省 RAM; 如果 RAM 充足也可以换 _F_ U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); void setup(void) { u8g2.begin(); u8g2.setFont(u8g2_font_unifont_t_chinese); // 支持 CJK 的字体 u8g2.setFontPosTop(); } void loop(void) { u8g2.firstPage(); do { u8g2.drawStr(0, 0, "温度: 25.5C"); // 中英文混排 } while (u8g2.nextPage()); delay(1000); }这里用_1_模式而不是_F_,原因是 unifont 字体本身很大,但真正吃内存的是全缓冲。_1_模式下 u8g2 只需要一小块页缓冲,Flash 里放字体,RAM 压力小很多。中英文混排时,drawStr 的字符串必须是 UTF-8 编码,Arduino IDE 默认保存为 UTF-8,所以源码里直接写中文一般没问题。如果你从别的地方复制代码,文件编码被转成了 GB2312 或 GBK,画出来就会是乱码,这个问题在第 5 章还会提到。
如果平台是 Uno 或 Nano,unifont 就太奢侈了,自建小字库是常见做法。u8g2 自带一个字体转换工具,思路是把 BDF 格式的字体文件转成 C 语言数组,然后再编译进项目。我一般会只保留项目里用到的那几十个汉字,做一个 16×16 的点阵字库,Flash 占用能压到几 KB 以内。流程大致是:先准备好 BDF 字体文件,用转换工具生成 .c 文件,把这个文件放进 U8g2 的 src 目录,引用后 setFont 就能使用新字体。这个路线对不熟悉字体格式的人来说有点门槛,但它是 AVR 上跑中文的唯一现实方案。
4.3 全缓冲、页缓冲与 u8x8:先把 8KB SRAM 算清楚
u8g2 在实际使用中经常被问“为什么编译通过,烧录后屏幕没反应”或“为什么加了几行代码就编译报错”,大部分答案在显存这关。看对比表:
| 模式 | RAM 占用 | 绘制方式 | 适合场景 |
|---|---|---|---|
| U8G2 全缓冲(F) | 约 1KB(128×64/8) | 整帧绘制完成后一次刷新 | 图形复杂、RAM 充裕的板子 |
| U8G2 页缓冲(1) | 约 128 字节(1/8 屏) | 分页绘制,逐页刷新 | AVR、RAM 紧张的板子 |
| U8X8(无缓冲) | 0(无绘图缓冲) | 直接写屏幕,只能文本 | 纯文字显示、极省 RAM |
Uno 的 SRAM 只有 2KB,光 u8g2 全缓冲吃掉 1KB,再加上 Wire 库、字符串缓冲区、程序里的全局变量,堆栈很容易爆。所以我的默认选择是:AVR 平台一律用_1_或 u8x8,ESP32 才放开使用_F_。u8x8 是 u8g2 的同门双胞胎,不支持图形绘制,只能显示固定字体的文本,但零缓冲,RAM 占用几乎可以忽略。如果你只需要显示传感器数值、状态信息,u8x8 是性价比最高的方案。
页缓冲模式对绘制逻辑有一个硬约束:每次 firstPage/nextPage 循环里重新画整帧内容,不能依赖“上一帧残留”。因为页缓冲只有一部分内容在内存里,u8g2 会分多次刷新屏幕,你无法准确知道哪一页正在被发送。所以绘制函数里所有状态都要在循环体内完成,不要在循环外画一部分、循环内画另一部分,否则会出现缺行、错位。
5. u8g2 踩坑手册:黑屏、乱码、花屏与编译爆内存的 5 个排查
5.1 黑屏无反应:先查 I2C 地址和 Reset 引脚
现象是代码编译烧录都正常,但屏幕一点反应都没有,背光都不亮。这个坑排在第一位,因为它的排查顺序很固定:先查电源,再查 I2C 地址,最后查 Reset。很多模块的 I2C 地址是 0x3C,但也有 0x3D 的,出厂设置不同,构造宏默认按 0x3C 工作,如果屏是 0x3D,就必须换构造版本。先跑一个 I2C 扫描程序确认地址:
#include <Wire.h> void setup() { Wire.begin(); Serial.begin(9600); for (byte addr = 8; addr < 120; addr++) { Wire.beginTransmission(addr); if (Wire.endTransmission() == 0) { Serial.print("found: 0x"); Serial.println(addr, HEX); } } } void loop() {}这个程序扫描总线上所有可能的 7 位地址,把有设备响应的地址打印出来。如果串口里一个地址都没有,说明接线或电源有问题;如果打印出来的地址是 0x3D,就把构造函数换成 U8G2_SSD1306_128X64_NONAME_F_HW_I2C 的 3D 版本(产品页通常标注)。Reset 引脚也很关键,有些模块的 RES 悬空,上电时序不稳,表现就是偶尔亮偶尔不亮。解决方法是把 RES 接到主控的一个数字引脚上,比如 D9,并在构造函数里传入引脚号,比如 U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, 9)。
5.2 屏亮了但全是方块乱码:编码与字体不匹配
现象是屏幕能显示,但中文全是乱码,英文和数字正常。这基本不是屏的问题,而是编码问题:源码文件不是 UTF-8,或字体对象不含中文字形。Arduino IDE 默认保存 UTF-8,但如果代码是从别的编辑器复制来的,或者 Arduino IDE 的语言设置被改过,字符串的编码就可能变成 GB2312。u8g2 的 unifont 字库是按 UTF-8 编码匹配字符的,GB2312 编码喂进去,查不到对应字形,就会乱码。
解决方法是统一编码:把源码文件另存为 UTF-8 格式,或者重新在 IDE 里新建文件、粘贴代码。另一个前提是字体必须选对,如果 setFont 用的是 u8g2_font_ncenB08_tr 这种英文字体,就算编码是 UTF-8,照样画不出中文,必须换成 unifont_t_chinese 或自建中文字库。还有一个隐秘细节:有的中文字体对象只覆盖了部分字符集,比如只做了 GB2312 一级汉字,你用的某个生僻字不在里面,就会出现“大部分正常、个别字乱码”的诡异现象,查字体覆盖范围才能定位。
5.3 刷新时花屏带残影:供电和线长的问题
现象是屏幕能亮,但刷新时出现噪点、残影,或者画面越刷越花,放一会儿又正常。常见原因有两个:一是供电不足,OLED 模块动态刷新时电流波动,如果和主控共用一根细供电线,电压被拉低,屏幕就会发花;二是 I2C 线太长,超过了 20cm,信号反射和干扰导致数据传输错误。这问题玄学味道很重,因为有时候换一根线就好,有时候把线缩短就好,有时候加个电容就好。
解决思路按成本从低到高试:先给模块的 VCC 和 GND 之间加一个 100uF 电解电容,位置尽量靠近屏幕电源脚;然后把 I2C 导线缩短到 20cm 以内,并让 SDA 和 SCL 尽量平行走线;如果还花,把 I2C 时钟频率降下来。u8g2 在一些板子上可以用 setBusClock 调整总线频率,降到 100kHz 后稳定性会明显改善。最后才是考虑独立供电,不跟主控的电源稳压器抢电流。
5.4 编译报“内存不足”:一个字符数组把 AVR 打死
现象是代码写了一半,编译时 IDE 直接报“Not enough memory”或者“内存不足”,通常还伴随一堆变量地址溢出的提示。u8g2 在全缓冲模式下默认分配约 1KB 的静态 RAM 给显存,Uno 总共 2KB SRAM,剩下的空间给全局变量、局部变量和堆栈。如果程序里再放几个 String、几个大的 char 数组,编译器就会明确告诉你装不下。
解决方法是换模式:全缓冲_F_改页缓冲_1_,显存占用从 1KB 降到 128 字节,绝大多数 AVR 项目都能活下来。如果还缺,就把所有只读数据挪进 PROGMEM,比如中文字库、日志字符串、配置表。另一个常见诱因是在循环里用 String 拼接,String 在 AVR 上会制造大量内存碎片,换成 char 数组能省很多。记住一个检查习惯:全局变量总量加显存占用,不要超过板子 SRAM 的一半,剩下的一半留给堆栈。
5.5 换了一块同尺寸屏反而不亮:控制器型号和构造宏要同步换
现象是原来 128×64 的 SSD1306 屏一切正常,换成另一块同样是 128×64 的屏后,黑屏或者显示错位。很多卖家标注“SSD1306 兼容”,但实际用的可能是 SH1106。SSD1306 和 SH1106 虽然都是 128×64 单色 OLED 驱动,但内部显存结构和初始化命令有差异,构造宏用错就会出现白屏、下半屏空白、画面偏移。u8g2 对两种控制器都有专门构造:SSD1306 用 U8G2_SSD1306_128X64_NONAME_F_HW_I2C,SH1106 用 U8G2_SH1106_128X64_NONAME_F_HW_I2C,换屏时要同步换这一整条宏,只改尺寸或只改控制器都会出问题。
排查方法很简单:看屏幕主控芯片丝印,或者直接看卖家给的型号描述,然后对照 u8g2 的构造列表选择。如果没有丝印,可以试几个常见的构造宏,编译烧录后能正常显示的那个就是对的。还有一个坑是“兼容屏”的初始化地址不同,有些兼容屏默认 I2C 地址不是标准地址,用第 5.1 节的扫描程序确认一次最保险。
6. 从 Demo 到界面:页面函数、局部刷新和 u8g2 的显存瘦身习惯
Demo 跑通之后,下一步是把屏幕当成一个真正的界面来用。最值得掌握的技巧是“页面函数封装”:把每个界面的绘制逻辑收进一个函数,配合 switch 实现页面切换。这样代码结构清楚,也方便后续加动画状态。
uint8_t currentPage = 0; void drawStatusBar() { u8g2.drawStr(0, 0, "TEMP: 25.5C"); u8g2.drawHLine(0, 10, 128); } void drawMainPage() { drawStatusBar(); u8g2.drawFrame(0, 16, 128, 32); u8g2.drawStr(4, 20, "DATA OK"); } void drawPage(uint8_t page) { switch (page) { case 0: drawMainPage(); break; case 1: drawStatusBar(); break; } } void loop() { u8g2.firstPage(); do { drawPage(currentPage); } while (u8g2.nextPage()); delay(50); }局部刷新是另一个值得用起来的功能。u8g2 整帧刷新在页缓冲模式下已经很省内存,但如果你只需要更新一个数值区域,可以用 setClipWindow 限定绘制范围,减少数据量:
u8g2.setClipWindow(40, 32, 88, 56); // 只更新 40,32 到 88,56 区域 u8g2.drawBox(40, 32, 48, 24); // 先清掉旧内容 u8g2.drawStr(42, 40, "new value"); u8g2.undoClipWindow(); // 恢复全屏裁剪这套代码的思路是先把要更新的区域用 drawBox 填成背景色,再画新内容,最后恢复裁剪。这样避免全屏刷新时的闪烁感,也更省时间。u8g2 的 API 设计让我最舒服的一点是:所有绘制调用都往缓冲里画,并不直接上屏,所以你怎么组合都不会有撕裂。
最后分享一个我自己的固定习惯。最初做带屏项目时,为了省事直接全缓冲加 unifont 中文字库,Uno 编译当场爆内存,后来就养成了“先算显存、再选缓冲模式”的习惯:拿到一块新屏,先查控制器型号和 I2C 地址,再决定_F_还是_1_,最后才写绘制代码。这三步能省掉后面八成的排查时间。u8g2 本身是个成熟可靠的库,出错多发生在使用方式上,把这几个关键参数守住,屏幕基本不会给你制造惊喜。希望帮到你。
本文还有配套的精品资源,点击获取