简介:U8glib for Arduino是一份面向Arduino开发者的图形库资源,用于在LCD或OLED显示屏上绘制图形、文本与位图,适合开发仪表盘、游戏界面、数据可视化等小型嵌入式项目。压缩包共129个文件,包含105个C语言核心源码、18个INO示例、2个头文件及少量说明与版本记录,整体1.05MB,结构紧凑,便于快速查阅。该资源已被723人学习下载。库内提供SSD1306、SH1106等常见屏幕的驱动,以及I2C、SPI等接口的初始化和丰富的绘图函数,通过示例可掌握drawStr、drawCircle、drawBox、页面刷新等核心用法,同时能学习内存管理和显示效率优化方法,为自制显示类作品提供完整且可直接套用的参考。 最近调一个ESP32小项目,翻出一块吃灰的0.96寸OLED,想加个状态显示。好几年没碰这个模块,下意识就在Arduino IDE里输入了u8glib关键字。没错,就是那个老牌图形库U8glib,哪怕现在大家都在推U8g2,它在实际项目里依然能打,尤其是给UNO这类小内存板子做单色屏显示时,轻量、稳定、资料多。这篇东西就从u8glib_arduino这个库出发,聊聊我怎么用它驱动OLED、刷新变量、接ESP32联网显示时间,以及那些折腾大半夜才搞明白的坑。适合刚开始用Arduino做屏显、或者想把老代码翻出来复用的朋友参考。
1. 项目核心思路:为什么U8glib至今没被淘汰
1.1 库的定位和能干什么
U8glib是一个面向单色图形显示器的Arduino图形库,支持的控制器一大票:SSD1306、SH1106、ST7920、SSD1327、LD7032等。日常用的0.96寸OLED基本都是SSD1306,1.3寸OLED用SH1106,经典12864LCD用ST7920。它的价值在于把“画点”这种底层操作封装成了可以直接调用的绘图函数,你不需要关心像素怎么写到屏幕的显存里,只要调drawLine、drawCircle、drawStr,剩下的交给库。
很多人可能会问,Adafruit也有SSD1306库,U8g2也在更新,为什么还要回头用U8glib?我的看法是:如果你是做一个小型传感器节点、舵机控制板、或者只需要固定显示几行数字和图标,U8glib编译后的体积更小,内存占用也更友好。U8g2因为要兼容更多字体和彩色屏,底层资源开销明显更大,在ATmega328P这种只有2KB SRAM的板子上,稍微多放几帧缓冲就可能爆内存。U8glib的设计还保留着“极简”的味道,对老硬件相当友好。
1.2 U8glib和U8g2怎么选
选型其实不复杂,可以直接参考这个表:
| 对比项 | U8glib | U8g2 |
|---|---|---|
| 维护状态 | 停止大版本更新,但稳定 | 持续更新 |
| 字体支持 | 内置英文、数字、符号,可扩展 | 全字库,支持中文大量字体 |
| 彩色屏支持 | 不支持 | 支持 |
| 内存占用 | 相对更小 | 稍大 |
| API风格 | 基础绘图函数 | 兼容U8glib绝大多数函数,新增缓冲/全屏模式 |
| 适用场景 | 小内存Arduino、极简显示 | 功能复杂、需要丰富字体 |
我个人的习惯是:项目如果只是显示几个数值、跑一个进度条,U8glib就够了。如果要用中文字库、做菜单系统、或者用彩色TFT屏,那直接上U8g2,别在老库里过度纠结。这里要提一句,U8glib和U8g2的API大体兼容,但字体的名称、构造函数不完全相同,迁移的时候要看准。
2. U8glib库的安装和最小工程框架
2.1 从安装到第一个工程
不管你是用Arduino UNO还是ESP32,安装库都走同一条路:打开Arduino IDE,在“工具—管理库”里搜索u8glib,找到“U8glib by Oliver Kraus”点击安装。如果网络慢,也可以去GitHub上下载zip包,再通过“项目—包含库—添加.ZIP库”导入。
安装完以后,新建一个Arduino工程,需要根据你屏幕的硬件接口和驱动芯片来选择构造函数。我手头这块是I2C接口的0.96寸SSD1306,第一行代码通常写成:
#include <U8glib.h> U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE);如果是SPI接口,则要指定CS、DC、RES等引脚,例如:
U8GLIB_SSD1306_128X64 u8g(13, 11, 10, 9); // SCK=13, MOSI=11, CS=10, DC=9接线方面,I2C屏只需要四根线:VCC接3.3V或5V(多数模块板载稳压,UNO可以接5V,ESP32建议接3.3V),GND接地,SCL接UNO的A5或ESP32的GPIO22,SDA接UNO的A4或ESP32的GPIO21。SPI屏接线会多两根控制线,而且不同模块引脚定义差异大,动手前先确认模块背面的丝印。
2.2 理解U8glib的页面缓冲机制
U8glib最核心也最容易让新手绕晕的是它的一套页面循环写法。经典的模板长这样:
void draw(void) { u8g.setFont(u8g_font_unifont); u8g.drawStr(0, 20, "Hello U8glib"); } void loop(void) { u8g.firstPage(); do { draw(); } while (u8g.nextPage()); }为什么非要写firstPage()和nextPage()?这是因为U8glib在内存里只保留了一个很小的页面缓冲,不是把整块128x64的显存放在MCU内存里(那样需要1KB,对UNO来说也能承受,但库里为了省内存做成页面缓冲模式)。每次调用firstPage(),库把绘图指针指向屏幕的第一页,你在do-while循环里画的所有内容会写到当前页面缓冲;nextPage()会把这个缓冲发送到屏幕,然后滚动到下一页。如此循环,看起来就像全屏刷新,实际上是一页一页推过去的。
我最早用这个库时没看懂,直接在setup里写了几行绘图代码,结果屏幕死活不亮。搞明白页面循环以后就通了:U8glib的绘图逻辑必须在firstPage到nextPage之间执行,这是一个类似“重绘事件”的机制,不能像刷Serial那样随心所欲。
2.3 一个能跑起来的最小工程
下面是最小工程的完整代码,用来显示一行文字和一个不断递增的数字:
#include <U8glib.h> U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE); int counter = 0; void drawFrame() { u8g.setFont(u8g_font_unifont); u8g.drawStr(0, 16, "Counter:"); u8g.setPrintPos(0, 40); u8g.print(counter); } void setup() { } void loop() { u8g.firstPage(); do { drawFrame(); } while (u8g.nextPage()); counter++; delay(50); }这里特别注意u8g.setPrintPos()和u8g.print(),类似Arduino的Serial.print,但在U8glib里,print输出的是图形坐标,而不是串口。如果直接u8g.print("abc")不设坐标,是看不到内容的。
3. 图形、文字和变量的显示实操
3.1 基础图形绘制
U8glib提供的基础绘图函数足够应付大多数场景。画线用drawLine(x0, y0, x1, y1),画矩形框用drawFrame(x, y, w, h),填充矩形用drawBox(x, y, w, h),画圆用drawCircle(x, y, r),画像素点用drawPixel(x, y)。这些函数的坐标系和屏幕像素一一对应,左上角是(0,0),x轴向右,y轴向下,范围根据屏幕是128x64还是128x32而定。
做一个简单的仪表盘背景,可以在drawFrame里加上:
u8g.drawFrame(0, 0, 128, 64); // 最外框 u8g.drawCircle(64, 28, 20); // 圆心在(64,28)的圆环 u8g.drawBox(20, 50, 30, 8); // 一个小的填充块,用来做状态指示这里容易踩坑的是,U8glib的drawFrame和drawBox的宽高参数是否会超出屏幕边界。如果你把x+width写超了128,有的驱动会截断,有的会花屏。保险做法是在画之前加个边界判断,或者把坐标控制在0~127、0~63之间。
3.2 字体选择与动态数字显示
U8glib内置了一大堆字体,比如u8g_font_unifont是12x12的宽字体,适合显示可读性高的大数字;u8g_font_7x14、u8g_font_8x13B是紧凑型字体,适合显示标签。字体换起来很简单:
u8g.setFont(u8g_font_7x14); u8g.drawStr(0, 10, "Temp:"); u8g.setFont(u8g_font_unifont); u8g.setPrintPos(60, 10); u8g.print(temperature);这里有个我想特别提醒的细节:setFont不仅仅影响后面的drawStr,也影响print的字符宽高。如果你在显示数字时发现数字特别大,或者宽度算错了,多半是字体没切换过来。另一个坑是,U8glib默认不带ASCII码表之外的特殊符号,想显示°这种字符时,建议直接用u8g.drawStr(0, 10, "Temp: "); u8g.print(val); u8g.drawStr(90, 10, "\xb0C");,0xB0在部分字体里就是度符号,但不同字体的映射不一致,最好提前测试。
3.3 结合舵机角度显示的场景
做舵机控制时,OLED上显示当前角度是常见的需求。假设你用的是Servo库,pulseWidth是舵机的PWM脉宽,角度是0~180。显示核心代码就是:
int angle = map(pulseWidth, 544, 2400, 0, 180); u8g.setPrintPos(20, 40); u8g.print("Angle:"); u8g.print(angle); u8g.print((char)223); // 用字体自带的度符号注意,U8glib的页面刷新速度受I2C时钟影响,而舵机控制通常要求50Hz的刷新率。如果你在loop里频繁执行U8glib刷新,可能会影响舵机PWM的时序。我的做法是:OLED显示每200ms更新一次,舵机控制放在中断或定时器里,两者互不干扰。这一点在做多任务时非常重要,否则会看到舵机时不时抖动一下。
4. ESP32上使用U8glib的实战记录
4.1 ESP32的引脚和Wire初始化
在ESP32上跑U8glib,绝大多数I2C OLED模块需要自己指定引脚,因为ESP32默认的I2C引脚和UNO不一样。首先在代码里调用Wire.begin(SDA_PIN, SCL_PIN);,然后初始化U8glib:
#include <Wire.h> #include <U8glib.h> #define SDA_PIN 21 #define SCL_PIN 22 U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE); void setup() { Wire.begin(SDA_PIN, SCL_PIN); }如果你不写Wire.begin,有些模块会在U8glib内部自动初始化默认引脚,但不同开发板定义不同,最好显式指定。另外,ESP32的I2C频率默认是100kHz,U8glib刷全屏会略微偏慢。可以调高Wire时钟,例如在Wire.begin后面加一句:
Wire.setClock(400000);实测下来,从100kHz提到400kHz,刷新速度明显改善,但部分老化模块在长线传输时可能不稳定,如果出现花屏就降回来。
4.2 获取HTTP时间并显示在OLED
这个需求很多人问过:ESP32联网后获取当前时间,然后在OLED上显示。我在项目里用一个HTTP接口获取时间字符串,再解析成“时:分:秒”显示,代码片段如下:
WiFiClient client; HTTPClient http; http.begin("http://worldtimeapi.org/api/ip"); // 返回JSON含datetime字段 int httpCode = http.GET(); String payload = http.getString(); // 解析datetime后面的HH:MM:SS int idx = payload.indexOf("datetime"); String timeStr = payload.substring(idx + 11, idx + 19); u8g.firstPage(); do { u8g.setFont(u8g_font_fur14); u8g.drawStr(10, 30, timeStr.c_str()); u8g.drawStr(10, 50, "Local Time"); } while (u8g.nextPage());这里注意,U8glib的drawStr接收的是const char*,不能直接传String,必须用timeStr.c_str()。另外,HTTP请求会阻塞几十毫秒,如果OLED每帧都去请求,那整个流程就卡住了。正确做法是每隔一分钟获取一次时间,存到全局变量里,显示的时候只从变量读。
4.3 智能小车状态屏的显示思路
智能小车项目里,OLED最适合显示电压、电机PWM值、循迹传感器状态等。我自己做的一版里,用了两个进度条来显示左右电机的PWM百分比:
void drawCarStatus(int leftPWM, int rightPWM) { u8g.drawFrame(0, 0, 128, 64); u8g.drawStr(4, 12, "L"); u8g.drawBox(16, 4, (leftPWM * 50) / 255, 8); u8g.drawStr(4, 28, "R"); u8g.drawBox(16, 20, (rightPWM * 50) / 255, 8); u8g.setPrintPos(80, 28); u8g.print(rightPWM); }进度条的长度根据PWM值等比缩放,分母255对应Arduino的analogWrite范围。这里容易忽略的一点是,PWM值如果是负数(比如倒车),要先做绝对值处理,否则drawBox宽度为负,显示会异常。我在实际测试时发现,某些驱动对负数宽度的处理不可预知,有的直接画满屏,有的报错,所以代码里最好加一句:
leftPWM = constrain(abs(leftPWM), 0, 255);5. 常见问题排查与避坑记录
5.1 屏幕不亮或只亮一行
这种情况十有八九是接线问题或地址不对。如果是I2C屏,先运行I2C扫描程序看设备地址:
#include <Wire.h> void setup() { Wire.begin(); Serial.begin(115200); for (byte i = 8; i < 127; i++) { Wire.beginTransmission(i); if (Wire.endTransmission() == 0) { Serial.print("Found: 0x"); Serial.println(i, HEX); } } }大部分SSD1306模块地址是0x3C,少数是0x3D。如果扫描到0x3C,但U8glib还是没显示,可能你的模块不是SSD1306,而是SH1106或者SSD1309。U8glib对SSD1306和SH1106的初始化指令不一样,选错构造函数就会显示异常或干脆不亮。老老实实把构造函数换成U8GLIB_SH1106_128X64试试。
5.2 编译报错:构造函数找不到
U8glib不同版本的构造函数名略有差异,比如U8GLIB_SSD1306_128X64在旧版本里可能是U8GLIB_SSD1306_128X64_2X。如果编译报错提示找不到这个类,先打开库里的U8glib.h看一眼支持列表,或者换用更通用的初始化方式:
U8GLIB u8g(U8G_I2C_OPT_NONE);这个通用构造会在运行时根据设备地址和ID自动识别一部分芯片,但识别能力有限,还是推荐指定具体型号。
5.3 内存不足,尤其是UNO
ATmega328P只有2KB SRAM,U8glib的页面缓冲虽然不大,但如果你同时用了WiFi模块或一堆字符串,很容易爆内存。编译时报错里经常出现“low memory available”或“region 'data' overflowed”。我的解法是:尽量减少全局变量,尽量用PROGMEM存储常量字符串,字体选择偏小的如u8g_font_5x7而不是庞大的unifont。这里有一张经验公式:每换一个更大的字体,可能多占几十字节FLASH,但如果字符串内容变多,RAM反而没多大变化,因为drawStr的字符串是从FLASH读的。真正的RAM大头是I2C缓冲和动态数组,尤其是String类型,能少用就少用。
5.4 刷新慢、闪烁,怎么优化
U8glib的全页面刷新在SPI接口下速度尚可,在I2C下确实慢,肉眼能看到明显的扫描刷新。解决办法有三个:一是把I2C时钟提到400kHz;二是改用局部刷新,不要全屏重绘;三是切换到U8g2的全缓冲模式,但内存占用会上升。局部刷新最实用的做法是先用drawBox覆盖掉旧内容,再画新内容,只更新变化区域,例如:
u8g.setColorIndex(0); // 黑色 u8g.drawBox(20, 30, 30, 10); // 清除旧数字区域 u8g.setColorIndex(1); // 白色 u8g.setPrintPos(20, 40); u8g.print(newValue);setColorIndex(0)在单色屏上表示清成背景色,也就是不点亮像素。这个技巧在显示大数字时尤其有效,不然旧数字和尾数残影叠在一起,看着很脏。
5.5 老代码迁移到U8g2容易翻车的地方
我在一个新项目里试过把U8glib直接改到U8g2,发现三个地方最容易出问题。一是构造函数,U8g2的名字变成了U8G2_SSD1306_128X64_NONAME_1_HW_I2C,还多了缓冲模式参数。二是字体名,U8g2里改成了u8g2_font_unifont_t_*,后缀不一样。三是页面循环里的draw()函数,U8g2完全兼容firstPage/nextPage,但如果用全缓冲模式,就不再需要页面循环,直接调用u8g2.clearBuffer()、绘图、u8g2.sendBuffer()。所以迁移时先想清楚用哪种缓冲模式,不要照抄。
6. 我的经验和一点小建议
折腾U8glib这些年,我最大的感受是:别被它“老”的外表劝退,在很多轻量显示场景下,它比一切花里胡哨的新库都省心。尤其是当你只有一块UNO、一个OLED和一个传感器时,U8glib几乎就是开销最小的选择。我通常会在项目最开始就写一个“屏幕自检”函数,把画线、画圆、数字显示、字符串显示一次跑一遍,确认屏幕没有问题再往上层加逻辑,这个习惯帮我省了大量排错时间。
最后再分享一个小技巧:U8glib的页面渲染机制虽然看起来别扭,但很适合做“多页面切换”。比如我在一个仪表项目里,用页数变量控制第一页显示状态、第二页显示参数、第三页显示历史曲线,只要在draw()函数里用switch切换不同的绘图内容,再配合一个按键切换页码,就能快速做一个极简的多级菜单。这个思路和现在流行的UI框架比,可能显得朴素,但对资源受限的MCU来说,是最实在的方案。希望这篇从库安装到实战排查的记录,能帮你少走一些弯路。
本文还有配套的精品资源,点击获取