SHT31 温度湿度传感器驱动:从协议原理到完整驱动实现
2026/8/21 6:43:50 网站建设 项目流程

1. 引言:为什么选择 SHT31

在温湿度采集领域,Sensirion 的 SHT 系列传感器凭借体积小、精度高、功耗低的特点,长期占据工业与消费类产品的核心位置。SHT31 是该家族中的经典型号,它相比早期的 SHT10、SHT20 在响应速度、长期稳定性和接口友好度上都有明显提升。对于嵌入式工程师来说,SHT31 的 I2C 接口简洁、命令集规整,非常适合作为传感器驱动开发的学习样板,也能直接用于量产产品。

SHT31 官方典型指标如下:温度测量范围 -40 到 +125 摄氏度,分辨率 0.01 摄氏度,典型精度正负 0.3 摄氏度;湿度测量范围 0 到 100%RH,分辨率 0.01%RH,典型精度正负 2%RH。从这些指标可以看出,它既能满足普通环境监测,也能覆盖冷链运输、工业过程监控等较宽温区场景。

本文会从传感器本身的硬件与协议讲起,逐步深入到驱动架构设计,最后给出一套可跨平台移植的完整 C 驱动实现。无论你使用的是 STM32、ESP32、GD32 还是其他带有 I2C 外设的 MCU,都能把本文的驱动框架直接落到工程中使用。

2. SHT31 传感器概述

2.1 核心特性

SHT31 属于 SHT3x 系列温湿度传感器,内部集成了电容式湿度敏感元件和带隙温度敏感元件。与分立方案相比,它在封装内就完成了模拟信号调理、模数转换、校准系数管理和数字接口封装,用户通过 I2C 总线读到的已经是经过线性化和温度补偿的原始数字量,剩下只需要做简单的公式换算。

它的关键特性可以归纳为以下几点:

  • 全数字化输出:内部 16 位 ADC 完成采样,无需外接运放和滤波电路。
  • 出厂校准:每个传感器在出厂时都经过精密校准,校准系数存储在内部,无需用户再做多点标定。
  • I2C 接口:最高支持 1MHz 的快速模式加强版通信速率,地址可通过地址引脚配置。
  • 多种测量模式:支持单次测量、周期测量和带报警阈值的周期测量。
  • 低功耗:单次测量模式下典型平均电流仅 1 微安级别,非常适合电池供电设备。
  • 内置加热器:可用于自检、防结露和在高湿环境下做短期除湿。

2.2 封装与引脚

常见的 SHT31-DIS 采用 DFN 封装,尺寸只有 2.5mm 乘 2.5mm 乘 0.9mm,在 PCB 上占用面积非常小。引脚功能如下:

引脚名称功能说明
1VDD电源正极,SHT31-DIS 供电范围 2.15V 到 3.6V,SHT31-ARP 可到 5.5V
2SDAI2C 数据线
3SCLI2C 时钟线
4ADDR地址选择引脚,接地时地址 0x44,接 VDD 时地址 0x45
5ALERT周期测量模式下的报警输出,开漏输出,需外接上拉电阻
6R接地引脚,连接到电源地

需要特别注意的是,SHT31 对电源上电速度有要求。官方建议 VDD 上电时间不超过 1 毫秒,如果上电太慢,可能触发内部不完全复位,导致通信异常。因此在电源设计时应避免使用上升沿很缓的电源方案,或者在启动后由 MCU 主动发送一次软复位命令。

3. I2C 通信基础与硬件接线

3.1 I2C 总线回顾

I2C 是一种两线制同步串行总线,由 SCL 时钟线和 SDA 数据线构成。SHT31 始终作为从机存在,所有通信都由主机 MCU 发起。一次完整的读写通常遵循这样的流程:主机发送起始条件,然后发送 7 位从机地址加 1 位读写方向,从机应答后,再传输命令字节或数据字节,最后主机发送停止条件。

在 7 位地址模式下,SHT31 的从机地址由 ADDR 引脚决定:

ADDR 引脚状态7 位地址写地址字节读地址字节
接地0x440x880x89
接 VDD0x450x8A0x8B

这里写地址字节等于 7 位地址左移一位再加 0,读地址字节等于 7 位地址左移一位再加 1。0x44 左移一位得到 0x88,写操作用 0x88,读操作用 0x89。驱动代码中一般直接保存 7 位地址,在收发函数里统一做移位处理,这样切换地址时只需要改一个宏。

3.2 硬件电路设计

典型电路非常简单。VDD 与 GND 之间放置 100nF 去耦电容,尽量靠近传感器引脚。SDA 和 SCL 各自通过 4.7k 到 10k 电阻上拉到 VDD。ADDR 引脚直接接地或接 VDD 以选择地址,不要悬空。如果使用周期测量模式的 ALERT 报警功能,ALERT 引脚需要额外接一个上拉电阻,并连接到 MCU 的中断输入引脚。

