ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SPI OLED改成I2C使用:SSD1306接口切换与GPIO节省指南

SPI OLED改成I2C使用:SSD1306接口切换与GPIO节省指南

把SPI OLED改成I2C使用,听起来像是不务正业的折腾,但真遇到开发板的SPI接口被占满、引脚不够用的时候,这招能帮你省出好几根GPIO,甚至省掉一次买新屏的等待。这几天我翻出一块吃灰的7针0.96寸OLED,丝印上赫然写着SPI接口,可手头这颗主控的硬件SPI总线已经被另一个外设模块占用,剩下的引脚刚好能凑出一路I2C。本来想直接下单买一块4针I2C屏,转念一想:SSD1306这颗驱动IC本身就同时支持SPI和I2C两种协议,同样是这块屏,为什么非要买新的?于是就有了这次改造。结果不但省了钱,还顺带把I2C总线上挂显示器的玩法摸了个透。这篇内容就完整记录整个改造思路、硬件接线和代码适配过程,手里有同样7针SPI OLED、又一直想并入I2C总线的朋友可以参考。

1. 为什么要把SPI OLED改I2C,值不值得折腾

1.1 省引脚是最直接的动力

先说最现实的理由:引脚不够用。7针SPI OLED正常工作需要占用4根信号线,分别是SCLK(时钟)、MOSI(数据)、DC(数据/命令选择)、CS(片选),再加上VCC和GND,总共6根线。而I2C只需要SCL和SDA两根信号线,加上电源也不过4根线。别小看省下来的这两根线,在一些封装紧凑的开发板上,GPIO资源紧张到需要精打细算,少占用一个引脚就多一分灵活性。

举个实际场景:某项目里已经有一个SPI接口的传感器模块挂在硬件SPI总线上,按照SPI的规范,同一总线上虽然可以挂多个设备,但每个设备都要独占一个CS引脚,而且驱动代码里还要处理设备切换。如果这时候再把OLED也挂到SPI总线上,CS引脚又少一个,代码也多了不少判断逻辑。把OLED改成I2C之后,就能把它安排到另一条总线上,跟传感器完全分开,互不干扰。

另一个常见场景是MCU的硬件SPI引脚被复用功能占了。很多芯片的SPI引脚和PWM、ADC等外设功能是复用的,一旦这些引脚被其他功能占用,硬件SPI就用不了了。这时候I2C的优势就体现出来了,I2C在多数芯片上都有独立引脚,有些芯片甚至可以通过软件映射到任意GPIO(比如ESP32),灵活性高得多。

1.2 SSD1306天生支持I2C,硬件基础早就备好了

很多人以为接口协议是屏幕出厂时焊死的,改不了。实际上,0.96寸OLED大部分用的都是SSD1306驱动芯片,这颗芯片在设计之初就内置了6800并行、SPI、I2C三种接口模式。市面上所谓7针SPI屏和4针I2C屏,核心硬件几乎没有差别,区别只在于PCB上几个配置电阻的焊接方式不同。

SSD1306芯片上有专门用于选择接口模式的引脚,通常叫BS0、BS1、BS2(不同批次芯片叫法可能略有差异,有的简化为PS)。这三个引脚的电平组合决定了芯片工作在哪种接口模式。换句话说,只要你能控制这三个引脚的电平状态,就能让同一颗芯片在SPI和I2C之间切换。这是整个改造方案最核心的技术依据。

知道了这个原理,改造的思路就清晰了:先查看模块板的BS配置方式,把三个引脚的电平从SPI模式改成I2C模式,然后按照I2C的引脚定义重新接线,最后把代码里的驱动库从SPI版换成I2C版。整个过程其实不复杂,难点全在第一步——怎么改模块板上的BS配置,后面我会展开细说。

2. 动手前的关键:确认你的模块能不能改

2.1 先看清7针OLED的引脚定义

不同厂家出的7针OLED,引脚顺序可能不一样,这是改造前必须先确认的第一件事。绝大多数0.96寸128x64的7针屏,引脚定义是下面这个顺序:

