I²C温湿度传感实战手记:从SHT35通信卡顿到稳定输出的全过程复盘
去年冬天调试一个部署在变电站户外机柜里的环境监测节点时,我连续三天被同一个问题困住:SHT35每隔十几分钟就突然返回0xFF 0xFF的“幽灵数据”,HAL_I2C_Master_Receive()超时返回HAL_ERROR,但示波器上看SCL/SDA波形明明“看起来挺正常”。后来发现,问题既不在代码逻辑里,也不在芯片手册第17页那个被我反复圈注的时序图上——而藏在PCB背面一条没包地的4.7 cm走线、一颗贴错位置的100 nF电容,以及STM32 HAL库里一个默认为ENABLE却该设为DISABLE的寄存器位。
这件事让我意识到:I²C不是“接上线就能通”的总线,尤其当它连着一颗精度标称±0.1°C的SHT35时,每一个看似微小的工程选择,都在悄悄改写最终读数的小数点后一位。
不是所有I²C都叫SHT35的I²C
先说个反常识的事实:SHT35的数据手册里,“I²C Interface”章节只有不到两页,但它真正决定你能不能拿到可靠数据的,其实是另外三处容易被跳过的细节:
地址不是固定的0x44
SHT35的I²C地址由ADDR引脚电平决定:接地为0x44,接VDD为0x45。但很多工程师画板时直接把ADDR拉到GND,结果现场想挂两个传感器做温差对比?得返工飞线。更隐蔽的是:某些LDO输出纹波大,导致ADDR引脚电压处于0.8V~1.2V的不确定区,SHT35会随机响应两个地址——现象就是偶尔能通信、偶尔NACK。它真的会“拖时间”
SHT35在执行高精度测量(如0x2C06命令)时,内部ADC需要16 ms完成采样+校准+CRC生成。这期间它会主动拉低SCL线(Clock Stretching),告诉主控:“别催,我还没好”。如果你在HAL初始化时写了hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_ENABLE;——恭喜,你亲手关掉了它的呼吸权。结果就是主控强行发STOP,SHT35吐出一串乱码。CRC不是可选项,是生存线
很多人把CRC校验当成“锦上添花”,直到某天发现湿度值在65%和95%之间疯狂跳变。SHT35的CRC-8(多项式0x31)校验是硬性协议层要求:温度数据块(2字节+1字节CRC)必须独立校验,湿度同理。绝不能对6字节一起算CRC——手册Figure 12明确画出了分段结构。跳过校验,等于主动拥抱噪声。
✅ 实战口诀:
- ADDR引脚务必用10 kΩ下拉/上拉电阻锁定电平,别悬空;
-NoStretchMode必须设为DISABLE;
- CRC必须按2+1+2+1字节分段验证,校验失败立即丢弃整帧。
看得见的波形,看不见的陷阱
上周帮客户排查一批批量失效的终端,用逻辑分析仪抓到这样的波形:SCL周期稳定400 kHz,SDA在传输第3字节时突然出现毛刺,紧接着SHT35返回NACK。表面看是干扰,但深入测了三组参数后真相浮出水面:
| 测试项 | 实测值 | 规范要求 | 偏差影响 |
|---|---|---|---|
| 总线电容(Cb) | 110 pF | ≤80 pF(400 kHz快速模式) | 上升时间超限,SDA无法在规定时间内达到VIH |
| 上拉电阻(RPULLUP) | 10 kΩ | 推荐2.2–4.7 kΩ | 驱动能力不足,边沿缓慢易受干扰 |
| SDA/SCL走线长度 | 18 cm | ≤10 cm(无屏蔽) | 分布电容放大,振铃加剧 |
根本原因?PCB设计时把I²C走线和电机驱动信号线并行走线了15 cm,且未铺地隔离。解决方案不是换更大上拉电阻(那只会让上升沿更慢),而是:
- 物理层手术:在SCL/SDA线下方挖空铺铜,改为完整GND覆铜;
- 电阻重配:将上拉电阻从10 kΩ改为2.2 kΩ,并直接焊在SHT35的SCL/SDA引脚旁(不是MCU端!);
- 时序再校准:重新计算HAL的
Timing参数,把上升时间余量从20%提到40%。
改完后,同一块板子在EMC实验室经受±2 kV静电放电测试,通信零中断。
🔧 关键动作:
- 用万用表二极管档实测ADDR引脚对地/对VDD电阻,确认电平锁定;
- 用示波器测SDA上升时间(10%→90%),必须≤300 ns(400 kHz模式);
- 每次修改硬件后,用HAL_I2C_IsDeviceReady()连续调用10次,检查地址响应稳定性。
SHT35固件里的“隐藏关卡”
HAL库让I²C通信变得像调用printf一样简单,但也埋下了几个新手必踩的坑。下面这段看似无害的代码,实际藏着三个致命隐患:
// ❌ 危险示范(不要这样写!) HAL_I2C_Master_Transmit(&hi2c1, 0x44<<1, cmd, 2, 10); HAL_Delay(20); // 等待测量完成 HAL_I2C_Master_Receive(&hi2c1, 0x44<<1, rx_buf, 6, 100);问题1:裸延时不可靠HAL_Delay(20)依赖SysTick,若系统开了FreeRTOS且任务切换频繁,实际等待可能远超20 ms。更糟的是,SHT35在测量中若被中断打断,可能进入未知状态。
问题2:地址硬编码0x44<<1写死地址,一旦硬件改成0x45,编译不报错,运行必失败。
问题3:忽略总线状态
两次HAL_I2C_*调用间没有检查总线是否空闲,若前次通信异常退出,hi2c1.State可能还是HAL_I2C_STATE_BUSY。
✅ 正确做法是构建一个带状态机的健壮读取函数:
typedef enum { SHT35_OK, SHT35_ERR_I2C, SHT35_ERR_CRC, SHT35_ERR_TIMEOUT } sht35_status_t; sht35_status_t SHT35_ReadTempHumidity(float *temp, float *rh) { // 1. 确保总线空闲(关键!) if (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY) { HAL_I2C_DeInit(&hi2c1); // 强制复位 HAL_I2C_Init(&hi2c1); } // 2. 发送测量命令(使用宏定义地址) uint8_t cmd[2] = {0x2C, 0x06}; if (HAL_I2C_Master_Transmit(&hi2c1, SHT35_ADDR << 1, cmd, 2, 100) != HAL_OK) { return SHT35_ERR_I2C; } // 3. 等待测量完成(轮询状态寄存器,非裸延时) uint8_t status[2]; uint32_t timeout = HAL_GetTick() + 100; while (HAL_GetTick() < timeout) { if (HAL_I2C_Master_Transmit(&hi2c1, SHT35_ADDR << 1, (uint8_t[]){0xF3, 0x2D}, 2, 10) == HAL_OK && HAL_I2C_Master_Receive(&hi2c1, SHT35_ADDR << 1, status, 2, 10) == HAL_OK) { if ((status[0] & 0x01) == 0) break; // Bit0=0表示测量完成 } HAL_Delay(1); } if (HAL_GetTick() >= timeout) return SHT35_ERR_TIMEOUT; // 4. 读取数据并校验 uint8_t rx_buf[6]; if (HAL_I2C_Master_Receive(&hi2c1, SHT35_ADDR << 1, rx_buf, 6, 100) != HAL_OK) { return SHT35_ERR_I2C; } if (SHT35_CRC8(rx_buf, 2) != rx_buf[2] || SHT35_CRC8(&rx_buf[3], 2) != rx_buf[5]) { return SHT35_ERR_CRC; } *temp = -45.0f + 175.0f * ((rx_buf[0] << 8) | rx_buf[1]) / 65535.0f; *rh = 100.0f * ((rx_buf[3] << 8) | rx_buf[4]) / 65535.0f; return SHT35_OK; }这个函数里藏着SHT35驱动的真正内功:
- 用Read Status Register(0xF32D)替代裸延时,精准感知测量完成时刻;
- 每次通信前检查HAL_I2C_GetState(),避免总线卡死;
- CRC校验失败直接返回错误,绝不把可疑数据喂给上层应用;
- 所有地址、命令都用宏定义,杜绝魔法数字。
当精度变成一场热力学博弈
SHT35标称±0.1°C精度,但我在一款工业网关上实测长期漂移达+0.7°C。用红外热像仪一扫,真相令人哑然:DC-DC电源芯片(MP2315)紧贴SHT35封装,工作时表面温度高达75°C,热量通过PCB铜箔传导至传感器底部——你测的不是环境温度,是电源芯片的体温。
解决思路不是换更高精度传感器,而是做热隔离:
- 物理隔离:在SHT35与电源芯片间开一道1 mm宽的散热槽(Slot),切断热传导路径;
- 空气隔离:SHT35焊接面下方PCB挖空,形成微型空气腔(Air Gap),利用空气低导热系数(0.024 W/m·K)缓冲;
- 软件补偿:在网关主板上加装一颗NTC热敏电阻,实时监测PCB基板温度Tpcb,对SHT35读数做线性补偿:
c compensated_temp = raw_temp - 0.023f * (pcb_temp - 25.0f);
补偿系数0.023°C/°C来自实测拟合(10组不同负载下的温漂数据)。
🌡️ 记住:传感器精度的敌人,从来不只是电路噪声,更是你忽视的热设计。一块没开槽的PCB,可能吃掉你花大价钱买的0.1°C精度。
写在最后:那些手册不会告诉你的事
- 上拉电阻不是越小越好:2.2 kΩ虽能加快上升沿,但会显著增加静态功耗(I = 3.3V / 2.2kΩ ≈ 1.5 mA)。对电池供电设备,建议用4.7 kΩ+软件优化时序;
- ALERT引脚别当普通GPIO用:SHT35的ALERT是开漏输出,必须外接上拉电阻(通常10 kΩ),且中断触发方式应设为下降沿——它只在告警发生时拉低;
- 加热功能慎用:SHT35内置加热器(0x306E命令)功耗约3.5 mW,持续加热会使自身温度升高2~3°C,若用于高精度测量,加热后需等待≥1秒再读数;
- 最可靠的故障诊断工具永远是示波器:当逻辑分析仪显示“通信成功”但数据异常时,请立刻切到模拟通道——眼见为实的边沿畸变、电源纹波耦合、地弹噪声,才是真正的破案线索。
如果你正在调试的SHT35也出现了“数据忽高忽低”“偶尔NACK”“上电后首帧必错”之类的问题,不妨从这三个地方开始检查:
① ADDR引脚有没有10 kΩ下拉/上拉电阻;
②NoStretchMode是不是被误设为ENABLE;
③ SCL/SDA走线底下有没有完整的GND覆铜。
真实的嵌入式开发,从来不是堆砌技术参数的游戏。它是一场与物理世界耐心周旋的过程——在硅片、铜箔、焊锡和空气的缝隙里,寻找那个让0.1°C精度真正落地的支点。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。