布局上要注意传感器底部焊盘通常也需要接地,同时应避免将传感器放置在发热器件附近,否则热源会直接影响温度测量精度。传感器的感应窗口应尽量朝向被测环境,避免被外壳完全密闭导致响应变慢。

3.3 通信速率与时钟拉伸

SHT31 支持标准模式 100kHz、快速模式 400kHz 和快速模式加强版 1MHz。需要注意的是,时钟拉伸功能只在某些测量命令下有意义。所谓时钟拉伸,是指从机在准备数据期间把 SCL 拉低,让主机等待,数据就绪后再释放 SCL。对于没有正确实现时钟拉伸检测的主机程序,建议统一使用不带时钟拉伸的命令,由主机通过延时或轮询的方式等待转换完成。

4. SHT31 命令系统详解

4.1 命令格式

SHT31 的所有操作都是 16 位命令。主机通过 I2C 写入两个命令字节,第一个字节是命令高 8 位,第二个字节是命令低 8 位。部分命令后需要跟额外数据。命令的两个字节通常不是随机定义的,第二个字节的低位段往往带有规律,掌握这些规律有助于理解命令含义。

命令可以分为以下几大类:单次测量命令、周期测量命令、状态寄存器命令、加热器命令、软复位命令、停止周期测量命令以及读取芯片标识命令。

4.2 单次测量命令

单次测量命令又分为时钟拉伸使能和时钟拉伸禁用两组,并且可以指定测量精度。精度等级的可重复性从高到低分别为低、中、高,重复性越高,测量时间越长,噪声越小。下表汇总了常用的单次测量命令:

命令十六进制重复性时钟拉伸
单次测量0x2400禁用
单次测量0x240B禁用
单次测量0x2416禁用
单次测量0x2C06使能
单次测量0x2C0D使能
单次测量0x2C10使能

观察规律可以发现,第一字节 0x24 表示禁用时钟拉伸,0x2C 表示使能时钟拉伸。第二字节的低 4 位决定重复性:0x00、0x0B、0x16 分别对应高、中、低重复性。高重复性的典型测量时间约 15 毫秒,中重复性约 6 毫秒,低重复性约 4 毫秒,实际工程中如果需要快速刷新,可以选择中或低重复性。

4.3 周期测量命令

周期测量模式让传感器自动按照设定频率重复采样,MCU 只需要在需要数据时发起读取即可。周期测量可以配合 ALERT 报警功能做到阈值自动监控。常用周期测量命令如下:

命令十六进制每秒测量次数
周期测量0x20320.5 次每秒
周期测量0x20241 次每秒
周期测量0x202F2 次每秒
周期测量0x20224 次每秒
周期测量0x202110 次每秒

周期测量命令自身的重复性固定为中等重复性,不需要像单次测量那样选择精度。进入周期测量模式后,读取数据的命令统一使用 0xE000。要退出周期测量模式,需要发送停止周期测量命令 0x3093,这相当于一个中断操作,让传感器停止自动采样。

4.4 状态寄存器与系统命令

除测量命令外,SHT31 还提供一组系统级命令:

命令十六进制说明
读状态寄存器0xF32D返回 16 位状态字加 CRC
清除状态寄存器0x3041清除所有状态标志
软复位0x30A2复位传感器内部逻辑
停止周期测量0x3093退出周期测量模式
加热器开启0x306D打开内部加热器
加热器关闭0x3066关闭内部加热器
读取序列号0x3780返回芯片序列号,用于溯源

软复位是上电初始化时非常有用的命令。由于 SHT31 对电源上电速度敏感,有些系统上电后直接通信可能失败,此时先执行软复位并等待 1 毫秒以上,可以有效提高初始化成功率。

5. 测量模式详解

5.1 单次测量模式

单次测量模式是所有电池供电设备首选的工作方式。主机发送一条单次测量命令,传感器完成一次转换,主机再读取 6 字节数据,随后传感器自动进入空闲低功耗状态。整个周期内平均电流极小,非常适合定时唤醒采集的场景,比如每 10 秒采集一次的无线温湿度节点。

单次测量模式的数据读取流程如下:先通过 I2C 写操作发送 16 位测量命令,等待测量完成后,执行一次读操作读取 6 个字节。这 6 个字节依次是:温度高字节、温度低字节、温度 CRC、湿度高字节、湿度低字节、湿度 CRC。温度数据在前,湿度数据在后,每个 16 位数据后都紧跟一个 8 位 CRC 校验字节。

