
简介STM32F103与HTU31D温湿度传感器的完整工程源码包面向嵌入式开发者和环境监测项目学习者解决I²C驱动、数据解码及串口输出的实践问题。压缩包含215个文件约5.96MB主要包含C/H源文件、编译中间文件o、crf、Keil工程配置文件uvprojx、uvoptx以及可烧录的hex文件等便于完整还原编译与调试流程。通过标准库与HAL代码可学习STM32F103C8T6的GPIO模拟I²C时序、HTU31D命令读取及16位温湿度数据转换并借助USART1将结果发送至串口终端。工程目录清晰区分Project、Source、Libraries与Hardware模块帮助快速定位驱动代码和硬件设计。已有1164人学习下载适合需要参考完整项目或移植传感器驱动的开发者。 在嵌入式交流群和论坛里“基于STM32F103驱动HTU31D温湿度传感器.rar”这个文件名经常能看到。我最早接触HTU31D是在一个冷链运输记录仪的项目里当时要把老旧的DHT11替换掉筛选了一圈最终选了TE Connectivity推出的HTU31D。今天这篇就结合这个压缩包工程把STM32F103驱动HTU31D的完整实现拆开讲讲从I2C时序到温湿度换算再到实际调试中容易踩的坑一次性说清楚。这个工程适合谁如果你正在做环境监测、温室大棚、冷链物流、暖通控制或者单纯想给毕业设计加一个靠谱的温湿度采集模块这篇都适用。HTU31D属于数字输出传感器驱动难点主要集中在对I2C时序的理解和数据处理上STM32F103又是国内市场上资料最丰富、使用群体最大的MCU把这两个组合吃透再做其他I2C传感器基本就是套路迁移。1. 项目概述与整体设计思路1.1 这个工程到底做了什么打开这个.rar压缩包正常情况下里面会包含STM32F103的工程文件、HTU31D的数据手册PDF、驱动源码和若干说明文档。这类工程的核心逻辑并不复杂STM32F103通过I2C总线向HTU31D发送测量命令传感器完成转换后把温湿度原始数据回传MCU端做数据拼接、CRC校验和物理量换算最终得到可以直接用于业务逻辑的温度和相对湿度值。但“不复杂”只是事后回看。实际写驱动的时候需要同时处理三块内容一是I2C底层时序的正确性起始信号、停止信号、应答位每一个环节都不能出错二是HTU31D本身的命令协议比如触发测量要发送什么字节、读取数据要发什么地址三是数据后处理24位原始数据怎么拼、负数温度怎么表示、CRC怎么校验。这个工程的价值就在于把这三块内容串成了一条完整的链路。1.2 用料选型背后的逻辑为什么是HTU31D而不是烂大街的DHT11我在冷链项目里曾经用过一段时间DHT11最大的痛点有两个一是精度太差湿度误差±5%RH温度误差±2°C做定性判断勉强够用做定量记录根本不行二是时序要求严苛DHT11是单总线协议对延时精度极敏感MCU主频只要稍微偏差读出来的数据就是乱码。而HTU31D走的是标准I2C只要时序在规范范围内通信稳定性要高一个量级。从性价比角度说HTU31D在精度上属于同价位里的均衡选手温度典型精度±0.2°C湿度典型精度±2%RH这个参数区间覆盖了大多数工业监测和消费级产品的需求。相比SHT30HTU31D的长期稳定性和抗结露能力做得更好这也符合它面向工业场景的产品定义。STM32F103就更不用多说了Cortex-M3内核72MHz主频I2C、SPI、USART外设齐全而且价格便宜、封装多样国产替代型号也极其丰富。做产品选型的时候我会优先考虑F103的LQFP48封装引脚少、布线容易核心板十几块钱就能搞定做原型验证成本极低。这个组合放到今天看依然不过时。2. HTU31D核心细节解析寄存器、命令与测量原理2.1 I2C通信基础与关键约定HTU31D是标准的I2C从设备7位设备地址是0x40换算成8位写地址是0x80读地址是0x81。I2C总线上要接两个上拉电阻典型值4.7kΩ如果总线长度超过20厘米或者上拉电阻太大会导致信号上升沿过缓出现通信不稳定的情况。单片机向HTU31D发命令的基本流程是主设备先发一个起始信号然后发送0x80设备地址写标志等待传感器返回ACK再发送具体的命令字节再次等待ACK最后发停止信号。在读取数据的阶段主设备先发起始信号然后发送0x81设备地址读标志之后每个字节读取完都要回一个ACK直到最后一个字节回NACK再发停止信号。这个流程是I2C的通用套路我见过很多新手在这里栽跟头要么忘了一开始要发两次地址先写后读要么在读取最后一个字节时回了ACK导致传感器一直输出数据。记住一个口诀读最后一个字节之前回NACK是告诉从设备“我读够了你闭嘴”。2.2 测量命令与数据格式HTU31D有一颗控制寄存器和一个状态寄存器日常使用不需要频繁读写寄存器直接用命令触发测量就行。最常见的命令是0xFC表示“触发温湿度转换使用正常分辨率”转换时间典型值约2.5ms实际代码里建议延时10ms以上留足裕量。数据读取阶段传感器会一次性返回7个字节前3个字节是24位温度原始数据高位在前接下来3个字节是24位湿度原始数据高位在前最后1个字节是CRC校验值。温度原始数据是一个无符号数映射范围从-40°C到125°C湿度原始数据同样是无符号数映射范围从0%RH到100%RH。为什么用24位为了分辨率。24位数据意味着温度分辨率可以达到0.01°C级别湿度分辨率可以达到0.01%RH级别对大多数应用来说已经远远够用。2.3 温湿度换算公式与CRC校验逻辑拿到24位原始数据之后物理量换算公式如下温度°C -40 165 ×温度原始值 / 0xFFFFFF湿度%RH 100 ×湿度原始值 / 0xFFFFFF举个例子温度原始值是0x666666十进制6710886代入公式就是 -40 165 × (6710886/16777215)结果大约是26.0°C。湿度原始值如果等于0x4CCCCC十进制5033164换算结果大约是30.0%RH。这个换算逻辑很简单但工程上要注意浮点运算在STM32F103上比较耗时如果系统对实时性要求高可以先左移16位转成整数运算最后再除一次精度损失完全可以接受。CRC校验是很多人容易忽略的点。HTU31D的CRC是CRC-8多项式0x31x8x5x41初始值0xFF。我在冷链项目的现场就碰到过一次传感器读取的数据偶尔跳变查了半天发现是数据线被电机干扰如果没有CRC校验这种偶发错误会被当成正常数据写进存储后期追溯非常痛苦。所以建议在产品代码里务必加上CRC校验一次通不过就重新读取连续三次失败再报错。3. 实操过程STM32F103驱动代码的完整实现3.1 硬件连接与工程准备我这里以STM32F103C8T6最小系统板为例HTU31D模块一般引出4个引脚VCC、GND、SCL、SDA。给传感器供电3.3V即可不要接5VHTU31D的绝对最大额定电压只有3.6V。SCL和SDA分别接PB6和PB7这两个引脚在F103上默认就是I2C1的引脚不过下面我要用GPIO模拟I2C的方式实现——原因后面说。连接关系VCC → 3.3VGND → GNDSCL → PB6SDA → PB7上拉电阻选择4.7kΩ如果模块上已经带了上拉电阻就不用额外加了。然后准备STM32标准外设库或者直接用HAL库也可以模拟I2C部分几乎不依赖库函数只要会配置GPIO就能跑通。为什么要用模拟I2C而不是STM32F103的硬件I2C外设这个我多说一句。F103的硬件I2C模块在业界口碑一直不怎么样主要问题表现在总线忙状态检测容易出错、中断处理不当会死锁虽然很大一部分原因是工程师配置问题而不是芯片本身的问题但模拟I2C在项目开发阶段省心得多时序完全可控代码可读性好而且换到任何一颗MCU上都能直接移植。如果你的产品要大规模量产再去优化成硬件I2CDMA也不迟。3.2 I2C底层驱动代码解析先初始化GPIO把PB6和PB7配成开漏输出模式这样配合外部上拉电阻就能实现真正的双向IOvoid I2C_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); I2C_SCL_H(); // 初始状态均为高 I2C_SDA_H(); }开漏输出的好处是既有输出能力又不干扰对方拉低电平这是实现I2C通信的基础。接下来是模拟I2C的核心时序函数。起始信号SCL保持高电平期间SDA从高变低。停止信号SCL保持高电平期间SDA从低变高。发送一个字节从最高位开始依次把数据送到SDA然后翻转SCL产生时钟脉冲void I2C_Start(void) { I2C_SDA_H(); I2C_SCL_H(); Delay_us(5); I2C_SDA_L(); Delay_us(5); I2C_SCL_L(); } void I2C_Stop(void) { I2C_SCL_L(); I2C_SDA_L(); Delay_us(5); I2C_SCL_H(); Delay_us(5); I2C_SDA_H(); } void I2C_SendByte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { if (dat 0x80) { I2C_SDA_H(); } else { I2C_SDA_L(); } dat 1; Delay_us(3); I2C_SCL_H(); Delay_us(5); I2C_SCL_L(); Delay_us(3); } } uint8_t I2C_ReadByte(uint8_t ack) { uint8_t i, dat 0; I2C_SDA_H(); // 释放SDA让从设备控制 for (i 0; i 8; i) { I2C_SCL_H(); Delay_us(5); dat (dat 1) | I2C_SDA_READ(); I2C_SCL_L(); Delay_us(5); } if (ack) { I2C_SDA_L(); // 回ACK } else { I2C_SDA_H(); // 回NACK } I2C_SCL_H(); Delay_us(5); I2C_SCL_L(); I2C_SDA_H(); return dat; }延时时间为什么要用5微秒I2C标准模式最高100kHz快速模式最高400kHz。模拟I2C的时序参数只要满足SCL高电平最小时间、低电平最小时间就可以。5微秒对应100kHz左右的时钟频率属于比较保守的节奏对F103 72MHz主频来说用几个空循环或者DWT计数器都能实现。3.3 温湿度读取与数据换算代码有了底层I2C函数驱动HTU31D就很简单了。完整的读取流程如下#define HTU31D_ADDR_W 0x80 #define HTU31D_ADDR_R 0x81 #define HTU31D_CMD_MEAS 0xFC uint8_t HTU31D_Read(float *temperature, float *humidity) { uint8_t buf[7]; uint32_t rawT, rawH; uint16_t i; // 发送测量命令 I2C_Start(); if (I2C_SendByte(HTU31D_ADDR_W) 0) return 1; // 等待ACK I2C_SendByte(HTU31D_CMD_MEAS); if (I2C_WaitAck() 0) return 1; I2C_Stop(); // 等待转换完成这里延时至少10ms Delay_ms(10); // 读取7字节数据 I2C_Start(); I2C_SendByte(HTU31D_ADDR_R); for (i 0; i 6; i) { buf[i] I2C_ReadByte(1); // 前6字节回ACK } buf[6] I2C_ReadByte(0); // 最后1字节回NACK I2C_Stop(); // 拼接原始数据 rawT ((uint32_t)buf[0] 16) | ((uint32_t)buf[1] 8) | buf[2]; rawH ((uint32_t)buf[3] 16) | ((uint32_t)buf[4] 8) | buf[5]; // CRC校验代码略建议实现 // if (HTU31D_CheckCRC(buf, 6, buf[6])) return 2; // 物理量换算 *temperature -40.0f 165.0f * (float)rawT / 16777215.0f; *humidity 100.0f * (float)rawH / 16777215.0f; return 0; }这里有个细节要注意读取数据时如果不做CRC校验至少要确认返回的7个字节都在合理范围内特别是温度原始值不能是0xFFFFFF或者0x000000这种极端值否则大概率是通信出了问题。换算之后如果想固定输出格式在串口打印时保留一位或者两位小数即可。工程上如果要把数据上报给上位机或者云平台建议直接传整数形式的原始值让服务器端做换算MCU端少做浮点运算可以降低功耗和计算压力。4. 常见问题与调试经验速查4.1 最典型的几个故障点第一类问题是I2C通信完全不通。现象是SCL、SDA引脚波形异常或者设备地址没有ACK响应。排查顺序先量供电确认VCC对GND是3.3V再量上拉电阻确认SCL和SDA都有3.3V的上拉电平用示波器或者逻辑分析仪看起始信号是不是SCL高电平期间SDA拉低。很多情况下是GPIO配置成了推挽输出而不是开漏输出导致SDA拉低之后释放不了总线直接被锁死。第二类问题是读取的湿度始终是100%或者温度始终是最大值。这个问题我遇到过一次排查后确认是因为测量命令发送之后没有等待足够长的时间就去读数据传感器还在转换中读回来的是无效数据。另外也要检查是不是把24位数据拼成了32位比如uint32_t强制转换时没有先转成uint32_t再做位移导致符号扩展把高位填充成了0xFF。第三类问题是数据偶尔跳变。这大概率是干扰问题。I2C线如果走线过长或者和电机驱动线并行就容易受到耦合干扰。解决思路缩短I2C线缆长度、加屏蔽、降低I2C速率、在SDA和SCL对地各加一个100pF左右的滤波电容。软件上一定要加CRC校验有了这一层偶发错误不会污染数据。下面整理成速查表方便对照现象可能原因排查手段无ACK响应供电异常/地址错误/总线锁死测量VCC、检查GPIO开漏配置数据全为0或全为1转换时间不足/接线断开增加延时、重新焊接连线读数偶发跳变干扰/上拉电阻偏大加滤波电容、减小上拉电阻湿度长时间100%传感器结露/测量命令未执行做防结露处理、检查命令流程温度偏高且恒定传感器自热/靠近热源检查PCB布局、降低测量频率4.2 低功耗与量产场景的补充建议这个工程文件在网上的流传版本大多是裸机轮询方式跑在主循环里每隔一秒读一次。如果要把这套驱动用到电池供电的产品里有几个点需要额外处理。HTU31D在非测量状态下的待机电流非常小典型值在0.14µA级别但前提是测量完成之后要让它进入空闲状态。在低功耗产品里可以让MCU进入STOP模式用定时器唤醒唤醒后测量一次温湿度然后再次进STOP。STM32F103的STOP模式实测功耗可以做到10µA以下加上传感器的待机电流整机功耗非常可观。传感器供电也可以单独用一个GPIO控制测量前拉高给传感器供电等待10ms稳定后再发命令测量完成立即拉低断电。这样做的目的是彻底切断传感器在待机时的漏电流缺点是每次测量前都需要重新初始化传感器实测下来多耗几毫秒对大多数应用来说完全无所谓。另外如果你在PCB上把HTU31D放在MCU附近要留意传感器自热问题传感器自身测量时会有微弱的自热效应如果长期连续测量读数会比环境温度偏高0.1~0.2°C。解决方法是测量间隔大于1秒避免长时间连续读取同时传感器的通风孔不要被外壳完全堵死。我从冷链项目里得到的经验是传感器驱动的价值不在于把官方例程抄一遍而在于把异常情况处理干净。数据校验、错误重试、合理性判断这些看着不起眼的代码才是产品在现场稳定运行的关键。做嵌入式这几年我越来越觉得一个工程文件最容易复制的是主路径代码最难复制的是把这些边界条件和坑都填平的细节。希望这篇拆解能帮你少走几步弯路。本文还有配套的精品资源点击获取