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的通信过程,说白了就是一套握手协议。我习惯把它拆成几个步骤来理解:
- 起始条件:SCL高电平时,SDA从高变低
- 发送地址:7位地址 + 读写位(0写/1读)
- 从机应答:从机拉低SDA表示ACK,保持高表示NACK
- 数据传输:每字节8位,MSB先行,每字节后跟ACK
- 停止条件: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通信流程图
下面这张图,是我画给团队新人看的。它把整个通信流程串起来了,你一看就明白。
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,打印出来全是乱码,还以为是传感器坏了。