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类型 |
|---|---|---|---|
| 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接口,操作步骤如下:
-
上电等待:VDD上电后需等待至少5ms,使传感器稳定。
-
发送测量命令 :主机发送写命令
0x70,然后发送3字节命令0xAC 0x33 0x00。 -
等待测量完成:传感器测量需至少80ms(典型值)。
-
读取数据 :主机发送读命令
0x71,读取7字节数据:-
字节0:状态字(Status)
-
字节1-2:湿度数据高16位(SRH[19:4])
-
字节3:湿度低4位+温度高4位
-
字节4-5:温度数据中低16位(ST[15:0])
-
字节6:CRC校验字节(对字节0~5计算CRC8)
-
-
CRC校验 :使用多项式
x^8 + x^5 + x^4 + 1(即0x31)计算CRC8,与接收的CRC字节比较,验证数据完整性。 -
数据转换:将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_temp和g_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_temp和g_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_temp和g_humi是uint32_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 调试与验证
-
验证I2C通信:使用逻辑分析仪抓取I2C波形,确认发送的命令和接收的数据是否正确。
-
验证CRC:手动计算CRC8与接收值比较。
-
验证温湿度值:用串口打印转换后的值,与实际情况对比(如对着传感器呼气湿度应上升)。
-
Modbus通信测试:用Modbus Poll读取输入寄存器地址0和1,观察数值是否合理变化。
10.9 本章核心知识点总结
-
AHT20操作三步曲:发送测量命令→等待80ms→读取数据+CRC校验。
-
独立任务的必要性:避免长时间阻塞影响Modbus通信。
-
数据解析:从6字节中提取20位原始值,注意位分布和移位操作。
-
CRC8校验:多项式0x31,初始值0xFF,确保数据完整性。
-
物理量转换:根据数据手册公式转换为实际单位,注意整数运算顺序。
-
共享数据保护:全局变量在多任务间共享,需考虑原子性(此处值较小,无保护也可,但严谨应用应加临界区或互斥信号量)。
-
Modbus映射更新:在收到请求时从全局变量获取最新值填入输入寄存器。