4. I2C总线驱动:从协议到驱动SSD1306 OLED屏幕

I2C总线,说实话,在嵌入式系统里太常见了。它只需要两根线——SCL(时钟)和SDA(数据),就能挂载多个设备。我最早接触I2C是在一个温湿度采集项目里,当时觉得这协议简单,结果一上手就踩了坑。嗯,今天咱们就把I2C从协议到实战,彻底讲透。

4.1 I2C协议时序:你得看懂波形

I2C的通信,说白了就是主设备控制时钟,从设备响应数据。每次通信都从起始条件开始,到停止条件结束。

  • 起始条件:SCL高电平时,SDA从高变低。
  • 停止条件:SCL高电平时,SDA从低变高。
  • 数据位:SCL低电平时改变SDA,SCL高电平时采样SDA。

每个字节后面必须跟一个应答位(ACK)。从设备拉低SDA表示应答,不拉低就是非应答(NACK)。

关键点:I2C地址是7位或10位,但发送时左移1位,最低位是读写位(0=写,1=读)。比如SSD1306的7位地址是0x3C,实际发送0x78(写)或0x79(读)。

我遇到过一个问题:示波器上看波形完全正确,但设备就是不响应。后来发现是上拉电阻没焊,SDA和SCL浮空了。I2C总线必须外接上拉电阻,一般4.7kΩ,速度高时用2.2kΩ。

4.2 ESP32作为I2C主设备

ESP32的I2C控制器很灵活。你可以用任意GPIO模拟,也可以用硬件I2C。我个人习惯用硬件I2C,省CPU资源。

配置步骤很简单:

  1. 调用 i2c_param_config() 设置参数。
  2. 调用 i2c_driver_install() 安装驱动。
  3. 用 i2c_master_write_read_device() 读写数据。
#include "driver/i2c.h"

#define I2C_MASTER_SCL_IO    22      // GPIO22
#define I2C_MASTER_SDA_IO    21      // GPIO21
#define I2C_MASTER_NUM       0       // I2C0
#define I2C_MASTER_FREQ_HZ   100000  // 100kHz

void i2c_master_init() {
    i2c_config_t conf = {
        .mode = I2C_MODE_MASTER,
        .sda_io_num = I2C_MASTER_SDA_IO,
        .scl_io_num = I2C_MASTER_SCL_IO,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .master.clk_speed = I2C_MASTER_FREQ_HZ,
    };
    i2c_param_config(I2C_MASTER_NUM, &conf);
    i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0);
}

小技巧:频率别设太高。100kHz最稳,400kHz需要PCB走线注意。我试过1MHz,结果从设备跟不上,数据全乱。

4.3 ESP32作为I2C从设备

ESP32也能当从设备。这在多主系统或传感器数据中转时很有用。配置时把模式改成 I2C_MODE_SLAVE,然后注册回调函数处理读写请求。

void i2c_slave_init() {
    i2c_config_t conf = {
        .mode = I2C_MODE_SLAVE,
        .sda_io_num = GPIO_NUM_21,
        .scl_io_num = GPIO_NUM_22,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .slave.addr_10bit_en = 0,
        .slave.slave_addr = 0x50,  // 从设备地址
    };
    i2c_param_config(I2C_NUM_0, &conf);
    i2c_driver_install(I2C_NUM_0, conf.mode, 1024, 1024, 0);
}

从设备模式下,你需要用 i2c_slave_read_buffer() 和 i2c_slave_write_buffer() 来收发数据。注意,从设备不能主动发起通信,只能响应主设备的请求。

注意:从设备地址不能和总线上其他设备冲突。我见过有人把两个设备设成同一个地址,结果总线直接锁死。

4.4 驱动SSD1306 OLED屏幕

SSD1306是128×64像素的OLED驱动芯片,支持I2C和SPI。咱们用I2C模式,因为它连线少。

驱动SSD1306,核心就三步:

  1. 初始化:发送配置命令序列。
  2. 写数据:把像素数据写入显存。
  3. 刷新显示:SSD1306会自动从显存刷新,你只需要保证显存数据正确。

