STM32F103实战:IIC协议驱动SHT31实现高精度环境监测
1. 环境监测项目实战:为什么选择STM32F103+SHT31组合
在智能家居、农业大棚或是工业现场监控场景中,环境温湿度数据采集是最基础却至关重要的需求。我经手过十几个类似项目,发现STM32F103和SHT31这对组合堪称性价比之王。前者是意法半导体经典的Cortex-M3内核MCU,72MHz主频配合丰富的外设接口,后者则是瑞士Sensirion出品的数字温湿度传感器,精度达到±2%RH和±0.3℃。
实际部署时有个细节要注意:SHT31的供电电压范围是2.4V-5.5V,而STM32F103的IO口耐压是3.3V。我曾在某个项目里偷懒直接接了5V电源,结果传感器数据出现漂移,后来改用3.3V供电就稳定了。这个坑提醒我们:硬件设计阶段一定要核对电压匹配性,别看是小细节,关键时刻能省去大量调试时间。
2. 硬件连接:IIC接口的物理层实现
2.1 引脚定义与接线方案
SHT31采用标准的IIC接口,只需要四根线就能完成通信。以正点原子Mini开发板为例,具体接线如下:
- VCC→ 3.3V(切记不要接5V)
- GND→ 共地连接
- SDA→ PC11(可配置为开漏输出)
- SCL→ PC12(需配置为推挽输出)
这里有个实战技巧:如果布线距离超过20cm,建议在SDA和SCL线上各加一个2.2kΩ上拉电阻。我曾在工厂环境监测项目中遇到过IIC通信失败的问题,后来发现是30cm长的杜邦线导致信号衰减,加上拉电阻后通信立即稳定。
2.2 硬件IIC vs 模拟IIC的选择
STM32F103自带硬件IIC控制器(I2C1和I2C2),但很多开发者更喜欢用GPIO模拟时序,原因有三:
- 硬件IIC的中断机制较复杂,新手容易卡在状态判断上
- 模拟IIC时序更直观,方便调试时用逻辑分析仪抓波形
- 某些国产兼容芯片的硬件IIC存在兼容性问题
不过硬件IIC也有优势:CPU占用率低,适合需要频繁读取数据的场景。我的经验是:低频采样(<1Hz)用模拟IIC,高频采样用硬件IIC。下面给出两种方式的初始化代码片段对比:
// 硬件IIC初始化(使用I2C1) void I2C1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // PB6-SCL, PB7-SDA GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStructure.I2C_OwnAddress1 = 0x00; I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed = 100000; // 100kHz I2C_Init(I2C1, &I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); }3. SHT31驱动开发关键点解析
3.1 传感器初始化与软复位
SHT31上电后需要15ms的启动时间(datasheet第8页明确标注),很多开发者容易忽略这点。我建议在初始化函数里添加延时,并执行软复位命令确保传感器处于已知状态:
void SHT31_Init(void) { delay_init(); // 确保延时函数已初始化 IIC_Init(); // IIC接口初始化 delay_ms(20); // 预留上电稳定时间 // 发送软复位命令(0x30A2) IIC_Start(); SHT31_Send_Byte(0x44<<1 | write); IIC_Wait_Ack(); SHT31_Send_Byte(0x30); IIC_Wait_Ack(); SHT31_Send_Byte(0xA2); IIC_Wait_Ack(); IIC_Stop(); delay_ms(10); // 等待复位完成 }3.2 高精度模式下的数据读取
SHT31提供三种测量模式,对应不同的采集时间和精度。经过实测对比,我推荐使用**时钟拉伸(clock stretching)**的中等精度模式(0x2C06),这个模式下单次测量仅需4ms,温湿度精度完全满足大多数场景:
| 命令码 | 模式 | 测量时间 | 温度精度 | 湿度精度 |
|---|---|---|---|---|
| 0x2400 | 单次 | 2.8ms | ±0.3℃ | ±2%RH |
| 0x2C06 | 时钟拉伸 | 4ms | ±0.2℃ | ±1.5%RH |
| 0x2C0D | 高精度 | 15ms | ±0.1℃ | ±1%RH |
数据读取时要注意CRC校验。虽然示例代码里为了简化跳过了校验,但在工业环境中强烈建议实现校验功能。校验算法在datasheet第15页有详细说明,这里给出关键代码:
uint8_t Check_CRC8(uint16_t data) { uint8_t crc = 0xFF; uint8_t byte = data >> 8; for(uint8_t bit=0; bit<8; bit++) { if((crc ^ byte) & 0x80) { crc = (crc << 1) ^ 0x31; } else { crc <<= 1; } byte <<= 1; } byte = data & 0xFF; // 重复上述过程处理低字节... return crc; }4. 数据校准与滤波处理
4.1 原始数据转换公式
SHT31输出的原始数据是16位无符号整数,需要按以下公式转换:
- 温度:T = -45 + 175 × (ST / 65535)
- 湿度:RH = 100 × (SRH / 65535)
在代码实现时要注意浮点运算效率问题。STM32F103没有硬件浮点单元,频繁的浮点计算会导致性能下降。我的优化方案是:预先计算比例因子,用定点数运算替代浮点:
// 优化后的温度计算(使用32位整数运算) int32_t Calculate_Temperature(uint16_t raw) { int32_t temp = raw * 1750; // 175×10 temp = temp / 65535; // 65535=2^16-1 temp = temp - 450; // -45×10 return temp; // 实际温度=返回值/10.0 }4.2 滑动平均滤波实战
工业现场常存在瞬时干扰,我推荐采用加权滑动平均滤波算法。相比简单平均,这种方法对新数据赋予更高权重,既平滑了波动又保持响应速度:
#define FILTER_LEN 5 float temp_history[FILTER_LEN] = {0}; uint8_t filter_index = 0; float Weighted_Filter(float new_val) { float weights[FILTER_LEN] = {0.1, 0.15, 0.2, 0.25, 0.3}; // 权重系数 float sum = 0; // 更新历史数据 temp_history[filter_index] = new_val; filter_index = (filter_index + 1) % FILTER_LEN; // 计算加权平均 for(uint8_t i=0; i<FILTER_LEN; i++) { sum += temp_history[(filter_index + i) % FILTER_LEN] * weights[i]; } return sum; }5. 系统集成与数据输出
5.1 串口格式化输出优化
通过printf输出数据时,频繁的字符串处理会占用大量资源。我改进的方案是:直接操作USART数据寄存器,避免使用标准库的开销:
void USART_SendString(USART_TypeDef* USARTx, char* str) { while(*str) { while(!(USARTx->SR & USART_SR_TXE)); USARTx->DR = (*str & 0xFF); str++; } } // 在main.c中的使用示例 char output_buf[32]; sprintf(output_buf, "T=%.1fC,H=%.1f%%\r\n", temperature, humidity); USART_SendString(USART1, output_buf);5.2 低功耗设计技巧
对于电池供电的环境监测节点,我总结出三个省电要点:
- 将SHT31设置为单次测量模式(0x2400),采集后立即进入休眠
- 在两次采集间隔里,让STM32进入STOP模式,通过RTC定时唤醒
- 关闭所有未使用的外设时钟
实测下来,采用这些措施后,系统平均电流从8mA降到了150μA,纽扣电池可以持续工作数月。关键配置代码如下:
void Enter_Stop_Mode(uint32_t seconds) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 配置RTC唤醒(需提前初始化RTC) RTC_SetWakeUpCounter(seconds * 1024); // LSE=32.768kHz RTC_WakeUpCmd(ENABLE); // 进入STOP模式 __WFI(); // 唤醒后重新配置系统时钟 SystemInit(); }6. 常见问题排查指南
6.1 IIC通信失败排查步骤
当SHT31无响应时,建议按以下流程排查:
- 用万用表测量VCC电压(确保在2.4-3.6V范围)
- 检查上拉电阻(通常4.7kΩ,长距离用2.2kΩ)
- 用逻辑分析仪抓取SCL/SDA波形(重点看起始信号和ACK)
- 尝试降低IIC时钟频率(可先设为10kHz测试)
最近帮客户调试时遇到个典型问题:SCL线上有毛刺导致通信失败。后来发现是PCB布局时IIC走线平行于电机控制线,重新布线后问题解决。这提醒我们:高频干扰环境要特别注意信号完整性。
6.2 数据异常处理方案
当出现以下情况时应启动异常处理流程:
- 温度超过-20~125℃合理范围
- 湿度超过0~100%RH范围
- 连续三次CRC校验失败
我的实现方案是引入传感器健康状态机,包含三个状态:
- NORMAL:正常采集模式
- WARNING:单次数据异常,触发重试
- FAULT:持续异常,上报错误并进入恢复流程
typedef enum { SHT31_NORMAL, SHT31_WARNING, SHT31_FAULT } SensorState; SensorState sht31_state = SHT31_NORMAL; void Handle_Sensor_Data(float temp, float humi) { static uint8_t error_count = 0; if(temp<-20 || temp>125 || humi<0 || humi>100) { if(sht31_state == SHT31_NORMAL) { sht31_state = SHT31_WARNING; error_count = 1; } else { error_count++; if(error_count >= 3) { sht31_state = SHT31_FAULT; Send_Alert("SHT31 Fault!"); } } } else { sht31_state = SHT31_NORMAL; error_count = 0; // 正常处理数据... } }7. 项目进阶:多节点组网方案
当需要监测大面积区域时,单个节点往往不够。我最近完成的智慧农业项目采用了STM32F103+ESP8266的组合方案:
- STM32负责传感器数据采集和本地控制
- ESP8266通过WiFi将数据上传至MQTT服务器
- 每个节点有唯一的设备ID,服务器可区分不同位置数据
硬件连接上,STM32的USART3(PB10/PB11)与ESP8266的UTXD/URXD相连,通信协议采用自定义的紧凑型二进制格式:
#pragma pack(push, 1) typedef struct { uint8_t dev_id; // 设备ID uint16_t seq_num; // 数据包序号 int16_t temperature;// 温度×10(避免浮点) uint16_t humidity; // 湿度×10 uint8_t crc8; // 校验和 } SensorPacket; #pragma pack(pop)这种方案在100亩的茶园部署中表现稳定,数据丢包率低于0.1%。关键点在于:STM32与ESP8266的波特率要严格匹配,建议先用示波器校准。