如果使用时钟拉伸使能的命令,传感器会在数据准备好之前把 SCL 拉低,主机无需手动延时,读操作会被传感器自动延长。如果使用时钟拉伸禁用的命令,主机必须等待标称测量时间后再发起读操作。两种方式各有利弊,时钟拉伸方式控制更简单,但要求主机的 I2C 控制器能正确处理时钟拉伸;手动延时方式兼容性最好,几乎在任何平台上都能稳定工作。

5.2 周期测量模式

周期测量模式适合传感器连续工作、主机周期性取数的场景。传感器进入周期测量模式后,会按照设定的频率自动测量并更新内部数据,使用 0xE000 命令读取时,它会返回最新一次转换结果。周期测量模式的优势在于读取延迟非常低,因为数据始终是最新的,不需要临时等待转换。

周期测量模式有三种变体,主要是为了配合报警功能。基础周期测量每秒测量 0.5 次、1 次、2 次、4 次或 10 次。带报警阈值的周期测量则额外允许设置温度上限、湿度上限、温度下限和湿度下限,当测量值越界时,ALERT 引脚会被拉低,主机可以通过外部中断立即响应。

周期测量模式下的读取命令 0xE000 会返回 6 字节数据,排列方式与单次测量完全一致,都是温度两字节加 CRC、湿度两字节加 CRC。因此无论是单次还是周期模式,驱动层的数据解析函数可以完全复用。

6. CRC 校验原理与实现

6.1 为什么需要 CRC

I2C 总线本身没有硬件校验机制,传输过程中如果受到电磁干扰,数据位可能发生翻转。对于温湿度这种对连续性较敏感的数据,一个错误的高字节足以让温度值跳变几十度。SHT31 在每个 16 位测量数据后附加一个 8 位 CRC,用来检测传输错误,这是工业级传感器保证数据可靠性的标准做法。

6.2 CRC-8 算法

SHT31 使用的 CRC-8 多项式是 x8 加 x5 加 x4 加 1,二进制表示为 0x31,初始值为 0xFF。校验数据只包含该组的前两个字节,不包含 CRC 字节本身。校验时把前两个字节与第三个字节一起送入 CRC 计算函数,如果最终结果等于 0,说明数据没有错误。

CRC-8 的逐位计算方法如下:为每个输入字节执行 8 次移位,每次先判断最高位是否为 1,如果为 1,则左移后与多项式 0x31 异或,否则直接左移。这种位运算法虽然直观,但速度较慢。工程中更常用查表法,预先把 0 到 255 每个字节对应的 CRC 值计算出来存成表,查表一次就能完成一个字节的 CRC 更新,效率提高数倍。

下面给出一个不带查表的 CRC-8 参考实现:

uint8_t sht31_crc8(const uint8_t *data, uint8_t len) { uint8_t crc = 0xFF; for (uint8_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t bit = 0; bit < 8; bit++) { if (crc & 0x80) { crc = (crc << 1) ^ 0x31; } else { crc <<= 1; } } } return crc; }

这段代码中,外层循环处理每个字节,内层循环按位处理。校验时把温度两个字节和温度 CRC 三个字节一起传入函数,返回 0 表示校验通过。实际使用时常会给 CRC 计算函数做一个包装,比如校验温度与校验湿度分开判断,这样出现错误时能立即定位到是温度段还是湿度段发生异常。

6.3 常见失败原因

CRC 校验失败的常见原因包括:I2C 总线速率过高导致信号质量差、上拉电阻过大使上升沿变缓、线路过长带来反射和串扰、传感器供电不稳等。遇到 CRC 持续失败时,优先级最高的排查动作是降低 I2C 速率到 100kHz 再测试,同时用示波器观察 SDA 和 SCL 的波形质量。

7. 温湿度数据转换

7.1 温度计算公式

SHT31 返回的温度原始值是 16 位无符号整数,范围是 0 到 65535。官方给出温度换算公式如下,其中 St 表示温度原始值:

T = -45 + 175 × (St / 65535)

这个公式把 0 映射到 -45 摄氏度,把 65535 映射到 130 摄氏度。虽然传感器的标称上限是 125 摄氏度,但公式的上限留有余量。计算出的温度单位是摄氏度。在工程中为了保留小数精度,通常会先放大后除以,比如把结果放大 100 倍,得到百分之一摄氏度单位的整数,再进行显示和比较。

7.2 湿度计算公式

湿度原始值 Srh 同样是 16 位无符号数,公式如下:

RH = 100 × (Srh / 65535)

湿度的最小值是 0,对应 0%RH,最大值 65535 对应 100%RH。本质上湿度原始值就是相对湿度的线性映射,因此转换极为简单。

