1. 项目概述与环境准备

大家好,今天我来分享一个非常实用的STM32实战项目——使用HAL库驱动AHT20温湿度传感器。这个项目特别适合刚接触STM32和I2C通信的朋友,我会从最基础的硬件连接开始,一步步带你完成整个开发过程。

AHT20是一款高精度的温湿度传感器,采用I2C接口通信,测量温度范围-40℃~85℃,湿度范围0~100%RH。相比常见的DHT11,它的精度更高,稳定性更好,而且采用数字接口,抗干扰能力更强。在实际项目中,我经常用它来做环境监测,比如智能家居、农业大棚、仓库监控等场景。

需要准备的硬件设备:

  • STM32开发板(我用的STM32F103C8T6)
  • AHT20温湿度传感器模块
  • 杜邦线若干
  • USB转串口模块(用于调试输出)
  • ST-Link下载器(或其他调试器)

软件环境:

  • STM32CubeMX 6.0+
  • Keil MDK 5.3+
  • 串口调试助手(推荐使用波特律动或者Putty)

我第一次用AHT20的时候,发现它的精度确实比DHT11好很多,温度误差能控制在±0.3℃以内,湿度误差±2%RH。不过要注意的是,AHT20需要正确的初始化流程,否则读数会不准,这个后面我会详细说明。

2. 硬件连接与I2C原理

2.1 硬件连接详解

先来看看硬件怎么连接。AHT20模块一般有4个引脚:VCC、GND、SDA、SCL。接线非常简单:

AHT20引脚STM32引脚
VCC3.3V
GNDGND
SDAPB7
SCLPB6

这里我选择PB6和PB7作为I2C引脚,是因为这两个引脚在STM32F103C8T6上正好是I2C1的备用引脚。在实际焊接时,建议使用4.7K的上拉电阻将SDA和SCL线拉到3.3V,这样能保证信号稳定性。不过很多AHT20模块已经内置了上拉电阻,所以直接连接也可以。

注意: 一定要确保VCC接3.3V,不要接到5V!AHT20是3.3V器件,接5V会烧坏传感器。我之前就犯过这个错误,损失了一个传感器。

2.2 I2C通信原理浅析

I2C是一种同步、半双工的串行通信协议,只需要两根线:SDA(数据线)和SCL(时钟线)。它支持多主多从的架构,每个设备都有唯一的地址,AHT20的默认地址是0x38(7位地址),换算成8位写地址是0x70,读地址是0x71。

I2C的通信过程是这样的:主机(STM32)先发出起始条件,然后发送从机地址和读写位,等待从机应答,之后开始数据传输,每传输一个字节都要等待应答,最后发出停止条件结束通信。

与软件模拟I2C相比,硬件I2C的效率更高,占用CPU资源少,而且稳定性更好。STM32的硬件I2C外设可以自动处理时序,我们只需要配置好参数,调用HAL库函数就可以了。

3. CubeMX工程配置

3.1 引脚配置

打开STM32CubeMX,新建工程,选择你的STM32型号。首先配置时钟树,根据你的晶振频率设置系统时钟,我一般喜欢调到72MHz,这样性能足够用。

在Pinout & Configuration标签页中,找到I2C1,选择I2C模式。将SDA设置为PB7,SCL设置为PB6。模式选择Standard Mode,速度用默认的100kHz就可以,AHT20最高支持400kHz,但100kHz更稳定。

接下来配置串口,我们需要用串口输出温湿度数据。找到USART1(或其他可用的串口),模式选择Asynchronous,波特率设为115200,数据位8位,无校验,停止位1位。

3.2 工程生成设置

在Project Manager页面,设置工程名称和路径,Toolchain选择MDK-ARM。在Code Generator页面,勾选"Generate peripheral initialization as a pair of .c/.h files per peripheral",这样每个外设的初始化代码都会生成单独的文件,方便管理。

还有一个重要的设置:在Project Properties -> C/C++ Build -> Settings -> MCU Settings中,勾选"Use float with printf (-u _printf_float)",这样我们才能在printf中正常输出浮点数。

点击Generate Code生成工程,用Keil打开。第一次生成工程时,我建议先编译一下,确保没有错误。

4. 代码实现详解

4.1 AHT20驱动代码

新建两个文件:aht20.h和aht20.c,添加到工程中。先来看头文件aht20.h:

#ifndef __AHT20_H__
#define __AHT20_H__

#include "main.h"
#include "i2c.h"

#define AHT20_ADDRESS 0x70  // 7位地址左移一位

void AHT20_Init(void);
void AHT20_Read(float *temperature, float *humidity);

#endif

接下来是aht20.c的实现,这是最核心的部分:

#include "aht20.h"
#include "stdio.h"