引脚序号常见丝印原始SPI功能改I2C后的功能
1GND电源地电源地(不变)
2VCC电源正极电源正极(不变)
3D0SCLK(SPI时钟)SCL(I2C时钟)
4D1MOSI(SPI数据)SDA(I2C数据)
5RES复位复位(不变)
6DC数据/命令选择SA0(I2C地址选择位)
7CS片选接高电平(I2C模式下不使用)

注意,有些模块的丝印会把D0标成SCL、D1标成SDA,这种模块往往出厂就支持I2C模式,只是用7根针把接口全部引出来了,这类模块改造起来非常方便,只需要重新定义接线就行,连BS电阻都不用动。还有极少数模块的第7脚不是CS而是NC(空脚),遇到这种更要仔细看丝印,别想当然。

还有个容易忽略的点:VCC到底接3.3V还是5V。SSD1306芯片本身的逻辑供电范围是1.65V到3.3V,但很多OLED模块板载了稳压或者电平转换电路,使得它们可以直接在5V下工作。拿到模块后先看丝印标注或者商品描述,如果标注了5V,那接5V没问题;如果没标注,老老实实接3.3V最保险,尤其是那些从裸屏加上转接板DIY出来的模块,直接上5V容易烧芯片。

2.2 检查模块背面有没有模式切换配置

这是整个改造中最关键的一步。拿到7针屏,翻过来看PCB背面,通常有三种情况:

第一种是背面印着PS、MODE或者BS0/BS1/BS2这几个字符,旁边还有焊盘或者0欧电阻的焊位。这种模块在设计时就把接口模式的选择权留给了用户,是最容易改造的类型。有的模块甚至直接印了表格,标明不同焊法对应SPI还是I2C,照做就行。

第二种情况是背面有跳线帽或者拨码开关。这种更省事,直接拨到I2C档位就完成了硬件层面的切换,连烙铁都不用拿。

第三种情况是背面干干净净,只有几个元件的标识,没有模式选择相关的丝印。这时候要稍微费点功夫,需要找到SSD1306芯片周围的BS电阻。一般来说,如果模块使用了QFP封装的芯片(可以明显看到芯片引脚),BS0、BS1、BS2这几个引脚会有对应的电阻连接到VCC或GND。用万用表的通断档,对照芯片数据手册上的引脚定义,可以一一找出来。

这里要特别提醒:很多便宜模块用的是COG封装,芯片直接邦定在玻璃基板上,外部看不到引脚。这种模块基本没有飞线改造的可能。如果背面没有任何模式选择焊盘,也没有外露的芯片引脚,那我建议你趁早放弃改造,直接买一块4针I2C屏用,省下的时间和精力远比那块屏值钱。

判断模块能不能改,有一个很实用的经验:商品的详情页或者说明书里如果提到"支持SPI/I2C切换"或者标注了"PS"选项,说明这块屏预留了接口模式的选择功能,可以改。如果商品描述里只写了SPI接口,并且没有提到任何切换功能,那大概率是硬死在SPI模式的,改造难度会很大。

3. 硬件改造实操:从SPI到I2C的完整过程

3.1 情况A:背面有模式焊盘,改起来最省心

如果你的模块背面有标注"PS"或"BS"的焊盘,先别急着动烙铁,仔细看一下焊盘旁边的丝印说明。常见的设计是三个焊盘并排,标着SPI、I2C的字样,或者用0代表某个状态,用1代表另一个状态。

以最常见的三焊盘设计为例,出厂默认状态通常是SPI模式,焊盘上的焊锡连接的是SPI档位。改成I2C的时候,先把原来的焊锡用吸锡带清理干净,再把焊盘短接到I2C档位对应的位置。操作上有个小技巧:用刀头烙铁加一点助焊剂,烙铁温度控制在300度左右,动作要快,避免长时间加热把焊盘烫脱落。改完之后用万用表测一下,确认连接关系正确再做下一步。

有个容易踩的坑:有些模块的BS焊盘默认是断开的,丝印上标的"0"表示开路,"1"表示短接。如果没搞清楚定义就乱焊,可能把SPI和I2C的配置焊反了,屏幕反而点不亮。建议每次焊完之后,对照丝印的说明重新确认一遍,别嫌麻烦。

