STM32F103驱动HTU31D温湿度传感器:I2C通信与驱动代码全解析
简介:这套源码包基于STM32F103C8T6实现HTU31D温湿度传感器的驱动与读取,面向嵌入式初学者、单片机爱好者及环境监测项目开发者,解决传感器通信与数据解析的入门问题。工程内置完整I²C主机读写驱动,覆盖起始信号、地址发送、数据读取与停止信号等时序,并针对HTU31D的命令序列进行寄存器配置;同时包含USART1串口输出模块,可直接将解码后的温湿度值打印至串口终端。包内共215个文件,以c源码与h头文件为主,辅以uvprojx工程文件、hex烧录固件、axf调试映像、map映射表及配置文档,压缩包大小5.96MB,目录涵盖工程源码、库文件与硬件资料,结构清晰便于定位。目前已有1164人学习浏览。通过分析该工程可掌握STM32F103的I²C与串口外设初始化流程、标准外设库调用方式,并参考其温湿度数据帧的拼接、校验与浮点转换逻辑,对开发类似环境监测节点具有直接借鉴价值。 这块板子是我给一个小型环境监测盒做的核心传感器方案,主控用了ST的STM32F103C8T6,传感器选了TE的HTU31D温湿度传感器。一开始项目用的是DHT11,精度和长期稳定性实在顶不住,温度偏差在夏天能到3度,湿度更是飘得离谱。换到HTU31D之后,I2C接口直接挂到F103的I2C1上,驱动代码两天就调通了,整套系统运行了半年多,数据一致性比我预期好很多。这篇就把整个驱动从零实现的过程、关键命令和踩过的坑都摊开来讲,适合正打算把DHT11升级成数字I2C温湿度传感器、或者第一次在STM32F103上挂HTU31D的朋友。这套方案能做的事情其实不少:室内环境监测、恒温恒湿柜、农业大棚、冷链运输记录仪,核心都是“F103 + I2C温湿度传感器”这个组合。你不需要很复杂的硬件知识,只要会点STM32基础,照着这篇文章就能把数据稳定读出来。
1. 选型背景:HTU31D强在哪,F103还够不够用
1.1 HTU31D的参数解读
HTU31D是TE Connectivity推出的数字温湿度传感器,DFN-8封装,尺寸非常小,I2C接口,7位从机地址默认0x40。拿到一颗这样的传感器,先别急着写代码,把手册里的关键参数圈出来:
- 湿度精度:±2%RH(典型值),这个精度在国产数字湿度传感器里算是第一梯队。
- 温度精度:±0.2°C(典型值),比DHT11的±2°C整整高出一个数量级。
- 分辨率:14位,湿度、温度都是14位,也就是说湿度从0到100%RH被切成16383个台阶。
- 温度量程:-40~125°C,湿度量程0~100%RH,覆盖日常环境监测绰绰有余。
- 供电范围:1.8~3.6V,工程上直接给3.3V最省事。
DHT11和HTU31D放在一起,差距非常直观:
| 参数 | DHT11 | HTU31D |
|---|---|---|
| 接口 | 单总线(时序敏感) | I2C(标准协议) |
| 湿度精度 | ±5%RH | ±2%RH |
| 温度精度 | ±2°C | ±0.2°C |
| 分辨率 | 1%RH / 1°C | 14位 |
| 采样 | 建议1Hz | 可更高,实际1Hz足够 |
| 校准 | 出厂粗校准 | 出厂校准,通常免标定 |
单总线这种协议最大的问题是时序窗口太窄,稍微有点中断延迟或者线长变化,读出来的数据就变成无效位。而I2C是标准同步协议,时钟由主机控制,时序裕量大得多,这也是我在项目里坚持换掉DHT11的根本原因。
1.2 F103为什么还值得选
很多新入行的朋友觉得STM32F103是“上古芯片”,但在这个场景下它反而非常合适。芯片价格便宜、库存量大,CubeMX图形化配置几分钟就能生成工程,标准外设库、HAL库、LL库的资料一抓一大把。驱动一个I2C温湿度传感器,F103的性能富余量非常大,而且待机功耗能做到很低,适合电池供电的物联网节点。
选型的核心原则是:够用、稳定、好买。项目不是越贵越好,而是把每一分成本花在该花的地方。传感器决定数据质量,MCU决定系统成本,F103+HTU31D这个组合在两者之间找到了一个很好的平衡点。如果你手头正好有F103最小系统板或者一块以F103为核心的开发板,拿来做这个驱动验证非常合适。
2. HTU31D的I2C通信机制:地址、命令与数据换算
2.1 从机地址与两条核心命令
I2C设备连接后第一步是确认地址。HTU31D的7位地址默认是0x40。在STM32 HAL库中,传参时要左移一位变成0x80(写地址)或0x81(读地址),因为HAL库的I2C地址参数是8位格式。如果你买的模块上有ADDR选择焊盘,注意可能被配置成其他地址,读不到的时候先量一下,别急着怀疑接线。
驱动HTU31D,最核心的命令其实就两条:
- 0x00:启动一次温湿度测量,温度、湿度同时采集。
- 0xFE:软复位,让传感器恢复到上电初始状态。
手册里可能还有用户寄存器配置、加热器控制这类功能,但日常环境监测用不到,先把这两条命令用透,后面按需扩充即可。量产的板子上我用0x00一直很稳定,你手里的芯片批次如果不同,以最新手册为准。
2.2 原始数据与物理量的换算
测量命令发出后,等待转换完成,然后从从机读出6字节数据。我板子上的实测顺序是:湿度高字节、湿度低字节、湿度CRC、温度高字节、温度低字节、温度CRC。如果你手册上拿到的是温度在前、湿度在后,把解析顺序对调一下就行,这个务必以你自己手上的手册为准。
14位有效数据,高字节左移8位后或上低字节,得到原始码值。换算公式:
- 温度:T(°C) = raw_T × 165.0 / 16383.0 - 40.0
- 湿度:RH(%RH) = raw_RH × 100.0 / 16383.0
这里16383就是2的14次方减1,对应14位ADC的最大码值。温度范围跨度为165度,从-40到125度,所以先把原始码值按比例映射到0~165的区间,再整体减去40。湿度更直接,0到100%RH线性映射到0~16383,按比例乘100即可。
很多人在这一步容易翻车:直接用raw_T除以65535去算,因为习惯了16位ADC的满量程。HTU31D是14位分辨率,高位字节里低2位是无效的,必须用16383而不是65535,否则温度会整体偏低,湿度也不对。这个问题我在帮同事查代码时遇到过不止一次。
2.3 CRC-8校验的重要性
I2C传输在强电干扰环境里可能出错,HTU31D为每个测量值附带1字节CRC-8校验。多项式是0x31,初始值是0x00。刚开始做的时候我嫌麻烦,觉得工地环境没那么恶劣,结果在继电器吸合瞬间读出来的湿度偶尔会莫名跳一下,加了三串CRC校验后才定位到是I2C数据被干扰。
CRC校验代码很固定,逐位计算的版本就够用:
uint8_t HTU31D_CRC8(uint8_t *data, uint8_t len)
{
uint8_t crc = 0x00;
for (uint8_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;
}
把温度或者湿度的两个数据字节喂进去,得到的结果和第三个字节比较,一致就说明这次传输没问题。有了这层校验,后续做工业现场的设备时心里踏实很多。
3. 驱动代码从零实现:HAL库实战
3.1 驱动结构体设计
驱动文件我习惯分成htu31d.h和htu31d.c两个文件。用结构体把I2C句柄、温湿度数据、CRC错误计数封装到一起,调用起来非常顺手:
typedef struct {
I2C_HandleTypeDef *hi2c;
float temperature;
float humidity;
uint8_t crc_error_cnt;
} HTU31D_HandleTypeDef;
I2C句柄用指针而不是直接复制一份,这样多个传感器共用同一个I2C总线时,结构体里存的只是指针,不浪费内存。F103的RAM本来就不大,能省一点是一点。
3.2 初始化与软复位
初始化函数里先执软复位,目的是把传感器恢复到确定状态。上电瞬间如果电源抖动厉害,传感器可能处于异常状态,软复位能把它拉回来。
uint8_t HTU31D_Init(HTU31D_HandleTypeDef *dev, I2C_HandleTypeDef *hi2c)
{
dev->hi2c = hi2c;
dev->crc_error_cnt = 0;
if (HTU31D_Reset(dev) != 0) {
return 1;
}
HAL_Delay(2); // 软复位后至少要等1.5ms,给2ms更保险
return 0;
}
uint8_t HTU31D_Reset(HTU31D_HandleTypeDef *dev)
{
uint8_t cmd = 0xFE;
return HAL_I2C_Master_Transmit(dev->hi2c, 0x80, &cmd, 1, 100);
}
HAL_I2C_Master_Transmit的最后一个参数是超时时间,单位是毫秒。100ms超时足够判断总线是否卡死,如果连这个都超时,说明接线或者地址有问题。
3.3 一次完整测量流程
核心函数是一次测量并读出温湿度,我把读数据函数返回不同的错误码,方便上层判断:
uint8_t HTU31D_ReadData(HTU31D_HandleTypeDef *dev)
{
uint8_t cmd = 0x00;
uint8_t buf[6];
uint16_t rh_code = 0, t_code = 0;
uint8_t rh_raw[2], t_raw[2];
// 1. 发出测量命令
if (HAL_I2C_Master_Transmit(dev->hi2c, 0x80, &cmd, 1, 100) != HAL_OK) {
return 1;
}
// 2. 等待转换完成,第一版可以先给10ms,再逐步往下压
HAL_Delay(10);
// 3. 读取6字节结果
if (HAL_I2C_Master_Receive(dev->hi2c, 0x81, buf, 6, 100) != HAL_OK) {
return 2;
}
// 4. 按湿度在前、温度在后的顺序解析(以手册为准)
rh_raw[0] = buf[0];
rh_raw[1] = buf[1];
t_raw[0] = buf[3];
t_raw[1] = buf[4];
// 5. CRC校验
if (HTU31D_CRC8(rh_raw, 2) != buf[2]) {
dev->crc_error_cnt++;
return 3;
}
if (HTU31D_CRC8(t_raw, 2) != buf[5]) {
dev->crc_error_cnt++;
return 4;
}
// 6. 合成14位码值并换算
rh_code = (buf[0] << 8) | buf[1];
t_code = (buf[3] << 8) | buf[4];
dev->humidity = (float)rh_code * 100.0f / 16383.0f;
dev->temperature = (float)t_code * 165.0f / 16383.0f - 40.0f;
return 0;
}
关于延时这步多说一句:HTU31D的转换时间在不同供电电压、不同温度下会有点差异,第一版驱动别追求极限性能,先来个10ms保证每次都能读到新数据。等系统跑稳了,再根据实测压缩延时,甚至可以用状态寄存器查询忙标志来替代固定延时。
3.4 主函数调用示例
在main函数里,初始化I2C外设后,调用HTU31D_Init,然后主循环每秒钟读一次:
HTU31D_HandleTypeDef htu31d;
if (HTU31D_Init(&htu31d, &hi2c1) != 0) {
printf("HTU31D init failed\r\n");
}
while (1) {
if (HTU31D_ReadData(&htu31d) == 0) {
printf("T=%.2f C, RH=%.2f %%\r\n", htu31d.temperature, htu31d.humidity);
} else {
printf("HTU31D read error\r\n");
}
HAL_Delay(1000);
}
printf重定向到串口,用串口助手直接看数据。跑通这一步,你的F103就已经成功驱动HTU31D了。如果你还在用标准外设库,也别慌,寄存器字段和HAL库完全一致,只是把HAL_I2C_Master_Transmit换成标准库里I2C_GenerateSTART、I2C_SendData那一套流程而已。
4. 硬件接线与F103实测中的坑
4.1 接线和上拉电阻
HTU31D接F103,最基本的四根线:
| HTU31D引脚 | STM32F103引脚 | 说明 |
|---|---|---|
| VIN | 3.3V | 供电,传感器典型工作电压 |
| GND | GND | 共地 |
| SCL | PB6 | I2C1时钟线 |
| SDA | PB7 | I2C1数据线 |
I2C总线需要上拉电阻,这是协议本身的要求。一般用4.7kΩ到3.3V,如果走线短、速率低,10kΩ也能用。如果你买的是现成的HTU31D模块,模块上通常已经带上拉电阻和滤波电容,直接用杜邦线连接即可。如果是自绘PCB裸焊芯片,DFN-8封装建议用转接板,烙铁温度不要超过350°C,单个引脚焊接时间控制在3秒内,不然容易把焊盘烫坏。
4.2 F103硬件I2C的经典坑
F103的硬件I2C在标准外设库时代有个著名的坑:总线忙标志(BUSY)在通信异常之后会一直置1,导致程序死等。HAL库把底层封装好了,但偶发的NACK和超时仍然存在,尤其是电源纹波大或者干扰强的时候。
我的实战经验有三条:
- 所有HAL I2C调用都必须检查返回值,不要裸奔,返回值不是HAL_OK就按异常处理。
- 通信异常时先尝试软复位传感器,再重新初始化I2C外设,很多问题到这里就能解决。
- 如果还是失败,手动释放总线:把SCL翻转9次,让从机释放SDA。这个操作相当于“教会”从机跳出异常状态。
void I2C_Recovery(I2C_HandleTypeDef *hi2c)
{
// 先把I2C外设关掉,让PB6/PB7变成普通GPIO
HAL_I2C_DeInit(hi2c);
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7;
gpio.Mode = GPIO_MODE_OUTPUT_OD;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &gpio);
for (int i = 0; i < 9; i++) {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET);
delay_us(10);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);
delay_us(10);
}
// 之后重新初始化I2C即可
}
这段代码在真机上验证过,能挽救大部分“莫名其妙I2C卡死”的场景。
4.3 什么时候改用软件模拟I2C
如果你的设备旁边有电机、继电器这类强干扰源,且走线避不开,硬件I2C被拉死的概率会明显上升。这种情况我建议直接上软件模拟I2C。用GPIO电平翻转实现I2C时序,对时序容忍度高,调试也直观,出问题的时候用示波器看GPIO波形就能定位。
模拟I2C的核心思路是:SCL和SDA都配置成开漏输出,配合外部上拉电阻,读SDA时把引脚切到输入模式。起始条件、停止条件、ACK应答这些时序严格按I2C协议来。写一个delay_us的微秒延时函数,剩下的事情就是按位收发。
void I2C_Soft_Start(void)
{
SDA_HIGH();
SCL_HIGH();
delay_us(5);
SDA_LOW();
delay_us(5);
SCL_LOW();
}
uint8_t I2C_Soft_WriteByte(uint8_t data)
{
for (uint8_t i = 0; i < 8; i++) {
if (data & 0x80) SDA_HIGH();
else SDA_LOW();
data <<= 1;
delay_us(2);
SCL_HIGH();
delay_us(2);
SCL_LOW();
delay_us(2);
}
// 释放SDA,准备接收从机ACK
SDA_HIGH();
SCL_HIGH();
delay_us(2);
uint8_t nack = READ_SDA();
SCL_LOW();
return nack;
}
软件模拟I2C的缺点是占用CPU,但对每秒读一次温湿度的场景来说完全不是问题。
5. 数据稳定性和校准经验
5.1 刚上电的数据漂移与滤波
HTU31D上电后,前几十秒的数据可能会出现缓慢漂移,这不是芯片坏了,而是湿敏电容需要一点时间从运输存储状态恢复到正常状态。我习惯在固件里做“上电预热”逻辑:开机后前30秒的数据只采集不上报,等稳定后再正式使用。
日常读取时,连续采集5次取平均值,或者取5次结果的中位数,能有效滤掉偶发的尖峰毛刺。中位数滤波尤其好用,因为平均值会被极端值拉偏,中位数不会。5次采集间隔100ms,整个滤波周期500ms,对温湿度这种慢变量完全不影响实时性。
5.2 CRC错误增多怎么排查
如果CRC错误计数持续增长,优先检查三件事:
- 检查I2C线长。传感器通过排线和主板连接,且排线超过20cm时,把I2C时钟降到100kHz标准模式,不要跑400kHz快速模式。
- 检查上拉电阻阻值。线缆电容变大的时候,4.7kΩ可能太弱,换成2.2kΩ会让信号边沿更陡,但也不能太小,否则功耗和灌电流会超标。
- 检查供电。传感器供电不要和继电器、电机共用一个LDO,最好单独用一路低噪声LDO,并在VIN引脚附近放0.1uF和10uF两级滤波电容。
有一次CRC错误飙升,排查到最后是传感器旁边的一颗DC-DC电感辐射干扰,把电感换成屏蔽电感后问题消失。这类问题用示波器看I2C波形上的毛刺会非常清晰。
5.3 饱和盐溶液校准法
HTU31D出厂校准做得不错,但批量板子之间总有个体差异。如果项目对湿度精度要求高,可以用饱和盐溶液做校准。这个方法的原理是:特定盐的饱和溶液在密闭空间里能维持一个固定的相对湿度。
以氯化钠(食盐)为例,20°C时饱和氯化钠溶液上方的相对湿度约为75.3%RH。把传感器和一杯饱和氯化钠溶液放进密封盒,等30分钟以上,记录传感器的湿度读数,和75.3%做差,这个差值就是偏移量,在固件里做补偿即可。温度校准可以用冰水混合物做0°C点,用室温水银温度计做室温点,做两点线性修正。
校准的时候要注意,密封盒体积不能太小,溶液表面积要足够大,否则湿度平衡会很慢。我一般用那种装零食的密封罐,盐溶液用个小碟子盛着,效果很好。
6. 扩展:低功耗与FreeRTOS场景下的驱动改造
6.1 STOP(停机)模式下的传感器处理
环境监测项目很多是电池供电,F103进入STOP模式后,整机待机电流可以做到很低,但传感器本身还在耗电。要让功耗再降一个台阶,最彻底的办法是在传感器供电回路里加一颗PMOS管,MCU睡眠时把PMOS关断,传感器完全断电。
唤醒后需要重新初始化传感器,并且要注意一点:HTU31D重新上电后要等10ms左右再发命令,否则第一个请求很可能被漏掉。我的做法是在HTU31D_Init函数开头固定HAL_Delay(10),反正就10ms,用户感知不到。这种方式比频繁软复位靠谱,因为冷启动状态比任何软件复位都干净。
6.2 FreeRTOS环境下加I2C互斥锁
如果系统里还有EEPROM、OLED之类的I2C设备,HTU31D驱动就得考虑总线竞争问题。FreeRTOS下最简单的做法是给I2C总线加一把互斥锁:
<p>
<a href="https://download.csdn.net/download/ywf147/16129271" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a>
<img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;">
</p>
更多推荐

所有评论(0)