一、OLED驱动基础:SSD1306驱动芯片介绍、I2C通信协议详解、OLED像素矩阵与显存映射

各位同学,欢迎来到《血压计OLED屏幕驱动与UI设计实战》的第一课。

说实话,做嵌入式UI这么多年,我接触过各种各样的屏幕。从最古老的段式LCD,到TFT彩屏,再到今天我们要讲的OLED。但要说哪个屏幕最适合做小尺寸、低功耗的医疗设备?我个人首选就是SSD1306驱动的OLED屏。为什么?因为它简单、便宜、功耗极低,而且显示效果在室内环境下非常棒。

1.1 SSD1306驱动芯片介绍

SSD1306,这是一颗来自Solomon Systech的驱动芯片。它专门为OLED点阵屏而生。我最早接触它是在2016年,做一个便携式血氧仪的项目。当时选型时对比了好几款驱动芯片,最后还是选了它。

这颗芯片有几个关键特性,我给大家列一下:

  • 分辨率支持:常见的是128×64像素,也有128×32的版本。我们血压计项目用128×64就足够了。
  • 通信接口:支持I2C、SPI、并行接口。我建议初学者先用I2C,因为接线少,只有两根线。
  • 工作电压:1.65V到3.3V,兼容3.3V的MCU系统。注意,它不兼容5V!
  • 内部集成:内置了DC-DC升压电路、振荡器、显示RAM。说白了,你给它供电和指令,它自己就能干活。
  • 功耗:正常显示时约20mA,休眠模式可以降到微安级别。这对电池供电的血压计来说,太重要了。

核心要点:SSD1306本质上是一个「显存映射型」驱动芯片。你往它的内部RAM写什么数据,屏幕上就显示什么内容。这个思想贯穿整个课程,请务必理解。

1.2 I2C通信协议详解

好,接下来我们聊聊I2C。很多初学者觉得I2C协议很复杂,其实不然。你想想看,它只有两根线:SCL(时钟线)和SDA(数据线)。所有设备都挂在这两根线上,通过地址来区分。

SSD1306的I2C地址是多少?通常是0x3C(写操作)或0x3D(读操作)。为什么有两个?因为I2C协议规定,地址的最后一位是读写标志位。0表示写,1表示读。所以实际器件地址是0x78(7位地址),左移一位后变成0x3C(写)和0x3D(读)。

嗯,这里要注意:有些模块的地址可能是0x3C,有些是0x3D。我遇到过好几次,买回来的OLED模块地址不对,折腾了半天才发现是地址搞错了。所以拿到模块后,第一件事就是看数据手册确认地址。

I2C通信的基本流程是这样的:

  1. 主机发送起始信号(SCL高电平时,SDA从高变低)
  2. 主机发送7位器件地址 + 1位读写位
  3. 从机应答(拉低SDA)
  4. 主机发送数据字节(8位),每发完一个字节等待从机应答
  5. 所有数据发送完毕,主机发送停止信号(SCL高电平时,SDA从低变高)

用代码实现的话,我习惯这样写I2C写操作:

// 伪代码示例:向SSD1306写入一个字节
void ssd1306_write_byte(uint8_t data) {
    i2c_start();                    // 起始信号
    i2c_write_byte(0x3C);          // 器件地址 + 写标志
    i2c_wait_ack();                // 等待应答
    i2c_write_byte(0x00);          // 控制字节:0x00表示后续是命令,0x40表示数据
    i2c_wait_ack();
    i2c_write_byte(data);          // 实际数据
    i2c_wait_ack();
    i2c_stop();                    // 停止信号
}

个人经验:I2C通信最容易出问题的地方是时序。我曾经在一个项目里,因为SCL时钟频率设置得太高(超过400kHz),导致OLED屏幕偶尔显示异常。后来把频率降到100kHz,问题就解决了。所以,如果遇到显示不稳定,先检查I2C时钟频率。

1.3 OLED像素矩阵与显存映射

这是今天最核心的内容。理解了显存映射,你就掌握了SSD1306的「灵魂」。

SSD1306的显存大小是128×64位,也就是1024字节(128×64÷8)。为什么是字节?因为每个像素用1位表示:1代表点亮,0代表熄灭。所以128列×64行,每8行组成一个字节,一共64行÷8=8页(Page)。

显存的组织方式是这样的:

页(Page) 行范围 列范围 字节数
Page 0 第0~7行 第0~127列 128字节
Page 1 第8~15行 第0~127列 128字节
... ... ... ...
Page 7 第56~63行 第0~127列 128字节

你想想看,如果我想在屏幕的(10, 20)位置点亮一个像素,该怎么做?

