第十章:温湿度传感器(AHT20)从设备程序设计

10.1 项目概述

本章设计一个温湿度传感器从设备,硬件资源包括:

  • AHT20温湿度传感器(I2C接口)

  • 2个有源蜂鸣器(BEEP1、BEEP2):高电平发声

  • 3个LED(LED1~LED3):低电平点亮

设备通过RS485以Modbus RTU协议与主站通信,从站地址设为03H。温湿度数据通过AHT20传感器采集,因测量一次需至少80ms,不能在Modbus接收任务中直接读取,故创建独立任务定期更新温湿度值,Modbus任务只需从全局变量获取最新值。

10.2 硬件电路与引脚定义

10.2.1 AHT20连接

根据原理图,AHT20通过I2C接口连接:

  • SCL → PB6(I2C1_SCL)

  • SDA → PB7(I2C1_SDA)

  • VDD → 3.3V

  • GND → GND

AHT20的I2C设备地址为0x70(写地址)或0x71(读地址),7位地址为0x38,加上读写位后分别为0x700x71

10.2.2 输出控制引脚
功能 引脚 电平逻辑 Modbus类型
BEEP1 PB15 高电平发声 DO
BEEP2 PB14 高电平发声 DO
LED1 PB11 低电平发光 DO
LED2 PB12 低电平发光 DO
LED3 PB13 低电平发光 DO

10.3 Modbus寄存器地址分配

设备地址:03H

寄存器地址 类别 用途 描述
0000H DO 控制蜂鸣器1 1-响
0001H DO 控制蜂鸣器2 1-响
0002H DO 控制LED1 1-亮
0003H DO 控制LED2 1-亮
0004H DO 控制LED3 1-亮
0000H AI 读取温度 单位0.1°C,16位有符号整数
0001H AI 读取湿度 单位0.1%RH,16位无符号整数

10.4 AHT20传感器操作详解

10.4.1 AHT20通信时序(参考图片)

AHT20使用I2C接口,操作步骤如下:

  1. 上电等待:VDD上电后需等待至少5ms,使传感器稳定。

  2. 发送测量命令 :主机发送写命令0x70,然后发送3字节命令0xAC 0x33 0x00

  3. 等待测量完成:传感器测量需至少80ms(典型值)。

  4. 读取数据 :主机发送读命令0x71,读取7字节数据:

    • 字节0:状态字(Status)

    • 字节1-2:湿度数据高16位(SRH[19:4])

    • 字节3:湿度低4位+温度高4位

    • 字节4-5:温度数据中低16位(ST[15:0])

    • 字节6:CRC校验字节(对字节0~5计算CRC8)

  5. CRC校验 :使用多项式x^8 + x^5 + x^4 + 1(即0x31)计算CRC8,与接收的CRC字节比较,验证数据完整性。

  6. 数据转换:将20位原始湿度数据和20位原始温度数据转换为实际物理量。

10.4.2 数据格式解析(参考图片)

AHT20返回的7字节数据排列如下:

字节索引 内容 说明
0 Status 状态字(通常为0x1C表示正常)
1 Humidity[19:12] 湿度高8位
2 Humidity[11:4] 湿度中8位
3 Humidity[3:0] + Temperature[19:16] 湿度低4位(高4位)+温度高4位(低4位)
4 Temperature[15:8] 温度中8位
5 Temperature[7:0] 温度低8位
6 CRC 对字节0~5的CRC8校验值

从这6字节中提取20位湿度原始值S_RH和20位温度原始值S_T

cs 复制代码
S_RH = (datas[1] << 12) | (datas[2] << 4) | (datas[3] >> 4)
S_T  = ((datas[3] & 0x0F) << 16) | (datas[4] << 8) | datas[5]
10.4.3 物理量转换公式

根据AHT20数据手册:

  • 相对湿度RH[%] = (S_RH / 2^20) * 100%

  • 温度T[°C] = (S_T / 2^20) * 200 - 50