void AHT20_Init(void)
{
    uint8_t cmd[3] = {0xBE, 0x08, 0x00};  // 初始化命令
    uint8_t status = 0;
    
    // 读取状态字
    HAL_I2C_Master_Receive(&hi2c1, AHT20_ADDRESS, &status, 1, 100);
    
    // 检查传感器是否已经初始化
    if ((status & 0x08) == 0) {
        // 发送初始化命令
        HAL_I2C_Master_Transmit(&hi2c1, AHT20_ADDRESS, cmd, 3, 100);
        HAL_Delay(10);  // 等待初始化完成
    }
}

初始化函数中,我们先读取AHT20的状态字,检查第3位是否为1(表示已经初始化)。如果没有初始化,就发送0xBE命令进行初始化。这里有个坑要注意:发送初始化命令后需要延时10ms左右,等待传感器完成初始化。

4.2 数据读取函数

数据读取函数稍微复杂一些,需要处理数据转换:

void AHT20_Read(float *temperature, float *humidity)
{
    uint8_t cmd[3] = {0xAC, 0x33, 0x00};  // 触发测量命令
    uint8_t data[6] = {0};
    
    // 发送测量命令
    HAL_I2C_Master_Transmit(&hi2c1, AHT20_ADDRESS, cmd, 3, 100);
    HAL_Delay(75);  // 等待测量完成,至少75ms
    
    // 读取6字节数据
    HAL_I2C_Master_Receive(&hi2c1, AHT20_ADDRESS, data, 6, 100);
    
    // 检查状态位
    if ((data[0] & 0x80) == 0) {
        // 湿度计算
        uint32_t hum_raw = ((uint32_t)data[1] << 12) | 
                          ((uint32_t)data[2] << 4) | 
                          ((uint32_t)data[3] >> 4);
        *humidity = (float)hum_raw * 100.0f / (1 << 20);
        
        // 温度计算
        uint32_t temp_raw = (((uint32_t)data[3] & 0x0F) << 16) | 
                           ((uint32_t)data[4] << 8) | 
                           (uint32_t)data[5];
        *temperature = (float)temp_raw * 200.0f / (1 << 20) - 50.0f;
    } else {
        // 读取失败
        *temperature = 0;
        *humidity = 0;
    }
}

这个函数先发送测量命令0xAC,然后等待75ms让传感器完成测量。读取到的6字节数据中,前4位是状态字,后面20位是湿度数据,再后面20位是温度数据。需要经过一系列位移和转换才能得到真实的温湿度值。

我第一次写这个函数的时候,位移操作老是搞错,导致读数不对。后来仔细看了数据手册才明白,AHT20的数据格式是20位的原始数据,需要按公式转换成实际值。

5. 主程序与串口输出

5.1 main函数实现

在主函数中,我们需要初始化外设,然后循环读取温湿度数据:

#include "main.h"
#include "i2c.h"
#include "usart.h"
#include "aht20.h"
#include "stdio.h"

// 重定义printf,方便串口输出
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif

PUTCHAR_PROTOTYPE
{
    HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_I2C1_Init();
    MX_USART2_UART_Init();
    
    AHT20_Init();  // 初始化AHT20
    
    float temperature, humidity;
    char buffer[50];
    
    while (1) {
        AHT20_Read(&temperature, &humidity);
        
        // 格式化输出
        sprintf(buffer, "温度: %.1f ℃, 湿度: %.1f %%\r\n", temperature, humidity);
        HAL_UART_Transmit(&huart2, (uint8_t*)buffer, strlen(buffer), HAL_MAX_DELAY);
        
        HAL_Delay(2000);  // 每2秒读取一次
    }
}

这里我重定义了printf函数,这样可以直接用printf通过串口输出数据,比用HAL_UART_Transmit方便很多。每2秒读取一次温湿度数据,这个间隔可以根据需要调整,但最好不要小于1秒,因为AHT20需要时间完成测量。

5.2 数据处理与优化

在实际使用中,我发现AHT20的读数偶尔会有跳动,可以通过软件滤波来平滑数据:

#define SAMPLE_SIZE 5

float filter_data(float *values, uint8_t count)
{
    float sum = 0;
    for (uint8_t i = 0; i < count; i++) {
        sum += values[i];
    }
    return sum / count;
}

// 在主循环中使用
float temp_samples[SAMPLE_SIZE];
float hum_samples[SAMPLE_SIZE];
uint8_t sample_index = 0;