首先,计算它属于哪一页:20 ÷ 8 = 2(Page 2)。然后,计算它在页内的行偏移:20 % 8 = 4(第4行)。最后,列就是10。

所以,我需要往Page 2、第10列的那个字节里,写入一个值,让第4位(bit 4)为1。这个值就是0x10(二进制00010000)。

用代码实现就是:

// 设置像素点 (x, y) 为点亮状态
void ssd1306_set_pixel(uint8_t x, uint8_t y) {
    uint8_t page = y / 8;
    uint8_t bit_pos = y % 8;
    // 先读取当前显存值,再修改对应位
    buffer[page][x] |= (1 << bit_pos);
}

// 将缓冲区数据刷新到OLED
void ssd1306_refresh(void) {
    for (uint8_t page = 0; page < 8; page++) {
        ssd1306_write_command(0xB0 + page);  // 设置页地址
        ssd1306_write_command(0x00);         // 设置列地址低4位
        ssd1306_write_command(0x10);         // 设置列地址高4位
        for (uint8_t col = 0; col < 128; col++) {
            ssd1306_write_data(buffer[page][col]);
        }
    }
}

避坑指南:我曾经犯过一个低级错误——忘记维护本地缓冲区。每次要显示新内容时,直接往OLED显存里写。结果导致屏幕闪烁严重,因为频繁的I2C写入占用了大量时间。正确的做法是:先在MCU的RAM里维护一个128×8字节的缓冲区,所有绘图操作都在缓冲区上进行,最后一次性刷新到OLED。这样既高效又稳定。

1.4 初始化流程与实战要点

SSD1306的初始化其实不复杂,但顺序很重要。我总结了一个「三步走」流程:

  1. 上电复位:给OLED供电后,等待至少100ms,让内部电路稳定。
  2. 发送初始化指令序列:包括关闭显示、设置对比度、设置扫描方向、设置COM引脚配置、开启显示等。
  3. 清屏并刷新:将本地缓冲区全部清零,然后调用刷新函数。

这里给出一份我常用的初始化代码:

void ssd1306_init(void) {
    delay_ms(100);  // 等待上电稳定
    
    ssd1306_write_command(0xAE);  // 关闭显示
    ssd1306_write_command(0xD5);  // 设置振荡器频率
    ssd1306_write_command(0x80);
    ssd1306_write_command(0xA8);  // 设置多路复用比
    ssd1306_write_command(0x3F);  // 64行
    ssd1306_write_command(0xD3);  // 设置显示偏移
    ssd1306_write_command(0x00);
    ssd1306_write_command(0x40);  // 设置显示起始行
    ssd1306_write_command(0x8D);  // 启用电荷泵
    ssd1306_write_command(0x14);
    ssd1306_write_command(0x20);  // 设置内存地址模式
    ssd1306_write_command(0x00);  // 水平寻址模式
    ssd1306_write_command(0xA1);  // 列段重映射(左右翻转)
    ssd1306_write_command(0xC8);  // COM扫描方向(上下翻转)
    ssd1306_write_command(0xDA);  // 设置COM引脚配置
    ssd1306_write_command(0x12);
    ssd1306_write_command(0x81);  // 设置对比度
    ssd1306_write_command(0xCF);
    ssd1306_write_command(0xD9);  // 设置预充电周期
    ssd1306_write_command(0xF1);
    ssd1306_write_command(0xDB);  // 设置VCOMH电压
    ssd1306_write_command(0x40);
    ssd1306_write_command(0xA4);  // 全局显示开启
    ssd1306_write_command(0xA6);  // 正常显示(非反色)
    ssd1306_write_command(0xAF);  // 开启显示
    
    // 清屏
    memset(buffer, 0x00, 1024);
    ssd1306_refresh();
}

小技巧:如果你发现屏幕显示的内容是镜像的,或者上下颠倒了,别慌。检查一下0xA1(列重映射)和0xC8(COM扫描方向)这两个指令。我习惯把0xA1改成0xA00xC8改成0xC0,就能翻转显示方向。这个在血压计UI设计时经常用到,因为不同安装方向需要不同的显示方向。

1.5 本章小结

好了,今天的内容就到这里。我们讲了SSD1306的基本特性、I2C通信协议、显存映射原理,以及初始化流程。这些都是后续UI设计的基础。

说实话,OLED驱动并不难,难的是理解「显存映射」这个思想。一旦你理解了,后面画点、画线、画汉字、画波形图,都是在这个基础上做文章。

下一章,我们会正式开始UI设计——从画一个简单的血压计界面开始。到时候,你会看到今天学的这些知识是如何落地到实际项目中的。

记住:纸上得来终觉浅,绝知此事要躬行。赶紧拿出你的开发板,把今天的代码跑一遍吧!