3.2 情况B:需要挪动0欧电阻,看清配置再下手

有些模块不提供直接标注的焊盘,而是在芯片附近放了三颗0欧电阻,分别对应BS0、BS1、BS2。出厂时这三颗电阻的焊接位置决定了当前的工作模式。这种情况下的改造思路是:把电阻从原来的焊位挪到目标焊位。

先对照数据手册里的模式配置表,确认四线SPI和I2C对应BS0/BS1/BS2的状态分别是多少。常见配置是这样的:

工作模式BS0BS1BS2
4线SPI001
3线SPI000
I2C100
6800并行011

表里的0和1代表该引脚连接到GND还是VCC。看到这个表你就明白了,从四线SPI改成I2C,需要把BS0从0改成1,把BS2从1改成0,BS1保持0不动。

实际操作时,找到这三颗电阻的焊位,用镊子配合烙铁把两颗电阻取下来,重新焊到目标焊位上。如果没有同规格的0欧电阻,也可以用焊锡直接短接需要连通的焊盘,但不推荐这么做,毕竟焊锡短接的可靠性不如元件本身,而且万一后面想改回来很麻烦。

还有一种更隐蔽的情况:模块上没有0欧电阻,而是直接通过PCB走线固定了BS引脚的电平。这种模块在视觉上完全看不出哪里能改,只能走飞线方案。顺着芯片引脚往外查走线,找到BS引脚连到VCC或者GND的过孔,用美工刀小心切断走线,再飞线到需要的电平上。这个操作对焊工和耐心要求都比较高,新手不建议尝试。

3.3 情况C:飞线到芯片引脚,谨慎评估再动手

飞线只适用于芯片引脚外露的模块,操作思路是绕过板上的电阻配置,直接把BS0/BS1/BS2三个引脚按目标电平强制连接。操作前需要准备好芯片数据手册,确认引脚编号。以SSD1306的常见封装为例,BS0、BS1、BS2在芯片上的位置相对固定,用万用表通断档从芯片引脚往上找PCB走线,确认无误后再动烙铁。

实际操作中有几个细节要特别注意:

  • 飞线要用细一点的硅胶线或者漆包线,粗线太硬容易把焊盘拽掉。
  • 焊之前先在芯片引脚上补一点助焊剂,让焊锡更好地附着。
  • 焊完后每根线都用万用表测一遍,确保没有虚焊,也没有和其他引脚短路。
  • BS0、BS1、BS2三个引脚不要直接并联在一起,先想清楚各自该接VCC还是GND再动手。

飞线方案的风险在于:一旦操作失误,可能损坏芯片或者PCB走线。手头模块如果价值不高,试错成本还能接受;如果是比较贵或者已经焊在系统里的模块,我建议还是算了,换一块支持I2C的屏更稳妥。

3.4 全模式接线对照,改完照着这张表接

不管是哪种情况,硬件层面的BS配置改完之后,最后一步都是按照I2C模式重新接线。对照下面这张表操作,基本不会出错:

原SPI引脚接到I2C总线上的位置说明
VCC3.3V或5V(视模块标注)电源正极
GNDGND电源地
D0SCLI2C时钟线
D1SDAI2C数据线
RESMCU的复位引脚或GPIO低电平有效复位
DCGND(地址0x3C)或VCC(地址0x3D)I2C模式下变成地址选择位SA0
CSVCCI2C模式下必须拉高

关于DC引脚接GND还是VCC,很多人疑惑。I2C模式下,SSD1306的7位从机地址是固定的0111100加上SA0位,SA0的电平决定了最后一个地址位是0还是1。DC接GND,实际地址是0x3C;DC接VCC,实际地址变成0x3D。绝大多数驱动库默认地址是0x3C,所以一般建议DC接GND。

CS引脚也要特别说明:I2C模式下没有片选概念,CS引脚建议直接接到VCC,让它处于不使能状态。有些资料说悬空也可以,但实测下来接VCC更稳定,尤其是有其他设备共用总线时,悬空引脚容易受干扰。RES引脚不能悬空,I2C模式下芯片同样需要复位信号,接到MCU的GPIO或者RC复位电路都可以。

