YAOTU INSIGHTS

STM32 F103C8T6驱动AHT10温湿度传感器:I2C采集实战与避坑指南

STM32 F103C8T6驱动AHT10温湿度传感器:I2C采集实战与避坑指南
简介STM32 F103C8T6与AHT10温湿度传感器集成是环境监测、智能家居类项目的常见场景这份学习笔记压缩包正是面向此类开发需求整理而成适合有一定C语言基础、正在入门STM32外设驱动并希望实现温湿度采集的开发者。资源以标准Keil工程形式呈现共227个文件源码与头文件可完整查阅I2C初始化、寄存器配置和命令序列编译中间文件、宏定义文件及链接脚本可辅助理解工程构建方式压缩包约6.12MB便于下载后直接导入开发环境。目前已有1104人学习下载内容经过实际验证。通过本篇配套资料读者能够掌握AHT10单次测量与连续测量两种模式的配置方法学会拼接和位转换原始数据得到温度湿度数值并了解I2C通信失败时的排查思路与传感器待机省电方案。资源中还包含工程构建与下载配置说明便于快速烧录到开发板验证效果切实缩短项目开发中的摸索时间。1. 把 AHT10 接到 STM32 F103C8T6 上I2C 温湿度采集的资源拆解做嵌入式温湿度采集时我最早用的是 DHT11后来换成 DHT22再到 AHT10才彻底告别了时序尺度和稳定性这两个老问题。AHT10 走的是 I2C 总线一次测量流程读完温湿度原始值换算公式也很直观但真正落地到 STM32 F103C8T6 上有几个细节容易让新手翻车I2C 的时钟速率不能按默认去调、上电后必须等 40ms 再发测量命令、状态字里的校准标志位不置位时算出的数据完全是错的。这篇笔记就拆这份 AHT10 学习资源把驱动代码、换算公式、踩过的坑和验证手段一并讲清适合正在调 I2C 传感器的嵌入式开发者参考也适合把 AHT10 当入门传感器练手的人。2. AHT10 的关键特性与选型理由为什么是 I2C 而不是单总线2.1 硬件接口与引脚定义两块最小系统板的接线对照AHT10 是一个四引脚贴片传感器引脚定义分别是 VCC、GND、SDA、SCL没有地址选择脚。对比 DHT11 的单总线AHT10 的 I2C 接口优势在于不需要严格的微秒级时序模拟时钟由主机控制从机不主动占用总线。在 STM32 F103C8T6 上I2C 硬件外设可以直接接管通信省去 DHT 系列那种 GPIO 翻转延时的心累。接线时需要注意的几点引脚STM32 F103C8T6 对应位置常见错误VCC3.3V 输出脚接到 5V 会永久损坏传感器GNDGND与系统共地不能浮空SDAPB7I2C1 默认或任意开漏引脚漏接上拉电阻通信不稳定SCLPB6I2C1 默认与 SDA 交叉接反输出全乱AHT10 模块上一般已经焊了 10K 上拉电阻但如果用的是裸芯片或者面包板接线需要在 SDA 和 SCL 上各加一个 4.7K 到 10K 的上拉电阻到 3.3V。STM32 的 GPIO 配置为开漏输出时内部没有上拉能力全靠外部电阻把总线拉高。我常看到有人直接把 PB6、PB7 配成推挽输出这样在设备地址应答阶段给出的 ACK 位会异常通信成功率大打折扣。2.2 I2C 地址与时钟速率两个影响初始化的隐形参数AHT10 的 7 位 I2C 地址是 0x38写操作地址是 0x70读操作地址是 0x71。看网上很多例程里直接写 0x70 或 0x71其实那是把读写位一起算进去了。在标准库的 I2C 驱动里如果用的是I2C_Send7bitAddress()这类函数传参传 0x38 就行读写方向由函数里的第二个参数控制如果用自己写的 GPIO 模拟 I2C则需要自己拼地址字节。官方数据手册标称 AHT10 的最大 SCL 时钟频率是 400kHz。实际在 F103C8T6 上很多人直接沿用 I2C 外设的默认配置274 分频后得到约 360kHz跑起来可能正常但线材稍长或者接触不良时就容易出现应答超时。稳妥起见我会把 SCL 时钟压到 100kHz 标准模式。在标准外设库中需要修改I2C_InitStructure.I2C_ClockSpeed 100000;并配合I2C_InitStructure.I2C_ClockDutyCycle I2C_DutyCycle_2;来获得更稳定的占空比。另一个容易忽略的是上电时序。AHT10 上电后到可以接收命令之间需要至少 40ms 的稳定时间数据手册里写的是两次上电之间间隔至少 5 秒。如果复位后立刻发测量命令芯片不回复 ACK代码会卡在等待应答的死循环里。初始化代码里应加一个延时函数实测用DHT11 采集间隔的教训可以照搬上电后Delay_Ms(50)再进入校准流程。3. AHT10 数据手册核心测量命令、状态字与原始数据格式3.1 触发测量命令0xAC 与参数 0x33 0x00 的组合含义AHT10 的测量流程分为三步发送测量命令、等待测量完成、读取 6 字节数据。测量命令是固定的三个字节0xAC、0x33、0x00。0xAC是触发测量的操作码0x33表示开启温度与湿度同时测量0x00作为保留位填充目前没有公开的其他组合参数。我在某开发者移植的驱动里见过0xA8的旧版命令那是 AHT20 的写法不能混用。发送完测量命令后从机需要一段时间完成测量。数据手册标注的典型测量时间是 80ms最坏情况在 150ms 左右。轮询状态字的方式更稳健读取设备状态寄存器检查第 7 位bit7是否为 0为 0 说明测量完成为 1 说明忙。如果用延时固定等待建议给 200ms 余量省得在低温高湿场景下数据没准备好就读取。uint8_t AHT10_StartMeasure(void) { uint8_t cmd[3] {0xAC, 0x33, 0x00}; // 发送测量命令到地址 0x38 if (I2C_WriteBytes(AHT10_ADDR, cmd, 3) ! 0) { return 1; // 发送失败检查总线状态 } // 等待测量完成超时 200ms uint16_t timeout 200; while ((AHT10_ReadStatus() 0x80) (timeout-- 0)) { Delay_Ms(1); } if (timeout 0) { return 2; // 测量超时传感器可能未校准 } return 0; }这段代码的逻辑是先把三个命令字节通过 I2C 总线写到从机然后循环读取状态寄存器。AHT10_ReadStatus()返回的字节里bit7 是忙标志位。0x80的掩码就是专门提取这个位的。如果 200ms 内 busy 位一直不释放直接返回错误码 2方便上层调用判断。参数里的timeout是可调的在低温环境下测量时间可能会变长可以改成 300 或 500但不建议无限等待否则 I2C 总线卡住时整个程序会死掉。3.2 状态寄存器与校准标志位上电后必须检查 bit3AHT10 的状态寄存器只有一个字节复位默认值是 0x18也就是二进制 0001 1000。其中第 4 位bit4和第 3 位bit3分别是校准使能和校准状态标志。上电后如果读到 bit3 为 0说明芯片内部没有校准数据此时测量的湿度值没有意义。数据手册给的处理方式是发送一条初始化命令0xE1、0x08、0x00然后等待 10ms 再检查。我踩过一个坑某次拿到一批拆机 AHT10上电后 bit3 一直是 0无论发多少次0xE1初始化都没反应。后来换了一颗全新的芯片就好了判断是拆机件内部校准数据区已经损坏。这个问题在测量端是没法通过软件修复的只能换传感器。在驱动里建议把初始化流程写完整先检查 bit3再决定是否发送初始化命令最后才进入测量循环。uint8_t AHT10_Init(void) { uint8_t status AHT10_ReadStatus(); if ((status 0x08) 0) // bit3 无校准数据 { uint8_t cmd[3] {0xE1, 0x08, 0x00}; if (I2C_WriteBytes(AHT10_ADDR, cmd, 3) ! 0) { return 1; } Delay_Ms(10); status AHT10_ReadStatus(); if ((status 0x08) 0) { return 2; // 校准失败 } } return 0; }这段初始化代码先读一次状态寄存器如果校准位已经为 1说明上次使用过的芯片可以直接进入测量如果为 0发送校准命令0xE1并等待 10ms再读状态。这里的0xE1命令字节后跟的两个参数0x08和0x00是固定格式分别表示开启校准和保留位。返回值 2 意味着芯片可能损坏或者供电不足这时不要继续测量直接在上位机提示错误日志。3.3 6 字节原始数据布局湿度 20 位、温度 20 位、状态位 4 位AHT10 一次测量完成后返回 6 个字节按顺序排列字节 0状态字bit7 是忙标志bit3 是校准标志低四位是保留位字节 1 到字节 2 的高 4 位湿度原始数据共 20 位高字节在前字节 2 的低 4 位加上字节 3 和字节 4温度原始数据共 20 位字节 5保留位读到什么值都不需要处理用图表描述就是字节索引bit7bit6..bit4bit3..bit00忙标志保留校准/保留1湿度 bit19..bit12湿度 bit11..bit4湿度 bit3..bit0 温度 bit19..bit162温度 bit15..bit8温度 bit7..bit0温度 bit19..bit163温度 bit15..bit8温度 bit7..bit0状态保留4温度 bit19..bit16温度 bit15..bit8温度 bit7..bit05保留这个十六位分布是新手最容易搞混的地方。很多人直接把 6 个字节塞进一个 uint8_t 数组里然后按temp_raw (buf[3] 12) | (buf[4] 4) | (buf[5] 4)去拼温度结果算出来的数完全不对。正确的拼接应该看数据手册的时序图湿度是字节 1 左移 12 位加字节 2 左移 4 位加字节 3 的高 4 位温度是字节 3 的低 4 位左移 16 位加字节 4 左移 8 位加字节 5。3.4 温湿度换算公式线性插值法与边界值处理拿到 20 位原始数据后换算公式是湿度 RH (hum_raw / 1048576) * 100温度 T (temp_raw / 1048576) * 200 - 50这里的分母 1048576 是 2 的 20 次方也就是 20 位 ADC 满量程值。AHT10 内部用的 ADC 是 20 位的湿度量程 0 到 100温度量程 -50 到 150。注意温度开口是 -50 到 150 摄氏度不是 -40 到 85虽然精度在极端区间内没有保证但换算公式本身会生成超出常规范围的值。换算在 C 语言里要注意溢出和浮点性能。F103C8T6 有 FPU 吗没有它只是 Cortex-M3没有硬件浮点单元所以大量使用 float 运算是消耗 CPU 的。在 72MHz 主频下换算一次温湿度大约耗时十几微秒不算严重但如果每秒采集 100 次以上建议用整数运算替代// 假设 hum_raw 和 temp_raw 是 uint32_t uint32_t hum_raw 的组合 (uint32_t)(buf[1] 12) | (buf[2] 4) | (buf[3] 4); uint32_t temp_raw (uint32_t)((buf[3] 0x0F) 16) | (buf[4] 8) | buf[5]; // 整数方式计算结果扩大 1000 倍避免浮点 uint32_t hum_permille hum_raw * 100000UL / 1048576UL; uint32_t temp_permille (temp_raw * 200000UL / 1048576UL) - 50000UL; // 实际温度 temp_permille / 1000 // 实际湿度 hum_permille / 1000这段代码的要点在于先用 32 位整数保存中间结果避免 16 位截断。100000UL后缀的 UL 是强制无符号长整型防止在 16 位编译器上被截断。hum_permille得到的是湿度乘以 1000 的整数结果比如 52680 表示 52.68%。temp_permille同理50000 表示 0 摄氏度的偏移。这样上位机打印时只需要处理一次小数点位。4. 基于标准外设库的 F103C8T6 工程搭建从 GPIO 模拟到硬件 I2C4.1 硬件 I2C 外设配置标准库结构体参数详解STM32 F103C8T6 有 I2C1 和 I2C2 两个外设引脚分布是 I2C1 在 PB6/PB7I2C2 在 PB10/PB11。AHT10 一般接在 I2C1 上。标准外设库初始化 I2C 的代码比较啰嗦但结构清晰void I2C_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; // 使能 GPIOB 和 I2C1 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // PB6: SCL, PB7: SDA 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); // I2C1 参数配置 I2C_InitTypeDef I2C_InitStructure; I2C_InitStructure.I2C_Mode I2C_Mode_I2C; I2C_InitStructure.I2C_ClockSpeed 100000; // 100kHz 标准模式 I2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_2; I2C_InitStructure.I2C_Ack I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_OwnAddress 0x00; I2C_Init(I2C1, I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); }这里的关键是把 GPIO 配置为复用开漏模式GPIO_Mode_AF_OD而不是推挽输出。开漏配合外部上拉电阻才能产生正确的 I2C 总线电平。时钟速度设成 100kHz 比默认 400kHz 更稳。I2C_DutyCycle_2表示低电平时间和高电平时间比例接近 1:1这个参数对线缆电容较大的场景更友好。I2C_OwnAddress随便填 0x00 就行因为 F103 只做主机不参与从机寻址。4.2 软件模拟 I2C 方案什么时候放弃硬件外设硬件 I2C 外设在 F103 上有一些历史遗留的坑比如总线错误后 BUSY 位不自动清除、NACK 处理需要手动干预。如果调试中发现硬件 I2C 死锁频率很高可以考虑切换到 GPIO 模拟 I2C。在 F103C8T6 上模拟 I2C 的代码量不大而且排错更直观每个时钟沿都能用示波器抓到。模拟 I2C 的核心是四个函数起始信号、停止信号、写一个字节、读一个字节。其中写入时需要接收从机的 ACK 响应这里最容易出错。很多初学者在模拟 I2C 里用了延时函数Delay_Us()导致整个时序被拖慢AHT10 的应答超时窗口很短实测在 72MHz 主频下每位的翻转时间不宜超过 10us。void I2C_Sim_Start(void) { SDA_GPIO_Output_Mode(); // SDA 设为输出 SDA_HIGH(); // 空闲状态 SDA 高 SCL_HIGH(); Delay_Us(2); SDA_LOW(); // 拉低 SDA 产生起始信号 Delay_Us(2); SCL_LOW(); // 占用总线 }起始信号的逻辑是在 SCL 高电平期间把 SDA 从高拉到低。这个时序必须严格保持如果 SDA 拉低后 Delay_Us 时间太短小于 1us从机可能识别不到。建议多个时钟周期都留至少 2us 余量。模拟 I2C 最大的好处是可以在 SCL 的每个上升沿插入调试打印硬件 I2C 就没这么方便。4.3 轮询式读函数与中断式读函数两种方式使用场景差异AHT10 的读取流程里等待测量完成的时长有 80ms这期间如果 MCU 死等会浪费大量 CPU 时间。在简单采集系统里死等没有关系但在带显示、通信、控制任务的系统里应该改成非阻塞方式。轮询方式实现简单每 200ms 采集一次调用测量函数循环等忙位释放。这种方式在 F103C8T6 跑 RTOS 或前后台系统里都可以工作但缺点是 CPU 占用率高。非阻塞方式是把测量命令发出去后立刻退出用定时器中断或系统 Tick 每 10ms 检查一次状态位状态位为 0 时再读取数据。// 非阻塞测量启动 void AHT10_TriggerNonBlocking(void) { uint8_t cmd[3] {0xAC, 0x33, 0x00}; I2C_WriteBytes(AHT10_ADDR, cmd, 3); measurement_pending 1; // 标记等待状态 } // 每隔 10ms 在中断或主循环里调用 uint8_t AHT10_PollNonBlocking(float *hum, float *temp) { if (measurement_pending 0) { return 0; // 没有等待中的测量 } uint8_t status AHT10_ReadStatus(); if (status 0x80) { return 1; // 忙继续等 } // 读取 6 字节数据 uint8_t buf[6]; I2C_ReadBytes(AHT10_ADDR, buf, 6); // 换算温湿度 // ...省略换算代码 measurement_pending 0; return 2; // 完成 }这里的measurement_pending变量可以定义成 volatile 全局避免编译器优化掉轮询循环。返回值设计成三态上层逻辑可以区分空闲、等待中和已完成方便拓展。5. 避坑与常见问题I2C 通信失败的五个典型排查点5.1 上拉电阻缺失导致的总线卡死现象代码执行到 I2C 读状态函数时一直返回超时SDA 和 SCL 用示波器测量是低电平或悬空乱跳。原因模块没有自带独立上拉电阻而 F103 的开漏输出模式下内部上拉能力很弱总线电平无法被可靠拉高。解决在 SDA 和 SCL 上分别接 4.7K 电阻到 3.3V。如果模块自带电阻确认焊接无误。另有一种隐藏情况是面包板接触不良用杜邦线连接时注意把线插到底J 型排针有时只接触一半。5.2 忙标志位一直被置位的初始化死循环现象启动后程序卡在while((status 0x80) timeout--)指示灯不闪。原因硬件 I2C 时序不对芯片没有正确识别测量命令状态字里 busy 位一直保持 1。解决先检查 SCL 时钟是否真的输出。用示波器或逻辑分析仪看电平如果只有起始信号没有数据位说明 GPIO 配置错了。对照GPIO_InitTypeDef的GPIO_Mode_AF_OD是否正确。另一个常见原因是芯片没焊接好引脚虚焊导致从机无应答。5.3 温度值偏大或偏小几十度的拼接错误现象显示的室温是 124 摄氏度或者湿度超过 200%。原因20 位原始数据的拼接方式搞错把字节 3 的高 4 位当成湿度的一部分算进了温度。数据手册上的位图布局容易看反。解决采样一批数据打印原始 6 字节十六进制然后手工按第三节的拼接公式算一次对比当前环境温湿度如果方向一致说明公式用对了如果差了一个固定倍数检查位移位方向。5.4 硬件 I2C 死锁后无法恢复现象程序跑了几十分钟后 I2C 通信卡死复位后恢复正常隔一阵又卡住。原因F103 硬件 I2C 在某些异常时序下会进入 BUSY 状态标准库的I2C_GetFlagStatus()查不到错误标志但总线已经被锁住。解决把硬件 I2C 的使能先关闭再打开重新初始化外设。我的习惯是在检测到超时后调用I2C_DeInit()加I2C_Init()重新配置或者干脆在项目里直接换用 GPIO 模拟 I2C稳定性和可排查性都更好。5.5 湿度读数突然跳到 0 的供电问题现象正常采集了几分钟湿度突然显示 0%之后一直保持 0%温度和状态字正常。原因电源纹波干扰导致 AHT10 内部 ADC 转换异常湿度通道数据溢出为 0。常见于用电池供电或者从电机的 PWM 电源上取电的场景。解决在 AHT10 的 VCC 和 GND 之间加一个 10uF 的电解电容和 100nF 的陶瓷电容靠近传感器引脚。如果电源线比较长用屏蔽线或者把电源走线加粗。6. 进阶验证与调试技巧逻辑分析仪校准与长时间采样稳定性拿到这份 AHT10 资源后别急着直接往产品里用先做两个验证动作。第一个是确认 I2C 时序波形是否符合数据手册第二个是长时间采样观察数据漂移。逻辑分析仪是排查 I2C 问题最好的工具24MHz 采样率的逻辑分析仪就能完整捕捉 400kHz 的 I2C 波形。把探针夹在 SCL 和 SDA 引脚上抓一段测量流程重点看两个位置测量命令发送后从机是否在第九个时钟沿拉低 SDA表示 ACK读取数据阶段是否有 NACK。如果软件时序有问题往往能在波形里直接发现数据位错误。长时间稳定性测试需要注意传感器自发热的问题。AHT10 本身发热量极小但如果紧挨着芯片或者 F103 的 LDO 附近热量传递会让温度读数偏高 1 到 2 度。我一般会把传感器放在 PCB 边缘留出空气流通的空间测试时记录初始温度和 2 小时后的稳定温度差值。如果差值大于 1.5 摄氏度就要重新考虑传感器布局。日志输出建议带时间戳用ms为单位的系统 Tick 配合串口打印格式类似[t123456ms] hum52.3 temp24.5。这样分析长时间漂移时可以直接用脚本处理不用人工翻串口助手的窗口。每 100 条记录算一次平均值和标准差如果标准差突然变大说明采样过程中出现过异常值你可以回头检查对应的原始字节。那段驱动代码里的换算因子1048576.0是固定常量但如果换用不同批次的 AHT10 芯片可以通过对比标准温湿度计来估算一个校准偏置。一个简单做法是把传感器和标准表放在同一个密闭空间里记录 20 组读数计算平均偏差然后在换算公式最后加一个offset修正量。这个偏置值不用写进驱动直接放在应用层处理方便不同批次单独标定。从那以后我每次调完 I2C 传感器都会强制走一遍逻辑分析仪抓波形、长跑 2 小时看漂移、再对比标准表校准这三步血泪经验告诉我跳过任何一步后面产线上都会补回来。希望帮到你。本文还有配套的精品资源点击获取