简介:面向STM32嵌入式开发者的IIC接口OLED显示程序工程,基于标准外设库完成OLED屏的驱动与显示,适合正在学习I2C通信协议、OLED时序控制或需要快速搭建显示模块的入门及进阶开发者。工程包内含130个文件,主要为h头文件与c源文件,并配套Keil的uvprojx工程文件、hex烧录文件、s启动文件、sct链接脚本及bat清理脚本等,整体仅383KB,结构清晰,方便直接打开编译或烧录验证。除OLED驱动外,还集成了STM32F10x系列常用的定时器、Flash、RCC、ADC、I2C、CAN等外设驱动代码,可作参考模板多次复用;OLED具备自发光、高对比度、低功耗等特性,IIC接口仅需两根信号线即可实现显示,适合在资源受限的小系统中作为人机交互方案。已有2040人学习下载,通过该工程既可查看IIC时序、初始化配置及显示函数实现,也能借助hex文件快速对比硬件效果,减少重复移植工作量。
1. 这个项目到底在解决什么:两根线、一颗芯片和一块屏
见过不少同行在拿到“STM32 基于IIC的OLED显示程序”这类需求时,第一反应是找例程、改引脚、烧录,结果屏幕死活不亮,最后发现是 I2C 地址 0x3C 和 0x3D 一字之差。IIC 和 I2C 是同一总线的两种写法,这个项目往深了说就是两件事:把 I2C 的时序跑对,把 SSD1306 这颗驱动芯片的显存协议理清。它适合任何想做小体积显示界面的场景,DIY 温湿度计、音频电平表、工装调试信息屏,I2C OLED 都是引脚占用最少、上手最快的方案。下面把原理、最小工程、参数设置、排障套路和刷新优化一次讲透,照着做能点亮,点亮后能调稳。
2. I2C 总线与 SSD1306 的通信模型:从两根线到一块屏
2.1 I2C 与 OLED 之间到底在传什么:控制字节与数据字节的分工
I2C OLED 模块的硬件结构很简单:SCL 走时钟,SDA 走数据,两根线串联起单片机和 SSD1306。SSD1306 内部自带一块 1024 字节的显示 RAM,单片机只要把显示内容写进这颗 RAM,芯片就会自己扫描并驱动 OLED 像素,MCU 不需要持续刷新屏幕。这正是 I2C 速度不高也够用的根本原因。
I2C 每次传输的基本帧是:起始位、7 位从机地址加读写位、ACK、数据字节、停止位。SSD1306 在 I2C 协议上多加了一个“控制字节”,用来告诉芯片后面跟着的字节到底是命令还是显示数据。这个控制字节是 I2C OLED 和普通 I2C 外设最大的区别,也是新手最容易踩坑的地方。
| 控制字节 | 含义 | 典型使用 |
|---|---|---|
| 0x00 | 后随字节为命令 | 初始化、设置页地址、设置对比度 |
| 0x40 | 后随字节为显示数据 | 向显示 RAM 写入像素数据 |
一个常见错误是把命令用数据通道发出去,SSD1306 会把它当成像素数据写进显存,表现就是“有波形、屏幕乱码”。如果你准备好的数据包是[地址][0x40][内容],那就是数据传输;如果是[地址][0x00][内容],那就是命令传输。两者在逻辑分析仪上看第二字节就能区分。
2.2 SSD1306 的显存结构与页寻址:128x64 不是一整块
SSD1306 的 GRAM 是 128 列乘 64 行,按 8 个像素行分成一页,一共 8 页。每页有 128 字节,每个字节的 8 个 bit 正好对应一列上的 8 个像素。所以显存布局不是“一行一行”,而是“一页一页”,页地址模式下的常用命令是0xB0 ~ 0xB7(选择第 0 到第 7 页),列地址是0x00 ~ 0x7F。
在这个模式下,写入一个字节后列地址会自动加一,但写到页尾会回卷到本页开头,不会自动跳到下一页。这就是为什么很多例程里每写一页前都要重新设置页地址和列地址。另一个可选模式是水平寻址模式,连续写 1024 字节会自动跨页,代码短一些,但出问题时更难定位。我一般默认用页寻址模式,逻辑直观,调试时也容易对着显存计算位置。
画点时的坐标换算公式是:页号 = y / 8,页内偏移 = y % 8,目标索引 = 页号 * 128 + x。这个公式后面写驱动函数时每一步都会用到,建议先记下来。
2.3 选型与接线:0.96 寸模块的常见差异与电平适配
市面上 0.96 寸 I2C OLED 绝大多数控制芯片是 SSD1306,但也有 SH1106、SSD1315 等兼容或替代芯片。SH1106 的页宽度是 132 字节,列地址映射和 SSD1306 不一样,直接套 SSD1306 例程会出现错位或花屏,买模块时最好确认芯片型号再动手。
显示分辨率也会影响初始化参数:128x64 和 128x32 的屏,多路复用比命令0xA8的值不同,前者是0xA8 0x3F,后者是0xA8 0x1F。接线方面,常见的 4 针模块只需要接电源和两根 I2C 线。模块一般自带 I2C 上拉电阻,如果买到的模块没有上拉,需要自己加 4.7k 电阻到 VCC。
| OLED 引脚 | 方向 | 接 STM32 |
|---|---|---|
| VCC | 电源输入 | 3.3V |
| GND | 地 | GND |
| SCL | I2C 时钟 | 任选 GPIO,开漏输出 |
| SDA | I2C 数据 | 任选 GPIO,开漏输出 |
电平问题也要提前确认:模块上拉电阻接的是模块 VCC,如果 VCC 是 5V,总线高电平就是 5V,STM32 引脚不一定扛得住。最稳的做法是模块和单片机都用 3.3V 供电。还有一点,模块地址由 SA0 引脚的电平决定,默认 0x3C,部分模块焊了跳线后是 0x3D,读一下模块背面丝印就能确认。
3. 最小驱动工程:I2C 底层、初始化序列与显示函数
3.1 底层 I2C:硬件 I2C 还是模拟 I2C 的选择
STM32 的硬件 I2C 外设功能完善,但配套的事件标志和错误处理比较绕,很多开发者调不通时会怀疑人生。我的习惯是:新工程先用 GPIO 模拟 I2C 跑通显示逻辑,后续有性能需求再切换到硬件 I2C。模拟 I2C 代码量小、时序完全可控、出错时也好排查,作为第一版驱动非常合适。
模拟 I2C 的核心是实现起始、停止、发送字节三个操作。注意 SCL 和 SDA 都要配置为开漏输出,并外接上拉电阻,这样 SDA 置 1 时总线被上拉释放,才能正确读取从机的 ACK。下面这段代码是模拟 I2C 的骨架:
#define OLED_SCL_PORT GPIOB #define OLED_SCL_PIN GPIO_PIN_8 #define OLED_SDA_PORT GPIOB #define OLED_SDA_PIN GPIO_PIN_9 static void I2C_Delay(void) { // 在 72MHz 主频下,空循环约 5 次约 1us for (volatile uint32_t i = 0; i < 5; i++); } static void I2C_Start(void) { HAL_GPIO_WritePin(OLED_SDA_PORT, OLED_SDA_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(OLED_SCL_PORT, OLED_SCL_PIN, GPIO_PIN_SET); I2C_Delay(); HAL_GPIO_WritePin(OLED_SDA_PORT, OLED_SDA_PIN, GPIO_PIN_RESET); // SCL 高时 SDA 拉低 I2C_Delay(); HAL_GPIO_WritePin(OLED_SCL_PORT, OLED_SCL_PIN, GPIO_PIN_RESET); } static void I2C_Stop(void) { HAL_GPIO_WritePin(OLED_SDA_PORT, OLED_SDA_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(OLED_SCL_PORT, OLED_SCL_PIN, GPIO_PIN_SET); I2C_Delay(); HAL_GPIO_WritePin(OLED_SDA_PORT, OLED_SDA_PIN, GPIO_PIN_SET); // SCL 高时 SDA 拉高 } static uint8_t I2C_SendByte(uint8_t dat) { for (uint8_t i = 0; i < 8; i++) { uint8_t bit = (dat & 0x80) ? GPIO_PIN_SET : GPIO_PIN_RESET; HAL_GPIO_WritePin(OLED_SDA_PORT, OLED_SDA_PIN, bit); dat <<= 1; I2C_Delay(); HAL_GPIO_WritePin(OLED_SCL_PORT, OLED_SCL_PIN, GPIO_PIN_SET); I2C_Delay(); HAL_GPIO_WritePin(OLED_SCL_PORT, OLED_SCL_PIN, GPIO_PIN_RESET); } // 释放 SDA,读取从机 ACK HAL_GPIO_WritePin(OLED_SDA_PORT, OLED_SDA_PIN, GPIO_PIN_SET); I2C_Delay(); HAL_GPIO_WritePin(OLED_SCL_PORT, OLED_SCL_PIN, GPIO_PIN_SET); I2C_Delay(); uint8_t ack = (HAL_GPIO_ReadPin(OLED_SDA_PORT, OLED_SDA_PIN) == GPIO_PIN_RESET) ? 1 : 0; HAL_GPIO_WritePin(OLED_SCL_PORT, OLED_SCL_PIN, GPIO_PIN_RESET); return ack; }这段代码里I2C_Delay控制时钟频率,空循环次数决定 SCL 高低电平的持续时间。72MHz 主频下循环 5 次大约产生 1us 级延时,实际频率会在 200kHz 上下,属于 I2C 快速模式范围。如果屏幕在长线缆下不稳定,把循环次数加大到 10~20 次,降速到 100kHz 标准模式,稳定性会好很多。I2C_SendByte的返回值是从机 ACK,发送完后检查它是否为 1,就能确认设备是否在线。这个函数后面会被 OLED 驱动反复调用,也是排查问题的关键锚点。
3.2 写命令与写数据:两个函数的本质区别
OLED 驱动层只要封装两个函数:写命令和写数据。它们的区别只在于控制字节是 0x00 还是 0x40。这里我推荐一次传输发两个字节,控制字节和内容拼在一个包里,减少 I2C 起始/停止的次数,比逐字节调用更高效。
#define OLED_ADDR 0x3C // 模块实际地址,部分模块为 0x3D static void OLED_WriteCmd(uint8_t cmd) { I2C_Start(); I2C_SendByte(OLED_ADDR << 1); // 7 位地址左移一位,再加写位 0 I2C_SendByte(0x00); // 控制字节:命令 I2C_SendByte(cmd); I2C_Stop(); } static void OLED_WriteData(uint8_t dat) { I2C_Start(); I2C_SendByte(OLED_ADDR << 1); I2C_SendByte(0x40); // 控制字节:数据 I2C_SendByte(dat); I2C_Stop(); }注意这里OLED_ADDR << 1是把 7 位地址 0x3C 左移一位,再加上写位 0,变成 8 位地址 0x78。如果你用的 HAL 库硬件 I2C 接口,有些版本的HAL_I2C_Master_Transmit接收的是 8 位地址,有些版本接收 7 位地址,移植时先查一下当前 HAL 版本的实现,这是最常见的移植报错点。
两个函数封装好后,OLED 的所有操作都是“先发命令、再发数据”的组合。初始化序列是一连串写命令,显示内容是一连串写数据,命令和数据不能混在一个包里,否则芯片会把命令解释成像素数据,屏幕立刻花掉。
3.3 初始化序列:每个命令在干什么
SSD1306 的初始化序列在网上有各种版本,命令大同小异,但顺序很重要。标准做法是:先关闭显示、配置基本参数、开启电荷泵、设置寻址模式、最后开启显示。下面这段数组是 128x64 I2C 屏的常用初始化命令序列:
static const uint8_t ssd1306_init_cmds[] = { 0xAE, // 关闭显示,配置期间不要点亮屏幕 0xD5, 0x80, // 显示时钟分频与振荡器频率 0xA8, 0x3F, // 多路复用比 1/64,对应 128x64 0xD3, 0x00, // 显示偏移 0 0x40, // 起始行 0 0x8D, 0x14, // 开启内部电荷泵,这是点亮的关键 0x20, 0x02, // 页寻址模式 0xA1, // 段重映射,列 0 映射到 SEG127 0xC8, // COM 扫描方向,上下不颠倒 0xDA, 0x12, // COM 引脚硬件配置,128x64 常用值 0x81, 0xCF, // 对比度,默认 0x7F 也可视效果调整 0xD9, 0xF1, // 预充电周期 0xDB, 0x40, // VCOMH 倍率 0xA4, // 显示内容来自 GRAM 0xA6, // 正常显示,不反显 0xAF // 开启显示 }; for (uint32_t i = 0; i < sizeof(ssd1306_init_cmds); i++) { OLED_WriteCmd(ssd1306_init_cmds[i]); }逐个解释关键参数:0x8D 0x14是内部电荷泵开关,如果漏掉这一条,屏幕可能整体发暗或完全黑屏,这是“初始化后不亮”的高频原因。0xA8 0x3F必须和屏幕分辨率匹配,用了 128x32 屏就要改成0xA8 0x1F。0x20 0x02选择了页寻址模式,如果你后面想用水平寻址模式,这里改成0x20 0x00,否则刷新函数里的页跳转逻辑要一起调整。0xA1和0xC8组合决定画面左右、上下方向,如果显示内容镜像或倒置,改这两个命令比改坐标代码更省事。
初始化完成后要立即清屏并刷新一次,否则显存里是随机值,屏幕上可能显示花点。清屏就是把帧缓冲数组全部写 0,然后调用一次刷新函数。
3.4 刷新与显示:画点、画字、显示中文字符串
清屏和刷新依赖一个全局帧缓冲数组。SSD1306 的显存大小是 128 乘 64 除以 8,等于 1024 字节,定义一个uint8_t fb[1024],用页号和列号索引。画点函数只操作这个数组,不直接和 I2C 打交道:
uint8_t fb[1024]; // 8 页 * 128 列 void OLED_SetPixel(int16_t x, int16_t y, uint8_t on) { if (x < 0 || x > 127 || y < 0 || y > 63) { return; } uint16_t idx = (y / 8) * 128 + x; uint8_t bit = 1 << (y % 8); if (on) { fb[idx] |= bit; } else { fb[idx] &= ~bit; } }刷新函数是整个显示链路的核心。它按页寻址模式逐页发送,每页先设置页地址和列地址,再连续写 128 个字节:
void OLED_Refresh(void) { for (uint8_t page = 0; page < 8; page++) { OLED_WriteCmd(0xB0 + page); // 选择第 page 页 OLED_WriteCmd(0x00); // 列地址低 4 位 OLED_WriteCmd(0x10); // 列地址高 4 位 for (uint8_t col = 0; col < 128; col++) { OLED_WriteData(fb[page * 128 + col]); } } }列地址命令有个细节:0x00是低 4 位,0x10是高 4 位,组合起来列地址是 0。如果漏写高 4 位或写成0x00 0x00,数据会写到错误列区域,屏幕出现整体偏移。刷新时每次都设置页地址列地址,能保证数据从正确位置开始写入,避免连续刷新时“上次停在哪、这次从哪继续”的错位问题。
显示字符时,用 8x16 的 ASCII 字模数组,每个字符 16 字节,对应 8 列 16 行像素。核心逻辑是把字模的每一位映射到帧缓冲里:
void OLED_DrawChar(uint8_t x, uint8_t y, char c, uint8_t on) { // 简化示例:font_8x16 为预置字模表,每个字符 16 字节 for (uint8_t row = 0; row < 16; row++) { uint8_t data = font_8x16[(uint8_t)c - 0x20][row]; for (uint8_t col = 0; col < 8; col++) { if (data & (0x80 >> col)) { OLED_SetPixel(x + col, y + row, on); } } } OLED_Refresh(); // 实际工程里应该改成批量绘制后统一刷新 }注意字模的取模方式要和显示代码保持一致。网上常见的是纵向取模、高位在前,如果你从别的项目复制字模表,先确认取模设置,否则字符会上下颠倒或左右镜像。中文字符同理,16x16 点阵汉字占 2 页 32 字节,显示位置要按页对齐,绘制时先算页号再写入两页数据。这套最小工程跑通后,显示字符串、数字、汉字都只是往帧缓冲里画点的问题,底层 I2C 驱动不用再动。
4. I2C OLED 避坑排查:从黑屏到花屏的五个现场
4.1 屏幕完全不亮:先查 ACK 再查地址
现象是程序跑起来了,逻辑分析仪能看到 SCL 和 SDA 有波形,但屏幕一直黑的。此时第一件事不是调初始化序列,而是确认从机有没有正确应答。I2C 总线上如果地址不对或模块没上电,从机不会拉低 SDA 产生 ACK,发送方看到的一直是高电平。写一个 I2C 扫描函数,逐个地址发送并检查 ACK,很快就能定位问题:
void I2C_Scan(void) { for (uint8_t addr = 1; addr < 127; addr++) { I2C_Start(); uint8_t ack = I2C_SendByte(addr << 1); I2C_Stop(); if (ack) { printf("I2C device found at 0x%02X\n", addr); } } }这个扫描函数能直接暴露地址到底是 0x3C 还是 0x3D,也能验证模拟 I2C 底层有没有写对。扫不到设备时,逐一检查模块 VCC 是否 3.3V、SCL 和 SDA 是否接反、引脚是否配成了推挽输出而不是开漏。开漏配置错误时,SDA 被强拉高,从机根本无法拉低总线,ACK 永远不会出现。
4.2 点亮但花屏:页地址错乱与控制字节写错
现象是屏幕亮了,但显示内容上下错位、左右偏移或出现随机噪点。最常见原因是每页刷新时没有设置页地址,或者数据写入时把命令和数据通道搞混了。比如你用写数据函数去发送初始化命令,SSD1306 会把命令字节当作像素数据写进显存,初始化图形就变成了一堆乱码。
排查步骤很固定:先确认初始化部分每条命令是否都走OLED_WriteCmd,确认刷新函数每次循环是否都有0xB0 + page和列地址设置,再检查控制字节是不是写错了。SH1106 这类兼容芯片的列地址映射和 SSD1306 不同,套用例程时也会出现横向错位,直接查模块芯片型号最稳妥。花屏还有一个隐性原因:初始化序列里的电荷泵命令0x8D 0x14没执行,屏幕供电不足,显示内容对比度极低,看起来像“花了一半”。
4.3 显示闪烁:刷新姿势不对
画面能正常显示,但数字或图表更新时明显闪烁,这个现象在刷屏频率超过 10Hz 时尤其突出。多数情况是代码里每次刷新前先执行了清屏命令,或者反复发送0xAE关屏再0xAF开屏。清屏会把整个显存清零,然后再一页页写入新内容,这个过程中屏幕会在“亮->暗->亮”之间切换,人眼就感觉到了闪烁。
正确做法是把清屏只放在初始化阶段,运行时刷新直接覆盖写新内容,不要在刷新函数内部开关显示。动画场景下要用帧缓冲配合局部刷新,只更新变化区域,而不是每次把全部 1024 字节重发一遍。另外,刷新函数的 I2C 速度太低也会放大闪烁效果,确认模拟 I2C 的延时没有把时钟拖到几十 kHz。
4.4 硬件 I2C 卡死或复位后 BUSY 标志不消失
使用 STM32 硬件 I2C 外设时,偶尔会出现程序卡在发送函数里,或者复位后外设 BUSY 标志一直为 1。常见原因是 SDA 引脚电平没有按预期释放:如果引脚配置成了推挽输出,或者外部上拉电阻缺失,从机拉低总线后总线状态无法恢复,I2C 状态机就卡死了。另一个典型场景是传输过程中 MCU 被复位,总线停留在半途状态,硬件 I2C 认为总线仍被占用。
解决思路有两个方向:一是把 SCL 和 SDA 引脚配置为开漏输出,并确保模块或板卡上有 4.7k 上拉电阻;二是在初始化 I2C 外设之前,先对 GPIO 做一次完整复位,必要时执行软件复位 I2C 外设。如果硬件 I2C 反复出现莫名其妙的卡死,直接换回模拟 I2C 也是一种务实选择,毕竟 OLED 显示对实时性要求不高,模拟 I2C 的稳定性反而更好。
4.5 5V 系统接 3.3V OLED:电平不匹配发热和锁死
有的开发者习惯用 5V 单片机的 I2C 引脚直接接 3.3V 的 OLED 模块,结果屏幕不亮或总线异常,摸一下模块还有点烫。原因是模块上的 I2C 上拉电阻接到模块 VCC 3.3V,总线上拉电平是 3.3V,MCU I/O 是 5V 推挽输出,灌电流方向不对,甚至可能损坏模块。
解决方法是先确认模块 VCC 的供电来源:如果模块由 5V 供电但号称兼容 3.3V 逻辑,要看它是否内置电平转换电路,没有的话不能直接连;最安全的是给模块单独供 3.3V,MCU I/O 也工作在 3.3V。电平问题在调试初期就确认好,能省掉后面一大半的玄学排查。
5. 提升刷新率:帧缓冲、局部刷新与 DMA 的取舍
5.1 帧缓冲:绘制与显示解耦
前面已经引入了fb[1024],这里专门说说为什么帧缓冲是 OLED 驱动的基本盘。没有帧缓冲时,每个画点操作都直接触发一次 I2C 发送,画 100 个点就是 100 次总线事务,速度慢且闪烁明显。有了帧缓冲,所有绘制函数都往内存里写,最后统一刷一次显存,I2C 事务数量降到最低。
更进一步,帧缓冲让“绘制”和“发送”彻底分离。比如在显示波形的场景里,先在帧缓冲里把整条曲线算好、画好,再一次性刷新;在菜单界面里,可以先绘制背景和文字再整体上屏,避免用户看到半个界面。清屏函数也只是把fb全部置 0,不操作硬件:
void OLED_Clear(void) { memset(fb, 0x00, sizeof(fb)); }页面绘制代码全部以fb为画布,避免任何绘制接口直接调用 I2C。这个习惯能让显示模块的代码结构清晰很多,调试时也能随时把fb内容通过串口打印出来,对比屏幕实际显示,定位到底是绘制问题还是传输问题。
5.2 局部刷新:只更新变化的页区域
整屏刷新是 1024 字节的 I2C 传输,加上每页的地址设置命令,总耗时在大几百微秒到几毫秒之间。对于只改变一小块区域的界面来说,这个开销是浪费的。局部刷新的思路是:用一个 8 位的脏页标记,哪位为 1 就说明对应页的内容有变化,刷新时只发送这些页的数据。
static uint8_t dirty_pages = 0x00; // bit0~bit7 对应第 0~7 页 void OLED_MarkDirtyPage(uint8_t page) { dirty_pages |= (1 << page); } void OLED_RefreshDirty(void) { for (uint8_t page = 0; page < 8; page++) { if (!(dirty_pages & (1 << page))) { continue; } OLED_WriteCmd(0xB0 + page); OLED_WriteCmd(0x00); OLED_WriteCmd(0x10); for (uint8_t col = 0; col < 128; col++) { OLED_WriteData(fb[page * 128 + col]); } dirty_pages &= ~(1 << page); } }局部刷新的最小单位是一整页,也就是 8 个像素行。如果你只改了 y 坐标为 3 的一个像素,刷新时也必须把整个第 0 页发出去,这是 SSD1306 页寻址的特性决定的。所以绘制元素时尽量把重要内容放在同一页里,或者按页对齐元素位置,让一次刷新覆盖尽量少的页。比如时钟数字是 16 像素高,正好占 2 页,刷新两个数字只需要发 256 字节,比全屏 1024 字节快得多。
5.3 DMA I2C 的取舍:省了 CPU 但没省总线时间
很多开发者把 OLED 刷新率上不去归咎于模拟 I2C 占用 CPU,想用硬件 I2C 加 DMA 解决。这个方向有一定道理:DMA 能让 CPU 在 I2C 传输期间去干别的事,但总线物理速率的上限是 400kHz,全屏 1024 字节的传输时间并没有因为 DMA 而缩短。如果场景是持续全屏动画,DMA 带来的体感提升有限,却引入了更复杂的事件管理和 NACK 处理。
| 方案 | 适合场景 | CPU 占用 | 工程复杂度 |
|---|---|---|---|
| 模拟 I2C + 局部刷新 | 菜单、数字、图表局部变化 | 传输期间占用 | 低 |
| 硬件 I2C + DMA | 全屏动画、波形滚动 | 低 | 高 |
| SPI 接口 OLED + DMA | 高帧率全屏显示 | 低 | 中 |
我一般这样决策:界面以局部更新为主时,模拟 I2C 加局部刷新足够稳定;真需要高帧率全屏动画,直接换 SPI 接口的 OLED 更实际,SPI 的时钟能跑到 MHz 级别,DMA 的价值才真正体现出来。不要在 I2C 上死磕 DMA,这个接口的物理带宽决定了它的上限。
6. 用逻辑分析仪验证 I2C:一次写入看清 ACK 与命令数据
逻辑分析仪是调试 I2C OLED 最值得投资的工具,几十块的入门款够用。接线时把逻辑分析仪的通道 0 接 SCL,通道 1 接 SDA,共地后设置采样率 1MHz 以上,触发方式选下降沿,这样能稳定抓到起始位。
| 逻辑分析仪 | OLED 引脚 | 说明 |
|---|---|---|
| CH0 | SCL | 采样时钟线 |
| CH1 | SDA | 采样数据线 |
| GND | GND | 必须共地 |
抓一次初始化过程,波形里第一字节应该是地址字节 0x78(7 位地址 0x3C 左移加写位 0),后面紧跟 ACK 低电平。第二字节是控制字节,如果你发的是命令,这里应该是 0x00;如果是显示数据,应该是 0x40。很多花屏问题的根源在波形上非常直观:控制字节错误时,一眼就能看到第二字节不是预期值。
我自己的一个习惯是:收到一块新屏或者换了一个新平台,第一件事不是直接接显示逻辑,而是先抓一次总线波形,确认地址、ACK、控制字节三个关键点。这个习惯帮我挡掉了很多“屏幕不亮”的瞎猜时间。OLED 驱动一旦跑通,后面所有显示功能的开发都只是在帧缓冲上做文章,希望帮到你。
本文还有配套的精品资源,点击获取