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。嗯,就这样。