4. I2C与SPI总线通信:驱动OLED与传感器

通信协议是嵌入式系统的“神经”。ESP32 集成了多个 I2C 和 SPI 硬件控制器,用起来很方便。但很多初学者在这上面栽过跟头——时序不对、地址错误、电平不匹配。今天咱们就把这两个协议彻底讲透。

本章核心: 掌握 I2C/SPI 协议差异 → 驱动 SSD1306 OLED 屏幕 → 驱动 MPU6050 六轴传感器 → 实战调试技巧
I2C vs SPI 协议对比 I2C 总线 2 线制:SDA(数据)+ SCL(时钟) 多主多从,地址寻址(7位/10位) 速度:标准 100kHz / 快速 400kHz 半双工,需应答位(ACK) 适合:传感器、小屏、低速外设 SPI 总线 4 线制:MOSI + MISO + SCLK + CS 一主多从,片选(CS)寻址 速度:可达 10MHz+(ESP32 支持 80MHz) 全双工,无应答机制 适合:大屏、SD卡、高速数据流

4.1 I2C 与 SPI 协议深度对比

先说说我个人的选择习惯。如果项目里只有一两个传感器,对速度要求不高,我肯定选 I2C——省引脚。但如果是驱动 TFT 彩屏或者高速 ADC,SPI 是唯一选择。

对比项I2CSPI
引脚数量2(SDA + SCL)4(MOSI + MISO + SCLK + CS)
通信模式半双工全双工
寻址方式软件地址(7/10位)硬件片选(CS)
最大速度400kHz(快速模式)80MHz(ESP32)
应答机制有 ACK/NACK无
多主支持支持不支持
抗干扰能力较弱(开漏输出)较强(推挽输出)
典型应用MPU6050、OLED、RTCSD卡、TFT屏、Flash
我的经验: 在 ESP32 上,I2C 的 GPIO 可以任意映射,但 SPI 的 MISO/MOSI/SCLK 建议使用硬件默认引脚,否则可能遇到 DMA 不工作的问题。我曾经为了省事把 SPI 映射到非标准引脚,结果调试了两天才发现是硬件限制。

4.2 驱动 SSD1306 OLED 屏幕

SSD1306 是市面上最常见的 128×64 单色 OLED 驱动芯片。它同时支持 I2C 和 SPI 两种接口。我个人更推荐用 I2C——少接 2 根线,代码也更简洁。

4.2.1 硬件连接

ESP32 引脚SSD1306 (I2C)SSD1306 (SPI)
3.3VVCCVCC
GNDGNDGND
GPIO21SDA—
GPIO22SCL—
GPIO23—MOSI
GPIO18—SCLK
GPIO5—CS
GPIO4—DC(数据/命令)
GPIO2—RST(复位)
注意: SSD1306 的 I2C 地址默认为 0x3C。如果发现屏幕不亮,先检查地址。我遇到过一批国产屏地址是 0x3D,折腾了半天才发现。

4.2.2 代码实现(I2C 模式)

使用 ESP-IDF 的 I2C 驱动库,配合 SSD1306 的初始化序列。核心代码就这几步:

// 1. 配置 I2C 参数
i2c_config_t conf = {
    .mode = I2C_MODE_MASTER,
    .sda_io_num = 21,
    .scl_io_num = 22,
    .sda_pullup_en = GPIO_PULLUP_ENABLE,
    .scl_pullup_en = GPIO_PULLUP_ENABLE,
    .master.clk_speed = 400000  // 400kHz
};
i2c_param_config(I2C_NUM_0, &conf);
i2c_driver_install(I2C_NUM_0, conf.mode, 0, 0, 0);

// 2. 发送初始化命令序列
uint8_t init_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        // 开启显示
};

// 3. 写命令函数
void ssd1306_write_cmd(uint8_t cmd) {
    i2c_cmd_handle_t cmd_handle = i2c_cmd_link_create();
    i2c_master_start(cmd_handle);
    i2c_master_write_byte(cmd_handle, (0x3C << 1) | I2C_MASTER_WRITE, true);
    i2c_master_write_byte(cmd_handle, 0x00, true); // 控制字节:命令
    i2c_master_write_byte(cmd_handle, cmd, true);
    i2c_master_stop(cmd_handle);
    i2c_master_cmd_begin(I2C_NUM_0, cmd_handle, 1000 / portTICK_PERIOD_MS);
    i2c_cmd_link_delete(cmd_handle);
}

