4. I2C与SPI总线通信:驱动OLED与传感器
通信协议是嵌入式系统的“神经”。ESP32 集成了多个 I2C 和 SPI 硬件控制器,用起来很方便。但很多初学者在这上面栽过跟头——时序不对、地址错误、电平不匹配。今天咱们就把这两个协议彻底讲透。
本章核心: 掌握 I2C/SPI 协议差异 → 驱动 SSD1306 OLED 屏幕 → 驱动 MPU6050 六轴传感器 → 实战调试技巧
4.1 I2C 与 SPI 协议深度对比
先说说我个人的选择习惯。如果项目里只有一两个传感器,对速度要求不高,我肯定选 I2C——省引脚。但如果是驱动 TFT 彩屏或者高速 ADC,SPI 是唯一选择。
| 对比项 | I2C | SPI |
|---|---|---|
| 引脚数量 | 2(SDA + SCL) | 4(MOSI + MISO + SCLK + CS) |
| 通信模式 | 半双工 | 全双工 |
| 寻址方式 | 软件地址(7/10位) | 硬件片选(CS) |
| 最大速度 | 400kHz(快速模式) | 80MHz(ESP32) |
| 应答机制 | 有 ACK/NACK | 无 |
| 多主支持 | 支持 | 不支持 |
| 抗干扰能力 | 较弱(开漏输出) | 较强(推挽输出) |
| 典型应用 | MPU6050、OLED、RTC | SD卡、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.3V | VCC | VCC |
| GND | GND | GND |
| GPIO21 | SDA | — |
| GPIO22 | SCL | — |
| 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 有零偏误差,需要校准。我的做法是:
- 静态校准: 传感器静止时,采集 100 组数据取平均,得到零偏值。
- 动态滤波: 使用互补滤波器融合加速度计和陀螺仪数据,得到稳定的角度。
// 互补滤波器计算角度
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 的核心内容就讲完了。记住:协议是死的,应用是活的。多动手、多抓波形,你很快就能成为总线通信的老手。