7.3 定点数处理技巧

在资源有限的 MCU 上,浮点数运算可能带来额外的代码体积和运行时间。对于精确定时要求严格的场景,可以把浮点运算转换为整数运算。例如温度把原始值先乘以 17500 再除以 65535,就能得到放大 100 倍的温度值。若需要更高精度,放大倍数可以继续提高,例如放大 1000 倍再统一处理。这种定点计算方法是驱动层常用的优化手段。

typedef struct { uint16_t raw_temperature; uint16_t raw_humidity; } sht31_raw_data_t; typedef struct { int32_t temperature_mc; /* 温度,单位 0.01 摄氏度 */ int32_t humidity_mrh; /* 湿度,单位 0.01 %RH */ } sht31_th_data_t;

上面定义了原始数据结构和物理量数据结构,原始温度与湿度分别保存 16 位原始值,物理量则保存放大 100 倍的整数值。这样的结构让上层既能看到原始值用于调试,又能直接取物理值用于业务逻辑。

8. 驱动架构设计

8.1 分层思想

一个可维护、可移植的传感器驱动,通常分为硬件抽象层、协议逻辑层和业务应用层。硬件抽象层负责 I2C 的读写时序、延时等平台相关操作;协议逻辑层负责拼装命令、解析数据、校验 CRC,这一层与硬件无关;业务应用层则根据产品需求决定单次测量还是周期测量、是否设置报警阈值、如何做数据滤波。

分层的好处非常明显:把 SHT31 驱动从 STM32 移植到 ESP32,只需要改硬件抽象层的几个函数,其他代码一行不动。即使将来更换主控芯片,驱动的核心逻辑也保持稳定,这大大降低了维护成本。

8.2 硬件抽象层接口

驱动需要平台提供的接口通常只有两个:写数据与读数据。为了支持寄存器式访问和命令式访问两种风格,可以定义如下接口:

typedef struct { int (*i2c_write)(uint8_t addr, const uint8_t *buf, uint16_t len); int (*i2c_read)(uint8_t addr, uint8_t *buf, uint16_t len); void (*delay_ms)(uint32_t ms); } sht31_hal_t;

这个结构体把 I2C 写、I2C 读和毫秒延时三个平台相关操作抽象为函数指针。初始化驱动时,把具体平台的实现函数绑定进去。后续所有协议层代码都通过这个结构体访问硬件,绝不直接调用平台 API。

8.3 设备描述结构

驱动内部需要一个设备句柄来保存从机地址和硬件抽象层指针。所有驱动 API 的第一个参数都是这个句柄,以支持总线上同时挂载多个 SHT31 的场景。

typedef struct { uint8_t addr; sht31_hal_t hal; uint8_t is_initialized; } sht31_dev_t;

这种句柄式设计在很多成熟的驱动库中都能看到,例如 Linux 内核中的 i2c_client。它让一个驱动实例化多个设备变得自然,也便于后续做单元测试时替换硬件。

9. 驱动代码实现:命令与初始化

9.1 命令定义

驱动代码的第一部分是把所有命令做成宏或枚举。宏定义清晰直观,也是后续维护的重点。下面给出完整命令集:

#define SHT31_CMD_MEAS_HIGH_NOCS 0x2400 #define SHT31_CMD_MEAS_MED_NOCS 0x240B #define SHT31_CMD_MEAS_LOW_NOCS 0x2416 #define SHT31_CMD_MEAS_HIGH_CS 0x2C06 #define SHT31_CMD_MEAS_MED_CS 0x2C0D #define SHT31_CMD_MEAS_LOW_CS 0x2C10 #define SHT31_CMD_PERIODIC_0P5 0x2032 #define SHT31_CMD_PERIODIC_1 0x2024 #define SHT31_CMD_PERIODIC_2 0x202F #define SHT31_CMD_PERIODIC_4 0x2022 #define SHT31_CMD_PERIODIC_10 0x2021 #define SHT31_CMD_READ_DATA 0xE000 #define SHT31_CMD_READ_STATUS 0xF32D #define SHT31_CMD_CLEAR_STATUS 0x3041 #define SHT31_CMD_SOFT_RESET 0x30A2 #define SHT31_CMD_STOP_PERIODIC 0x3093 #define SHT31_CMD_HEATER_ON 0x306D #define SHT31_CMD_HEATER_OFF 0x3066 #define SHT31_CMD_READ_SERIAL 0x3780 #define SHT31_DEFAULT_ADDR 0x44 #define SHT31_POLY_CRC8 0x31 #define SHT31_CRC8_INIT 0xFF #define SHT31_MEAS_WAIT_HIGH_MS 15 #define SHT31_MEAS_WAIT_MED_MS 6 #define SHT31_MEAS_WAIT_LOW_MS 4

