简介:面向嵌入式与物联网开发者的 ESP32 硬件 I2C 读取 SHT30 温湿度传感器完整示例工程,解决从引脚接线、I2C 初始化、传感器寻址到数据解析的环境监测开发问题,适合需要快速搭建温湿度采集节点的初中级开发者,也可作为智能家居或工业环境监控项目的前置原型。压缩包共 15 个文件,约 21KB,以 C 源码、头文件与 ESP-IDF 构建配置为主,同时包含说明文档、SDK 配置与依赖锁定文件,目录层级清晰,便于直接导入 ESP-IDF 工具链编译验证。资源中提供 SHT30 驱动组件和主程序参考代码,覆盖 I2C 引脚配置、传感器初始化、温湿度读取及串口输出等关键步骤,并保留容器开发与依赖管理配置,降低环境搭建门槛,方便在 Windows/Linux 等平台快速复现。当前已有 1479 人学习使用,既可作为学习 I2C 协议与传感器驱动的实操范本,也能作为真实温湿度监测系统的起步模板。 先说说我为什么写这篇。SHT30这传感器我在ESP32上用过好几回,网上教程其实不少,但大多有个通病:要么直接把Wire库代码贴出来,换一下设备地址就跑,至于为什么用硬件IIC而不是自己用GPIO模拟时序、为什么读回来6个字节要拆分还要算CRC、温度公式里的175和-45从哪来的,基本没人讲透。这篇就把“ESP32硬件IIC读取SHT30”这件事完整拆开,从接线、协议、代码到排查,一次讲清楚,适合刚接触ESP32或者被IIC读时序折腾过的朋友直接照着做。
1. 项目梳理:为什么坚持用硬件IIC
1.1 硬件IIC和软件IIC到底差在哪
IIC通信在ESP32上有两种实现方式,一种是用芯片自带的I2C外设,也就是标题里说的硬件IIC;另一种是用GPIO口模拟SCL和SDA的时序,叫软件IIC或者模拟IIC。两种方式都能让SHT30正常工作,但实际用起来差异不小。
硬件IIC的核心优势在于时序由芯片内部外设产生,SCL的时钟频率、起始停止条件、ACK响应这些统统由硬件完成,CPU只需要往寄存器里塞数据、读数据就行。好处是稳定,时钟抖动小,即便系统里跑了WiFi、蓝牙或者其他任务,只要I2C外设的中断优先级处理好,读取时序不会因为任务切换被打乱。软件IIC就不一样了,每个bit都要GPIO拉高拉低,还要用delay来凑时序,一旦系统里开了WiFi导致CPU频繁被中断,bit之间的delay很容易被拉长,实际通信速率就会飘,严重时直接读取失败。
我在实测中对比过:同样一块SHT30,硬件IIC在室温下连续读取1小时,数据稳定输出,没有一次卡死;软件IIC在ESP32开启WiFi连接路由器的情况下,大概每100次读取会出现1到2次超时。这就是为什么我在项目中坚持用硬件IIC——不是说软件IIC不能用,而是ESP32这种带协议栈的单片机上,能用硬件外设就别折腾GPIO模拟,把CPU留出来做更有价值的事。
1.2 为什么选SHT30做温湿度采集
很多入门玩家第一个接触的温湿度传感器是DHT11,那玩意儿用单总线协议,读取一次还要卡时序,精度也只有±2℃、±5%RH,做个演示还行,真要记录数据就不太够看了。SHT30属于Sensirion的第三代温湿度传感器,典型精度±0.2℃和±2%RH,已经能覆盖大多数环境监测场景。
SHT30采用标准IIC接口,地址固定(0x44或0x45),数据通信带CRC校验,不用像DHT11那样掐着时间读电平。此外它还支持单次测量和周期测量两种模式,周期模式下传感器可以自己按设定频率采集数据,主机想读就直接拉数据,适合做低功耗的温湿度记录节点。我拿SHT30做过大棚环境监测、家庭温湿度盒子、甚至放进过一个自制的小型气象站,表现都很稳。
2. 硬件准备与连接要点
2.1 材料清单和接线表
这项目需要的材料非常简单:
- ESP32开发板一块(我用的是DevKit V1,也就是最常见的30引脚版本,ESPRESSIF官方模组方案)
- SHT30模块一块,或者SHT30裸芯片自己焊转接板
- 4根公对母杜邦线
- 面包板(可选,方便整理线路)
接线就四根线,没有特别复杂的引脚复用问题:SHT30的VCC接ESP32的3.3V,GND接GND,SDA接GPIO21,SCL接GPIO22。GPIO21和GPIO22是ESP32默认的I2C引脚,对应开发板丝印上的D21和D22,不同板子丝印可能略有差异,以板载印刷为准。
| SHT30引脚 | 连接ESP32引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 供电,绝对不要接5V |
| GND | GND | 共地 |
| SDA | GPIO21 | 默认I2C数据线 |
| SCL | GPIO22 | 默认I2C时钟线 |
我之前用ESP32-C3也做过一次,C3的默认I2C引脚不是21和22,而是GPIO8和GPIO9,这时候必须在代码里用Wire.begin(8, 9)显式指定。所以接线前先查一下你手里那块板子的引脚图,千万别默认所有ESP32都是21和22。
2.2 上拉电阻怎么选
IIC总线是开漏结构,SCL和SDA线必须接上拉电阻到VCC,否则信号根本拉不高,通信必然失败。SHT30模块大多数在PCB上已经集成了4.7kΩ的上拉电阻,直接接杜邦线就行。如果你用的是SHT30裸芯片自己飞线,那就必须在SCL和SDA上各接一个上拉电阻到3.3V。
上拉电阻的取值其实有个经验范围,总线频率400kHz以下、总线长度不超过20厘米时,4.7kΩ是标准答案。如果你把传感器用长线接到了1米开外,或者总线上挂了多个IIC设备,建议换成2.2kΩ,这样总线电容充放电更快,上升沿不会太缓。至于网上经常问的“上拉电阻取多大”,就按这个逻辑来:线越长、设备越多,阻值越低,但最低不要低于1kΩ,不然灌电流太大,端口吃不消。
值得提醒的是,市面上有些ESP32开发板在GPIO21和GPIO22上已经自带上拉电阻,尤其是那些号称“兼容Arduino”的板子,这时候即使模块上没有上拉电阻也能跑通,因为板载上拉已经在起作用。但如果你换到其他引脚,比如用Wire.begin(4, 5),那就要确认模块本身有没有上拉,没有的话自己补上,不然会踩“找不到设备”的坑。
2.3 电平匹配和供电说明
SHT30的工作电压范围是2.4V到5.5V,但注意它的IIC引脚逻辑电平是跟随VCC的。你用3.3V给VCC供电,SDA和SCL的逻辑高电平就是3.3V,而ESP32的GPIO也是3.3V逻辑,两者直接连完全没问题。有些人看到SHT30支持5V就顺手接5V,结果传感器模块上的上拉电阻接到5V,SDA和SCL高电平就变成5V,直接把ESP32的GPIO干烧了。所以记住:ESP32这种3.3V逻辑的单片机,传感器一律接3.3V。
3. SHT30通信协议与硬件IIC核心配置
3.1 I2C地址、命令集和测量模式
SHT30的7位I2C地址由ADDR引脚决定。ADDR接地时地址是0x44,接VCC时是0x45,大部分模块默认已经把ADDR拉低,所以地址就是0x44。这在代码里对应的是Wire.beginTransmission(0x44),注意Wire库要求的是8位地址,但beginTransmission内部会自动左移一位,所以直接写7位地址0x44就行,不用自己算。
SHT30的命令集很简洁,最常用的几个:
| 命令功能 | 命令字(16位) | 说明 |
|---|---|---|
| 单次测量,高重复性,Clock Stretching | 0x2C 0x06 | 最常用,读一次量一次 |
| 单次测量,高重复性,无Clock Stretching | 0x24 0x00 | 适合IIC超时敏感场景 |
| 周期测量,10Hz,高重复性 | 0x22 0x36 | 后台持续测量 |
| 读取状态寄存器 | 0xF3 0x2D | 返回3字节(含CRC) |
| 软复位 | 0x30 0xA2 | 重置传感器状态 |
单次测量模式是一次性触发一次采集,适合低功耗或者低频率读取的场景。周期测量模式则是传感器自己按频率不断刷新内部数据,主机随时可以读取最新结果,适合需要连续监控的场景。实际项目中我大多数时候用单次高重复性模式,命令是0x2C 0x06,因为每次读取前主动触发一次,数据新鲜度有保证。
3.2 Clock Stretching机制和Wire库超时设置
单次测量命令里分Clock Stretching和非Clock Stretching两种。Clock Stretching是IIC协议里一个比较特别的机制:传感器收到测量命令后,会把SCL拉低,告诉主机“我还没准备好,你先等着”,直到测量完成才释放SCL。这个机制在SHT30上默认开启,Arduino的Wire库是支持clock stretching的,但需要设置好超时时间,不然传感器测量耗时比较长(高重复性约12.5ms)时,Wire可能直接判定超时。
我实际测试下来,在ESP32上如果使用0x2C 0x06这个带Clock Stretching的命令,需要把Wire.setTimeOut(100)设置成100ms,否则在某些Wire库版本里会出现偶发的“I2C transaction failed”错误。如果你不想依赖这个机制,可以用非Clock Stretching的命令0x24 0x00,发出测量命令后主动delay(30)再读取数据,逻辑上更可控,也更容易排查问题。
这里延伸一句IIC仲裁机制的背景知识:IIC总线上如果有多个主机,或者主机和从机同时想控制总线时,靠SDA电平做仲裁,低电平优先。SHT30作为从机一般不会主动发起通信,但在Clock Stretching期间它会一直把SCL拉低,这个动作本质上也是一种“总线占用”,如果主机侧超时设置不对,仲裁机制会误判为总线忙,导致锁死。理解了这个原理,就明白为什么Wire.setTimeOut这么重要。
3.3 数据包格式、CRC校验和温湿度计算
读一次SHT30数据,主机先发测量命令,等测量完成后,用Wire.requestFrom(0x44, 6)向传感器请求6个字节。这6字节的排列规律是:温度高字节、温度低字节、温度CRC校验字节、湿度高字节、湿度低字节、湿度CRC校验字节。
CRC校验多项式是0x31,初始值0xFF,按MSB first方式计算。这个校验机制是SHT30比DHT系列靠谱很多的一个原因,尤其当传感器和主机之间有较长杜邦线时,数据发生bit错误是常有的事,CRC能帮你识别出无效数据,而不是直接显示一个离谱的数值。
温度原始值的计算方式是:将温度的两个字节拼成一个16位无符号整数rawTemp,然后套公式:
temp = -45.0 + 175.0 * rawTemp / 65535.0湿度同理:
hum = 100.0 * rawHum / 65535.0这两个公式是Sensirion官方给的线性换算,65535对应的是16位ADC的满量程。实际出来的数值就是小数形式的摄氏温度和相对湿度百分比,不需要再做任何修正。
4. 完整实现:初始化到稳定读取
4.1 工程搭建和依赖
这项目我用Arduino框架开发,开发环境用Arduino IDE 2.x或者PlatformIO都行。需要先在开发板管理器里装好ESP32开发板支持包(关键字搜esp32安装),然后在代码里引入Arduino自带的Wire.h。Wire.h就是ESP32硬件I2C外设的驱动库,直接用它就能做到“硬件IIC”,用户层面看到的几个API都是对底层寄存器的封装。
如果用的是PlatformIO,工程配置里在platformio.ini写一句:
[env:esp32dev] platform = espressif32 board = esp32dev framework = arduino monitor_speed = 115200然后直接写main.cpp就行。两种方式最终编译出来的固件行为一致,只是工程管理方式不同。
4.2 完整代码和分块讲解
下面这段代码是我实际在用的版本,去掉了一些业务逻辑,只保留核心读取流程。代码结构是:初始化I2C总线,定义SHT30读取函数,主循环里每2秒读取一次并打印温湿度。SHT30每次读取前发单次测量命令,等30ms让测量完成,再读6字节,解包后做CRC校验,最后转成温度和湿度打印出来。
#include <Wire.h> #define SHT30_ADDR 0x44 uint8_t sht30_crc8(uint8_t *data, size_t len) { uint8_t crc = 0xFF; for (size_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t j = 0; j < 8; j++) { crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : (crc << 1); } } return crc; } void sht30_read(float *temp, float *hum) { Wire.beginTransmission(SHT30_ADDR); Wire.write(0x2C); // 单次测量命令 Wire.write(0x06); // 高重复性,Clock Stretching uint8_t err = Wire.endTransmission(); if (err != 0) { Serial.printf("SHT30 write error: %d\n", err); return; } delay(30); uint8_t buf[6]; uint8_t rxLen = Wire.requestFrom(SHT30_ADDR, (uint8_t)6); if (rxLen != 6) { Serial.printf("SHT30 read error, got %d bytes\n", rxLen); return; } for (int i = 0; i < 6; i++) { buf[i] = Wire.read(); } if (sht30_crc8(buf, 2) != buf[2]) { Serial.println("Temperature CRC mismatch"); return; } if (sht30_crc8(buf + 3, 2) != buf[5]) { Serial.println("Humidity CRC mismatch"); return; } uint16_t rawTemp = (uint16_t)((buf[0] << 8) | buf[1]); uint16_t rawHum = (uint16_t)((buf[3] << 8) | buf[4]); *temp = -45.0f + 175.0f * rawTemp / 65535.0f; *hum = 100.0f * rawHum / 65535.0f; } void setup() { Serial.begin(115200); Wire.begin(); // 默认引脚 SDA=21, SCL=22 Wire.setClock(400000); // 硬件IIC时钟设为400kHz Wire.setTimeOut(100); // 超时100ms delay(100); } void loop() { float temp = 0.0f, hum = 0.0f; sht30_read(&temp, &hum); if (temp != 0.0f || hum != 0.0f) { Serial.printf("Temp: %.2f C, Hum: %.2f %%RH\n", temp, hum); } delay(2000); }代码里有个细节值得说一下:Wire.setClock(400000)把I2C时钟从默认的100kHz提到了400kHz,这是IIC的快速模式,SHT30完全支持。实测在400kHz下读取一次加转换的时间约为15ms,比100kHz快不少。如果你发现数据偶发CRC错误,先把时钟降到100kHz再试,大概率能解决。
还有一个经验之谈:代码里用了指针传参返回温湿度,这样调用方可以决定怎么处理数据,比直接在函数里写死串口打印更好复用。如果你同时接了两块SHT30(地址分别设为0x44和0x45),把sht30_read函数加一个地址参数就能直接复用。
4.3 读取频率与扩展思路
传感器上电后本身有个准备时间,大约几毫秒,所以我在setup里加了delay(100)让它稳定。实际项目里如果要做低功耗设备,可以把这段代码改造成:进入睡眠前关掉I2C引脚,定时唤醒后读取一次再睡回去,SHT30的单次测量模式配合ESP32的deep sleep,一节18650电池撑几个月没问题。
另外如果你在玩micro-ROS,想把温湿度数据发到ROS2系统里做节点数据源,核心采集逻辑还是这段代码,只是把Serial.printf那行换成micro_ros的publisher发布消息。我后来在ESP32上跑micro_ros时就是直接把这段代码的采集函数嵌进回调,非常省事,说明底层采集稳了,上层怎么接都顺手。
5. 踩坑实录与排查速查表
5.1 找不到设备、读取卡死、数据诡异
我调试IIC类传感器总结过一条经验:遇到问题先看地址对不对,再看上拉有没有,最后才怀疑时序和代码逻辑。下面这几个坑是我真实踩过的,列出来给大家参考。
现象一:I2C扫描不到0x44地址。这是最常见的坑。先用下面这段扫描代码确认设备地址:
#include <Wire.h> void setup() { Serial.begin(115200); Wire.begin(); for (uint8_t addr = 1; addr < 127; addr++) { Wire.beginTransmission(addr); if (Wire.endTransmission() == 0) { Serial.printf("Found I2C device at 0x%02X\n", addr); } } } void loop() {}如果扫描出来是0x45,说明板子上的ADDR被拉高了,把代码里的地址改成0x45就行。如果啥都没扫到,优先检查SDA和SCL是不是接反了,或者杜邦线虚接——这俩问题占了六成以上的概率。还有一种可能:你手里的SHT30模块压根没带上拉电阻,而你的ESP32板子GPIO21和22也没有上拉,这时候补两个4.7kΩ电阻到3.3V即可解决。
现象二:Wire.endTransmission()返回错误码2。这个错误码表示从机在地址阶段没有响应,也就是NACK。除了地址写错之外,还有可能是传感器供电不稳定,尤其是模块上同时接了EDP接口和杜邦线两种供电时,可能出现供电冲突。我遇到过一次是SHT30模块上两个电源引脚同时接了3.3V和5V,不仅读不到数据,模块还微微发烫,把电源线重新整理成仅接3.3V后恢复正常。
现象三:CRC校验频繁失败。这个问题的根源基本是通信链路质量不行。可能是杜邦线太长,超过20厘米后信号完整性下降;也可能是面包板接触不良;也可能是I2C时钟太快,400kHz下从机跟不上。排查顺序是:先降到100kHz试,再把线缩短或者换成焊接方式,最后检查一下上拉电阻是否在2.2kΩ~4.7kΩ范围内。实在不行就在读取结果上做个重试机制,CRC不过就重新读,连续三次失败才报错。
现象四:读回来的温度是0.00或者湿度是0.00。这种一般是数据包解出来rawTemp和rawHum都是0,可能是请求数据时没有等待足够时间。我原来用0x24 0x00命令时delay只写了5ms,结果读出来一半是0,改成delay(30)之后彻底解决。另外rawTemp如果是0xFF或0xFFFF,说明传感器还没完成测量或者总线被其他设备占用,同样加delay或者适当增加Wire.setTimeOut值。
现象五:传感器模块发热。SHT30本身功耗极低,运行时不应该有明显温升。如果你摸到芯片烫手,大概率是不小心打开了内置加热器。SHT30里确实集成了一颗加热电阻,主要用于高湿环境下清除凝露,但它可以被状态寄存器的HEAT位控制。正常读取流程不会触发加热器,如果你改过配置寄存器或者用了厂商的example代码,记得检查一下。软复位命令0x30 0xA2可以让传感器回到默认状态,加热器也会随之关闭。
5.2 一个快速定位的小技巧:逻辑分析仪
排查IIC问题的时候,如果单靠串口日志看不出来,强烈建议用一个十几块钱的逻辑分析仪,配合开源的PulseView软件,把SCL和SDA两根线夹住,就能直观看到时序波形。
你不需要懂太多协议细节,只要看两点:一是地址阶段有没有ACK,二是数据字节排列是不是6个。IIC通信过程中每个字节后都有一个应答位,从机收到自己的地址后会拉低SDA表示ACK,如果波形里SDA一直保持高电平,说明从机压根没回应。有了波形图,是主机没发命令还是从机没回复,一眼就清楚。
另外提一下IIC信号质量测试里经常说的上升沿问题:规范里快速模式要求SCL和SDA的上升时间不超过300ns,但实际上在面包板上用杜邦线,上升沿到1μs都很正常,只要不超过从机容忍范围就能工作。SHT30对时序的容忍度还不错,我试过用30厘米杜邦线依然能跑400kHz,只是CRC错误率明显升高。所以如果追求长期稳定运行,尽量缩短走线、降低时钟频率,这两招能解决大部分信号质量问题。
5.3 实测数据参考
最后给一组我在室温环境下实测的数据作为参考。环境温度约26℃,室内湿度约55%RH,连续读取100次,打印出来的数据:
- 温度范围:25.8℃ ~ 26.3℃
- 湿度范围:54.2%RH ~ 56.1%RH
- CRC错误次数:0次
- 每次读取耗时:约16ms
这个波动属于正常范围,传感器本身的噪声加上空气流动影响,温湿度小幅波动是正常的。如果某次数据偏离特别大,先检查是不是手碰到了传感器,或者传感器紧挨着开发板上的WiFi模块——ESP32的射频模块发热明显,如果SHT30靠得太近,测出来的温度会偏高,实测最高能偏2℃左右。所以传感器的安装位置要尽量远离发热源,或者在结构上隔热,这是一个被很多人忽略的细节。
我在后来的项目中特意把SHT30做成独立的传感器探头,用四根软线引出到机壳外部,配合一段短的PCB延长线,实测温度读数跟水银温度计对比误差在0.3℃以内,效果很好。如果你的项目对温湿度精度有要求,这个“远离板载热源”的小改动非常值得做。
本文还有配套的精品资源,点击获取