接入温湿度风扇产品固件(仲裁/条件/定时/显示),并补齐规格书与外设驱动

新增 usr_app 产品逻辑:手动>定时>条件仲裁、条件控温湿、定时任务、数码管显示、
按键/LED、风扇档位、NVM 掉电恢复、配对与开关机,上电按规格恢复上次状态。
新增 usr_periph:AHT20、CT1642、板级引脚与硬件封装;见宝协议 cond_config
改为 7 字节编解码,并接入产品 DP。
补充《温湿度风扇_固件需求规格书》,工程加入对应源文件。
.gitignore 忽略 WorkBuddy/Cursor 本地会话
This commit is contained in:
2026-08-31 15:17:27 +08:00
parent 4cb7418a02
commit 5cc7e9dbd6
53 changed files with 7028 additions and 513 deletions
+436
View File
@@ -0,0 +1,436 @@
/******************************************************************************
* @file bsp_aht20.c
* @brief CBR3 AHT20 软件 I2C 驱动实现
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.3
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布
* - V1.0.1, 2026.08.29, cyWu, 修复 SCL 高电平期间推挽输出 SDA 导致的假 STOP
* (读数据必 CRC fail 的根因);补 AHT20 0x71 校准检查 + 0xBE 初始化
* - V1.0.2, 2026.08.29, cyWu, 排障结束:回退诊断代码(电平自检/地址扫描/IN 直读),
* 保留时序修复、内部上拉依赖、总线恢复、0x71/0xBE 初始化流程
* - V1.0.3, 2026.08.29, cyWu, IO 分离:AHT20 独占 PC4(SCL)/PC5(SDA),删除
* bsp_bus 共线锁与 measure_start/finish 拆分锁,采集恢复一次完成;
* PC4/PC5 上拉配置从 bsp_bus_init 移入本文件 init
******************************************************************************/
#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
}