每一个宏都对应数据手册中的一条命令,注释应当标注命令含义。延时宏存放不同重复性对应的典型测量时间,使用时钟拉伸禁用命令时,主机按照这些数值等待即可。

9.2 发送 16 位命令

SHT31 的所有命令都是两个字节,因此可以把发送命令封装成一个统一函数。命令传输时通常先发高字节再发低字节,也就是大端序。

static int sht31_send_command(const sht31_dev_t *dev, uint16_t cmd) { uint8_t buf[2]; buf[0] = (uint8_t)((cmd >> 8) & 0xFF); buf[1] = (uint8_t)(cmd & 0xFF); return dev->hal.i2c_write(dev->addr, buf, 2); }

这个函数内部把 16 位命令拆成两个字节,然后调用硬件抽象层的写接口。返回值沿用底层返回约定,一般 0 表示成功,负数表示错误。

9.3 初始化与软复位

初始化流程通常包括绑定硬件层、发送软复位、等待复位完成。软复位后传感器进入空闲状态,如果之前处于周期模式,也会被退出。初始化函数同时校验底层 I2C 读写是否正常,确保通信可用。

int sht31_init(sht31_dev_t *dev, const sht31_hal_t *hal, uint8_t addr) { if ((dev == NULL) || (hal == NULL) || (hal->i2c_write == NULL) || (hal->i2c_read == NULL)) { return -1; } dev->addr = addr; dev->hal = *hal; dev->is_initialized = 0; if (sht31_send_command(dev, SHT31_CMD_SOFT_RESET) != 0) { return -2; } if (dev->hal.delay_ms != NULL) { dev->hal.delay_ms(1); } dev->is_initialized = 1; return 0; }

函数首先对传入参数做完整性检查,避免空指针导致崩溃。随后保存地址和硬件层,发送软复位命令,等待 1 毫秒让内部逻辑稳定,最后标记初始化完成。所有错误都通过返回码区分,方便上层根据不同错误执行对应处理。

10. 驱动代码实现:单次测量读取

10.1 测量与读取流程

单次测量的完整流程可以抽象为三步:发送测量命令、等待转换完成、读取并解析 6 字节数据。针对时钟拉伸禁用模式,等待时间由重复性决定;针对时钟拉伸使能模式,可以跳过等待,但为了代码统一,也可以保留极短延时。

下面实现一个接口,允许调用者指定测量命令,从而自由选择重复性和时钟拉伸配置:

static int sht31_read_raw(const sht31_dev_t *dev, uint16_t meas_cmd, uint32_t wait_ms, uint8_t data[6]) { if (sht31_send_command(dev, meas_cmd) != 0) { return -1; } if (dev->hal.delay_ms != NULL) { dev->hal.delay_ms(wait_ms); } if (dev->hal.i2c_read(dev->addr, data, 6) != 0) { return -2; } return 0; }

这个函数先发送测量命令,再延迟等待,最后连续读取 6 字节。之所以把测量命令和等待时间作为参数传入,是因为不同调用方对精度和响应速度的需求不同,一个函数即可覆盖所有单次测量场景。

10.2 高频采集接口

在业务层,最常见的需求是高精度单次测量。可以基于上面的底层函数封装一个专用接口:

int sht31_read_th_high_res(const sht31_dev_t *dev, sht31_th_data_t *th) { uint8_t data[6]; uint16_t temp_raw; uint16_t humi_raw; if (sht31_read_raw(dev, SHT31_CMD_MEAS_HIGH_NOCS, SHT31_MEAS_WAIT_HIGH_MS, data) != 0) { return -1; } temp_raw = ((uint16_t)data[0] << 8) | data[1]; if (sht31_crc8(data, 3) != 0) { return -2; } humi_raw = ((uint16_t)data[3] << 8) | data[4]; if (sht31_crc8(&data[3], 3) != 0) { return -3; } th->temperature_mc = sht31_calc_temperature_mc(temp_raw); th->humidity_mrh = sht31_calc_humidity_mrh(humi_raw); return 0; }

这段代码的核心是数据校验。温度原始值由前两个字节拼成,随后用前三个字节做 CRC 校验,返回非 0 表示温度段传输错误;湿度同理。只有两段都校验通过,才进行物理量转换。CRC 校验放在转换之前,保证错误数据不会进入后续业务逻辑。

10.3 转换函数

温度与湿度的定点转换函数如下:

int32_t sht31_calc_temperature_mc(uint16_t raw) { return (-4500L + ((int32_t)raw * 17500L) / 65535L); } int32_t sht31_calc_humidity_mrh(uint16_t raw) { return ((int32_t)raw * 10000L) / 65535L; }

