1. OLED驱动基础:SSD1306/SSH1106控制器原理、I2C/SPI接口时序、像素映射与显存结构
各位同学,咱们今天正式开讲OLED驱动。说实话,这块内容我做了快十年嵌入式,踩过的坑能绕实验室三圈。但别怕,我会把最核心的东西掰开了讲给你听。
OLED驱动,说白了就是两件事:怎么让屏幕亮起来,以及怎么高效地控制它。今天这一章,咱们先把底层的控制器原理、接口时序和显存结构搞清楚。这些是后面所有优化策略的基础。
1.1 SSD1306与SSH1106:两个主流控制器的恩怨情仇
市面上小尺寸OLED屏,绝大多数用的就是这两颗芯片。SSD1306是Solomon Systech的经典款,SSH1106则是Sino Wealth的后来者。我最早接触OLED时用的是SSD1306,后来有个项目因为供货问题被迫换成SSH1106,结果发现驱动代码几乎不用改——嗯,这里要注意,几乎不等于完全。
| 特性 | SSD1306 | SSH1106 |
|---|---|---|
| 最大分辨率 | 128×64 | 128×64 |
| 内部显存 | 128×64 bits (1KB) | 128×64 bits (1KB) |
| 页模式 | 8页,每页128字节 | 8页,每页128字节 |
| I2C地址 | 0x3C / 0x3D | 0x3C / 0x3D |
| SPI支持 | 4线/3线 | 4线/3线 |
| 命令集兼容性 | 标准 | 约95%兼容 |
| 典型功耗 | ~20mA (全亮) | ~18mA (全亮) |
SET_DISPLAY_OFFSET命令的默认值不同。如果你直接把SSD1306的初始化代码搬过去,屏幕可能会整体偏移8个像素。别问我怎么知道的——那次调试花了我整整一个下午。
1.2 I2C与SPI接口时序:选对总线,事半功倍
这两种接口我都用过。我个人习惯是:引脚紧张选I2C,追求刷新率选SPI。为什么?咱们看数据。
I2C接口
I2C只需要两根线:SCL和SDA。标准模式100kHz,快速模式400kHz。对于128×64的OLED,如果你要刷新全屏,I2C的瓶颈很明显。
算一笔账:
- 全屏数据量:128 × 64 / 8 = 1024字节
- I2C每字节需要9个时钟(含ACK)
- 400kHz下,理论传输时间:1024 × 9 / 400000 ≈ 23ms
- 加上命令开销,实际刷新率约30fps
够用吗?静态显示绰绰有余。但如果你要做动画或者滚动效果,就会感觉卡顿。
SPI接口
SPI就快多了。4线制:CS、DC、SCLK、MOSI。有些屏还支持MISO,但OLED基本用不上。SPI时钟可以跑到10MHz甚至更高。
同样算一笔账:
- 全屏数据量:1024字节
- SPI每字节8个时钟,无ACK开销
- 10MHz下,理论传输时间:1024 × 8 / 10000000 ≈ 0.82ms
- 加上命令开销,实际刷新率轻松超过100fps
1.3 像素映射与显存结构:屏幕背后的内存游戏
OLED的显存结构,是很多新手栽跟头的地方。你想想看,屏幕是128列×64行,但显存不是按行排列的,而是按页排列的。
SSD1306把64行分成8页,每页8行。每一页有128个字节,每个字节对应一列上的8个像素点。
为什么会这样设计?说白了,是为了减少显存寻址的开销。你想想看,如果按行排列,每次写一个字节只能控制一行上的8个点。但按页排列,一个字节就能控制一列上的8个点,配合页地址自动递增,刷屏效率高得多。
显存操作的核心命令
要操作显存,你需要掌握这几个命令:
- 0x21 — 设置列地址范围
- 0x22 — 设置页地址范围
- 0xB0~0xB7 — 设置起始页地址
- 0x00~0x0F — 设置列地址低4位
- 0x10~0x1F — 设置列地址高4位
0x20命令的水平寻址模式。这样写完一个字节后,列地址自动+1,写满128列后自动跳到下一页。刷全屏时,你只需要连续发送1024个数据字节,不需要反复设置地址。
1.4 实战:初始化一个SSD1306
光说不练假把式。咱们写一段实际的初始化代码。这是我个人习惯的写法,经过多个项目验证,稳定可靠。
// SSD1306 初始化序列(I2C接口)
void OLED_Init(void) {
// 软复位
OLED_WriteCmd(0xAE); // 关闭显示
// 显示配置
OLED_WriteCmd(0xD5); // 设置振荡频率
OLED_WriteCmd(0x80); // 默认值
OLED_WriteCmd(0xA8); // 设置多路复用比
OLED_WriteCmd(0x3F); // 64行
OLED_WriteCmd(0xD3); // 设置显示偏移
OLED_WriteCmd(0x00); // 无偏移
OLED_WriteCmd(0x40); // 设置显示起始行(行0)
OLED_WriteCmd(0x8D); // 电荷泵设置
OLED_WriteCmd(0x14); // 开启电荷泵
// 内存寻址模式
OLED_WriteCmd(0x20); // 设置内存寻址模式
OLED_WriteCmd(0x00); // 水平寻址模式
// 段和COM配置
OLED_WriteCmd(0xA1); // 段重映射(列127映射到SEG0)
OLED_WriteCmd(0xC8); // COM扫描方向(从COM63到COM0)
// 对比度
OLED_WriteCmd(0x81); // 设置对比度
OLED_WriteCmd(0xCF); // 对比度值
// 预充电周期
OLED_WriteCmd(0xD9); // 设置预充电周期
OLED_WriteCmd(0xF1); // [1] 时钟周期, [1] 预充电周期
// COM引脚配置
OLED_WriteCmd(0xDA); // 设置COM引脚配置
OLED_WriteCmd(0x12); // 交替COM引脚配置
// 消隐
OLED_WriteCmd(0xDB); // 设置VCOMH消隐级别
OLED_WriteCmd(0x40); // VCOMH = 0.77 x VCC
// 清屏
OLED_Clear();
// 开启显示
OLED_WriteCmd(0xAF); // 开启显示
}
1.5 像素映射的实战陷阱
咱们来写一个实际画点的函数。你想想看,要在(x, y)位置画一个点,需要做三步:
- 计算目标字节在显存中的位置
- 读取该字节的当前值
- 修改对应的位,然后写回
// 在(x, y)位置画一个点
void OLED_DrawPixel(uint8_t x, uint8_t y, uint8_t color) {
uint8_t page = y / 8; // 计算页号
uint8_t bit_pos = y % 8; // 计算位位置
uint8_t bit_mask = 1 << bit_pos; // 生成位掩码
// 设置页地址和列地址
OLED_WriteCmd(0xB0 + page); // 设置页地址
OLED_WriteCmd(0x00 + (x & 0x0F)); // 设置列地址低4位
OLED_WriteCmd(0x10 + (x >> 4)); // 设置列地址高4位
// 读取当前值(SSD1306不支持读,需要维护本地显存)
// 这里假设我们有一个本地缓冲区 fb[8][128]
if (color) {
fb[page][x] |= bit_mask; // 置1
} else {
fb[page][x] &= ~bit_mask; // 置0
}
// 写回
OLED_WriteData(fb[page][x]);
}
1.6 本章小结
这一章咱们把OLED驱动最基础的东西讲清楚了:
- 控制器:SSD1306和SSH1106基本兼容,但要注意细节差异
- 接口:I2C省引脚但慢,SPI快但多两根线
- 显存:页模式是核心,理解它才能正确操作像素
- 初始化:顺序很重要,电荷泵、对比度、扫描方向一个都不能错
这些内容看起来基础,但我在实际项目中见过太多人在这里栽跟头。把这一章吃透,后面的低功耗刷新策略和帧缓冲管理,你才能游刃有余。