简介:SHT3x温湿度传感器代码详解,面向嵌入式开发者与智能硬件爱好者,覆盖环境监测、智能家居、工业自动化等场景,帮助快速掌握基于I²C/SPI通信的高精度温湿度数据采集实现。资源压缩包共13个文件,以4个C源文件与4个H头文件为主体,配合Keil工程文件(uvproj/uvopt)、启动文件、PDF结构说明与Readme文本,整包仅34KB,结构紧凑。已有997人学习下载。示例代码围绕初始化、发送测量命令、读取两字节温度与两字节湿度数据、错误处理等核心步骤展开,并配有SHT3x_Samplecode_Structure.pdf说明代码框架,可直接导入嵌入式工程编译调试,帮助开发者跳过传感器时序调试难点,快速集成到实际项目中。
1. 拿到 SHT3x 别急着读温度:先想清楚它是测量芯片,不是“模块”
SHT3x 温湿度传感器代码是最容易让嵌入式新手放松警惕的模块:I2C 读写不过几行函数,但真正的麻烦在示波器看不到的地方。A同学按模块卖家给的例程读到 25.3℃,自认为驱动已经调通,等产品进入连续运行才发现温度慢慢往上飘、偶发读回 0xFFFF,最后花了两天才定位到测量模式、CRC 和总线时序三个问题。这篇文章就是要把这条链路完整串起来:从命令选型、原始值换算、CRC 校验到多平台移植与排错,让做环境监测、冷链记录、智能家居的开发者可以照着这份思路把驱动从“能用”改成“可信”。熟手可以重点跳到第 5 章避坑和第 6 章自检,新手按章节顺序走一遍就能上手。
2. 上驱动前先对齐 SHT3x 的测量命令:单次与周期模式的选型对照
SHT3x 和单总线类的 DHT 系列不同,它走标准 I2C,从机地址由 ADDR 引脚的电平决定:ADDR 接地时是 0x44,接高电平或悬空处理成 0x45。这一点先确认,否则后面所有读写都会扑空。除了地址,SHT3x 的“测量命令”分成单次测量和周期测量两大阵营,每条命令都是两个字节,第一个字节是命令码,第二个字节是参数,两个字节一起在一条 I2C 写事务里发出去。实际工程里很多人不去看这一层,直接拿旧驱动里的 0x2C 0x06 套用,换成别的场景就出问题。
2.1 单次测量命令表:0x2C 和 0x24 两组,区别在时钟延展
单次测量表示“发一条命令,测一次,然后进入空闲”,适合电池供电、低采集频率的设备。SHT3x 把测量命令又拆成两套:允许时钟延展和不允许时钟延展。允许时钟延展的命令从 0x2C 开头,传感器在转换期间会把 SCL 拉低,主机端要一直等它释放;不允许时钟延展的命令从 0x24 开头,传感器不拉低 SCL,主机只能先等转换时间,再去发起读操作。
我一般在驱动里默认用 0x2C 那一组,因为读时序更简单,发完命令等够时间直接读 6 个字节就行。但如果你的 MCU 硬件 I2C 外设不支持时钟延展,或者你用的是 GPIO 模拟 I2C,0x2C 组会把人卡在等待循环里出不来,这时候必须换 0x24 组。高、中、低三档重复性对应不同的转换耗时:高约 12ms 量级,中约 5ms,低在 2.5ms 左右。工程上我习惯在数据手册给出的最大值基础上再加 5ms 余量,高重复性等 20ms,中等 10ms,低等 8ms,宁多勿少。
| 测量方式 | 高重复性 | 中重复性 | 低重复性 | 适用场景 |
|---|---|---|---|---|
| 单次,开启时钟延展 | 0x2C 0x06 | 0x2C 0x0D | 0x2C 0x10 | 硬件 I2C 支持延展时首选 |
| 单次,关闭时钟延展 | 0x24 0x00 | 0x24 0x0B | 0x24 0x16 | GPIO 模拟 I2C 或对延展敏感 |
| 周期,1 mps 高重复性 | 0x21 0x30 | 0x21 0x22 | 0x21 0x13 | 记录仪、周期性上报设备 |
2.2 周期测量命令与数据就绪机制:记录仪场景的默认选择
周期测量适合需要定时采样的场景,比如冷链记录仪每隔一秒采一次温湿度。SHT3x 的周期模式测量频率有 0.5 mps、1 mps、2 mps、4 mps、10 mps 五档,mps 是每秒测量次数。选择哪一档取决于你的上报频率和功耗预算:0.5 mps 适合几分钟才上报一次的设备,10 mps 适合做快速变化监测。要注意的是,周期模式一旦开启,传感器会一直按固定频率测量,即使你不去读数据,它也在耗电,所以电池设备不能光看“单次测量电流”去估算功耗。
周期模式的数据读取用 0xE0 0x00 作为读取命令,它会返回和单次测量一样的 6 字节数据布局。关键机制在于,如果上一次数据已经被读走,而新数据还没准备好,传感器会在主机发送读取命令时用 NACK 回应,告诉主机“现在没有新数据”。驱动里要把这个 NACK 当成正常状态处理,不能一收到 NACK 就当错误上报。实际代码中我会先发 0xE0 0x00,再发起一次读事务,如果读失败就延时 10ms 重试,最多重试三次。
2.3 原始值换算公式和 CRC-8 校验规则:别省掉最后一步
SHT3x 返回的是 16 位原始值,温度和湿度各占 2 字节,后面各跟 1 字节 CRC 校验,所以一次完整读取是 6 字节:温度高字节、温度低字节、温度 CRC、湿度高字节、湿度低字节、湿度 CRC。温度原始值 raw_t 换算成摄氏度的公式是:
T = -45 + 175 × raw_t / 65535
湿度原始值 raw_h 换算成相对湿度的公式是:
RH = 100 × raw_h / 65535
这里有几个容易忽略的地方。第一,raw_t 和 raw_h 都是无符号 16 位整数,不是有符号数,代码里用uint16_t接收,不能声明成int16_t,否则温度低于 0℃ 时会解析出负数错乱。第二,65535 是 16 位满量程值,不是 65536,除数是 65535,写错会引入千分之一的固定误差,虽然不大,但校准时就说不清了。第三,CRC 多项式是 0x31,初始值是 0xFF,每个数据字后紧跟自己的 CRC 字节,CRC 校验失败时必须丢弃整组数据,不能只保留温度而丢弃湿度,因为这两个字共享同一组测量过程。
3. 手写一版可移植的 SHT3x 驱动:三个回调、两次等待、一次校验
网上能找到很多 SHT3x 驱动,但大多数和具体平台绑得太死。我在项目里更愿意维护一版不依赖任何 MCU 厂商库的驱动,底层只留三个回调函数:写 I2C、读 I2C、延时。换平台时只改回调和延时实现,算法部分一行不动,这样批量验证过的代码可以反复复用。
3.1 先定义驱动结构体:地址、读写回调、延时回调三要素
驱动的核心是一个结构体,把设备地址、I2C 写函数指针、I2C 读函数指针、延时函数指针都放进去。后续所有函数都接收这个结构体指针,而不是使用全局变量,多实例时特别有用,比如一个产品里同时接了 0x44 和 0x45 两颗传感器。
/* sht3x.h */ #ifndef SHT3X_H #define SHT3X_H #include <stdint.h> typedef struct { uint8_t addr; /* 0x44 或 0x45,取决于 ADDR 引脚电平 */ int (*write)(uint8_t addr, const uint8_t *buf, uint16_t len); int (*read)(uint8_t addr, uint8_t *buf, uint16_t len); void (*delay_ms)(uint32_t ms); } sht3x_dev_t; uint8_t sht3x_read_once(sht3x_dev_t *dev, float *temp, float *humi); uint8_t sht3x_start_periodic(sht3x_dev_t *dev, uint8_t mps, uint8_t repeatability); uint8_t sht3x_fetch_periodic(sht3x_dev_t *dev, float *temp, float *humi); uint8_t sht3x_soft_reset(sht3x_dev_t *dev); #endif逻辑说明:addr是 I2C 从机地址,注意这个地址是 7 位地址,不是带读写位的 8 位地址,调用底层 I2C 时大多数库会自动左移一位。write和read返回值统一约定为 0 表示成功、非 0 表示失败,具体非 0 值由平台层决定。delay_ms必须提供,因为在单次测量模式下,主机需要主动等待转换完成。
3.2 单次测量驱动代码:发送命令、等待、读取、CRC 校验一条龙
这是最常用的一段代码,完整实现了“发命令 → 等待 → 读 6 字节 → 分字校验 → 换算出温度湿度”的全流程。
/* sht3x.c 核心实现 */ static uint8_t sht3x_crc8(const uint8_t *data, uint16_t len) { uint8_t crc = 0xFF; while (len--) { crc ^= *data++; for (uint8_t bit = 0; bit < 8; bit++) { if (crc & 0x80) crc = (uint8_t)((crc << 1) ^ 0x31); else crc = (uint8_t)(crc << 1); } } return crc; } static uint8_t sht3x_read_raw(sht3x_dev_t *dev, uint16_t *raw_temp, uint16_t *raw_humi) { uint8_t buf[6]; if (dev->read(dev->addr, buf, 6) != 0) return 1; /* I2C 读失败 */ if (sht3x_crc8(&buf[0], 2) != buf[2]) return 2; /* 温度 CRC 错误 */ if (sht3x_crc8(&buf[3], 2) != buf[5]) return 3; /* 湿度 CRC 错误 */ *raw_temp = (uint16_t)((buf[0] << 8) | buf[1]); *raw_humi = (uint16_t)((buf[3] << 8) | buf[4]); return 0; } uint8_t sht3x_read_once(sht3x_dev_t *dev, float *temp, float *humi) { uint8_t cmd[2] = {0x2C, 0x06}; /* 单次测量,高重复性,开启时钟延展 */ uint16_t raw_t, raw_h; if (dev->write(dev->addr, cmd, 2) != 0) return 4; /* 命令发送失败 */ dev->delay_ms(20); /* 高重复性转换时间留足余量 */ uint8_t err = sht3x_read_raw(dev, &raw_t, &raw_h); if (err) return err; *temp = -45.0f + 175.0f * raw_t / 65535.0f; *humi = 100.0f * raw_h / 65535.0f; return 0; }参数说明:0x2C 0x06是单次高重复性且开启时钟延展的命令,如果你用的是 GPIO 模拟 I2C 或有 I2C 外设不支持延展,把命令改成0x24 0x00,并把delay_ms(20)改成先延时再发起读事务。delay_ms(20)为什么是 20ms?因为高重复性转换时间典型在两位数毫秒以内,我留了 5ms 以上的余量,低重复性场景可以改成 8ms。CRC 校验函数对温度两个字节和湿度两个字节分别算,算出的值必须和传感器给的第三个字节相等,否则说明这次读到的数据已损坏。
3.3 周期测量驱动代码:开启周期模式与轮询读取
周期模式的驱动和单次模式不同,它不需要每次都发送测量命令,而是开启后传感器自动按频率测量。读取时用0xE0 0x00命令去取最新一组数据。
uint8_t sht3x_start_periodic(sht3x_dev_t *dev, uint8_t mps, uint8_t repeatability) { uint8_t cmd[2]; /* 以 1 mps 为例,高/中/低重复性分别为 0x21 0x30 / 0x21 0x22 / 0x21 0x13 */ switch (repeatability) { case 1: cmd[0] = 0x21; cmd[1] = 0x22; break; case 2: cmd[0] = 0x21; cmd[1] = 0x13; break; default: cmd[0] = 0x21; cmd[1] = 0x30; break; } (void)mps; /* 本函数固定使用 1 mps,其他速率需要改 cmd[0] */ return dev->write(dev->addr, cmd, 2); } uint8_t sht3x_fetch_periodic(sht3x_dev_t *dev, float *temp, float *humi) { uint8_t cmd[2] = {0xE0, 0x00}; uint8_t buf[6]; uint16_t raw_t, raw_h; /* 周期模式下,没有新数据时从机会回 NACK */ if (dev->write(dev->addr, cmd, 2) != 0) return 1; /* 数据未就绪,或总线异常 */ if (dev->read(dev->addr, buf, 6) != 0) return 2; if (sht3x_crc8(&buf[0], 2) != buf[2]) return 3; if (sht3x_crc8(&buf[3], 2) != buf[5]) return 4; raw_t = (uint16_t)((buf[0] << 8) | buf[1]); raw_h = (uint16_t)((buf[3] << 8) | buf[4]); *temp = -45.0f + 175.0f * raw_t / 65535.0f; *humi = 100.0f * raw_h / 65535.0f; return 0; }逻辑说明:0x21开头的是 1 mps 的周期测量命令,想用 0.5 mps 就把命令首字节改成0x20,4 mps 是0x23,10 mps 是0x27。返回值为 1 或 2 时,调用方应该做重试而不是立刻上抛错误,因为周期模式下数据未就绪本身就是常态。注意周期模式一旦开启,读到的一直是“上一次转换完成后的最新值”,不会和单次模式混淆,所以不用再等转换时间。切换模式前建议先发软复位命令0x30 0xA2,把芯片恢复到已知状态。
3.4 换算和数据类型:什么时候用 float,什么时候必须用整型
很多项目在 M0 级单片机上做温湿度采集,这类内核没有硬件浮点单元,float运算表面看着简洁,但每条浮点指令都被编译器转换成软件库调用,既占 Flash 又拖慢速度。我的习惯是:在开发阶段用float方便调试,定型后如果资源紧张,就切到定点整数计算。整型换算的思路是把温度乘以 100、湿度乘以 100,用整数表示两位小数。
/* 返回温度值乘以 100,例如 2531 表示 25.31℃ */ int16_t sht3x_temp_x100(uint16_t raw_t) { return (int16_t)((17500 * (int32_t)raw_t / 65535) - 4500); } /* 返回湿度值乘以 100,例如 5360 表示 53.60%RH */ int16_t sht3x_humi_x100(uint16_t raw_h) { return (int16_t)(10000 * (int32_t)raw_h / 65535); }注意int32_t的显式转换:17500 * raw_t在 16 位单片机里会溢出,必须先把其中一个操作数提升到 32 位。湿度公式也不需要节省,直接 10000 乘以原始值,避免在中间结果中出现小数。这个写法省掉整个浮点库,对于没有 FPU 的芯片收益很明显。
4. SHT3x 驱动上板移植:从裸机轮询到 RTOS 互斥的迁移要点
驱动写完之后,真正花费时间的往往是移植层。SHT3x 本身不复杂,复杂的是你的 I2C 总线可能还被其他器件复用,比如 EEPROM、气压计、RTC 都挂在同一条 I2C 上,这时总线的访问冲突会比传感器算法更让人头疼。
4.1 平台 I2C 读写与超时策略:先保证不卡死,再考虑性能
底层 I2C 回调如果写得不好,驱动再对也会卡死。常见做法是给读操作加超时,特别是 SHT3x 转换期间从机会通过时钟延展拉住 SCL,如果你的硬件 I2C 外设对时钟延展支持不完善,读操作可能永远等不到完成事件。我一般会在平台层包一层超时。
/* 示意代码:平台 I2C 读超时封装 */ int platform_i2c_read_timeout(uint8_t addr, uint8_t *buf, uint16_t len, uint32_t timeout_ms) { uint32_t start = get_tick_ms(); while (1) { int ret = hal_i2c_read(addr, buf, len); if (ret == 0) return 0; if (get_tick_ms() - start > timeout_ms) return -1; delay_ms(1); } }逻辑说明:hal_i2c_read是平台原来的阻塞接口,这里包了一层轮询重试。超时时间要看总线负载,读取 6 字节在 100kHz 下理论只要 1ms 左右,我把超时设置成 50ms,足够覆盖时钟延展和总线繁忙。如果超时还失败,就不要再试了,直接返回错误给上层,让上层决定是软复位还是重启任务。底层回调里尽量别做复杂的重试逻辑,重试放在上层的状态机里,这样不干扰总线上其他设备。
4.2 多任务访问同一个 SHT3x:互斥锁与事务边界
跑 RTOS 之后有个很容易翻车的点:主任务在发测量命令,另一个任务同时也在发读数命令,两个 I2C 事务交叉,数据就错位了。SHT3x 的驱动调用不是原子的,发命令、等待、读数据这三个步骤必须作为一个整体保护起来,中间不能插入其他 I2C 操作。
/* 示意代码:RTOS 下用互斥锁保护整个读事务 */ void task_read_sht3x(void) { float t, h; if (osMutexAcquire(i2c_mutex, 200) == 0) { uint8_t ret = sht3x_read_once(&sht3x_dev, &t, &h); osMutexRelease(i2c_mutex); if (ret == 0) { /* 正常使用 t 和 h */ } } }这里osMutexAcquire、osMutexRelease只是示意,实际以你的 RTOS API 为准,核心是锁的粒度必须覆盖“发命令 → 延时 → 读数据”整个流程。有人偷懒只锁读不锁写,结果命令在任务切换期间被对方的读事务打断,照样丢数据。另一个细节是延时 20ms 期间也不要占着锁不放?这里有个权衡:如果这条 I2C 总线还被别的设备用,锁着 20ms 会拖累其他设备;如果总线只挂了 SHT3x,那就放心锁住,简单可靠。
4.3 总线异常恢复:软复位和状态寄存器怎么配合
SHT3x 提供了软复位命令0x30 0xA2,可以不清除用户寄存器的前提下重置内部状态机。实际运行中如果连续读取失败,我会先发软复位,等 10ms,再重新发送测量命令。但要注意:软复位之后必须重新设置测量模式和加热器状态,因为寄存器会回到默认值。
读取状态寄存器用0xF3 0x2D,返回 3 字节,前两字节是状态字,第三字节是 CRC。状态字里各位的意义以数据手册寄存器表为准,常用的有加热器状态位和数据跟踪状态位。当读数长期异常时,先把状态寄存器读出来,比反复发复位命令更能定位根因。
uint8_t sht3x_read_status(sht3x_dev_t *dev, uint16_t *status) { uint8_t cmd[2] = {0xF3, 0x2D}; uint8_t buf[3]; if (dev->write(dev->addr, cmd, 2) != 0) return 1; if (dev->read(dev->addr, buf, 3) != 0) return 2; if (sht3x_crc8(&buf[0], 2) != buf[2]) return 3; *status = (uint16_t)((buf[0] << 8) | buf[1]); return 0; }逻辑说明:状态寄存器本身只有 16 位有效数据,但 SHT3x 仍然给它配了 CRC,读取时同样要校验,不能因为是状态帧就跳过。软复位后我习惯读一次状态字,确认设备确实响应了,再做后续测量,这样可以把“设备离线”和“设备在转换中”区别开。
5. SHT3x 调试避坑实录:五个实测翻车场景与处理方式
这一章是血泪经验汇总。SHT3x 看起来协议简单,但批量接入后各种现象都会冒出来,而且很多时候不是传感器本身坏了,是周边电路和软件时序的问题。
5.1 读回全 0xFF 先查地址,再查上电时序
现象:驱动程序读回来的温度、湿度全是 0xFF,或者全是 0x00,偶尔还伴随 I2C 总线卡死。
原因:最大嫌疑是地址不对。SHT3x 的 ADDR 引脚如果悬空或没接好,芯片上电时采到的电平可能不是预期值,0x44 和 0x45 就会混淆。还有一种是上电后立刻发命令,芯片内部还在复位,从机根本不应答,主机读到的是总线默认电平 0xFF。
解决:先用示波器或逻辑分析仪确认 ADDR 引脚电平,然后分别试 0x44 和 0x45 两个地址扫描。上电后再加 100ms 的延时才发第一条命令。如果是 SDA 被拉死,用 GPIO 模拟九个 SCL 时钟脉冲把总线上残留状态机复位。
5.2 温度比环境值偏高 2℃,不是传感器超差,是热源在板子上
现象:SHT3x 读数稳定,但就是比空调面板和标准温度计高 2℃ 左右,而且高出的数值很恒定。
原因:传感器旁边有 DC-DC 电感、LDO 或者 MCU 主控,PCB 局部温度被抬高了,SHT3x 测量的其实是自己封装附近的空气温度,不是环境温度。很多人一上来就怀疑传感器精度,先做排除法。
解决:把 SHT3x 挪到板边,下方 PCB 挖空或者开散热孔,避免和功率器件共用一块铜皮。实在不能挪位置,就在结构上做个导风罩,让空气流过传感器。如果以上都做不了,可以在代码里做热补偿,但我一般最后才考虑,治标不治本。
5.3 从周期模式切回单次后,偶尔读到 0xFFFF
现象:程序先开启周期测量,之后切到单次测量模式,大部分时间正常,但偶尔会读到异常值 0xFFFF。
原因:这是状态机切换的问题。周期模式一直在运行,你发单次测量命令时,芯片内部可能正处于下一次自动转换中,单次命令和周期内部状态打架了。另一个可能性是切换后旧的周期数据还在缓冲里,主机先把旧数据显示出来。
解决:切换模式前先发软复位0x30 0xA2,等 10ms,再发送新的测量命令。软复位会把内部状态机和测量定时器清干净,代价是每次切换多花十几毫秒,但对可靠性来说值。
5.4 CRC 偶发校验失败,I2C 时钟和中断时机都是嫌疑
现象:CRC 校验偶尔失败,不是每次,重读一次又好了。失败率可能在百分之一到千分之一之间波动。
原因:常见的有三个。一是 I2C 时钟配置太高,SHT3x 最高支持 1MHz,但很多 MCU 的 I2C 抖动大,400kHz 时沿容易出问题;二是 SDA/SCL 上拉电阻匹配不当,寄生电容大时信号边沿变形;三是读 6 字节的 I2C 事务在中间被打断,比如优先级更高的中断插入,把读数据切碎了。
解决:先把 I2C 时钟降到 100kHz,排除信号完整性问题。上拉电阻在 1kΩ 到 10kΩ 之间根据总线电容调整。最隐蔽的是中断问题,解决办法是把整个读事务放到临界区,或者确保读事务期间不会被高优先级中断打断。我在一个项目里就是被一个定时器中断坑了,关掉中断保护读事务后 CRC 失败率直接降到零。
5.5 高湿环境长时间运行后读数漂移,先考虑凝露污染
现象:传感器装在户外设备里,运行几个月后湿度读数逐渐偏高,温度还算正常,甚至在饱和湿度环境下卡在量程上限。
原因:长期高湿环境下,水汽在传感器感应区凝露,或者灰尘、盐分附着在芯片表面,影响了湿度感应膜的响应。这不是芯片坏了,是环境防护没做到位。
解决:结构上加防水透气膜,只让水汽透过、不让液态水接触芯片。PCB 上其他部分可以涂三防漆,但传感区域要避开。定期让传感器跑一次加热器自检,利用内部加热把凝露烘干。如果设备已定型无法改结构,代码里可以设置湿度变化率报警,连续湿度超过阈值时触发加热器工作一段时间。
6. 让 SHT3x 数据“可信”的最后一公里:对照基准与传感器自检
驱动跑通只是开始,数据能不能用于生产,还需要用基准点验证和自检手段兜底。我每批板子回来后不会直接信任读数,而是先抽几台做静态验证。
6.1 用饱和盐水做简易湿度基准点,验证校准偏移
在密封玻璃罐底部放一层氯化钠,加少量纯净水搅拌成饱和盐水,确保底部始终有未溶解的盐结晶。把罐子密封放在恒温环境静置 12 小时以上,罐内空气的湿度会稳定在约 75%RH。这个时候把 SHT3x 放进去读湿度,理论上应当非常接近这个基准值。如果偏差超过 3%RH,先检查传感器是否受污染,再做软件校准补偿。这个方法的优点是不需要买昂贵的湿度发生器,缺点是只能提供单一基准点,不能覆盖低湿度段。
6.2 用内置加热器判断传感器是否“假装健康”
SHT3x 内部有加热器,驱动里可以用0x30 0x6D打开、0x30 0x66关闭。自检流程是:先记录环境温度,打开加热器,等待 2 秒,再读温度,正常情况下读数会有明显上升;然后关闭加热器,等芯片散热恢复后再测。如果温度完全不变化,很可能传感器内部已经损坏或者焊接虚焊。注意加热自检之后要等 10 分钟以上再采集正式数据,否则读数会偏高。
6.3 把 CRC 失败率当作健康指标,建立长期监测
我在驱动里维护一个计数器,统计总读取次数和校验失败次数。上线初期 CRC 失败率如果连续 1000 次内出现多次,总线信号大概率有问题;长期运行阶段如果失败率慢慢爬升,则说明焊点老化或防护层失效。这个指标比单纯看温度数据更能提前暴露硬件隐患。
我的习惯是每台设备上电时做一次加热自检,然后采集 100 组数据统计 CRC 失败率,只有两者都通过才允许设备进入正式工作状态。做完这套流程之后,SHT3x 才真正从一个“能读数”的模块变成“能信”的传感器。希望帮到你。
本文还有配套的精品资源,点击获取