STM32驱动AHT10温湿度传感器:从I2C时序到代码实现

发布时间:2026/9/9 2:23:10
STM32驱动AHT10温湿度传感器:从I2C时序到代码实现 简介STM32 F103C8T6搭配AHT10温湿度传感器的嵌入式学习资料围绕I2C引脚配置、通信时序、数据解析的完整开发链路适合正在学习STM32 HAL库、具备基础C语言与单片机概念的开发者也可用于环境监测、智能家居等课设项目参考。压缩包为RAR格式共227个文件约6.12MB以Keil MDK工程为主体包含36个.c源码、38个.h头文件及.uvprojx等工程配置另含编译中间文件与备份目录结构清晰可直接用Keil编译下载至F103C8T6开发板。已有1101人学习/下载在入门级温湿度应用开发中具有较高参考价值。资源内容覆盖AHT10初始化命令、单次/连续测量模式配置、I2C读写时序、温湿度数值拼接换算与校验处理并提供HAL库函数调用示例及低功耗、异常处理思路读者既可对照笔记逐步复现实验也可直接提取驱动模块集成到自己的项目中快速实现精确环境监控。 从DHT11一路折腾到AHT10这个模块我必须要单独写一篇笔记。上一回用DHT11测温湿度单总线协议虽然简单但时序要求苛刻稍微有点干扰就出错而且精度实在一般。这次换AHT10I2C接口的温湿度一体传感器精度高、体积小、功耗还低关键是连STM32 F103C8T6这种入门级芯片都能轻松驱动对新手来说简直不要太友好。这篇笔记是系列第13篇直接把我调试AHT10的全过程扒开揉碎了讲。硬件的连接方式、I2C通信协议的关键细节、初始化流程为什么必须按照特定顺序来、以及我在实际测试中踩过的坑都会展开说明。如果你是正在学STM32的同学或者项目上刚好要用数字温湿度传感器这篇应该能帮你省下不少查手册、试错的时间。1. 为什么选AHT10而不是DHT11/SHT30先聊一个很实际的问题市面上温湿度传感器那么多为什么我偏偏在F103C8T6的例程里选了AHT10。其实最早我的开发板上用的是DHT11网上教程多代码满天飞看起来入门很轻松。但我实际用下来发现几个痛点一是DHT11的湿度精度只有正负5%RH温度精度正负2℃做环境监测勉强能用做精密点的项目就不够了二是单总线协议对时序要求太苛刻GPIO模拟时序稍微有点偏差就容易读回全1或者干脆卡死三是DHT11每次读取间隔要求至少1秒以上没法做快速的连续采样。SHT30也不错精度比AHT10更高某些指标上甚至更好但价格也更贵采购渠道相对少一些入门学习阶段没必要上来就用这么好的料。AHT10则取了一个比较中间的平衡点。它的湿度精度是正负2%RH温度精度正负0.3℃对比DHT11提升非常明显价格却在几块钱人民币这个量级性价比很突出。它走I2C接口只需要两根线SDA、SCL挂在STM32硬件I2C总线上就行通信时序由外设自己管理不用像DHT11那样用延时函数精打细算。工作电压范围1.8V到3.6V直接接到F103C8T6的3.3V供电上毫无压力。我拿AHT10跟手头的DHT11对比过一组数据在同样的环境下AHT10读回来的数值明显更稳定跳动幅度小得多。这里要单独强调一下AHT10的I2C地址是0x387位地址这在同类传感器里算比较特殊。常见的SHT20是0x40BMP280是0x76不少朋友会把AHT10当成0x40去驱动结果读回一堆0xFF然后排查了半天找不到原因。这块我后面还会再展开讲。围绕这颗芯片整个笔记分成几个重点硬件连接、I2C通信协议的关键寄存器、上电初始化的完整顺序、温湿度数据怎么从原始码换算成真实的物理量、以及我在代码层面踩过的坑。每一块我都会结合F103C8T6的具体资源和实操代码说明尽量做到拿过来就能用。2. 硬件连接与电路设计细节AHT10模块在市面上通常有两种形态一种是裸芯片加外围电路焊接在板子上引脚已经帮你处理好了另一种是单独的芯片需要自己搭电路。无论是哪种形态跟STM32 F103C8T6的接线逻辑都是一样的核心就是I2C的两根线加上电源和地。2.1 引脚连接与上拉电阻计算AHT10模块的标准接法是VCC接3.3V千万不要接5V芯片手册写的最大电压3.6VGND接GNDSCL接PB6I2C1时钟线SDA接PB7I2C1数据线我手头这块模块板上已经自带了4.7k上拉电阻所以直接接到F103C8T6的PB6和PB7上就可以正常工作。但如果你用的是裸芯片就一定要自己加上拉电阻。I2C协议是开漏结构没有上拉电阻的话总线根本拉不到高电平通信必失败。上拉电阻的阻值怎么选这里可以给一个简单的估算思路。I2C总线上的上升时间由RC决定C主要包括总线寄生电容一般PCB走线加芯片引脚每根线的寄生电容在10pF到20pF之间。400kHz快速模式要求上升时间不超过300ns100kHz标准模式要求不超过1000ns。拿400kHz模式估算如果总线电容按20pF算时间常数大概要控制在100ns左右这时候电阻取4.7k欧就是比较合适的。实际应用中3.3V系统用4.7k到10k都可以再大就可能导致上升沿太缓。ST的参考手册对I2C上拉也有说明但我实测下来4.7k在标准库的100kHz配置下非常稳定。2.2 电源去耦与布局建议AHT10对电源纹波虽然不像射频芯片那么敏感但温度测量本质上是对芯片内部热敏电阻阻值变化的量化供电不稳会导致ADC参考电位抖动最终反映为读数跳动。我通常在模块的VCC和GND之间靠近芯片引脚的位置放一个100nF的陶瓷电容如果手头有10uF的钽电容也可以并联一颗起低频滤波作用。这个操作成本极低但对测量稳定性帮助不小。布局上I2C的两根线尽量短不要和电机驱动线、继电器控制线这些强干扰源并行走线。我的开发板上PB6和PB7旁边恰好有一根PWM输出线调试时发现只要电机一转湿度读数就往下跳几个点把走线拉开后恢复正常。很多奇怪的数据波动问题根源都在布局和干扰上而不是芯片本身。2.3 关于模块供电电压的一点提醒AHT10手册标称工作电压范围是1.8V到3.6V但我建议在3.3V下使用。实际测试中发现在电压低于2.8V时芯片虽然能应答I2C通信但读数会出现一定的偏差尤其是湿度部分。原因也很好理解湿度传感器需要给内部的加热部件供电电压不足时加热功率上不去会让传感器进入非最佳工作状态。所以如果你的板子有多个电压输出优先用3.3V给AHT10供电不要为了省事拿去接在某个1.8V的LDO后面。3. AHT10数据手册关键信息解读AHT10的官方数据手册并不长但里面的信息密度很高有几处地方值得反复揣摩。我把手册里最重要的寄存器地址和命令字整理成了一张表方便以后查阅。寄存器/命令地址/字节功能说明初始化命令0xE1 0x08 0x00上电后必须执行进入正常测量模式触发测量命令0xAC 0x33 0x00启动一次温湿度测量测量完成后数据在数据寄存器中软复位命令0xBA复位传感器恢复到上电初始状态数据寄存器0x71读取6字节数据包含状态位、湿度20位、温度20位校准使能位Bit30x08在初始化命令的第二个字节中置1开启校准有一处新手容易误读的地方AHT10的数据手册里没有列出像普通I2C传感器那样的单字节寄存器地图而是用命令帧的方式来交互。比如你要触发测量就向0x38这个设备地址发起写操作依次发送0xAC、0x33、0x00三个字节这是一个完整的命令帧。命令的定义不是什么寄存器映射而是由固定的字节序列构成。这一点跟BMP280那种通过寄存器地址读写的方式不同初次接手的同学要注意。另外手册中提到AHT10的数据输出是无符号的原始ADC值。其中湿度占20位温度占20位数据帧一共6个字节组成结构是Byte0状态字Bit7为忙标志Bit3为校准标志Byte1~Byte2湿度数据高16位Byte3湿度低4位加上温度高4位Byte4~Byte5温度数据低16位转换成实际的相对湿度和温度需要套用下面的公式湿度百分比RH (hum_raw / 1048576.0) * 100%其中1048576等于2的20次方温度T (temp_raw / 1048576.0) * 200 - 50等一下这个公式我实际验证过确实是正确的。湿度的满量程是0到100%温度的范围则是-50℃到150℃其中200和-50这两个常数就是这样来的。手册里给的计算公式是T (temp_raw / 2^20) * 200 - 50网上有些抄来抄去的代码写的是乘以200再减去50没问题是同一回事。测量触发后的等待时间也需要说明。手册给了一个参考值触发测量后等待约80ms再读取数据就能保证数据有效。实际上芯片内部完成一次测量大约需要十几毫秒留80ms的余量是为了保险。但如果你的系统对响应速度有要求可以先用I2C读取状态字的Bit7来判断测量是否完成忙标志为0时再读取数据这样能把单次测量的周期压缩到20ms以内。4. 标准库驱动代码与关键实现解析F103C8T6的开发方式有很多种标准库、HAL库、LL库都有各自的拥趸。我个人偏向标准库原因是它的代码直接操作寄存器层面的封装看起来更透明方便理解芯片的实际工作过程。AHT10的驱动不复杂用STM32的硬件I2C外设写起来非常顺手。4.1 I2C初始化配置首先要把I2C1外设跑起来。F103C8T6的I2C1挂载在APB1总线上时钟频率默认36MHz所以I2C的时钟控制寄存器配置为36MHz除以100kHz再除以2就差不多是标准模式。void I2C1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); 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); RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, DISABLE); I2C_InitStructure.I2C_Mode I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_2; I2C_InitStructure.I2C_OwnAddress2 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); }这里有一个细节很多人会忽略GPIO模式要配置成GPIO_Mode_AF_OD也就是复用开漏输出。I2C协议要求总线支持线与功能所以无论是主机还是从机驱动总线时都应该是开漏结构。如果你配成了推挽复用输出短时间内可能也能通但一旦总线冲突就容易把芯片烧掉这个风险没必要冒。4.2 传感器初始化与校准流程AHT10上电后芯片默认处于休眠状态必须先发送初始化命令。这个命令帧的时序是起始位、I2C地址写位、0xE1、0x08、0x00、停止位。其中0x08就是校准使能的那个位数据手册明确说了出厂前芯片已经校准过但上电后需要由主机发送校准命令来激活内部校准数据。void AHT10_Init(void) { uint8_t initCmd[3] {0xE1, 0x08, 0x00}; while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)) ; I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) ; I2C_Send7bitAddress(I2C1, 0x38, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) ; for (int i 0; i 3; i) { I2C_SendData(I2C1, initCmd[i]); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) ; } I2C_GenerateSTOP(I2C1, ENABLE); delay_ms(10); // 手册要求初始化后等待至少10ms }上述代码中等待总线空闲是必要的。如果上一次通信异常终止总线可能还被拉低此时直接发START会被从机误解为数据造成通信混乱。所以在发起START之前我习惯先判断总线是否忙等它空闲了再走流程。这也是排错时百试不爽的一步。初始化完成后等待10ms数据手册里的建议数值是10ms这个时间是为了让芯片完成内部校准。如果等待太短后面读出来的湿度可能显示成一个固定的错值而且不会自愈只能重新初始化。4.3 单次温湿度读取的完整流程初始化完成后每次测量需要发送触发命令帧然后延时80ms再读取6字节数据。触发的命令帧是0xAC、0x33、0x00其中0xAC是触发命令本身0x33和0x00分别是测量模式相关的配置参数网上有些代码写的0xAC 0x08 0x00那是老的样例代码新批次芯片建议用0x33。读取端先发送设备地址加读位0x71然后连续接收6字节。注意前两字节接收完成后要回ACK最后一字节回NACK然后发停止位。如果用标准库的I2C_ReceiveData连续读配合I2C_AcknowledgeConfig在每个字节前设置ACK或NACK就能完整读出这6个字节。uint8_t AHT10_ReadData(uint8_t *buf, uint8_t len) { uint16_t timeout 0; I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) { if(timeout5000) return 1; } I2C_Send7bitAddress(I2C1, 0x38, I2C_Direction_Receiver); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)) { if(timeout5000) return 1; } for (int i 0; i len; i) { if (i len - 1) I2C_AcknowledgeConfig(I2C1, DISABLE); // 最后一字节回NACK while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)) { if(timeout5000) return 1; } buf[i] I2C_ReceiveData(I2C1); } I2C_AcknowledgeConfig(I2C1, ENABLE); I2C_GenerateSTOP(I2C1, ENABLE); return 0; }这里要留心的是标准库的I2C在接收模式下发送ACK/NACK的时机要提前一个字节设置。如果你在读完最后一个字节之后才去关闭ACK那等于这个字节还是带着ACK发出的主机就会收到一个异常。正确做法是循环里判断本字节如果是最后一字节在进入字节接收完成等待之前就把ACK关掉。这个顺序踩一次坑就能记住。4.4 原始数据换算成温湿度拿到6字节数据后先把它们组合成一个40位的原始值再做换算。我封装了一个结构体来存放换算结果这样主函数里调用起来思路更清晰。typedef struct { float temperature; float humidity; } AHT10_DataTypeDef; uint8_t AHT10_ReadTempHum(AHT10_DataTypeDef *data) { uint8_t buf[6] {0}; uint32_t hum_raw 0; uint32_t temp_raw 0; // 触发一次测量 uint8_t measCmd[3] {0xAC, 0x33, 0x00}; I2C_WriteBytes(0x38, measCmd, 3); delay_ms(80); if (AHT10_ReadData(buf, 6) ! 0) return 1; // 检查忙标志 if (buf[0] 0x80) return 2; hum_raw ((uint32_t)buf[1] 12) | ((uint32_t)buf[2] 4) | ((buf[3] 0xF0) 4); temp_raw (((uint32_t)(buf[3] 0x0F)) 16) | ((uint32_t)buf[4] 8) | buf[5]; style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />

相关新闻