☰
STM32F4+HAL库驱动SHT25温湿度传感器:I2C时序与CRC-8校验
2026/10/21 10:13:41 网站建设 项目流程

STM32F4系列配HAL库驱动SHT25-TR-0.4KS这颗温湿度传感器,是最近几年环境监测和冷链项目里非常常见的一套组合。SHT25本身定位不低,温度典型精度能做到±0.4°C,湿度典型精度±1.8%RH,数字I2C输出,驱动逻辑并不复杂。但真要把这套C语言驱动写得干净、稳定、可复用,从I2C时序处理到CRC-8校验,再到原始数据的线性转换,每一层都有容易踩的坑。这篇文章会把我的实现思路和实际调试经验完整过一遍,给正在调SHT2x系列驱动,或者准备选型温湿度传感器又拿不准怎么落地的朋友做个参考。

1. 先明确SHT25这颗传感器的定位和选型逻辑

1.1 型号后缀“TR”和“0.4KS”到底代表什么

拿到SHT25-TR-0.4KS这个型号,第一件事是先看懂后缀,不然采购和选型容易对不上号。

“TR”在绝大多数元器件型号里指卷带包装(Tape and Reel),是给贴片机用的盘装形式,没有实际电气含义。真正有精度含义的是“0.4KS”。SHT25系列按精度等级分成好几个版本,常见的有1KS和0.4KS两类,0.4KS代表温度精度标称能做到±0.4°C。数据手册上写的SHT25温度典型精度是±0.3°C,但量产分级时厂家把满足更高指标的部分归为0.4K档,剩下综合表现稍宽的归为1K档,所以0.4KS在选型表里通常意味着更严格的精度筛选,价格也相应高一些。

这个精度分级直接影响我的选型判断。如果项目只是做个室内温湿度显示,±1°C、±5%RH的传感器就绰绰有余,用0.4KS纯粹浪费预算。但如果用在冷链运输的温度记录、培养箱环境监控,或者需要给采集数据做溯源,高精度版本的意义就体现出来了。SHT25出厂带校准,数据可追溯到NIST标准,这在需要定期校验的行业里是个很重要的加分项。

1.2 和SHT20、SHT30放在一起比,差别在哪里

SHT2x系列和后来推出的SHT3x系列,我用下来最大的感受是定位差异很明显。SHT20是走量款,温度精度不错但湿度精度一般,典型±3%RH,胜在便宜。SHT30是新一代架构,有可配置地址脚,支持多个传感器挂一条总线,功耗也更低,但湿度精度同样没有超过SHT25。

SHT25的湿度精度是±1.8%RH,在整个SHT2x系列里属于高配,而且它保留了SHT21的引脚和命令兼容性。这意味着如果你的项目老代码里已经有SHT21驱动,换到SHT25几乎不用改驱动层,只改器件型号和精度声明就行。我做过一个模拟项目X,之前用的是SHT20,因为客户对湿度一致性提出更高要求,换SHT25之后软件层面只改了I2C器件地址和精度注释,采集数据直接平滑了很多。

这一轮对比给我的结论是:SHT25不是性能参数最亮眼的传感器,但它赢在精度等级明确、校准可追溯、兼容性好。在医疗、实验室、冷链类项目里,“指标被量化和可证明”往往比“峰值性能高”更重要,SHT25就是这种风格。

1.3 什么场景适合用SHT25,什么场景没必要

根据实际项目经验,我建议这几个方向优先考虑SHT25:

  • 需要记录温度和湿度的长期趋势,并且数据要经得起复核的
  • 传感器可能面临冷凝、高湿环境,需要对低端湿度准确性有更高要求的
  • 产品设计上要求传感器模组可拆卸、可更换、可单独校准的

反过来,如果是消费级室内温湿度计,或者只是做简单的环境联动控制,SHT30甚至更便宜的国产传感器就够用了。SHT25在100%RH高湿环境下的长期稳定性确实比普通传感器好,但它也有自己的短板,比如I2C地址固定为0x40,一条总线上没法挂多颗SHT25,这在多点分布式采集方案里是个明显限制。

2. 硬件接线和HAL库工程准备:底子打不好,驱动写得再漂亮也没用

2.1 引脚分配和上拉电阻的细节

SHT25是标准I2C接口,SDA和SCL两根线就够了。在STM32F4XX上,最常用的是I2C1外设映射到PB6/SCL、PB7/SDA,或者I2C2映射到PB10/PB11。我习惯用I2C1加PB6/PB7的组合,因为CubeMX里默认生成最快,调试时逻辑分析仪也好夹。

