4、I2C总线通信:I2C协议详解,ESP32作为主机,扫描总线设备,驱动温湿度传感器(SHT30)

说到I2C,这玩意儿在嵌入式圈子里太常见了。两根线,就能挂一堆设备,省GPIO又方便。我个人习惯是,只要不是对速率有变态要求的场景,首选I2C。今天咱们就拿ESP32当主机,把SHT30这个温湿度传感器给调通。

4.1 I2C协议,其实没那么玄乎

I2C总线就两条线:SCL(时钟线)和SDA(数据线)。所有设备都挂在这两根线上,靠地址区分谁是谁。主机(咱们的ESP32)负责产生时钟,从机(比如SHT30)听令行事。

通信流程说白了就三步:

  • 起始信号:SCL高电平时,SDA从高变低。告诉所有从机:“注意,我要开始说话了。”
  • 地址+读写位:主机发送7位从机地址,再加1位读写标志(0表示写,1表示读)。从机收到后,如果地址匹配,会拉低SDA回应一个ACK。
  • 数据传输:每发8位数据,等待从机ACK。发完停止信号(SCL高电平时,SDA从低变高)。

关键点:I2C是半双工,同一时刻只能一个方向传数据。速率一般有100kHz(标准模式)和400kHz(快速模式)。SHT30支持快速模式,咱们就用400kHz。

我在项目中遇到过一个问题:两根线太长,导致信号反射,数据错乱。后来加了上拉电阻(4.7kΩ),问题就解决了。嗯,这里要注意,I2C总线必须要有上拉电阻,ESP32内部虽然有弱上拉,但不够稳,建议外部加。

4.2 ESP32的I2C外设,怎么配?

ESP32有两个I2C控制器,咱们用I2C0。配置起来很简单,直接上代码:

#include <Wire.h>

void setup() {
  Wire.begin(21, 22); // SDA=GPIO21, SCL=GPIO22
  Serial.begin(115200);
  Serial.println("I2C Scanner Starting...");
}

void loop() {
  byte error, address;
  int deviceCount = 0;

  for (address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();

    if (error == 0) {
      Serial.print("Found device at 0x");
      if (address < 16) Serial.print("0");
      Serial.print(address, HEX);
      Serial.println(" !");
      deviceCount++;
    }
  }
  if (deviceCount == 0) {
    Serial.println("No I2C devices found.");
  }
  delay(5000);
}

这段代码就是扫描总线上的所有设备。Wire.begin(21, 22)指定了引脚,你也可以换成别的。跑起来后,串口会打印出所有响应地址。SHT30的默认地址是0x44(ADDR引脚接GND)或0x45(接VCC)。

小技巧:如果你不确定传感器地址,先跑这个扫描程序。我曾经被一个国产传感器坑过,手册写0x48,实际是0x49。扫描一下,真相大白。

4.3 SHT30驱动,从读到算

SHT30是 Sensirion 家的,精度不错,还带加热功能(防凝露)。驱动它分两步:发命令,读数据。

第一步:发测量命令

SHT30支持多种测量模式。咱们用最常用的“周期采集模式”,命令是0x2C06。发命令的代码:

void sendCommand(uint16_t cmd) {
  Wire.beginTransmission(0x44);
  Wire.write(cmd >> 8);   // 高字节
  Wire.write(cmd & 0xFF);  // 低字节
  Wire.endTransmission();
}

第二步:读数据

发完命令后,等个50ms(手册建议),然后读6个字节:温度高、温度低、CRC、湿度高、湿度低、CRC。CRC校验可选,但建议加上,防止数据被干扰。

bool readData(float &temp, float &hum) {
  Wire.requestFrom(0x44, 6);
  if (Wire.available() != 6) return false;

  uint8_t data[6];
  for (int i = 0; i < 6; i++) {
    data[i] = Wire.read();
  }

  // 计算温度
  uint16_t rawTemp = (data[0] << 8) | data[1];
  temp = -45.0 + 175.0 * rawTemp / 65535.0;

  // 计算湿度
  uint16_t rawHum = (data[3] << 8) | data[4];
  hum = 100.0 * rawHum / 65535.0;

  return true;
}

计算公式是SHT30手册给的。温度范围-40°C到125°C,湿度0%到100%。你想想看,如果读出来温度是-40,那八成是传感器没焊好或者地址错了。

注意:SHT30的CRC校验是多项式0x31(初始值0xFF)。如果你发现数据偶尔跳变,大概率是CRC没通过。我曾经在项目里没加CRC,结果湿度偶尔飙到120%,排查了两天才发现是线缆干扰。

4.4 完整示例:每秒采集一次

把上面代码整合一下,写个完整的Arduino程序:

#include <Wire.h>

#define SHT30_ADDR 0x44

void setup() {
  Wire.begin(21, 22);
  Serial.begin(115200);
  delay(100);
}

void loop() {
  float temp, hum;

  sendCommand(0x2C06);
  delay(50);

  if (readData(temp, hum)) {
    Serial.print("Temp: ");
    Serial.print(temp, 1);
    Serial.print(" °C, Hum: ");
    Serial.print(hum, 1);
    Serial.println(" %");
  } else {
    Serial.println("Read failed!");
  }

  delay(1000);
}

跑起来后,串口每秒打印一次温湿度。如果显示“Read failed!”,先检查接线,再跑扫描程序确认地址。

4.5 避坑指南

搞I2C这么多年,踩过的坑不少。给你列几个常见的:

  • 上拉电阻:别省!ESP32内部上拉只有几十kΩ,总线长了根本拉不动。我一般用4.7kΩ,线长超过30cm就换2.2kΩ。
  • 地址冲突:两个设备地址一样?要么改硬件(比如SHT30的ADDR引脚),要么换I2C mux芯片。
  • 时钟延展:有些从机处理慢,会拉低SCL让主机等。ESP32的Wire库默认支持,但如果你用其他库,可能得手动处理。
  • 电平匹配:ESP32是3.3V,如果接5V的I2C设备,必须加电平转换。我吃过亏,直接烧了一个传感器的IO口。

总结一下:I2C不难,但细节多。先把扫描程序跑通,确认地址和接线,再搞驱动。SHT30的精度不错,适合做环境监测。下一章咱们把它跟其他传感器融合,搞个智能联动。

好了,今天就到这。代码可以直接复制到Arduino IDE里用。有问题?先查接线,再查地址,最后查CRC。嗯,就这样。