☰
Arduino OLED显示实战:从I2C接线到SSD1306库应用
2026/10/3 1:03:11 网站建设 项目流程

一直想找一种显示效果好、接线又简单的屏幕来做Arduino项目展示,最终我固定在OLED上。上一篇我们聊完基本GPIO和串口通信之后,很多朋友在评论里催更,问得最多的就是显示方案。这次索性把OLED在Arduino UNO上的用法一次说透,而且直接给两套完整可跑的使用方法,分别对应市面上最常见的两种屏:0.96寸128x64和0.91寸128x32。这两块屏核心驱动都是SSD1306系列,协议走的都是I2C,但实际调试起来小坑不少,我会把从选型、接线、库安装到最终显示图形的完整过程全部拆开讲。

这篇内容非常适合刚接触显示模块的Arduino新手,也适合手里正好囤了屏幕却一直没点亮的老手。看完之后你不会只停留在“抄一段能亮的代码”,而是能够理解OLED为什么用I2C、地址是怎么回事、为什么有时候屏幕白屏、为什么加了显示功能之后程序反而卡死,这些问题是群里出现频率最高的几个,我会全部结合自己的实操经历来聊。

1. 选型思路:0.96寸和0.91寸怎么选才不踩坑

先说说我为什么把这两块屏放在一起讲。很多新手第一次接触OLED,看到淘宝上那么多型号直接懵了,0.96寸、0.91寸、1.3寸、1.54寸,还有SPI接口和I2C接口的区别,价格从几块到几十块都有。我比较推荐入门阶段先用0.96寸I2C版本,因为它的库支持最成熟、案例最多、卖得也最多。0.91寸则是很多小体积项目里的常客,分辨率只有128x32,高度是0.96寸的一半,适合显示状态栏、传感器读数这类单行或双行信息。

1.1 尺寸、分辨率与驱动芯片的对应关系

0.96寸OLED常见的分辨率是128x64,也就是横向128个像素点,纵向64个像素点。0.91寸OLED常见的分辨率是128x32。虽然都是SSD1306驱动,但屏幕内部像素排列不一样,初始化的时候就必须告诉库“我这块屏是64行还是32行”,这一点非常容易出错,后面我会详细演示。

驱动芯片方面,0.96寸基本都是SSD1306,0.91寸则有一些批次用的是SSD1315。SSD1315和SSD1306在绝大多数情况下可以当作兼容芯片使用,Adafruit_SSD1306库和U8g2库都能正常驱动。如果你用的是U8g2库,甚至可以直接选择UNIVISION系列构造函数来兼容这些屏幕。

1.2 接口方案:I2C和SPI选哪个更适合自己

OLED模块一般有两种接口,一种是I2C,引出四个引脚:VCC、GND、SCL、SDA。另一种是SPI,通常会多出几个引脚,比如CS、DC、RESET。Arduino UNO的SPI引脚在D10到D13,而I2C引脚固定在A4和A5。从我个人的使用习惯来说,除非你特别需要高速刷新,否则I2C优先。

为什么?第一,I2C只占两个引脚,UNO本来就引脚紧张,省下来的引脚可以用来接传感器、按键、舵机。第二,SPI虽然刷新速度更快,但对于大多数静态显示和低频率刷新的画面,两者肉眼几乎看不出差别。第三,I2C接线极其友好,四根线一插就完事,SPI接错一个引脚屏幕就没反应,排错成本高。所以这篇博文的两套使用方法,我都采用I2C方案。

1.3 供电与电压兼容问题

OLED模块的逻辑电压一般是3.3V,但绝大多数模块板载了稳压电路,可以直接用5V电源供电,也就是说你可以直接把VCC接到UNO的5V引脚,不用担心烧屏。不过我见过个别精简版模块没有5V兼容设计,稳妥起见,可以在买之前问清楚或者直接选标注“5V/3.3V兼容”的模块。

另外,供电电流也没有想象中那么大,正常显示时背光亮起,整个屏幕功耗大约在20mA到40mA之间,USB供电完全扛得住。但如果你的项目里还有舵机、电机这类大电流设备,我强烈建议把屏幕的电源独立出来,或者在UNO的电源入口并联一个大电容。我踩过真坑:屏幕和舵机共用一个电源,舵机一动作,屏幕要么变暗要么直接重启,后来分开供电才解决。