还有一个不少新手忽略的点:I2C总线的SCL和SDA是需要上拉电阻的。如果模块板载已经焊了上拉电阻,外部就不用再加;如果模块本身没有上拉,你需要在SCL和SDA上分别接一个4.7k到10k的电阻到VCC。怎么判断板子上有没有上拉?用万用表电阻档测SCL到VCC之间的阻值,如果有几十千欧左右的稳定读数,说明上拉电阻已经在板上了。

4. 软件适配:驱动从SPI切换到I2C

4.1 图形库一行代码完成切换

硬件改完之后,软件部分其实是最简单的。拿最常用的U8g2库来说,SPI版本和I2C版本的差别就一个构造函数,其他代码全部不用动。

SPI版本的构造函数是这样:

#include <U8g2lib.h> U8G2_SSD1306_128X64_NONAME_1_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/ 10, /* dc=*/ 9, /* reset=*/ 8);

改成I2C版本后,只需要替换成:

#include <U8g2lib.h> U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ 8, /* clock=*/ 400000);

两种用法在初始化上完全一样,都是:

u8g2.begin();

需要注意的是,I2C版本的构造参数里不再需要CS和DC引脚,因为这两个引脚在I2C模式下各有各的归属(一个拉高,一个决定地址),驱动代码不再关心它们的电平状态。RES引脚在I2C版本的构造函数里是可选的,如果你希望让驱动库帮忙控制复位时序,就传一个有效的GPIO号;如果RES已经接到了RC复位电路或者不想占用GPIO,可以直接传U8X8_PIN_NONE。

另一个常用的开源图形库是GFX库(搭配SSD1306驱动使用),它的切换方式更加直观。SPI版本:

#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &SPI, 9, 10, 8);

改成I2C版本:

#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &Wire, -1);

注意这里的地址参数在库里默认尝试0x3C,如果你的DC接的是VCC(地址是0x3D),需要在初始化这前调用display.setI2CAddr(0x3D)或者直接改库文件的宏定义。这个细节特别坑,很多人改了硬件之后屏幕怎么都不亮,最后发现是地址不匹配。

4.2 裸寄存器驱动:理解I2C控制字节的妙处

如果不想用现成图形库,或者是在某些资源受限的环境下开发,你也可以自己用I2C总线直接操作SSD1306。这就要理解I2C模式下SSD1306的数据帧结构了。

SPI模式下,区分命令和数据靠的是DC引脚的电平:DC拉低发命令,DC拉高发数据。I2C模式下没有专门的DC引脚,取而代之的是一个控制字节。每次I2C通信时,在从机地址之后要发送一个控制字节,8位数据中第1位是Co位,第7位(bit0)是D/C位,其中D/C位的作用和DC引脚一样:D/C为0表示后续字节是命令,D/C为1表示后续字节是数据。Co位通常置0,表示后续只有一个字节,命令或数据被连续发送。

用Arduino的Wire库写起来就是:

// 写命令 void oledWriteCommand(uint8_t cmd) { Wire.beginTransmission(0x3C); Wire.write(0x00); // 控制字节:Co=0, D/C#=0,表示随后是命令 Wire.write(cmd); Wire.endTransmission(); } // 写数据 void oledWriteData(uint8_t data) { Wire.beginTransmission(0x3C); Wire.write(0x40); // 控制字节:Co=0, D/C#=1,表示随后是数据 Wire.write(data); Wire.endTransmission(); }

这里0x00和0x40这两个值的来源我拆解一下:0x00的二进制是00000000,第1位Co=0,第7位D/C#=0,所以它表示"后续字节是命令";而0x40的二进制是01000000,第7位D/C#=1,所以它表示"后续字节是数据"。这个设计思路其实挺巧妙的,用同一个控制字节把SPI模式下DC引脚的功能替代掉了。

知道了如何写命令、写数据之后,剩下的就是SSD1306的初始化命令序列了,这套序列在SPI和I2C模式下完全一模一样,没有任何区别。下面是一套常用的初始化序列:

