
做Arduino项目最痛苦的事情是什么我的答案是看不清状态。数码管只能显示几个数字串口监视器又必须连着电脑才能看产品一旦脱机运行就成了黑盒。所以这个系列走到第五篇我特别想聊聊Arduino UNO上最值得折腾的显示外设——OLED显示器。这篇文章会一次覆盖两种最常见的OLED模块0.96寸128x64和0.91寸128x32从硬件原理讲到接线、点亮、显示中文和图片最后把常见的坑都给你列出来。OLED这玩意儿在目前的Arduino项目里几乎可以说是标配外设。原因很简单功耗低、对比度高、可视角度大而且I2C接口版本只需要接两根数据线加电源一共四根线接线难度几乎为零。对于刚把LED、按键、数码管玩明白的初学者来说它是从“会点灯”过渡到“会做人机交互”最好的中间台阶。哪怕你之前完全没碰过I2C协议看完这篇也能在十分钟内让屏幕亮起来。1. 为什么偏偏选OLED显示方案怎么挑1.1 从数码管到OLED几种常见显示的对比很多朋友第一次做项目需要显示数据第一反应是“我能不能用数码管”。数码管确实简单一个TM1637模块四根线就能显示四位数字但它本质上只能显示数字和极少数字母想画个波形、显示个汉字门都没有。1602字符屏能显示字母数字了但模块体积大、需要背光、通常要占6根以上的I/O口如果还要调对比度那又是一个电位器的事。我直接用一张表把这几种方案的差别列出来大家选型的时候照着看就行显示方案接口复杂度功耗显示内容典型场景数码管低TM1637中数字、少量字母计时器、计数器、温度显示1602字符屏中6~8根线高背光ASCII字符文本菜单、调试信息LCD12864高8位并口或SPI中图形、汉字老式汉显方案TFT彩屏高SPI/并行高彩色图形游戏机、GUI界面OLED极低I2C四线低图形、汉字、动画仪表盘、菜单、可穿戴OLED最大的优势在于它是自发光器件不需要背光亮的地方才耗电黑的地方完全关闭。一个128x64的OLED全亮时的电流也就20mA左右对Arduino UNO的板载稳压器来说非常轻松。对比LCD那种“整个屏幕永远亮着黄绿背光”的方案OLED在电池供电的项目里优势特别明显。1.2 0.96寸与0.91寸两块屏到底差在哪标题里的“x2”说的就是这两块屏。它们应该是初学者最常买到的OLED模块0.96寸的分辨率是128x640.91寸的分辨率是128x32。从命名就能看出来分辨率差了一倍0.91寸的高度只有0.96寸的一半左右物理尺寸更扁。驱动芯片上绝大多数0.96寸和0.91寸模块用的都是Solomon Synotech的SSD1306通信协议是I2C或SPI。少部分0.96寸模块用的是SH1106驱动指令集和SSD1306基本兼容但内部显存寻址方式有细微区别。如果你用Adafruit SSD1306库驱动SH1106的屏幕花屏换成U8g2库选SH1106型号基本能解决。这也是为什么我在后面的库选择章节会特别强调U8g2的兼容性。那到底选哪块我的建议是做仪表盘、数据曲线、动画效果选0.96寸128x64可视信息量多一倍做状态指示灯、标题栏、单行滚动字幕选0.91寸128x32够了模块小一圈塞进外壳里不占地方。1.3 I2C还是SPI接口方案怎么选同型号的OLED模块通常会有I2C和SPI两种版本。I2C版本引脚少一般只有四个VCC、GND、SDA、SCL接Arduino UNO的时候插四根线就完事。SPI版本则是七个引脚除了电源地线之外还有CS、DC、RES、SDA、SCK虽然速度更快但占了UNO本来就紧张的I/O资源。我平时推荐新手无脑选I2C版本理由有三个第一接线简单不容易错第二I2C总线可以挂多个设备后面你想加个温湿度传感器、RTC时钟模块全部并联到A4和A5两个引脚上就行不用额外占引脚第三Arduino UNO的处理器主频只有16MHz跑128x64分辨率的话I2C的100kHz/400kHz速度用来刷新静态页面已经完全够用只有做全屏动画时才可能感觉到SPI更有优势。有一点要注意I2C总线上每个设备都有一个地址多个设备不允许地址重复。后面我会专门讲怎么扫描I2C地址这是排查OLED点不亮的核心手段。2. 硬件准备与接线点亮之前必做的几件事2.1 材料清单与模块选购要点动手之前先把东西备齐。需要的东西很基础一块Arduino UNO主板或者兼容板、一个OLED显示模块、四根母对母杜邦线如果加面包板就准备公对公或母对公以及一根USB数据线。OLED模块建议买焊好排针的版本免去自己焊接的麻烦。选购的时候别只盯着“OLED 0.96寸”这几个字一定要看模块背面的丝印或者商品详情页的接线图。部分模块板载了3.3V稳压芯片和电平转换电路这种模块可以放心接UNO的5V电源但也存在一些裸屏模块内部就是一颗SSD1306芯片标称3.3V供电只支持3.3V逻辑电平。这类模块如果直接接5V轻则显示异常发热重则烧掉芯片。判断方法很简单看模块背面有没有一颗类似1117的三脚稳压芯片有就放心接5V没有就老老实实接3.3V。2.2 大白话理解I2C通信I2CInter-Integrated Circuit总线是飞利浦公司发明的一种串行通信协议中文常叫“集成电路间总线”。它用两根信号线搞定所有通信SDA负责传数据SCL负责传时钟。主机通过SCL产生时钟节拍通信双方在时钟的上升沿或下降沿按位传递数据。总线上的每个设备都拥有一个唯一的7位地址主机发起通信时先发送设备地址只有地址匹配的从设备才会应答。这就是为什么OLED模块背面通常会标注“0x3C”或者“0x3D”——0x3C是SSD1306最常见的I2C地址0x3D则是把模块背面的地址电阻换掉之后得到的地址。另外补充一句0x3C完整的I2C地址字节是0x78因为7位地址左移一位后最低位是读写标志位但Arduino的Wire库里面写0x3C就行不用自己左移。UNO上的I2C引脚是A4SDA和A5SCL这两个引脚内部已经接了上拉电阻到5V实际由板载电路处理所以多数情况下不需要外接上拉电阻。如果你把OLED接到其他开发板比如ESP32或者STM32就得重新查数据手册确认引脚位置不能拿UNO的习惯硬套。2.3 接线表与地址扫描先用代码确认硬件接线非常直接对照下表操作OLED模块引脚接Arduino UNO说明VCC5V 或 3.3V先看模块是否板载稳压5V供电一般更稳GNDGND共地必须接SDAA4I2C数据线SCLA5I2C时钟线接好线之后别急着写显示程序先烧一个I2C扫描程序确认模块能被系统识别。这是排查地址和接线问题的第一利器代码本身很短#include Wire.h void setup() { Serial.begin(9600); Wire.begin(); 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.println(address, HEX); nDevices; } } if (nDevices 0) Serial.println(No I2C devices found); delay(2000); }打开串口监视器波特率设置9600正常的话每隔两秒会打印一行“I2C device found at address 0x3C”。如果提示“No I2C devices found”先别怀疑屏幕坏了按这个顺序排查确认VCC和GND有没有接反、SDA和SCL有没有接反、换四个杜邦线杜邦线接触不良是新手第一大坑、确认模块背面有无稳压芯片从而判断电源电压是否合适。我遇到过一个诡异的现象明明线都接对了扫描却找不到设备最后发现是杜邦线内部断裂换了根线立刻识别。3. 写第一个点亮程序从Hello OLED开始3.1 库的选择Adafruit还是U8g2Arduino的点亮代码其实是在调用别人写好的图形库。目前主流有两套Adafruit SSD1306库配Adafruit GFX库以及U8g2库。我自己两个都用过各有明显的倾向。Adafruit库的优点是代码结构清楚画点、画线、画矩形、写字都有现成函数和Processing的绘图API风格类似对新手上手极其友好。配套的GFX库提供了大量几何图形函数适合做仪表盘、柱状图。缺点是中文支持基本没有想显示中文只能自己取模而且对SH1106驱动的兼容性一般。U8g2库的优点是兼容性极强SSD1306、SH1106、SSD1309等几十种OLED驱动都能驱动并且内置大量字体其中包括部分中文字体。缺点是API设计比较绕需要理解“用户区缓存的clearBuffer/sendBuffer”机制不过理解了之后反而觉得刷新更可控。我的建议是新手先用Adafruit库把基本显示跑通后面需要显示中文或者遇到驱动兼容问题时切到U8g2。安装库的方法很简单Arduino IDE菜单栏选择“工具 - 管理库”搜索“Adafruit SSD1306”点击安装会弹出提示需要依赖“Adafruit GFX Library”一并安装即可。如果后续要用U8g2同样在库管理器里搜索“U8g2”安装。这里有个小提示如果你已经切换到VSCode PlatformIO记得在platformio.ini里声明lib_deps否则编译会报找不到头文件。3.2 点亮0.96寸的完整代码与逐行拆解下面这段代码是点亮0.96寸128x64 OLED的最小工程直接在Arduino IDE里新建文件粘贴编译上传即可#include Wire.h #include Adafruit_GFX.h #include Adafruit_SSD1306.h #define OLED_WIDTH 128 #define OLED_HEIGHT 64 #define OLED_ADDR 0x3C Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, Wire, -1); void setup() { Serial.begin(9600); if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { Serial.println(SSD1306 allocation failed); for (;;); } display.clearDisplay(); display.setTextSize(2); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.println(Hello OLED!); display.display(); } void loop() { }逐行解释几个关键点。Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, Wire, -1)的作用是构造显示对象四个参数依次是宽度、高度、使用的I2C接口对象以及复位引脚号。UNO上我们直接把复位引脚设置为-1表示不用软件控制复位靠模块上电自动复位少接一根线。display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)负责初始化屏幕SSD1306_SWITCHCAPVCC是电荷泵相关的参数意思是让SSD1306内部升压电路给屏幕供电0x3C是刚才扫描出来的I2C地址。返回值是布尔值如果初始化失败多半是地址不对或者接线有问题代码里用串口打印错误信息然后死循环避免程序在屏幕不可用的情况下继续跑出莫名其妙的结果。后面的操作顺序是固定的clearDisplay()清空缓冲区设置字体大小、颜色、光标位置然后用println()往缓冲区里写字最后必须调用display()才能真正把缓冲区内容刷到屏幕上。这是Adafruit库的典型使用流程所有绘图操作都作用于内存里的FrameBuffer只有display()才触发一次I2C传输。漏掉display()是最常见的“我明明写了文字屏幕却空白”的原因。3.3 切换显示分辨率的坑0.91寸怎么适配把代码从0.96寸移植到0.91寸看着只是把高度从64改成32实际上有几个容易忽略的点。首先构造对象时应该写Adafruit_SSD1306 display(128, 32, Wire, -1);这样图形库才知道显示区域只有32行像素。其次就算你把高度改成32如果代码里仍然画了超出32像素的内容Adafruit库并不会报错而是被自动裁剪掉。比如你在屏幕上写两行字每行8像素高字体大小为2这时第二行光标的y坐标如果设置成16第一行字高度是16像素两者正好占满32行没问题但如果继续使用64像素高度时的坐标布局内容就会被下半部分裁掉看起来像是显示不完整。0.91寸的高度只有0.96寸的一半控制好布局很重要。另外0.91寸的模块本身也有极少数使用SH1106驱动的用SSD1306库驱动时可能出现显示偏移或花屏。这时直接换U8g2库它的初始化代码更稳健这一点在下面的进阶章节里会再提到。4. 进阶玩法中文、图片、动态数据一样都不能少4.1 中文显示选择带中文字形的字体或手动取模Adafruit库默认不包含中文字库如果你想直接打印“你好”最省力的办法是切换到U8g2库。U8g2内置了一款名为u8g2_font_unifont_t_chinese的全字库字体体积较大但覆盖了常用汉字。用U8g2显示中文的完整代码如下#include Arduino.h #include U8g2lib.h #include Wire.h U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); void setup() { u8g2.begin(); u8g2.enableUTF8Print(); } void loop() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_unifont_t_chinese); u8g2.setCursor(0, 20); u8g2.print(你好OLED); u8g2.sendBuffer(); delay(1000); }注意几个细节。enableUTF8Print()必须调用否则中文会以乱码形式输出。字体必须设置成u8g2_font_unifont_t_chinese换成普通英文字体则中文打印不出来。另外Arduino IDE的.ino文件默认以UTF-8编码保存这没问题但如果你用其他编辑器保存成了GBK编码屏幕上就会显示乱码。如果你的项目需要显示特殊汉字而unifont字体里没有这个字那就只能手动取模。用取模软件比如PCtoLCD2002生成该汉字的字形点阵再通过u8g2.drawXBM或者自定义字体API画出来。这种方式灵活度高但工作量大我个人只有在做产品原型需要特定字体风格时才会用。4.2 显示图片取模软件参数与drawXBM使用OLED是单色屏幕显示图片前需要把图片转换成二值位图数组。我常用的流程是准备一张128x64像素的BMP图片用Image2Lcd或PCtoLCD2002打开设置输出为C语言数组颜色模式选单色取模方式选择“逐行式”。生成出来的数组长这样const unsigned char logo[] PROGMEM { 0x00, 0x00, 0x3C, 0x00, ... };绘制图片时用drawXBMdisplay.clearDisplay(); display.drawXBM(0, 0, 128, 64, logo); display.display();drawXBM的四个参数分别是起点x坐标、起点y坐标、图片宽度、图片高度。这里最值得说的坑是取模方式。XBM格式要求每个字节的MSB对应最左侧像素即“逐行式”。如果取模软件默认选了“列行式”或“字节倒序”画出来的图像就会错乱我最初做这个的时候捣鼓了半小时才反应过来是取模参数的问题。换一句话说用Image2Lcd的“扫描方式”设置务必核对清楚别贪快跳过。4.3 动态数据可视化柱状图与波形曲线的实现思路OLED的常见价值是显示传感器数据但光把数字打出来还是太单调。以0.96寸屏幕为例完全可以用它的图形能力画出柱状图和滚动波形。柱状图最简单读取一个模拟量映射到一定高度然后用fillRect画实心矩形。例如int raw analogRead(A0); int barHeight map(raw, 0, 1023, 0, 48); display.fillRect(20, 64 - barHeight, 20, barHeight, SSD1306_WHITE); display.display();波形图稍微复杂一点核心思路是维护一个长度为128的数组每次采样后把所有数据往左移一位新数据放到末尾然后逐点绘制。这段代码虽然每次都要全屏清理但在数据变化不快时足够用int samples[128]; void updateWave() { for (int i 0; i 127; i) { samples[i] samples[i 1]; } samples[127] analogRead(A0) / 16; // 映射到0~63 display.clearDisplay(); for (int i 0; i 128; i) { display.drawPixel(i, 63 - samples[i], SSD1306_WHITE); } display.display(); }如果你想在波形曲线下面同时显示数值和状态图标建议把整个页面按“状态栏区 图形区”划分固定区域刷新。4.4 刷新策略别再全屏清屏了做动画或者动态数据展示时最容易遇到的问题就是屏幕闪烁。闪烁的根源是频繁clearDisplay()清空整块缓冲区再重新画导致像素状态反复翻转。解决思路有两个一是降低刷新率把数据采样和屏幕刷新控制在10~15Hz人眼已经感觉不到卡顿二是做局部更新比如只清除旧柱状图所在的矩形区域然后在新位置重画而不是每次全清。Adafruit库的底层其实是一个1024字节的FrameBuffer128x64/8你对屏幕的所有操作都在这个缓冲区里进行最后通过display()一次性同步到硬件。这个架构本身没有闪烁问题闪烁只可能由用户代码里的反复全清导致。所以写动态界面时养成习惯哪个区域变了就清哪个区域和做网页DOM更新是一个思路。另外提一句如果手头暂时没有硬件也可以先到Wokwi这类在线仿真平台搜“Arduino Uno SSD1306”的示例仿真平台能模拟I2C OLED显示适合先用软件把逻辑跑通再买模块上手。5. 常见问题速查与避坑实录5.1 最常见的6个问题及排错思路这几类问题真的是群里问烂了的我直接做成表格对应操作即可症状可能原因解决办法屏幕完全不亮电源接反、接线虚接、模块已烧检查VCC/GND换杜邦线确认模块电压屏幕亮但全白初始化失败、I2C地址错误运行I2C扫描确认0x3C还是0x3D显示花屏乱码SDA/SCL接反、线过长干扰对调两根信号线缩短杜邦线程序卡死、屏幕无响应begin()返回false进入死循环看串口报错重点查接线和供电显示内容偏移或只有一半分辨率设置错误、SH1106驱动构造对象改128x32/128x64换U8g2屏幕很暗对比度低供电电压不足、对比度寄存器被改改接5V调setContrast其中卡死那个问题最隐蔽。很多人的代码在begin()失败后写了一个while(1)死循环之后屏幕不亮、串口不输出看起来完全“死机”。碰到这种情况先检查begin()那句有没有串口错误打印或者临时注释死循环改成delay(1000)让代码继续往下跑往往能把问题暴露出来。5.2 三个容易被忽略的接线与供电细节第一引脚顺序。不同厂家OLED模块的引脚顺序不一定一致有的是“VCC GND SDA SCL”有的是“GND VCC SCL SDA”。我接过一块标注和常规完全相反的模块幸好插之前看了一眼丝印否则VCC和GND接反大概率直接冒烟。接线前一定核对模块上的文字标注而不是想当然按颜色插。第二5V与3.3V的选择。前面提过要看模块背面有没有稳压芯片。但即便是板载稳压的模块我实测下来5V供电会比3.3V更稳屏幕亮度也更高。原因是OLED内部电荷泵需要一定电压才能驱动像素点亮供电略高时对比度更好。当然前提是模块明确支持5V输入。第三USB上传时拔掉OLED。Arduino UNO通过USB转串口芯片下载程序正常情况下OLED接在I2C引脚不影响下载。但如果遇到“上传卡死在Writing”或者“avrdude: stk500_getsync() attempt”这类老问题可以先把SDA和SCL杜邦线拔掉再上传传完再接回去。有些OLED模块在上电瞬间会拉低信号线电平干扰了下载时序拔线是最直接的解法。5.3 排查流程总结屏幕不亮时按这个顺序检查很多新手一上来就问“是不是屏幕坏了”我的排查顺序是固定的先量电压看VCC和GND之间有没有5V再跑I2C扫描确认总线上有没有设备接着看驱动把Adafruit库自带的ssd1306_128x64_i2c示例直接编译上传如果官方示例能亮问题就在你自己的代码如果官方示例也白屏再换U8g2的示例试一下排除SSD1306库和SH1106驱动的兼容问题。这套流程走完90%的问题都能定位。真正坏掉的OLED我见过很少大多数是接线、地址、库选择这三件事没搞定。最后再分享一点个人感受。玩OLED最有趣的地方不在于点亮它而在于把128x64这么小一块屏幕用明白。我第一次用它画滚动波形的时候突然理解了为什么嵌入式设备的界面设计可以这么讲究空间有限反而倒逼你聚焦核心信息。做温湿度计、做时钟、做桌面小仪表OLED都是投入产出比最高的外设。希望这篇能帮你少走点弯路直接看到实实在在的画面在屏上亮起来。