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 (全亮)
我曾经踩过的坑:SSH1106的初始化序列中,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
我的建议:如果你在做可穿戴设备或者电池供电的产品,I2C就够了。但如果你要做游戏机、示波器或者任何需要快速刷新的应用,老老实实上SPI。

1.3 像素映射与显存结构:屏幕背后的内存游戏

OLED的显存结构,是很多新手栽跟头的地方。你想想看,屏幕是128列×64行,但显存不是按行排列的,而是按页排列的。

SSD1306把64行分成8页,每页8行。每一页有128个字节,每个字节对应一列上的8个像素点。

SSD1306 显存结构:页模式下的像素映射 页0 (行0-7) 128 字节 → 128 列 页1 (行8-15) 128 字节 → 128 列 页2 (行16-23) 128 字节 → 128 列 ⋮ 页7 (行56-63) 128 字节 → 128 列 单个字节的位映射(以页0第0列为例) D7 D6 D5 D4 D3 D2 D1 D0 行0 行7 D7对应行0 D0对应行7

为什么会这样设计?说白了,是为了减少显存寻址的开销。你想想看,如果按行排列,每次写一个字节只能控制一行上的8个点。但按页排列,一个字节就能控制一列上的8个点,配合页地址自动递增,刷屏效率高得多。

显存操作的核心命令

要操作显存,你需要掌握这几个命令:

  • 0x21 — 设置列地址范围
  • 0x22 — 设置页地址范围
  • 0xB0~0xB7 — 设置起始页地址
  • 0x00~0x0F — 设置列地址低4位
  • 0x10~0x1F — 设置列地址高4位
小技巧:如果你用SPI,可以开启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)位置画一个点,需要做三步:

  1. 计算目标字节在显存中的位置
  2. 读取该字节的当前值
  3. 修改对应的位,然后写回
// 在(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]);
}
关键点:SSD1306不支持从显存读取数据!这意味着你不能先读后写。你必须维护一个本地的帧缓冲区,所有操作都在本地完成,然后一次性刷到屏幕上。这个缓冲区的大小是1024字节(128×64/8)。对于STM32来说,这点内存不算什么,但对于资源受限的MCU,就需要精打细算了。

1.6 本章小结

这一章咱们把OLED驱动最基础的东西讲清楚了:

  • 控制器:SSD1306和SSH1106基本兼容,但要注意细节差异
  • 接口:I2C省引脚但慢,SPI快但多两根线
  • 显存:页模式是核心,理解它才能正确操作像素
  • 初始化:顺序很重要,电荷泵、对比度、扫描方向一个都不能错

这些内容看起来基础,但我在实际项目中见过太多人在这里栽跟头。把这一章吃透,后面的低功耗刷新策略和帧缓冲管理,你才能游刃有余。