4. I2C 总线驱动:从时序到实战
I2C 总线,说实话,是嵌入式开发里最常用的通信接口之一。两根线,挂一堆设备,省引脚又灵活。但很多新手栽就栽在「以为它很简单」上。时序不对、地址冲突、时钟延展没处理……我在项目里见过不少这样的坑。
这一章,咱们就把 I2C 从协议到 ESP32 的驱动实现,再到挂载 SHT30 温湿度传感器,一步步拆开讲清楚。
4.1 I2C 协议核心时序
I2C 只有两条线:SCL(时钟)和 SDA(数据)。所有通信都由主设备发起。我习惯把它的时序拆成四个基本动作:
- 起始条件:SCL 高电平时,SDA 从高切换到低。
- 停止条件:SCL 高电平时,SDA 从低切换到高。
- 数据采样:数据在 SCL 低电平时变化,高电平时被采样。
- 应答位:每发送 8 位数据后,接收方在第 9 个时钟周期拉低 SDA 表示应答。
关键点:起始和停止条件必须由主设备产生。从设备不能主动发起通信。
为什么会强调这个?我曾经遇到一个项目,从设备偶尔会死锁,就是因为主设备在异常情况下没有正确发送停止条件,导致总线一直被占用。
4.2 ESP32 I2C 控制器配置
ESP32 的 I2C 控制器支持主模式和从模式。我个人习惯用主模式,因为大部分传感器都是作为从设备存在。配置步骤其实很固定:
- 选择 I2C 端口:ESP32 有两个 I2C 控制器,I2C_NUM_0 和 I2C_NUM_1。
- 配置引脚:指定 SDA 和 SCL 的 GPIO 号。
- 设置时钟频率:标准模式 100kHz,快速模式 400kHz。SHT30 支持 400kHz。
- 安装驱动:调用 i2c_driver_install()。
下面是一个典型的配置代码:
#include "driver/i2c.h"
#define I2C_MASTER_SCL_IO 22 // GPIO 22
#define I2C_MASTER_SDA_IO 21 // GPIO 21
#define I2C_MASTER_NUM I2C_NUM_0
#define I2C_MASTER_FREQ_HZ 400000 // 400kHz
void i2c_master_init(void) {
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);
}
小技巧:如果你的板子上没有外部上拉电阻,记得在配置里使能内部上拉。但内部上拉电阻值较大(约 50kΩ),在高速通信时可能不够用。我建议在 PCB 设计时直接加上 4.7kΩ 外部上拉。
4.3 驱动 SHT30 温湿度传感器
SHT30 是 Sensirion 出的一款高精度温湿度传感器。它的 I2C 地址是 0x44(ADDR 引脚接地时)。通信流程很简单:
- 发送测量命令:0x2C 0x06(高重复性,时钟延展使能)。
- 等待测量完成:约 15ms。
- 读取数据:6 个字节(温度 2 字节 + CRC 1 字节 + 湿度 2 字节 + CRC 1 字节)。
嗯,这里要注意 CRC 校验。SHT30 的每个数据块后面都跟了一个 CRC 字节,用来保证数据完整性。我在一个环境监测项目里,就是因为没做 CRC 校验,偶尔读到跳变的异常值,排查了好久才发现是总线干扰导致的。
下面是读取温湿度的核心代码:
#define SHT30_ADDR 0x44
#define SHT30_CMD_MEASURE 0x2C06
esp_err_t sht30_read_temperature_humidity(float *temp, float *hum) {
uint8_t cmd[2] = {0x2C, 0x06};
uint8_t data[6] = {0};
// 发送测量命令
i2c_cmd_handle_t cmd_handle = i2c_cmd_link_create();
i2c_master_start(cmd_handle);
i2c_master_write_byte(cmd_handle, (SHT30_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write(cmd_handle, cmd, 2, true);
i2c_master_stop(cmd_handle);
i2c_master_cmd_begin(I2C_MASTER_NUM, cmd_handle, pdMS_TO_TICKS(100));
i2c_cmd_link_delete(cmd_handle);
// 等待测量完成
vTaskDelay(pdMS_TO_TICKS(20));
// 读取数据
cmd_handle = i2c_cmd_link_create();
i2c_master_start(cmd_handle);
i2c_master_write_byte(cmd_handle, (SHT30_ADDR << 1) | I2C_MASTER_READ, true);
i2c_master_read(cmd_handle, data, 6, I2C_MASTER_LAST_NACK);
i2c_master_stop(cmd_handle);
i2c_master_cmd_begin(I2C_MASTER_NUM, cmd_handle, pdMS_TO_TICKS(100));
i2c_cmd_link_delete(cmd_handle);
// 计算温湿度(忽略 CRC 校验,实际项目请务必加上)
uint16_t raw_temp = (data[0] << 8) | data[1];
uint16_t raw_hum = (data[3] << 8) | data[4];
*temp = -45.0 + 175.0 * raw_temp / 65535.0;
*hum = 100.0 * raw_hum / 65535.0;
return ESP_OK;
}
避坑指南:我曾经在读取 SHT30 时忘记加 vTaskDelay,结果读回来的数据全是 0。原因是传感器还没准备好,主设备就急着去读数据了。测量命令发出后,一定要等够时间再读。
4.4 多设备总线管理
I2C 总线上可以挂多个设备,只要它们的地址不冲突就行。SHT30 的地址是 0x44,如果总线上还有别的设备,比如 OLED 显示屏(地址 0x3C),就需要分别操作。
多设备管理的关键点:
- 地址唯一性:每个设备必须有唯一的 7 位地址。
- 总线仲裁:如果两个主设备同时发起通信,I2C 协议会通过 SDA 线进行仲裁。但咱们一般只用单主设备模式,不用太担心这个。
- 时钟延展:有些从设备(比如 SHT30)会在处理数据时拉低 SCL,告诉主设备「等一下」。ESP32 的 I2C 硬件会自动处理这个,但如果你用软件模拟 I2C,就得自己实现超时机制。
下面这张图展示了 I2C 总线上挂载多个设备的结构:
在实际项目中,我建议给每个设备都封装一个独立的驱动文件。比如 sht30.c 只负责 SHT30 的操作,oled.c 只负责 OLED 的操作。主程序里只需要调用各自的初始化函数和读写函数就行。这样代码清晰,也方便后期维护。
总结一下:I2C 驱动其实不难,但细节决定成败。时序要对,地址要准,CRC 要验,延时要给够。把这些基础打牢了,挂什么传感器都不怕。