【前言】
根据试用计划,我需要使用STM32H747_CM4内核来驱动一个温湿度传感器,获取其温度值,然后通过OpemAMP传给STM32H747_CM7核,再用touchGFX展示出来。这一篇,我将分享如STM32H747_CM4中驱动AHT20。
【接口选择】
AHT20是IIC接口的传感器,通过查看原理图,开发板开放的硬件IIC为I2C4,但是I2C4已经分配给STM32H747_CM7的LCD屏的触摸屏的接口使用。因此不方便再做为STM32H747_CM4的核使用,因此决定采用摸拟IIC来驱动。
我使用开发板上的Arduino接口中的D6与D7,D7为PJ0做为SCL,D6为PJ7做为SDA使用。
【代码实现】
首先往stm32cubeIDE的src中添加两个驱动文件:ATH20.h与ATH20.c,其代码如下:
#ifndef __AHT20_H
#define __AHT20_H
#include "stm32h7xx_hal.h"
// -------------------------- 硬件引脚定义 --------------------------
#define AHT20_SCL_PIN GPIO_PIN_0 // PJ0 (SCL)
#define AHT20_SDA_PIN GPIO_PIN_7 // PJ7 (SDA)
#define AHT20_GPIO_PORT GPIOJ // 端口J
#define AHT20_GPIO_CLK __HAL_RCC_GPIOJ_CLK_ENABLE() // GPIOJ时钟使能
// -------------------------- AHT20参数定义 --------------------------
#define AHT20_I2C_ADDR 0x38 // AHT20 7位I2C地址
#define AHT20_WRITE_ADDR (AHT20_I2C_ADDR << 1) // 写地址(0x70)
#define AHT20_READ_ADDR (AHT20_I2C_ADDR << 1 | 0x01) // 读地址(0x71)
// AHT20命令
#define AHT20_CMD_INIT 0xBE // 初始化命令
#define AHT20_CMD_MEAS 0xAC // 温湿度测量命令
// 状态码
typedef enum {
AHT20_OK = 0,
AHT20_INIT_FAIL, // 初始化失败
AHT20_COMM_ERR, // I2C通信错误
AHT20_MEAS_TIMEOUT,// 测量超时
AHT20_CRC_ERR // CRC校验失败
} AHT20_StatusTypeDef;
// -------------------------- 函数声明 --------------------------
void DWT_Delay_Init(void); // DWT微秒延时初始化
void DWT_Delay_us(uint32_t us); // 微秒延时
AHT20_StatusTypeDef AHT20_Init(void); // AHT20初始化
AHT20_StatusTypeDef AHT20_ReadData(float *humidity, float *temperature); // 读取温湿度
#endif // __AHT20_H#include "AHT20.h"
// -------------------------- 模拟I2C底层宏定义 --------------------------
#define I2C_SCL_HIGH() HAL_GPIO_WritePin(AHT20_GPIO_PORT, AHT20_SCL_PIN, GPIO_PIN_SET)
#define I2C_SCL_LOW() HAL_GPIO_WritePin(AHT20_GPIO_PORT, AHT20_SCL_PIN, GPIO_PIN_RESET)
#define I2C_SDA_HIGH() HAL_GPIO_WritePin(AHT20_GPIO_PORT, AHT20_SDA_PIN, GPIO_PIN_SET)
#define I2C_SDA_LOW() HAL_GPIO_WritePin(AHT20_GPIO_PORT, AHT20_SDA_PIN, GPIO_PIN_RESET)
#define I2C_SDA_READ() HAL_GPIO_ReadPin(AHT20_GPIO_PORT, AHT20_SDA_PIN)
// -------------------------- DWT延时实现(精确微秒) --------------------------
// 依赖STM32H7内核DWT计数器,系统时钟需配置正确(默认480MHz)
void DWT_Delay_Init(void) {
if (CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk) {
DWT->CTRL &= ~DWT_CTRL_CYCCNTENA_Msk;
}
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能DWT
DWT->CYCCNT = 0; // 计数器清零
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能CYCCNT计数
}
void DWT_Delay_us(uint32_t us) {
uint32_t start = DWT->CYCCNT;
uint32_t ticks = (SystemCoreClock / 1000000) * us; // 1us = 系统时钟数/1e6
while ((DWT->CYCCNT - start) < ticks);
}
// -------------------------- 模拟I2C核心函数 --------------------------
// I2C起始信号:SCL高电平时,SDA从高→低
static void I2C_Start(void) {
I2C_SDA_HIGH();
I2C_SCL_HIGH();
DWT_Delay_us(5);
I2C_SDA_LOW();
DWT_Delay_us(5);
I2C_SCL_LOW(); // 拉低SCL,准备发送数据
DWT_Delay_us(5);
}
// I2C停止信号:SCL高电平时,SDA从低→高
static void I2C_Stop(void) {
I2C_SDA_LOW();
I2C_SCL_HIGH();
DWT_Delay_us(5);
I2C_SDA_HIGH();
DWT_Delay_us(5);
}
// 发送应答信号(0=应答,1=非应答)
static void I2C_SendAck(uint8_t ack) {
I2C_SCL_LOW();
DWT_Delay_us(5);
ack ? I2C_SDA_HIGH() : I2C_SDA_LOW();
DWT_Delay_us(5);
I2C_SCL_HIGH();
DWT_Delay_us(5);
I2C_SCL_LOW();
DWT_Delay_us(5);
I2C_SDA_HIGH(); // 释放SDA总线
}
// 接收应答信号(返回0=应答,1=非应答)
static uint8_t I2C_RecvAck(void) {
uint8_t ack = 1;
I2C_SCL_LOW();
DWT_Delay_us(5);
I2C_SDA_HIGH(); // 释放SDA,由从机控制
DWT_Delay_us(5);
I2C_SCL_HIGH();
DWT_Delay_us(5);
if (I2C_SDA_READ() == GPIO_PIN_RESET) ack = 0; // 从机拉低=应答
I2C_SCL_LOW();
DWT_Delay_us(5);
return ack;
}
// 发送1字节数据
static void I2C_SendByte(uint8_t data) {
uint8_t i;
for (i = 0; i < 8; i++) {
I2C_SCL_LOW();
DWT_Delay_us(5);
(data & 0x80) ? I2C_SDA_HIGH() : I2C_SDA_LOW(); // 高位先送
data <<= 1;
DWT_Delay_us(5);
I2C_SCL_HIGH();
DWT_Delay_us(5);
}
I2C_SCL_LOW();
DWT_Delay_us(5);
I2C_SDA_HIGH(); // 释放SDA,等待应答
}
// 接收1字节数据(ack=0:发送应答,ack=1:发送非应答)
static uint8_t I2C_RecvByte(uint8_t ack) {
uint8_t i, data = 0;
I2C_SDA_HIGH(); // 释放SDA
for (i = 0; i < 8; i++) {
I2C_SCL_LOW();
DWT_Delay_us(5);
I2C_SCL_HIGH();
DWT_Delay_us(5);
data <<= 1;
if (I2C_SDA_READ() == GPIO_PIN_SET) data |= 0x01;
DWT_Delay_us(5);
}
I2C_SCL_LOW();
DWT_Delay_us(5);
I2C_SendAck(ack); // 发送应答/非应答
return data;
}
// -------------------------- AHT20专用函数 --------------------------
// 读取AHT20状态寄存器
static uint8_t AHT20_ReadStatus(void) {
uint8_t status = 0;
I2C_Start();
I2C_SendByte(AHT20_READ_ADDR);
if (I2C_RecvAck() == 0) { // 从机应答
status = I2C_RecvByte(1); // 接收状态字节,发送非应答
}
I2C_Stop();
return status;
}
// CRC8校验(多项式0x31,AHT20专用)
static uint8_t AHT20_CRC8(uint8_t *data, uint8_t len) {
uint8_t crc = 0xFF;
for (uint8_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
crc = (crc & 0x80) ? (crc << 1 ^ 0x31) : (crc << 1);
}
}
return crc;
}
// AHT20初始化(GPIO+I2C+芯片初始化)
AHT20_StatusTypeDef AHT20_Init(void) {
GPIO_InitTypeDef gpio_conf = {0};
uint8_t retry = 0;
// 1. 初始化DWT延时
DWT_Delay_Init();
// 2. 配置I2C GPIO(开漏输出+上拉,避免总线冲突)
AHT20_GPIO_CLK; // 使能GPIOJ时钟
gpio_conf.Pin = AHT20_SCL_PIN | AHT20_SDA_PIN;
gpio_conf.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出
gpio_conf.Pull = GPIO_PULLUP; // 上拉电阻
gpio_conf.Speed = GPIO_SPEED_FREQ_HIGH; // 高速
HAL_GPIO_Init(AHT20_GPIO_PORT, &gpio_conf);
// 3. 初始电平:SCL/SDA均拉高
I2C_SCL_HIGH();
I2C_SDA_HIGH();
HAL_Delay(20); // 上电后等待20ms稳定
// 4. 检查AHT20是否已初始化(状态位bit3=1表示已初始化)
while (((AHT20_ReadStatus() & 0x08) == 0) && (retry < 10)) {
// 未初始化,发送初始化命令(0xBE + 0x08 + 0x00)
I2C_Start();
I2C_SendByte(AHT20_WRITE_ADDR);
if (I2C_RecvAck() == 0) {
I2C_SendByte(AHT20_CMD_INIT);
I2C_RecvAck();
I2C_SendByte(0x08); // 配置参数1
I2C_RecvAck();
I2C_SendByte(0x00); // 配置参数2
I2C_RecvAck();
}
I2C_Stop();
HAL_Delay(10); // 等待初始化完成
retry++;
}
return (retry < 10) ? AHT20_OK : AHT20_INIT_FAIL;
}
// 读取温湿度数据(返回:湿度(%)、温度(℃))
AHT20_StatusTypeDef AHT20_ReadData(float *humidity, float *temperature) {
uint8_t data[7] = {0};
uint32_t humi_raw = 0, temp_raw = 0;
// 1. 发送测量命令(0xAC + 0x33 + 0x00)
I2C_Start();
I2C_SendByte(AHT20_WRITE_ADDR);
if (I2C_RecvAck() != 0) {
I2C_Stop();
return AHT20_COMM_ERR;
}
I2C_SendByte(AHT20_CMD_MEAS);
I2C_RecvAck();
I2C_SendByte(0x33); // 测量配置参数1
I2C_RecvAck();
I2C_SendByte(0x00); // 测量配置参数2
I2C_RecvAck();
I2C_Stop();
// 2. 等待测量完成(AHT20忙时SDA拉低,超时100ms)
uint32_t timeout = 100;
while ((I2C_SDA_READ() == GPIO_PIN_RESET) && (timeout-- > 0)) {
HAL_Delay(1);
}
if (timeout == 0) return AHT20_MEAS_TIMEOUT;
// 3. 读取6字节数据(状态+湿度高2字节+温湿度混合字节+温度低2字节+CRC)
I2C_Start();
I2C_SendByte(AHT20_READ_ADDR);
if (I2C_RecvAck() != 0) {
I2C_Stop();
return AHT20_COMM_ERR;
}
for (uint8_t i = 0; i < 6; i++) {
data[i] = I2C_RecvByte(0); // 前5字节:发送应答
}
data[6] = I2C_RecvByte(1); // 第6字节(CRC):发送非应答
I2C_Stop();
// 4. CRC校验(可选,注释可关闭)
if (AHT20_CRC8(data, 6) != data[6]) {
return AHT20_CRC_ERR;
}
// 5. 解析温湿度(20位数据)
humi_raw = ((uint32_t)data[1] << 16) | ((uint32_t)data[2] << 8) | data[3];
humi_raw >>= 4; // 湿度:20位(data1[8位] + data2[8位] + data3[4位])
*humidity = (humi_raw / 1048576.0f) * 100.0f; // 1048576 = 2^20
temp_raw = ((uint32_t)(data[3] & 0x0F) << 16) | ((uint32_t)data[4] << 8) | data[5];
// 温度:20位(data3[4位] + data4[8位] + data5[8位])
*temperature = (temp_raw / 1048576.0f) * 200.0f - 50.0f;
return AHT20_OK;
}添加app.h以及app.c,用于用户代码实现,避免在main.h中添加过多的代码,也避免在重新生成代码后覆盖。
app.h代码如下:
#ifndef APPLICATION_USER_CORE_APP_H_ #define APPLICATION_USER_CORE_APP_H_ #include "main.h" void Run(void); #endif /* AP
app.c代码如下:
#include "app.h"
#include "stdio.h"
#include "AHT20.h"
void Run(void)
{
float humidity = 0.0f, temperature = 0.0f;
AHT20_StatusTypeDef aht_status;
printf("start\r\n");
// 1. 初始化AHT20
aht_status = AHT20_Init();
if (aht_status == AHT20_OK) {
printf("AHT20 Init Success!\r\n");
} else {
printf("AHT20 Init Fail! Code: %d\r\n", aht_status);
while (1); // 初始化失败,死循环
}
while(1)
{
aht_status = AHT20_ReadData(&humidity, &temperature);
if (aht_status == AHT20_OK) {
// 打印温湿度(保留2位小数)
printf("Humidity: %.2f %%RH | Temperature: %.2f °C\r\n",\
humidity, temperature);
} else {
// 打印错误信息
printf("Read Fail! Code: %d\r\n", aht_status);
}
HAL_Delay(1000); // 1秒读取一次
}
}Run放入main中调用。
【实验现象】
接上AHT20后,编译下载工程,打开串口,打印如下:

致此,AHT20已经成功的驱动。
后续将实现OpenAMP将温度显示在TouchGFX界面中。
我要赚赏金