温度函数中 -4500L 表示 -45.00 摄氏度,raw 乘以 17500 再除以 65535,得到放大 100 倍的温度增量,两部分相加即为最终温度。湿度函数把 raw 映射到 0 到 10000,也就是 0.00%RH 到 100.00%RH。使用 int32_t 存放中间结果,避免 16 位乘法溢出。

11. 驱动代码实现:周期测量与状态管理

11.1 启动周期测量

周期测量模式下,传感器会持续自动采样。启动时传入一个周期测量命令,例如每秒测量一次对应 0x2024。启动后进入空闲等待,传感器内部会以设定频率更新数据。

int sht31_start_periodic(const sht31_dev_t *dev, uint16_t period_cmd) { return sht31_send_command(dev, period_cmd); }

11.2 读取周期测量数据

周期模式下的数据读取与单次模式不同,它使用固定的读取命令 0xE000,不需要等待转换完成,因为传感器已经在后台持续刷新。读取后同样返回 6 字节并做 CRC 校验。

int sht31_read_periodic_th(const sht31_dev_t *dev, sht31_th_data_t *th) { uint8_t data[6]; uint16_t temp_raw; uint16_t humi_raw; if (sht31_send_command(dev, SHT31_CMD_READ_DATA) != 0) { return -1; } if (dev->hal.i2c_read(dev->addr, data, 6) != 0) { return -2; } temp_raw = ((uint16_t)data[0] << 8) | data[1]; if (sht31_crc8(data, 3) != 0) { return -3; } humi_raw = ((uint16_t)data[3] << 8) | data[4]; if (sht31_crc8(&data[3], 3) != 0) { return -4; } th->temperature_mc = sht31_calc_temperature_mc(temp_raw); th->humidity_mrh = sht31_calc_humidity_mrh(humi_raw); return 0; }

周期读取与单次读取的解析部分几乎一致,说明良好的底层封装可以显著减少重复代码。两者唯一差别在于测量命令不同以及是否需要等待转换。

11.3 状态寄存器

状态寄存器共 16 位,其中只有部分位有效。通过读状态寄存器可以了解报警状态、加热器状态以及最后一次测量是否有校验错误。读状态命令发送后,传感器返回两个状态字节和一个 CRC 字节。

int sht31_read_status(const sht31_dev_t *dev, uint16_t *status) { uint8_t buf[3]; if (sht31_send_command(dev, SHT31_CMD_READ_STATUS) != 0) { return -1; } if (dev->hal.i2c_read(dev->addr, buf, 3) != 0) { return -2; } if (sht31_crc8(buf, 3) != 0) { return -3; } *status = ((uint16_t)buf[0] << 8) | buf[1]; return 0; }

获取状态后,可以按位判断。状态字的第 15 位是报警挂起标志,第 14 位无定义,第 13 位是加热器状态,第 11 位是湿度报警,第 10 位是温度报警,第 4 位是系统复位检测。检测到异常后,通常读取状态并执行清除状态寄存器命令,为下一次报警检测做好准备。

12. 加热器功能与自检

12.1 加热器的作用

SHT31 内部集成一个小功率加热器,它的主要用途有三个。第一是高湿环境下防止结露,当传感器表面凝结水珠时,湿度读数会达到 100% 并暂时失效,短时间开启加热器可以驱散水汽。第二是功能自检,开启加热器后温度读数应该明显上升,如果温度毫无变化,说明传感器可能损坏或接触不良。第三是某些气流测量应用中,通过加热改变局部温度差来辅助判断。

12.2 开启与关闭

加热器控制非常简单,发送开启命令 0x306D 或关闭命令 0x3066 即可。加热器可以与其他测量命令叠加使用,也就是先开启加热器,再发送单次测量命令,同样能正常测量。

int sht31_heater_enable(const sht31_dev_t *dev) { return sht31_send_command(dev, SHT31_CMD_HEATER_ON); } int sht31_heater_disable(const sht31_dev_t *dev) { return sht31_send_command(dev, SHT31_CMD_HEATER_OFF); }

需要注意的是,加热器会明显增加整机功耗,同时加热过程会使温度读数高于环境温度。因此加热器只应在需要时短时间开启,加热结束后还要等待传感器恢复到环境温度,温度读数才有意义。加热器开启期间采集的温湿度数据不能直接当作环境值使用。

12.3 加热自检流程

一个完整的自检流程是:先关闭加热器,读取一次基准温度;开启加热器,等待几十毫秒;再次读取温度,与基准温度比较。如果温差超过设定阈值,说明加热器工作正常。反之则提示硬件异常。这个自检可以在出厂测试阶段执行,也可以在设备运行过程中定时做健康检查。

