第一次接触STM32F103驱动OLED,很多朋友走的弯路我都走过。从收到的模块一片黑,到怀疑接线、怀疑芯片、怀疑人生,再到最后把第一行字点亮,这个过程的成就感确实是折腾几小时才换来的。这篇文章把0.96寸OLED屏幕从接线、IIC协议原理、HAL库软件IIC驱动,到动态显示和各类高频故障排查一次性讲透,适合刚入手STM32F103最小系统板、还不清楚GPIO怎么操作IIC、以及用硬件IIC踩了坑的朋友。全文基于常用的SSD1306驱动的0.96寸IIC接口OLED模块,固件库和HAL库思路通用,代码可以直接照抄。
1. 接线全解:0.96寸OLED模块与STM32F103怎么连最稳
1.1 先认清手里的模块:SSD1306、引脚顺序和接口模式
市面最常见的0.96寸OLED,屏幕尺寸是0.96英寸,分辨率128x64,驱动芯片是SSD1306,通信接口分IIC、SPI、并行三类。新手买到的绝大多数是IIC接口,但也有不少7针模块可以切换IIC/SPI模式。模块背后的丝印或排针附近如果能看到BS0、BS1、BS2几个焊盘,那就说明是SPI/IIC模式可切换的多接口模块。
对于纯IIC模块,引脚一般是4个:GND、VCC、SCL、SDA,排针顺序每家略有不同,最常见的丝印是从左到右VCC GND SCL SDA,也有GND VCC SCL SDA的版本。接到STM32F103之前,第一件事是核对丝印,不要凭习惯接,烧模块一般不至于,但接反会直接导致屏幕无显示、发烫,甚至芯片发热异常。
多接口模块切换到IIC模式时,一般要把BS0和BS1跳线或者电阻断开,BS2接GND,具体看模块背面的标注。如果不切换直接插在IIC排针上,有可能不亮或者时序完全混乱。手头没有万用表时,最直接的判断方法是看模块背面有没有标IIC/SPI切换说明,以及排针旁边丝印是"SCL SDA"还是"SCK MOSI"。
1.2 IIC接线:SCL和SDA到底接哪两个引脚
STM32F103有多种引脚可以复用出I2C外设,但对我们新手阶段最友好的还是用软件IIC,也就是直接拿普通GPIO去模拟IIC时序。很多入门教程之所以默认用PB8和PB9,是因为这两个引脚默认连到板载LED或按键的项目少,冲突概率低,而且大部分国产最小系统板把PB8/PB9引出来了。
推荐接线如下:
- OLED的SCL → STM32F103的PB8
- OLED的SDA → STM32F103的PB9
- OLED的VCC → 3.3V(部分模块标5V兼容,但供电优先3.3V,下面细说)
- OLED的GND → GND
这套接法在正点原子、野火、普惠捷、各路国产STM32F103C8T6最小系统板,以及江协科技那套经典教程里都通用。SCL接PB8、SDA接PB9的搭配还有一个额外好处:如果以后想切到硬件I2C,PB8/PB9本身就是STM32F103的I2C1_SCL和I2C1_SDA引脚,硬件和软件方案都可以无缝切换。
还有一点要注意,如果屏幕的SCL和SDA接到的是板子的PA口而不是PB口,也没问题,只要你的代码里GPIO配置跟着变就行。软件IIC对引脚不做硬性约束,关键是代码里用的IO口和实际接线保持一致。
1.3 供电与地址:VCC接3.3V还是5V,0x78还是0x7A
OLED模块板载了稳压芯片,理论上支持宽电压输入,但我实测下来,3.3V供电是最稳的。原因很简单:模块上的SSD1306本身工作电压就是1.65V到3.3V,模块内部有稳压电路,5V供电虽然能亮,但如果模块做工一般、稳压芯片质量差,5V下可能会出现屏幕亮度不均匀、闪烁甚至花屏。接3.3V能少踩一半坑。
IIC地址这里有个新手特别容易困惑的点。0.96寸OLED模块的IIC地址默认是0x3C,而有些模块通过贴片电阻配置了SA0引脚,地址会变成0x3D。在驱动代码里写死0x78或0x7A的都有,这两个值是8位地址表示法(左移一位后带上读写位),而0x3C/0x3D是7位地址表示法。HAL库的I2C传输地址填的是7位形式,也就是0x3C;如果你在网上找的示例代码里出现了0x78,那是7位地址左移一位变成的8位地址,填到HAL库的DevAddress参数里就会出错。
判断地址最靠谱的方法:如果你改代码里的地址从0x3C到0x3D后屏幕还是不亮,先别急着怀疑地址问题,大概率是接线、初始化时序或者供电的问题,而不是地址配置问题。
2. 先懂原理再敲代码:IIC时序、SSD1306命令体系和显示坐标的关系
2.1 IIC协议到底在传输什么
IIC(Inter-Integrated Circuit)是一种两线制半双工串行总线,只有SCL(时钟线)和SDA(数据线)两根线。所有挂在总线上的设备都并联在这两线上。之所以要上拉电阻,就是因为IIC协议的引脚是开漏输出,平时靠上拉电阻把电平拉高,设备要拉低时主动把引脚拉低,通过操作何时拉低来构成逻辑0和逻辑1。
一次完整的IIC写操作长这样:
- 主机拉低SDA,再拉低SCL,产生一个起始信号(START)
- 主机依次发送8个bit:前7位是从机地址,第8位是读写标志位,写入操作标志位为0
- 从机收到地址后,在第9个时钟脉冲把SDA拉低,产生一个应答信号(ACK)
- 主机继续发送数据和寄存器地址、控制字节等,每发完一个字节等一个ACK
- 主机释放SDA,产生停止信号(STOP)
OLED屏幕的驱动芯片SSD1306作为从机,地址默认为0x3C,挂在IIC总线上只需要三根有效线:SCL、SDA、GND。主机就是我们的STM32F103。
这里补充一个很多新手不太注意的点:IIC本身是半双工的,同一时刻只能有一方驱动SDA总线,所以主机发送完一个字节后,必须立刻释放SDA改成输入状态去读取ACK,这就是为什么同一根线上又输出又输入,模拟时序时切换方向容易出问题。而硬件I2C外设自动帮你做完这一切,这也是硬件IIC时序上更可靠的原因之一。
2.2 SSD1306的命令体系和控制字节
SSD1306内部有一块GRAM(显存),大小正好对应128x64像素。在IIC模式下,主机向SSD1306发送的数据被分成"控制字节+数据字节"的组合。
控制字节是紧跟地址字节之后发送的,有两种值:
- 0x00:表示接下来发送的字节是命令
- 0x40:表示接下来发送的字节是显示数据
所以向OLED写命令和写数据的本质区别,就在这个控制字节。这里也是很多驱动代码看起来"一行一个"的原因:
OLED_WR_Byte(0xAE, OLED_CMD); // 关闭显示 OLED_WR_Byte(0xAF, OLED_CMD); // 开启显示第二个参数OLED_CMD就是0x00,OLED_DATA就是0x40。如果控制字节写错,比如把数据当命令发,屏幕就会出现奇怪的乱码,或者一个像素都不显示。
2.3 显示坐标和GRAM的映射关系
128x64的OLED,X坐标范围0到127,Y坐标范围0到63,分成8页(Page0到Page7),每页实际是8行像素。SSD1306内部把64行像素按8个像素一页的方式组织,每页有128个字节,每个字节的8个bit正好对应这一页竖直方向的8个像素。
驱动代码里常用的OLED_SetCursor(x, y)中的y指的是页,不是像素行!这是新手最容易搞混的一个点。OLED_ShowString(0, 0, "Hello")里的(0,0)意思是X从0开始,Y从第0页开始,也就是屏幕最顶部8行像素。如果显示字符的时候写y=10,那不是第十个像素行,而是第十页,直接跑到屏幕下半部分去了。
理解了这个映射关系,后面写动态显示、清屏、滚动、动画时就不会觉得代码是在"变魔术"了。
3. 手把手复现HAL库软件IIC驱动:从GPIO配置到字符显示
3.1 硬件IIC还是软件IIC,新手不要纠结
STM32F103的硬件I2C外设口碑两极分化,有人用得飞起,有人一上硬件I2C就卡死在HAL_I2C_Mem_Write里。我的观点很明确:第一块OLED驱动用软件IIC。
原因有三:
- 软件IIC每一根线走到哪一步都看得见,出了问题能靠逻辑分析仪或示波器快速定位
- 硬件I2C虽然代码更简洁,但涉及时序配置、中断、错误处理,新手踩坑成本高
- HAL库的软件IIC实现不依赖外设时钟,只要GPIO配置对、时序对就能跑
等软件IIC把整个流程跑通了,再回头去试硬件I2C,你会更清楚硬件I2C出错时错误寄存器里到底在说什么。
3.2 CubeMX配置与GPIO初始化
用STM32CubeMX配置时,只要把PB8和PB9设为GPIO_Output模式,不需要开启I2C外设。输出电压等级不用管,默认就行。核心是开漏还是推挽?注意:如果要严格模拟IIC总线,引脚应该配置为开漏输出(Open Drain),这样配合外部上拉才能实现线与逻辑。网上很多教程直接配成推挽输出也能跑,那是因为模块板上自带上拉电阻,推挽输出强行驱动电平也能工作。但如果你自己手工搭线、模块没有上拉,推挽输出会导致通信不稳定。建议直接用开漏输出。
CubeMX里生成代码后在main函数中调用:
MX_GPIO_Init(); OLED_Init(); OLED_Clear(); OLED_ShowString(0, 0, "Hello STM32"); OLED_ShowString(0, 2, "OLED IIC OK");3.3 核心时序代码:IIC起始、停止、发送字节
以下代码用HAL库的GPIO接口模拟IIC时序,直接在main.c文件里追加即可,不依赖额外库:
#define OLED_SCL_GPIO_PORT GPIOB #define OLED_SCL_PIN GPIO_PIN_8 #define OLED_SDA_GPIO_PORT GPIOB #define OLED_SDA_PIN GPIO_PIN_9 #define OLED_SCL_H() HAL_GPIO_WritePin(OLED_SCL_GPIO_PORT, OLED_SCL_PIN, GPIO_PIN_SET) #define OLED_SCL_L() HAL_GPIO_WritePin(OLED_SCL_GPIO_PORT, OLED_SCL_PIN, GPIO_PIN_RESET) #define OLED_SDA_H() HAL_GPIO_WritePin(OLED_SDA_GPIO_PORT, OLED_SDA_PIN, GPIO_PIN_SET) #define OLED_SDA_L() HAL_GPIO_WritePin(OLED_SDA_GPIO_PORT, OLED_SDA_PIN, GPIO_PIN_RESET) void OLED_I2C_Start(void) { OLED_SDA_H(); OLED_SCL_H(); OLED_SDA_L(); OLED_SCL_L(); } void OLED_I2C_Stop(void) { OLED_SDA_L(); OLED_SCL_H(); OLED_SDA_H(); } void OLED_I2C_SendByte(uint8_t byte) { for (uint8_t i = 0; i < 8; i++) { if (byte & 0x80) OLED_SDA_H(); else OLED_SDA_L(); OLED_SCL_H(); OLED_SCL_L(); byte <<= 1; } // 释放SDA,等待从机ACK(这里简单处理,不读取) OLED_SDA_H(); OLED_SCL_H(); OLED_SCL_L(); }这段代码里有个ACK不读取的处理,新手可以先把"每次发完字节都拉高SDA再给一个时钟"做好,不去判断从机是否应答。等需要排查问题时再用读SDA引脚的方式看ACK是否正常。实际上SSD1306正常工作时返回的ACK是拉低,只要你在示波器或逻辑分析仪上能看到SDA上的低电平脉冲,说明从机活着,问题大概率在别处。
3.4 初始化序列、坐标定位、显示字符和字符串
SSD1306的初始化序列网络上流传的版本大同小异,核心就是关闭显示、设置时钟分频、设置多路复用比、设置显示偏移、启动显示。这里给出一个精简但稳定的版本:
void OLED_WriteCmd(uint8_t cmd) { OLED_I2C_Start(); OLED_I2C_SendByte(0x78); // 8位地址形式,相当于0x3C左移一位 OLED_I2C_SendByte(0x00); // 控制字节:命令 OLED_I2C_SendByte(cmd); OLED_I2C_Stop(); } void OLED_Init(void) { HAL_Delay(100); OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0x20); OLED_WriteCmd(0x00); // 水平寻址模式 OLED_WriteCmd(0x21); OLED_WriteCmd(0x00); OLED_WriteCmd(0x7F); // 列地址范围0-127 OLED_WriteCmd(0x22); OLED_WriteCmd(0x00); OLED_WriteCmd(0x07); // 页地址范围0-7 OLED_WriteCmd(0x40); // 起始行0 OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); // 多路复用比1/64 OLED_WriteCmd(0xC0); // 扫描方向 OLED_WriteCmd(0xA1); // 列地址重映射 OLED_WriteCmd(0xA5); OLED_WriteCmd(0x00); // 显示整体亮度 OLED_WriteCmd(0x81); OLED_WriteCmd(0x7F); // 对比度 OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 时钟分频 OLED_WriteCmd(0xD3); OLED_WriteCmd(0x00); // 显示偏移0 OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); // COM引脚硬件配置 OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 电荷泵开启 OLED_WriteCmd(0xAF); // 开启显示 }写数据函数和显示字符的思路是:把显示字符转换成对应的字模数组,逐字节写到指定页面。字模本质上是把字符的16x8点阵或者16x16点阵编码成数组,然后按位写入GRAM。通常我们直接调用显示字符串函数,内部循环遍历字库:
void OLED_SetPos(uint8_t x, uint8_t y) // y是页号0-7 { OLED_WriteCmd(0xB0 + y); OLED_WriteCmd(((x & 0xF0) >> 4) | 0x10); OLED_WriteCmd(x & 0x0F); } void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t chr) { uint8_t c = chr - ' '; for (uint8_t i = 0; i < 8; i++) { OLED_WriteData(F8X16[c * 16 + i * 2]); // 上半部分字模 OLED_WriteData(F8X16[c * 16 + i * 2 + 1]); // 下半部分字模 } }这里最常见的坑是字模数组索引越界。如果定义了F8X16[][16],而chr - ' '的结果大于数组行数,程序会访问越界内存,表现就是显示到某个特殊字符时系统卡死或者字符乱码。写代码时做个长度判断非常有必要。
4. 动态显示实战:局部刷新、滚动、动画和翻转
4.1 局部刷新和整屏刷新怎么选
0.96寸OLED在IIC模式下的理论刷新率并不高,IIC标准模式100Kbps,快速模式400Kbps,SSD1306数据最大也就几百千字节每秒。一帧完整的128x64显存是1KB,加上地址字节和控制字节,全屏刷新一次大约需要发送1090字节左右。在400Kbps下,全屏刷新时间大概是20多毫秒,也就是理论最快50帧,实际加上延时、字模读取、函数调用开销,能做到20到30帧就很不错了。
这也解释了为什么OLED动态显示常见的做法是局部刷新而不是全屏刷新:
- 动态时钟只需要更新秒数那几位,没必要整屏清空重画
- 画一条移动的线或图标,可以通过先恢复旧位置像素,再画新位置像素实现
- 做动画时才考虑帧缓冲区方式
局部刷新的核心是修改OLED_SetPos函数,让它只定位到指定区域,然后连续写数据。清屏函数OLED_Clear之所以慢,是因为它要遍历128x64每个字节,如果你只更新一个16x16图标,其实只需要OLED_SetPos(x, y)后连续写32个字节,一句话就能实现局部更新。
4.2 用SSD1306硬件滚动命令实现省心动态效果
很多人不知道SSD1306内置了硬件滚动功能,不需要单片机逐像素操作,一条命令就能让整屏内容滚动。这比用软件循环刷GRAM省太多CPU资源。
启用水平滚动需要设置滚动窗口、偏移量、方向和滚动速度:
void OLED_ScrollHorizontal(uint8_t direction, uint8_t speed) { // speed: 0x00 ~ 0x07,数值越小滚动越快 OLED_WriteCmd(0x2E); // 停止滚动 OLED_WriteCmd(0x26 + direction); // 0x26右滚,0x27左滚 OLED_WriteCmd(0x00); // 虚拟字节偏移 OLED_WriteCmd(0x00); // 起始页 OLED_WriteCmd(speed); OLED_WriteCmd(0x07); // 结束页 OLED_WriteCmd(0x00); // 起始列 OLED_WriteCmd(0x7F); // 结束列 OLED_WriteCmd(0x2F); // 开始滚动 }注意滚动命令在OLED_Clear后可能被清掉,后续再想停止滚动就发送0x2E命令。还有一个坑是睡眠模式或低功耗配置后,滚动可能停止,需要重新发送滚动命令。
4.3 动画帧和帧缓冲区优化思路
如果要做比较复杂的动画,比如小飞机飞过屏幕或者波形图刷新,推荐把1KB显存缓存在单片机内存中,先在内存里改好一帧,再一次性写入OLED。对于STM32F103C8T6来说,它内部有20KB RAM,1KB显存缓冲区完全没问题。
uint8_t oled_buffer[8][128]; // 8页 x 128列 void OLED_Update(void) { for (uint8_t page = 0; page < 8; page++) { OLED_WriteCmd(0xB0 + page); OLED_WriteCmd(0x10); OLED_WriteCmd(0x00); for (uint8_t col = 0; col < 128; col++) { OLED_WriteData(oled_buffer[page][col]); } } }这种缓冲方式带来的好处是:你可以在RAM里做像素级运算,比如画线、画圆、位图叠加、算法生成的波形,最后一次性OLED_Update()把整帧推上去,动画效果明显比逐点刷GRAM流畅得多。坏处是RAM占用多了,极端情况下如果同时启用了FreeRTOS,要注意任务栈空间别开太大,避免RAM溢出导致硬件故障。
5. 避坑指南:IIC调试中我踩过的那些坑和完整排查链路
5.1 硬件IIC一调用就卡死:不是外设坏了,是总线被锁死了
使用STM32F103硬件I2C外设时,最典型的故障是程序卡死在HAL_I2C_Mem_Write内部,一进函数就出不来。绝大多数情况下不是代码配置错了,而是I2C总线出现了总线锁死(Bus Lock):SDA和SCL中有一根或两根线被拉低,总线无法产生有效的起始信号。
常见原因一个是:初始化时序还没完成,OLED模块还没上电就发起了I2C通信;另一个是通信过程中发生了错误中断,但没有正确处理,导致总线保持占用状态。解决方案有三种:
- 初始化I2C外设后、每次通信前,用GPIO手动把SCL翻转9个时钟脉冲,让挂在总线上的设备退出异常状态
- 在硬件I2C初始化之后加延时,等OLED模块稳定上电
- 检查CubeMX配置中I2C时钟速度,设置成100KHz(标准模式)而不是400KHz,提高时序容错率
软件IIC就没这么复杂,因为每一根线的电平都在自己掌控中,总线锁死概率低得多,这也是我推荐新手先用软件IIC的原因。
5.2 IIC上拉电阻到底取多大
SSD1306模块板载通常会带一组上拉电阻,如果你用的是正规模块,不需要额外接。但如果你用的是裸屏或者自己画的转接板,IIC上拉电阻取4.7KΩ到10KΩ都是正常范围。对STM32F103的GPIO配置为开漏输出来说,4.7K到10K都能保证信号上升沿足够快。
取值太小(比如低于1K)会让SDA拉低的电流太大,模块可能发热、信号变形;取值太大会让上升沿变缓,高速通信时波形圆钝,出现数据错误。如果你发现400KHz下通信不稳定,可以把上拉电阻换成2.2K或3.3K试试。还有一个经验:如果同一IIC总线上挂了多个设备,上拉电阻要综合考虑总线上所有设备的输入电容,设备越多,上拉电阻越小一些,但不要低于2K。
5.3 屏幕毫无反应时的完整排查链路
屏幕不亮、白屏、花屏是三种不同的故障现象。排查顺序我建议从简到繁,按照下面的步骤走:
第一步,检查供电:VCC有没有3.3V,GND有没有接好。用万用表量模块VCC和GND之间的电压,如果不到2V,大概率是杜邦线松动或板子供电不足。
第二步,检查SCL和SDA是否接反。软件IIC的引脚定义和实际接线不对应时,屏幕完全没反应,这个错误占比极高。把代码里的引脚换成GPIOA的另外两个端口重新编译烧录,或者把杜邦线对调一下,测试速度最快。
第三步,检查地址。把代码里OLED_I2C_SendByte的0x78改成0x7A,或者反过来,试试看屏幕是否亮。如果之前用0x78点亮过,那就是地址没错。
第四步,检查初始化序列有没有执行。在OLED_Init();前后加一个LED翻转或者串口打印,确认程序有没有跑到初始化这一步。很多情况下不是OLED的问题,而是程序根本没运行到那里。
第五步,用逻辑分析仪抓IIC总线。把SCL、SDA两个探头接上,触发方式设置为下降沿,抓一次开机初始化过程。理想的波形应该能看到:起始信号、一串地址字节、有ACK拉低信号、结尾停止信号。如果波形完全没有,就是程序没发数据;如果有波形但没有ACK,说明OLED模块没在总线上工作,基本可以确定是模块坏了或者接线问题。
完整排查的过程比直接得到答案有用得多,因为以后你做其他IIC设备,比如传感器、RTC时钟模块、EEPROM,这套排查流程全都能复用。
5.4 加了OLED显示函数后程序卡死
如果屏幕上已经显示出了内容,但加了某个OLED显示函数后整个程序卡死,多半不是IIC通信的问题,而是死循环或数组越界。
前面提到过的字模数组越界是最常见的一种。还有一个常见问题是显示字符串函数里用了while(*str)循环,但传入的字符串没有以'\0'结尾,比如这样调用:
char str[5] = {'H', 'e', 'l', 'l', 'o'}; OLED_ShowString(0, 0, str);这个数组缺了结尾的0,函数就会一直往后读内存,直到遇到0为止,期间可能读到非法地址触发HardFault。解决方法是声明时加长度:
char str[6] = {"Hello"};另外,在中断里调用OLED显示函数也是个大坑。IIC通信是慢速操作,一个字节一个字节地打时序,如果中断服务函数里调用了OLED显示,会阻塞整个中断执行,轻则影响其他中断实时性,重则导致任务堆积、看门狗超时复位。OLED显示函数尽量全都放在主循环或单独的任务里。
5.5 花屏、亮一半、闪烁,问题出在哪
花屏:表现是屏幕上出现随机亮点、字符错乱。最大的嫌疑是控制字节发错,把数据当成了命令。检查所有OLED_WriteCmd和OLED_WriteData的封装,确认命令函数第二个参数是0x00,数据函数第二个参数是0x40。
亮一半:比如上半屏正常、下半屏全黑。这往往是页地址设置不对,可能代码里的起始页写成了0xB0 + 0到0xB0 + 3,只写了4页(32行像素),自然只显示一半。0.96寸OLED是8页,0.91寸OLED是4页(128x32),如果你买的其实是0.91寸却用了0.96寸初始化序列,也会出现类似现象。购买前一定要确认分辨率,128x32和128x64的初始化参数不同。
闪烁:排除屏幕本身质量,最大嫌疑是清屏和重绘逻辑写了整屏刷新,而整屏刷新频率达不到肉眼的舒适区,看起来就像在闪。优化方法是改成局部刷新,或者把刷新率降到20帧以下、15帧以上,反而视觉上更稳定。
另一类"闪烁"其实是电源纹波造成的。OLED模块和STM32F103共用一块3.3V稳压,如果系统里还有大电流的电机、继电器、WiFi模块,频繁启停时3.3V电压波动会直接体现在OLED画面上。给OLED加一个10uF到100uF的电容,或者单独供电,能消除大部分电源噪声导致的显示异常。
5.6 STM32F103C8T6的复位和下载问题偶尔会搅局
和OLED本身无关,但新手容易忽略的一个问题:使用DAP或ST-Link下载程序失败、程序无法烧录时,会误以为OLED驱动把系统搞坏了。STM32F103C8T6在PB8/PB9附近有BOOT0和BOOT1引脚,如果BOOT0被意外拉高,芯片会进入系统存储器模式,下载器连不上,表现就是"程序烧不进去"。遇到这种情况把BOOT0跳线帽重新接地,再按一下复位键,下载器就正常了。
还有一种情况是PA11和PA12引脚被占用,导致USB虚拟串口不稳定或者代码卡住。如果你同时在使用USB转串口调试OLED,注意STM32F103的PA11/PA12是USB_DM/USB_DP引脚,别把OLED的SDA/SCL接到这两个脚上,否则USB通信和IIC会互相干扰,表现非常诡异。
驱动OLED屏幕这件事,真正难的不是那一堆初始化命令,也不是IIC协议本身,而是遇到问题时能不能按顺序排查。我个人的经验是:把每一根线的连接、每一个函数的作用都弄扎实,比背诵任何一段驱动代码都重要。你用软件IIC点亮过一次OLED,理解了起始信号、ACK、控制字节、页地址这些概念之后,以后不管换0.91寸屏、1.44寸屏,还是ESP32上的IIC驱动,都能很快上手。最后再分享一个小技巧:调试OLED时,手边常备一个逻辑分析仪,二手USB的逻辑分析仪几十块钱,关键时刻能帮你省下几个小时的时间,排查IIC问题它就是最快的路。