-
z5Q{N+aTwu?vnIW5ed&^D{!gmtqX0L;ZT)r2?I7-4^4xbkhi4=-rOq_GIU#{#Y
z`2Ny}E{ctTA7<5mtNm~hSRo?8*#@aMVB5;oM$K+T
zMj`Av=2&zdAo$kIvJg&N%vG=0J)4k$UZsx7mPB5@xyrcdS7L0;^f8wuk-O*KuYWja
zQP+3pT9HAPX2qC?4YEAC+TZ2TB!w)7lJ`dhEzhf^^`;Q!=6;~`GL@49W?%b!oMpJ}
zed7
+ * @date 2026.08.29
+ * @version V1.0.0
+ * @history
+ * - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 Bring-up)
+ *
+ * @note 产品 GPIO 全部集中到本文件;板级 board_jl701n_demo_cfg.h 只保留
+ * UART0_TX=PG6 一项改动。CT1642 用 PG0(CLK)/PG1(DATA),AHT20 用
+ * PC4(SCL)/PC5(SDA),两路 IO 独立、无总线共享(2026-08-29 改板飞线分离,
+ * 原 PC4/PC5 共线 + bsp_bus 分时机制整体删除)。
+ ******************************************************************************/
+
+#ifndef __BOARD_PIN_H__
+#define __BOARD_PIN_H__
+
+#include "asm/gpio.h"
+
+/*============================================================================*/
+/* 功能开关:某路硬件异常时可单独关掉,不影响配网 */
+/* 0 时对应模块 init/run 直接 return,GPIO 不配,日志打一次 xxx disabled */
+/*============================================================================*/
+
+#define BOARD_LED_ENABLE 1 /**< 1=状态 LED PC2(低亮) */
+#define BOARD_KEY_ENABLE 1 /**< 1=按键 PB1(低按下) */
+#define BOARD_FAN_ENABLE 1 /**< 1=风扇 MCPWM PC3(高导通) */
+#define BOARD_DISP_ENABLE 1 /**< 1=CT1642 数码管(PG0/PG1 独立) */
+#define BOARD_TH_ENABLE 1 /**< 1=AHT20 温湿度(PC4/PC5 独立) */
+#define BOARD_MODE_ENABLE 1 /* 通风/强力状态机;P1 开 */
+#define BOARD_COND_ENABLE 1 /* 条件引擎;P2 开 */
+#define BOARD_TIMER_ENABLE 1 /* 定时引擎;P2 开 */
+#define BOARD_NVM_ENABLE 1 /* VM 持久化;P1 开 */
+
+/*============================================================================*/
+/* GPIO */
+/*============================================================================*/
+
+#define BOARD_UART_TX_PIN IO_PORTG_06 /**< 调试 UART TX(只发不占 PC5) */
+#define BOARD_DISP_CLK_PIN IO_PORTG_00 /**< CT1642 CLK(独立,飞线 G0) */
+#define BOARD_DISP_DATA_PIN IO_PORTG_01 /**< CT1642 DATA(独立,飞线 G1) */
+#define BOARD_TH_SCL_PIN IO_PORTC_04 /**< AHT20 SCLK(独立,推挽驱动) */
+#define BOARD_TH_SDA_PIN IO_PORTC_05 /**< AHT20 SDA(开漏+内部上拉,外部 4.7~10k 双保险) */
+#define BOARD_FAN_PIN IO_PORTC_03 /**< 风扇 PWM,高电平导通,MCPWM */
+#define BOARD_LED_PIN IO_PORTC_02 /**< 状态 LED,低电平亮 */
+#define BOARD_KEY_PIN IO_PORTB_01 /**< 按键,低电平按下 */
+
+#define BOARD_LED_ON_LEVEL 0 /**< LED 点亮电平:0=低亮 */
+#define BOARD_FAN_ON_LEVEL 1 /**< 风扇有效电平:1=高导通 */
+#define BOARD_KEY_ACTIVE_LEVEL 0 /**< 按键有效电平:0=按下为低 */
+
+/*============================================================================*/
+/* 按键 / 开机 */
+/*============================================================================*/
+
+#define BOARD_KEY_CLICK_MS 1000 /**< 开机后:按下 <1s 松开为短按切档 */
+#define BOARD_KEY_POWER_MS 3000 /**< 关机态:按住 3s 开机;开机态:按住 3s 关机 */
+#define BOARD_KEY_PAIR_MS 5000 /**< 关机态:按住 5s 开机并进配对 */
+#define BOARD_KEY_OTA_MS 12000 /**< 关机态:按住 12s 开机并进 OTA */
+#define BOARD_KEY_DEBOUNCE_MS 20 /**< 按键消抖 */
+#define BOARD_PAIR_TIMEOUT_MS (120u * 1000u) /**< 配对超时 120s,失败后保持开机 */
+
+/*============================================================================*/
+/* LED(单灯 PC2,事件表见 led_manager.c) */
+/*============================================================================*/
+
+#define BOARD_LED_FAST_ON_MS 100 /**< 配对快闪:亮 */
+#define BOARD_LED_FAST_OFF_MS 100 /**< 配对快闪:灭 */
+#define BOARD_LED_OTA_ON_MS 200 /**< OTA 闪:亮 */
+#define BOARD_LED_OTA_OFF_MS 200 /**< OTA 闪:灭 */
+#define BOARD_LED_PAIR_OK_MS 3000 /**< 配对成功常亮时长(允许直接常亮) */
+#define BOARD_LED_PAIR_FAIL_MS 3000 /**< 配对失败 200ms 闪提示时长 */
+#define BOARD_LED_STANDBY_ON 1 /**< 1=开机常亮 */
+
+/*============================================================================*/
+/* 风扇 PWM */
+/*============================================================================*/
+
+#define BOARD_FAN_PWM_HZ 20000 /**< MCPWM 频率(Hz),20k 避开可听啸叫 */
+#define BOARD_FAN_DUTY_GEAR0 0 /**< 0% */
+#define BOARD_FAN_DUTY_GEAR1 5000 /**< 50%(用户拍板,2026-08-31) */
+#define BOARD_FAN_DUTY_GEAR2 7500 /**< 75%(用户拍板,2026-08-31) */
+#define BOARD_FAN_DUTY_GEAR3 10000 /**< 100% */
+
+/*============================================================================*/
+/* 显示(CT1642) */
+/*============================================================================*/
+
+/**
+ * CT1642 动态扫描节拍(ms)。
+ * ⚠ CT1642 内部只有单个 12 位锁存器(4 位选 + 8 段码),无按位显示 RAM、不自动扫描,
+ * 一次只点亮当前锁存的那 1 位。必须 MCU 持续轮询 4 位才能 4 位同亮、不闪。
+ * 本节拍每 BOARD_DISP_SCAN_MS 点亮 1 位、4 位轮询 → 每位 4*BOARD_DISP_SCAN_MS 一轮。
+ * 4ms:每位 16ms/62.5Hz、25% 占空,均匀不闪,且不饿死 priority=1 扫描中断里的
+ * 主循环喂狗(实测 2ms 节拍会导致上电硬复位,禁止)。
+ * 注:SCAN_TEST=1 段扫描用"值变化才刷"机制,本节拍不敏感;正常显示(0)与
+ * 位选验证(2)用分时轮询,必须 ≤4ms 否则肉眼可见闪烁。
+ */
+#define BOARD_DISP_SCAN_MS 4
+
+#define BOARD_DISP_BLINK_MS 500 /**< 告警闪烁半周期(P1 用) */
+
+/**
+ * 告警闪烁调试宏(P1 台上验证用,正常固件保持 0):
+ * 0 = 关:闪烁由 app_disp_set_temp/humi_alarm 驱动(P2 条件引擎喂)
+ * 1 = 强制温度管闪(模拟温度比较成立)
+ * 2 = 强制湿度管闪(模拟湿度比较成立)
+ */
+#define BOARD_DISP_ALARM_TEST 0
+
+/**
+ * CT1642 协议(18bit/帧,CLK 上升沿采样,DATA 仅在 CLK=低时改变):
+ * [0:3] C1-C4 位选(低有效,入参 com 取反后先发 C1)
+ * [4:9] 6 位保留位(固定填 1)
+ * [10:17] Q2-Q9 段码(LSB 先,seg 的 bit0 → Q2 引脚)
+ * 发完 18bit:CLK=高 时 DATA 0→1 = 锁存;CLK=低 时 DATA 0→1 = 输出刷新。
+ * 刷新一位数码管 = 发一帧;4 位连发 4 帧(每帧仅一个 Cx 位选生效)。
+ *
+ * ⚠ P0 扫段:段码/位选与硬件物理映射为"待实测"值,台上用 SCAN_TEST 扫清后修正。
+ * seg_tbl 段码位定义:bit7=DP bit6=G bit5=F bit4=E bit3=D bit2=C bit1=B bit0=A
+ * (bit0 先发 → 落到 Q2 引脚;PCB 上 Q2 接哪段即"软件 A 段"对应的物理段)
+ * COM 位选 → 物理管位映射(2026-08-29 SCAN_TEST=2 实测定型):
+ * C1=湿度十位 C2=湿度个位 C3=温度十位 C4=温度个位(默认假定温/湿相反,已对调)
+ */
+#define BOARD_DISP_COM_TEMP_TEN 0x04 /**< C3 = 温度十位(实测定型) */
+#define BOARD_DISP_COM_TEMP_ONE 0x08 /**< C4 = 温度个位(实测定型) */
+#define BOARD_DISP_COM_HUMI_TEN 0x01 /**< C1 = 湿度十位(实测定型) */
+#define BOARD_DISP_COM_HUMI_ONE 0x02 /**< C2 = 湿度个位(实测定型) */
+
+/**
+ * 扫段调试模式(P0 台上定型用,2026-08-29 已全部定型,正常固件保持 0):
+ * 0 = 正常:无采样时写死 25/60(分时轮询 + 提速移位)
+ * 1 = 段扫描:4 位同显一段,值变化才刷(无鬼影);段序已实测确认 A..G 顺序正确
+ * 2 = 位扫描:4 位分显 1/2/3/4(分时轮询);位序已实测确认 C1=湿十/C2=湿个/C3=温十/C4=温个
+ */
+#define BOARD_DISP_SCAN_TEST 0
+
+/*============================================================================*/
+/* 传感器(AHT20) */
+/*============================================================================*/
+
+#define BOARD_TH_PERIOD_MS 2000 /**< 采集周期 2s(手册要求 >1s,规格书第 2.3/18 章) */
+#define BOARD_TH_FAIL_MAX 3 /**< 连续失败 3 次判传感器故障 */
+#define BOARD_AHT20_WAIT_MS 80 /**< 测量等待时长 */
+#define BOARD_AHT20_I2C_HZ 100000 /**< 软件 I2C 时钟 */
+
+/*============================================================================*/
+/* 模式(P1 起用,P0 关闭) */
+/*============================================================================*/
+
+#define BOARD_MODE_VENT_ON_MS (25u * 60u * 1000u) /**< 通风开段 25min */
+#define BOARD_MODE_VENT_OFF_MS (5u * 60u * 1000u) /**< 通风停段 5min */
+#define BOARD_MODE_POWER_ON_MS (15u * 60u * 1000u) /**< 强力一轮 15min */
+
+#endif /* __BOARD_PIN_H__ */
diff --git a/apps/usr_periph/bsp_aht20.c b/apps/usr_periph/bsp_aht20.c
new file mode 100644
index 0000000..a7c6a96
--- /dev/null
+++ b/apps/usr_periph/bsp_aht20.c
@@ -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 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
+}
diff --git a/apps/usr_periph/bsp_aht20.h b/apps/usr_periph/bsp_aht20.h
new file mode 100644
index 0000000..56c9d41
--- /dev/null
+++ b/apps/usr_periph/bsp_aht20.h
@@ -0,0 +1,48 @@
+/******************************************************************************
+ * @file bsp_aht20.h
+ * @brief CBR3 AHT20 温湿度传感器:软件 I2C + 测量 + CRC8
+ * @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
+ * - V1.0.2, 2026.08.29, cyWu, 回退诊断代码,保留时序修复/上拉/总线恢复/0x71 校准
+ * - V1.0.3, 2026.08.29, cyWu, IO 分离:AHT20 独占 PC4(SCL)/PC5(SDA),删除
+ * bsp_bus 共线锁与 measure_start/finish 拆分锁,恢复一次完成采集
+ *
+ * @note I2C 7 位地址 0x38(写 0x70 / 读 0x71)。测量命令 0xAC 0x33 0x00,
+ * 等 ≥80ms 后读 7 字节:Status + 20bit 湿度 + 20bit 温度 + CRC8。
+ * CRC8 多项式 0x31,初值 0xFF,覆盖前 6 字节。
+ * PC4=SCL(推挽)/ PC5=SDA(开漏+内部上拉),与 CT1642 的 PG0/PG1
+ * 完全独立,无总线竞争(2026-08-29 改板分离)。
+ ******************************************************************************/
+
+#ifndef __BSP_AHT20_H__
+#define __BSP_AHT20_H__
+
+#include
+
+/**
+ * @brief 初始化:配置 PC4/PC5(上拉+die)+ 软件 I2C 总线复位 + 0x71 校准检查
+ * @return 无
+ */
+void bsp_aht20_init(void);
+
+/**
+ * @brief 发起一次测量:发 0xAC 0x33 0x00
+ * @note 调用后必须等 ≥80ms 再 bsp_aht20_measure_finish。
+ * 等待期间 I2C 总线空闲(AHT20 内部测量),CT1642 显示不受影响。
+ * @return 0=命令已发出,非 0=失败(未初始化/通讯失败)
+ */
+int8_t bsp_aht20_measure_start(void);
+
+/**
+ * @brief 读取测量结果:读 7 字节 + CRC8 校验 + 换算
+ * @param temp_x10 输出温度 ×10(如 25.3℃ → 253)
+ * @param humi_pct 输出湿度 0~100 整数 %
+ * @return 0=成功,非 0=失败(CRC 错/时序错,本次采样视为失败)
+ */
+int8_t bsp_aht20_measure_finish(int16_t *temp_x10, uint8_t *humi_pct);
+
+#endif /* __BSP_AHT20_H__ */
diff --git a/apps/usr_periph/bsp_ct1642.c b/apps/usr_periph/bsp_ct1642.c
new file mode 100644
index 0000000..300cc21
--- /dev/null
+++ b/apps/usr_periph/bsp_ct1642.c
@@ -0,0 +1,153 @@
+/******************************************************************************
+ * @file bsp_ct1642.c
+ * @brief CBR3 CT1642 类数码管驱动实现(软件移位)
+ * @author cyWu <1917507415@qq.com>
+ * @date 2026.08.29
+ * @version V1.0.1
+ * @history
+ * - V1.0.0, 2026.08.29, cyWu, 首次发布
+ * - V1.0.1, 2026.08.29, cyWu, IO 分离:CT1642 独立 PG0=CLK/PG1=DATA,删除
+ * bsp_bus 共线机制;删调试遗留 latch/show4/scan_seg/scan_com,
+ * 新增 bsp_ct1642_init()
+ *
+ * @note 驱动协议严格参照 CT1642 手册(与 CSDN yitx 一致):
+ * 一帧 = 18bit,CLK 上升沿采样 DATA(DATA 只在 CLK=低时改变):
+ * [0:3] C1-C4 位选(低有效,入参 com 取反后发送,先发 C1)
+ * [4:9] 6 位保留位(固定填 1)
+ * [10:17] Q2-Q9 段码(LSB 先,seg 的 bit0 → Q2 引脚)
+ * 发完 18bit 后:CLK=高时 DATA 0→1 = 锁存;CLK=低时 DATA 0→1 = 输出。
+ * 段码/位选物理映射由 PCB 决定,SCAN_TEST 扫段/扫位已定型(见 board_pin.h)。
+ * 本驱动独立使用 PG0/PG1,与 AHT20 的 PC4/PC5 互不干扰。
+ ******************************************************************************/
+
+#include "bsp_ct1642.h"
+#include "board_pin.h"
+#include "asm/gpio.h"
+
+/* 软件延时:1 次空循环(24MHz 下 ~0.1us 量级,gpio 调用本身开销另计)。
+ * ⚠ 提速历程:8→2(鬼影明显→一点点)→1(继续压残余微亮)。
+ * 若出现显示乱码/错位(低于 CT1642 最小脉宽),回调到 2。
+ * 软件压到极限后残余的微亮 = 硬件层(Q2-Q9 段线加 1~10k 下拉可根除)。 */
+static void ct1642_delay(void)
+{
+ volatile uint32_t i;
+
+ for (i = 0; i < 1; i++) {
+ /* nop */
+ }
+}
+
+/**
+ * @brief 初始化 PG0(CLK)/PG1(DATA) 为推挽输出,初始 0
+ * @note ⚠ 必须在第一次发帧前调用(app_disp_init 内)。
+ * CT1642 为纯输入从机,两根线全程推挽驱动即可,无需上拉/开漏。
+ * @return 无
+ */
+void bsp_ct1642_init(void)
+{
+ gpio_set_pull_up(BOARD_DISP_CLK_PIN, 0);
+ gpio_set_pull_down(BOARD_DISP_CLK_PIN, 0);
+ gpio_set_die(BOARD_DISP_CLK_PIN, 1);
+ gpio_direction_output(BOARD_DISP_CLK_PIN, 0);
+
+ gpio_set_pull_up(BOARD_DISP_DATA_PIN, 0);
+ gpio_set_pull_down(BOARD_DISP_DATA_PIN, 0);
+ gpio_set_die(BOARD_DISP_DATA_PIN, 1);
+ gpio_direction_output(BOARD_DISP_DATA_PIN, 0);
+}
+
+static void ct1642_clk(uint8_t level)
+{
+ gpio_direction_output(BOARD_DISP_CLK_PIN, level);
+ ct1642_delay();
+}
+
+static void ct1642_data(uint8_t level)
+{
+ gpio_direction_output(BOARD_DISP_DATA_PIN, level);
+ ct1642_delay();
+}
+
+/**
+ * @brief 7 段数码管数字转段码(共阴,A=bit0 ... G=bit6,DP=bit7)
+ * @param digit 0~9;10='-'(G 段);11=全灭
+ * @return 段码
+ */
+uint8_t bsp_ct1642_seg(uint8_t digit)
+{
+ static const uint8_t seg_tbl[12] = {
+ 0x3F, /* 0: abcdef */
+ 0x06, /* 1: bc */
+ 0x5B, /* 2: abdeg */
+ 0x4F, /* 3: abcdg */
+ 0x66, /* 4: bcfg */
+ 0x6D, /* 5: acdfg */
+ 0x7D, /* 6: acdefg */
+ 0x07, /* 7: abc */
+ 0x7F, /* 8: abcdefg */
+ 0x6F, /* 9: abcdfg */
+ 0x40, /* '-': g */
+ 0x00, /* 灭 */
+ };
+
+ if (digit > 11) {
+ digit = 11;
+ }
+ return seg_tbl[digit];
+}
+
+/**
+ * @brief 发一帧 18bit(不含消影):C1-C4 → 6保留 → Q2-Q9,再锁存+输出
+ * @note 内部函数;com/seg 的消影由 bsp_ct1642_send_frame 负责。
+ */
+static void ct1642_xfer(uint8_t seg, uint8_t com)
+{
+ uint8_t i;
+ uint8_t posn = (uint8_t)(~com); /* C1-C4 低有效:位0=选中位 */
+
+ /* 起始空闲:CLK=低,DATA=高(规格“上电 CLK 低、DATA 高”) */
+ ct1642_data(1);
+ ct1642_clk(0);
+
+ /* 1. 移位 18bit:C1-C4(4) → 6位保留(填1) → Q2-Q9(8,LSB=Q2=seg bit0) */
+ for (i = 0; i < 4; i++) { /* C1-C4,先发 C1 */
+ ct1642_data((uint8_t)(posn & 0x01u));
+ ct1642_clk(0); ct1642_clk(1); /* 下降沿改完,上升沿采样 */
+ posn >>= 1;
+ }
+ for (i = 0; i < 6; i++) { /* 6 位保留,固定填 1 */
+ ct1642_data(1);
+ ct1642_clk(0); ct1642_clk(1);
+ }
+ for (i = 0; i < 8; i++) { /* Q2-Q9 段码,先发 Q2(=seg bit0) */
+ ct1642_data((uint8_t)(seg & 0x01u));
+ ct1642_clk(0); ct1642_clk(1);
+ seg >>= 1;
+ }
+
+ /* 2. 锁存:CLK 保持高,DATA 给 0→1 上升沿 */
+ ct1642_clk(1);
+ ct1642_data(0);
+ ct1642_data(1);
+
+ /* 3. 输出:CLK 拉低,DATA 给 0→1 上升沿(刷新该位显示) */
+ ct1642_clk(0);
+ ct1642_data(0);
+ ct1642_data(1);
+}
+
+/**
+ * @brief 发一帧刷新一位数码管(CT1642 18bit 协议)
+ * @param seg 段码(bit7=DP ... bit0=A,与 seg_tbl 一致;先发 bit0→Q2)
+ * @param com COM 位选掩码(bit3=C4 ... bit0=C1,仅 1 位置位;硬件低有效内部取反)
+ * @note ⚠ 不做任何消影预帧:CT1642 移位过程实时输出,“持续刷新”才是鬼影根因
+ * (切位瞬间杂段),预帧/全灭帧都无法根除,且每帧翻倍会饿死 ISR 喂狗导致
+ * 上电复位(实测)。鬼影的正确解法在应用层:数字没变就不刷新(值变化才
+ * 刷一次,其余时间锁存静止显示)。本函数保持单帧、极短,适配 ISR 上下文。
+ * @return 无
+ */
+void bsp_ct1642_send_frame(uint8_t seg, uint8_t com)
+{
+ // ct1642_xfer(0, 0);
+ ct1642_xfer(seg, com); /* 单帧:18bit 移位 + 锁存 + 输出,无消影预帧 */
+}
diff --git a/apps/usr_periph/bsp_ct1642.h b/apps/usr_periph/bsp_ct1642.h
new file mode 100644
index 0000000..6c1933d
--- /dev/null
+++ b/apps/usr_periph/bsp_ct1642.h
@@ -0,0 +1,46 @@
+/******************************************************************************
+ * @file bsp_ct1642.h
+ * @brief CBR3 CT1642 类数码管驱动:18bit 移位、锁存、送出
+ * @author cyWu <1917507415@qq.com>
+ * @date 2026.08.29
+ * @version V1.0.1
+ * @history
+ * - V1.0.0, 2026.08.29, cyWu, 首次发布
+ * - V1.0.1, 2026.08.29, cyWu, IO 分离:CT1642 独立 PG0=CLK/PG1=DATA(飞线),
+ * AHT20 独占 PC4/PC5,删除 bsp_bus 共线机制;删调试遗留
+ * latch/show4/scan_seg/scan_com,新增 bsp_ct1642_init()
+ *
+ * @note PG0=CLK,PG1=DATA,独立引脚、无总线锁,可随时调用。
+ * 协议:一帧 18bit(C1-C4 位选 + 6 位保留 + Q2-Q9 段码),CLK 上升沿采样
+ * DATA(DATA 只在 CLK=低时改变);发完 CLK=高 时 DATA 上升沿=锁存,
+ * CLK=低 时 DATA 上升沿=输出刷新。
+ ******************************************************************************/
+
+#ifndef __BSP_CT1642_H__
+#define __BSP_CT1642_H__
+
+#include
+
+/**
+ * @brief 初始化 PG0/PG1 为推挽输出(CLK/DATA 初始 0)
+ * @note app_disp_init() 内调用;被 BOARD_DISP_ENABLE 编译开关保护
+ * @return 无
+ */
+void bsp_ct1642_init(void);
+
+/**
+ * @brief 7 段数码管数字转段码(共阴,A=bit0 ... G=bit6,DP=bit7)
+ * @param digit 0~9;10='-'(G 段);11=全灭
+ * @return 段码
+ */
+uint8_t bsp_ct1642_seg(uint8_t digit);
+
+/**
+ * @brief 发一帧刷新一位:18bit(C1-C4 位选 + 6 保留 + Q2-Q9 段码,LSB 先)
+ * @param seg 段码(bit7=DP ... bit0=A,先发 bit0→Q2)
+ * @param com COM 位选掩码(bit3=C4 ... bit0=C1,仅 1 位置位;硬件低有效内部取反)
+ * @return 无
+ */
+void bsp_ct1642_send_frame(uint8_t seg, uint8_t com);
+
+#endif /* __BSP_CT1642_H__ */
diff --git a/apps/usr_periph/bsp_hw.c b/apps/usr_periph/bsp_hw.c
new file mode 100644
index 0000000..7a75983
--- /dev/null
+++ b/apps/usr_periph/bsp_hw.c
@@ -0,0 +1,225 @@
+/******************************************************************************
+ * @file bsp_hw.c
+ * @brief CBR3 板级 GPIO / MCPWM:LED、按键、风扇
+ * @author cyWu <1917507415@qq.com>
+ * @date 2026.08.29
+ * @version V1.0.0
+ * @history
+ * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹:删电机/充电/电量,改单 LED + 风扇 PWM)
+ *
+ * @note CT1642 在 PG0/PG1、AHT20 在 PC4/PC5,各自独立驱动,
+ * bsp_hw 只管 LED/KEY/FAN,不碰这两组脚。
+ ******************************************************************************/
+
+#include "system/includes.h"
+#include "asm/gpio.h"
+#include "asm/mcpwm.h"
+#include "bsp_hw.h"
+
+#define LOG_TAG_CONST APP
+#define LOG_TAG "[BSP_HW]"
+#define LOG_ERROR_ENABLE
+#define LOG_INFO_ENABLE
+#include "debug.h"
+
+static uint8_t s_initialized;
+
+/** 风扇 PWM 状态 */
+typedef struct {
+ uint8_t pwm_clk_on; /**< 1=MCPWM 时钟已打开 */
+ uint16_t duty; /**< 当前占空比 */
+} BspFanCtx_t;
+
+static BspFanCtx_t s_fan;
+
+#if (BOARD_LED_ENABLE || BOARD_KEY_ENABLE)
+/**
+ * @brief 配置 GPIO 为推挽输出
+ * @param pin GPIO 编号
+ * @param value 输出初值
+ * @return 无
+ */
+static void bsp_pin_output(u32 pin, int value)
+{
+ gpio_set_pull_up(pin, 0);
+ gpio_set_pull_down(pin, 0);
+ gpio_set_die(pin, 1);
+ gpio_direction_output(pin, value);
+}
+#endif
+
+#if BOARD_KEY_ENABLE
+/**
+ * @brief 配置 GPIO 为数字输入(可内部上拉)
+ * @param pin GPIO 编号
+ * @param pull_up 1=内部上拉,0=不上拉
+ * @return 无
+ */
+static void bsp_pin_input(u32 pin, int pull_up)
+{
+ gpio_direction_output(pin, 1);
+ gpio_set_direction(pin, 1);
+ gpio_set_pull_down(pin, 0);
+ gpio_set_pull_up(pin, pull_up ? 1 : 0);
+ gpio_set_die(pin, 1);
+}
+#endif
+
+#if BOARD_FAN_ENABLE
+/**
+ * @brief 初始化风扇 MCPWM ch0,初始占空比 0(关)
+ * @return 无
+ */
+static void bsp_fan_pwm_init(void)
+{
+ struct pwm_platform_data pwm;
+
+ gpio_set_pull_up(BOARD_FAN_PIN, 0);
+ gpio_set_pull_down(BOARD_FAN_PIN, 0);
+ gpio_set_die(BOARD_FAN_PIN, 1);
+
+ pwm.pwm_aligned_mode = pwm_edge_aligned;
+ pwm.frequency = BOARD_FAN_PWM_HZ;
+ pwm.duty = 0;
+ pwm.l_pin = (u8) - 1;
+ pwm.complementary_en = 0;
+ pwm.pwm_ch_num = pwm_ch0;
+ pwm.h_pin = BOARD_FAN_PIN;
+ mcpwm_init(&pwm);
+
+ mcpwm_set_duty(pwm_ch0, 0);
+ s_fan.pwm_clk_on = 1;
+ s_fan.duty = 0;
+ log_info("fan pwm %uHz duty=0\n", (unsigned)BOARD_FAN_PWM_HZ);
+}
+
+/**
+ * @brief 打开 MCPWM 并绑回 PC3
+ * @return 无
+ */
+static void bsp_fan_pwm_clock_on(void)
+{
+ if (s_fan.pwm_clk_on) {
+ return;
+ }
+ mcpwm_open(pwm_ch0);
+ gpio_och_sel_output_signal(BOARD_FAN_PIN, OUTPUT_CH_SIGNAL_MC_PWM0_H);
+ gpio_set_direction(BOARD_FAN_PIN, 0);
+ s_fan.pwm_clk_on = 1;
+}
+
+/**
+ * @brief 关闭 MCPWM 时钟并拉低(待机/关机必关)
+ * @return 无
+ */
+static void bsp_fan_pwm_clock_off(void)
+{
+ mcpwm_set_duty(pwm_ch0, 0);
+ if (s_fan.pwm_clk_on) {
+ mcpwm_close(pwm_ch0);
+ gpio_och_disable_output_signal(BOARD_FAN_PIN, OUTPUT_CH_SIGNAL_MC_PWM0_H);
+ JL_MCPWM->MCPWM_CON0 = 0;
+ s_fan.pwm_clk_on = 0;
+ }
+ gpio_direction_output(BOARD_FAN_PIN, 0); /* 关 = 拉低(风扇高导通) */
+}
+#endif
+
+/**
+ * @brief 初始化 GPIO / 风扇 MCPWM
+ * @return BSP_HW_OK=成功
+ */
+BspHw_Ret_t bsp_hw_init(void)
+{
+ if (s_initialized) {
+ return BSP_HW_OK;
+ }
+
+#if BOARD_LED_ENABLE
+ bsp_pin_output(BOARD_LED_PIN, !BOARD_LED_ON_LEVEL); /* 初始灭 */
+#endif
+
+#if BOARD_KEY_ENABLE
+ bsp_pin_input(BOARD_KEY_PIN, 1);
+#endif
+
+#if BOARD_FAN_ENABLE
+ bsp_fan_pwm_init();
+ bsp_fan_pwm_clock_off(); /* 初始关,待机不耗电 */
+#endif
+
+ s_initialized = 1;
+ log_info("hw init led=%d key=%d fan=%d\n",
+ BOARD_LED_ENABLE, BOARD_KEY_ENABLE, BOARD_FAN_ENABLE);
+ return BSP_HW_OK;
+}
+
+/**
+ * @brief 查询板级硬件是否已初始化
+ * @return BSP_HW_OK=已初始化,BSP_HW_ERR_NOT_INIT=未初始化
+ */
+BspHw_Ret_t bsp_hw_get_status(void)
+{
+ return s_initialized ? BSP_HW_OK : BSP_HW_ERR_NOT_INIT;
+}
+
+/**
+ * @brief 状态 LED 开关(PC2,低电平亮)
+ * @param on 1=亮,0=灭
+ * @return 无
+ */
+void bsp_led_set(uint8_t on)
+{
+#if BOARD_LED_ENABLE
+ gpio_direction_output(BOARD_LED_PIN, on ? BOARD_LED_ON_LEVEL : !BOARD_LED_ON_LEVEL);
+#else
+ (void)on;
+#endif
+}
+
+/**
+ * @brief 风扇 PWM 占空比(PC3,高有效,20kHz)
+ * @param duty 0~10000(0%=关 MCPWM 并拉低;>0 打开时钟输出)
+ * @return 无
+ */
+void bsp_fan_set_duty(uint16_t duty)
+{
+#if BOARD_FAN_ENABLE
+ if (duty > 10000) {
+ duty = 10000;
+ }
+ if (duty == 0) {
+ bsp_fan_pwm_clock_off();
+ s_fan.duty = 0;
+ return;
+ }
+ bsp_fan_pwm_clock_on();
+ mcpwm_set_duty(pwm_ch0, duty);
+ s_fan.duty = duty;
+#else
+ (void)duty;
+#endif
+}
+
+/**
+ * @brief 读取按键
+ * @return 1=按下,0=未按下
+ */
+uint8_t bsp_key_is_pressed(void)
+{
+#if BOARD_KEY_ENABLE
+ return (uint8_t)(gpio_read(BOARD_KEY_PIN) == BOARD_KEY_ACTIVE_LEVEL);
+#else
+ return 0;
+#endif
+}
+
+/**
+ * @brief 进深睡前:风扇 PWM 关掉并拉低,灯全灭
+ * @return 无
+ */
+void bsp_hw_enter_sleep_io(void)
+{
+ bsp_fan_set_duty(0);
+ bsp_led_set(0);
+}
diff --git a/apps/usr_periph/bsp_hw.h b/apps/usr_periph/bsp_hw.h
new file mode 100644
index 0000000..f8207aa
--- /dev/null
+++ b/apps/usr_periph/bsp_hw.h
@@ -0,0 +1,61 @@
+/******************************************************************************
+ * @file bsp_hw.h
+ * @brief CBR3 板级硬件:LED / 按键 / 风扇 PWM
+ * @author cyWu <1917507415@qq.com>
+ * @date 2026.08.29
+ * @version V1.0.0
+ * @history
+ * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹:删电机/充电/电量,改单 LED + 风扇 PWM)
+ ******************************************************************************/
+
+#ifndef __BSP_HW_H__
+#define __BSP_HW_H__
+
+#include
+#include "board_pin.h"
+
+typedef enum {
+ BSP_HW_OK = 0,
+ BSP_HW_ERR_PARAM,
+ BSP_HW_ERR_NOT_INIT
+} BspHw_Ret_t;
+
+/**
+ * @brief 初始化 GPIO / 风扇 MCPWM
+ * @return BSP_HW_OK=成功
+ */
+BspHw_Ret_t bsp_hw_init(void);
+
+/**
+ * @brief 查询是否已初始化
+ * @return BSP_HW_OK=已初始化,BSP_HW_ERR_NOT_INIT=未初始化
+ */
+BspHw_Ret_t bsp_hw_get_status(void);
+
+/**
+ * @brief 状态 LED 开关(PC2,低电平亮)
+ * @param on 1=亮,0=灭
+ * @return 无
+ */
+void bsp_led_set(uint8_t on);
+
+/**
+ * @brief 风扇 PWM 占空比(PC3,高有效,20kHz)
+ * @param duty 0~10000(0%=关 MCPWM 并拉低;>0 打开时钟输出)
+ * @return 无
+ */
+void bsp_fan_set_duty(uint16_t duty);
+
+/**
+ * @brief 读取按键
+ * @return 1=按下,0=未按下
+ */
+uint8_t bsp_key_is_pressed(void);
+
+/**
+ * @brief 进深睡前:风扇 PWM 关掉并拉低,灯全灭
+ * @return 无
+ */
+void bsp_hw_enter_sleep_io(void);
+
+#endif /* __BSP_HW_H__ */
diff --git a/dp_definition.json b/dp_definition.json
index 11e2b2b..c6cb4e6 100644
--- a/dp_definition.json
+++ b/dp_definition.json
@@ -99,10 +99,10 @@
"key": "cond_config1",
"kind": "normal",
"type": "raw",
- "len": 6,
+ "len": 7,
"scale": 1.0,
"dir": "rw",
- "description": "温度\n温度精度是0.1,需要*10换算成整数进行传输\nByte 5\nBit7 ~ Bit5:温度比较关系\nBit4 ~ Bit2:湿度比较关系\nBit1 :Reserved\nBit0 :Reserved\n比较关系建议统一定义\n000 = 无条件\n001 = <\n010 = <=\n011 = =\n100 = >=\n101 = >\n110 = Reserved\n111 = Reserved",
+ "description": "温度阈值:有符号int16大端,值=℃×10;Byte0=使能;Byte1-2=温度阈值;Byte3=湿度阈值;Byte4=持续分钟(0=立即);Byte5=动作模式(0/1/2);Byte6=Bit7-5温度比较, Bit4-2湿度比较, Bit1-0保留",
"values": {}
},
{
@@ -111,10 +111,10 @@
"key": "cond_config2",
"kind": "normal",
"type": "raw",
- "len": 6,
+ "len": 7,
"scale": 1.0,
"dir": "rw",
- "description": "同上",
+ "description": "同上(7字节)",
"values": {}
},
{
@@ -123,10 +123,10 @@
"key": "cond_config3",
"kind": "normal",
"type": "raw",
- "len": 6,
+ "len": 7,
"scale": 1.0,
"dir": "rw",
- "description": "同上",
+ "description": "同上(7字节)",
"values": {}
},
{
@@ -135,10 +135,10 @@
"key": "cond_config4",
"kind": "normal",
"type": "raw",
- "len": 6,
+ "len": 7,
"scale": 1.0,
"dir": "rw",
- "description": "同上",
+ "description": "同上(7字节)",
"values": {}
},
{
@@ -147,10 +147,10 @@
"key": "cond_config5",
"kind": "normal",
"type": "raw",
- "len": 6,
+ "len": 7,
"scale": 1.0,
"dir": "rw",
- "description": "同上",
+ "description": "同上(7字节)",
"values": {}
},
{
@@ -159,10 +159,10 @@
"key": "cond_config6",
"kind": "normal",
"type": "raw",
- "len": 6,
+ "len": 7,
"scale": 1.0,
"dir": "rw",
- "description": "同上",
+ "description": "同上(7字节)",
"values": {}
},
{
@@ -171,10 +171,10 @@
"key": "cond_config7",
"kind": "normal",
"type": "raw",
- "len": 6,
+ "len": 7,
"scale": 1.0,
"dir": "rw",
- "description": "同上",
+ "description": "同上(7字节)",
"values": {}
},
{