SHT25的数据手册明确说了器件内部没有集成上拉电阻,所以PCB上必须自己做上拉。我一般放在4.7kΩ到10kΩ之间,具体取值和总线速率、线长都有关系。100kHz标准模式下10kΩ没问题,400kHz快速模式下建议用4.7kΩ。如果传感器和MCU距离超过20cm,我会把上拉降到2.2kΩ,同时保证总线电容不至于太大。

这里有个容易被忽略的细节:STM32F4的GPIO虽然可以配置内部上拉,但千万不要只依赖内部上拉去跑I2C。内部上拉阻值大约在30kΩ到50kΩ,对I2C来说太弱了,尤其在快速模式下沿会很难看,容易导致通信偶发失败。我在一个早期原型板上偷懒没焊外部上拉,结果I2C初始化时好时坏,用示波器一量SCL上升沿软绵绵的,加上外部电阻立刻正常。

电源方面,SHT25供电范围是2.1V到3.6V,和STM32F4的3.3V系统直接对接没有问题。但要注意在传感器VDD和GND之间放一个100nF去耦电容,位置尽量贴近器件引脚。如果传感器和MCU距离较远,我还建议在靠近传感器端再并联一个1μF电容,防止线缆上的干扰影响内部参考电压。

2.2 CubeMX里配置I2C外设的关键参数

用STM32CubeMX配置I2C外设时,有几个参数不是默认就能直接用的,需要按实际需求调整。

首先是I2C速度模式。SHT25支持最高400kHz的快速模式,但我不建议一开始就往高了跑。开发阶段先用100kHz标准模式把驱动功能验证通,再根据实际波形决定要不要提到400kHz。标准模式下的时序容限大,很多边界问题会被掩盖,等代码稳定后再提速,遇到问题排查范围会小很多。

其次是I2C的7位地址。SHT25的地址固定是0x40,注意CubeMX的“Slave Address”栏填写的是7位地址还是8位地址,这很关键。HAL库里HAL_I2C_Master_Transmit和HAL_I2C_Master_Receive传入的地址参数,要求的是8位左移后的地址,也就是0x40左移一位变成0x80。很多人第一次写驱动时在这里昏头,把0x40直接传进去,结果就是总线上的地址对不上,设备永远无应答。

另外,CubeMX生成代码后,I2C的时序参数会根据选择的速率自动计算并填到初始化结构体里,正常情况下不需要手改。但如果你用的HAL库版本比较旧,或者改了系统时钟频率,最好重新生成一次代码,避免I2C时序参数和实际时钟不匹配。

2.3 我在I2C速率选择上的实际建议

实际项目里,我最终用的是400kHz快速模式配合4.7kΩ上拉。SHT25的转换时间在毫秒级别,I2C速率本身不是瓶颈,但400kHz能让单次读取从几十微秒降到十几微秒,对提高系统响应有微弱帮助。

需要留个心眼的是,400kHz下总线对线缆长度和走线质量更敏感。我用过一种排线比较长的扩展板,400kHz下偶发出现接收超时,降到100kHz后就完全正常。如果你的传感器不是直接贴装在主板上,而是通过线缆引出,老老实实先用100kHz,等系统稳定后再评估能不能提速。

3. C语言驱动程序设计:接口清晰、命令完整、CRC不省

3.1 驱动结构怎么拆分,才能在不同项目间复用

我写传感器驱动有一个基本习惯:驱动层只依赖HAL库的I2C句柄,不掺入任何业务逻辑,这样换MCU型号或者换主控平台时,驱动文件基本能原封不动搬过去。

SHT25驱动建议拆成两个文件。头文件里定义器件地址、命令码、状态枚举和数据结构,源文件里实现初始化、单次读取、CRC校验等函数。对外只暴露三个接口:设备初始化、读取温度、读取湿度,必要时加一个读取温湿度的组合接口。

这种设计的好处是应用层代码非常简洁。主循环里只需要定义一个sht25_data_t结构体变量,调用一次sht25_read_all,然后直接用结构体里的temperature_c和humidity_rh字段即可。后续如果要加多传感器轮询,只需要在应用层创建多个设备上下文,驱动层保持不变。

3.2 SHT25命令集和初始化函数的实现

SHT25的命令码不多,常用的就几个:0xE3是带主机保持的温度测量,0xE5是带主机保持的湿度测量,0xF3和0xF5分别是对应的无主机保持测量命令,0xFE是软复位,0xE0是读取上一次测得的温度值。

