ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Arduino OLED显示实战:从I2C接线到SSD1306库应用

Arduino OLED显示实战:从I2C接线到SSD1306库应用 一直想找一种显示效果好、接线又简单的屏幕来做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寸基本都是SSD13060.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 UNOVCC5VGNDGNDSCLA5SDAA4注意这里有两种排列顺序有的模块标的是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 SRAM128x32的显存是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是不是接A5SDA是不是接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上先扫描一遍地址这是建立调试信心的最快方式。接下来不管是显示传感器、做小游戏还是做仪表盘这套底子都会一直用得上。
返回列表