1. 项目概述:一块能说会道的“小窗口”
如果你玩过Arduino、树莓派或者ESP32这类开发板,大概率见过那种小小的、能显示几行文字或简单图形的屏幕。今天要聊的这块Grove - OLED 显示屏 1.12 (SH1107) V3.0,就是这类屏幕中一个非常经典且实用的型号。它不像手机屏幕那样色彩斑斓,但胜在结构简单、功耗极低、接口灵活,特别适合嵌入到各种需要显示状态、数据或简单交互的电子项目中,比如智能家居的温湿度显示、迷你气象站、或者一个桌面电子时钟。
这块屏幕的核心是一块1.12英寸的OLED面板,驱动芯片是SH1107。你可能听过更常见的SSD1306,SH1107可以看作是它的“兄弟”,驱动逻辑相似,但分辨率更高一些(128x128像素),显示面积也稍大。V3.0版本最大的亮点在于其接口的“全能性”:它同时支持SPI和I2C两种通信协议,并且供电电压兼容3.3V和5V。这意味着无论你手头是古老的5V Arduino Uno,还是现代的3.3V ESP32,甚至是树莓派,几乎都能即插即用,省去了电平转换的麻烦。Grove接口的加持更是让连接变得傻瓜化,一个四芯线缆搞定电源、地和数据线,极大降低了硬件接线的门槛和出错概率。
对于开发者,尤其是嵌入式入门者和创客来说,这块屏幕的价值在于它提供了一个极其友好的显示解决方案。你不用去研究复杂的LCD时序,不用焊接排针,甚至不用太担心电源问题。你的精力可以完全集中在“让屏幕显示什么”这个核心创意上。无论是想显示传感器读数、系统状态,还是滚动播放一段文字,它都能成为一个可靠的信息输出“窗口”。
2. 核心硬件与接口深度解析
2.1 OLED屏幕与SH1107驱动芯片
首先得明白OLED和普通LCD的区别。LCD(液晶显示屏)本身不发光,需要背光灯照亮,所以即使显示全黑,背光也在耗电。而OLED(有机发光二极管)的每个像素点都是独立发光的,显示黑色时像素点直接关闭,因此可以实现极高的对比度和真正的黑色,功耗也更低,特别适合电池供电的设备。这块1.12英寸的屏幕就是单色(通常是蓝白或黄白)OLED,显示效果清晰锐利,可视角度大。
驱动芯片SH1107是屏幕的“大脑”。它内部集成了显示RAM(显存),你通过SPI或I2C接口发送的命令和数据,实际上就是写入了这片RAM。SH1107会按照固定的扫描频率,自动将RAM中的数据转换成控制每个OLED像素点亮灭的电压信号。它支持的最大分辨率是128x128,但请注意,这块Grove屏幕的物理像素是128x128,而更常见的0.96寸OLED(SSD1306驱动)通常是128x64。多出一倍的行数,意味着你可以显示更多行文字或更复杂的图形。
SH1107与SSD1306的指令集高度兼容,这意味着为SSD1306编写的绝大多数库(例如Arduino的Adafruit_SSD1306或U8g2)经过简单配置(修改分辨率、I2C地址等)后,通常可以直接用于SH1107,这是其生态友好的一个重要体现。
2.2 SPI vs I2C:两种通信协议的选择与配置
这是本模块最核心的部分。SPI和I2C是嵌入式领域两种最常用的短距离、板级同步串行通信协议。这块屏幕的V3.0版本通过板载的一个微型拨码开关(或零欧姆电阻焊点)来切换这两种模式。
I2C模式:
- 特点:仅需两根信号线——SDA(数据线)和SCL(时钟线),所有设备都挂在这两根总线上,通过唯一的设备地址来寻址。这是一种多主多从、半双工的总线协议。
- 接线:在Grove系统中,连接到主控板的I2C接口(例如Arduino的A4/SDA, A5/SCL)。屏幕的I2C地址通常是0x3C(这是SH1107的常见地址,但需以实际扫描为准)。
- 优点:节省引脚,布线简单,总线上可以挂多个设备(如同时连接OLED、温湿度传感器、气压计)。
- 缺点:通信速度相对较慢(标准模式100kbps,快速模式400kbps),协议中有应答机制,软件开销稍大。
- 适用场景:对引脚数量敏感、对显示刷新速度要求不高的项目,例如数据仪表盘、状态显示器。当你的主控板I2C引脚被占用或需要更高速刷新时,就要考虑SPI。
注意:I2C总线需要上拉电阻。幸运的是,这块Grove模块和大多数开发板(如Arduino、ESP32)的I2C接口通常已经内置了上拉电阻。如果你是自己用MCU的GPIO模拟I2C(软件I2C),则必须在SDA和SCL线上各接一个4.7kΩ - 10kΩ的电阻到正电源(3.3V或5V),否则通信无法建立。
SPI模式:
- 特点:通常需要四根线——SCK(时钟)、MOSI(主机输出从机输入,即数据线)、DC(数据/命令选择,有时也叫D/C)、CS(片选,低电平有效)。这是一种全双工、高速的通信协议。
- 接线:需要连接到主控板的硬件SPI接口(如Arduino Uno的 13/SCK, 11/MOSI, 10/SS)。除了电源线,你需要连接SCK, MOSI, DC, CS四根线。RST(复位)和D/C引脚在某些库中是必需的。
- 优点:速度极快(轻松达到10Mbps以上),协议简单直接,无应答机制,效率高。
- 缺点:占用引脚多(至少4个),且每个从设备需要独立的片选线(CS),当连接多个SPI设备时布线会复杂。
- 适用场景:需要快速刷新、显示动态图形或动画的项目,比如迷你游戏机、频谱可视化等。SPI是追求性能的首选。
如何选择?
- 新手、引脚紧张、多设备:优先选I2C。
- 需要高速刷新、动态内容:优先选SPI。
- 这块Grove模块的背面有一个很小的拨动开关,标有“I2C”和“SPI”,用镊子或笔尖拨动即可切换。切换后,务必断电再重新上电,让屏幕驱动芯片重新初始化以识别新的接口模式。
2.3 宽电压供电(3.3V/5V)与Grove接口便利性
V3.0版本的另一个巨大优势是电源兼容性。模块内部集成了高效的电源管理电路,能够自动适应3.3V或5V的输入电压。这意味着:
- 无需电平转换器:你可以直接将5V的Arduino Uno与3.3V的传感器或其他模块混用在这块屏幕上,而不用担心烧毁屏幕。
- 供电灵活:无论你的主控系统是3.3V逻辑(如ESP32、STM32F103C8T6“蓝板”)还是5V逻辑(Arduino Uno、Mega),都可以直接供电。
Grove接口是Seeed Studio推广的一套标准化连接器系统,它把电源(VCC)、地(GND)和两条信号线封装在一个四针接口里。对于这块屏幕:
- 在I2C模式下,这四条线分别是:VCC, GND, SDA, SCL。
- 在SPI模式下,这四条线分别是:VCC, GND, SDA(此时用作MOSI), SCL(此时用作SCK)。而DC和CS信号则通过模块上的两个单独的排针孔引出(通常标记为DC和CS),你需要用杜邦线另行连接到主控板。RST(复位)引脚也可能作为一个排针孔存在。
- 使用配套的Grove线缆,可以防止电源反接(防呆设计),并且连接牢固可靠,非常适合原型开发和教育场景。
3. 软件驱动与库函数实战
要让屏幕亮起来并显示内容,你需要借助软件库。下面以最流行的Arduino平台为例,讲解两种最常用库的用法。
3.1 使用U8g2库:全能型选择
U8g2是一个功能极其强大的单色图形库,支持数百种显示控制器,包括SH1107。它的优势在于统一的API,一旦学会,可以驱动几乎所有单色屏。
- 安装:在Arduino IDE中,通过“库管理器”搜索“U8g2”并安装。
- 选择构造函数:这是最关键的一步。你需要根据你的连接方式(SPI/I2C)和具体引脚,在代码开头实例化一个
U8G2对象。- I2C连接示例:
#include <U8g2lib.h> #include <Wire.h> // I2C库 // 对于Grove OLED 1.12‘ (SH1107), I2C连接 U8G2_SH1107_128X128_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); // 参数解释:U8G2_R0(不旋转),复位引脚未接(用U8X8_PIN_NONE) - SPI连接示例(假设使用Arduino Uno硬件SPI):
#include <U8g2lib.h> // 对于SPI连接,需要指定DC和CS引脚。假设DC接D9, CS接D10, 复位接D8 U8G2_SH1107_128X128_F_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/ 10, /* dc=*/ 9, /* reset=*/ 8);
- I2C连接示例:
- 初始化与基本绘图:
核心概念:U8g2采用“缓冲区”机制。所有void setup() { u8g2.begin(); // 初始化显示屏 u8g2.setFont(u8g2_font_ncenB08_tr); // 设置字体 u8g2.setDrawColor(1); // 设置绘制颜色(1为白色,0为黑色) } void loop() { u8g2.clearBuffer(); // 清除内部缓冲区 u8g2.drawStr(0, 20, "Hello World!"); // 在坐标(0,20)处绘制字符串 u8g2.drawCircle(64, 64, 20, U8G2_DRAW_ALL); // 画一个圆 u8g2.sendBuffer(); // 将缓冲区内容发送到显示屏 delay(1000); }drawXxx()函数都是在内存中的缓冲区作图,最后调用sendBuffer()一次性更新到屏幕。这避免了屏幕闪烁。
3.2 使用Adafruit SSD1306/GFX库:经典组合
Adafruit的库同样非常流行,它分为Adafruit_SSD1306(驱动层)和Adafruit_GFX(图形层)两个部分。虽然名为SSD1306,但通过修改参数可以兼容SH1107。
- 安装:安装“Adafruit SSD1306”和“Adafruit GFX Library”。
- 配置与初始化:
#include <Adafruit_GFX.h> #include <Adafruit_SH110X.h> // 注意:需要专门支持SH110X的库,或使用修改后的SSD1306库 // 对于I2C,使用以下构造函数(地址0x3C, 无复位引脚) Adafruit_SH1107 display(128, 128, &Wire, -1); // 宽度,高度,Wire对象,复位引脚(-1表示无) // 对于SPI(假设DC=D9, CS=D10, RST=D8) // Adafruit_SH1107 display(128, 128, &SPI, D10, D9, D8); void setup() { Serial.begin(9600); if(!display.begin(0x3C, true)) { // I2C地址0x3C Serial.println(F("SH1107 allocation failed")); for(;;); // 死循环,初始化失败 } display.display(); // 显示Adafruit的启动logo delay(2000); display.clearDisplay(); display.setTextSize(1); display.setTextColor(SH110X_WHITE); } - 绘图与显示:
Adafruit库的特点:API直观,但部分高级功能(如多种字体)可能不如U8g2丰富。它采用“立即模式”思维,调用void loop() { display.clearDisplay(); display.setCursor(0,0); display.println("Hello World!"); display.drawCircle(64, 64, 20, SH110X_WHITE); display.display(); // 与U8g2的sendBuffer()作用相同 delay(1000); }display()后才真正更新屏幕。
库的选择建议:
- 追求功能全面、字体丰富、跨平台:选U8g2。
- 项目简单、喜欢Adafruit生态系统:选Adafruit SSD1306/GFX + SH1107补丁。
- 对于ESP32等平台,可能还有基于ESP-IDF的专用驱动,性能优化更好。
3.3 字体、图形与动画创建技巧
单色屏的显示效果全靠创意。以下是一些实用技巧:
- 字体:U8g2内置了海量字体(
u8g2_font_开头)。使用setFont()设置。注意字体高度会影响drawStr的y坐标基准(通常是字体的基线)。getStrWidth()函数可以获取字符串的像素宽度,用于居中计算:x = (displayWidth - u8g2.getStrWidth(text)) / 2。 - 图形:除了基本的点、线、圆、矩形,U8g2和Adafruit GFX都支持绘制位图。你需要先将图片转换为单色位图数组。可以使用在线工具(如
LCD Assistant)或Python脚本(如PIL库)进行转换。 - 动画:实现动画的关键是“清屏-重绘-显示”循环。避免在
loop()中直接使用delay()进行长时间等待,这会导致界面卡顿。推荐使用millis()进行非阻塞定时,实现平滑的帧刷新。unsigned long previousMillis = 0; const long interval = 100; // 刷新间隔100ms void loop() { unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= interval) { previousMillis = currentMillis; // 更新动画帧并重绘屏幕 updateAnimationFrame(); u8g2.clearBuffer(); drawFrame(); u8g2.sendBuffer(); } // 这里可以执行其他任务 } - 页面管理:对于多页面显示(如第一页温度,第二页湿度),可以定义一个状态变量(如
int currentPage = 0),根据变量值在drawFrame()函数中绘制不同的内容,通过按键或定时切换currentPage。
4. 典型项目应用与实战连接示例
理论说得再多,不如动手接一个。下面以两个最典型的开发板为例,展示完整的连接和代码。
4.1 连接Arduino Uno(I2C模式)
这是最简单的连接方式,充分利用了Grove的便利性。
- 硬件连接:
- 将Grove OLED模块背面的拨码开关拨到I2C一侧。
- 使用Grove四芯线缆,一端连接OLED模块,另一端连接Arduino Uno的I2C接口(通常标记为I2C或A4/A5附近)。
- 如果Arduino Uno上没有标准的Grove插座,你需要一个Grove转杜邦线模块,将线缆的四个引脚分别接到:VCC -> 5V, GND -> GND, SDA -> A4, SCL -> A5。
- 代码示例(使用U8g2库):
#include <U8g2lib.h> #include <Wire.h> U8G2_SH1107_128X128_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); void setup(void) { u8g2.begin(); u8g2.setFont(u8g2_font_ncenB10_tr); } void loop(void) { u8g2.clearBuffer(); u8g2.drawStr(10, 30, "Arduino Uno"); u8g2.drawStr(10, 60, "Grove OLED"); u8g2.drawStr(10, 90, "I2C Test"); u8g2.sendBuffer(); delay(1000); u8g2.clearBuffer(); u8g2.setFont(u8g2_font_7Segments_26x42_mn); u8g2.drawStr(15, 70, "1234"); // 绘制大型数字 u8g2.sendBuffer(); delay(1000); } - 上传与测试:将代码上传到Arduino Uno,屏幕应该会交替显示文本和大数字。
4.2 连接ESP32开发板(SPI模式)
ESP32性能强大,引脚多,适合用SPI驱动以获得更流畅的体验。
- 硬件连接:
- 将OLED模块拨码开关拨到SPI一侧。
- 电源:模块VCC连接ESP32的3.3V引脚。
- 地:模块GND连接ESP32的GND。
- SPI信号:使用杜邦线连接。
- 模块SCL (SPI CLK) -> ESP32的GPIO 18(常用SPI时钟引脚)。
- 模块SDA (SPI MOSI) -> ESP32的GPIO 23(常用SPI MOSI引脚)。
- 模块DC -> ESP32的任意GPIO,例如GPIO 4。
- 模块CS -> ESP32的任意GPIO,例如GPIO 5。
- 模块RST(如有)-> ESP32的任意GPIO,例如GPIO 2,或者接3.3V常高(如果模块内部已上拉)。
- 注意:ESP32的默认硬件SPI引脚是VSPI:CLK=GPIO18, MOSI=GPIO23, MISO=GPIO19。我们这里没有用到MISO。
- 代码示例(使用U8g2库,硬件SPI):
#include <U8g2lib.h> #include <SPI.h> // 构造函数:指定旋转、CS引脚、DC引脚、复位引脚 U8G2_SH1107_128X128_F_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/ 5, /* dc=*/ 4, /* reset=*/ 2); void setup(void) { u8g2.begin(); u8g2.setFont(u8g2_font_unifont_t_chinese2); // 设置中文字体(需包含相应字体文件) u8g2.setFontDirection(0); } void loop(void) { u8g2.clearBuffer(); u8g2.drawStr(0, 20, "ESP32 SPI驱动"); u8g2.drawStr(0, 40, "刷新率测试"); // 绘制一个移动的小方块,测试刷新速度 static int x = 0; u8g2.drawBox(x, 60, 10, 10); u8g2.sendBuffer(); x++; if (x > 118) x = 0; // 去掉delay,以最大速度刷新,观察方块移动是否流畅 // delay(10); } - 效果:上传代码后,你会看到文字和一个快速移动的方块。由于SPI的高速特性,动画会非常流畅。你可以尝试注释掉
delay(10),观察极限刷新率下的表现。
4.3 进阶项目构想
掌握了基础显示后,可以尝试这些项目:
- 网络时钟:用ESP32连接Wi-Fi,通过NTP协议获取网络时间,并显示在OLED上,可以加上日期、星期。
- 环境监测站:连接DHT11/DHT22温湿度传感器、BMP280气压传感器(均可通过I2C),在OLED上轮播显示实时环境数据。
- 系统状态监视器:用于树莓派,通过Python脚本(如
luma.oled库)读取CPU温度、内存使用率、IP地址并显示。 - 迷你游戏:利用128x128的分辨率,可以制作简单的贪吃蛇、Flappy Bird等像素游戏。
5. 常见问题排查与调试心得
即使按照教程操作,也难免会遇到屏幕不亮、显示乱码、通信失败等问题。下面是我在多年项目中总结的排查清单和心得。
5.1 屏幕完全不亮(无显示)
这是最常见的问题,请按顺序排查:
电源检查:
- 电压确认:用万用表测量模块VCC和GND之间的电压,确保在3.3V-5V之间。电压过低(<3V)可能无法启动。
- 电流能力:某些微型开发板(如某些ESP-01)的3.3V引脚输出电流能力有限(可能只有100-200mA),而OLED在点亮所有像素时瞬时电流可能较大(几十mA),可能导致电压被拉低。尝试使用外部稳压电源或开发板上电流输出能力更强的引脚(如USB直接输入的5V引脚,如果模块支持5V)。
- 接触不良:检查Grove线缆或杜邦线是否插紧,特别是GND线。虚焊或接触不良是硬件项目的头号杀手。
复位信号:
- 检查代码中是否正确地控制了RST引脚。对于SH1107,一个完整的复位序列通常是:拉低RST > 延时 > 拉高RST。如果硬件上RST引脚悬空,而代码中又配置了某个引脚为RST,可能会导致初始化失败。如果硬件未连接RST,在库的构造函数中应使用
U8X8_PIN_NONE或-1来指定无复位引脚。
- 检查代码中是否正确地控制了RST引脚。对于SH1107,一个完整的复位序列通常是:拉低RST > 延时 > 拉高RST。如果硬件上RST引脚悬空,而代码中又配置了某个引脚为RST,可能会导致初始化失败。如果硬件未连接RST,在库的构造函数中应使用
通信协议与模式开关:
- 双重确认:肉眼再次确认模块背面的拨码开关是否拨到了正确的位置(I2C或SPI)。这个开关非常小,容易看错。
- 软件匹配:确保代码中使用的库构造函数与硬件模式完全匹配。用I2C的构造函数去驱动SPI接线的屏幕,必然失败。
5.2 显示花屏、乱码或内容错位
初始化序列问题:
- SH1107和SSD1306的初始化指令大部分相同,但可能有细微差别。确保你使用的库明确支持SH1107,或者使用了正确的
SH1107构造函数。使用为SSD1306写的库而不修改参数,可能导致显示错位或乱码。 - 尝试在
setup()的begin()函数后,增加一个短暂的delay(100),给屏幕足够的初始化时间。
- SH1107和SSD1306的初始化指令大部分相同,但可能有细微差别。确保你使用的库明确支持SH1107,或者使用了正确的
缓冲区与显示区域不匹配:
- 确认你设置的显示缓冲区大小与屏幕物理分辨率一致。例如,对于128x128的屏幕,构造函数中的参数必须是
128X128。如果误用了128X64的构造函数,会导致只有上半屏或下半屏显示正常,另一半花屏。
- 确认你设置的显示缓冲区大小与屏幕物理分辨率一致。例如,对于128x128的屏幕,构造函数中的参数必须是
SPI模式下的DC引脚:
- DC引脚(数据/命令选择)是SPI通信的关键。必须在发送命令前拉低,发送数据前拉高。如果这个引脚接触不良或配置错误,屏幕会收到一堆被误解的指令,导致完全乱码。用逻辑分析仪或示波器检查DC引脚的波形是最直接的排查方法。
5.3 I2C通信失败(找不到设备)
地址扫描:
- 首先运行一个I2C扫描程序,确认屏幕是否被MCU识别。
// Arduino I2C扫描程序 #include <Wire.h> void setup() { Wire.begin(); Serial.begin(115200); Serial.println("I2C Scanner ..."); } void loop() { byte error, address; int nDevices = 0; for(address = 1; address < 127; address++ ) { Wire.beginTransmission(address); error = Wire.endTransmission(); if (error == 0) { Serial.print("I2C device found at address 0x"); if (address<16) Serial.print("0"); Serial.print(address,HEX); Serial.println(); nDevices++; } } if (nDevices == 0) Serial.println("No I2C devices found"); delay(5000); }- 常见的SH1107地址是0x3C(有时是0x3D)。如果扫描不到,检查接线。
上拉电阻:
- 这是I2C总线最经典的问题。如果总线上没有上拉电阻(约4.7kΩ),SDA和SCL线无法被拉高到逻辑1,通信会失败。测量方法:断电状态下,用万用表测量SDA和SCL引脚对VCC的电阻。如果电阻在4.7kΩ左右,说明已有上拉。如果电阻极大(兆欧级),则需要外接上拉电阻。
总线冲突:
- 总线上是否有其他设备地址冲突?或者有其他设备(如某些传感器)在通信时拉低了总线导致死锁?可以尝试暂时断开其他I2C设备,只连接OLED进行测试。
5.4 SPI通信问题(无显示或速度慢)
引脚映射错误:
- 确认代码中
MOSI、SCK、DC、CS的引脚号与硬件连接完全一致。ESP32等芯片的引脚功能可以重映射,但硬件SPI的默认引脚(如GPIO 23, 19, 18, 5for VSPI)通常最可靠。
- 确认代码中
软件片选(CS)管理:
- 在SPI通信中,必须在每次传输前拉低对应从设备的CS引脚,传输完成后拉高。如果使用硬件SPI,库函数通常会处理CS。但如果使用软件模拟SPI,或者多个SPI设备,你必须手动控制CS。常见错误是忘记在初始化序列后拉高CS,导致屏幕一直处于被选中状态,影响其他SPI设备。
时钟极性与相位(SPI Mode):
- SPI有4种模式(CPOL和CPHA的组合)。SH1107通常工作在Mode 0(CPOL=0, CPHA=0)。绝大多数库(如U8g2、Adafruit)默认使用Mode 0。如果你自己编写底层SPI驱动,务必设置正确。使用不正确的模式会导致数据采样错位,显示乱码。
实操心得:
- 备一个逻辑分析仪:一个几十块钱的简易逻辑分析仪(配合Sigrok软件)是调试I2C/SPI通信的“神器”。它能直观地显示总线上的时钟、数据波形,帮你一眼看出是地址不对、应答缺失,还是数据错误,比串口打印调试信息高效十倍。
- 库的版本很重要:不同版本的U8g2或Adafruit库,其构造函数或底层驱动可能有变化。如果从网上找的例程不工作,检查一下库的版本,或者查看库自带的例程(Examples)。
- 电源去耦:在OLED模块的VCC和GND之间,靠近引脚处焊接一个10uF的电解电容和一个0.1uF的陶瓷电容,可以显著改善因电源纹波导致的显示闪烁或随机复位问题,尤其是在使用长导线或面包板连接时。
- 初始化失败后的重试:在生产环境或长期运行的项目中,可以在代码中加入初始化重试逻辑。如果
begin()失败,不要立刻死循环,可以记录错误、延时几秒后重试,提高系统的鲁棒性。