初始化我一般只做一件事:发送软复位命令,然后延时至少15ms让器件完成复位。SHT25上电后默认分辨率就是14位温度、12位湿度,不需要额外配置用户寄存器。除非你有特殊需求,比如想降低分辨率来缩短转换时间,否则初始化越简单越不容易出错。

源文件里开头定义一个静态的I2C句柄指针,初始化时把外部传入的hi2c保存下来,后续所有读写函数都用这个句柄。

/* sht25.h */ #ifndef SHT25_H #define SHT25_H #include "main.h" #define SHT25_I2C_ADDR_7BIT 0x40U #define SHT25_I2C_ADDR_WRITE (SHT25_I2C_ADDR_7BIT << 1) #define SHT25_I2C_ADDR_READ (SHT25_I2C_ADDR_7BIT << 1 | 0x01U) #define SHT25_CMD_T_HOLD 0xE3U #define SHT25_CMD_RH_HOLD 0xE5U #define SHT25_CMD_T_NOHOLD 0xF3U #define SHT25_CMD_RH_NOHOLD 0xF5U #define SHT25_CMD_READ_T 0xE0U #define SHT25_CMD_SOFT_RESET 0xFEU typedef enum { SHT25_STATUS_OK = 0, SHT25_STATUS_ERR_I2C, SHT25_STATUS_ERR_CRC, SHT25_STATUS_ERR_TIMEOUT } sht25_status_t; typedef struct { float temperature_c; float humidity_rh; } sht25_data_t; sht25_status_t sht25_init(I2C_HandleTypeDef *hi2c); sht25_status_t sht25_read_temperature(I2C_HandleTypeDef *hi2c, float *temp); sht25_status_t sht25_read_humidity(I2C_HandleTypeDef *hi2c, float *hum); sht25_status_t sht25_read_all(I2C_HandleTypeDef *hi2c, sht25_data_t *data); #endif
/* sht25.c */ #include "sht25.h" static I2C_HandleTypeDef *sht25_i2c; static uint8_t sht25_crc8(uint8_t *data, uint8_t len) { uint8_t crc = 0; while (len--) { crc ^= *data++; for (int i = 0; i < 8; i++) { crc = (crc & 0x80U) ? (uint8_t)((crc << 1) ^ 0x31U) : (uint8_t)(crc << 1); } } return crc; } sht25_status_t sht25_init(I2C_HandleTypeDef *hi2c) { uint8_t cmd = SHT25_CMD_SOFT_RESET; HAL_StatusTypeDef hal_st; sht25_i2c = hi2c; hal_st = HAL_I2C_Master_Transmit(hi2c, SHT25_I2C_ADDR_WRITE, &cmd, 1, 100); if (hal_st != HAL_OK) { return SHT25_STATUS_ERR_I2C; } HAL_Delay(20); return SHT25_STATUS_OK; }

这段代码我在多个项目里改过几个版本,最终保留的就是这个精简风格。软复位失败时直接返回I2C错误,应用层可以据此做重试或者异常上报。

3.3 CRC-8校验为什么不能省

SHT25每个测量结果会返回3个字节:前两个字节是16位的原始测量值,第三个字节是CRC-8校验值。如果省掉校验直接使用原始值,在电磁环境复杂的工业现场就可能拿到错误的温度和湿度数据,而且你根本不知道数据是错的。

SHT25的CRC-8算法基于多项式x^8 + x^5 + x^4 + 1,也就是0x31,初始值为0x00。计算时只需要对前两个数据字节做运算,然后和第三个字节比较。

我在一个温湿度记录仪原型上专门做过故障注入实验:人为翻转原始数据的一个位,不做CRC校验时,显示温度直接从25.3°C跳成31.8°C;开启CRC校验后,驱动能立刻识别数据异常并返回错误码,应用层重新读取一次就恢复了。这个差异在真实场景里非常重要,尤其采集数据要存库的场合,少一个错点比多一个看似合理但错误的数值重要得多。

4. 温湿度采集流程:从原始二进制到物理量的完整链路

4.1 hold mode和no-hold mode到底选哪个

SHT25触发测量有两种方式。0xE3/0xE5是主机保持模式,MCU发出测量命令后,传感器在转换期间会把SCL线拉低,强制主机等待,转换完成后释放SCL,主机再读取数据。0xF3/0xF5是无主机保持模式,传感器发完应答后立刻释放总线,主机需要自己估算转换时间,等够了再去读取。

