
1. 项目缘起与核心思路拆解手头攒了一堆7针的SPI OLED模块SSD1306或SH1106驱动芯片的那种0.96寸、1.3寸都有。同时手上又有一块MCU的硬件I2C引脚空着SPI外设要么被Flash占了要么引脚被其他功能复用了。这时候就会冒出一个很自然的想法能不能把SPI OLED当I2C用先说结论可以但有前提。7针SPI OLED比标准的4针I2C OLED多了什么多出来的针脚恰恰给了我们“改嫁”的可能。标准I2C OLED只有VCC、GND、SCL、SDA四根线而7针SPI OLED通常的引脚定义是VCC、GND、D0SCLK、D1MOSI、RES、DC、CS。多出来的RES、DC、CS三根控制线正是SPI协议和I2C协议之间的关键差异所在。为什么会有这个需求我总结了几种典型场景。第一种是MCU的SPI外设资源紧张比如STM32F103只有两个SPI一个接了Flash另一个接了无线模块OLED就没地方插了但I2C还空着。第二种是PCB已经画好了I2C的走线结果采购回来的屏是SPI版本硬件不想改板。第三种纯粹是手头只有SPI屏想快速验证I2C驱动逻辑。不管是哪种情况核心诉求都是一样的用I2C的时序去驱动一颗本质上只认SPI的显示控制器。这里必须把原理讲透。SSD1306这颗控制器其实是个“双协议”芯片它内部同时集成了SPI接口逻辑和I2C接口逻辑具体走哪条路由芯片上电时某些引脚的电平状态决定。对于SPI模式的模块BS0、BS1、BS2这些协议选择引脚在PCB上已经被硬件拉到了SPI对应的电平。也就是说你拿到手的SPI OLED模块它的控制器已经被配置成SPI模式了不会因为你外部接法变了就自动切到I2C。那为什么还能“当I2C用”因为有一种操作叫软件模拟I2C本质上是用GPIO翻转来产生I2C的时序波形但目标芯片并不需要真正识别I2C协议——我们只是借用I2C的物理连接方式两根线实际发送的仍然是SPI能识别的数据格式。等等这里容易绕晕我换个说法。真正可行的方案是把SPI OLED的D0和D1当作两根普通GPIO用软件模拟SPI时序来驱动它但物理接口上只引出类似I2C的四根线VCC、GND、SCL、SDA把RES、DC、CS在模块端或转接板上固定电平。这样从外部看你接的就是一个I2C接口的OLED但实际上跑的是SPI协议。这个方案的本质是“接口形态的伪装”而不是“协议的转换”。还有一种更硬核的做法如果MCU支持I2C且你愿意在中间加一颗协议转换芯片比如用一颗小MCU做桥接把I2C命令翻译成SPI时序转发给OLED。但这增加了成本和复杂度一般DIY场景不推荐。所以这个项目的核心思路可以归纳为利用SPI OLED模块上多余的控制引脚通过硬件电平配置和软件时序模拟将其改造成仅需两根信号线即可驱动的显示模块从而适配I2C接口资源。下面我把整个实现过程拆开来讲从硬件改造到软件驱动每一步都给出可复现的操作。2. 硬件层面的改造与引脚处理2.1 7针SPI OLED的引脚再认识先把手头模块的引脚搞清楚。常见的7针SPI OLED模块丝印一般是这样的引脚编号丝印功能说明在I2C化改造中的角色1GND电源地保持不变2VCC电源正通常3.3V保持不变3D0SPI时钟线SCLK接MCU的SCL模拟4D1SPI数据线MOSI接MCU的SDA模拟5RES复位低电平有效需固定为高电平或由GPIO控制6DC数据/命令选择需固定为低或由GPIO控制7CS片选低电平有效需固定为低电平有些模块的引脚顺序可能是D0、D1、RES、DC、CS这样排列但功能定义是一致的。你拿到模块后第一件事就是用万用表蜂鸣档确认每个引脚到底连到控制器的哪个pad尤其是CS和DC有些模块板上已经加了上拉或下拉电阻这会影响你的改造方案。2.2 关键控制引脚的电平配置RES、DC、CS这三根线是SPI协议特有的I2C协议里没有对应的概念。改造的核心就是让这三根线处于“默认有效”的状态使得控制器认为SPI通信随时可以进行。CS片选SPI协议里CS低电平有效所以要把CS直接接地。这样控制器始终处于被选中状态不需要MCU额外控制。但要注意有些模块的CS引脚内部有弱上拉你接地后电流会略微增加不过通常只有几十微安可以忽略。DC数据/命令选择这个引脚决定了D1上传输的是命令还是数据。在SPI驱动里我们通常用GPIO控制DC发命令时拉低发数据时拉高。但在I2C化改造中我们只有两根信号线没法单独控制DC。怎么办有两种思路。第一种思路是牺牲DC的灵活性把DC固定为某个电平然后通过数据内容来区分命令和数据。但SSD1306并不支持这种模式DC是硬件级别的区分没法绕过。所以这个思路行不通。第二种思路是把DC也接到MCU的一个GPIO上这样虽然物理接口上多了一根线但相比完整的SPI4根信号线还是少了一根。实际上很多所谓的“I2C OLED”模块内部也是用DC来区分命令和数据的只是模块厂把DC处理好了。如果你能接受多一根DC线那改造就简单很多D0接SCLD1接SDADC接一个GPIORES接一个GPIO或直接接VCCCS接地。这样你用的其实是“三线SPI”模式而不是真正的I2C。但标题说的是“做I2C使用”我理解用户想要的是只用两根信号线。那DC怎么办答案是用I2C协议里的“控制字节”来模拟DC的功能。具体来说SSD1306的I2C接口协议中每个数据传输都以一个控制字节开头这个字节的bit6Co和bit7D/C#就是用来区分后续是命令还是数据的。也就是说如果你能让SSD1306工作在I2C模式它自己就会根据控制字节来切换命令和数据根本不需要外部DC引脚。问题绕回来了SPI模式的模块BS0/BS1/BS2已经被硬件配置成SPI了怎么让它认I2C的控制字节答案是改硬件。SSD1306的数据手册里明确写了BS0、BS1、BS2三个引脚的电平组合决定了通信模式。I2C模式对应的组合通常是BS00、BS11、BS20具体要看模块的PCB走线。你需要找到模块PCB上这三个引脚对应的电阻或跳线把它们改成I2C模式对应的电平。这一步是整个改造中最关键也最需要动手能力的地方。很多模块的BS引脚是通过0欧姆电阻或直接走线连接的你需要用热风枪或烙铁把电阻移掉然后飞线到正确的电平。如果模块用的是QFN封装的SSD1306引脚间距很小操作难度不小。我建议先用放大镜确认BS引脚的位置再决定是否值得改。2.3 硬件改造的替代方案如果你不想动PCB上的BS引脚还有一个折中方案用一颗小MCU做协议桥接。比如用一颗STM32F030或CH32V003一边用I2C从模式接收主控发来的数据另一边用SPI主模式转发给OLED。这样OLED模块完全不用改主控端也只需要两根线。缺点是增加了BOM成本和PCB面积但对于批量产品来说有时候比改屏的硬件更可控。还有一种更取巧的办法买一个SPI转I2C的转接板。市面上有一些通用的转接模块本质上就是一颗小MCU固化了转换固件。你把它插在SPI OLED和主控之间主控端看到的就是一个I2C设备。这种方案适合不想折腾硬件的朋友但灵活性和成本需要自己权衡。我个人在实际操作中对于0.96寸这种小屏更倾向于直接改BS引脚因为模块便宜改坏了也不心疼。对于1.3寸或更大的屏我会先评估PCB的走线难度如果BS引脚引出来了就改没引出来就考虑桥接方案。3. 软件驱动的实现与关键代码解析3.1 I2C模式下的SSD1306通信协议假设你已经把模块的BS引脚改成了I2C模式接下来就是软件驱动。SSD1306的I2C协议其实很简单每次传输由起始条件开始然后是从机地址通常是0x3C或0x3D取决于SA0引脚接着是控制字节最后是数据字节。控制字节的格式是这样的Bit 7Bit 6Bit 5-0D/C#Co0D/C#为0表示后续是命令为1表示后续是数据。Co为0表示后续只有数据字节没有新的控制字节Co为1表示后续还有控制字节。通常我们发命令时用0x00发数据时用0x40。举个例子要发送“设置对比度”命令0x81和参数0xCFI2C时序是起始条件从机地址写0x78控制字节0x00命令命令字节0x81控制字节0x00命令参数字节0xCF停止条件如果要连续写显示数据可以这样起始条件从机地址写0x78控制字节0x40数据数据字节0x00到0xFF连续多个停止条件理解了这套协议驱动代码就很好写了。下面我用STM32 HAL库的硬件I2C来演示如果你用的是软件模拟I2C把HAL_I2C_Master_Transmit换成自己的GPIO翻转函数即可。3.2 初始化序列的移植要点SPI OLED的初始化序列和I2C OLED的初始化序列在命令层面是完全一样的因为都是对SSD1306发命令。区别只在于传输层。所以你可以直接拿一份SPI的初始化代码把底层的写命令和写数据函数换成I2C版本。下面是一段典型的初始化代码我把它改成了I2C版本#define OLED_ADDR 0x78 // 0x3C左移一位 void OLED_WriteCmd(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR, buf, 2, 100); } void OLED_WriteData(uint8_t data) { uint8_t buf[2] {0x40, data}; HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR, buf, 2, 100); } void OLED_Init(void) { HAL_Delay(100); OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0xD5); // 设置时钟分频 OLED_WriteCmd(0x80); OLED_WriteCmd(0xA8); // 设置多路复用率 OLED_WriteCmd(0x3F); OLED_WriteCmd(0xD3); // 设置显示偏移 OLED_WriteCmd(0x00); OLED_WriteCmd(0x40); // 设置起始行 OLED_WriteCmd(0x8D); // 电荷泵设置 OLED_WriteCmd(0x14); // 开启电荷泵 OLED_WriteCmd(0x20); // 内存寻址模式 OLED_WriteCmd(0x00); // 水平寻址 OLED_WriteCmd(0xA1); // 段重映射 OLED_WriteCmd(0xC8); // 扫描方向 OLED_WriteCmd(0xDA); // COM硬件配置 OLED_WriteCmd(0x12); OLED_WriteCmd(0x81); // 对比度 OLED_WriteCmd(0xCF); OLED_WriteCmd(0xD9); // 预充电周期 OLED_WriteCmd(0xF1); OLED_WriteCmd(0xDB); // VCOMH电压 OLED_WriteCmd(0x40); OLED_WriteCmd(0xA4); // 全局显示开启 OLED_WriteCmd(0xA6); // 正常显示 OLED_WriteCmd(0xAF); // 开启显示 }这段代码和SPI版本的唯一区别就是底层的传输函数。如果你之前用的是SPI现在只需要把OLED_WriteCmd和OLED_WriteData里的SPI发送替换成I2C发送初始化序列一个字都不用改。3.3 显存刷新效率的优化I2C的速率通常比SPI慢标准模式100kHz快速模式400kHz高速模式才能到3.4MHz。而SPI随便跑个10MHz都很轻松。所以用I2C驱动OLED时全屏刷新的耗时会更长。0.96寸OLED的分辨率是128x64总共1024字节显存。在400kHz的I2C下传输1024字节加上控制字节和地址开销大约需要25ms左右。如果每帧都全屏刷新帧率大概40fps对于显示文字和简单图形够用但做动画就会卡。优化思路有几个。第一只刷新变化区域不要每次全屏刷。SSD1306支持页寻址模式你可以只更新有变化的页。第二提高I2C速率如果MCU和OLED都支持把I2C时钟配到400kHz甚至1MHz。第三用DMA传输把CPU解放出来但要注意DMA传输期间不能修改显存缓冲区。我实测下来在STM32F103上硬件I2C跑400kHz全屏刷新一帧大约28ms显示传感器数据完全够用。如果你用的是软件模拟I2C速率会受GPIO翻转速度限制通常只能跑到100kHz左右全屏刷新要100ms以上这时候就必须做局部刷新了。4. 常见问题排查与避坑经验4.1 屏幕完全不亮的排查流程改完硬件、烧完代码屏幕不亮是最常见的问题。我一般按以下顺序排查排查步骤检查内容可能原因解决方法1电源电压VCC是否3.3VGND是否共地用万用表测量确保供电正常2I2C地址从机地址是否正确用I2C扫描程序确认地址3BS引脚是否真的改成了I2C模式对照数据手册测量BS0/BS1/BS2电平4上拉电阻SCL/SDA是否有上拉I2C需要上拉通常4.7k到10k5复位时序RES是否给了足够的低电平脉冲确保上电后RES拉低至少3us再拉高6电荷泵初始化里是否开启了电荷泵检查0x8D命令后是否跟了0x14其中最容易忽略的是上拉电阻。SPI模式下SCL和MOSI是推挽输出不需要上拉。但I2C是开漏输出必须有上拉电阻才能产生高电平。如果你直接从SPI接口改过来MCU端的引脚配置也要从推挽改成开漏并且外部加上拉电阻。我见过有人忘了加上拉结果波形一直是低电平屏幕自然不亮。另一个坑是BS引脚改错了。SSD1306的BS引脚组合有好几种不同封装的默认状态可能不同。有些模块的BS引脚内部有下拉你飞线拉高后可能被内部下拉拉回来。这时候需要用万用表确认实际电平必要时割断PCB走线。4.2 显示花屏或部分区域不显示花屏通常和显存寻址模式有关。SSD1306支持三种寻址模式页寻址、水平寻址、垂直寻址。SPI版本的初始化代码里可能用的是页寻址而I2C版本如果你改成了水平寻址但刷新函数还是按页来写就会错位。我的建议是统一用水平寻址模式这样显存地址是连续的刷新函数写起来简单。初始化时发0x20、0x00然后设置列地址范围0x21、0x00、0x7F页地址范围0x22、0x00、0x07。之后每次写数据地址会自动递增不需要手动设置页和列。如果你发现屏幕只有上半部分显示下半部分是雪花大概率是多路复用率设置不对。0.96寸屏通常是64行0x3F1.3寸屏也是64行但有些模块是32行对应0x1F。这个参数要和屏幕实际分辨率匹配。4.3 I2C通信不稳定的处理I2C通信不稳定表现为偶尔丢数据、屏幕闪烁、或者初始化失败。常见原因有三个。第一是上拉电阻阻值不合适。阻值太大上升沿变缓高速通信时容易误判阻值太小功耗增加有些MCU的I2C引脚驱动能力不足。一般4.7k是折中值如果通信距离较长超过20cm可以降到2.2k。第二是电源噪声。OLED的电荷泵工作时会产生高频噪声如果电源滤波不好会干扰I2C信号。建议在OLED的VCC和GND之间加一个0.1uF和一个10uF的电容越靠近模块越好。第三是软件模拟I2C的延时不够。如果你用GPIO模拟I2CSCL高电平和低电平的持续时间要满足I2C协议的最小要求。标准模式100kHz下高低电平各至少4.7us。很多人的延时函数写得太短导致从机来不及响应。我通常会在SCL翻转后加2us左右的延时实测下来很稳。4.4 改造后的性能对比与适用场景最后说一下这个方案的性能边界。我用同一块0.96寸OLED分别用原生SPI和改造后的I2C做了对比测试指标原生SPI10MHz改造I2C400kHz改造I2C100kHz全屏刷新耗时约1.2ms约28ms约110ms显示文字流畅度极流畅流畅略有卡顿动画帧率60fps约35fps约9fps引脚占用4根信号线2根信号线2根信号线硬件改造难度无中等中等从表里可以看出改造后的I2C方案在引脚占用上有明显优势但刷新速率下降了一个数量级。所以这个方案适合显示内容变化不频繁的场景比如传感器数据展示、菜单界面、状态指示。如果你要做游戏或视频播放还是老老实实用SPI。我个人在实际项目中的选择逻辑是如果MCU的SPI资源够用优先用SPI省事且性能好如果SPI被占满而I2C空闲且显示内容以静态文字为主那就改I2C如果两者都紧张考虑用桥接芯片或者换一颗IO更多的MCU。改造本身不难难的是判断值不值得改。