目录

10.1 项目概述

10.2 硬件电路与引脚定义

10.2.1 AHT20连接

10.2.2 输出控制引脚

10.3 Modbus寄存器地址分配

10.4 AHT20传感器操作详解

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

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

10.4.3 物理量转换公式

10.4.4 CRC校验函数

10.5 程序结构设计

10.6 代码逐行解析

10.6.1 宏定义与全局变量

10.6.2 CRC计算函数

10.6.3 AHT20任务函数

10.6.4 Modbus任务中的处理

10.6.5 输出控制部分

10.7 关键难点解析

10.7.1 为什么不能直接在Modbus任务中读取AHT20?

10.7.2 I2C地址问题

10.7.3 数据拼接与移位运算

10.7.4 CRC校验的必要性

10.7.5 单位转换的数学推导

10.8 调试与验证

10.9 本章核心知识点总结


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,加上读写位后分别为0x70和0x71。

10.2.2 输出控制引脚
功能引脚电平逻辑Modbus类型
BEEP1PB15高电平发声DO
BEEP2PB14高电平发声DO
LED1PB11低电平发光DO
LED2PB12低电平发光DO
LED3PB13低电平发光DO

10.3 Modbus寄存器地址分配

设备地址:03H

寄存器地址类别用途描述
0000HDO控制蜂鸣器11-响
0001HDO控制蜂鸣器21-响
0002HDO控制LED11-亮
0003HDO控制LED21-亮
0004HDO控制LED31-亮
0000HAI读取温度单位0.1°C,16位有符号整数
0001HAI读取湿度单位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字节数据排列如下:

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

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

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:

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_temp和g_humi。

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

10.6 代码逐行解析

10.6.1 宏定义与全局变量
#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_temp和g_humi为全局变量,供Modbus任务读取。

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

10.6.2 CRC计算函数

已在10.4.4给出。

10.6.3 AHT20任务函数
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或任务启动前):

osThreadNew(AHT20Task, NULL, &aht20_task_attributes);

在Modbus任务循环中:

#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函数简单读取全局变量:

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

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

10.6.5 输出控制部分

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

#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映射更新:在收到请求时从全局变量获取最新值填入输入寄存器。

更多推荐