我推荐大部分场景使用主机保持模式。原因很简单:HAL库的HAL_I2C_Master_Receive在等待从设备释放SCL时会一直阻塞,转换完成的时间由传感器自己决定,MCU不用关心具体的转换耗时。代码逻辑上就是发一条命令,然后直接读数据,简单可靠,不容易出现时序竞态。

无主机保持模式更适合多主机总线或者MCU需要在传感器转换期间去处理其他任务的场景。用这个模式时,驱动里得根据分辨率手动延时,比如14位温度转换最长约85ms,12位湿度转换最长约29ms,延时必须留足余量,否则会读到上一次的旧数据。

以下代码用的是hold mode,这也是我产品里最终采用的方案。

4.2 带CRC校验的完整读取函数

读取函数的关键点在于数据接收长度必须是3字节。如果只接收2字节,传感器返回的CRC字节会残留在I2C总线上,下一次通信可能错位。

sht25_status_t sht25_read_temperature(I2C_HandleTypeDef *hi2c, float *temp) { uint8_t cmd = SHT25_CMD_T_HOLD; uint8_t buf[3]; HAL_StatusTypeDef hal_st; uint16_t raw; hal_st = HAL_I2C_Master_Transmit(hi2c, SHT25_I2C_ADDR_WRITE, &cmd, 1, 100); if (hal_st != HAL_OK) { return SHT25_STATUS_ERR_I2C; } hal_st = HAL_I2C_Master_Receive(hi2c, SHT25_I2C_ADDR_READ, buf, 3, 300); if (hal_st != HAL_OK) { return SHT25_STATUS_ERR_I2C; } if (sht25_crc8(buf, 2) != buf[2]) { return SHT25_STATUS_ERR_CRC; } raw = (uint16_t)((buf[0] << 8) | buf[1]); raw &= 0xFFFCU; *temp = -46.85f + 175.72f * ((float)raw / 65535.0f); return SHT25_STATUS_OK; } sht25_status_t sht25_read_humidity(I2C_HandleTypeDef *hi2c, float *hum) { uint8_t cmd = SHT25_CMD_RH_HOLD; uint8_t buf[3]; HAL_StatusTypeDef hal_st; uint16_t raw; hal_st = HAL_I2C_Master_Transmit(hi2c, SHT25_I2C_ADDR_WRITE, &cmd, 1, 100); if (hal_st != HAL_OK) { return SHT25_STATUS_ERR_I2C; } hal_st = HAL_I2C_Master_Receive(hi2c, SHT25_I2C_ADDR_READ, buf, 3, 300); if (hal_st != HAL_OK) { return SHT25_STATUS_ERR_I2C; } if (sht25_crc8(buf, 2) != buf[2]) { return SHT25_STATUS_ERR_CRC; } raw = (uint16_t)((buf[0] << 8) | buf[1]); raw &= 0xFFFCU; *hum = -6.0f + 125.0f * ((float)raw / 65535.0f); if (*hum < 0.0f) *hum = 0.0f; if (*hum > 100.0f) *hum = 100.0f; return SHT25_STATUS_OK; } sht25_status_t sht25_read_all(I2C_HandleTypeDef *hi2c, sht25_data_t *data) { sht25_status_t st; st = sht25_read_temperature(hi2c, &data->temperature_c); if (st != SHT25_STATUS_OK) { return st; } st = sht25_read_humidity(hi2c, &data->humidity_rh); if (st != SHT25_STATUS_OK) { return st; } return SHT25_STATUS_OK; }

读取结束后,我做了个掩码操作raw &= 0xFFFC,把原始数据的低两位清掉。SHT25测量数据的低两位是状态标记位,可能反映器件状态或者测量分辨率,不是有效数据,必须屏蔽后再参与转换计算。

4.3 线性转换公式的原理和容易写错的地方

SHT25的温度和湿度原始值到物理量的转换都是线性映射,公式本身不长,但很容易写错的是缩放因子。温度换算公式是:

T = -46.85 + 175.72 * S_T / 65535

湿度换算公式是:

RH = -6 + 125 * S_RH / 65535

这里S_T和S_RH是掩码后的14位原始值,分母用的是65535而不是65536或16383,这是个细致活。Sensirion在出厂时把原始值映射到整个16位数据范围内,即使分辨率只有14位,原始数据也只是占了其中的高14位,低位置零后再按65535做归一化。我见过有人把分母误写成65536,结果整体偏了约0.02%,放在精度要求高的项目里还是能看出来的。

