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 控制器支持主模式和从模式。我个人习惯用主模式,因为大部分传感器都是作为从设备存在。配置步骤其实很固定:

  1. 选择 I2C 端口:ESP32 有两个 I2C 控制器,I2C_NUM_0 和 I2C_NUM_1。
  2. 配置引脚:指定 SDA 和 SCL 的 GPIO 号。
  3. 设置时钟频率:标准模式 100kHz,快速模式 400kHz。SHT30 支持 400kHz。
  4. 安装驱动:调用 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 总线上挂载多个设备的结构:

I2C 多设备总线结构 ESP32 主设备 R_pullup 4.7kΩ R_pullup 4.7kΩ SCL SDA SHT30 (0x44) OLED (0x3C) 所有设备共享 SCL 和 SDA 线,通过地址区分

在实际项目中,我建议给每个设备都封装一个独立的驱动文件。比如 sht30.c 只负责 SHT30 的操作,oled.c 只负责 OLED 的操作。主程序里只需要调用各自的初始化函数和读写函数就行。这样代码清晰,也方便后期维护。

总结一下:I2C 驱动其实不难,但细节决定成败。时序要对,地址要准,CRC 要验,延时要给够。把这些基础打牢了,挂什么传感器都不怕。