ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

U8glib实战:Arduino/ESP32驱动OLED显示从入门到避坑指南

U8glib实战:Arduino/ESP32驱动OLED显示从入门到避坑指南 简介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基本都是SSD13061.3寸OLED用SH1106经典12864LCD用ST7920。它的价值在于把“画点”这种底层操作封装成了可以直接调用的绘图函数你不需要关心像素怎么写到屏幕的显存里只要调drawLine、drawCircle、drawStr剩下的交给库。很多人可能会问Adafruit也有SSD1306库U8g2也在更新为什么还要回头用U8glib我的看法是如果你是做一个小型传感器节点、舵机控制板、或者只需要固定显示几行数字和图标U8glib编译后的体积更小内存占用也更友好。U8g2因为要兼容更多字体和彩色屏底层资源开销明显更大在ATmega328P这种只有2KB SRAM的板子上稍微多放几帧缓冲就可能爆内存。U8glib的设计还保留着“极简”的味道对老硬件相当友好。1.2 U8glib和U8g2怎么选选型其实不复杂可以直接参考这个表对比项U8glibU8g2维护状态停止大版本更新但稳定持续更新字体支持内置英文、数字、符号可扩展全字库支持中文大量字体彩色屏支持不支持支持内存占用相对更小稍大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); // SCK13, MOSI11, CS10, DC9接线方面I2C屏只需要四根线VCC接3.3V或5V多数模块板载稳压UNO可以接5VESP32建议接3.3VGND接地SCL接UNO的A5或ESP32的GPIO22SDA接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的宽高参数是否会超出屏幕边界。如果你把xwidth写超了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频率默认是100kHzU8glib刷全屏会略微偏慢。可以调高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 内存不足尤其是UNOATmega328P只有2KB SRAMU8glib的页面缓冲虽然不大但如果你同时用了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来说是最实在的方案。希望这篇从库安装到实战排查的记录能帮你少走一些弯路。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进