在程序中,我们需要将结果转换为指定的单位(温度0.1°C,湿度0.1%),因此:

  • 温度(0.1°C):temp = (S_T * 200 * 10) / 0x100000 - 500

    • 解释:S_T / 2^20 * 200 得到摄氏度,再乘以10得到0.1°C单位,减去50*10=500。
  • 湿度(0.1%):humi = (S_RH * 100 * 10) / 0x100000

    • 解释:S_RH / 2^20 * 100 得到百分比,乘以10得到0.1%单位。
10.4.4 CRC校验函数

CRC8计算函数如下,多项式0x31,初始值0xFF:

cs 复制代码
unsigned char Calc_CRC8(unsigned char *message, unsigned char Num)
{
    unsigned char i;
    unsigned char byte;
    unsigned char crc = 0xFF;
    for (byte = 0; byte < Num; byte++)
    {
        crc ^= (message[byte]);
        for (i = 8; i > 0; --i)
        {
            if (crc & 0x80)
                crc = (crc << 1) ^ 0x31;
            else
                crc = (crc << 1);
        }
    }
    return crc;
}

10.5 程序结构设计

由于AHT20测量一次需80ms,如果在Modbus接收任务中直接读取,会严重阻塞通信(Modbus超时通常较短)。因此采用双任务设计:

  • AHT20任务 :独立任务,循环读取传感器,更新全局变量g_tempg_humi

  • Modbus任务:收到请求后,直接从全局变量获取最新温湿度值,填入输入寄存器,快速回复。

10.6 代码逐行解析

10.6.1 宏定义与全局变量
cs 复制代码
#ifdef USE_TMP_HUMI_SENSOR
void AHT20Task(void *argument);
static void aht20_get_datas(uint16_t *temp, uint16_t *humi);

#define SLAVE_ADDR 3
#define NB_BITS             5
#define NB_INPUT_BITS       0 
#define NB_REGISTERS        0
#define NB_INPUT_REGISTERS  2  

static uint32_t g_temp, g_humi;  // 存储最新温湿度原始值(已转换单位)
#endif
  • g_tempg_humi为全局变量,供Modbus任务读取。

  • aht20_get_datas函数用于安全获取这两个值(可能用临界区保护,但此处简单直接读取)。

10.6.2 CRC计算函数

已在10.4.4给出。

10.6.3 AHT20任务函数
cs 复制代码
void AHT20Task(void *argument)
{
    uint8_t cmd[] = { 0xAC, 0x33, 0x00};   // 测量命令
    uint8_t datas[7];                       // 接收数据缓冲区
    uint8_t crc;
    extern I2C_HandleTypeDef hi2c1;          // 引用HAL库的I2C句柄

    vTaskDelay(10); /* 等待传感器上电稳定 */

    while (1)
    {
        // 1. 发送测量命令
        if (HAL_OK == HAL_I2C_Master_Transmit(&hi2c1, 0x70, cmd, 3, 100))
        {
            // 2. 等待测量完成(至少80ms)
            vTaskDelay(100);   // 延时100ms,确保测量完成

            // 3. 读取7字节数据
            if (HAL_OK == HAL_I2C_Master_Receive(&hi2c1, 0x70, datas, 7, 100))
            {
                // 4. CRC校验
                crc = Calc_CRC8(datas, 6);   // 对前6字节计算CRC
                if (crc == datas[6])          // 与接收的第7字节比较
                {
                    // 5. 数据解析
                    // 湿度原始值:datas[1]高8位,datas[2]中8位,datas[3]高4位为湿度低4位
                    g_humi = ((uint32_t)datas[1] << 12) | ((uint32_t)datas[2] << 4) | ((uint32_t)datas[3] >> 4);
                    // 温度原始值:datas[3]低4位为温度高4位,datas[4]中8位,datas[5]低8位
                    g_temp = (((uint32_t)datas[3] & 0x0F) << 16) | ((uint32_t)datas[4] << 8) | ((uint32_t)datas[5]);

                    // 6. 转换为实际单位
                    // 温度:原始值/2^20 * 200 - 50,再乘以10得到0.1°C
                    g_humi = g_humi * 100 * 10 / 0x100000;   // 0.1%
                    g_temp = g_temp * 200 * 10 / 0x100000 - 500; // 0.1°C
                }
            }
        }

        // 7. 延时20ms再进行下一次测量(可根据需要调整)
        vTaskDelay(20);
    }
}