2. 硬件接线与屏幕驱动原理

接线是整个过程中最简单也最容易出问题的一环。在开始接线之前,先把UNO断电,按照下面的方式把屏幕和UNO连起来。

2.1 接线表与引脚对应关系

以0.96寸I2C版OLED为例,模块上一般会标出对应的引脚名称:

OLED引脚接到Arduino UNO
VCC5V
GNDGND
SCLA5
SDAA4

注意这里有两种排列顺序,有的模块标的是SCL和SDA,有的标的是SCK和SDA,含义一样。如果是0.91寸I2C版,接线完全一致。

接好之后,如果你的模块是带引脚的,直接插面包板就行;如果是小熊板或者说邮票孔模块,需要用杜邦线或者烙铁焊上排针再接。

2.2 A4和A5为什么是I2C引脚

UNO上的A4和A5不只是模拟输入引脚,它们还承担了I2C总线的SDA和SCL功能。这是因为ATmega328P芯片内部有专门的I2C硬件模块,I2C通信通过这两根线在设备和主机之间传递数据。SCL是时钟线,由主机也就是UNO产生;SDA是数据线,双向传输数据。

I2C协议的特点是所有设备共享两根线,每个设备有一个唯一地址。主机通过地址来选择和哪个设备通信,所以你在代码里看到的0x3C,就是这块OLED屏幕在I2C总线上的地址。SSD1306最常见的I2C地址是0x3C,部分模块通过背后的电阻焊盘改成0x3D。

2.3 关于上拉电阻要不要手动加

I2C总线要求SDA和SCL都有上拉电阻,这样空闲时两根线保持高电平。UNO内部已经有上拉电阻,但强度偏弱。大多数OLED模块的PCB上已经集成了上拉电阻,所以你直接用杜邦线短距离连接没有任何问题。但如果你用了很长的延长线或者总线上挂了很多I2C设备,就可能出现通信不稳定,这时候可以自己在SCL和SDA上外接两个4.7kΩ的电阻到3.3V或者5V,能让波形更稳定。

我之前挂过一组测试,总线上同时接了OLED和BME280传感器,线材总长度超过半米,一开始每隔几秒屏幕就闪烁一下,后来加了上拉电阻并降低I2C速度,问题消失。所以当你遇到诡异现象时,优先怀疑总线电气特性,而不是代码。

2.4 屏幕显示的基本原理:SSD1306与显存

SSD1306并不是简单的“你让它画什么它立刻显示什么”,它内部有一块和像素区域一一对应的显存。对128x64的屏幕来说,显存大小是128x64个位,也就是每8个像素点组成了一个字节,整块显存总共1024字节。你在代码里调用绘图函数,实际上是在修改这块显存里的数据,修改完之后需要主动发送“更新显存”的命令,屏幕才会把显存内容刷新到像素上。

这解释了为什么Adafruit库每次画完东西都要调用display.display(),U8g2库则要调用u8g2.sendBuffer()。如果漏掉这一步,代码看着没毛病,屏幕上却什么都没有,这是新手最容易犯的错误之一。

SSD1306内部把屏幕按页划分,128x64被分成8页,每页对应纵向8行像素,页地址从0xB0到0xB7。当你写入数据时,内部指针从指定的页和列开始,每写一个字节指针自动加一。库函数把这些过程封装好了,但了解这个机制你才能理解“为什么分辨率写错了屏幕就会乱掉”。

3. 软件准备:IDE配置与库的选用

代码能不能跑起来,一半取决于库选得对不对。Arduino IDE里的库管理器可以很轻松地安装这些库,但不同库之间的差异很大,用不对地方会出现各种莫名其妙的问题。

3.1 安装Adafruit SSD1306库和Adafruit GFX库

第一个要讲的是Adafruit方案。Adafruit是Arduino生态里出库大户,它的SSD1306库封装得很清晰,而且依赖Adafruit GFX图形库,后者提供了画线、画圆、画矩形、绘制位图等一系列函数,很适合用来做图形界面。

