OLED12864 IIC通讯 SH1106驱动技术深度解析
在嵌入式设备越来越追求“小而美”的今天,一块清晰、省电又易于集成的显示屏几乎成了标配。无论是智能手环上的微小界面,还是工业仪表中的状态指示,OLED 因其自发光、高对比度和超薄特性,早已成为开发者眼中的香饽饽。其中, 128×64 分辨率的 OLED 模块 凭借成本低、接口简单、资源消耗少等优势,在各类项目中频频亮相。
这类模块虽然外观相似,但内部控制器却可能大不相同——常见的有 SSD1306 和 SH1106。两者引脚兼容、通信协议一致,甚至很多开源库都默认当作同一类处理。可一旦你发现屏幕显示总往右偏两列,或者初始化后一片漆黑,十有八九就是踩进了 SH1106 与 SSD1306 不兼容的坑里 。
问题出在哪?根本原因在于: SH1106 的显存地址映射从第2列开始,而非第0列 。这意味着如果你直接套用为 SSD1306 编写的驱动代码,画面就会整体右移两个像素位置,严重时还会导致部分数据写入无效区域,造成显示错乱或残影。
更麻烦的是,这种差异不会报错,也不会中断通信,它只是“默默”地把你的图像画歪了。对于追求精准显示的设计来说,这显然是不能接受的。
要真正掌控这块小小的屏幕,就不能只依赖现成库函数
display.begin()
一劳永逸。我们必须深入到底层,理解 SH1106 是如何通过 I²C 接收命令、组织显存、驱动像素的。只有搞清楚它的脾气,才能让每一次刷新都准确无误。
先来看核心角色: SH1106 。这是由 Sino Wealth(矽旺)推出的一款专用 OLED 驱动 IC,支持 128 列 × 64 行点阵显示,可通过 I²C 或 4 线 SPI 与主控芯片通信。它内置 DC-DC 升压电路,仅需 3.3V 或 5V 供电即可点亮 OLED 面板,极大简化了电源设计。
SH1106 内部有一块 1024 字节的显示 RAM(GRAM) ,对应 128×64 = 8192 个像素点。每个字节存储 8 个垂直排列的像素(bit),采用“页寻址模式”管理。整个屏幕被划分为 8 页,每页高 8 像素,宽 128 像素,正好占用 128 字节。例如:
- 第0页:Y=0~7
- 第1页:Y=8~15
- ……
- 第7页:Y=56~63
当你想在某个坐标 (x, y) 上点亮一个像素时,需要先计算它属于哪一页(
page = y / 8
),再确定该字节内的位位置(
bit = y % 8
),然后将数据写入对应的列地址。
但关键来了: SH1106 实际显示时,并不是从 Column 0 开始映射的,而是从 Column 2 起始!
换句话说,你在显存中写入的第0列数据,实际上并不会出现在屏幕最左边,而是被丢弃或移位处理。真正的可视区域是从 Column 2 到 Column 129 —— 可屏幕只有 128 列啊?于是最后两列又被截断。这就形成了典型的“左右各缺一列、中间偏移”的诡异现象。
这也是为什么许多开发者用 Adafruit_SSD1306 库驱动 SH1106 模块时,必须手动修改初始化序列,加入设置列偏移的指令:
// 必须设置起始列为 2,否则画面偏移!
i2c_write(0x02); // Set Lower Column Start Address to 2
i2c_write(0x10); // Set Higher Column Start Address to 0 (combined: 0x10 + 0x02 = column 2)
相比之下,SSD1306 默认从 Column 0 开始,因此不需要这一步。这个细微差别,正是两者不可互换的核心所在。
除了地址映射,SH1106 在硬件集成上也有一定优势。比如其内置升压结构更为稳定,某些型号无需外接电荷泵电容也能正常工作;而 SSD1306 往往需要精心匹配外部元件才能保证高压输出稳定。这也使得基于 SH1106 的模块在一致性方面表现更好,尤其适合批量生产的小型设备。
既然控制器这么讲究,那它是怎么和主控 MCU “对话”的呢?答案就是 I²C 协议 。
I²C 作为一种经典的双线串行总线,仅需 SCL(时钟)和 SDA(数据)两根线就能实现多设备通信,非常适合引脚紧张的嵌入式场景。OLED 模块通常挂载在 I²C 总线上作为从机,MCU(如 STM32、ESP32、Arduino 等)则扮演主机角色。
每次通信前,主机发起 Start 条件,随后发送目标设备的 7 位地址 + 读写位(R/W)。对于大多数 SH1106 模块,这个地址是
0x3C
(写)或
0x3D
(读),但在 I²C 传输中会左移一位,变成
0x78
/
0x79
。注意:有些模块支持地址跳线,实际使用前最好用 I²C 扫描工具确认真实地址。
更重要的是,SH1106 要求在每条消息开头明确告知后续数据是“命令”还是“数据”。这是通过一个特殊的 控制字节(Control Byte) 实现的:
| 控制字节 | 含义 |
|---|---|
0x00
| 接下来的是命令 |
0x40
| 接下来的是显存数据 |
举个例子,如果你想关闭显示,流程如下:
i2c_start(OLED_ADDR);
i2c_write(0x00); // 告诉 SH1106:接下来是命令
i2c_write(0xAE); // 发送“关闭显示”命令
i2c_stop();
而如果要发送图像数据,则换成:
i2c_start(OLED_ADDR);
i2c_write(0x40); // 告诉 SH1106:接下来是数据
for (int i = 0; i < 128; i++) {
i2c_write(buffer[i]); // 连续写入一行数据
}
i2c_stop();
这个机制看似繁琐,实则是为了节省引脚。如果没有控制线(如 D/C#),就必须靠软件约定来区分命令流和数据流,而控制字节正是这一逻辑的体现。
在实际开发中,建议将 I²C 速率设置为 400kHz(快速模式),既能保证刷新效率,又不至于因信号完整性不足导致通信失败。当然,首次调试时可以降频至 100kHz,排除干扰因素。
另外别忘了上拉电阻。SCL 和 SDA 都是开漏输出,必须外接 4.7kΩ 上拉电阻至 VCC 才能形成有效电平。好在大多数 OLED 模块已经集成了这些电阻,但如果遇到通信不稳定的情况,仍需检查是否缺失或阻值不当。
回到系统层面,一个典型的连接方式如下:
[MCU]
│ SCL → SCL
│ SDA → SDA
│ GND → GND
└ VCC → 3.3V/5V
↓
[OLED12864 Module]
↓
[SH1106 Driver IC]
↓
[OLED Panel 128x64]
只要接好四根线,配合正确的初始化序列,就能点亮屏幕。但“点亮”只是第一步,真正考验功力的是如何高效、稳定地更新内容。
下面是一段经过验证的 SH1106 初始化代码(以 ESP-IDF 风格为例):
void sh1106_init() {
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, OLED_I2C_ADDR | I2C_MASTER_WRITE, true);
// 进入命令模式
i2c_master_write_byte(cmd, 0x00, true);
// 关闭显示
i2c_master_write_byte(cmd, 0xAE, true);
// 设置列偏移(SH1106 特有)
i2c_master_write_byte(cmd, 0x02, true); // low col = 2
i2c_master_write_byte(cmd, 0x10, true); // high col = 0 → start at col 2
// 起始行设置
i2c_master_write_byte(cmd, 0x40, true); // start at line 0
// 段重映射与扫描方向
i2c_master_write_byte(cmd, 0xA1, true); // SEG remap (left-right flip)
i2c_master_write_byte(cmd, 0xC8, true); // COM scan reverse (up-down)
// 多路复用比
i2c_master_write_byte(cmd, 0xA8, true);
i2c_master_write_byte(cmd, 0x3F, true); // 1/64 duty
// COM 引脚配置
i2c_master_write_byte(cmd, 0xDA, true);
i2c_master_write_byte(cmd, 0x12, true); // alternative COM config
// 对比度调节
i2c_master_write_byte(cmd, 0x81, true);
i2c_master_write_byte(cmd, 0x7F, true); // max brightness
// 显示模式
i2c_master_write_byte(cmd, 0xA4, true); // normal display
i2c_master_write_byte(cmd, 0xA6, true); // non-inverted
// 开启显示
i2c_master_write_byte(cmd, 0xAF, true);
i2c_master_stop(cmd);
i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
}
这段代码的关键之处在于前三条命令设置了列起始地址为 2,这是区别于 SSD1306 的标志性操作。此外,段重映射和 COM 扫描方向可根据实际安装方向调整,避免图像倒置。
完成初始化后,就可以进行页面定位和数据写入了:
void sh1106_set_cursor(uint8_t page, uint8_t col) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, OLED_I2C_ADDR | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, 0x00, true); // command mode
i2c_master_write_byte(cmd, 0xB0 | page, true); // set page address
i2c_master_write_byte(cmd, 0x00 | (col & 0x0F), true); // lower col
i2c_master_write_byte(cmd, 0x10 | ((col >> 4) & 0x0F), true); // higher col
i2c_master_stop(cmd);
i2c_master_cmd_begin(I2C_NUM_0, cmd, 100 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
}
void sh1106_write_data(const uint8_t *data, size_t len) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, OLED_I2C_ADDR | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, 0x40, true); // data mode
i2c_master_write(cmd, data, len, true);
i2c_master_stop(cmd);
i2c_master_cmd_begin(I2C_NUM_0, cmd, 100 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
}
有了这两个基础函数,就可以逐页写入字符或图形数据。推荐的做法是在 RAM 中维护一份 1024 字节的帧缓冲区,所有绘制操作先在本地完成,再整页或局部刷新到 OLED,这样可以显著减少 I²C 通信次数,提升响应速度。
当然,开发过程中难免遇到各种“玄学”问题。以下是一些常见故障及其应对策略:
-
屏幕完全无反应?
先用 I²C 扫描工具检查设备是否存在。若找不到地址,可能是接线错误、电源未供或模块损坏。确保 SDA/SCL 正确连接,GND 共地,且上拉到位。 -
显示全黑或模糊?
检查对比度设置(0x81 后的值),尝试设为 0x7F。也可能是未正确开启显示(遗漏 0xAF 命令)。 -
画面左右偏移 2 列?
绝对是用了 SSD1306 初始化序列!务必加入0x02和0x10设置列偏移。 -
出现残影或重影?
说明旧数据未清除。每次更新前应清屏(向全部显存写 0x00)或使用反色模式临时排查。 -
通信偶尔失败?
降低 I²C 速率至 100kHz,检查布线长度是否过长,避免与其他高速信号平行走线。
还有一些工程细节值得留意:
- 在 VCC 引脚附近加一个 0.1μF 陶瓷电容,吸收 OLED 开关瞬间的大电流波动;
- 使用深色背景 UI,因为 OLED 黑色像素不发光,功耗更低;
- 不使用时调用
0xAE
关闭显示,进一步节能;
- 字体资源可用 PCtoLCD2002 工具生成,常用 6×8、8×16 ASCII,中文建议 16×16 点阵。
最终你会发现,OLED12864 + SH1106 + I²C 这套组合之所以广受欢迎,正是因为它的平衡性:够小、够省、够简单。虽然调试初期可能会被地址偏移等问题困扰,但一旦掌握其规律,就能像搭积木一样快速构建出可靠的显示系统。
更重要的是,理解 SH1106 的底层工作机制,不仅能帮你避开兼容性陷阱,也为后续接入更复杂的图形库(如 u8g2、LVGL)打下坚实基础。毕竟,任何高级抽象的背后,都是对硬件本质的深刻把握。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
1301

被折叠的 条评论
为什么被折叠?



