Grove OLED屏SH1107驱动指南:SPI/I2C双模通信与U8g2图形库实战

📅 2026/8/2 13:42:12
Grove OLED屏SH1107驱动指南:SPI/I2C双模通信与U8g2图形库实战
1. 项目概述一块能说会道的“小窗口”如果你玩过单片机或者树莓派大概率会对那些能显示点文字、图案的小屏幕感兴趣。今天要聊的这块Grove - OLED 显示屏 1.12 (SH1107) V3.0就是这样一位“多面手”。它不像手机屏幕那样色彩斑斓但在嵌入式开发的世界里这块单色、高对比度的OLED屏却是调试信息、状态显示、甚至小型UI界面的绝佳载体。我手头这块是V3.0版本最大的亮点是同时支持SPI和I2C两种通信协议并且兼容3.3V和5V供电这意味着它几乎可以无缝接入你手边任何一款主控板无论是Arduino Uno5V、ESP323.3V还是STM32。为什么这块屏幕值得单独拿出来说因为在项目开发中一个可靠的显示输出往往能极大提升调试效率和用户体验。想象一下你的智能温湿度计不再需要连接电脑串口才能读数数据直接显示在屏幕上或者你的迷你游戏机需要一个低功耗的显示核心。这块OLED屏就是为这些场景而生的。它的核心驱动芯片是SH1107这是一款非常经典的128x128像素OLED驱动IC控制逻辑清晰社区资源丰富。接下来我会带你从硬件接口、通信协议选择、到软件驱动编写完整地走一遍使用流程并分享一些我实际项目中积累的、数据手册上不会写的经验和“坑”。2. 硬件接口与通信协议深度解析拿到这块屏幕首先得搞清楚怎么把它和你的主板连起来。Grove接口的便利性在于它用了一个4针的标准化连接器但里面藏着选择通信方式的玄机。2.1 引脚定义与电源兼容性设计先看四个引脚VCC、GND、SDA、SCL。如果你熟悉I2C会立刻发现这就是标准的I2C四线接法。但这款屏幕的SPI功能是如何实现的呢奥秘在于板载的跳线帽或焊点选择。I2C模式这是出厂默认模式。SDA和SCL就是I2C的数据线和时钟线。此时屏幕的I2C地址通常是0x3C这是SH1107的常见地址但务必以实际扫描为准。SPI模式需要通过改变硬件连接来启用。通常需要短接板子上标记为“SPI”的焊点或者改变跳线帽的位置。在SPI模式下SDA引脚变身为SPI的数据线MOSI/SDISCL引脚则变为SPI的时钟线SCK。除此之外SPI还需要额外的片选CS和数据/命令选择DC引脚这些信号是通过Grove接口旁边的单独排针引出的。所以使用SPI模式时你需要连接6根线VCC, GND, SCK, MOSI, CS, DC而不是简单的4线Grove接口。关于3.3V/5V兼容这是V3.0版本一个非常实用的特性。屏幕内部大概率集成了一颗电平转换芯片或采用了兼容两种电压的逻辑器件。这意味着无论你的主控板逻辑电平是3.3V如ESP32、STM32F4还是5V如Arduino Uno、AVR都可以直接连接VCC和信号线无需担心电平不匹配烧毁芯片或通信失败。这大大简化了硬件设计。不过一个经验之谈是如果你的系统是3.3V尽量使用3.3V为屏幕供电可以降低整体功耗和噪声。2.2 SPI vs I2C如何做出你的选择这是新手最常困惑的问题。两种协议没有绝对的好坏只有适合与否。I2C协议的优势与局限优点接线极其简单仅需2根信号线SDA, SCL节省IO口。支持总线挂载多个设备地址不同即可。协议本身包含应答机制通信相对可靠。缺点速度较慢标准模式100kbps快速模式400kbps。在需要频繁刷新全屏画面或显示动态效果时可能会成为瓶颈。通信距离短抗干扰能力相对SPI弱。SPI协议的优势与局限优点全双工通信速度极高轻松达到10Mbps以上。对于OLED这种需要持续刷新显存GRAM的设备SPI可以带来极其流畅的刷新体验尤其是在实现动画、滚动效果时。时序简单由硬件直接控制CPU开销小。缺点需要至少4根线SCK, MOSI, CS, DC如果还要读回数据MISO则需要5根。每个设备需要独立的CS片选线挂载多个设备时会占用大量IO。没有硬件应答机制主设备无法直接确认从设备是否收到数据通常靠延时或软件模拟。我的选择建议对于大多数显示静态文本、简单图表或更新不频繁的数据如温度每小时更新一次的应用I2C模式足矣它能最大程度保持接线的简洁。但如果你要制作一个需要平滑滚动菜单、显示动态波形图或小型游戏的设备那么SPI模式是必须的否则你会看到明显的闪烁和拖影。我个人的项目里数据监控仪表盘用I2C而手持游戏机则毫不犹豫地选择SPI。2.3 SH1107驱动芯片关键特性理解驱动芯片才能更好地驾驭屏幕。SH1107是一款132x132像素内存映射的OLED驱动IC但我们常用的显示区域是128x128。它的显存GRAM结构是“页-列”式。整个屏幕在垂直方向被分成16个“页”Page每页8行像素即高度为8。每个页有132列实际用128列。当你发送数据时需要先指定页地址0~15和列地址0~131。这种结构影响了我们的编程逻辑当你想要在某个坐标x, y画点时需要先计算出该点位于第几页Page y / 8以及在该页的第几列Col x还要计算出在该列字节中的具体位Bit y % 8。许多现成的图形库如U8g2, Adafruit GFX已经帮我们封装好了这些底层操作。另一个重要特性是SH1107需要电荷泵电压。OLED像素点需要较高的电压通常十几伏才能发光而我们的系统只有3.3V或5V。SH1107内部集成了电荷泵电路我们只需要通过命令0xAD, 0x8B, 0x30等序列将其开启即可。几乎所有驱动库的初始化函数里都包含了这部分配置。3. 软件驱动与图形库实战硬件连接妥当后下一步就是让屏幕亮起来并显示内容。我们通常不会直接去操作SH1107的寄存器而是借助成熟的驱动库。3.1 初始化流程与通信底层实现无论使用SPI还是I2C初始化流程大同小异核心是向SH1107发送一系列初始化命令。以下是一个典型的初始化序列概要关闭显示(0xAE)在配置过程中避免屏幕上出现乱码。设置显示时钟分频和振荡频率(0xD5, 0x80)调整驱动IC的内部工作频率。设置多路复用率(0xA8, 0x7F)对于128x128屏幕设置为1270x7F。设置显示偏移(0xD3, 0x00)将显示起始行设置为0。设置显示起始行(0x40)。开启内部电荷泵(0xAD, 0x8B, 0x30等)这是点亮屏幕的关键步骤。设置内存地址模式(0x20, 0x00)通常使用页地址模式Page Addressing Mode便于逐页写入数据。设置列地址范围(0x21, 0x00, 0x7F)设置列起始和结束地址为0~127。设置页地址范围(0x22, 0x00, 0x0F)设置页起始和结束地址为0~15。整个显示开启(0xA4)不采用全亮模式。设置正常显示(0xA6)非反相显示。设置COM扫描方向(0xC8)通常为反向扫描。设置对比度(0x81, 0x7F)默认对比度值。开启显示(0xAF)配置完成点亮屏幕。在代码层面你需要实现两个最基础的函数写命令函数writeCommand(uint8_t cmd)和写数据函数writeData(uint8_t data)。这两个函数的内部实现就是SPI和I2C的区别所在。I2C实现每次传输以一个控制字节开始。通常控制字节为0x00表示后续字节是命令0x40表示后续字节是数据。所以你的一次I2C传输可能是Start - 写设备地址(0x3C) - 写控制字节(0x00) - 写命令字节 - Stop。SPI实现通过DC引脚的高低电平来区分命令和数据。DC拉低时接下来通过MOSI发送的是命令DC拉高时发送的是数据。CS引脚则在每次传输前拉低传输后拉高。3.2 U8g2库跨平台的图形解决方案对于绝大多数应用我强烈推荐使用U8g2库。它是一个功能极其强大的单色图形库支持上百种显示控制器包括SH1107以及几乎所有的硬件平台Arduino, ESP32, STM32 HAL, Linux等和通信方式I2C, SPI, 并行8080等。在Arduino IDE中安装U8g2库后针对这块屏幕你可以这样初始化对于I2C连接#include U8g2lib.h // 使用硬件I2C并指定正确的引脚和地址 U8G2_SH1107_128X128_F_HW_I2C u8g2(U8G2_R0, /* reset*/ U8X8_PIN_NONE); // U8G2_R0表示旋转0度U8X8_PIN_NONE表示没有硬件复位引脚 void setup() { u8g2.begin(); // 初始化通信并执行初始化序列 }对于SPI连接#include U8g2lib.h // 假设CS接引脚10DC接引脚9 U8G2_SH1107_128X128_F_4W_HW_SPI u8g2(U8G2_R0, /* cs*/ 10, /* dc*/ 9, /* reset*/ U8X8_PIN_NONE); void setup() { u8g2.begin(); // 初始化 }初始化完成后你就可以使用一套统一的API进行绘图了。U8g2采用“页面缓冲器”或“全屏缓冲器”机制。对于SH1107使用全屏缓冲_F_在构造函数中会占用128 * 128 / 8 2048字节的内存但绘制和刷新非常简单void loop() { u8g2.clearBuffer(); // 清除内部缓冲区 u8g2.setFont(u8g2_font_ncenB08_tr); // 设置字体 u8g2.drawStr(0, 20, Hello World!); // 在坐标(0,20)绘制字符串 u8g2.drawCircle(64, 64, 30, U8G2_DRAW_ALL); // 画一个圆 u8g2.sendBuffer(); // 将缓冲区内容发送到屏幕显示 delay(1000); }实操心得在内存紧张的MCU如Arduino Uno只有2KB RAM上使用全屏缓冲2048字节可能会吃紧。这时可以考虑使用“页面缓冲”模式构造函数中不带_F_它一次只处理一页128字节的数据内存占用小但编程模型稍复杂需要你自己管理分页绘制逻辑。对于ESP32、STM32等资源丰富的平台直接上全屏缓冲体验最好。3.3 自定义图形与动画技巧U8g2提供了丰富的绘图函数点、线、矩形、圆、三角形、位图等。制作动画的原理就是“清除-绘制-发送”的循环。实现平滑动画的关键技巧避免全局清屏clearBuffer()会清除整个缓冲区如果每帧都全清全画在复杂图形下可能导致闪烁。可以尝试只重绘发生变化的部分区域使用setDrawColor设置为背景色进行局部覆盖。双缓冲如果支持一些高级平台或库支持双缓冲即在后台缓冲区绘制完成后再一次性交换到前台显示可以完全消除闪烁。U8g2本身不直接支持硬件双缓冲但你可以自己维护两个软件缓冲区。使用sendBuffer()而非refreshDisplay()sendBuffer()是U8g2推荐的更新方式它高效地将整个缓冲区发送到屏幕。避免在循环中调用初始化或重置函数。精简绘图操作计算和绘图是耗时的。对于固定不变的UI框架如边框、标题栏只需在初始化时绘制一次之后只更新数据变化的部分。例如制作一个移动的小球int ballX 10; int ballY 64; int ballSpeedX 2; void loop() { u8g2.clearBuffer(); // 绘制静态背景如果复杂可以只画一次 // u8g2.drawFrame(0,0,128,128); // 更新小球位置 ballX ballSpeedX; if(ballX 118 || ballX 10) { ballSpeedX -ballSpeedX; } // 绘制小球 u8g2.drawDisc(ballX, ballY, 10); u8g2.sendBuffer(); delay(16); // 约60FPS }4. 典型应用场景与进阶玩法这块屏幕的应用远不止显示几行文字。结合不同的主控它能玩出很多花样。4.1 嵌入式系统信息仪表盘这是最直接的应用。你可以用ESP32读取网络时间NTP、传感器数据温湿度、气压然后美观地呈现在OLED上。实现要点布局设计在纸上或绘图软件中先规划好各个数据块的位置避免显示杂乱。字体选择U8g2内置多种字体大小各异。对于数字可以选择等宽字体如u8g2_font_profont22_mr便于对齐。混合使用大字体显示关键数据小字体显示标签。数据更新策略传感器数据无需每秒刷新多次。可以设置一个定时器每5秒或10秒读取并更新一次传感器数据和屏幕其余时间MCU可以进入低功耗模式这对于电池供电设备至关重要。图标使用可以使用U8g2的drawXBM()函数显示自定义的位图图标比如温度计图标、Wi-Fi信号图标让界面更友好。4.2 结合ESP32-S3实现网络时钟结合最新的网络热词“esp32s3获取时间并显示在oled屏幕”这是一个完美的实践。ESP32-S3内置Wi-Fi可以轻松连接网络获取NTP时间。核心步骤硬件连接将OLED屏幕通过I2C或SPI连接到ESP32-S3。注意ESP32-S3的默认I2C引脚是GPIO8SDA和GPIO9SCL但你可以通过Wire.begin(SDA_PIN, SCL_PIN)自定义。软件配置引入U8g2库和NTPClient库。编写连接Wi-Fi的函数。初始化NTP客户端设置时区。主循环逻辑void loop() { if (WiFi.status() WL_CONNECTED) { timeClient.update(); // 从NTP服务器更新时间 formattedTime timeClient.getFormattedTime(); formattedDate timeClient.getFormattedDate(); } u8g2.clearBuffer(); u8g2.setFont(u8g2_font_logisoso32_tn); // 大号数字字体 u8g2.drawStr(centerX(formattedTime.substring(0,5)), 50, formattedTime.substring(0,5).c_str()); // 显示时:分 u8g2.setFont(u8g2_font_helvB10_tr); u8g2.drawStr(centerX(formattedDate), 80, formattedDate.c_str()); // 显示日期 u8g2.sendBuffer(); // 每分钟同步一次NTP即可减少网络请求 static unsigned long lastSync 0; if (millis() - lastSync 60000) { timeClient.forceUpdate(); lastSync millis(); } delay(100); // 控制刷新率 }4.3 低功耗设备的状态显示器得益于OLED屏幕像素自发光、黑色不耗电的特性它非常适合低功耗设备。在显示静态内容时功耗可以非常低。省电技巧降低刷新率如果不是必须不要以最高频率刷新屏幕。可以仅在数据变化时更新。使用深色背景OLED显示黑色像素时该像素点不工作。因此设计UI时尽量采用黑色背景亮色文字和图形。利用驱动IC的睡眠命令在设备长时间待机时可以通过发送命令0xAE完全关闭显示将屏幕功耗降到几乎为零。需要显示时再发送0xAF开启。动态调整对比度在环境光较暗时可以适当降低对比度0x81, value也能节省少量功耗。5. 常见问题排查与调试心得即使按照教程操作也难免会遇到屏幕不亮、显示乱码、通信失败等问题。这里总结几个我踩过的“坑”和解决方法。5.1 屏幕完全不亮无显示这是最常见的问题排查思路如下检查电源和接线这是第一步也是最容易出错的一步。用万用表测量VCC和GND之间是否为稳定的3.3V或5V。确认所有连接线接触牢固特别是Grove连接器是否插紧。确认通信协议模式你用的是I2C还是SPI板子上的跳线帽或焊点设置是否正确如果你写的是I2C程序但硬件跳到了SPI模式肯定无法通信。检查I2C地址使用一个I2C扫描程序Arduino IDE有示例扫描总线确认屏幕上显示的地址。SH1107常见地址是0x3C但也可能是0x3D。U8g2初始化时需要传入正确的地址。检查复位引脚有些驱动库或硬件需要复位信号。如果屏幕有RST引脚确保它被正确初始化通常先拉低再拉高。如果未使用在代码中设置为U8X8_PIN_NONE。初始化序列是否完整特别是开启电荷泵的命令序列是否成功发送。可以尝试在初始化后手动发送一个写全屏亮的命令0xA5测试屏幕硬件是否正常。如果全屏能亮说明硬件和基础通信是好的问题在初始化或GRAM操作。5.2 显示乱码、花屏或内容错位出现显示内容不正常通常与数据发送有关。SPI模式下的DC引脚在SPI模式下DC引脚必须在发送命令前拉低发送数据前拉高。这是一个非常容易疏忽的时序问题。用逻辑分析仪或示波器抓取CS、DC、SCK、MOSI的波形是最直接的调试方法。通信速度过快特别是对于I2C如果主控MCU的I2C时钟设置过快比如超过400kHz而屏幕或线材质量不佳可能导致数据出错。尝试降低I2C时钟速度例如Wire.setClock(100000)。电源噪声OLED屏在刷新时电流会有波动如果电源纹波过大可能导致驱动IC工作异常。在VCC和GND之间并联一个10uF~100uF的电解电容和一个0.1uF的陶瓷电容可以显著改善。软件库配置错误确认U8g2构造函数选择的设备型号完全正确。U8G2_SH1107_128X128_F_HW_I2C和U8G2_SH1106_128X64_...是不同的驱动芯片初始化序列不同混用必然导致花屏。内存溢出如果使用了全屏缓冲且进行了复杂的图形操作可能导致栈溢出或内存混乱从而影响显示。可以尝试简化程序或切换到页面缓冲模式测试。5.3 I2C通信不稳定或无法识别上拉电阻I2C总线需要上拉电阻通常4.7kΩ~10kΩ到VCC。虽然很多主控板如Arduino Uno、ESP32开发板和模块内部可能已经集成但如果通信距离稍长或设备多外接上拉电阻会大大增加稳定性。这是解决I2C通信问题最有效的措施之一。总线冲突总线上是否有其他I2C设备地址冲突用扫描程序检查。电平兼容虽然屏幕兼容3.3V/5V但要确保主控的I2C引脚电平与VCC匹配。一个5V的Arduino与一个工作在3.3V逻辑的屏幕直接连接长期可能损坏屏幕。最好使用电平转换器或者确保屏幕由5V供电此时其信号电平也预期为5V。5.4 刷新速度慢或有闪烁协议瓶颈如前所述I2C的刷新率有上限。计算一下全屏128x128像素1位色深共2048字节。I2C每字节传输需要9个时钟周期8位数据1位ACK在100kHz时钟下传输一屏数据理论最快需要2048 * 9 / 100000 ≈ 0.184秒即约5.4 FPS。这还不算命令开销和协议本身的开销。因此I2C不适合高速刷新。解决方案换用SPI接口。软件瓶颈clearBuffer()和复杂的draw函数调用非常耗时。优化绘图逻辑只更新变化区域。对于固定UI使用setDrawColor进行局部擦除而非全屏清除。缓冲区管理确保使用的是sendBuffer()并且不要在循环中频繁初始化库。调试这类问题最有力的工具是逻辑分析仪。一个几十块钱的简易逻辑分析仪配合PulseView软件就能清晰地看到I2C或SPI总线上的每一个命令和数据对照SH1107的数据手册可以精准定位是哪个命令没发、哪个数据发错了效率远超盲目猜测。