逐行解释

  • HAL_I2C_Master_Transmit(&hi2c1, 0x70, cmd, 3, 100):发送3字节命令到设备地址0x70(写操作)。0x70是写地址(7位地址0x38左移1位,最低位0)。

  • 发送成功后,延时100ms(大于80ms,保证测量完成)。

  • HAL_I2C_Master_Receive(&hi2c1, 0x70, datas, 7, 100):从设备地址0x70读取7字节数据(注意读地址应为0x71,但HAL库函数会根据地址最低位自动处理,所以此处仍用0x70?实际上HAL库中DevAddress是7位地址左移1位,所以0x70对应写,0x71对应读。这里用0x70作为读地址是错误的,应该用0x71。可能是代码笔误,正确应为0x71。讲解时需指出。)

  • Calc_CRC8(datas, 6):计算前6字节的CRC8,与接收的第7字节比较,确保数据无误。

  • 数据拼接:

    • g_humi = ((uint32_t)datas[1] << 12) | ((uint32_t)datas[2] << 4) | ((uint32_t)datas[3] >> 4);

      • datas[1]是湿度高8位,左移12位到20位的高位。

      • datas[2]是湿度中8位,左移4位到中间。

      • datas[3]的高4位是湿度低4位,右移4位后放到最低4位。

    • g_temp = (((uint32_t)datas[3] & 0x0F) << 16) | ((uint32_t)datas[4] << 8) | ((uint32_t)datas[5]);

      • datas[3]的低4位是温度高4位,先掩码取出,左移16位。

      • datas[4]是温度中8位,左移8位。

      • datas[5]是温度低8位。

  • 单位转换:

    • g_humi = g_humi * 100 * 10 / 0x100000; 先乘1000,再除以2^20,结果就是0.1%单位。

    • g_temp = g_temp * 200 * 10 / 0x100000 - 500; 先乘2000,除以2^20,再减去500(因为减去50*10)。

  • 最后延时20ms,循环下一次测量。注意两次测量间隔应至少100ms(80ms测量+20ms延时),但这里实际间隔约为100+20=120ms,合理。

10.6.4 Modbus任务中的处理

在Modbus任务中,需要创建AHT20任务,并在收到请求后获取温湿度值填入输入寄存器。

创建AHT20任务 (通常在MX_FREERTOS_Init或任务启动前):

cs 复制代码
osThreadNew(AHT20Task, NULL, &aht20_task_attributes);

在Modbus任务循环中

cs 复制代码
#ifdef USE_TMP_HUMI_SENSOR
    // 获取最新温湿度值
    uint16_t temp, humi;
    aht20_get_datas(&temp, &humi);
    mb_mapping->tab_input_registers[0] = temp;  // 温度存入输入寄存器0
    mb_mapping->tab_input_registers[1] = humi;  // 湿度存入输入寄存器1
#endif

// 回复请求 rc = modbus_reply(ctx, query, rc, mb_mapping);

aht20_get_datas函数简单读取全局变量:

cs 复制代码
static void aht20_get_datas(uint16_t *temp, uint16_t *humi)
{
    *temp = (uint16_t)g_temp;  // 假设温度在int16范围内
    *humi = (uint16_t)g_humi;
}

注意:g_tempg_humiuint32_t,但实际值可能超出16位范围?根据公式,温度范围-50~150°C,0.1°C单位下为-500~1500,适合int16;湿度0~1000,适合uint16。所以强转安全。

10.6.5 输出控制部分

与第九章相同,根据tab_bits控制蜂鸣器和LED:

cs 复制代码
#ifdef USE_TMP_HUMI_SENSOR
    /* beep1 */
    if (mb_mapping->tab_bits[0])
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_SET);
    else
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_RESET);
    // ... 类似处理其他
#endif

10.7 关键难点解析