打开Arduino IDE,点击工具 -> 管理库,搜索“Adafruit SSD1306”,安装第一个结果。它会提示你需要安装依赖库,确认安装Adafruit GFX Library即可。如果你的IDE版本比较老,建议先升级到1.8.19或2.x版本,否则库管理器可能搜索不到最新包。

有一点要注意,Adafruit SSD1306库默认支持很多种屏幕尺寸,但初始化时尺寸是由你在代码里创建的显示器对象决定的,和库本身没关系。很多人在用128x32的0.91寸屏时,忘记把高度参数改成32,直接沿用例程里的128x64,结果屏幕显示内容一半溢出、另一半花屏。

3.2 安装U8g2库,一个库通吃几乎所有单色屏

第二个方案是U8g2库,全称是Universal 8bit Graphics Library 2nd edition。作者Oliver Kraus把几乎所有常见的单色OLED、LCD都纳入了支持列表,而且内置了大量字体,包括很多中文点阵字体。U8g2的代码风格和Adafruit不太一样,它通过构造函数来声明你要驱动的屏幕型号。

在库管理器里搜索“U8g2”,安装名为“U8g2”的库即可。U8g2的兼容性极强,同一块0.96寸屏,它有六七个不同的构造函数可选,分别对应硬件I2C、软件I2C、硬件SPI等不同接线方式。初次上手建议优先选择名字里带HW_I2C的版本,比如U8G2_SSD1306_128X64_NONAME_F_HW_I2C。

3.3 I2C地址扫描:比盲目改代码更有效的一步

不管用哪个库,我建议你第一次接上屏幕之后,先不急着写显示代码,而是先跑一个I2C地址扫描程序,确认UNO能正确“看到”屏幕。这个程序来自Wire库自带示例,文件 -> 示例 -> Wire -> i2c_scanner,直接用就行。它会扫描总线上所有设备地址,并在串口监视器打印出来。如果屏幕上明明接好了,串口却没有输出任何地址,那大概率是接线、供电或者模块本身的问题。先跑扫描器能省下后面一大半排查时间。

4. 两套可复用的示例:从显示文字到画图形

现在进入核心部分。我准备了两个完整示例,第一个用Adafruit库驱动0.96寸128x64屏,第二个用U8g2库驱动0.91寸128x32屏。两套代码都验证过,里面包含了初始化、显示文本、绘制基本图形和实时刷新,可以直接抄。

4.1 示例一:Adafruit库驱动0.96寸128x64屏

先看代码:

#include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); void setup() { Serial.begin(115200); if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println(F("SSD1306 allocation failed")); for (;;); } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.println(F("Hello OLED!")); display.println(F("128x64 I2C")); display.display(); delay(2000); drawShapes(); } void loop() { display.clearDisplay(); display.setTextSize(2); display.setCursor(20, 20); display.print(millis() / 1000); display.println(F(" sec")); display.display(); delay(500); } void drawShapes() { display.clearDisplay(); display.drawRect(0, 0, 128, 64, SSD1306_WHITE); display.drawLine(0, 0, 127, 63, SSD1306_WHITE); display.drawCircle(64, 32, 20, SSD1306_WHITE); display.fillRect(100, 10, 20, 10, SSD1306_WHITE); display.display(); delay(2000); }

代码细节我拆开讲一下。Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET)这行创建了一个显示器对象,其中&Wire表示使用UNO的硬件I2C。OLED_RESET设置为-1,意思是不使用复位引脚,因为SSD1306本身有上电复位逻辑,UNO的I2C版模块不需要额外接RST。如果你手里的模块引出了RESET引脚,也可以把它连接到UNO的D9并在代码里把-1改成9。

display.begin(SSD1306_SWITCHCAPVCC, 0x3C)有两个参数,第一个SSD1306_SWITCHCAPVCC表示由3.3V或者5V供电,让芯片内部电荷泵升压给屏幕供电;第二个0x3C是I2C地址。如果你的屏幕地址是0x3D,这里改成0x3D即可。

