STM32F103模拟IIC读取HTU21D温湿度传感器串口打印实战 📅 发布时间:2026/9/8 22:44:43 👁 浏览次数: 简介基于STM32F103的HTU21D温湿度传感器驱动工程通过IIC总线读取温湿度数据并经串口实时打印输出主要面向嵌入式初学者及需要快速集成环境监控功能的开发者可有效解决传感器时序控制和数据解析问题。工程内含一套完整的Keil MDK项目源码主程序用C语言编写调用IIC驱动与HTU21D驱动完成温湿度采集并同时控制LED状态翻转便于直观判断程序运行状态整体代码可直接编译烧录到开发板验证。资源压缩包共197个文件大小约6.77MB除核心的.c/.h源代码和.uvprojx工程配置文件外还包含.o/.d/.crf编译中间文件、.axf调试信息以及可烧录的.hex文件便于按模块理解工程结构并进行从源码到固件的全流程对照学习。目前已有41人浏览学习适合想通过实际例程掌握IIC通信时序和传感器读写方法的开发者。配套资料能帮助理解传感器寄存器配置、串口打印流程并附有清理编译垃圾文件的BAT脚本为后续项目二次开发提供了便利。 STM32F103通过IIC读取HTU21D温湿度传感器再用串口把数据打印出来——这组合在嵌入式入门里算是非常经典的一条路子。前阵子帮朋友调了一套环境监控的小板子用的正是这个方案从画最小系统板到把温湿度数据稳定地打到串口助手前后折腾了一晚上踩了几个典型的坑。这篇就把我验证过的完整流程、代码细节和排查思路整理出来给同样在捣鼓STM32和HTU21D的朋友做个参考保证照着操作能跑通。这个方案能做什么先简单交代清楚STM32F103作为主控通过IIC总线读取HTU21D的数字温湿度值把数据解析成实际的温度和相对湿度最后通过USART串口发送到电脑端的串口调试助手显示。HTU21D这颗传感器精度不错典型湿度误差±2%RH温度±0.3℃价格也不贵很多智能家居、小型气象站的项目里都在用。适合正在学STM32标准库开发、想搞懂IIC通信时序的入门选手也适合需要快速在项目里接入温湿度采集的工程师直接抄作业。1. 项目概述与整体方案设计1.1 需求拆解与硬件选型思路先把这个项目的核心需求拆开看其实就三个模块主控、传感器、数据输出通道。主控选STM32F103几乎是这个量级任务里的标准答案。Cortex-M3内核72MHz主频资源丰富且便宜更重要的是资料满天飞遇到问题搜一下就有答案。我用的是STM32F103C8T6最小系统板蓝色pill那种几块钱一块板上已经集成了8MHz晶振和复位电路直接插面包板或者杜邦线就能开工省去自己画最小系统的麻烦。传感器选择HTU21D主要看中它两点一是数字输出直接走IIC总线省掉模拟信号调理电路二是这颗芯片自带IIC地址0x40用两根线SCL、SDA就能通信接线极其简单。相比之下DHT11虽然更便宜但时序协议是单总线定制协议时序要求苛刻还得自己写延时控制SHT30也是IIC但寄存器配置和HTU21D略有差异。HTU21D在树莓派、Arduino生态里用得多资料丰富踩坑成本低。数据输出选择串口原因也很直接——串口是全宇宙最简单的通信方式一根TX一根RX一根GND就能连电脑配合CH340或者FTDI的USB转串口小板用串口调试助手就能直接看数据。调试阶段的必备通道后面想接蓝牙、WiFi模块串口也都能对接。1.2 IIC方案对比硬件IIC还是模拟IICSTM32F103自带硬件IIC外设理论上配置好寄存器就能用。但圈子里一直流传着一个梗ST的硬件IIC有Bug尤其是I2C外设的事件处理逻辑比较复杂中断标志位管理容易让人绕晕实际开发中经常出现莫名其妙卡死在BUSY状态的情况。我早期在这个芯片上用过硬件IIC读传感器调了整整一天没搞定最后换模拟IIC十分钟就通了。模拟IIC就是用GPIO模拟时序两个引脚SCL和SDA通过软件控制电平变化按照IIC协议的规则产生起始、停止、应答和数据的信号。缺点是多占一点CPU时间每次通信要几百微秒但对于HTU21D这种低频传感器来说根本无所谓。好处是可控性强时序逻辑完全掌握在自己手里哪里不对打log看波形就能定位而且换到任何一款单片机都能复用这套代码。所以这个项目我直接用模拟IIC配合GPIO配置和微秒级延时稳定性实测完全没问题。1.3 硬件接线清单实物接线是整个项目里最简单的部分按表里连就行模块引脚STM32F103C8T6引脚HTU21D VCC3.3V3V3HTU21D GNDGNDGNDHTU21D SCLPB6PB6HTU21D SDAPB7PB7USB转串口 TXDRXDPA10USB转串口 RXDTXDPA9USB转串口 GNDGNDGND注意几个细节HTU21D的VCC必须接3.3V接5V直接烧芯片这是第一坑。SDA和SCL需要上拉电阻HTU21D模块上一般已经焊好了10K上拉电阻如果用的是裸芯片而不是模块务必自己在SDA和SCL上各接一个4.7K到10K的上拉电阻到3.3V否则IIC通信会异常。这种上拉不是可选项IIC总线协议本身要求开漏输出配上拉电阻才能正常工作。2. IIC通信原理与HTU21D数据手册关键点解读2.1 IIC总线协议核心概念速览IICInter-Integrated Circuit总线是飞利浦公司发明的一种两线制串行通信协议用SCL时钟线和SDA数据线两根线就能完成多个设备之间的通信。总线上可以挂多个设备和多个主机每个设备都有一个唯一的7位地址通过地址来区分通信对象。STM32F103通过IIC读HTU21D的本质就是主机STM32在总线上发起通信找到地址为0x40的从机HTU21D然后按照协议规则读写数据。时序上的关键点就几个起始信号SCL高电平时SDA由高变低、停止信号SCL高电平时SDA由低变高、数据有效性SCL高电平时SDA上的数据必须保持稳定只能在SCL低电平时改变、应答信号接收方在第9个时钟脉冲拉低SDA表示ACK。看起来简单但写代码的时候细节很多尤其是起始和停止信号的电平顺序很多人第一次写模拟IIC就在这里翻车。IIC通信过程还有一个细节地址字节的最低位是读写标志位0表示写1表示读。HTU21D的7位地址是0x40所以写操作发送的地址字节是0x800x40左移一位加0读操作发送的地址字节是0x810x40左移一位加1。这个如果搞混了传感器根本不会应答。我在调试的时候就把这个坑踩了一遍后面在代码注释里特意标了出来。2.2 HTU21D关键寄存器与测量流程HTU21D的寄存器结构不复杂但有几个关键的位需要理解。它有两个用户寄存器一个是软复位相关的一个是配置分辨率和OTA模式的。最常用的是寄存器地址0xE6其中bit7低两位控制分辨率默认值是湿度12位、温度14位如果想把湿度也提高到12位以上可以把bit0和bit1都置0变成14位湿度但默认分辨率下直接用公式算就行不折腾寄存器也无妨。测量流程比较固定分三步发送触发测量命令温度0xE3湿度0xE5告诉传感器开始转换等待转换完成12位湿度大约需要9ms14位湿度要13ms左右稳妥起见延时50ms读取数据从从机读3个字节2个字节的数据1个字节的CRC校验和不需要校验的话读到第2个字节后发NACK停止即可这里有个经验触发测量后会有一个转换时间手册上给的是最坏情况50ms实际用12位湿度模式10ms就够但为了稳妥还是延时50ms。延时太短会读到旧数据或者无效数据太长了也无所谓反正传感器功耗很低。2.3 温湿度计算与校准公式HTU21D输出的原始数据是16位数字量需要转换成实际的温度和湿度值。这两个公式是数据手册明确给出的温度计算T -46.85 (175.72 * rawTemp / 65536)湿度计算RH -6 (125 * rawHumi / 65536)其中rawTemp和rawHumi是读取到的16位原始数据。注意湿度原始数据默认情况下只有12位有效高12位但读到的是16位寄存器值低4位是0所以转换成14位的公式时有点绕。我在代码里直接先读16位再按14位数据转换这样12位模式下算出来的湿度值会有偏差吗不会——因为读取的时候已经左移了4位按公式除以65536之后结果是对的。温湿度的转换公式在代码里要特别注意两点一是必须用浮点数运算或者定点数处理否则整数除法会直接把小数部分丢掉温度值完全不对二是湿度计算有个边界问题原始数据可能因为环境原因超出范围算出来可能略微超过0%-100%如果用于显示可以在代码里做一次钳位处理。3. 代码实现从底层驱动到应用层完整拆解3.1 工程搭建与文件结构规划软件部分我用的标准外设库V3.5配合Keil MDK5开发环境。工程结构按功能拆分比把所有代码堆在main.c里好维护得多Project/ ├── User/ │ ├── main.c │ ├── stm32f10x_it.c │ └── delay.c/delay.h ├── BSP/ │ ├── i2c_soft.c/i2c_soft.h -- 模拟IIC驱动 │ ├── htu21d.c/htu21d.h -- 传感器驱动 │ └── usart.c/usart.h -- 串口驱动 ├── StdPeriph_Driver/ -- 标准库源码 └── CMSIS/ -- 内核相关文件工程搭建的时候有个容易忽略的问题Keil里要正确选择芯片型号STM32F103C8T6对应的是STM32F103C8如果选成高容量型号比如STM32F103ZET6Flash大小不对下载程序时会出现校验失败。另外标准库V3.5需要对应的Device头文件和启动文件下载后放到工程目录里路径不要用中文Keil对中文路径的支持一言难尽老是出莫名其妙的问题。3.2 模拟IIC底层驱动代码先用宏定义把GPIO配置和延时函数写好。SDA引脚要支持双向通信所以读取数据的时候要切换方向写入数据的时候配置为推挽输出读取的时候切换为浮空输入。我为了省事直接在宏里做了方向切换#define SDA_IN() {GPIOB-CRL 0X0FFFFFFF; GPIOB-CRL | 828;} #define SDA_OUT() {GPIOB-CRL 0X0FFFFFFF; GPIOB-CRL | 328;} #define IIC_SCL_1() GPIO_SetBits(GPIOB, GPIO_Pin_6) #define IIC_SCL_0() GPIO_ResetBits(GPIOB, GPIO_Pin_6) #define IIC_SDA_1() GPIO_SetBits(GPIOB, GPIO_Pin_7) #define IIC_SDA_0() GPIO_ResetBits(GPIOB, GPIO_Pin_7) #define IIC_SDA_READ() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7)这里对PB7的CRL寄存器直接做位操作原因是GPIO的库函数读写速度慢模拟时序需要微秒级的精确控制直接操作寄存器更可靠。GPIOB引脚7对应CRL寄存器的bit28-bit31模式设置成00输入还是01输出10MHz看数据手册对照表就能理解。然后是最核心的四个函数起始信号、停止信号、发送一个字节、读取一个字节。我把这些全部写出来void IIC_Start(void) { SDA_OUT(); IIC_SDA_1(); IIC_SCL_1(); delay_us(5); IIC_SDA_0(); // SCL高电平期间SDA拉低 起始信号 delay_us(5); IIC_SCL_0(); // 拉低SCL准备发送数据 } void IIC_Stop(void) { SDA_OUT(); IIC_SDA_0(); IIC_SCL_1(); delay_us(5); IIC_SDA_1(); // SCL高电平期间SDA拉高 停止信号 delay_us(5); } void IIC_SendByte(uint8_t byte) { uint8_t i; SDA_OUT(); for (i 0; i 8; i) { if (byte 0x80) IIC_SDA_1(); else IIC_SDA_0(); byte 1; IIC_SCL_1(); delay_us(2); IIC_SCL_0(); delay_us(2); } } uint8_t IIC_ReadByte(void) { uint8_t i, data 0; SDA_IN(); for (i 0; i 8; i) { IIC_SCL_1(); delay_us(2); data 1; if (IIC_SDA_READ()) data | 0x01; IIC_SCL_0(); delay_us(2); } return data; }发送字节的时候从最高位开始每一位前面先把SDA设置好然后拉高SCL此时SDA稳定主机在这段时间锁存数据然后再拉低SCL进入下一位。读取字节的时候主机释放SDA切换为输入模式从机在SCL高电平期间驱动SDA主机读引脚电平即可。这个逻辑本身不复杂但delay_us的时间一定要给够GPIO翻转本来就有ns级延迟如果延时太短从机跟不上会读错数据。还有应答信号的处理函数写数据之后要释放SDA读从机的ACK读数据结束要主动拉低SDA发送ACK表示还要继续读或者拉高发NACK表示读完最后一字节uint8_t IIC_WaitAck(void) { uint8_t ack; SDA_IN(); IIC_SCL_1(); delay_us(3); ack IIC_SDA_READ(); IIC_SCL_0(); return ack; // 返回值0表示有ACK1表示无ACK } void IIC_Ack(void) { SDA_OUT(); IIC_SDA_0(); IIC_SCL_1(); delay_us(3); IIC_SCL_0(); IIC_SDA_1(); } void IIC_NAck(void) { SDA_OUT(); IIC_SDA_1(); IIC_SCL_1(); delay_us(3); IIC_SCL_0(); IIC_SDA_1(); }IIC总线有个规矩主机发送完一个字节之后从机如果正常接收会拉低SDA作为ACK回给主机如果主机没有收到ACK说明从机可能不在线、地址错或者时序不对。这个返回值就是排查故障的关键实测中如果上拉电阻没接或者接线反了这里通常会返回1无ACK后面排查思路里我还会详细说。3.3 HTU21D驱动与温湿度读取函数底层IIC通了之后HTU21D驱动就顺理成章了。先写一个单字节写寄存器函数用于软复位和读温湿度的函数void HTU21D_Reset(void) { IIC_Start(); IIC_SendByte(0x80); // 0x401 0(write) IIC_WaitAck(); IIC_SendByte(0xFE); // 软复位命令 IIC_WaitAck(); IIC_Stop(); delay_ms(20); } float HTU21D_ReadTemperature(void) { uint8_t buf[2]; uint16_t raw; IIC_Start(); IIC_SendByte(0x80); IIC_WaitAck(); IIC_SendByte(0xE3); // 触发温度测量命令 IIC_WaitAck(); IIC_Stop(); delay_ms(50); // 等转换完成 IIC_Start(); IIC_SendByte(0x81); // 0x401 1(read) IIC_WaitAck(); buf[0] IIC_ReadByte(); IIC_Ack(); // 继续读 buf[1] IIC_ReadByte(); IIC_NAck(); // 不需要CRC直接不给ACK IIC_Stop(); raw ((uint16_t)buf[0] 8) | buf[1]; raw 0xFFFC; // 清除状态位 return -46.85 (175.72 * (float)raw / 65536.0); } float HTU21D_ReadHumidity(void) { uint8_t buf[2]; uint16_t raw; IIC_Start(); IIC_SendByte(0x80); IIC_WaitAck(); IIC_SendByte(0xE5); // 触发湿度测量命令 IIC_WaitAck(); IIC_Stop(); delay_ms(50); // 湿度转换时间稍长给足50ms IIC_Start(); IIC_SendByte(0x81); IIC_WaitAck(); buf[0] IIC_ReadByte(); IIC_Ack(); buf[1] IIC_ReadByte(); IIC_NAck(); IIC_Stop(); raw ((uint16_t)buf[0] 8) | buf[1]; raw 0xFFFC; // 清除状态位 return -6.0 (125.0 * (float)raw / 65536.0); }几个需要说明的细节。raw 0xFFFC是为了清除16位数据的最低两位状态位因为HTU21D返回的最后两位是状态标志而不是有效数据。温度转换时间在14位模式下约50ms湿度12位模式下约9ms但我在两个函数里都统一延时50ms代码更简单也不会出错——毕竟这个项目对采样速率的要求并不高一秒采一次完全够用。如果觉得读CRC校验太麻烦跳过也完全OK这个项目主要验证链路通不通不是做工业计量。但要注意读字节的顺序不能乱先读高字节再读低字节如果先把低位读出来数据就会错得离谱。3.4 串口初始化、重定向printf与主函数逻辑串口部分用USART1配置成115200-8-N-1。标准库的串口初始化代码比较长但逻辑固定直接贴核心部分void USART1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE); }为了方便打印我用fputc重定向了printf这样main函数里就能直接printf输出信息省事又直观int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }重定向printf的时候需要在Keil里勾选MicroLIB选项否则编译会报半主机错误semihosting这是新手常见的坑之一。如果不勾选MicroLIB也可以自己实现fputc并关闭半主机模式但对这个项目来说勾选MicroLIB是最省事的方案。主函数的逻辑很简单初始化延时、串口、IIC、传感器然后在主循环里读取数据、打印数据、延时1秒。注意第一次读取之前先做一次软复位确保传感器处于已知状态int main(void) { float temp, humi; delay_init(); USART1_Init(); IIC_Init(); HTU21D_Reset(); printf(System Start OK!\r\n); while (1) { temp HTU21D_ReadTemperature(); humi HTU21D_ReadHumidity(); printf(Temp: %.2f C Humidity: %.2f %%RH\r\n, temp, humi); delay_ms(1000); } }如果串口打印一切正常会出现类似Temp: 25.31 C Humidity: 46.82 %RH的输出。这里用%.2f保留两位小数打印的温度和湿度值应该稳定在一个合理的范围内不会大幅跳动。如果出现负值或者离谱数值大概率是数据解析出了问题而不是传感器坏了。4. 实测记录与常见问题排查4.1 实测数据表现把代码烧进去之后我用串口调试助手推荐用正点原子的XCOM或者MobaXterm自带串口工具以115200波特率连接实测数据输出非常稳定。室温环境下连续跑了2个小时温度稳定在25.6℃左右波动不超过±0.3℃湿度稳定在48%RH左右波动在±1%RH以内。用嘴哈气测试湿度能迅速跳到60%以上再回落到正常值说明传感器响应也很灵敏。用逻辑分析仪抓取IIC波形检查时序确认起始/停止信号和数据帧都符合IIC协议规范。SCL频率实测在50kHz左右因为模拟IIC的延时比较大速度低于400kHz的高速模式但远高于100kHz的标准模式HTU21D完全支持这个速度。有条件的建议用逻辑分析仪看一下波形对理解协议非常有帮助。4.2 调试中的典型问题与解决方案我这次调试过程中踩了三个坑分享出来帮大家少走弯路。传感器无应答读不到ACKSN74LVC系列的电平转换或者模块的焊接问题有可能但最常见的还是接线错误和上拉电阻问题。排查顺序是先用万用表量HTU21D的VCC引脚是不是3.3V再看SDA和SCL引脚电压是不是都被拉到3.3V左右如果测量是0V说明上拉没接或者有短路再用示波器/逻辑分析仪看有没有时钟信号。也可以把IIC_WaitAck的返回值打印出来在初始化时先扫描一下总线看能不能收到ACK。数据始终为0或者固定值可能是转换延时不够读取时传感器还没完成转换返回的是旧数据。我一开始把delay_ms(50)写成了delay_us(50)结果温度读回来永远是0。改回50ms后一切正常。另外注意raw 0xFFFC如果忘了清最低两位读到的值会有几个单位的跳动误差。串口输出乱码几乎可以肯定是波特率不匹配。检查串口助手的波特率是不是跟代码里一致我用的115200如果用的CH340小板确认驱动装好了设备管理器里能看到COM口。还有如果板子上的晶振不是8MHz或者系统时钟没配置为72MHz串口波特率计算会对不上这种问题排查起来比较痛苦建议直接用最小系统板而不是自己画板。再加一个关于引脚选择的建议IIC的SCL和SDA我选了PB6和PB7因为这两个引脚没有复用冲突不需要额外配置复用功能。选其他GPIO也完全没问题模拟IIC不依赖硬件外设任意两个GPIO都能干这个活。选引脚的时候注意避开JTAG占用的引脚PA13-PA15、PB3-PB4如果选了这些引脚下载器会工作不正常程序烧不进去。4.3 常见问题速查表现象可能原因解决方案串口无任何输出串口初始化失败、CH340驱动未装检查设备管理器COM口确认波特率一致串口输出乱码波特率不匹配、晶振频率不对统一115200检查系统时钟配置IIC读不到ACK接线错误、上拉电阻缺失、地址错误万用表量VCC检查SDA/SCL接法确认地址0x40温度/湿度恒为0延时不够、读取数据未等待转换完成确保延时至少50ms再读取数据有规律跳动未清除状态位、供电不稳raw 0xFFFC检查VCC稳定性和滤波电容数据偶尔错误一次时序边沿不稳定、EMC干扰缩短杜邦线尽量用短线连接加0.1uF电容5. 实用扩展与个人心得整套方案跑通之后扩展方向其实很多。如果想要实时性更好可以把采样周期缩短到200ms配合DMA传输串口数据CPU占用会进一步降低。想要低功耗可以在HTU21D测量完成之后通过GPIO控制传感器电源这样待机时可以把传感器供电完全切断适合做电池供电的无线温湿度节点。HTU21D内部还带一个加热器heater可以用0xE6寄存器的bit2开启用于在高湿环境下除雾虽然这个功能在普通项目里用不太上需要的话可以研究一下。如果想把数据做进一步处理可以考虑以下几点。第一温湿度数据做滑动平均滤波采样10次取平均再输出可以滤掉传感器本身的随机噪声数据曲线会平滑很多。第二传感器的校准HTU21D出厂时已经校准过但对于要求精度更高的应用场景可以用饱和盐溶液法在实验室里做多点的曲线校准把校准系数存在Flash里每次上电读出来套用到公式里。第三数据上传云端串口接一个ESP8266模块把温湿度数据通过MQTT协议发到云平台这就是一个完整的物联网环境监测节点了。我个人的体会是这种看似简单的传感器驱动项目其实把嵌入式开发的核心技能都串起来了芯片手册阅读能力需要从数据手册的时序图里提取关键参数、GPIO操作能力模拟时序本质就是精准的电平控制、外设配置能力串口开发是基本功、排查调试能力从现象定位到根因。把这些基础练扎实了以后不管遇到什么传感器无非就是根据数据手册重写驱动层的事情一通百通。建议新手朋友别急着抄代码就完事要对着时序图把代码一行一行读懂再用逻辑分析仪看看实际波形这一步的收获比跑通一万个demo都大。本文还有配套的精品资源点击获取