1. 项目背景与核心需求
在嵌入式开发领域,环境参数监测是最基础也最经典的应用场景之一。STM32F103C8T6作为性价比极高的ARM Cortex-M3内核微控制器,搭配SHT20这类高精度数字温湿度传感器,可以构建出稳定可靠的监测系统。这个组合特别适合需要低成本、低功耗但要求一定精度的应用场景,比如智能家居环境监测、农业大棚监控、仓储环境记录等。
我最近在一个智能花盆项目中实际应用了这个方案,发现虽然整体架构简单,但从硬件连接到软件实现仍有不少值得注意的细节。特别是SHT20作为I2C接口器件,与STM32的通信过程中有几个关键时序需要特别注意,否则很容易出现读取失败或数据异常的情况。下面我就把整个实现过程拆解开来,包括硬件连接、软件配置、通信协议解析等关键环节,并分享几个我在调试过程中总结的实用技巧。
2. 硬件设计与连接要点
2.1 元器件选型考量
STM32F103C8T6选择"蓝 pill"开发板是最经济实惠的方案,这块板子虽然价格低廉(通常20元以内),但完全具备驱动SHT20所需的所有外设资源。SHT20是Sensirion推出的第二代数字温湿度传感器,相比前代SHT1x系列,它的精度更高(湿度±3%RH,温度±0.3℃),且采用DFN封装体积更小。市面上常见的有带防护膜的工业级版本和不带防护膜的普通版本,对于一般室内应用选择普通版即可,能节省约30%成本。
注意:购买SHT20时务必确认是原装正品,市面上有不少仿制品虽然价格便宜但精度和稳定性差很多。正品SHT20的背面激光刻字清晰,且测量响应速度明显更快。
2.2 电路连接示意图
SHT20与STM32F103C8T6的连接非常简单,只需要4根线:
code复制SHT20引脚 STM32F103C8T6引脚
VDD(1) 3.3V
GND(4) GND
SCL(3) PB6(I2C1_SCL)
SDA(2) PB7(I2C1_SDA)
实际接线时有几个关键点需要注意:
- SHT20的工作电压范围是2.1V-3.6V,必须接3.3V而非5V
- 虽然SHT20内部已有上拉电阻(约10kΩ),但为了通信稳定建议在SCL和SDA线上各加一个4.7kΩ外部上拉电阻到3.3V
- 如果传输距离超过20cm,建议使用双绞线并考虑降低I2C通信速率
2.3 电源滤波设计
SHT20对电源噪声比较敏感,在要求较高的应用中,建议在VDD和GND之间加一个0.1μF的陶瓷电容进行滤波。如果环境中有较强电磁干扰,还可以再并联一个10μF的钽电容。我在实际测试中发现,良好的电源滤波可以使湿度测量结果的波动范围从±2%RH降低到±0.5%RH。
3. 软件环境配置
3.1 开发工具准备
推荐使用STM32CubeIDE作为开发环境,它集成了STM32CubeMX配置工具和Eclipse IDE,可以一站式完成从硬件配置到代码编写的全部工作。安装完成后需要做以下准备:
- 安装STM32F1系列的HAL库支持包
- 为项目选择正确的芯片型号:STM32F103C8T6
- 配置调试工具(通常用ST-Link V2)
- 设置正确的时钟源(外部8MHz晶振)
3.2 I2C外设配置
在CubeMX中配置I2C1的步骤如下:
- 在Pinout视图中激活I2C1模式
- 确认SCL(PB6)和SDA(PB7)引脚已正确映射
- 在Configuration选项卡中设置I2C参数:
- Timing参数:选择Standard Mode(100kHz)
- 地址模式:7-bit
- 器件地址:0x40(SHT20的默认地址)
- 生成代码时勾选"Generate peripheral initialization as a pair of .c/.h files"
技巧:如果发现I2C通信不稳定,可以尝试将时钟速度降低到50kHz。虽然速度变慢但稳定性会显著提高,特别是在长导线连接时。
3.3 必要的驱动代码
除了HAL库自动生成的代码外,还需要手动添加几个关键函数:
c复制// 重定义I2C超时时间
#define I2C_TIMEOUT 100
// I2C写函数封装
HAL_StatusTypeDef SHT20_Write(uint8_t cmd) {
return HAL_I2C_Master_Transmit(&hi2c1, SHT20_ADDR<<1, &cmd, 1, I2C_TIMEOUT);
}
// I2C读函数封装
HAL_StatusTypeDef SHT20_Read(uint8_t* buf, uint8_t len) {
return HAL_I2C_Master_Receive(&hi2c1, SHT20_ADDR<<1, buf, len, I2C_TIMEOUT);
}
4. SHT20通信协议实现
4.1 基本命令集
SHT20支持的命令非常简单,主要包含以下几个:
| 命令代码 | 功能描述 | 备注 |
|---|---|---|
| 0xF3 | 触发温度测量 | 无时钟拉伸 |
| 0xF5 | 触发湿度测量 | 无时钟拉伸 |
| 0xFE | 软复位 | 需要等待15ms后才能操作 |
| 0xE6 | 写用户寄存器 | 用于配置分辨率等参数 |
| 0xE7 | 读用户寄存器 | 返回1字节状态信息 |
4.2 温湿度读取流程
完整的温度读取流程如下:
- 发送触发测量命令(0xF3)
- 等待测量完成(典型时间85ms)
- 读取3字节数据(MSB+LSB+CRC)
- 校验CRC(可选但建议)
- 将原始数据转换为实际温度值
湿度读取流程类似,只是命令改为0xF5,等待时间稍长(典型29ms)。
4.3 数据转换算法
SHT20返回的是14位原始数据(高2位固定为状态位),转换公式如下:
温度转换:
c复制float rawToTemperature(uint16_t rawValue) {
return -46.85 + 175.72 * (rawValue / 65536.0);
}
湿度转换:
c复制float rawToHumidity(uint16_t rawValue) {
return -6.0 + 125.0 * (rawValue / 65536.0);
}
在实际应用中,我建议对这些公式做两点优化:
- 将65536.0预先计算为常量避免重复浮点运算
- 添加范围检查,防止异常数据导致输出不合理值
5. 完整代码实现
5.1 头文件定义
c复制// sht20.h
#ifndef SHT20_H
#define SHT20_H
#include "stm32f1xx_hal.h"
#define SHT20_ADDR 0x40
#define I2C_TIMEOUT 100
typedef enum {
SHT20_RES_12_14BIT = 0x00,
SHT20_RES_8_12BIT = 0x01,
SHT20_RES_10_13BIT = 0x80,
SHT20_RES_11_11BIT = 0x81
} SHT20_Resolution;
HAL_StatusTypeDef SHT20_Init(I2C_HandleTypeDef *hi2c);
HAL_StatusTypeDef SHT20_Reset(void);
HAL_StatusTypeDef SHT20_SetResolution(SHT20_Resolution res);
float SHT20_ReadTemperature(void);
float SHT20_ReadHumidity(void);
#endif
5.2 源文件实现
c复制// sht20.c
#include "sht20.h"
#include <math.h>
static I2C_HandleTypeDef *hi2c_sht20;
HAL_StatusTypeDef SHT20_Init(I2C_HandleTypeDef *hi2c) {
hi2c_sht20 = hi2c;
return SHT20_Reset();
}
HAL_StatusTypeDef SHT20_Reset(void) {
uint8_t cmd = 0xFE;
HAL_StatusTypeDef ret = HAL_I2C_Master_Transmit(hi2c_sht20, SHT20_ADDR<<1, &cmd, 1, I2C_TIMEOUT);
HAL_Delay(15); // 复位后需要等待15ms
return ret;
}
float SHT20_ReadTemperature(void) {
uint8_t cmd = 0xF3;
uint8_t data[3];
if(HAL_I2C_Master_Transmit(hi2c_sht20, SHT20_ADDR<<1, &cmd, 1, I2C_TIMEOUT) != HAL_OK)
return NAN;
HAL_Delay(85); // 等待测量完成
if(HAL_I2C_Master_Receive(hi2c_sht20, SHT20_ADDR<<1, data, 3, I2C_TIMEOUT) != HAL_OK)
return NAN;
uint16_t raw = (data[0] << 8) | data[1];
raw &= 0xFFFC; // 清除状态位
return -46.85f + 175.72f * (raw / 65536.0f);
}
// 类似地实现湿度读取函数...
6. 实际应用中的优化技巧
6.1 降低功耗的策略
SHT20在非测量状态下功耗极低(约0.3μA),但在实际应用中还可以进一步优化:
- 采用间歇工作模式:每分钟唤醒一次进行测量,其余时间保持休眠
- 降低测量分辨率:对于不需要高精度的场景,使用较低的12位湿度/14位温度分辨率
- 关闭内部加热器:除非在冷凝环境���,否则保持加热器关闭
实现代码示例:
c复制void SHT20_LowPowerMode(void) {
// 设置低分辨率
SHT20_SetResolution(SHT20_RES_8_12BIT);
// 关闭加热器
uint8_t cmd[2] = {0xE6, 0x00};
HAL_I2C_Master_Transmit(hi2c_sht20, SHT20_ADDR<<1, cmd, 2, I2C_TIMEOUT);
}
6.2 数据滤波处理
传感器数据常有小幅波动,可以采用以下滤波算法:
- 移动平均滤波:保留最近N次测量值的平均值
- 中值滤波:取最近几次测量的中间值
- 一阶滞后滤波:新值 = α×当前值 + (1-α)×旧值
我比较推荐一阶滞后滤波,实现简单且效果不错:
c复制float temp_filtered = 0.0f;
const float alpha = 0.2f; // 滤波系数
void updateTemperature(void) {
float new_temp = SHT20_ReadTemperature();
if(!isnan(new_temp)) {
temp_filtered = alpha * new_temp + (1-alpha) * temp_filtered;
}
}
6.3 异常数据处理
在实际部署中,传感器可能会偶尔返回异常值,需要添加合理性检查:
c复制#define TEMP_MIN -40.0f
#define TEMP_MAX 125.0f
#define HUMID_MIN 0.0f
#define HUMID_MAX 100.0f
int validateData(float temp, float humid) {
if(temp < TEMP_MIN || temp > TEMP_MAX) return 0;
if(humid < HUMID_MIN || humid > HUMID_MAX) return 0;
// 温湿度相关性检查(可选)
if(temp > 25.0f && humid > 80.0f) return 0; // 高温高湿组合可疑
return 1;
}
7. 常见问题与解决方案
7.1 I2C通信失败排查
如果遇到I2C通信问题,可以按照以下步骤排查:
-
检查硬件连接
- 确认电源电压稳定在3.3V
- 检查SCL/SDA线是否接反
- 测量上拉电阻是否正常
-
软件调试
- 使用逻辑分析仪抓取I2C波形
- 降低I2C时钟频率测试
- 检查HAL库版本是否兼容
-
典型错误代码处理
- HAL_ERROR:检查器件地址是否正确(应该是0x40<<1)
- HAL_TIMEOUT:增加I2C_TIMEOUT值或检查硬件连接
- HAL_BUSY:确保没有其他进程占用I2C总线
7.2 数据异常的可能原因
当测量值明显异常时,可能的原因包括:
- 电源噪声过大:添加滤波电容
- 传感器结露:启用内部加热器(0xE6寄存器的bit2)
- 长期漂移:传感器需要重新校准(放置到已知环境中对比)
- 焊接高温损伤:更换传感器并注意焊接温度(建议<260℃)
7.3 提高测量精度的技巧
- 避免热源干扰:传感器不要靠近MCU或其他发热元件
- 保持空气流通:避免将传感器密封在狭小空间
- 定期校准:每3-6个月与标准仪器对比一次
- 温度补偿:湿度测量值需要根据温度进行补偿(SHT20已内置)
8. 项目扩展思路
这个基础方案可以进一步扩展为更完整的系统:
- 添加无线传输功能:通过ESP8266或nRF24L01将数据上传到服务器
- 实现本地显示:连接OLED屏幕实时显示温湿度曲线
- 增加报警功能:当温湿度超出设定范围时触发蜂鸣器或LED
- 数据记录:添加SD卡模块存储历史数据
- 低功耗优化:配合STM32的停止模式实现电池供电
一个典型的扩展代码框架:
c复制void app_main(void) {
SHT20_Init(&hi2c1);
while(1) {
float temp = SHT20_ReadTemperature();
float humid = SHT20_ReadHumidity();
if(validateData(temp, humid)) {
display_update(temp, humid);
wireless_send(temp, humid);
check_alarm(temp, humid);
}
HAL_Delay(60000); // 每分钟测量一次
enter_low_power_mode();
}
}
在实际项目中,我发现这个方案最关键的还是保证基础测量的稳定性。特别是在工业环境中,电磁干扰较强时,需要特别注意I2C信号的完整性。建议在正式部署前,先进行至少24小时的连续测试,记录数据曲线检查是否有异常波动。