13. 报警模式与阈值配置

13.1 报警模式命令

SHT31 的报警周期测量模式允许设置温度和湿度的上下限。当测量值越过阈值时,内部报警标志置位,ALERT 引脚被拉低。主机可以通过查询状态寄存器或触发外部中断来获知报警事件。报警周期的测量频率命令与基础周期测量不同,需要查阅数据手册中带报警功能的对应命令。

报警阈值的写入命令格式比较特殊。以设置温度上限为例,主机先发送阈值写命令,再写入数值高字节、数值低字节和 CRC 字节,共三个字节。阈值数值采用与测量数据相同的 16 位原始值格式,因此设置前需要把目标摄氏温度按公式反向换算成原始值。

13.2 阈值反向换算

温度原始值与摄氏温度的关系在前面已经给出,反向换算公式为:

St = (T + 45) × 65535 / 175

湿度原始值反向换算更为直接:

Srh = RH × 65535 / 100

这两个公式可以把人类可读的阈值转换为传感器需要的原始值。设置阈值程序可以把摄氏度输入换算为原始值,再按大端序写入并附带 CRC。

13.3 报警处理建议

报警模式下,MCU 可以长时间休眠,把 ALERT 引脚接到外部中断输入。一旦温湿度越界,传感器主动拉低 ALERT,MCU 被唤醒后读取状态寄存器,确认是哪一项报警,执行对应业务动作,最后清除报警标志。这种方式可以让系统在无异常时保持极低功耗,同时又不会错过关键事件,非常适合冷链运输记录仪、仓库环境监控等场景。

14. 低功耗设计实践

14.1 功耗估算

低功耗应用的核心是理解每一毫安电流花在哪里。SHT31 在空闲状态下电流通常只有几微安,主要功耗集中在测量转换期间。单次测量高重复性的转换时间约 15 毫秒,转换期间电流约 800 微安,那么一次测量的耗电约等于 800 微安乘以 15 毫秒,换算后大约 3.3 微安时。如果每分钟测量一次,平均电流几乎可以忽略,此时系统总功耗往往被 MCU 的睡眠电流和无线通信功耗主导。

14.2 建议的采集策略

对于电池供电设备,推荐采用单次测量加深度睡眠的组合。MCU 定时唤醒后,先给传感器上电,等待稳定后发起单次高重复性测量,读取并校验数据,随后关闭传感器电源或让传感器进入空闲,MCU 回到睡眠状态。如果系统可以控制传感器的 VDD,还可以把传感器完全断电,彻底消除空闲电流。

在周期测量模式下,永远不要让 MCU 以轮询方式高频询问传感器。正确做法是把测量频率和 MCU 唤醒周期对齐,或者干脆改用 ALERT 报警中断来替代轮询,从而最大限度减少无效唤醒。

14.3 电源瞬态注意

测量转换过程中传感器内部数字电路和 ADC 都会工作,电流会从微安级瞬时上升到毫安级。如果传感器供电走线过长或去耦电容太小,瞬态电流会导致供电电压跌落,轻则测量值抖动,重则通信中断。建议在 VDD 引脚旁放置至少 100nF 电容,必要时再并联 1uF 到 4.7uF 电容,保证瞬态响应。

15. 调试方法与常见问题排查

15.1 通信失败的排查路径

调试 I2C 传感器时,最常遇到的第一个问题就是初始化后读不到数据、CRC 错误或设备无应答。排查时建议按照从硬件到软件的顺序逐层确认:

  • 检查地址:用示波器或逻辑分析仪确认地址字节是否与 ADDR 引脚配置一致,0x44 和 0x45 混淆是最常见的低级错误。
  • 检查上拉:确认 SDA 和 SCL 都有上拉电阻,且在 100kHz 速率下波形上升沿足够陡峭。
  • 降低速率:如果高频率下不稳定,先降到 100kHz 测试,排除信号质量问题。
  • 软复位:在初始化流程最前面加软复位和延时,排除上电时序导致的内部状态异常。
  • 检查电源:测量传感器 VDD 在通信和转换期间的电压波动,确认没有低于最低工作电压。

15.2 数据异常的表现与原因

数据异常可以分为几类。读数恒定为 0 或 65535,往往说明传感器还没完成转换就被读取,或者 I2C 总线出现了挂死。温度读数明显偏高,可能是传感器靠近发热芯片,或者加热器没有关闭。湿度读数长期为 100%,很可能是传感器表面结露或受污染。温度湿度来回跳变但 CRC 一直通过,多半是采样策略不合适,需要做滤波处理。

15.3 常用滤波方法