转换后的湿度理论上可能落在-6%到114%RH的数学范围内,但物理上湿度不可能低于0%或高于100%。所以我在驱动里加了两行钳位,保证输出值始终在合理范围内。温度则不做裁剪,因为SHT25本身支持-40°C到+125°C的工作范围,超出这个范围时保留原始计算值反而便于上层做故障判断。

5. 实测中遇到的坑和排查思路

5.1 I2C无应答或者超时的常见原因

第一类常见问题是传感器完全无应答,HAL_I2C_Master_Transmit返回HAL_BUSY或者HAL_TIMEOUT。排查顺序我固定是这样:先查地址,确认传入的是左移后的8位地址;再查上拉电阻有没有焊接;然后用示波器看SCL和SDA有没有波形。如果SCL有波形但SDA一直高电平,基本就是从设备地址错误或者传感器没上电。

第二类问题更隐蔽,总线在初始化时是通的,但运行一段时间后偶发超时。我遇到过的最典型原因是I2C工作在中断模式时,上一次传输还没完成就发起了下一次传输,HAL返回HAL_BUSY。这种问题往往不是硬件而是驱动调用逻辑导致的。建议所有I2C操作都做成独占式,或者用互斥量保护,避免两个任务同时访问总线。

还有一次是传感器在转换过程中MCU发生了复位,SCL被传感器拉住不放。此时整个I2C总线看起来就像短路了一样,任何通信都失败。处理办法是初始化时先对GPIO做几次手动翻转,把SCL时钟驱动起来让从设备释放总线,再重新初始化I2C。HAL库自带的复位机制不一定能覆盖到这种情况,我在产品代码里加了一段总线恢复逻辑才彻底解决。

5.2 CRC校验一直失败,到底是谁的问题

如果CRC校验通过率很低或者稳定失败,先不要怀疑传感器坏了,从三个方面查。

首先是接线质量。SHT25的I2C信号如果走线过长或者经过接插件,信号完整性问题会直接体现为数据位翻转,CRC自然过不了。把速率降到100kHz试一下,如果故障消失,大概率是信号质量问题而不是器件问题。我在一个用杜邦线飞的调试板上就遇到过,杜邦线一碰就CRC失败,焊到PCB上之后就再也没出现过。

其次是电源纹波。SHT25对电源噪声比较敏感,如果传感器的VDD是从数字电路直接拉过来的,而旁边又有大电流开关,转换结果可能不稳定。在VDD引脚加100nF电容,必要时再加1μF,基本能解决。

最后是焊接和污染。SHT25的湿敏元件对助焊剂残留很敏感,如果回流焊后没有清洗,器件在高湿环境下可能出现漂移,表现为读数异常但CRC正常。务必要保证器件周围干净,特别是不要用手指直接触摸传感器探头区域。

5.3 驱动跑通之后,做产品还要注意什么

驱动功能正常只是第一步。真正做产品时,我至少还会做三件事。

第一是控制采集频率。SHT25在转换期间会消耗较大电流,如果每秒读十几次,不仅浪费电,还会因为传感器自身发热导致温度读数偏高。我在设计里把温湿度采集频率限制在1秒一次,功耗和实测精度都能兼顾。对于低功耗电池供电的设备,建议每10秒甚至更久唤醒一次,采集完立刻再次进入休眠,SHT25待机电流可以降到微安级别。

第二是考虑长期漂移。SHT25虽然出厂校准精度高,但任何湿度传感器在长时间暴露于极端环境后都会有漂移。我在数据记录类产品里,会在传感器旁边预留一个可拆卸的校准口,定期用标准温湿度源做比对,把偏移量在应用层修正。这个做法比直接把传感器焊死在主板上要灵活得多。

第三是关注传感器布局和系统热量。SHT25不要紧挨着STM32主控芯片,也不要放在电源电路的散热风道上。PCB铜箔会导热,传感器附近的铜皮越少越好。我最早一版PCB把SHT25放在CPU旁边,实测温度比环境温度高了0.6°C,后来重新布局才把偏差降下来。

串口打印调试信息时,我习惯把原始值、CRC值、转换后的物理量一起打出来。开发阶段多打一行,排障时就能少猜一个问题。产品量产前再关掉这部分打印输出,实测并不会影响驱动稳定性。

整段驱动从设计到稳定,最大的感受是:传感器驱动没有太多玄学,把I2C时序吃透、把CRC校验做扎实、把数据转换公式写准确,剩下的问题大多能从硬件布局和信号完整性上找到答案。SHT25这颗物料精度好、接口简单,值得在可靠性优先的项目里认真对待。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询