oledWriteCommand(0xAE); // 显示关闭 oledWriteCommand(0xD5); // 设置时钟分频 oledWriteCommand(0x80); // 推荐值 oledWriteCommand(0xA8); // 设置多路复用比 oledWriteCommand(0x3F); // 64行 oledWriteCommand(0xD3); // 设置显示偏移 oledWriteCommand(0x00); // 无偏移 oledWriteCommand(0x40); // 起始行0 oledWriteCommand(0x8D); // 电荷泵设置 oledWriteCommand(0x14); // 开启电荷泵 oledWriteCommand(0x20); // 内存寻址模式 oledWriteCommand(0x00); // 水平寻址 oledWriteCommand(0xA1); // 段重映射 oledWriteCommand(0xC8); // 扫描方向 oledWriteCommand(0xDA); // COM引脚配置 oledWriteCommand(0x12); // 推荐值 oledWriteCommand(0x81); // 对比度设置 oledWriteCommand(0xCF); // 默认值 oledWriteCommand(0xD9); // 预充电周期 oledWriteCommand(0xF1); // 推荐值 oledWriteCommand(0xDB); // VCOMH检测 oledWriteCommand(0x40); // 默认值 oledWriteCommand(0xA4); // 显示开启,不检测RAM内容 oledWriteCommand(0xA6); // 正常显示(非反白) oledWriteCommand(0xAF); // 显示开启

这套命令里最值得关注的是0x8D和0x14这一组,它负责开启内部电荷泵。很多自己手写驱动的案例里屏幕一直不亮,就是漏掉了这一步或者写错了参数。电荷泵的作用是为OLED面板提供驱动电压,不开启的话面板不工作。

4.3 地址与速率的几个关键细节

I2C通信要用的从机地址,硬件的设置逻辑上文已经讲过。这里补充一个容易混淆的知识点:SSD1306的数据手册里写的从机地址是0111100X,其中X就是SA0位。当DC接GND时,X=0,整个地址是0x3C(7位地址是0x3C的二进制左移一位,实际Wire库发送时用的地址是0x3C这个8位形式)。当DC接VCC时,X=1,地址变成0x3D。这块屏没有A0/A1这样的地址引脚,所以一条I2C总线上最多只能挂两块同款OLED,之一用0x3C,之一用0x3D。

速率方面,I2C标准模式是100kHz,快速模式是400kHz。SSD1306的数据手册标注最大I2C时钟是400kHz,所以把I2C速率调到400kHz是最合适的。对一块128x64的单色屏来说,整帧数据是128×64÷8=1024字节,在400kHz速率的理想状态下,传完一整帧大约需要20多毫秒,理论刷新率能到40到50帧每秒,显示静态文字和简单图形绰绰有余。如果用来刷新大范围动画,I2C确实比SPI慢不少,这是接口物理特性决定的,不是代码能优化的。

如果你的MCU硬件I2C不支持400kHz或者系统里还有其他慢速I2C设备,可以用100kHz标准速率,显示静态内容一样够用,代价仅仅是刷新速度减半。对于任何一位做显示需求不高的项目,I2C的速率都不会成为瓶颈。

超高频别的MCU上还有一个进阶用法:如果改完硬件后发现I2C信号波形不好,可以检查一下上拉电阻的阻值。总线上挂的设备越多,等效上拉电阻越小,信号边沿会越陡峭;但上拉电阻太小会让总线静态电流明显增大,这在低功耗项目里要格外注意。4.7k在多数场景下是平衡点,如果只有一块OLED和少量设备,10k也可以,能省一点静态功耗。

5. 常见问题与排查技巧实录

5.1 故障速查表

改造和调试过程中遇到的问题,九成以上集中在下面这张表里,按图索骥能省不少时间:

现象可能原因排查思路
I2C扫描不到设备地址不对确认DC接的是GND(0x3C)还是VCC(0x3D)
I2C扫描不到设备BS模式没改成功重新检查BS0/BS1/BS2的电平配置
I2C扫描不到设备SCL/SDA接反对调D0和D1试试
I2C扫描不到设备上拉电阻缺失检查SCL/SDA到VCC之间是否有4.7k到10k电阻
能扫描到但屏幕不亮复位信号异常检查RES引脚是否接对,上电后是否有一个低电平脉冲
能扫描到但屏幕不亮初始化命令电荷泵没开确认0x8D命令后紧跟0x14
屏幕全亮花屏对比度太低或电源问题检查VCC供电是否稳定,对比度命令0x81的参数
显示正常但刷新慢I2C速率太低把时钟频率调到400kHz
和传感器共用总线时通信异常地址冲突检查总线上是否有其他设备占用0x3C或0x3D

