/****************************************************************************** * @file bsp_aht20.c * @brief CBR3 AHT20 软件 I2C 驱动实现 * @author cyWu <1917507415@qq.com> * @date 2026.08.29 * @version V1.0.3,IO 分离: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 fail(V1.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 START:SCL 高时 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 STOP:SCL 高时 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=收到 ACK,1=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 CRC8(poly 0x31,init 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(); /* 释放 SDA(SCL=低时切输入) */ 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 最大 ~2e7,int32 安全 */ 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 }