10.7.1 为什么不能直接在Modbus任务中读取AHT20?
  • AHT20测量需要80ms等待,而Modbus通信通常有超时限制(如响应超时可能设为1秒,但如果在任务中阻塞80ms,会影响其他请求的及时处理,且若多个请求连续到来,可能导致响应超时。更重要的是,在单任务循环中,阻塞80ms会使整个任务停滞,无法接收其他请求。因此必须用独立任务。
10.7.2 I2C地址问题
  • AHT20的7位地址是0x38(二进制0111000)。写操作为0x70(0x38<<1 | 0),读操作为0x71(0x38<<1 | 1)。在HAL库函数中,DevAddress参数通常要求传入8位地址(即包含读写位)。因此发送命令时应使用0x70,接收数据时应使用0x71。代码中接收时用了0x70,可能是笔误,但有些HAL库版本会自动处理?通常应纠正为0x71
10.7.3 数据拼接与移位运算
  • 湿度数据20位分布在3个字节中,需要仔细理解每个字节的位分布。通过位运算正确提取。

  • 温度数据同理。注意datas[3]同时包含湿度低4位和温度高4位,所以需要掩码分离。

10.7.4 CRC校验的必要性
  • I2C通信可能受干扰,CRC校验能确保数据正确,防止错误值被用于控制。如果CRC失败,应丢弃本次数据,保留上次有效值。
10.7.5 单位转换的数学推导
  • 公式 RH = S_RH / 2^20 * 100,乘以10得0.1%单位:RH_0_1 = S_RH * 1000 / 2^20

  • 温度公式 T = S_T / 2^20 * 200 - 50,乘以10得0.1°C:T_0_1 = S_T * 2000 / 2^20 - 500

  • 注意整数运算顺序,先乘后除避免精度损失,但需防止溢出。这里S_T最大约2^20-1≈1e6,乘以2000≈2e9,在32位范围内,安全。

10.8 调试与验证

  1. 验证I2C通信:使用逻辑分析仪抓取I2C波形,确认发送的命令和接收的数据是否正确。

  2. 验证CRC:手动计算CRC8与接收值比较。

  3. 验证温湿度值:用串口打印转换后的值,与实际情况对比(如对着传感器呼气湿度应上升)。

  4. Modbus通信测试:用Modbus Poll读取输入寄存器地址0和1,观察数值是否合理变化。

10.9 本章核心知识点总结

  1. AHT20操作三步曲:发送测量命令→等待80ms→读取数据+CRC校验。

  2. 独立任务的必要性:避免长时间阻塞影响Modbus通信。

  3. 数据解析:从6字节中提取20位原始值,注意位分布和移位操作。

  4. CRC8校验:多项式0x31,初始值0xFF,确保数据完整性。

  5. 物理量转换:根据数据手册公式转换为实际单位,注意整数运算顺序。

  6. 共享数据保护:全局变量在多任务间共享,需考虑原子性(此处值较小,无保护也可,但严谨应用应加临界区或互斥信号量)。

  7. Modbus映射更新:在收到请求时从全局变量获取最新值填入输入寄存器。

相关推荐
Hello_Embed5 小时前
Modbus 传感器开发:STM32F030 串口编程
笔记·stm32·单片机·嵌入式·freertos·modbus
Hello_Embed3 天前
libmodbus STM32 板载串口实验(双串口主从通信)
笔记·stm32·单片机·学习·modbus
Hello_Embed4 天前
libmodbus STM32 移植(板载 485 串口作后端)
笔记·stm32·学习·嵌入式·freertos·modbus
Hello_Embed6 天前
libmodbus STM32 主机实验(USB 串口版)
笔记·stm32·学习·嵌入式·freertos·modbus
柱子jason11 天前
使用IOT-Tree Server模拟Modbus设备对接西门子PLC S7-200
网络·物联网·自动化·modbus·西门子plc·iot-tree·协议转换
Hello_Embed14 天前
libmodbus 移植 STM32(基础篇)
笔记·stm32·单片机·学习·modbus
wotaifuzao14 天前
STM32多协议网关-FreeRTOS事件驱动架构实战
stm32·嵌入式硬件·can·freertos·uart·modbus·spi
Hello_Embed15 天前
libmodbus 源码分析(接收请求篇)
笔记·学习·嵌入式·freertos·modbus
Hello_Embed18 天前
Modbus 协议报文解析
笔记·stm32·单片机·学习·modbus