Files
JBao_Temp_Humi_Fan_FW/apps/usr_periph/bsp_aht20.c
T
wuchuyuan e324ebd99b 精简 usr_app 架构:去掉关机/休眠死代码,按键复用 key.c 状态机,ISR 重活统一延后到任务上下文
Type-C 长供电、上电即工作、无软关机,删除 app_boot/app_power 及 DP0 开关相关逻辑,
去掉空占位 app_fan_run 和无效的 usr_timer_pick_mode/rearm(数码管 4ms 扫描已 priority=1,
系统本来就不会进低功耗)。按键 5s 配对 / 12s OTA 改走 key.c 长按分级,去掉自计时与 1~5s 死区。
新增 app_defer,NVM/配对/OTA 只在 ISR 置位、在 500ms 任务上下文落地,避免再在 usr_timer 里写 flash。
规格书与注释同步精简。
2026-08-31 17:57:52 +08:00

430 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/******************************************************************************
* @file bsp_aht20.c
* @brief CBR3 AHT20 软件 I2C 驱动实现
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.3IO 分离:AHT20 独占 PC4(SCL)/PC5(SDA),无总线共享锁,
* 采集一次完成;修复 SCL 高电平期间推挽输出 SDA 导致的假 STOP,
* 补 0x71 校准检查 + 0xBE 初始化
******************************************************************************/
#include "bsp_aht20.h"
#include "board_pin.h"
#include "asm/gpio.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[AHT20]"
#define LOG_ERROR_ENABLE
#define LOG_INFO_ENABLE
#include "debug.h"
#define AHT20_ADDR_W 0x70 /**< 写地址(7bit 0x38 << 1 */
#define AHT20_ADDR_R 0x71 /**< 读地址 */
#define AHT20_CMD_MEASURE 0xAC
#define AHT20_CMD_INIT 0xBE /**< 初始化校准寄存器命令 */
#define AHT20_CMD_SOFTRESET 0xBA /**< 软复位命令(备用) */
#define AHT20_READ_LEN 7 /**< status + 20bit humi + 20bit temp + crc */
static uint8_t s_inited;
/* 软件 I2C:PC5=SDA(三态开漏,内部上拉 + 外部 4.7~10k 双保险),PC4=SCL(推挽)
*
* ⚠ 时序铁律:SDA 的方向切换 / 推挽输出只能在 SCL=0 时进行!
* 若在 SCL=1 期间把 SDA 推挽输出为高,而从机正拉低 SDA(数据位 0 或 ACK),
* 会在 SCL 高电平期间制造 SDA 上升沿 = 从机误判 STOP,传输中止 →
* 之后读到的全是上拉的垃圾字节 → CRC failV1.0.0 实测 bug)。
*
* ⚠ IO 分离(V1.0.3):CT1642 已移到 PG0/PG1,本驱动独占 PC4/PC5
* 无总线锁、无模式切换,可直接调用。
*/
/**
* @brief ~2us 延时(24MHz,约 48 条指令),对应 I2C ≤250kHz
* @return 无
*/
static void i2c_delay(void)
{
volatile uint32_t i;
for (i = 0; i < 16; i++) {
/* nop */
}
}
/**
* @brief SCL 拉高/拉低(SCL 推挽输出,主控独占驱动,无方向切换)
* @param level 1=高,0=低
* @return 无
*/
static void i2c_scl(uint8_t level)
{
gpio_direction_output(BOARD_TH_SCL_PIN, level ? 1 : 0);
i2c_delay();
}
/**
* @brief 设置 SDA 输出电平(内部含"输出高=推 1 后切高阻"
* @note ⚠ 仅允许在 SCL=0 时调用(START/STOP 条件除外,那两处从机不驱动总线)
* @param level 1=高(推 1 后切高阻,靠外部上拉保持),0=低(推挽拉低)
* @return 无
*/
static void i2c_sda_out(uint8_t level)
{
if (level) {
gpio_direction_output(BOARD_TH_SDA_PIN, 1);
gpio_set_direction(BOARD_TH_SDA_PIN, 1); /* 高阻,外部上拉拉高 */
} else {
gpio_direction_output(BOARD_TH_SDA_PIN, 0);
}
i2c_delay();
}
/**
* @brief 释放 SDA 为输入(高阻)
* @note ⚠ 仅允许在 SCL=0 时调用;释放后用 i2c_sda_read() 采样
* @return 无
*/
static void i2c_sda_release(void)
{
gpio_direction_output(BOARD_TH_SDA_PIN, 1);
gpio_set_direction(BOARD_TH_SDA_PIN, 1);
i2c_delay();
}
/**
* @brief 采样 SDA 电平(调用时 SDA 必处于输入态)
* @return 1=高,0=低
*/
static uint8_t i2c_sda_read(void)
{
return (uint8_t)(gpio_read(BOARD_TH_SDA_PIN) ? 1 : 0);
}
/**
* @brief I2C STARTSCL 高时 SDA 高→低
* @note 起始时 SCL 必为低(上一帧收尾),先释放 SDA 再抬 SCL
* @return 无
*/
static void i2c_start(void)
{
i2c_scl(0);
i2c_sda_out(1);
i2c_scl(1);
i2c_sda_out(0); /* SCL 高时 1→0 = START */
i2c_scl(0);
}
/**
* @brief I2C STOPSCL 高时 SDA 低→高
* @return 无
*/
static void i2c_stop(void)
{
i2c_scl(0);
i2c_sda_out(0);
i2c_scl(1);
i2c_sda_out(1); /* SCL 高时 0→1 = STOP;此时从机已释放总线,推 1 无冲突 */
}
/**
* @brief 写一个字节(MSB 先;每位 SDA 都在 SCL 低电平期间变化)
* @param byte 数据
* @return 无
*/
static void i2c_write_byte(uint8_t byte)
{
uint8_t i;
for (i = 0; i < 8; i++) {
i2c_sda_out((uint8_t)((byte & 0x80u) ? 1 : 0));
byte <<= 1;
i2c_scl(1);
i2c_scl(0);
}
}
/**
* @brief 等待从机 ACK(第 9 个时钟;SDA 方向切换只在 SCL=0 时做)
* @return 0=收到 ACK1=NACK
*/
static uint8_t i2c_wait_ack(void)
{
uint8_t ack;
i2c_scl(0);
i2c_sda_release(); /* SCL 低时切输入,绝不污染 SCL 高电平区间 */
i2c_scl(1);
ack = i2c_sda_read();
i2c_scl(0);
return ack; /* 0=ACK(从机拉低),1=NACK */
}
/**
* @brief 读一个字节(先在 SCL=0 时释放 SDA,循环内只采样不切方向)
* @param ack 1=收完发 ACK(继续读),0=发 NACK(最后一字节)
* @return 读到的字节
*/
static uint8_t i2c_read_byte(uint8_t ack)
{
uint8_t i;
uint8_t byte = 0;
i2c_sda_release();
for (i = 0; i < 8; i++) {
i2c_scl(1);
byte = (uint8_t)((byte << 1) | i2c_sda_read());
i2c_scl(0);
}
i2c_sda_out(ack ? 0 : 1); /* 第 9 位:ACK 拉低 / NACK 释放(SCL 低时设置) */
i2c_scl(1);
i2c_scl(0);
i2c_sda_out(1);
return byte;
}
/**
* @brief CRC8poly 0x31init 0xFF
* @param data 数据指针
* @param len 长度
* @return CRC 值
*/
static uint8_t aht20_crc8(const uint8_t *data, uint8_t len)
{
uint8_t crc = 0xFF;
uint8_t i, b;
for (i = 0; i < len; i++) {
crc ^= data[i];
for (b = 0; b < 8; b++) {
crc = (uint8_t)((crc & 0x80u) ? ((crc << 1) ^ 0x31u) : (crc << 1));
}
}
return crc;
}
/**
* @brief I2C 总线恢复:发 9 个 SCL 时钟 + STOP,解开"从机拉死 SDA"的卡死态
* @note 卡死场景:之前某次传输中途被复位/打断,从机停在"还有 N 位没发完"
* 持续拉低 SDA,此后所有 START/ACK 全废。标准 I2C 恢复手段。
* @return 无
*/
static void i2c_bus_recover(void)
{
uint8_t i;
i2c_scl(0);
i2c_sda_release(); /* 释放 SDASCL=低时切输入) */
for (i = 0; i < 9; i++) {
i2c_scl(1);
if (i2c_sda_read()) {
break; /* SDA 回到高:从机已释放 */
}
i2c_scl(0);
}
i2c_stop(); /* 补一个 STOP 收尾 */
}
/**
* @brief 读状态寄存器(0x71):检查校准使能位,未校准则发 0xBE 0x08 0x00
* @note IO 分离后 AHT20 独占 PC4/PC5,无需总线锁,直接调用
* @return 0=就绪(已校准),-1=通讯失败
*/
static int8_t aht20_check_and_calib(void)
{
uint8_t status;
/* 写 0x71 读状态命令 */
i2c_start();
i2c_write_byte(AHT20_ADDR_W);
if (i2c_wait_ack()) {
goto fail;
}
i2c_write_byte(0x71);
if (i2c_wait_ack()) {
goto fail;
}
i2c_stop();
/* 读 1 字节状态 */
i2c_start();
i2c_write_byte(AHT20_ADDR_R);
if (i2c_wait_ack()) {
goto fail;
}
status = i2c_read_byte(0);
i2c_stop();
if (status & 0x08u) {
return 0; /* Bit3 Calibration Enabled:出厂默认已校准 */
}
/* 未校准:发初始化命令 0xBE 0x08 0x00 */
log_error("aht20 not calib(0x%02x), init 0xBE\n", status);
i2c_start();
i2c_write_byte(AHT20_ADDR_W);
if (i2c_wait_ack()) {
goto fail;
}
i2c_write_byte(AHT20_CMD_INIT);
if (i2c_wait_ack()) {
goto fail;
}
i2c_write_byte(0x08);
if (i2c_wait_ack()) {
goto fail;
}
i2c_write_byte(0x00);
if (i2c_wait_ack()) {
goto fail;
}
i2c_stop();
return 0;
fail:
i2c_stop();
return -1;
}
/**
* @brief 初始化:配置 PC4/PC5 + 软件 I2C 总线复位 + 0x71 校准检查 + 0xBE 初始化
* @return 无
*/
void bsp_aht20_init(void)
{
if (s_inited) {
return;
}
#if BOARD_TH_ENABLE
/* 上电后等待 ≥100ms 由 app 启动节奏保证,此处直接操作 */
/* PC5(SDA) 开内部上拉:I2C 释放 SDA 后必须有上拉,否则浮空
* (实测无上拉时:init 读状态=0xFF 假 init ok;等 80ms 后漏电读全 0 → crc fail)。
* 板上有外部 4.7~10k 上拉时双保险。PC4(SCL) 推挽驱动,上拉无害防浮空。 */
gpio_set_pull_up(BOARD_TH_SDA_PIN, 1);
gpio_set_pull_down(BOARD_TH_SDA_PIN, 0);
gpio_set_die(BOARD_TH_SDA_PIN, 1);
gpio_set_pull_up(BOARD_TH_SCL_PIN, 1);
gpio_set_pull_down(BOARD_TH_SCL_PIN, 0);
gpio_set_die(BOARD_TH_SCL_PIN, 1);
/* 总线复位:先 9 clock + STOP 解开可能的从机卡死,再补一次 STOP */
i2c_bus_recover();
i2c_stop();
if (aht20_check_and_calib() == 0) {
log_info("aht20 init ok\n");
} else {
/* 校准检查失败不阻塞测量(多数 AHT20 出厂已校准),后续采样重试;
* 顺带做一次总线恢复,解开可能的从机卡死 */
log_error("aht20 calib check fail\n");
i2c_bus_recover();
}
s_inited = 1;
#else
log_info("aht20 disabled\n");
#endif
}
/**
* @brief 发起一次测量:发 0xAC 0x33 0x00
* @note 调用后必须等 ≥80ms 再 bsp_aht20_measure_finish。等待期间 I2C 总线
* 空闲(AHT20 内部测量),CT1642 在 PG0/PG1 独立刷新,互不影响。
* @return 0=命令已发出,非 0=失败
*/
int8_t bsp_aht20_measure_start(void)
{
#if !BOARD_TH_ENABLE
return -1;
#else
if (!s_inited) {
bsp_aht20_init(); /* init 失败时此处补做 */
if (!s_inited) {
return -1;
}
}
i2c_start();
i2c_write_byte(AHT20_ADDR_W);
if (i2c_wait_ack()) {
log_error("addr nack\n");
goto fail;
}
i2c_write_byte(AHT20_CMD_MEASURE);
if (i2c_wait_ack()) {
log_error("cmd nack\n");
goto fail;
}
i2c_write_byte(0x33);
if (i2c_wait_ack()) {
log_error("param1 nack\n");
goto fail;
}
i2c_write_byte(0x00);
if (i2c_wait_ack()) {
log_error("param2 nack\n");
goto fail;
}
i2c_stop();
return 0;
fail:
i2c_stop();
return -1;
#endif
}
/**
* @brief 读取测量结果:读 7 字节 + CRC8 校验 + 换算
* @param temp_x10 输出温度 ×10(如 25.3℃ → 253
* @param humi_pct 输出湿度 0~100 整数 %
* @return 0=成功;-1=通讯失败(CRC 错/超时/忙)
*/
int8_t bsp_aht20_measure_finish(int16_t *temp_x10, uint8_t *humi_pct)
{
#if !BOARD_TH_ENABLE
return -1;
#else
uint8_t buf[AHT20_READ_LEN];
uint8_t i;
uint32_t humi_raw;
uint32_t temp_raw;
int32_t temp_10;
int8_t ret = -1;
i2c_start();
i2c_write_byte(AHT20_ADDR_R);
if (i2c_wait_ack()) {
log_error("read addr nack\n");
goto out;
}
for (i = 0; i < AHT20_READ_LEN; i++) {
buf[i] = i2c_read_byte((uint8_t)((i < (AHT20_READ_LEN - 1)) ? 1 : 0));
}
i2c_stop();
if (aht20_crc8(buf, AHT20_READ_LEN - 1) != buf[AHT20_READ_LEN - 1]) {
log_error("crc fail: %02X %02X %02X %02X %02X %02X crc=%02X\n",
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]);
goto out;
}
if (buf[0] & 0x80u) { /* busy 位 */
log_error("sensor busy\n");
goto out;
}
humi_raw = ((uint32_t)buf[1] << 12u) | ((uint32_t)buf[2] << 4u) | ((uint32_t)buf[3] >> 4u);
temp_raw = (((uint32_t)buf[3] & 0x0Fu) << 16u) | ((uint32_t)buf[4] << 8u) | (uint32_t)buf[5];
/* T = raw * 200 / 2^20 - 50(℃);×10 输出。整数运算:raw*2000 最大 ~2e7int32 安全 */
temp_10 = (int32_t)((temp_raw * 2000u) / 1048576u) - 500;
*temp_x10 = (int16_t)temp_10;
*humi_pct = (uint8_t)((humi_raw * 100u) / 1048576u);
if (*humi_pct > 100) {
*humi_pct = 100;
}
ret = 0;
out:
return ret;
#endif
}