初始化序列比较长,我贴个精简版:

void ssd1306_init() {
    uint8_t cmds[] = {
        0xAE, // 关闭显示
        0xD5, 0x80, // 设置振荡频率
        0xA8, 0x3F, // 设置多路复用比
        0xD3, 0x00, // 设置显示偏移
        0x40,       // 设置显示起始行
        0x8D, 0x14, // 启用电荷泵
        0x20, 0x00, // 设置内存地址模式(水平)
        0xA1,       // 段重映射(左右翻转)
        0xC8,       // COM扫描方向(上下翻转)
        0xDA, 0x12, // 设置COM引脚配置
        0x81, 0xCF, // 设置对比度
        0xD9, 0xF1, // 设置预充电周期
        0xDB, 0x40, // 设置VCOMH电压
        0xA4,       // 全局显示开启
        0xA6,       // 正常显示(非反色)
        0xAF        // 开启显示
    };
    for (int i = 0; i < sizeof(cmds); i++) {
        i2c_master_write_slave(I2C_MASTER_NUM, SSD1306_ADDR, &cmds[i], 1);
    }
}

注意:SSD1306的I2C写操作,第一个字节是控制字节。0x00表示后续是命令,0x40表示后续是数据。很多新手忘了这个,直接发命令,屏幕死活不亮。

写像素数据时,你需要维护一个128×64/8 = 1024字节的显存缓冲区。每个bit对应一个像素,1亮0灭。我习惯用 uint8_t buffer[128][8] 来管理。

void ssd1306_draw_pixel(uint8_t x, uint8_t y, uint8_t color) {
    if (x >= 128 || y >= 64) return;
    if (color) {
        buffer[x][y / 8] |= (1 << (y % 8));
    } else {
        buffer[x][y / 8] &= ~(1 << (y % 8));
    }
}

void ssd1306_refresh() {
    uint8_t cmd[] = {0x21, 0, 127, 0x22, 0, 7}; // 设置列和页范围
    i2c_master_write_slave(I2C_MASTER_NUM, SSD1306_ADDR, cmd, 6);
    for (int page = 0; page < 8; page++) {
        uint8_t data[129];
        data[0] = 0x40; // 数据控制字节
        for (int col = 0; col < 128; col++) {
            data[col + 1] = buffer[col][page];
        }
        i2c_master_write_slave(I2C_MASTER_NUM, SSD1306_ADDR, data, 129);
    }
}

4.5 知识结构总览

下面这张图,把I2C驱动SSD1306的整个流程串起来了。你一看就明白。

I2C驱动SSD1306 OLED知识结构图 I2C协议基础 ESP32主设备 ESP32从设备 i2c_param_config() i2c_driver_install() i2c_slave_read_buffer() i2c_slave_write_buffer() SSD1306 OLED驱动 初始化命令序列 显存缓冲区管理 刷新显示 OLED屏幕正常显示 核心流程:I2C协议 → 主从配置 → 驱动初始化 → 显存操作 → 屏幕显示

4.6 常见问题与避坑

问题 原因 解决方法
屏幕不亮 初始化序列错误或电荷泵未开启 检查0x8D 0x14命令是否发送
显示花屏 显存地址模式设置错误 确保0x20 0x00(水平模式)已设置
I2C通信超时 上拉电阻缺失或频率过高 加4.7kΩ上拉,降频到100kHz
从设备无应答 地址错误或设备未上电 用示波器确认地址波形

我曾经在一个项目里,SSD1306怎么调都不亮。折腾了两天,最后发现是I2C地址搞错了。SSD1306的默认地址是0x3C,但有些模块把SA0引脚拉高,地址变成0x3D。你拿到模块后,先看原理图确认地址。

嗯,I2C驱动SSD1306,说白了就是协议加时序加显存管理。你只要把这三块吃透,换任何I2C设备都能快速上手。下次咱们聊聊SPI总线,那个速度更快,但连线也多两根。