4. I2C总线通信:I2C协议详解与SHT30温湿度传感器驱动

I2C总线,说实话,是我在嵌入式开发中用得最多的通信接口之一。它只需要两根线,就能挂一堆设备,省GPIO又灵活。但很多新手上来就踩坑——要么设备不应答,要么数据读出来全是0xFF。这一章,咱们就把I2C协议掰开揉碎,再拿SHT30这个经典温湿度传感器练手。

4.1 I2C协议核心要点

I2C是Philips(现NXP)发明的同步串行总线。它只有两根线:SCL(时钟线)和SDA(数据线)。所有设备都挂在同一条总线上,通过地址区分。

关键特性一览:

  • 主从架构:ESP32作为主机(Master),SHT30作为从机(Slave)
  • 7位地址:SHT30的I2C地址是0x44(ADDR引脚接GND)或0x45(ADDR接VCC)
  • 速率标准:标准模式100kHz,快速模式400kHz,ESP32最高支持到1MHz
  • 开漏输出:需要外部上拉电阻,典型值4.7kΩ

我刚开始用I2C时,总觉得它比SPI慢。但后来发现,对于传感器这类低速设备,I2C完全够用,而且接线简单太多了。

4.2 I2C通信时序详解

I2C的通信过程,说白了就是一套握手协议。我习惯把它拆成几个步骤来理解:

  1. 起始条件:SCL高电平时,SDA从高变低
  2. 发送地址:7位地址 + 读写位(0写/1读)
  3. 从机应答:从机拉低SDA表示ACK,保持高表示NACK
  4. 数据传输:每字节8位,MSB先行,每字节后跟ACK
  5. 停止条件:SCL高电平时,SDA从低变高

避坑指南:我曾经在一个项目里,因为上拉电阻选太大(10kΩ),导致400kHz速率下波形变圆,通信时好时坏。后来换成4.7kΩ就稳了。总线电容大时,上拉电阻要适当减小。

4.3 SHT30温湿度传感器简介

SHT30是Sensirion家的数字温湿度传感器,精度不错,价格也亲民。我做过一个环境监测项目,用了20多个SHT30,一致性很好。

参数 典型值 说明
温度范围 -40 ~ 125°C 精度±0.3°C(0~65°C)
湿度范围 0 ~ 100%RH 精度±2%RH(20~80%RH)
I2C地址 0x44 / 0x45 由ADDR引脚电平决定
供电电压 2.4 ~ 5.5V 3.3V直接接ESP32

4.4 ESP32 I2C外设配置

ESP32的I2C外设用起来很方便。我个人习惯用ESP-IDF的I2C驱动库,它封装好了底层时序,我们只需要配置参数、调用API就行。

#include "driver/i2c.h"

#define I2C_MASTER_SCL_IO    22      // GPIO22
#define I2C_MASTER_SDA_IO    21      // GPIO21
#define I2C_MASTER_NUM       0       // I2C端口0
#define I2C_MASTER_FREQ_HZ   100000  // 100kHz

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);
}

注意:GPIO21和GPIO22是ESP32默认的I2C引脚,但不同开发板可能不一样。我遇到过一块板子把SDA和SCL接反了,查了半天才发现。上电前一定用万用表量一下引脚通断。

4.5 SHT30驱动实现

SHT30的通信流程很清晰:先发命令,再读数据。它支持多种测量模式,我一般用“周期采集模式”,这样传感器会自己定时测量,我们只需要去读结果就行。

4.5.1 发送测量命令

#define SHT30_ADDR          0x44
#define SHT30_CMD_MEAS_HIGH 0x2C06  // 高精度测量命令

esp_err_t sht30_send_cmd(uint16_t cmd)
{
    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_byte(cmd_handle, (cmd >> 8) & 0xFF, true);  // 高字节
    i2c_master_write_byte(cmd_handle, cmd & 0xFF, true);          // 低字节
    i2c_master_stop(cmd_handle);
    
    esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd_handle, 1000 / portTICK_PERIOD_MS);
    i2c_cmd_link_delete(cmd_handle);
    
    return ret;
}

4.5.2 读取温湿度数据

esp_err_t sht30_read_data(float *temp, float *humi)
{
    uint8_t data[6] = {0};
    
    // 先发测量命令
    sht30_send_cmd(SHT30_CMD_MEAS_HIGH);
    vTaskDelay(20 / portTICK_PERIOD_MS);  // 等待测量完成
    
    // 读取6字节数据
    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_READ, true);
    i2c_master_read(cmd_handle, data, 6, I2C_MASTER_LAST_NACK);
    i2c_master_stop(cmd_handle);
    
    esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd_handle, 1000 / portTICK_PERIOD_MS);
    i2c_cmd_link_delete(cmd_handle);
    
    if (ret == ESP_OK) {
        // 计算温度:原始值 / 65535 * 175 - 45
        uint16_t raw_temp = (data[0] << 8) | data[1];
        *temp = (raw_temp / 65535.0f) * 175.0f - 45.0f;
        
        // 计算湿度:原始值 / 65535 * 100
        uint16_t raw_humi = (data[3] << 8) | data[4];
        *humi = (raw_humi / 65535.0f) * 100.0f;
    }
    
    return ret;
}

经验之谈:SHT30的CRC校验位在data[2]和data[5],我早期偷懒没校验,结果有次数据跳变查了半天。后来老老实实加上CRC校验,问题就再没出现过。生产环境一定要做数据校验。

4.6 I2C通信流程图

下面这张图,是我画给团队新人看的。它把整个通信流程串起来了,你一看就明白。

SHT30 I2C通信流程 ESP32 发送起始条件 发送从机地址 0x44 + W SHT30 应答 ACK 发送测量命令 0x2C06 等待 20ms 测量 读取6字节数据 数据格式 data[0:1] 温度原始值 data[2] 温度CRC data[3:4] 湿度原始值 data[5] 湿度CRC

4.7 完整示例代码

把上面几个函数拼起来,就是一个完整的温湿度读取程序。我习惯在main函数里先初始化I2C,然后循环读取。

void app_main(void)
{
    float temperature = 0.0f;
    float humidity = 0.0f;
    
    // 初始化I2C
    i2c_master_init();
    
    while (1) {
        if (sht30_read_data(&temperature, &humidity) == ESP_OK) {
            printf("温度: %.2f°C, 湿度: %.2f%%\n", temperature, humidity);
        } else {
            printf("读取失败,检查I2C连接\n");
        }
        vTaskDelay(2000 / portTICK_PERIOD_MS);
    }
}

常见问题排查:

  • 设备不应答:检查地址对不对,ADDR引脚电平是否匹配
  • 数据全0xFF:上拉电阻没接,或者SDA/SCL接反了
  • 数据偶尔跳变:电源纹波大,或者I2C线太长(超过20cm建议加缓冲)
  • 读到的温湿度不变:忘了加延时,传感器还没测量完就去读了

4.8 性能优化建议

I2C虽然简单,但用好了也能提升系统效率。我总结了几点:

优化点 做法 效果
提高时钟频率 从100kHz提到400kHz 读取时间从~5ms降到~1.5ms
批量读取 一次读6字节,别分两次 减少总线占用时间
使用DMA ESP32 I2C支持DMA传输 CPU负载降低30%以上
减少重复初始化 只初始化一次,别每次读都重配 避免不必要的开销

嗯,到这里I2C和SHT30的内容就差不多了。你把这些代码烧进去,串口应该就能看到温湿度数据了。如果读出来是乱码,先检查波特率对不对——我当年就犯过这种低级错误,115200设成了9600,打印出来全是乱码,还以为是传感器坏了。