5.2 几个踩过才知道的坑

改造这类模块最大的坑不是硬件操作本身,而是你以为改完之后就万事大吉了。我见过有人信心满满地把模块上的BS电阻从SPI挪到I2C,结果上电之后I2C扫描器依然找不到设备。排查了半天,才发现模块上不但有BS配置电阻,还有一个隐藏的CS跳线没有处理——CS引脚在I2C模式下必须接VCC,而他那个模块的CS在SPI模式下被一个0欧电阻拉到了GND,两股配置力互相打架,总线自然起不来。

第二个容易踩的坑是复位时序。SSD1306在上电后需要RES引脚产生一个低电平脉冲才能完成内部初始化,而且低电平持续时间要大于几个微秒。很多人的接线里把RES直接接到了VCC或者GND,以为芯片会自动复位,实际上如果复位时序不对,芯片可能进入不确定状态,表现就是I2C扫描能得到正确的设备地址,但怎么发命令屏幕都没反应。遇到这种情况,把RES接到一个GPIO上,用代码控制先拉低20毫秒再拉高,一般就能解决。

第三个坑是DC引脚和地址的关系。我见过某种模块的说明书里明确写着"I2C模式下DC接地,地址是0x3C",但实际模块的PCB在出厂时已经把DC引脚内部上拉到VCC了,外接GND根本没用,地址实际是0x3D。这种模块只有用万用表实测DC引脚的电压才能发现。所以不要完全相信丝印和说明书,遇到扫描不到设备的情况,记得量一下DC引脚上的实际电平。

5.3 排错实战流程

给你一套我自己常用的排错流程,照着走一遍,大部分问题都能定位。第一步先跑一遍I2C扫描程序,看看能不能发现0x3C或者0x3D地址的设备。如果发现了,说明硬件接线和芯片的I2C模式基本没问题,问题出在驱动程序或初始化时序上。如果扫描不到,先检查SCL和SDA有没有接反,然后量一下两个引脚的上拉电压,正常应该在VCC附近。排除接线问题后,多半就是BS配置没改成功,仔细用万用表确认一下BS0、BS1、BS2这三个引脚的实际电平。

扫描到设备后屏幕仍不亮,建议先用示波器或者逻辑分析仪看一眼RES引脚上电时的波形。如果RES由GPIO控制,检查代码里有没有正确执行拉低再拉高的时序。再检查初始化命令序列,特别是0x8D和0x14这一组电荷泵命令,漏掉这个命令是很常见的翻车原因。如果屏幕能亮但显示内容不对,比如字体重影、镜像、上下颠倒,那是段重映射(0xA1/0xA0)和扫描方向(0xC8/0xC0)的配置问题,和SPI改I2C没有关系,按屏的方向调整这两条命令就行。

还有一个很实用的调试技巧:初始化成功后先往显存里写一个全屏填充模式,让屏幕所有像素均匀发光,这样能快速判断屏幕本身有没有问题。如果全屏点亮正常,基本可以确定硬件和初始化命令都对了,接下来再显示具体图案就简单了。

我在实际操作中最意外的体会是,大部分时间其实花在了排查上,真正动烙铁焊接的过程反而很快。这也解释了为什么我说要先确认模块能不能改再动手——如果模块本身没有预留模式切换焊盘,又用的是COG封装,那不管你软件写得再好也是白搭。反过来,只要硬件层面能正确进到I2C模式,软件部分基本上就是换一行构造函数的事。

最后再分享一个小技巧:如果你手头有很多块SPI OLED和一块原生I2C OLED,与其挨个改造,不如直接拿原生I2C屏当参考。用万用表分别测一下两块屏的SCL和SDA引脚对地电阻,再对比一下BS配置焊盘的状态,改起来心里会更有数。毕竟眼见为实,理论分析再多也不如拿实物对比来得直观。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进