STM32驱动AHT10温湿度传感器:从I2C时序到代码实现
2026/9/9 2:23:07 网站建设 项目流程

简介: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测温湿度,单总线协议虽然简单,但时序要求苛刻,稍微有点干扰就出错,而且精度实在一般。这次换AHT10,I2C接口的温湿度一体传感器,精度高、体积小、功耗还低,关键是连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地址是0x38(7位地址),这在同类传感器里算比较特殊。常见的SHT20是0x40,BMP280是0x76,不少朋友会把AHT10当成0x40去驱动,结果读回一堆0xFF,然后排查了半天找不到原因。这块我后面还会再展开讲。

围绕这颗芯片,整个笔记分成几个重点:硬件连接、I2C通信协议的关键寄存器、上电初始化的完整顺序、温湿度数据怎么从原始码换算成真实的物理量、以及我在代码层面踩过的坑。每一块我都会结合F103C8T6的具体资源和实操代码说明,尽量做到拿过来就能用。

2. 硬件连接与电路设计细节

AHT10模块在市面上通常有两种形态:一种是裸芯片加外围电路焊接在板子上,引脚已经帮你处理好了;另一种是单独的芯片,需要自己搭电路。无论是哪种形态,跟STM32 F103C8T6的接线逻辑都是一样的,核心就是I2C的两根线加上电源和地。

2.1 引脚连接与上拉电阻计算

AHT10模块的标准接法是:

  • VCC接3.3V(千万不要接5V,芯片手册写的最大电压3.6V)
  • GND接GND
  • SCL接PB6(I2C1时钟线)
  • SDA接PB7(I2C1数据线)

我手头这块模块板上已经自带了4.7k上拉电阻,所以直接接到F103C8T6的PB6和PB7上就可以正常工作。但如果你用的是裸芯片,就一定要自己加上拉电阻。I2C协议是开漏结构,没有上拉电阻的话,总线根本拉不到高电平,通信必失败。

上拉电阻的阻值怎么选?这里可以给一个简单的估算思路。I2C总线上的上升时间由RC决定,C主要包括总线寄生电容,一般PCB走线加芯片引脚,每根线的寄生电容在10pF到20pF之间。400kHz快速模式要求上升时间不超过300ns,100kHz标准模式要求不超过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位
校准使能位Bit3(0x08)在初始化命令的第二个字节中,置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(++timeout>5000) return 1; } I2C_Send7bitAddress(I2C1, 0x38, I2C_Direction_Receiver); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)) { if(++timeout>5000) 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(++timeout>5000) 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;" />

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询