while (1) {
    AHT20_Read(&temperature, &humidity);
    
    temp_samples[sample_index] = temperature;
    hum_samples[sample_index] = humidity;
    sample_index = (sample_index + 1) % SAMPLE_SIZE;
    
    float avg_temp = filter_data(temp_samples, SAMPLE_SIZE);
    float avg_hum = filter_data(hum_samples, SAMPLE_SIZE);
    
    // 输出平滑后的数据
    sprintf(buffer, "温度: %.1f ℃, 湿度: %.1f %%\r\n", avg_temp, avg_hum);
    HAL_UART_Transmit(&huart2, (uint8_t*)buffer, strlen(buffer), HAL_MAX_DELAY);
    
    HAL_Delay(1000);
}

这种简单的移动平均滤波能有效减少读数跳动,使数据显示更稳定。在我的实际测试中,滤波后的数据波动明显减小,更适合长期监测应用。

6. 常见问题与调试技巧

6.1 I2C通信失败排查

I2C通信最容易出问题,下面是我总结的排查步骤:

  1. 检查硬件连接:用万用表测量SDA和SCL线的电压,空闲时应该是高电平(3.3V)。如果电压偏低,可能是上拉电阻太大或者漏接了上拉电阻。

  2. 检查地址是否正确:AHT20的地址是0x38(7位),但HAL库要求左移一位,所以写地址是0x70,读地址是0x71。可以用I2C扫描程序检查设备是否在线。

  3. 时序问题:AHT20的测量需要时间,发送测量命令后必须等待至少75ms才能读取数据。如果等待时间不够,读到的会是无效数据。

  4. 电源噪声:AHT20对电源噪声比较敏感,建议在VCC和GND之间加一个100nF的滤波电容。

我遇到过最奇怪的问题是读数偶尔为0,后来发现是电源噪声导致的,加上滤波电容后就稳定了。

6.2 数据精度优化

要提高测量精度,可以注意以下几点:

  1. 避免传感器自加热:尽量降低采样频率,不要过于频繁地读取数据。AHT20在测量时会产生少量热量,频繁测量会导致温度读数偏高。

  2. 正确安装位置:不要让传感器直接暴露在阳光下或靠近热源,避免空气不流通的死角。

  3. 校准:虽然AHT20出厂时已经校准,但如果对精度要求极高,可以用标准温湿度计进行对比,然后在代码中加入校准偏移量。

在我的项目中,通常温度精度能到±0.5℃,湿度±3%以内,完全满足大多数应用需求。

7. 项目扩展与应用

7.1 添加OLED显示

除了串口输出,还可以添加OLED显示屏实时显示温湿度。SSD1306 OLED屏也是I2C接口,可以和AHT20共用I2C总线:

// 初始化OLED
OLED_Init();

// 在主循环中显示数据
char temp_str[10], hum_str[10];
sprintf(temp_str, "%.1fC", temperature);
sprintf(hum_str, "%.1f%%", humidity);

OLED_ShowString(0, 0, "Temperature:", 16);
OLED_ShowString(0, 16, temp_str, 16);
OLED_ShowString(0, 32, "Humidity:", 16);
OLED_ShowString(0, 48, hum_str, 16);

这样就能在OLED屏上实时显示温湿度了,做成一个独立的温湿度监测仪。

7.2 数据上传与远程监控

还可以通过ESP8266 WiFi模块将数据上传到云平台,实现远程监控:

// 连接WiFi
ESP8266_Connect("SSID", "password");

// 上传数据到云平台
sprintf(post_data, "temp=%.1f&hum=%.1f", temperature, humidity);
ESP8266_HTTPPost("api.thingspeak.com", "80", "/update", post_data);

我做过一个农业大棚监控系统,就是用STM32+AHT20采集数据,通过ESP8266上传到云平台,农民在手机上就能随时查看大棚内的温湿度。

7.3 报警功能

添加报警功能,当温湿度超过设定范围时触发报警:

#define TEMP_MAX 30.0
#define TEMP_MIN 10.0
#define HUM_MAX 80.0
#define HUM_MIN 20.0

if (temperature > TEMP_MAX || temperature < TEMP_MIN) {
    // 温度异常,触发报警
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);  // 点亮LED
    sprintf(alarm_msg, "温度异常: %.1f ℃", temperature);
    HAL_UART_Transmit(&huart2, (uint8_t*)alarm_msg, strlen(alarm_msg), HAL_MAX_DELAY);
}

if (humidity > HUM_MAX || humidity < HUM_MIN) {
    // 湿度异常,触发报警
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET);  // 点亮另一个LED
    sprintf(alarm_msg, "湿度异常: %.1f %%", humidity);
    HAL_UART_Transmit(&huart2, (uint8_t*)alarm_msg, strlen(alarm_msg), HAL_MAX_DELAY);
}

这个功能在仓库监控、实验室等场景非常有用,可以及时发现环境异常。

在实际项目中,我发现AHT20的长期稳定性很好,连续运行几个月读数都不会漂移。不过要注意防尘防潮,灰尘积累会影响湿度测量的准确性。

更多推荐