简介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 模拟 I2C0x2C 组会把人卡在等待循环里出不来这时候必须换 0x24 组。高、中、低三档重复性对应不同的转换耗时高约 12ms 量级中约 5ms低在 2.5ms 左右。工程上我习惯在数据手册给出的最大值基础上再加 5ms 余量高重复性等 20ms中等 10ms低等 8ms宁多勿少。测量方式高重复性中重复性低重复性适用场景单次开启时钟延展0x2C 0x060x2C 0x0D0x2C 0x10硬件 I2C 支持延展时首选单次关闭时钟延展0x24 0x000x24 0x0B0x24 0x16GPIO 模拟 I2C 或对延展敏感周期1 mps 高重复性0x21 0x300x21 0x220x21 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 就把命令首字节改成0x204 mps 是0x2310 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 才真正从一个“能读数”的模块变成“能信”的传感器。希望帮到你。本文还有配套的精品资源点击获取