STM32软件模拟I2C驱动SHT30温湿度传感器:CRC校验与实战解析
简介STM32SHT30温湿度计驱动工程是一套围绕SHT30传感器与STM32单片机的完整可运行资源面向嵌入式入门及环境监测开发人员解决I2C通信配置、数据采集与显示的实现问题。工程基于STM32CubeMX生成覆盖单次与周期两种采样模式SHT30温度精度达0.1℃、湿度精度1.5%详细给出采样触发、数据读取、CRC校验以及温湿度转换公式并集成1.54寸TFT屏显示代码可在主程序中自由切换工作模式。资源包为7z压缩格式共156个文件以h头文件、c源文件及o目标文件为主同时包含uvprojx工程文件、hex烧录文件、ioc配置和map/axf等编译产物其中c/h文件涵盖I2C、SPI、RCC、Flash等外设驱动整体大小约488KB结构清晰便于对照学习。已有115人学习下载适合需要快速搭建SHT30应用并理解驱动细节的开发者参考也可作为课程设计或毕业设计的基础模板。1. 项目定位与驱动思路1.1 驱动工程要解决的核心问题搞嵌入式做了这么久温湿度采集算是外设驱动里最常碰到的需求之一。很多人初期图省事直接接个DHT11能用是能用但精度和一致性真的让人头疼尤其是湿度在60%以上的区间DHT11的误差有时候能到±5%给上位机或者LCD显示的时候数据跳得你怀疑人生。SHT30这颗传感器我前后在三个项目里用过第一次是在一个冷链运输记录仪上第二次是室内空气质量监测节点第三次就是最近做的这个多通道环境采集板。整体下来的体验是测量精度够稳、I2C通信简单、尺寸小、功耗低而且驱动代码写好了可以很干净地复用到不同项目里。这颗传感器能在保持±2%RH和±0.3℃精度的前提下把价格控制在十元以内对于中小型项目来说是性价比极高的选择。这篇博文就把我调通SHT30的完整过程、驱动代码框架、以及踩过的坑整理出来。内容偏实战面向的是已经在用STM32标准库或HAL库做开发、想快速接入SHT30的工程师也适合刚学完GPIO和I2C通信的初学者照着搭。1.2 I2C总线与SHT30通信协议要点SHT30的数据接口是标准的I2C工作在100kHz或400kHz下都没问题。总线上可以并联多个I2C设备所以如果你的板上同时还挂了OLED、BMP280、AT24C02之类的器件共用一个I2C总线完全OK。SHT30有两根地址线引脚ADDR注意这里是一个引脚不是两个。ADDR拉低时7位地址是0x44拉高时是0x45。也就是说你在同一条I2C总线上最多只能挂两颗SHT30。如果项目有三路以上的温湿度检测需求要么换别的传感器要么用I2C软件模拟的方式多开几路总线。我在冷链记录仪项目里就是同时用了0x44和0x45两颗分别采集箱内和外壁的温湿度实现起来很简单。SHT30的写操作流程是主机发送起始信号→发送从机地址写位→发送16位命令→停止。读操作则是起始→发送从机地址读位→读取6个字节数据→停止。这里的6个字节是有讲究的前两个是温度原始值第三个是温度数据的CRC校验值接下来两个是湿度原始值最后一个又是CRC校验值。这个数据格式设计得很用心每个物理量的数据都紧跟一个独立的校验字节驱动里就可以逐段校验。1.3 为什么推荐软件I2C而不是硬件I2C这个问题在STM32圈子里争论了很久。我在F103上用过硬件I2C标准库的I2C事件标志EV5、EV7在初始化或收发过程中经常莫名其妙卡死尤其当处理函数里同时有中断优先级冲突的时候。排查一个硬件I2C死锁花的时间足够把软件I2C写完并调试通过了。因此在大多数实际项目中我建议直接用GPIO模拟I2C。软件I2C的优点有两个第一是时序完全可控不会因为MCU主频和外设时钟的差异出现兼容性问题第二是GPIO选择自由可以用任意两个引脚PCB布线时不用特意绕到I2C外设固定的引脚上去。性能方面100kHz的软件时序在STM32的72MHz主频下轻松实现传感器的数据量本身很小每次测量只传6个字节性能完全不是瓶颈。2. 驱动代码框架与核心实现2.1 工程分层与文件规划驱动写得好不好看文件结构就能判断个大概。我习惯把SHT30的驱动分为两层底层是I2C读写接口上层是传感器业务逻辑。Project/ ├── Hardware/ │ ├── i2c_soft.c / i2c_soft.h // 软件I2C底层驱动 │ └── sht30.c / sht30.h // SHT30传感器驱动 ├── App/ │ └── main.c // 应用逻辑调度测量和显示 └── ...这个分层的核心思想是SHT30驱动层不关心I2C具体是怎么实现的只需要调用三个接口函数——i2c_start()、i2c_write_byte()、i2c_read_byte()。这样当你换一块板子或者MCU平台的时候只需要重写底层那三个函数即可传感器相关的命令、数据解析、CRC校验逻辑完全不用动。2.2 初始化与复位时序SHT30上电后默认进入空闲状态不需要复杂的初始化流程发送一个软复位命令可以确保传感器处于已知的默认状态。/* sht30.c */ #define SHT30_ADDR 0x44 /* ADDR引脚接GND */ #define SHT30_CMD_SOFTRESET 0x30A2 #define SHT30_CMD_MEAS_HIGH 0x2C06 /* 单次测量高重复性时钟拉伸模式 */ void sht30_soft_reset(void) { i2c_start(); i2c_write_byte(SHT30_ADDR 1); i2c_write_byte(SHT30_CMD_SOFTRESET 8); i2c_write_byte(SHT30_CMD_SOFTRESET 0xFF); i2c_stop(); delay_ms(10); }软复位后我一般延时10毫秒再操作。SHT30内部完成复位只需要几百微秒但预留一些余量可以避免在总线繁忙时传感器还没准备好的情况。2.3 单次测量流程与数据解析SHT30支持单次测量和周期测量两种模式。周期测量可以省去主机反复发命令的麻烦但大多数MCU应用有自己固定的任务调度周期单次测量更贴合实际工程。每次需要数据时发一条测量命令传感器完成测量后主机再读取结果。uint8_t sht30_read_temp_humi(float *temp, float *humi) { uint8_t buf[6]; uint16_t temp_raw, humi_raw; /* 发送测量命令 */ i2c_start(); i2c_write_byte(SHT30_ADDR 1); i2c_write_byte(SHT30_CMD_MEAS_HIGH 8); i2c_write_byte(SHT30_CMD_MEAS_HIGH 0xFF); i2c_stop(); /* 高重复性测量最大耗时15.5ms这里延时30ms留足裕量 */ delay_ms(30); /* 读取6字节数据 */ i2c_start(); i2c_write_byte((SHT30_ADDR 1) | 0x01); buf[0] i2c_read_byte(1); buf[1] i2c_read_byte(1); buf[2] i2c_read_byte(1); buf[3] i2c_read_byte(1); buf[4] i2c_read_byte(1); buf[5] i2c_read_byte(0); i2c_stop(); /* 校验温度和湿度的CRC */ if (sht30_crc8(buf, 2) ! buf[2]) return 1; if (sht30_crc8(buf 3, 2) ! buf[5]) return 2; temp_raw (buf[0] 8) | buf[1]; humi_raw (buf[3] 8) | buf[4]; *temp -45.0f 175.0f * (float)temp_raw / 65535.0f; *humi 100.0f * (float)humi_raw / 65535.0f; return 0; }这里读字节函数的参数表示是否发送ACK。最后一字节不需要应答直接发NACK然后停止这是I2C协议的基本要求漏掉会导致传感器在下次通信时仍然在线发送数据。2.4 CRC校验传感器数据可靠性的最后一关SHT30的CRC采用多项式x8x5x41即0x31初值为0xFF。这其实就是常见的CRC-8/MAXIM算法实现不算复杂。uint8_t sht30_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 j 0; j 8; j) { if (crc 0x80) crc (crc 1) ^ 0x31; else crc 1; } } return crc; }有经验的工程师可能觉得每次读数据都做CRC校验是多余的但在工业现场或通信线比较长的环境里这条校验真的能帮你挡掉大量噪声数据。我之前在一台设备上测试电机启动瞬间的电磁干扰会让I2C线上的数据偶尔冒出一个错误字节不校验的话温湿度偶发跳变非常难查加了CRC之后驱动层直接丢弃错误帧问题就消失了。3. 关键环节的细节打磨3.1 软件I2C时序参数怎么调软件I2C的逻辑非常简单SCL和SDA两个GPIO按照时序拉高拉低。但实际调试时容易出问题的是延时时间。我见过有人直接用delay_us(1)I2C频率跑到了几百kHzSHT30不认账返回的数据全是0xFF。SHT30数据手册要求的I2C时序中SCL低电平和高电平的最小时间都是0.5微秒标准模式。考虑GPIO翻转本身的耗时我建议把半周期延时设为5微秒也就是SCL频率约100kHz。这样既满足时序要求又对EMC干扰有更好的容忍度。SCL高电平期间SDA数据必须保持稳定所以每次SCL拉高之前就要把SDA设置好SCL拉高之后不要动SDA等SCL拉低之后再改变SDA。这个顺序一旦搞反SHT30就会把SDA的变化误判为起始或停止信号通信直接失败。3.2 温湿度换算公式的推导与验证SHT30的原始数据是一个16位无符号整数范围从0到65535。温度换算公式是线性映射温度范围-40℃~125℃实际工作范围是-40℃~85℃温度值 -45 175 × (原始值 / 65535)湿度范围0%RH~100%RH湿度值 100 × (原始值 / 65535)为什么要乘以175然后减45因为SHT30的温度传感器输出的是一个与绝对温度成正比的电压信号ADC转换后得到的原始值和温度呈线性关系数据手册给出的线性映射就是-45℃对应原始值0130℃对应原始值65535。实际测试时可以拿一个已知精度的水银温度计放在一起对比比如室温25℃时读取到的原始值应该在21925左右。如果你用单片机做浮点运算觉得慢也可以把温湿度放大10倍用整数存储比如25.3℃存成253交给上层显示时再处理小数点。这个方法在低主频MCU上很有效。3.3 数据滤波与异常值处理传感器本身精度没问题但环境里总有干扰。我实际测试中发现把传感器放在空调出风口附近时读数会以秒为周期波动3~5%RH。这不代表传感器坏了而是空气流动造成的真实变化。如果是做精密测量仪器建议在驱动上层加一个滑动平均滤波#define FILTER_NUM 5 static uint16_t temp_buf[FILTER_NUM]; static uint16_t humi_buf[FILTER_NUM]; static uint8_t buf_index; void filter_update(uint16_t temp_raw, uint16_t humi_raw) { temp_buf[buf_index] temp_raw; humi_buf[buf_index] humi_raw; buf_index (buf_index 1) % FILTER_NUM; } uint16_t filter_get_temp(void) { uint32_t sum 0; for (uint8_t i 0; i FILTER_NUM; i) sum temp_buf[i]; return (uint16_t)(sum / FILTER_NUM); }如果数据波动特别大大概率不是滤波的问题而是传感器引脚接触不良、线缆过长或者电源纹波太大。稳定的电源对传感器数据的准确性非常重要SHT30的供电电压工作范围是2.4V到5.5V但电源纹波不要超过100mV否则ADC的参考电压会被拉偏数据自然不准。所以给传感器供电的位置并一颗100nF的去耦电容是必须的如果板子上有空间再并一颗10μF的钽电容效果更好。4. 常见问题排查与避坑记录4.1 传感器无响应ACK异常这是我遇到最多的问题。SHT30无响应主要从这几点排查排查点检查方法解决办法地址是否正确确认ADDR引脚电平用逻辑分析仪抓从机地址0x44或0x45两者不要搞混上拉电阻用万用表量SCL/SDA对地电压高电平应接近VCC加4.7kΩ上拉电阻到VCC接线顺序确认SCL和SDA没有接反对照原理图核对供电是否稳定万用表量VCC引脚确认在2.4V以上检查电源电路焊接质量用放大镜观察DFN封装引脚有没有虚焊补焊或重新贴片DFN-8封装是SHT30最常用的封装引脚间距很小手工焊接时容易连锡或虚焊。我的建议是焊完之后用万用表二极管档量一下VCC到GND的压降正常的PN结压降大约0.4V~0.6V如果接近0说明焊连了如果开路说明没焊上。4.2 数据读回来全是0xFF这个现象说明主机和从机之间的通信没有真正建立起来。常见原因之一是SDA引脚在读取时没有切到输入模式。软件模拟I2C时SDA这根线既要做输出又做输入如果一直保持推挽输出从机的微弱拉低信号根本拉不动主机的强驱动电平读到的一直是高电平0xFF。解决方法是读SDA之前把GPIO配置改为输入模式读完之后再切回输出模式。如果用开漏输出加上拉电阻的方式就没有这个问题但开漏输出需要确保外部上拉电阻足够小否则在400kHz高速模式下波形会变形。我测过4.7kΩ上拉在100kHz下能用但在400kHz下波形上升沿会明显变缓传感器偶发读取失败。高速率下建议换成2.2kΩ甚至1kΩ上拉。4.3 湿度数据跳变或一直偏高湿度数据跳变首先要确认不是环境本身的问题。手靠近传感器、对着传感器吹气、传感器旁边有发热元件都会引起湿度快速变化。排除这些因素后再怀疑传感器被测元件被污染。SHT30传感器的感湿部位是封装表面的一个小孔如果在焊接、涂覆三防漆的过程中有助焊剂或其他污染物进入了这个小孔湿度读数会明显偏高且响应变慢。板子做三防涂覆时一定要用遮蔽胶带把传感器的小孔贴起来再喷涂这个细节我吃过亏。如果湿度读数偏高了10%RH以上可以尝试把板子放在60℃左右的烘箱里烘2小时让污染物挥发很多情况下能恢复。但如果传感器已经被助焊剂渗透了基本只能换新。4.4 硬件I2C卡死的应对方案如果你确实想用硬件I2C也完全可以在HAL库下跑通STM32F4系列。但F1标准库的硬件I2C容易出问题建议的方案是改用HAL库HAL的I2C状态机比标准库健壮很多在I2C中断回调里不要写耗时操作比如别在中断里做CRC计算、浮点转换、串口打印如果I2C卡死可以用定时器做一个看门狗每次I2C通信前判断上次通信是否超时超时则重新初始化I2C外设并软复位传感器我在F407上跑HAL库I2C连续运行一个月没出过一次通信异常所以硬件I2C不是不能用关键还是看库的成熟度和写法。5. 工程扩展与应用优化5.1 对接OLED显示与串口输出温湿度计项目最经典的展示方式就是一块OLED屏加一个串口输出。OLED用SSD1306驱动芯片同样是I2C接口地址是0x3C可以跟SHT30挂在同一条I2C总线上。工程里有一个注意点SHT30的测量命令延时30msSSD1306初始化时也有几百毫秒的启动时间。第一次执行显示和测量任务时建议先延时等待所有I2C设备稳定后再开始通信。串口输出方面重定向printf到USART1每隔1秒打印一次温湿度数据printf(Temp: %.2f C, Humi: %.2f %%RH\r\n, temperature, humidity);我建议输出格式统一为两位小数字符串解析时也按固定格式来这样上位机或者串口助手可以稳定解析。5.2 多传感器并联与地址扩展SHT30一颗传感器只能测一个点的温湿度。如果你需要同时监测多个位置的温湿度有两种思路第一种是使用不同ADDR地址挂两颗SHT300x440x45。第二种是使用多个软件I2C总线每组总线只挂一颗传感器。我在一个恒温箱项目里用了四颗SHT30就是用的第二种方案——F103有足够的GPIO软件I2C又是任意引脚可配四路总线同时采集互不干扰。具体实现时底层I2C函数需要支持总线号参数SHT30驱动里用结构体维护每颗传感器的总线号和地址typedef struct { uint8_t bus_id; uint8_t addr; float temperature; float humidity; } sht30_device_t;将I2C相关的函数入口增加总线号参数驱动就完成了多实例化改造。5.3 低功耗设计思路如果是做电池供电的便携设备低功耗是必须考虑的问题。SHT30的休眠电流只有微安级别真正消耗功耗的是测量和通信过程。设计思路是默认让SHT30保持在空闲状态不周期测量定时器每隔10秒唤醒MCUMCU向SHT30发送一次单次测量命令等待30ms测量完成读取数据立即进入低功耗模式一次完整测量加通信的时间大约50ms电流平均3~5mA测量时传感器功耗约1.5mA折算下来平均电流不到0.1mA两节AA电池供电维持几个月没有问题。功耗优化里有个容易被忽视的点I2C总线上的上拉电阻在传感器空闲时也在持续消耗电流4.7kΩ上拉在3.3V下漏电约0.7mA。低功耗电路里可以把上拉电阻接到MCU的GPIO平时输出低电平关闭上拉测量前再拉高。6. 项目复盘与个人心得SHT30驱动工程做完之后我最大的感受是这种简单的I2C传感器驱动真正考验人的不是怎么写代码而是怎么把时序、校验、错误处理这些边界问题都想清楚。代码总共不到两百行但每个细节都可能成为现场问题。我最开始做这个驱动时直接照搬了网上的代码结果在STM32F103上跑出了数据全是0xFF的问题。后来仔细排查才发现是SDA在读取时没有切换到输入模式。这个教训让我养成了一个习惯系统外设驱动必须把底层的时序逻辑弄明白而不是只关注上层API叫什么名字。另一个值得推荐的实践是在SHT30驱动里把错误码设计得细一点。我的驱动就返回0成功、1温度CRC错误、2湿度CRC错误、3传感器无响应四种结果。上层收到非零返回值时可以打印错误码定位问题快得多。如果你准备在自己项目里使用SHT30建议先用手头的开发板把驱动调通再决定是否往正式板上移植。用软件I2C做一遍配合逻辑分析仪把时序看清楚后面不管换什么MCU平台都难不倒你。本文还有配套的精品资源点击获取