// 4. 写数据函数(显示内容)
void ssd1306_write_data(uint8_t *data, size_t len) {
    i2c_cmd_handle_t cmd_handle = i2c_cmd_link_create();
    i2c_master_start(cmd_handle);
    i2c_master_write_byte(cmd_handle, (0x3C << 1) | I2C_MASTER_WRITE, true);
    i2c_master_write_byte(cmd_handle, 0x40, true); // 控制字节:数据
    i2c_master_write(cmd_handle, data, len, true);
    i2c_master_stop(cmd_handle);
    i2c_master_cmd_begin(I2C_NUM_0, cmd_handle, 1000 / portTICK_PERIOD_MS);
    i2c_cmd_link_delete(cmd_handle);
}
避坑指南: 我曾经在写数据时忘记设置控制字节(0x00 表示命令,0x40 表示数据),结果屏幕一直显示乱码。另外,I2C 的时钟速度不要超过 400kHz,有些便宜的 OLED 模块在 1MHz 下会丢数据。

4.3 驱动 MPU6050 六轴传感器

MPU6050 集成了三轴陀螺仪和三轴加速度计,是平衡车、姿态检测项目的首选。它使用 I2C 接口,地址为 0x68(AD0 接地时)。

4.3.1 初始化与配置

// MPU6050 寄存器地址
#define MPU6050_ADDR         0x68
#define MPU6050_PWR_MGMT_1   0x6B
#define MPU6050_ACCEL_XOUT_H 0x3B
#define MPU6050_GYRO_XOUT_H  0x43

// 初始化 MPU6050
void mpu6050_init() {
    // 唤醒传感器(退出休眠模式)
    uint8_t data = 0x00;
    mpu6050_write_reg(MPU6050_PWR_MGMT_1, &data, 1);
    vTaskDelay(100 / portTICK_PERIOD_MS);
    
    // 配置陀螺仪量程:±250°/s(默认)
    // 配置加速度计量程:±2g(默认)
}

// 读取原始数据
void mpu6050_read_raw(int16_t *accel, int16_t *gyro) {
    uint8_t buf[14];
    mpu6050_read_reg(MPU6050_ACCEL_XOUT_H, buf, 14);
    
    // 加速度计数据(高字节在前)
    accel[0] = (buf[0] << 8) | buf[1];  // X轴
    accel[1] = (buf[2] << 8) | buf[3];  // Y轴
    accel[2] = (buf[4] << 8) | buf[5];  // Z轴
    
    // 温度数据(可选)
    // int16_t temp = (buf[6] << 8) | buf[7];
    
    // 陀螺仪数据
    gyro[0] = (buf[8] << 8) | buf[9];   // X轴
    gyro[1] = (buf[10] << 8) | buf[11]; // Y轴
    gyro[2] = (buf[12] << 8) | buf[13]; // Z轴
}

4.3.2 数据校准与滤波

原始数据直接拿来用?不行。MPU6050 有零偏误差,需要校准。我的做法是:

  1. 静态校准: 传感器静止时,采集 100 组数据取平均,得到零偏值。
  2. 动态滤波: 使用互补滤波器融合加速度计和陀螺仪数据,得到稳定的角度。
// 互补滤波器计算角度
float complementary_filter(float acc_angle, float gyro_rate, float dt) {
    static float angle = 0.0f;
    const float alpha = 0.98f;  // 权重系数
    
    // 陀螺仪积分 + 加速度计修正
    angle = alpha * (angle + gyro_rate * dt) + (1 - alpha) * acc_angle;
    return angle;
}
注意: 互补滤波器的 alpha 值需要根据实际采样率调整。alpha 越大,陀螺仪权重越高,响应快但会漂移;alpha 越小,加速度计权重越高,角度稳定但有延迟。我一般从 0.98 开始调,根据实际效果微调。

4.4 总线调试技巧

通信出问题怎么办?别慌,按这个顺序排查:

问题现象可能原因排查方法
I2C 设备无应答地址错误、电平不匹配、上拉电阻缺失用逻辑分析仪抓波形,检查地址位
SPI 数据错位时钟极性/相位不匹配、CS 时序错误检查 SPI 模式(0/1/2/3)
OLED 显示乱码初始化序列错误、数据/命令混淆逐条检查初始化命令
传感器读数跳变电源噪声、I2C 速率过高加 100nF 去耦电容,降低速率
我的调试利器: 逻辑分析仪。几十块钱的 USB 逻辑分析仪就能看到 I2C/SPI 的每个字节。我曾经靠它发现一个国产 MPU6050 模块的地址竟然是 0x69(标准是 0x68),省了三天排查时间。

嗯,到这里 I2C 和 SPI 的核心内容就讲完了。记住:协议是死的,应用是活的。多动手、多抓波形,你很快就能成为总线通信的老手。