然后就是常见的操作序列:clearDisplay()清空显存,setCursor(x, y)设置起点坐标,setTextSize(1)设置字号,println()写入文本,display()刷新屏幕。注意display()被调用之前,字符只是在显存缓冲区里,屏幕上不会出现任何变化。

drawShapes()函数展示了几个最常用的绘图函数,drawRect画空心矩形、drawLine画直线、drawCircle画圆、fillRect画实心矩形。这些函数的参数都很直观,第一个参数是x坐标,第二个是y坐标,后面是宽度、高度或半径。修改这些数字你马上能在屏幕上看到效果。

4.2 示例二:U8g2库驱动0.91寸128x32屏

用U8g2时的代码风格完全不同,看下面这段:

#include <Arduino.h> #include <U8g2lib.h> #include <Wire.h> U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); void setup() { u8g2.begin(); u8g2.setFont(u8g2_font_6x10_tf); u8g2.clearBuffer(); u8g2.drawStr(0, 10, "Hello 128x32"); u8g2.drawFrame(0, 16, 128, 16); u8g2.drawBox(4, 20, 30, 8); u8g2.sendBuffer(); } void loop() { u8g2.clearBuffer(); u8g2.setCursor(10, 20); u8g2.print(millis() / 1000); u8g2.sendBuffer(); delay(200); }

这里定义了一个U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C对象,从名字就能看出来它适配的是128x32分辨率、SSD1306芯片、硬件I2C。U8G2_R0是旋转方向参数,0表示不旋转;U8X8_PIN_NONE表示不使用复位引脚。

U8g2里有一个概念叫“缓冲模式(F)”,代码里的_F_就代表Full Buffer,整块显存被完整保存在内存中。另一个常见模式是页面模式(1_或_2),它为了节省内存分多次刷新。UNO有2KB SRAM,128x32的显存是512字节,128x64的显存是1024字节,用完整缓冲模式都放得下,所以直接用_F_没问题。

setFont选择字体,drawStr直接显示字符串,drawFrame画空心矩形,drawBox画实心矩形。U8g2的函数命名比Adafruit更短,也稍微抽象一点,习惯之后还挺顺手。最后同样要记得sendBuffer()才会把内容推到屏幕。

如果你用的是0.91寸但实际上是128x64的像素,或者想把这套代码改成0.96寸,只需要把构造函数换成U8G2_SSD1306_128X64_NONAME_F_HW_I2C,代码顶部再调整一下字体大小和坐标即可。U8g2用构造函数区分屏幕型号,代码主体不用大改。

4.3 两个库的对比和选型建议

如果你刚入门,我会更推荐先学Adafruit库,因为它的函数命名更接近日常思维,画圆就是drawCircle,画线就是drawLine,字符显示用println,和串口打印非常像。U8g2的优势在于字体系统强大,中文、大字号、小字号选择丰富,适合做界面比较复杂的项目。但U8g2在UNO上编译时,如果选了大字体,Flash空间会比较紧张,所以用UNO时尽量选u8g2_font_6x10_tf这类小字体,或者考虑换成支持中文字库的芯片平台。

5. 常见问题与排查实录

驱动OLED遇到的坑,我来来回回都遇到过,这里挑几个出现频率最高的,连同我的排查思路一起写下来。

5.1 屏幕完全不亮或白屏

先检查供电和接线,这是最基础也最容易出问题的一步。用万用表量VCC和GND之间有没有5V电压,SCL是不是接A5,SDA是不是接A4。确认硬件没问题后,下载I2C扫描程序,看能不能扫描到0x3C或0x3D这个地址。如果扫描不到,逐根线重新插拔一次,或者换一块屏幕试。有一回我折腾了一个小时,最后发现是杜邦线内部断了,换根线就好了。

5.2 屏幕能亮但只显示几条横线或花屏

这种情况大概率是分辨率设置错误。你用的是128x32屏,但代码里初始化的高度是64,或者反过来。SSD1306的显存分页机制对分辨率非常敏感,行数不对会导致写入的数据落在错误的行上,表现出来的就是花屏、重影、显示位置偏移。把SCREEN_HEIGHT改成32,或者把U8g2的构造函数换乘128X32版本,问题就解决了。

