简介:SHT3x 温湿度传感器代码包面向嵌入式开发初学者与物联网应用工程师,围绕盛思锐 SHT3x 高精度温湿度传感器,提供一套基于 STM32 平台的完整示例工程,帮助解决环境监测、智能家居、工业自动化等场景中温湿度数据精确采集与解析的难题。压缩包共 13 个文件,大小 34KB,核心代码为 4 个 C 源文件和 4 个头文件,另含汇编启动文件、Keil µVision 工程文件、PDF 结构说明与 Readme 使用指引,导入工程即可编译调试。目前已有 997 人学习下载。通过源码可以系统掌握传感器初始化、I²C 通信时序、测量命令发送、原始数据读取及温湿度值换算的完整流程;配套 PDF 详解代码组织与配置方法,Readme 提供入门说明,便于快速定位关键函数并移植到其他硬件平台,为后续集成与二次开发节省大量时间。
1. 一块几块钱的传感器,代码却卡住大半个项目
SHT3x 温湿度传感器在嵌入式项目里几乎是“默认选项”:体积小、功耗低、精度够用。实际调试中却常出现这种情况——传感器能读到数,但湿度偶尔跳到 99%,或者温度比室温高 3℃,查到最后发现是命令时序和校验环节偷懒了。SHT3x 的测量代码并不复杂,核心就是发送命令、等待转换、读出 6 字节并用 CRC 校验,但这三个步骤里任何一步不严谨,都会让数据变得“正常但不可信”。这篇笔记按“原理 → 最小驱动 → 参数调整 → 踩坑 → 进阶验证”的顺序,整理出一套可复现的 SHT3x 采集方案。新手可以跟着把驱动跑通,熟手可以直接看第 5 章的排查清单。
2. 读懂 SHT3x 的测量模式:代码结构由命令时序决定
SHT3x 默认通过 I2C 通信,但它的工作方式分单次和周期两种,代码结构差异很大。先把测量模式理解清楚,后面写驱动才不会出现“发命令位置不对、读数据读到旧值”的问题。
2.1 单次测量与周期测量是两条完全不同的代码路径
单次测量模式下,主控每次发送一条测量命令,传感器完成一次转换后进入空闲状态。命令可以指定是否允许时钟拉伸:如果允许拉伸,传感器在转换期间会主动拉低 SCL,通知主机“请稍等”;如果禁止拉伸,主机就只能自己算好转换时间,延时够了再读数据。所以单次模式代码是典型的“请求-响应”结构:发命令、延时、读数据、校验。任何一个环节出错都容易定位。
周期测量则完全不同。传感器以固定速率持续转换,数据在内部不断刷新,主机想取数时直接发送“读取测量值”命令读回当前结果。周期模式适合屏幕显示、低功耗唤醒这类需要“数据随时可用”的场景,但代码里必须有状态判断,否则可能读到上一次的旧值。更麻烦的是,周期模式下你并不知道传感器此刻是在转换中还是已经就绪,很多总线卡死和 CRC 错误都出在这个模糊地带。
实际选择有一个朴素原则:如果你只是 1 秒采一次,或者还在调试排障,用单次模式更好,因为它独立、可控、出错好定位。如果系统要求传感器“一直热着”,数据随时可拿,才值得换周期模式。很多翻车现场都是单次模式的代码里套用了周期模式的读取逻辑,或者反过来。
2.2 必须记住的命令字和 CRC 多项式
SHT3x 的命令都是 16 位,发送时先发高字节再低字节。代码里我会保留下面这几个最小命令集,注释里写清用途:
| 命令字 | 用途 | 说明 |
|---|---|---|
| 0x30A2 | 软复位 | 传感器僵死时兜底用 |
| 0xF32D | 读状态寄存器 | 检查自检结果、命令状态 |
| 0x2C06 | 单次测量,高重复性,时钟拉伸 | 转换期间传感器拉低 SCL |
| 0x2C24 | 单次测量,高重复性,非时钟拉伸 | 主控自己延时后读数 |
| 0xE000 | 周期模式读取测量值 | 周期模式下取数 |
| 0x306D | 开启加热器 | 防凝露场景用 |
| 0x3066 | 关闭加热器 | 加热完毕必须关 |
CRC 校验是 SHT3x 代码里最容易被忽略的一环。传感器对每 16 位原始数据附一个 CRC 字节,温度占 3 字节、湿度占 3 字节,合在一起就是每次读取 6 字节。CRC 采用 CRC-8/MAXIM 风格,多项式 0x31,初始值 0xFF,既不需要反转输入也不需要反转输出。
下面是按位实现,配合寄存器资源吃紧的 MCU 也能跑:
static uint8_t sht3x_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 bit = 0; bit < 8; bit++) { if (crc & 0x80) crc = (crc << 1) ^ 0x31; else crc <<= 1; } } return crc; }这个函数按字节喂数据,每个字节都按多项式 0x31 逐位右挪。参数 len 对 SHT3x 来说每次传 2 即可,因为温度原始数据和湿度原始数据都是 16 位。换成查表法在速度上会更快,但按位版本更容易读懂、移植时不容易出错。
注意:不要只校验数据而不处理失败。CRC 不过时要丢弃本次结果,不能把坏数据交给业务层,否则温湿度偶尔跳变就会被当成传感器硬件问题。
2.3 地址引脚决定 I2C 地址,代码里不能写死
SHT3x 的 ADDR 引脚电平决定设备地址:接地时是 0x44,接 VCC 时是 0x45。很多人在面包板上用跳线帽换地址后忘记重启设备,导致连续读不到传感器。代码里建议把地址统一成宏管理,不要散落在各个函数里。
#define SHT3X_ADDR_0 (0x44 << 1) /* ADDR 接地 */ #define SHT3X_ADDR_1 (0x45 << 1) /* ADDR 接高 */这里 0x44 是 7 位地址,左移一位后变成 8 位地址,包含 R/W 位。不同 MCU 的 I2C 库对地址的接受方式不一样,有的收 7 位地址,有的收 8 位地址。我习惯统一在宏里转成 8 位地址,这样换个平台驱动代码不用改,只是 check 一下底层库的约定。
另外,如果总线上同时挂了多颗 SHT3x,就要通过 ADDR 引脚区分。此时建议在硬件上把引脚电平固定住,不要悬空,悬空会导致上电瞬间地址不确定,最后表现为“时而读到 0x44 时而读到 0x45”。这种偶发问题最难排查,不如一开始就用跳线或电阻固定电平。
3. 从零写最小驱动:I2C 初始化、测量命令与 CRC 校验一条龙
这一章进入可复现代码。驱动设计成“平台相关层 + 传感器逻辑层”两部分:底层 I2C 读写函数由具体 MCU 提供,上层代码只关心命令、数据、校验和换算。
3.1 先写标准的 I2C 读写函数
假设底层已经封装好两个接口,它们的原型如下:
int user_i2c_write(uint8_t dev_addr, uint8_t *buf, uint16_t len); int user_i2c_read(uint8_t dev_addr, uint8_t *buf, uint16_t len);dev_addr传 8 位地址,buf是数据缓冲区,len是字节数。两个函数返回 0 表示成功,非 0 表示失败。不同厂家的库可能提供复合接口write_then_read,但封装成两个函数后,上层代码可以保持平台无关。
接着封装 SHT3x 的命令发送和普通读数据:
static int sht3x_send_cmd(uint8_t addr, uint16_t cmd) { uint8_t buf[2]; buf[0] = (uint8_t)(cmd >> 8); buf[1] = (uint8_t)(cmd & 0xFF); return user_i2c_write(addr, buf, 2); } static int sht3x_read_bytes(uint8_t addr, uint8_t *buf, uint16_t len) { return user_i2c_read(addr, buf, len); }写命令时先发高字节,这是 SHT3x 的固定时序。如果高低字节写反,传感器不会应答或读回的数据全是 0xFF。很多第一次移植失败都是这个问题。
3.2 单次测量流程:发送命令、等待转换、读回 6 字节
以下用非时钟拉伸的高重复性单次测量为例,这是最稳、兼容性最好的用法:
#define CMD_MEASURE_NOHOLD 0x2C24 /* 高重复性,非时钟拉伸 */ typedef struct { uint8_t addr; int16_t temperature_x100; /* 实际温度乘以 100 */ uint16_t humidity_x100; /* 实际湿度乘以 100 */ } sht3x_t; static int16_t sht3x_convert_temp(uint16_t raw) { return (int16_t)(((int32_t)raw * 17500) / 65535 - 4500); } static uint16_t sht3x_convert_humi(uint16_t raw) { return (uint16_t)(((uint32_t)raw * 10000) / 65535); } static int sht3x_measure_nonhold(sht3x_t *dev) { uint8_t data[6]; uint16_t raw_t, raw_h; uint8_t crc_t, crc_h; if (sht3x_send_cmd(dev->addr, CMD_MEASURE_NOHOLD) != 0) return -1; /* 高重复性转换时间典型 12ms,最长 15ms */ platform_delay_ms(15); if (sht3x_read_bytes(dev->addr, data, 6) != 0) return -2; raw_t = ((uint16_t)data[0] << 8) | data[1]; crc_t = data[2]; raw_h = ((uint16_t)data[3] << 8) | data[4]; crc_h = data[5]; if (sht3x_crc8(&data[0], 2) != crc_t || sht3x_crc8(&data[3], 2) != crc_h) return -3; dev->temperature_x100 = sht3x_convert_temp(raw_t); dev->humidity_x100 = sht3x_convert_humi(raw_h); return 0; }命令 0x2C24 表示高重复性、非时钟拉伸单次测量。选择非时钟拉伸是为了避免传感器拉低 SCL 后,某些 MUC 的 I2C 外设不擅长处理这种等待状态。平台延时 15ms 是按高重复性的最大转换时间取的上限,中重复性约 6ms、低重复性约 3ms,但工程上统一用高重复性更省心,15ms 的延时代价在绝大多数应用里可以忽略。
温度和湿度换算用了纯整数运算。温度公式是 -45 + 175 × raw / 65535,为了返回实际温度乘以 100,改写成(raw * 17500) / 65535 - 4500。湿度公式是 100 × raw / 65535,乘以 100 后用(raw * 10000) / 65535。这样避免了浮点运算,在低成本 MCU 上更有优势,误差不超过 0.02℃。
3.3 数据换算:把原始值变成带单位的温湿度
上面的代码已经调用换算函数。单独拆出来是为了方便做单元测试,也方便以后扩展成浮点版本。换算函数在 3.2 中已经给出,这里补一点使用建议:
temperature_x100是int16_t,存的是实际温度 × 100,例如 2323 表示 23.23℃;负温度也能正常表达。humidity_x100是uint16_t,存储值范围 0~10000,显示时除以 100 即可。- 打印到日志或屏幕时,建议保留一位小数即可,SHT3x 的湿度分辨率本身就是 0.01%RH,没必要展示满打满算。
CRC 校验失败时,函数返回 -3。调用方不要忽略返回值,至少要在日志里记一笔。很多“偶尔跳变”的现场问题,靠的就是这个错误码统计才定位到总线干扰。
4. 把代码调稳:测量模式、加热器与滤波参数的取舍
驱动能读到数只是第一步,真正影响体验的是参数选择。这一章讲三个高频选择:重复性、滑动平均窗口、加热器要不要开。
4.1 测量重复性参数怎么选:耗时、噪声和功耗的平衡
SHT3x 的重复性分低、中、高三档,对应转换时间大约 3ms、6ms、15ms。高重复性噪声更低,适合长期记录和标定;低重复性响应快,适合快速扫描。如果系统每秒采一次,建议直接固定高重复性,因为功耗差异微乎其微,而数据质量差别能直接看得到。
有些代码为了省电,把每次测量都设成低重复性,结果湿度波动明显变大。实际上单次测量的功耗本身就在微安级,对电池供电的产品来说,关键是降低测量频率,而不是反复压缩单次转换时间。所以我的习惯是:低频采集优先保证质量,高频采集才考虑用中低重复性。
4.2 滑动平均滤波的窗口多大合适
温湿度传感器本身有随机噪声,加上环境微小波动,原始数据会抖动。滑动平均是最稳妥的滤波手段,窗口大小直接决定响应速度和平滑程度。参考下表:
| 窗口长度 | 平滑效果 | 响应速度 | 适用场景 |
|---|---|---|---|
| 4 | 一般 | 快 | 快速记录,保留变化细节 |
| 8 | 较好 | 中 | 室内环境监测 |
| 16 | 平稳 | 慢 | 长时间平均,仓储类监测 |
窗口 8 是大多数场景的折中点。实现上可以用环形缓冲维护累加和,避免每次求平均都遍历窗口:
#define FILTER_WIN 8 static uint32_t sum_temp_x100 = 0; static uint16_t buf_temp_x100[FILTER_WIN]; static uint8_t idx_temp = 0; static uint8_t cnt_temp = 0; static uint16_t filter_update(uint16_t new_val, uint32_t *sum, uint16_t *buf, uint8_t *idx, uint8_t *cnt) { if (*cnt < FILTER_WIN) { buf[*idx] = new_val; *sum += new_val; (*cnt)++; } else { *sum -= buf[*idx]; buf[*idx] = new_val; *sum += new_val; } *idx = (*idx + 1) % FILTER_WIN; return (uint16_t)(*sum / *cnt); }这个函数的逻辑是:窗口未满时直接加入累加和;窗口满后,新值进来先减掉最旧的值,再加新值。cnt用来处理前几个数据不足窗口长度的情况,避免除以 0。注意累加和sum要用uint32_t,因为温度 × 100 后最大值约 13000,窗口 8 时总和约 104000,虽然uint16_t勉强装得下,但窗口一旦扩大到 16 或 64 就会溢出,提前用 32 位是习惯。
4.3 加热器在防凝露场景里要不要开
SHT3x 内置加热器,可以给传感器局部升温,用于排除冷凝水、潮湿环境恢复。但加热期间温度读数会明显偏高,它不是用来测环境温度的。什么时候开?如果设备会经历结露,比如户外机柜、冷链箱,在检测到湿度持续高企且温度接近露点时,开启加热器一段时间,再关掉等传感器回稳,然后继续正常测量。
我一般建议加热周期设 1~5 秒,关闭后等 10 秒以上再取数,否则读到的温度会比环境高不少。还有一点容易被误解:加热器不能当作“自检”恒开,开了就必须关,否则传感器温度长期偏高,加速老化且耗电。
提示:加热器命令是 0x306D 开、0x3066 关。开关后都需要读一次状态寄存器确认状态位,但实际代码里很少有人这么做;省事一点的做法是开关后固定延时 2ms 再继续。
5. SHT3x 调试避坑:从地址错误到湿度跳变的五个典型问题
这一章是我在调 SHT3x 时最容易踩的坑,按出现概率排序。每条都按“现象 → 原因 → 解决”来写,方便对照排查。
5.1 传感器无应答:先查地址,再查电源
现象:调用读写函数返回失败,示波器上看不到 ACK,或者读回的数据全为 0xFF。
原因:最常见是 I2C 地址不匹配。SHT3x 有 0x44 和 0x45 两个地址,由 ADDR 引脚决定。硬件上把 ADDR 接地,代码里却用 0x45,自然无应答。其次是供电问题,SHT3x 供电必须在 2.4V 到 5.5V 区间内,如果 VDD 引脚纹波过大,传感器也会偶发无响应。
解决:先用 I2C 扫描函数或逻辑分析仪确认总线上实际出现的地址,再对照代码宏。然后检查 VDD 和 GND 之间是否放了 100nF 去耦电容,没有的话加上。最后确认 SDA/SCL 上拉电阻在 1k 到 10k 之间,过大的上拉在总线电容高时拉不上去。
5.2 CRC 校验频繁失败:多半是没等转换完成
现象:读回数据时 CRC 错误率很高,但偶尔还能读到一组看起来合理的值。
原因:发送单次测量命令后立即读取,传感器还没完成转换,或者转换命令要求时钟拉伸但主机库不支持等待,导致读到半更新状态的数据。还有一种情况是延时设在了转换时间边界上,比如高重复性转换需要 15ms 最长,代码只延时了 10ms。
解决:把延时加到最大转换时间之上。高重复性用 15ms,中重复性用 7ms,低重复性用 4ms。如果是周期模式,不要每次发送启动命令,而是用 0xE000 直接读测量值,同时检查状态寄存器里的数据就绪位。读状态寄存器命令是 0xF32D,返回的 16 位状态中某一位标记数据是否就绪,具体位号以数据手册为准。
5.3 湿度读数停在 100% 或反复跳变:先排查环境与焊点
现象:湿度长时间显示 100%,或者在几十秒内跳变超过 10%RH。
原因:传感器周围局部湿度饱和,比如探头沾了水、结露、贴膜没撕;也可能是 PCB 焊接残留助焊剂受潮后导电,改变了湿度读数。供电噪声大时,湿度跳变也会更剧烈。
解决:先看环境,把传感器放在通风处,避开呼吸、热气出口。再看硬件,用酒精清洗焊点,烘干后再测;确保走线避开电源开关节点。软件层面可以加滤波,但治标不治本。如果换一颗传感器对比后故障消失,那就是旧探头本身的问题。
5.4 I2C 总线卡死:SDA 被拉低后不恢复
现象:程序在某个 I2C 操作后卡死,复位 MCU 后仍然失败,逻辑分析仪显示 SDA 一直被拉低。
原因:传感器在异常时序下进入错误状态,或者主机时钟线被干扰打断,导致从机一直等待一个不存在的时钟边沿。某些 I2C 外设遇到 NACK 后也不会自动复位状态机,于是总线被锁死。
解决:在初始化阶段增加总线恢复流程:先释放 SDA,再把 SCL 翻转 9 次,最后发一个 STOP。也可以直接对传感器执行软复位 0x30A2。代码里每个读写调用都要加超时,超时后调用总线恢复函数。这能避免大多数“运行几个月后突然死掉”的现场问题。
5.5 长期运行后无响应:软复位命令兜底
现象:设备连续运行几天后传感器突然无响应,断电重启后恢复,但复现时间不确定。
原因:可能源于 I2C 总线竞争、供电瞬断或传感器内部状态机跑飞。这类问题很难在台架上一次复现,属于典型的运行期黑匣子问题,必须用代码兜底。
解决:在主循环里做一个健康检查,连续读 3 次失败就发送软复位命令 0x30A2,延时 20ms 后再重新初始化;如果仍然失败,记录错误码并再次初始化 I2C。软复位会保留设备地址和配置,是安全兜底。注意软复位后至少等 1ms 再发测量命令,否则可能不生效。
6. 进阶技巧:用状态机调度周期采集,并拿 24 小时数据验证可靠性
6.1 用状态机代替阻塞式延时
裸机环境下,每次阻塞 15ms 等传感器转换会让 MCU 浪费大量时间,尤其是在已经有屏幕刷新、按键扫描和通信协议栈的时候。更优雅的做法是把采集流程拆成状态机。
简单来说,定义 IDLE、START_CONV、WAIT_CONV、READ_DATA、CRC_FAIL 几个状态。IDLE 时判断采集周期是否到了,到了就发送测量命令并切到 WAIT_CONV;WAIT_CONV 里用定时器 tick 计数判断 15ms 是否过去,过去后发送读命令并切到 READ_DATA;READ_DATA 里读回数据、做 CRC 校验和换算,然后回到 IDLE,同时记录本次耗时。CRC 错误时切到 CRC_FAIL,做一次计数,连续多次失败再触发软复位。
这种结构比阻塞式代码复杂一点,但好处很明显:测量过程不再阻挡其它任务,超时和复位逻辑也更容易加。我现在写 SHT3x 驱动,只要是上 RTOS 或定时器调度,一律先用状态机把流程搭出来,再往里塞具体实现。
6.2 与标准温湿度计做对比:误差怎么算才可信
代码写完别急着上线,先做 24 小时对比验证。把 SHT3x 和标准温湿度计放在同一个空间,避免阳光直射和气流干扰,每小时记录一组数据。采集时不要只用瞬时值,取 10 分钟均值更合理,因为传感器和标准表的响应速度不同。
计算平均偏差和标准差能帮你判断数据是否可信:温度平均偏差在 ±0.5℃以内、湿度在 ±3%RH 以内基本没问题;如果标准差偏大,先排查硬件布局和滤波窗口。比如湿度标准差大于 3%RH,多半是探头局部出现了微小气流,而不是传感器本身坏掉。
对比验证的另一个作用是确认代码里的换算单位没有用错。我曾经因为换算了两次,温度读数整体高了 1.5℃,单看数据很“平稳”,但一对比标准表就露馅了。现在我的习惯是,任何新的 SHT3x 驱动都会先写一个五分钟连续采样统计,把最小值、最大值、平均值打印出来,确认没有毛刺再交给业务层。这个习惯帮我挡掉了不少后期才暴露的问题,希望帮到你。
本文还有配套的精品资源,点击获取