温湿度数据容易受到气流、人体活动、设备发热等短时干扰。常用的滤波手段有中值滤波、滑动平均和加权指数滤波。中值滤波对偶发的脉冲毛刺特别有效,取连续 5 次采样中间值即可。滑动平均能平滑高频抖动,但响应稍慢。加权指数滤波实现简单,内存占用小,通过调节系数可以在响应速度和稳定性之间平衡,是嵌入式应用中最常用的选择。

16. 完整工程示例:STM32 平台适配

16.1 HAL 层适配代码

以 STM32 的 HAL 库为例,硬件抽象层的三个函数可以这样实现:

static int stm32_i2c_write(uint8_t addr, const uint8_t *buf, uint16_t len) { HAL_StatusTypeDef st = HAL_I2C_Master_Transmit( hi2c_instance, (uint16_t)(addr << 1), (uint8_t *)buf, len, 100); return (st == HAL_OK) ? 0 : -1; } static int stm32_i2c_read(uint8_t addr, uint8_t *buf, uint16_t len) { HAL_StatusTypeDef st = HAL_I2C_Master_Receive( hi2c_instance, (uint16_t)((addr << 1) | 1), buf, len, 100); return (st == HAL_OK) ? 0 : -1; } static void stm32_delay_ms(uint32_t ms) { HAL_Delay(ms); }

这里 hi2c_instance 是 STM32 CubeMX 生成的 I2C 句柄全局变量,实际工程中可以改成设备句柄里的指针。写地址为地址左移一位,读地址为左移一位后或 1,这与 SHT31 协议完全一致。超时时间设置为 100 毫秒,如果总线异常,函数会返回错误而不是无限阻塞。

16.2 初始化硬件层

把平台函数绑定到驱动结构体的过程如下:

static sht31_dev_t g_sht31; static sht31_hal_t g_hal = { .i2c_write = stm32_i2c_write, .i2c_read = stm32_i2c_read, .delay_ms = stm32_delay_ms, }; int sht31_platform_init(void) { return sht31_init(&g_sht31, &g_hal, SHT31_DEFAULT_ADDR); }

整个平台的差异被收敛到这一个初始化位置。以后移植到其他平台,只需要替换 hal 结构体中三个函数指针,驱动核心完全不动。

16.3 业务层调用

业务层通过一个定时任务周期调用采集接口,并把结果送给显示或上报模块。典型的采集处理如下:

void temp_humi_task(void) { sht31_th_data_t th; if (sht31_read_th_high_res(&g_sht31, &th) == 0) { display_update(th.temperature_mc, th.humidity_mrh); report_upload(th.temperature_mc, th.humidity_mrh); } else { handle_sensor_error(); } }

当读取接口返回 0 表示成功,业务层才使用数据。返回负值则进入错误处理。这个简单的调用模式已经能覆盖大多数温湿度采集产品的核心逻辑。

17. 进阶话题:多传感器与滤波

17.1 总线多个传感器

一条 I2C 总线上理论上可以挂接两个 SHT31,分别使用地址 0x44 和 0x45。再多的话就需要借助 I2C 多路复用器,或者使用不同总线的独立通道。驱动句柄设计中每个设备实例保存自己的地址,因此多传感器场景下只需要创建两个设备句柄并分别初始化即可,读取逻辑完全复用。

17.2 温湿度滤波策略

如果直接把原始转换值展示给用户,数值会有明显抖动。建议在驱动层之上增加一个滤波层,对温度采用加权指数滤波,对湿度采用滑动平均。温度响应快,主要滤除瞬时噪声;湿度响应慢,需要处理气流的阶跃变化。两者参数应独立调节,不要共用一套滤波系数。

17.3 自校准与长期漂移

SHT31 出厂校准数据已经保证了典型精度,但长期暴露在污染、高湿或化学气体环境中,精度仍可能发生漂移。对于高可靠应用,建议周期性做基准校验。例如在恒温恒湿箱中多点比对,建立修正表写入 MCU 存储。部分高端应用会同时挂接两个传感器做冗余互检,当两者读数偏差超过阈值时触发维护告警。

18. 总结

SHT31 是一款协议清晰、资料完善、工程友好的温湿度传感器。本文从硬件引脚、命令系统、CRC 校验、数据换算讲到分层驱动设计,并给出了可移植的完整 C 驱动框架。总结起来,掌握一个传感器驱动的关键在于三件事:透彻理解数据传输协议、严格做好错误处理与校验、把平台相关代码和协议逻辑彻底分离。

在实际项目中,建议优先使用单次测量模式配合低功耗调度,数据校验严格把关,错误路径要完备。只要按照本文的思路落地,SHT31 驱动可以在很短的时间内完成移植和验证,为产品提供稳定可靠的温湿度采集能力。

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

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

立即咨询