复位引脚也会引起类似问题。如果用Adafruit库时OLED_RESET设置成了一个未连接的引脚,而模块其实没引出来,这时候初始化可能不成功,所以要严格保持reset参数和实际接线一致。

5.3 加入OLED之后整个程序卡死

这类问题我在群里见过不少,最常见的原因是I2C总线上有设备地址冲突,或者某根线接触不良导致I2C通信挂起。排查方法是把其他I2C设备全部断开,只留OLED,再看程序是否正常运行。如果正常了,说明是地址冲突或总线负载过大,考虑给不同设备配置不同地址,或者换用软件I2C方式驱动其中一个设备。

另一个隐蔽原因是display.begin()失败后进入死循环。很多示例代码在初始化失败时会写for(;;),一旦运行到这里程序就会卡死。调试阶段建议把死循环换成while(1){Serial.println("init failed"); delay(1000);}或者直接注释掉,先让程序能跑起来,再判断问题出在哪。

5.4 字符能显示但亮度很暗

亮度问题一般不是代码能解决的,先看OLED的VCC电压是否偏低,或是供电电流不足。部分屏幕带对比度寄存器,你可以尝试通过I2C命令提高0x81对比度设置后的系数值,但很多库默认已经设置了较高对比度,手动调整空间有限。如果屏幕用了很久变暗,那就是硬件老化了,换屏更实际。

5.5 中文字库编译失败

U8g2支持中文,但UNO只有32KB Flash存储空间,部分中文字体动辄几十KB,编译时会直接爆掉。如果你非要在UNO上显示中文,可以选u8g2_font_unifont_t_chinese2这类针对性字体,或者自己用取模工具生成小字库。如果项目不强制用UNO,换用ESP32这类Flash更大的开发板,中文字体随意放。

6. 基于OLED的后续玩法扩展

屏幕亮起来只是第一步。OLED在Arduino项目里能做的事情非常多,我简单列几个方向,都是我自己做过或者见别人做过比较靠谱的玩法。

把OLED和DHT11温湿度传感器组合,做一个桌面环境监测器。5分钟能搞定:DHT11接数字引脚,读取温度湿度,OLED每2秒刷新一次数据。把上次代码里的display.println("Hello")换成传感器读数,整个项目就活了。

再进一步,可以把OLED接到DS3231或者DS1302时钟模块上,做一个带温度显示的桌面时钟。注意时钟模块在I2C总线上通常也有地址,比如DS3231的地址是0x68,和OLED的0x3C不会冲突,两个设备可以共用A4和A5两根线。

如果你有UBLOX GPS模块或者MPU6050姿态传感器,OLED还适合用来显示定位信息和角度数据。把数据实时显示在屏幕上,比打开串口监视器看数的体验要直观得多。我的做法是先在电脑上用串口把数据格式DEBUG稳定,再交给OLED刷新,这样能避免高频调用显示函数导致主程序阻塞。

图形接口还能做得更花哨一点,比如绘制电池电量图标、信号强度图标、进度条。这些本质上都是drawRect、drawTriangle、fillCircle这些基本函数的组合。你可以在loop()里根据数值动态调整矩形宽度,实现类似于手机电池电量的动画效果,代码量不超过20行。

7. 我的使用心得与最终建议

OLED驱动说到底就是一个I2C设备的操作,整个过程最核心的其实不是代码,而是“先确认硬件,再怀疑代码”的排查思路。Adafruit库和U8g2库都封装好了底层,所以新手不用害怕,按顺序跑通一次,后面换任何型号的OLED,基本原理都是一样的。

我个人的习惯是,如果是快速验证想法,直接上Adafruit SSD1306库,它的例程多、函数直观,画个矩形画个圆基本不用查文档。但如果项目需要多种字体切换、更多排版控制,或者你可能之后要换其他屏幕型号,我会优先选U8g2。它的构造函数看起来唬人,其实就是一个字符串选择器,选对了屏幕型号其他都好说。

最后建议你把这篇文章里的第一个扫描程序好好利用起来。不管什么I2C设备,接到UNO上先扫描一遍地址,这是建立调试信心的最快方式。接下来不管是显示传感器、做小游戏还是做仪表盘,这套底子都会一直用得上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询