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

新增 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
+159
View File
@@ -0,0 +1,159 @@
/******************************************************************************
* @file app_arbiter.c
* @brief CBR3 模式仲裁器实现(规格书第 12 章:手动 > 定时 > 条件)
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.31, cyWu, 首次发布(P2
*
* @note 本模块是唯一模式占用源,运行在 usr_timer_loop(任务上下文)。
* 规则落地(规格书 12 章):
* - 手动动作 → manual_intervene()owner=NONE
* 条件引擎感知后作废本轮(RAM,须回安全再满时长才重新触发);
* 定时引擎感知后本窗剩余时间不自动抢回(等下一窗开始沿)。
* - 定时窗开始 → timer_take()(定时 > 条件,条件即使满时长也不开);
* - 条件触发 → cond_take()(仅 owner!=TIMER 允许;快照只存第一次);
* - 两路条件后触发覆盖 → 仍走 cond_take(),快照不变(不回栈)。
******************************************************************************/
#include "app_arbiter.h"
#include "app_mode.h"
#include "app_fan.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[ARB]"
#define LOG_INFO_ENABLE
#include "debug.h"
/** 仲裁器上下文 */
typedef struct {
ArbOwner_t owner; /**< 当前占用者 */
uint8_t snap_valid; /**< 条件快照是否有效 */
uint8_t snap_mode; /**< 快照模式(第一次条件抢权前) */
uint8_t snap_gear; /**< 快照档位 */
} AppArbiterCtx_t;
static AppArbiterCtx_t s_arb;
/**
* @brief 初始化仲裁器
* @return 无
*/
void app_arbiter_init(void)
{
s_arb.owner = ARB_OWNER_NONE;
s_arb.snap_valid = 0;
s_arb.snap_mode = 0;
s_arb.snap_gear = 0;
log_info("arbiter init\n");
}
/**
* @brief 当前模式占用者
* @return ArbOwner_t
*/
uint8_t app_arbiter_get_owner(void)
{
return (uint8_t)s_arb.owner;
}
/**
* @brief 手动动作统一入口:释放一切自动化占用
* @note 条件/定时引擎下个 tick 查 owner 感知后各自作废;不恢复快照
* (手动已接管,快照对应的用户状态已过时)
* @return 无
*/
void app_arbiter_manual_intervene(void)
{
if (s_arb.owner != ARB_OWNER_NONE) {
log_info("manual intervene, owner %d -> none\n", s_arb.owner);
}
s_arb.owner = ARB_OWNER_NONE;
}
/**
* @brief 条件引擎尝试占用
* @return 1=成功;0=定时占用中(规格书 12:定时 vs 条件 → 定时胜)
*/
uint8_t app_arbiter_cond_take(void)
{
if (s_arb.owner == ARB_OWNER_TIMER) {
return 0; /* 定时窗内条件不触发 */
}
if (!s_arb.snap_valid) {
/* 第一次条件抢权前快照用户状态;后覆盖/多次触发不改快照(10.3-6) */
s_arb.snap_mode = app_mode_get_active();
s_arb.snap_gear = app_fan_get_gear();
s_arb.snap_valid = 1;
log_info("cond take, snapshot mode=%d gear=%d\n", s_arb.snap_mode, s_arb.snap_gear);
}
s_arb.owner = ARB_OWNER_COND;
return 1;
}
/**
* @brief 条件是否仍在占用
* @return 1=owner==COND
*/
uint8_t app_arbiter_cond_active(void)
{
return (s_arb.owner == ARB_OWNER_COND) ? 1 : 0;
}
/**
* @brief 条件自然失效:恢复快照后调用(清 owner 与快照)
* @return 无
*/
void app_arbiter_cond_release(void)
{
if (s_arb.owner == ARB_OWNER_COND) {
s_arb.owner = ARB_OWNER_NONE;
}
s_arb.snap_valid = 0;
}
/**
* @brief 取条件快照
* @param mode 输出模式
* @param gear 输出档位
* @return 无
*/
void app_arbiter_cond_get_snapshot(uint8_t *mode, uint8_t *gear)
{
*mode = s_arb.snap_mode;
*gear = s_arb.snap_gear;
}
/**
* @brief 条件被手动打断:只清快照(不恢复、不清 owner)
* @return 无
*/
void app_arbiter_cond_snapshot_clear(void)
{
s_arb.snap_valid = 0;
}
/**
* @brief 定时窗开始占用(优先级高于条件)
* @return 1=成功
*/
uint8_t app_arbiter_timer_take(void)
{
if (s_arb.owner != ARB_OWNER_TIMER) {
log_info("timer take\n");
}
s_arb.owner = ARB_OWNER_TIMER;
return 1;
}
/**
* @brief 定时窗结束释放(调用方负责收口:风扇 0、mode 0)
* @return 无
*/
void app_arbiter_timer_release(void)
{
if (s_arb.owner == ARB_OWNER_TIMER) {
s_arb.owner = ARB_OWNER_NONE;
}
}
+105
View File
@@ -0,0 +1,105 @@
/******************************************************************************
* @file app_arbiter.h
* @brief CBR3 模式仲裁器(规格书第 12 章):手动 > 定时 > 条件
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.31, cyWu, 首次发布(P2
*
* @note 把"谁在驱动模式"收成一处,避免 mode/cond/timer 互调成环。
* 唯一状态源是 ownerNONE/COND/TIMER+ 条件快照;
* 条件/定时引擎各自维护占用细节,通过查询 owner 感知被打断:
* - 手动动作(短按/APP 写档/mode)→ app_arbiter_manual_intervene()
* 清 owner;条件引擎感知后作废本轮(RAM),定时引擎感知后本窗
* 不再抢回(规格书 12 章规则 1/2)。
* - 条件自然失效(比较不成立)→ 条件引擎恢复快照后
* app_arbiter_cond_release()。
* - 定时窗结束 → app_arbiter_timer_release() + 收口。
* 快照:第一次条件抢权前保存用户 mode+gear(规格书 10.3-3),
* 后覆盖/手动介入均不更新它(10.3-6),只在条件自然失效恢复后清空。
******************************************************************************/
#ifndef __APP_ARBITER_H__
#define __APP_ARBITER_H__
#include <stdint.h>
/** 当前模式占用者 */
typedef enum {
ARB_OWNER_NONE = 0, /**< 空闲 / 手动接管 */
ARB_OWNER_COND = 1, /**< 条件引擎占用 */
ARB_OWNER_TIMER = 2 /**< 定时引擎占用 */
} ArbOwner_t;
/**
* @brief 初始化仲裁器
* @return 无
*/
void app_arbiter_init(void);
/**
* @brief 当前模式占用者
* @return ArbOwner_t
*/
uint8_t app_arbiter_get_owner(void);
/**
* @brief 手动动作统一入口(短按 / APP 写 fan_gear / APP 写 mode
* @note 立即释放一切自动化占用;条件/定时引擎下个 tick 感知后各自作废
* @return 无
*/
void app_arbiter_manual_intervene(void);
/* ═══ 条件引擎接口 ═══ */
/**
* @brief 条件引擎尝试占用(触发模式前调用)
* @note 定时占用中返回 0(规格书 12:定时窗 vs 条件 → 定时胜,条件不开);
* 占用成功时若尚无快照则保存当前用户 mode+gear(仅第一次)
* @return 1=占用成功,0=被定时占用拒绝
*/
uint8_t app_arbiter_cond_take(void);
/**
* @brief 条件是否仍在占用
* @return 1=owner==COND
*/
uint8_t app_arbiter_cond_active(void);
/**
* @brief 条件自然失效:恢复快照后调用(清 owner 与快照)
* @return 无
*/
void app_arbiter_cond_release(void);
/**
* @brief 取条件快照(恢复用)
* @param mode 输出快照模式
* @param gear 输出快照档位
* @return 无
*/
void app_arbiter_cond_get_snapshot(uint8_t *mode, uint8_t *gear);
/**
* @brief 条件被手动打断:只清快照(快照对应的用户状态已过时),
* 不恢复、不清 ownerowner 已被 manual_intervene 清掉)
* @return 无
*/
void app_arbiter_cond_snapshot_clear(void);
/* ═══ 定时引擎接口 ═══ */
/**
* @brief 定时引擎窗开始占用
* @return 1=成功(恒成功,定时优先级高于条件)
*/
uint8_t app_arbiter_timer_take(void);
/**
* @brief 定时窗结束释放
* @return 无
*/
void app_arbiter_timer_release(void);
#endif /* __APP_ARBITER_H__ */
+132
View File
@@ -0,0 +1,132 @@
/******************************************************************************
* @file app_boot.c
* @brief CBR3 上电按键判定实现(3s 开机 / 5s 配对 / 12s OTA
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹:删 USB 直通、删低电拒绝)
******************************************************************************/
#include "app_boot.h"
#include "app_pair.h"
#include "bsp_hw.h"
#include "board_pin.h"
#include "app_main.h"
#include "usr_le_api.h"
#include "led_manager.h"
#include "asm/power/power_api.h"
#include "asm/power/power_reset.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_BOOT]"
#define LOG_INFO_ENABLE
#include "debug.h"
extern void clr_wdt(void);
extern u32 timer_get_ms(void);
/**
* @brief 忙等若干毫秒,并喂狗
* @param ms 等待时长
* @return 无
*/
static void boot_delay_ms(uint32_t ms)
{
uint32_t t0 = timer_get_ms();
while ((timer_get_ms() - t0) < ms) {
clr_wdt();
}
}
/**
* @brief BLE 起来前进深睡(业务定时器尚未启动),原地喂狗不返回
* @note power_set_soft_poweroff() 只是向 SDK 底层发起软关机请求(置个标志位),
* 不是同步断电——真正切电源是底层电源管理在之后某个时机才执行的。
* 此时 BLE 栈、业务定时器都还没起来,如果直接 return,代码会接着把
* usr_jb_init 剩下的初始化跑完,等于白判定。所以必须用 while(1) 卡住:
* 一边喂狗防止看门狗打断,一边等底层真正把电断掉。
* @return 无(不会返回)
*/
static void boot_sleep(void)
{
log_info("boot sleep\n");
bsp_hw_enter_sleep_io();
power_set_soft_poweroff();
while (1) {
clr_wdt();
}
}
/**
* @brief 等待并判定开机按键
* @note 配对(5s)不在到点时立即提交:继续按住观察是否会按到 12s 触发 OTA,
* 真正的配对提交放在松手那一刻做。按满 5s 灯先切到配对快闪提示。
* 3s 常亮 / 5s 快闪直接复用 led_manager 的 LED_EVENT_STANDBY /
* LED_EVENT_PAIRING 事件。LED 事件库由调用方(usr_jb_init)在本函数
* 之前完成初始化,这里直接假定可用。
* @return 无(深睡分支不返回)
*/
void app_boot_wait_key(void)
{
uint32_t t0;
uint32_t elapsed;
uint8_t pair_armed = 0;
usr_var.usr_goto_pair = 0;
/* ⚠ 无 USB 直通:插电/冷启动一律走按键判定(规格书 5.1)。 */
if (!bsp_key_is_pressed()) {
log_info("boot no key, sleep\n");
boot_sleep();
}
t0 = timer_get_ms();
log_info("boot key hold, wait 3s on / 5s pair / 12s ota\n");
while (bsp_key_is_pressed()) {
elapsed = timer_get_ms() - t0;
clr_wdt();
/* 12s 优先于配对:按满 12s 直接进 OTA,不再等松手 */
if (elapsed >= BOARD_KEY_OTA_MS) {
log_info("boot hold %ums, power on + ota\n", (unsigned)elapsed);
usr_app_ota_init();
return;
}
/* 5s 只武装配对并切到快闪,真正提交放到松手那一刻 */
if (!pair_armed && (elapsed >= BOARD_KEY_PAIR_MS)) {
pair_armed = 1;
LED_EventDelete(LED_EVENT_STANDBY);
LED_EventAdd(LED_EVENT_PAIRING);
log_info("boot hold %ums, pair hint blink (release now for pair, "
"keep holding to %ums for ota)\n",
(unsigned)elapsed, (unsigned)BOARD_KEY_OTA_MS);
} else if (!pair_armed && !LED_IsEventExist(LED_EVENT_STANDBY)
&& (elapsed >= BOARD_KEY_POWER_MS)) {
LED_EventAdd(LED_EVENT_STANDBY);
log_info("boot hold 3s, will power on\n");
}
user_led_handle(); /* 按事件表当前状态刷新一次灯 */
}
elapsed = timer_get_ms() - t0;
if (elapsed < BOARD_KEY_POWER_MS) {
log_info("boot hold %ums < 3s, sleep\n", (unsigned)elapsed);
boot_sleep();
}
/* 松手时已过 5s(且没按到 12s):提交配对;否则只是正常开机 */
if (pair_armed) {
usr_var.usr_goto_pair = 1;
usr_clear_pair_info_noreset();
app_pair_start();
log_info("boot hold %ums, power on + pair\n", (unsigned)elapsed);
} else {
log_info("boot hold %ums, power on\n", (unsigned)elapsed);
}
}
+28
View File
@@ -0,0 +1,28 @@
/******************************************************************************
* @file app_boot.h
* @brief CBR3 上电按键判定:3s 开机,5s 配对,12s OTA,不足 3s 松手再睡
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹:删 USB 直通开机、删低电拒绝开机)
*
* @note 必须在 BLE 协议栈起来、业务定时器启动之前调用。杰理 jiffies 此时
* 往往 10ms 才跳一次,所以内部一律用 timer_get_ms 直接量时间。
* ⚠ 本产品 USB 常电、无电池:插电/冷启动【不会】自动进入工作态,
* 必须按住 3s(规格书 5.1/5.2)。门夹的 USB 直通开机逻辑禁止移植。
******************************************************************************/
#ifndef __APP_BOOT_H__
#define __APP_BOOT_H__
/**
* @brief 等待并判定开机按键:不足 3s 松手、或全程未按键,直接原地深睡(不返回);
* 满 3s 视为正常开机;按满 5s 绿灯改快闪提示"松手即配网",真正清绑定 /
* 开绑定广播仍等松手(内部调用 app_pair_start);持续按满 12s 直接开机
* 并进入 OTA(调用 usr_app_ota_init)。
* @return 无(深睡分支通过 while(1) 喂狗不返回;其余情况正常返回)
*/
void app_boot_wait_key(void);
#endif /* __APP_BOOT_H__ */
+464
View File
@@ -0,0 +1,464 @@
/******************************************************************************
* @file app_cond.c
* @brief CBR3 条件引擎(P2):7 组 7 字节条件,比较成立满时长触发模式
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V2.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 stub
* - V1.1.0, 2026.08.31, cyWu, P1set_en/set_config 存 VM(引擎仍关)
* - V2.0.0, 2026.08.31, cyWu, P2 实现完整条件引擎(规格书第 10 章):
* 7 字节解析/AND 比较/时长累计(采样成功才累计)/触发/保持/
* 条件强力循环/后覆盖不回栈/手动作废(RAM)/快照恢复/闪烁喂真值
*
* @note 运行上下文:app_cond_run 由 usr_timer_loop500ms,任务上下文)调用
* (规格书 8.4 时长按采样周期 2s 计,500ms 粒度足够;且触发时可能
* 要写 VM——单次定时在 app_timer_task,条件本身不写 VM,但保持
* 任务上下文与仲裁器同侧,避免跨上下文竞态)。
* 字节布局(规格书 10.2):Byte0=使能 Byte1-2=温度阈值(int16 大端×10)
* Byte3=湿度 Byte4=持续分钟(0=立即) Byte5=动作模式
* Byte6=Bit7-5 温度比较 / Bit4-2 湿度比较 / Bit1-0 保留。
* 比较编码:1=< 2=<= 3-== 4->= 5-> 0/6/7=无条件。
******************************************************************************/
#include "app_cond.h"
#include "app_arbiter.h"
#include "app_mode.h"
#include "app_fan.h"
#include "app_th.h"
#include "app_disp.h"
#include "app_nvm.h"
#include "jb_product.h"
#include "board_pin.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_COND]"
#define LOG_INFO_ENABLE
#include "debug.h"
/** 每组运行时状态(配置本体在 gDevData.cond_config1~7 */
typedef struct {
uint32_t acc_ms; /**< 比较成立且采样成功的累计毫秒(未满时长) */
uint8_t voided; /**< 1=被手动作废(RAM,不写 VM,须回安全再满时长) */
uint8_t trig_by; /**< 触发占用时 Byte5 动作(0=未在占用) */
} CondState_t;
static CondState_t s_cond[7]; /**< [0]~[6] 对应 config1~7 */
static uint8_t s_occupy_idx;/**< 当前占用组 0~60xFF=无 */
/**
* @brief 比较编码是否有效(1~5;0/6/7 视为无条件)
* @param cmp 3bit 比较编码
* @return 1=有效,0=无条件
*/
static uint8_t cond_cmp_valid(uint8_t cmp)
{
return ((cmp >= 1) && (cmp <= 5)) ? 1 : 0;
}
/**
* @brief 温度比较(当前温度 ×10 vs 阈值 ×10)
* @param cmp 比较编码
* @param th 温度阈值 ×10
* @return 1=成立
*/
static uint8_t cond_cmp_temp(uint8_t cmp, int16_t th)
{
int16_t cur = app_th_get_temp_x10();
switch (cmp) {
case 1: return (cur < th) ? 1 : 0;
case 2: return (cur <= th) ? 1 : 0;
case 3: return (cur == th) ? 1 : 0;
case 4: return (cur >= th) ? 1 : 0;
case 5: return (cur > th) ? 1 : 0;
default: return 1; /* 无条件 */
}
}
/**
* @brief 湿度比较(当前 % vs 阈值 %)
* @param cmp 比较编码
* @param th 湿度阈值 %
* @return 1=成立
*/
static uint8_t cond_cmp_humi(uint8_t cmp, uint8_t th)
{
uint8_t cur = app_th_get_humi();
switch (cmp) {
case 1: return (cur < th) ? 1 : 0;
case 2: return (cur <= th) ? 1 : 0;
case 3: return (cur == th) ? 1 : 0;
case 4: return (cur >= th) ? 1 : 0;
case 5: return (cur > th) ? 1 : 0;
default: return 1; /* 无条件 */
}
}
/**
* @brief 取第 idx 组条件配置(gDevData 里是 7 个独立数组字段)
* @param idx 0~6
* @return 配置指针;越界返回 NULL
*/
static const uint8_t *cond_cfg_ptr(uint8_t idx)
{
const uint8_t *tbl[7];
tbl[0] = gDevData.cond_config1;
tbl[1] = gDevData.cond_config2;
tbl[2] = gDevData.cond_config3;
tbl[3] = gDevData.cond_config4;
tbl[4] = gDevData.cond_config5;
tbl[5] = gDevData.cond_config6;
tbl[6] = gDevData.cond_config7;
return (idx < 7) ? tbl[idx] : NULL;
}
/**
* @brief 整组比较是否成立(两侧 AND / 单侧 / 双侧无条件=无效组)
* @param cfg 7 字节配置
* @return 1=成立,0=不成立或无效组
*/
static uint8_t cond_group_match(const uint8_t *cfg)
{
uint8_t temp_cmp = (uint8_t)((cfg[6] >> 5) & 0x7);
uint8_t humi_cmp = (uint8_t)((cfg[6] >> 2) & 0x7);
uint8_t temp_ok = cond_cmp_valid(temp_cmp);
uint8_t humi_ok = cond_cmp_valid(humi_cmp);
int16_t temp_th = (int16_t)((cfg[1] << 8) | cfg[2]); /* int16 大端 ×10 */
uint8_t humi_th = cfg[3];
/* 规格书 8.4:-- 或尚无有效采样时比较视为不成立,避免用旧值误触发 */
if (app_th_is_fault() || !app_th_get_valid()) {
return 0;
}
if (!temp_ok && !humi_ok) {
return 0; /* 两侧都无条件:无效组,不触发(10.2) */
}
if (temp_ok && humi_ok) {
return (cond_cmp_temp(temp_cmp, temp_th) && cond_cmp_humi(humi_cmp, humi_th)) ? 1 : 0;
}
if (temp_ok) {
return cond_cmp_temp(temp_cmp, temp_th);
}
return cond_cmp_humi(humi_cmp, humi_th);
}
/**
* @brief 触发时长(毫秒);Byte4=0 立即触发
* @param cfg 7 字节配置
* @return 时长 ms
*/
static uint32_t cond_duration_ms(const uint8_t *cfg)
{
return (uint32_t)cfg[4] * 60000u;
}
/**
* @brief 清全部组累计(定时窗抢权后须重新满时长,规格书 12 章)
* @return 无
*/
static void cond_reset_acc_all(void)
{
uint8_t i;
for (i = 0; i < 7; i++) {
s_cond[i].acc_ms = 0;
s_cond[i].trig_by = 0;
}
}
/**
* @brief 条件动作切模式。Byte5=0 是关风扇,不能走 set_manual(0)(那会恢复旧档)
* @param mode 0=关闭,1=通风,2=强力
* @return 无
*/
static void cond_apply_mode(uint8_t mode)
{
if (mode == 0) {
app_mode_stop();
app_fan_set_gear(0);
} else {
app_mode_set_manual(mode);
}
}
/**
* @brief 恢复快照并释放条件占用(比较不成立时,10.3-5)
* @return 无
*/
static void cond_restore_and_release(void)
{
uint8_t idx = s_occupy_idx;
uint8_t m, g;
app_arbiter_cond_get_snapshot(&m, &g);
app_mode_set_manual(m); /* 恢复快照模式(mode=0 时不动档位) */
app_fan_set_gear(g); /* 恢复快照档位 */
app_arbiter_cond_release();
s_cond[idx].acc_ms = 0;
s_cond[idx].trig_by = 0;
s_occupy_idx = 0xFF;
log_info("cond[%d] release, restore mode=%d gear=%d\n", idx + 1, m, g);
}
/**
* @brief 一组触发(占用 + 快照 + 切模式)
* @param idx 组索引 0~6
* @return 1=已触发,0=被定时占用拒绝
*/
static uint8_t cond_fire(uint8_t idx)
{
const uint8_t *cfg = cond_cfg_ptr(idx);
if (!app_arbiter_cond_take()) {
return 0; /* 定时占用中,条件不开(12 章) */
}
s_occupy_idx = idx;
s_cond[idx].trig_by = cfg[5];
s_cond[idx].acc_ms = 0;
cond_apply_mode(cfg[5]);
log_info("cond[%d] fire mode=%d\n", idx + 1, cfg[5]);
return 1;
}
/**
* @brief 扫描阶段:找满时长的组触发(owner==NONE 时)
* @param dt 距上次调用的毫秒数
* @return 无
*/
static void cond_scan_trigger(uint16_t dt)
{
uint8_t idx;
for (idx = 0; idx < 7; idx++) {
const uint8_t *cfg = cond_cfg_ptr(idx);
uint8_t ok;
if (!cfg || (cfg[0] == 0)) {
continue; /* 组未使能 */
}
if (s_cond[idx].voided) {
/* 作废组:等比较回安全(不成立)才解除作废(10.3-7) */
if (!cond_group_match(cfg)) {
s_cond[idx].voided = 0;
}
continue;
}
ok = app_th_last_sample_ok(); /* 8.4:只在采样成功时累加 */
if (cond_group_match(cfg)) {
if (ok) {
s_cond[idx].acc_ms += dt;
if (s_cond[idx].acc_ms >= cond_duration_ms(cfg)) {
if (cond_fire(idx)) {
return; /* 一拍只触发一个,下一拍进保持 */
}
}
}
/* 采样失败且比较成立:不累加、不清零(8.4) */
} else {
s_cond[idx].acc_ms = 0; /* 比较不成立:计时清零(10.3-2) */
}
}
}
/**
* @brief 保持阶段:扫描其它组后覆盖(10.3-6,不回栈、快照不变)
* @param dt 距上次调用的毫秒数
* @return 无
*/
static void cond_scan_override(uint16_t dt)
{
uint8_t idx;
for (idx = 0; idx < 7; idx++) {
const uint8_t *cfg = cond_cfg_ptr(idx);
if (idx == s_occupy_idx) {
continue;
}
if (!cfg || (cfg[0] == 0) || s_cond[idx].voided) {
continue;
}
if (cond_group_match(cfg)) {
if (app_th_last_sample_ok()) {
s_cond[idx].acc_ms += dt;
if (s_cond[idx].acc_ms >= cond_duration_ms(cfg)) {
/* 后触发覆盖当前条件模式,不回栈(D1) */
s_occupy_idx = idx;
s_cond[idx].trig_by = cfg[5];
s_cond[idx].acc_ms = 0;
cond_apply_mode(cfg[5]);
log_info("cond[%d] override mode=%d\n", idx + 1, cfg[5]);
return;
}
}
} else {
s_cond[idx].acc_ms = 0;
}
}
}
/**
* @brief 更新告警闪烁输入(规格书 8.2:任一使能组温度/湿度比较成立 → 对应管闪)
* @note 只看单侧比较成立,不看整组 ANDfault "--" 优先由 app_disp 处理
* @return 无
*/
static void cond_update_disp_alarm(void)
{
uint8_t t_alarm = 0;
uint8_t h_alarm = 0;
uint8_t idx;
if (app_th_is_fault() || !app_th_get_valid() || !gDevData.cond_en) {
app_disp_set_temp_alarm(0);
app_disp_set_humi_alarm(0);
return;
}
for (idx = 0; idx < 7; idx++) {
const uint8_t *cfg = cond_cfg_ptr(idx);
if (!cfg || (cfg[0] == 0) || s_cond[idx].voided) {
continue;
}
{
uint8_t temp_cmp = (uint8_t)((cfg[6] >> 5) & 0x7);
uint8_t humi_cmp = (uint8_t)((cfg[6] >> 2) & 0x7);
int16_t temp_th = (int16_t)((cfg[1] << 8) | cfg[2]);
if (cond_cmp_valid(temp_cmp) && cond_cmp_temp(temp_cmp, temp_th)) {
t_alarm = 1;
}
if (cond_cmp_valid(humi_cmp) && cond_cmp_humi(humi_cmp, cfg[3])) {
h_alarm = 1;
}
}
}
app_disp_set_temp_alarm(t_alarm);
app_disp_set_humi_alarm(h_alarm);
}
/**
* @brief 初始化条件引擎
* @return 无
*/
void app_cond_init(void)
{
uint8_t i;
#if !BOARD_COND_ENABLE
log_info("cond engine disabled\n");
return;
#endif
for (i = 0; i < 7; i++) {
s_cond[i].acc_ms = 0;
s_cond[i].voided = 0;
s_cond[i].trig_by = 0;
}
s_occupy_idx = 0xFF;
log_info("cond init\n");
}
/**
* @brief 推进条件引擎(500ms,任务上下文,由 usr_timer_loop 调用)
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_cond_run(uint16_t dt)
{
#if !BOARD_COND_ENABLE
(void)dt;
return;
#endif
/* 总开关:cond_en=0 不扫描;若正在跑视为条件结束恢复快照(10.1) */
if (!gDevData.cond_en) {
if (s_occupy_idx != 0xFF) {
cond_restore_and_release();
}
app_disp_set_temp_alarm(0);
app_disp_set_humi_alarm(0);
return;
}
if (s_occupy_idx != 0xFF) {
/* ── 占用保持阶段 ── */
if (!app_arbiter_cond_active()) {
uint8_t vidx = s_occupy_idx;
if (app_arbiter_get_owner() == ARB_OWNER_TIMER) {
/* 定时抢走:重新计时长,不作废(规格书 12:窗结束须重新满时长) */
cond_reset_acc_all();
s_occupy_idx = 0xFF;
log_info("cond[%d] preempted by timer, re-time\n", vidx + 1);
} else {
/* 手动/APP 介入:本轮作废,须回安全再满时长(10.3-7) */
s_cond[vidx].voided = 1;
app_arbiter_cond_snapshot_clear();
s_occupy_idx = 0xFF;
log_info("cond[%d] voided by manual\n", vidx + 1);
}
cond_update_disp_alarm();
return;
}
/* 其它组满时长 → 后触发覆盖(不回栈) */
cond_scan_override(dt);
/* 当前占用组:成立保持 / 强力到点再开一轮 / 不成立恢复快照 */
{
const uint8_t *cfg = cond_cfg_ptr(s_occupy_idx);
if (cfg && cond_group_match(cfg)) {
/* 条件强力:15min 到点(mode 已回 0)且仍成立 → 立即再开一轮(10.3-4) */
if ((s_cond[s_occupy_idx].trig_by == 2) && (app_mode_get_active() == 0)) {
app_mode_set_manual(2);
log_info("cond[%d] power 15min over, restart\n", s_occupy_idx + 1);
}
/* 通风 25/5 循环由 app_mode 自理 */
} else {
cond_restore_and_release(); /* 比较不成立 → 恢复快照,重新计时 */
}
}
cond_update_disp_alarm();
return;
}
/* ── 空闲扫描触发阶段 ── */
if (app_arbiter_get_owner() == ARB_OWNER_TIMER) {
/* 定时占用中:条件即使满时长也不开;累计清零,窗结束重新计(12 章) */
cond_reset_acc_all();
cond_update_disp_alarm();
return;
}
cond_scan_trigger(dt);
cond_update_disp_alarm();
}
/**
* @brief APP 写 DP5 条件总开关:存 gDevData + 触发 VM 持久化
* @param en 0=关,1=开
* @return 无
*/
void app_cond_set_en(uint8_t en)
{
app_nvm_mark_dirty();
log_info("cond_en=%d (stored)\n", en);
}
/**
* @brief APP 写 DP6~12 条件配置:触发 VM 持久化 + 复位该组计时(15 章)
* @param idx 1~7
* @note 引擎关闭时仍保存;作废标志不回安全不主动清(10.3-7 语义)
* @return 无
*/
void app_cond_set_config(uint8_t idx)
{
if ((idx >= 1) && (idx <= 7)) {
s_cond[idx - 1].acc_ms = 0; /* 条件状态机复位该组计时(15 章) */
}
app_nvm_mark_dirty();
log_info("cond_config[%d] stored\n", idx);
}
+51
View File
@@ -0,0 +1,51 @@
/******************************************************************************
* @file app_cond.h
* @brief CBR3 条件引擎(P2):7 组 7 字节条件,比较成立满时长触发模式
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 仅接口与 stub)
*
* @note P2 实现(BOARD_COND_ENABLE=1)。字节布局见规格书 10.2:
* Byte0=使能 Byte1-2=温度阈值(int16 大端 ×10) Byte3=湿度阈值
* Byte4=持续分钟(0=立即) Byte5=动作模式 Byte6=Bit7-5 温度比较 /
* Bit4-2 湿度比较 / Bit1-0 保留。比较编码:1=< 2=<= 3-== 4->= 5->
* 0/6/7=无条件;两侧都无条件=无效组;两侧有效=AND。
* ⚠ app_cond_run 由 usr_timer_loop500ms,任务上下文)调用,
* 与仲裁器/定时引擎同侧;不放在 20ms usr_timer_tick(中断上下文)。
******************************************************************************/
#ifndef __APP_COND_H__
#define __APP_COND_H__
#include <stdint.h>
/**
* @brief 初始化条件引擎
* @return 无
*/
void app_cond_init(void);
/**
* @brief 推进条件引擎,每个业务 tick 调用一次
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_cond_run(uint16_t dt);
/**
* @brief APP 写 DP5 条件总开关
* @param en 0=关,1=开
* @return 无
*/
void app_cond_set_en(uint8_t en);
/**
* @brief APP 写 DP6~12 条件配置(7 字节)
* @param idx 1~7
* @return 无
*/
void app_cond_set_config(uint8_t idx);
#endif /* __APP_COND_H__ */
+278
View File
@@ -0,0 +1,278 @@
/******************************************************************************
* @file app_disp.c
* @brief CBR3 数码管显示实现
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.1.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布
* - V1.0.1, 2026.08.29, cyWu, 改动态扫描:20ms 节拍只算数据写缓冲,
* 另起 BOARD_DISP_SCAN_MS 快节拍定时器轮询点亮 1 位,解决闪烁
* - V1.1.0, 2026.08.31, cyWu, P1 加告警闪烁:温度/湿度管独立 500ms 亮灭,
* BOARD_DISP_ALARM_TEST 宏可强制模拟超限;fault 时 "--" 优先
*
* @note ⚠ CT1642 内部只有单个 12 位锁存器(4 位选 + 8 段码),无按位显示 RAM、
* 不自动扫描,一次只点亮当前锁存的那 1 位。要让 4 位都亮且不闪,必须由 MCU
* 持续轮询:本模块用 app_disp_run()20ms)算好 4 位段码放进 s_disp_buf
* 用 app_disp_scan()BOARD_DISP_SCAN_MS)每次点亮 1 位、4 位轮询。
* 告警闪烁(规格书 8.2):比较成立侧 500ms 亮/500ms 灭,与吹风解耦;
* 闪烁触发源由 app_disp_set_temp/humi_alarm 喂入(P2 条件引擎),
* 传感器故障(连续 3 次失败)优先显示 "--" 不闪。
******************************************************************************/
#include "app_disp.h"
#include "app_th.h"
#include "app_power.h"
#include "bsp_ct1642.h"
#include "board_pin.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_DISP]"
#define LOG_INFO_ENABLE
#include "debug.h"
static uint8_t s_inited;
/* 4 位段码缓冲,下标顺序 = com 顺序:0 温度十 / 1 温度个 / 2 湿度十 / 3 湿度个 */
static uint8_t s_disp_buf[4];
static const uint8_t s_com_tbl[4] = {
BOARD_DISP_COM_TEMP_TEN, BOARD_DISP_COM_TEMP_ONE,
BOARD_DISP_COM_HUMI_TEN, BOARD_DISP_COM_HUMI_ONE
};
static uint8_t s_scan_idx; /* 动态扫描轮询指针 0..3 */
/* 告警闪烁(P1,规格书 8.2):比较成立侧 500ms 亮/500ms 灭 */
static uint8_t s_temp_alarm; /* 1=温度比较成立(P2 条件引擎喂) */
static uint8_t s_humi_alarm; /* 1=湿度比较成立(P2 条件引擎喂) */
static uint8_t s_blink_phase; /* 0=亮半周期,1=灭半周期 */
static uint16_t s_blink_ms; /* 半周期内累计毫秒 */
/* 段码索引:bsp_ct1642_seg(0~9)=数字,10='-'11=灭 */
#define SEG_MINUS 10
#define SEG_OFF 11
/**
* @brief 温度值 → 两位段码(0~99<0 显示 -9~-1<-9 钳位 -9>99 钳位 99
* @param temp_x10 温度 ×10
* @param out_t1 十位段码索引
* @param out_t0 个位段码索引
* @return 无
*/
static void disp_split_temp(int16_t temp_x10, uint8_t *out_t1, uint8_t *out_t0)
{
int16_t t = temp_x10 / 10; /* 取整数 ℃(P0 不显示小数) */
uint8_t tens;
uint8_t ones;
if (t < 0) {
if (t < -9) {
t = -9; /* 钳位 */
}
*out_t1 = SEG_MINUS;
*out_t0 = (uint8_t)(-t % 10);
return;
}
if (t > 99) {
t = 99;
}
tens = (uint8_t)(t / 10);
ones = (uint8_t)(t % 10);
*out_t1 = tens;
*out_t0 = ones;
}
/**
* @brief 湿度值 → 两位段码(0~99>99 钳位 99
* @param humi 湿度 %
* @param out_h1 十位段码索引
* @param out_h0 个位段码索引
* @return 无
*/
static void disp_split_humi(uint8_t humi, uint8_t *out_h1, uint8_t *out_h0)
{
uint8_t h = humi;
if (h > 99) {
h = 99;
}
*out_h1 = (uint8_t)(h / 10);
*out_h0 = (uint8_t)(h % 10);
}
/**
* @brief 初始化显示模块;注册快节拍动态扫描定时器
* @return 无
*/
void app_disp_init(void)
{
#if !BOARD_DISP_ENABLE
log_info("disp disabled\n");
return;
#endif
if (s_inited) {
return;
}
s_inited = 1;
s_scan_idx = 0;
bsp_ct1642_init(); /* PG0(CLK)/PG1(DATA) 推挽输出 */
/* 默认写死 25 / 60,供 P0 台上扫段验证(规格书 P0 验收) */
s_disp_buf[0] = bsp_ct1642_seg(2);
s_disp_buf[1] = bsp_ct1642_seg(5);
s_disp_buf[2] = bsp_ct1642_seg(6);
s_disp_buf[3] = bsp_ct1642_seg(0);
log_info("disp init, scan %ums\n", (unsigned)BOARD_DISP_SCAN_MS);
/* 注意:快节拍动态扫描定时器在 usr_jb_init 里注册(usr_timer_add 声明在那) */
}
/**
* @brief 业务节拍(20ms):计算 4 位要显示什么,写入段码缓冲(不直接打管)
* @note 软关机由 app_disp_scan 处理。本函数只更新 s_disp_buf。
* @return 无
*/
void app_disp_run(uint16_t dt)
{
#if !BOARD_DISP_ENABLE
(void)dt;
return;
#endif
if (!s_inited) {
return;
}
/* 闪烁相位推进:满 BOARD_DISP_BLINK_MS 切换亮/灭半周期 */
s_blink_ms = (uint16_t)(s_blink_ms + dt);
if (s_blink_ms >= BOARD_DISP_BLINK_MS) {
s_blink_ms = 0;
s_blink_phase ^= 1;
}
#if BOARD_DISP_ALARM_TEST == 1
s_temp_alarm = 1; /* P1 调试:强制温度管闪 */
#elif BOARD_DISP_ALARM_TEST == 2
s_humi_alarm = 1; /* P1 调试:强制湿度管闪 */
#endif
#if BOARD_DISP_SCAN_TEST == 1
/* 段扫描:4 位同时点亮同一段,每 600ms 切 A→B→...→G→灭→全亮,定性段码位序 */
{
static uint32_t tick;
static uint8_t step;
static const uint8_t seg_seq[] = {
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x00, 0x7F
};
if (++tick >= 30) { /* 30*20ms=600ms */
tick = 0;
step = (uint8_t)((step + 1) % (sizeof(seg_seq) / sizeof(seg_seq[0])));
}
uint8_t s = seg_seq[step];
s_disp_buf[0] = s; s_disp_buf[1] = s; s_disp_buf[2] = s; s_disp_buf[3] = s;
return;
}
#elif BOARD_DISP_SCAN_TEST == 2
/* 位扫描:4 位分别显示 1/2/3/4,定性 C1~C4 与物理管位对应 */
s_disp_buf[0] = bsp_ct1642_seg(1);
s_disp_buf[1] = bsp_ct1642_seg(2);
s_disp_buf[2] = bsp_ct1642_seg(3);
s_disp_buf[3] = bsp_ct1642_seg(4);
return;
#endif
if (app_th_is_fault()) {
/* 连续失败 3 次:双管 "--"(规格书 8.1;故障优先于闪烁,8.2) */
uint8_t m = bsp_ct1642_seg(SEG_MINUS);
s_disp_buf[0] = m; s_disp_buf[1] = m; s_disp_buf[2] = m; s_disp_buf[3] = m;
} else if (app_th_get_valid()) {
/* 有有效采样:显示真实值;比较成立且当前在灭半周期 → 该管熄灭 */
uint8_t t1, t0, h1, h0;
disp_split_temp(app_th_get_temp_x10(), &t1, &t0);
disp_split_humi(app_th_get_humi(), &h1, &h0);
if (s_temp_alarm && s_blink_phase) {
t1 = SEG_OFF;
t0 = SEG_OFF;
}
if (s_humi_alarm && s_blink_phase) {
h1 = SEG_OFF;
h0 = SEG_OFF;
}
s_disp_buf[0] = bsp_ct1642_seg(t1);
s_disp_buf[1] = bsp_ct1642_seg(t0);
s_disp_buf[2] = bsp_ct1642_seg(h1);
s_disp_buf[3] = bsp_ct1642_seg(h0);
} else {
/* 尚未有有效采样:写死 25 / 60,供 P0 台上扫段验证(规格书 P0 验收) */
s_disp_buf[0] = bsp_ct1642_seg(2);
s_disp_buf[1] = bsp_ct1642_seg(5);
s_disp_buf[2] = bsp_ct1642_seg(6);
s_disp_buf[3] = bsp_ct1642_seg(0);
}
}
/**
* @brief 显示动态扫描(BOARD_DISP_SCAN_MS 快节拍定时器回调):点亮 1 位、4 位轮询
* @note 必须短、不可阻塞。每次只发 1 帧,靠高频轮询让 4 位视觉同亮不闪。
* IO 分离后 CT1642 独占 PG0/PG1,与 AHT20 的 PC4/PC5 无总线竞争,
* 扫描回调无需锁、无跳过。
* 软关机流程:4 位全灭(com=0x00 → 所有 Cx 高,位选全关)。
* @return 无
*/
void app_disp_scan(void)
{
#if !BOARD_DISP_ENABLE
return;
#endif
if (!s_inited) {
return;
}
/* 软关机流程:数码管锁存灭(规格书 5.1:风扇 0、数码管锁存灭、LED 灭) */
if (app_power_poweroff_pending()) {
bsp_ct1642_send_frame(0x00, 0x00); /* com=0x00 → 4 位全灭 */
return;
}
#if BOARD_DISP_SCAN_TEST == 1
/* 段扫描:4 位同显一段。⭐ 值变化才刷(无鬼影),com=0xF 四位全选同显。 */
static uint8_t seg = 0;
if (seg == s_disp_buf[0]) {
return;
}
seg = s_disp_buf[0];
bsp_ct1642_send_frame(s_disp_buf[s_scan_idx], 0xF);
s_scan_idx = (uint8_t)((s_scan_idx + 1) % 4);
#else
/* 正常显示(0) / 位选验证(2):4 位分显不同数字,必须分时轮询。
* 鬼影(切位杂段)靠 ct1642_delay 8→2 提速缩短杂段窗口来减轻。 */
bsp_ct1642_send_frame(s_disp_buf[s_scan_idx], s_com_tbl[s_scan_idx]);
s_scan_idx = (uint8_t)((s_scan_idx + 1) % 4);
#endif
}
/**
* @brief 设置温度比较成立标志:成立时温度管 500ms 闪烁(P2 条件引擎喂)
* @param en 1=成立(闪),0=不成立(常亮数值)
* @return 无
*/
void app_disp_set_temp_alarm(uint8_t en)
{
s_temp_alarm = en ? 1 : 0;
}
/**
* @brief 设置湿度比较成立标志:成立时湿度管 500ms 闪烁(P2 条件引擎喂)
* @param en 1=成立(闪),0=不成立(常亮数值)
* @return 无
*/
void app_disp_set_humi_alarm(uint8_t en)
{
s_humi_alarm = en ? 1 : 0;
}
/**
* @brief 温度或湿度管是否处于告警闪烁
* @return 1=任一侧告警
*/
uint8_t app_disp_is_alarm(void)
{
return (s_temp_alarm || s_humi_alarm) ? 1 : 0;
}
+68
View File
@@ -0,0 +1,68 @@
/******************************************************************************
* @file app_disp.h
* @brief CBR3 数码管显示:温度/湿度整数、负号、-- 故障、告警闪烁
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.1.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布
* - V1.1.0, 2026.08.31, cyWu, P1 加告警闪烁:温度/湿度管独立 500ms 亮灭,
* 由 app_disp_set_temp/humi_alarm 驱动(P2 条件引擎喂真值,
* P1 可用 BOARD_DISP_ALARM_TEST 宏强制验证)
*
* @note 两块 2 位 7 段数码管:温度管整数 ℃、湿度管整数 %。DP 恒灭。
* 0~99 显示绝对值;<0 显示 -9~-1G 段负号);<-9 钳位 -9>99 钳位 99。
* 连续失败 3 次双管 "--"(优先于闪烁)。告警闪烁(规格书 8.2)
* 与吹风解耦:比较成立侧 500ms 亮/500ms 灭,不成立侧常亮数值。
******************************************************************************/
#ifndef __APP_DISP_H__
#define __APP_DISP_H__
#include <stdint.h>
/**
* @brief 初始化显示模块
* @return 无
*/
void app_disp_init(void);
/**
* @brief 业务节拍:计算 4 位要显示什么,写入段码缓冲(不直接打管)
* @param dt 距上次调用的毫秒数(闪烁相位累加)
* @note 软关机由 app_disp_scan 处理;本函数只算数据。
* @return 无
*/
void app_disp_run(uint16_t dt);
/**
* @brief 显示动态扫描(快节拍定时器回调):每次点亮 1 位,4 位轮询刷新
* @note 由 app_disp_init 注册的 BOARD_DISP_SCAN_MS 定时器调用;
* 必须短、不可阻塞;软关机时 4 位全灭。
* CT1642 无内部扫描 RAM,靠本函数持续轮询维持 4 位同亮不闪。
* @return 无
*/
void app_disp_scan(void);
/**
* @brief 设置温度比较成立标志:成立时温度管 500ms 闪烁(P2 条件引擎喂)
* @param en 1=成立(闪),0=不成立(常亮数值)
* @return 无
*/
void app_disp_set_temp_alarm(uint8_t en);
/**
* @brief 设置湿度比较成立标志:成立时湿度管 500ms 闪烁(P2 条件引擎喂)
* @param en 1=成立(闪),0=不成立(常亮数值)
* @return 无
*/
void app_disp_set_humi_alarm(uint8_t en);
/**
* @brief 温度或湿度管是否处于告警闪烁(供 fault_code=1
* @return 1=任一侧告警,0=无
*/
uint8_t app_disp_is_alarm(void);
#endif /* __APP_DISP_H__ */
+82
View File
@@ -0,0 +1,82 @@
/******************************************************************************
* @file app_fan.c
* @brief CBR3 风扇档位实现
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布
******************************************************************************/
#include "app_fan.h"
#include "bsp_hw.h"
#include "board_pin.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_FAN]"
#define LOG_INFO_ENABLE
#include "debug.h"
static uint8_t s_gear;
/**
* @brief 初始化风扇(档位 0PWM 关)
* @return 无
*/
void app_fan_init(void)
{
#if !BOARD_FAN_ENABLE
log_info("fan disabled\n");
return;
#endif
s_gear = 0;
bsp_fan_set_duty(BOARD_FAN_DUTY_GEAR0);
log_info("fan init gear=0\n");
}
/**
* @brief 设置档位并立即生效(P0 无软爬坡)
* @param gear 0=关,1=50%2=75%3=100%>3 钳位到 3
* @return 无
*/
void app_fan_set_gear(uint8_t gear)
{
#if !BOARD_FAN_ENABLE
(void)gear;
s_gear = 0;
return;
#endif
static const uint16_t duty_tbl[4] = {
BOARD_FAN_DUTY_GEAR0, BOARD_FAN_DUTY_GEAR1,
BOARD_FAN_DUTY_GEAR2, BOARD_FAN_DUTY_GEAR3
};
if (gear > 3) {
gear = 3;
}
if (gear == s_gear) {
return;
}
s_gear = gear;
bsp_fan_set_duty(duty_tbl[gear]);
log_info("fan gear=%d duty=%u/10000\n", gear, (unsigned)duty_tbl[gear]);
}
/**
* @brief 查询当前真实档位(DP2 必须等于它,通风停段时为 0)
* @return 0~3
*/
uint8_t app_fan_get_gear(void)
{
return s_gear;
}
/**
* @brief 业务节拍(P0 无动作,保留接口供 P1 爬坡)
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_fan_run(uint16_t dt)
{
(void)dt;
}
+42
View File
@@ -0,0 +1,42 @@
/******************************************************************************
* @file app_fan.h
* @brief CBR3 风扇档位:0/1/2/3 → 0%/50%/75%/100%
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布
******************************************************************************/
#ifndef __APP_FAN_H__
#define __APP_FAN_H__
#include <stdint.h>
/**
* @brief 初始化风扇(档位 0PWM 关)
* @return 无
*/
void app_fan_init(void);
/**
* @brief 设置档位并立即生效(P0 无软爬坡)
* @param gear 0=关,1=50%2=75%3=100%>3 钳位到 3
* @return 无
*/
void app_fan_set_gear(uint8_t gear);
/**
* @brief 查询当前真实档位(DP2 必须等于它,通风停段时为 0)
* @return 0~3
*/
uint8_t app_fan_get_gear(void);
/**
* @brief 业务节拍(P0 无动作,保留接口供 P1 爬坡)
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_fan_run(uint16_t dt);
#endif /* __APP_FAN_H__ */
+220
View File
@@ -0,0 +1,220 @@
/******************************************************************************
* @file app_function.c
* @brief CBR3 功能总装实现
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.1.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布
* - V1.1.0, 2026.08.31, cyWu, 长按关机改为延后到任务上下文执行:按键在
* usr_timer 20ms(硬件定时器中断上下文)里处理,直接调
* app_power_request_poweroff 会在中断里跑 VM 落盘(栈溢出复位);
* 现改为置 off_pending,由 app_function_loopusr_timer_loop
* BLE 线程)先落盘再关机
******************************************************************************/
#include "app_function.h"
#include "app_power.h"
#include "app_pair.h"
#include "app_mode.h"
#include "app_fan.h"
#include "app_nvm.h"
#include "app_arbiter.h"
#include "app_cond.h"
#include "app_timer_task.h"
#include "app_th.h"
#include "app_disp.h"
#include "app_led.h"
#include "jb_product.h"
#include "system/includes.h"
#include <string.h>
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_FUNC]"
#define LOG_INFO_ENABLE
#include "debug.h"
/** 功能模块状态 */
typedef struct {
uint8_t app_power; /**< DP01=开机态(本产品无 APP 软关) */
uint8_t off_pending; /**< 1=长按已触发关机,等任务上下文落盘后再关机 */
} AppFunctionCtx_t;
static AppFunctionCtx_t s_func;
/**
* @brief 初始化功能模块状态
* @return 无
*/
void app_function_init(void)
{
s_func.app_power = 0;
s_func.off_pending = 0;
}
/**
* @brief 任务上下文周期处理(由 usr_timer_loop 500ms 调用)
* @note ⚠ 本函数是唯一允许做 VM 落盘与关机收口的地方:usr_timer_loop 跑在
* BLE 线程(sys_timer 同步接口,任务上下文,栈充足)。20ms 的
* usr_timer_tick 是硬件定时器中断上下文,禁止做这两件事。
* @return 无
*/
void app_function_loop(void)
{
app_nvm_flush(); /* 防抖满后真正写 VMsyscfg_write 必须在任务上下文) */
if (s_func.off_pending) {
s_func.off_pending = 0;
app_power_request_poweroff(); /* 落盘后再走关机流程 */
}
}
/**
* @brief 同步 DPpower 恒 1、fan_gear、mode、fault_code),每个业务 tick 调用
* @note DP3/DP4 温湿度在 userHandle500ms)填;DP0 写忽略,运行中恒上报 1
* @return 无
*/
void app_function_poll(void)
{
gDevData.power = (s_func.app_power) ? 1 : 0;
gDevData.fan_gear = app_fan_get_gear(); /* DP2 必须等于当前真实档位 */
gDevData.mode = app_mode_get_active();
/* 规格书 8.2/15:传感器故障优先 2;否则温湿度比较成立报 1 */
if (app_th_is_fault()) {
gDevData.fault_code = 2;
} else if (app_disp_is_alarm()) {
gDevData.fault_code = 1;
} else {
gDevData.fault_code = 0;
}
}
/**
* @brief 停手动 + 模式,风扇关(关机请求 / 软关时用)
* @return 无
*/
void app_function_stop_all(void)
{
app_mode_stop();
app_fan_set_gear(0);
}
/**
* @brief 软关:风扇停、DP0=0(本产品 DP0 写忽略,软关仅按键 3s 触发)
* @return 无
*/
void app_function_soft_off(void)
{
s_func.app_power = 0;
gDevData.power = 0;
app_function_stop_all();
log_info("soft off\n");
}
/**
* @brief 上电默认开启功能(DP0=1),不带开机灯效提示
* @return 无
*/
void app_function_power_on_silent(void)
{
s_func.app_power = 1;
gDevData.power = 1;
}
/**
* @brief 查询功能开关状态(DP0)
* @return 1=开,0=关
*/
uint8_t app_function_is_power_on(void)
{
return s_func.app_power;
}
/**
* @brief 短按:风扇档位循环 1→2→3→0→1…(规格书 5.3)
* @note 立刻退出通风/强力改为手动档:用 app_mode_stop() 只清模式、
* 不动档位(若用 set_manual(0) 会恢复模式前快照档,导致通风 1 档
* 短按跳到旧档+1 而非 2 档);配对进行中短按仍可切档
* @return 无
*/
void usr_on_key_short(void)
{
uint8_t g;
if (app_power_poweroff_pending()) {
return;
}
if (!s_func.app_power) {
/* 软关态短按只唤醒,不切档(P0 无 APP 软关,保留分支) */
s_func.app_power = 1;
gDevData.power = 1;
app_led_hint_power_on();
return;
}
app_arbiter_manual_intervene(); /* 手动动作:释放条件/定时占用(12 章) */
app_mode_stop(); /* 退模式(不动档位,下面立刻切循环下一档) */
g = app_fan_get_gear(); /* 当前实际档:手动=手动档;模式中=模式强制档(1/3) */
g = (uint8_t)((g + 1) % 4); /* 1→2→3→0→1 */
app_fan_set_gear(g);
log_info("key short gear=%d\n", g);
}
/**
* @brief 长按 3s:请求软关机(配对中忽略,避免配到一半睡死,规格书 5.3)
* @note ⚠ 本回调在 usr_timer 20ms(硬件定时器中断上下文)执行,禁止直接
* 调 app_power_request_poweroff(其钩子会做 VM 落盘 → 栈溢出)。
* 只置 off_pending,由 app_function_loop(任务上下文)落盘后关机。
* @return 无
*/
void usr_on_key_long(void)
{
if (app_power_poweroff_pending()) {
return;
}
if (app_pair_is_active()) {
log_info("key hold 3s ignored, pairing\n");
return;
}
log_info("key hold 3s, poweroff pending (wait vm flush)\n");
s_func.off_pending = 1;
}
/**
* @brief 出厂重置:清业务 VM 与 RAM,保留 BLE 配对(规格书 14 章)
* @return 无
*/
void app_function_factory_reset(void)
{
uint8_t i;
uint8_t *c[7] = {
gDevData.cond_config1, gDevData.cond_config2, gDevData.cond_config3,
gDevData.cond_config4, gDevData.cond_config5, gDevData.cond_config6,
gDevData.cond_config7
};
uint8_t *tm[7] = {
gDevData.time_config1, gDevData.time_config2, gDevData.time_config3,
gDevData.time_config4, gDevData.time_config5, gDevData.time_config6,
gDevData.time_config7
};
app_nvm_reset();
app_arbiter_init();
app_cond_init();
app_timer_init();
app_mode_stop();
app_fan_set_gear(0);
gDevData.mode = 0;
gDevData.fan_gear = 0;
gDevData.cond_en = 0;
gDevData.time_en = 0;
gDevData.fault_en = 0;
gDevData.alarm_en = 0;
gDevData.fault_code = 0;
gDevData.alarm_code = 0;
for (i = 0; i < 7; i++) {
memset(c[i], 0, 7);
memset(tm[i], 0, 8);
}
log_info("factory reset: business vm cleared, pairing kept\n");
}
+82
View File
@@ -0,0 +1,82 @@
/******************************************************************************
* @file app_function.h
* @brief CBR3 功能总装:按键动作(短按切档/长按关机)+ DP 同步
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹语义重写)
*
* @note 本模块只做"按键 → 动作"和"状态 → DP",不写业务算法。
* 档位/模式/条件的执行在各自 app_* 模块(规格书第 0 章第 2 条)。
******************************************************************************/
#ifndef __APP_FUNCTION_H__
#define __APP_FUNCTION_H__
#include <stdint.h>
/**
* @brief 初始化功能模块状态
* @return 无
*/
void app_function_init(void);
/**
* @brief 同步 DPpower 恒 1、fan_gear、mode、fault_code),每个业务 tick 调用
* @return 无
*/
void app_function_poll(void);
/**
* @brief 任务上下文周期处理(由 usr_timer_loop 500ms 调用)
* @note ⚠ VM 落盘(syscfg_write)与关机收口只允许在这里做:usr_timer_loop
* 跑在 BLE 线程(任务上下文,栈充足);usr_timer_tick 是硬件定时器
* 中断上下文,做这两件事会栈溢出复位。
* @return 无
*/
void app_function_loop(void);
/**
* @brief 停手动 + 模式,风扇关(关机请求 / 软关时用)
* @return 无
*/
void app_function_stop_all(void);
/**
* @brief 软关:风扇停、DP0=0(本产品 DP0 写忽略,软关仅按键 3s 触发)
* @return 无
*/
void app_function_soft_off(void);
/**
* @brief 上电默认开启功能(DP0=1),不带开机灯效提示
* @return 无
*/
void app_function_power_on_silent(void);
/**
* @brief 查询功能开关状态(DP0)
* @return 1=开,0=关
*/
uint8_t app_function_is_power_on(void);
/**
* @brief 业务层短按回调(key_manager 触发):档位 1→2→3→0→1…
* @return 无
*/
void usr_on_key_short(void);
/**
* @brief 业务层长按 3s 回调(key_manager 触发):请求软关机
* @return 无
*/
void usr_on_key_long(void);
/**
* @brief 出厂重置:清业务 VM 与 RAM,保留 BLE 配对(规格书 14 章)
* @return 无
*/
void app_function_factory_reset(void);
#endif /* __APP_FUNCTION_H__ */
+143
View File
@@ -0,0 +1,143 @@
/******************************************************************************
* @file app_led.c
* @brief CBR3 灯效适配:单灯 PC2OTA/配对/常亮
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,删红绿双通道/充电/低电/空电)
******************************************************************************/
#include "app_led.h"
#include "led_manager.h"
#include "bsp_hw.h"
#include "board_pin.h"
#include "app_pair.h"
static uint8_t s_initialized;
static uint8_t s_ota; /* 1=OTA 灯效中,app_led_run 不再叠其它灯 */
/**
* @brief 重新播放一次「一次性/限时」灯效,让它从第 0 帧重新开始
* @note 先 Delete 清空槽位再 Add,强制重新计时(见 led.c LED_EventAdd 语义)
* @param evt 要重启的事件
* @return 无
*/
static void led_restart(LED_Event_t evt)
{
LED_EventDelete(evt);
LED_EventAdd(evt);
}
/**
* @brief 灯效初始化
* @return APP_LED_OK=成功
*/
AppLed_Ret_t app_led_init(void)
{
if (s_initialized) {
return APP_LED_OK;
}
user_led_init();
s_initialized = 1;
return APP_LED_OK;
}
/**
* @brief 查询模块是否已初始化
* @return APP_LED_OK=已初始化,APP_LED_ERR_NOT_INIT=未初始化
*/
AppLed_Ret_t app_led_get_status(void)
{
return s_initialized ? APP_LED_OK : APP_LED_ERR_NOT_INIT;
}
/**
* @brief 重新挂开机常亮
* @return 无
*/
void app_led_hint_power_on(void)
{
if (!s_initialized || s_ota) {
return;
}
led_restart(LED_EVENT_STANDBY);
}
/**
* @brief 进入 OTA:清掉其它灯,挂 200ms 闪一直闪
* @return 无
*/
void app_led_request_ota(void)
{
if (!s_initialized) {
return;
}
s_ota = 1;
LED_EventAllDelete();
bsp_led_set(0);
LED_EventAdd(LED_EVENT_OTA);
}
/**
* @brief 查询是否处于 OTA 灯效
* @return 1=OTA 灯效中,0=否
*/
uint8_t app_led_is_ota(void)
{
return s_ota;
}
/**
* @brief 按优先级刷新灯
* @param dt 距上次调用的毫秒数
* @param power_on 1=开机态(挂常亮),0=关机态(灯灭)
* @return 无
*/
void app_led_run(uint16_t dt, uint8_t power_on)
{
AppPairEvt_t pair_evt;
if (!s_initialized) {
return;
}
/* OTA 期间只跑 200ms 闪 */
if (s_ota) {
LED_EventAdd(LED_EVENT_OTA);
user_led_handle();
return;
}
/* 配对成功/失败是边沿事件,只在那一拍切灯效 */
pair_evt = app_pair_tick_1ms(dt);
if (pair_evt == APP_PAIR_EVT_OK) {
LED_EventDelete(LED_EVENT_PAIRING);
led_restart(LED_EVENT_PAIR_OK);
} else if (pair_evt == APP_PAIR_EVT_FAIL) {
LED_EventDelete(LED_EVENT_PAIRING);
led_restart(LED_EVENT_PAIR_FAIL);
}
if (!power_on) {
LED_EventDelete(LED_EVENT_STANDBY);
LED_EventDelete(LED_EVENT_PAIRING);
} else if (app_pair_is_active()) {
LED_EventAdd(LED_EVENT_PAIRING);
LED_EventDelete(LED_EVENT_STANDBY);
} else if (!LED_IsEventExist(LED_EVENT_PAIR_OK) && !LED_IsEventExist(LED_EVENT_PAIR_FAIL)) {
LED_EventAdd(LED_EVENT_STANDBY);
}
user_led_handle();
}
/**
* @brief 清空全部灯事件并灭灯
* @return 无
*/
void app_led_clear(void)
{
LED_EventAllDelete();
bsp_led_set(0);
}
+65
View File
@@ -0,0 +1,65 @@
/******************************************************************************
* @file app_led.h
* @brief CBR3 灯效:单灯 PC2OTA/配对/常亮按优先级
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,删红绿双通道/充电/低电)
******************************************************************************/
#ifndef __APP_LED_H__
#define __APP_LED_H__
#include <stdint.h>
typedef enum {
APP_LED_OK = 0,
APP_LED_ERR_NOT_INIT
} AppLed_Ret_t;
/**
* @brief 灯效初始化,先全灭
* @return APP_LED_OK=成功
*/
AppLed_Ret_t app_led_init(void);
/**
* @brief 按优先级刷新灯:配对从 app_pair 取,功能开关由调用方传入
* @param dt 距上次调用的毫秒数
* @param power_on 1=开机态,0=关机态(灯灭)
* @return 无
*/
void app_led_run(uint16_t dt, uint8_t power_on);
/**
* @brief 重新挂开机常亮
* @return 无
*/
void app_led_hint_power_on(void);
/**
* @brief 进入 OTA:清掉其它灯,挂 200ms 闪一直闪
* @return 无
*/
void app_led_request_ota(void);
/**
* @brief 查询是否处于 OTA 灯效
* @return 1=OTA 灯效中,0=否
*/
uint8_t app_led_is_ota(void);
/**
* @brief 清空全部灯事件并灭灯(关机前调用)
* @return 无
*/
void app_led_clear(void);
/**
* @brief 查询是否已初始化
* @return APP_LED_OK=已初始化,APP_LED_ERR_NOT_INIT=未初始化
*/
AppLed_Ret_t app_led_get_status(void);
#endif /* __APP_LED_H__ */
+177
View File
@@ -0,0 +1,177 @@
/******************************************************************************
* @file app_mode.c
* @brief CBR3 工作模式实现(P1):通风 25/5 循环、强力 15min 一轮
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.1.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 stub
* - V1.1.0, 2026.08.31, cyWu, P1 实现状态机(规格书第 9 章):
* 通风 25/5、强力 15min 后停、手动档快照、退出语义、开机从周期头
*
* @note 模式激活时强制覆盖风扇档位(通风=1 档、强力=3 档、通风停段=0),
* DP2 由 app_fan_get_gear 上报真实档位(app_function_poll 同步)。
* 退出方式(规格书 9 章):短按切档(app_mode_stop)、APP 写档位、
* APP 写 mode=0(恢复进入模式前的手动档快照)、强力自然到点(风扇 0)。
* 开机恢复不存相位:从 25min 开段 / 15min 从头计。
******************************************************************************/
#include "app_mode.h"
#include "app_fan.h"
#include "app_nvm.h"
#include "board_pin.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_MODE]"
#define LOG_INFO_ENABLE
#include "debug.h"
/** 通风循环段 */
typedef enum {
MODE_VENT_ON = 0, /**< 开段 25min,强制 1 档 */
MODE_VENT_OFF /**< 停段 5min,风扇 0 */
} ModeVentPhase_t;
/** 模式状态机上下文 */
typedef struct {
uint8_t mode; /**< 0=关闭 1=通风 2=强力 */
uint8_t phase; /**< 通风段;其它模式恒 0 */
uint32_t run_ms; /**< 当前段已运行毫秒 */
uint8_t saved_gear;/**< 进入模式前的手动档快照(APP 写 mode=0 时恢复) */
} AppModeCtx_t;
static AppModeCtx_t s_mode;
/**
* @brief 初始化模式状态机
* @return 无
*/
void app_mode_init(void)
{
#if !BOARD_MODE_ENABLE
log_info("mode disabled\n");
return;
#endif
s_mode.mode = 0;
s_mode.phase = 0;
s_mode.run_ms = 0;
s_mode.saved_gear = 0;
log_info("mode init\n");
}
/**
* @brief 推进模式状态机(每个业务 tick 调用)
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_mode_run(uint16_t dt)
{
#if !BOARD_MODE_ENABLE
(void)dt;
return;
#endif
switch (s_mode.mode) {
case 1: /* 通风:开 25min 强制 1 档,停 5min 风扇 0,循环 */
if (s_mode.phase == MODE_VENT_ON) {
app_fan_set_gear(1);
s_mode.run_ms += dt;
if (s_mode.run_ms >= BOARD_MODE_VENT_ON_MS) {
s_mode.phase = MODE_VENT_OFF;
s_mode.run_ms = 0;
app_fan_set_gear(0);
app_nvm_mark_dirty();
log_info("vent on 25min over, off 5min\n");
}
} else {
s_mode.run_ms += dt;
if (s_mode.run_ms >= BOARD_MODE_VENT_OFF_MS) {
s_mode.phase = MODE_VENT_ON;
s_mode.run_ms = 0;
app_fan_set_gear(1);
app_nvm_mark_dirty();
log_info("vent off 5min over, on 25min\n");
}
}
break;
case 2: /* 强力:强制 3 档,15min 到点风扇 0、mode 回 0 */
app_fan_set_gear(3);
s_mode.run_ms += dt;
if (s_mode.run_ms >= BOARD_MODE_POWER_ON_MS) {
log_info("power 15min over, mode=0 fan=0\n");
s_mode.mode = 0;
s_mode.phase = 0;
s_mode.run_ms = 0;
app_fan_set_gear(0);
app_nvm_mark_dirty();
}
break;
default:
break;
}
}
/**
* @brief APP 写 DP1 / 手动抢权 / 开机恢复:切到指定模式或退出
* @param mode 0=关闭模式(恢复手动档快照),1=通风,2=强力
* @note 模式 1/2 进入时快照当前实际档(即进入前的手动档),并立即强制档位;
* mode=0 时若原在模式中则恢复快照档。短按退出走 app_mode_stop()
* (不恢复快照,档位由短按逻辑立刻切到循环下一档)。
* @return 无
*/
void app_mode_set_manual(uint8_t mode)
{
#if !BOARD_MODE_ENABLE
(void)mode;
return;
#endif
if (mode > 2) {
mode = 0;
}
if (mode != 0) {
s_mode.saved_gear = app_fan_get_gear(); /* 快照进入前的手动档 */
s_mode.mode = mode;
s_mode.phase = MODE_VENT_ON;
s_mode.run_ms = 0; /* 不存相位:开机/手动切入都从周期头计 */
app_fan_set_gear((mode == 1) ? 1 : 3);
log_info("mode set %d, gear=%d\n", mode, app_fan_get_gear());
} else {
if (s_mode.mode != 0) {
/* 只在实际退出模式时恢复快照档,避免 load(0) 把恢复档清零 */
app_fan_set_gear(s_mode.saved_gear);
log_info("mode exit, restore gear=%d\n", s_mode.saved_gear);
}
s_mode.mode = 0;
s_mode.phase = 0;
s_mode.run_ms = 0;
}
app_nvm_mark_dirty();
}
/**
* @brief 查询当前逻辑模式
* @return 0/1/2
*/
uint8_t app_mode_get_active(void)
{
return s_mode.mode;
}
/**
* @brief 停止当前模式(短按退模式用:只清模式状态,不动风扇档位,
* 由调用方立刻切到循环下一档;关机流程则另行停风扇)
* @return 无
*/
void app_mode_stop(void)
{
#if !BOARD_MODE_ENABLE
return;
#endif
if (s_mode.mode != 0) {
app_nvm_mark_dirty();
}
s_mode.mode = 0;
s_mode.phase = 0;
s_mode.run_ms = 0;
}
+54
View File
@@ -0,0 +1,54 @@
/******************************************************************************
* @file app_mode.h
* @brief CBR3 工作模式:通风(25/5 循环)/ 强力(15min 一轮)
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 仅接口与 stub,P1 实现状态机)
*
* @note P1 语义(规格书第 9 章):mode=1 强制 1 档开 25min 停 5min 循环;
* mode=2 强制 3 档跑 15min 后风扇 0、mode 回 0。短按切档走
* app_mode_stop(档位由短按逻辑续切);APP 写档 / 写 mode 走
* app_mode_set_manual 手动抢权(mode=0 恢复进入前的手动档快照)。
* 开机恢复不存相位,从头计。BOARD_MODE_ENABLE=0 时全部空操作。
******************************************************************************/
#ifndef __APP_MODE_H__
#define __APP_MODE_H__
#include <stdint.h>
/**
* @brief 初始化模式状态机
* @return 无
*/
void app_mode_init(void);
/**
* @brief 推进模式状态机,每个业务 tick 调用一次
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_mode_run(uint16_t dt);
/**
* @brief APP 写 DP1 / 手动抢权:切到指定模式或退出(P0 关闭时忽略)
* @param mode 0=关闭模式,1=通风,2=强力
* @return 无
*/
void app_mode_set_manual(uint8_t mode);
/**
* @brief 查询当前逻辑模式
* @return 0/1/2
*/
uint8_t app_mode_get_active(void);
/**
* @brief 停止当前模式(风扇由 app_fan 接管,这里只清模式状态)
* @return 无
*/
void app_mode_stop(void);
#endif /* __APP_MODE_H__ */
+423
View File
@@ -0,0 +1,423 @@
/******************************************************************************
* @file app_nvm.c
* @brief CBR3 VM 持久化实现(P1):mode/fan_gear/条件/定时/使能
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.1.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 stub
* - V1.1.0, 2026.08.31, cyWu, P1 实现:3 个 VM item + 防抖 + 立即写
*
* @note VM item 分配在 syscfg 用户自定义区 [1~49]syscfg_id.h 已占用
* CFG_USER_PAIR_INFO=2 / CFG_USER_TIM_FLAG=3 / VM_ALARM*=4~24 /
* CFG_USER_DEVICE_STA=25 / CFG_USER_PLAN_INFO1=26 /
* CFG_USER_BATTERY_LEVEL=27),本产品从 28 开始:
* CFG_USER_CBR3_STATE=28 状态(magic+ver+mode+gear+en 等,10B
* CFG_USER_CBR3_COND =29 条件配置 cond_config[7][7]49B
* CFG_USER_CBR3_TIME =30 定时配置 time_config[7][8]56B
* 拆 3 个 item:改档/改模式只擦写 10B 小 item,不重写 105B 配置区,
* 减少 flash 磨损;首次上电(magic 不符)保持 gDevData 全 0 默认。
*
* 写入时机(规格书第 13 章):APP 改配置后(mark_dirty)、档位/模式
* 稳定变化(500ms 防抖)、软关机前(save 立即写)。
*
* ⚠⚠ 上下文硬约束(2026-08-31 复位事故定论,勿再踩):
* usr_timer_add() 是**异步接口**,回调在**硬件定时器中断上下文**执行
* (栈极小);sys_timer_add() 是同步接口,回调在 add 它的线程执行。
* 因此:
* - app_nvm_tick() 跑在 20ms 中断上下文 → **只能做变化检测与计时,
* 绝对不能调 syscfg_write**VM 擦写 flash 栈用量大且中断里做
* flash 操作 → 栈溢出软复位;实测 APP 下发数据后 500ms 必崩)。
* - 真正写入由 app_nvm_flush() 在 usr_timer_loop()BLE 线程、
* 任务上下文、栈充足)执行;usr_poweroff_request_hook 里的
* app_nvm_save() 也必须保证在任务上下文被调用(见 app_function_loop)。
* 另:nvm_write_item 的比较缓冲改 static,再省 128B 栈。
******************************************************************************/
#include "app_nvm.h"
#include "app_mode.h"
#include "app_fan.h"
#include "jb_product.h"
#include "board_pin.h"
#include "system/includes.h"
#include "syscfg_id.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_NVM]"
#define LOG_ERROR_ENABLE
#define LOG_INFO_ENABLE
#include "debug.h"
/* ── VM item id(用户区 1~49 内申请)── */
#define CFG_USER_CBR3_STATE 28
#define CFG_USER_CBR3_COND 29
#define CFG_USER_CBR3_TIME 30
#define CBR3_VM_MAGIC 0xCB03
#define CBR3_VM_VER 1
#define CBR3_VM_DEBOUNCE_MS 500 /* 档位/模式稳定变化防抖(规格书 13 章) */
/** 状态 item10 字节) */
typedef struct {
uint16_t magic; /**< CBR3_VM_MAGIC */
uint8_t ver; /**< CBR3_VM_VER */
uint8_t mode; /**< 逻辑模式 0/1/2 */
uint8_t fan_gear; /**< 真实档位 0~3(模式未激活时即手动档) */
uint8_t cond_en; /**< DP5 条件总开关 */
uint8_t time_en; /**< DP13 定时总开关 */
uint8_t fault_en; /**< DP21 */
uint8_t alarm_en; /**< DP23 */
} __packed Cbr3StateVm_t;
/** 条件配置 item49 字节) */
typedef struct {
uint8_t cond_config[7][7];
} __packed Cbr3CondVm_t;
/** 定时配置 item56 字节) */
typedef struct {
uint8_t time_config[7][8];
} __packed Cbr3TimeVm_t;
#if BOARD_NVM_ENABLE
static uint8_t s_dirty; /**< 1=防抖计时中 */
static uint8_t s_need_save; /**< 1=防抖已满,待任务上下文落盘 */
static uint16_t s_deb_ms; /**< 防抖计时 */
/* 上次已同步到 VM 的状态快照:变化检测基准(覆盖按键/APP/模式引擎所有入口) */
static uint8_t s_last_gear;
static uint8_t s_last_mode;
static uint8_t s_last_cond_en;
static uint8_t s_last_time_en;
#endif
/**
* @brief 从 gDevData 收集条件配置到 VM 结构
* @note gDevData.cond_config1~7 是 7 个独立数组字段,用指针表统一搬运
* @param out 输出结构
* @return 无
*/
static void nvm_collect_cond(Cbr3CondVm_t *out)
{
const uint8_t *src[7] = {
gDevData.cond_config1, gDevData.cond_config2, gDevData.cond_config3,
gDevData.cond_config4, gDevData.cond_config5, gDevData.cond_config6,
gDevData.cond_config7
};
uint8_t i;
for (i = 0; i < 7; i++) {
memcpy(out->cond_config[i], src[i], 7);
}
}
/**
* @brief 从 gDevData 收集定时配置到 VM 结构
* @param out 输出结构
* @return 无
*/
static void nvm_collect_time(Cbr3TimeVm_t *out)
{
const uint8_t *src[7] = {
gDevData.time_config1, gDevData.time_config2, gDevData.time_config3,
gDevData.time_config4, gDevData.time_config5, gDevData.time_config6,
gDevData.time_config7
};
uint8_t i;
for (i = 0; i < 7; i++) {
memcpy(out->time_config[i], src[i], 8);
}
}
/**
* @brief 写单个 item:先读回比较,内容相同跳过,避免无谓擦写 flash
* @param item_id VM item id
* @param buf 新数据
* @param len 数据长度
* @return 无
*/
static void nvm_write_item(u16 item_id, void *buf, u16 len)
{
/* static:比较缓冲不放栈上(省 128B 栈);本函数只在任务上下文调用,无重入 */
static uint8_t old[128];
if (len > sizeof(old)) {
log_error("nvm item %d len %d too big\n", item_id, len);
return;
}
if (syscfg_read(item_id, old, len) == len) {
if (memcmp(old, buf, len) == 0) {
return; /* 没变化,不擦写 */
}
}
if (syscfg_write(item_id, buf, len) > 0) {
log_info("nvm item %d saved %dB\n", item_id, len);
} else {
log_error("nvm item %d write fail\n", item_id);
}
}
/**
* @brief 写状态 itemmode/gear/使能)
* @return 无
*/
static void nvm_write_state(void)
{
Cbr3StateVm_t st;
st.magic = CBR3_VM_MAGIC;
st.ver = CBR3_VM_VER;
st.mode = app_mode_get_active();
st.fan_gear = app_fan_get_gear();
st.cond_en = gDevData.cond_en ? 1 : 0;
st.time_en = gDevData.time_en ? 1 : 0;
st.fault_en = gDevData.fault_en ? 1 : 0;
st.alarm_en = gDevData.alarm_en ? 1 : 0;
nvm_write_item(CFG_USER_CBR3_STATE, &st, sizeof(st));
}
/**
* @brief 写条件配置 item
* @return 无
*/
static void nvm_write_cond(void)
{
Cbr3CondVm_t cf;
nvm_collect_cond(&cf);
nvm_write_item(CFG_USER_CBR3_COND, &cf, sizeof(cf));
}
/**
* @brief 写定时配置 item
* @return 无
*/
static void nvm_write_time(void)
{
Cbr3TimeVm_t tf;
nvm_collect_time(&tf);
nvm_write_item(CFG_USER_CBR3_TIME, &tf, sizeof(tf));
}
/**
* @brief 初始化:校验 VM 有效性并打日志(加载在 app_nvm_load
* @return 无
*/
void app_nvm_init(void)
{
#if !BOARD_NVM_ENABLE
log_info("nvm disabled\n");
return;
#else
Cbr3StateVm_t st;
int ret = syscfg_read(CFG_USER_CBR3_STATE, &st, sizeof(st));
if ((ret == (int)sizeof(st)) && (st.magic == CBR3_VM_MAGIC) && (st.ver == CBR3_VM_VER)) {
log_info("nvm init, vm valid (mode=%d gear=%d cond_en=%d time_en=%d)\n",
st.mode, st.fan_gear, st.cond_en, st.time_en);
} else {
log_info("nvm init, vm invalid (ret=%d), use defaults\n", ret);
}
s_dirty = 0;
s_deb_ms = 0;
s_need_save = 0;
#endif
}
/**
* @brief 同步"已落盘状态"快照为当前值(save/load 后调用)
* @return 无
*/
static void nvm_sync_snapshot(void)
{
#if BOARD_NVM_ENABLE
s_last_gear = app_fan_get_gear();
s_last_mode = app_mode_get_active();
s_last_cond_en = gDevData.cond_en ? 1 : 0;
s_last_time_en = gDevData.time_en ? 1 : 0;
#endif
}
/**
* @brief 立即保存全部业务字段(软关机前调用)
* @return 无
*/
void app_nvm_save(void)
{
#if !BOARD_NVM_ENABLE
return;
#else
nvm_write_state();
nvm_write_cond();
nvm_write_time();
s_dirty = 0;
s_deb_ms = 0;
s_need_save = 0;
nvm_sync_snapshot();
#endif
}
/**
* @brief 从 VM 加载并应用(开机 3s 后由 usr_jb_init 调用)
* @note 顺序:先恢复档位再恢复模式(app_mode_set_manual 内部会快照当前档、
* 并让通风/强力从周期头开跑,规格书第 9 章"开机恢复不存相位")。
* VM 无效时保持 gDevData 全 0(首次上电默认 mode=0 gear=0)。
* @return 无
*/
void app_nvm_load(void)
{
#if !BOARD_NVM_ENABLE
return;
#else
Cbr3StateVm_t st;
Cbr3CondVm_t cf;
Cbr3TimeVm_t tf;
uint8_t i;
if ((syscfg_read(CFG_USER_CBR3_STATE, &st, sizeof(st)) != (int)sizeof(st))
|| (st.magic != CBR3_VM_MAGIC) || (st.ver != CBR3_VM_VER)) {
log_info("nvm load: no valid vm, defaults (mode=0 gear=0)\n");
return;
}
/* 条件/定时配置回填 gDevData(只存不跑,P2 引擎再消费) */
if (syscfg_read(CFG_USER_CBR3_COND, &cf, sizeof(cf)) == (int)sizeof(cf)) {
uint8_t *dst[7] = {
gDevData.cond_config1, gDevData.cond_config2, gDevData.cond_config3,
gDevData.cond_config4, gDevData.cond_config5, gDevData.cond_config6,
gDevData.cond_config7
};
for (i = 0; i < 7; i++) {
memcpy(dst[i], cf.cond_config[i], 7);
}
}
if (syscfg_read(CFG_USER_CBR3_TIME, &tf, sizeof(tf)) == (int)sizeof(tf)) {
uint8_t *dst[7] = {
gDevData.time_config1, gDevData.time_config2, gDevData.time_config3,
gDevData.time_config4, gDevData.time_config5, gDevData.time_config6,
gDevData.time_config7
};
for (i = 0; i < 7; i++) {
memcpy(dst[i], tf.time_config[i], 8);
}
}
gDevData.cond_en = st.cond_en ? 1 : 0;
gDevData.time_en = st.time_en ? 1 : 0;
gDevData.fault_en = st.fault_en ? 1 : 0;
gDevData.alarm_en = st.alarm_en ? 1 : 0;
/* 先档位后模式;mode 激活时引擎会从周期头强制档位 */
if (st.fan_gear > 3) {
st.fan_gear = 0;
}
app_fan_set_gear(st.fan_gear);
app_mode_set_manual(st.mode);
nvm_sync_snapshot(); /* 恢复值即为已落盘值,同步基准 */
log_info("nvm load: mode=%d gear=%d cond_en=%d time_en=%d\n",
st.mode, st.fan_gear, st.cond_en, st.time_en);
#endif
}
/**
* @brief 业务节拍:做档位/模式/总开关变化检测 + 防抖计时,**不执行写入**
* @param dt 距上次调用的毫秒数
* @note ⚠ 本函数跑在 usr_timer_add 的 20ms 回调里 = **硬件定时器中断上下文**
* (异步接口,栈极小)。这里只允许做轻量比较与计时;真正的
* syscfg_write 由 app_nvm_flush() 在 usr_timer_loopBLE 线程、
* 任务上下文)执行。曾在此直接调 app_nvm_save 导致栈溢出软复位。
* @return 无
*/
void app_nvm_tick(uint16_t dt)
{
#if !BOARD_NVM_ENABLE
(void)dt;
return;
#else
uint8_t gear = app_fan_get_gear();
uint8_t mode = app_mode_get_active();
uint8_t cond_en = gDevData.cond_en ? 1 : 0;
uint8_t time_en = gDevData.time_en ? 1 : 0;
if ((gear != s_last_gear) || (mode != s_last_mode)
|| (cond_en != s_last_cond_en) || (time_en != s_last_time_en)) {
s_last_gear = gear;
s_last_mode = mode;
s_last_cond_en = cond_en;
s_last_time_en = time_en;
s_dirty = 1;
s_deb_ms = 0; /* 变化重新计满防抖窗口 */
}
if (!s_dirty) {
return;
}
s_deb_ms = (uint16_t)(s_deb_ms + dt);
if (s_deb_ms >= CBR3_VM_DEBOUNCE_MS) {
s_deb_ms = 0;
s_dirty = 0;
s_need_save = 1; /* 只置标志:等任务上下文 app_nvm_flush 真正落盘 */
}
#endif
}
/**
* @brief 任务上下文落盘:防抖满后真正写 VM(由 usr_timer_loop 500ms 调用)
* @note 必须且只能在**非中断上下文**调用(sys_timer 回调 / BLE 线程 /
* app_core 任务)。VM 擦写 flash 栈用量大,禁止放进 20ms 业务节拍。
* @return 无
*/
void app_nvm_flush(void)
{
#if !BOARD_NVM_ENABLE
return;
#else
if (!s_need_save) {
return;
}
s_need_save = 0;
app_nvm_save();
#endif
}
/**
* @brief 标记业务字段有变化,防抖计时归零(重新计满 CBR3_VM_DEBOUNCE_MS 才写)
* @return 无
*/
void app_nvm_mark_dirty(void)
{
#if BOARD_NVM_ENABLE
s_dirty = 1;
s_deb_ms = 0;
#endif
}
/**
* @brief 出厂重置:清全部业务 VM(保留 BLE 配对,规格书第 14 章)
* @note 3 个 item 写全 0 → magic 失效,下次开机走首次上电默认值;
* BLE 配对三元组在 CFG_USER_PAIR_INFOitem 2),不受影响
* @return 无
*/
void app_nvm_reset(void)
{
#if !BOARD_NVM_ENABLE
return;
#else
Cbr3StateVm_t st;
Cbr3CondVm_t cf;
Cbr3TimeVm_t tf;
memset(&st, 0, sizeof(st));
memset(&cf, 0, sizeof(cf));
memset(&tf, 0, sizeof(tf));
syscfg_write(CFG_USER_CBR3_STATE, &st, sizeof(st));
syscfg_write(CFG_USER_CBR3_COND, &cf, sizeof(cf));
syscfg_write(CFG_USER_CBR3_TIME, &tf, sizeof(tf));
s_dirty = 0;
s_deb_ms = 0;
s_need_save = 0;
log_info("nvm factory reset, business vm cleared (pairing kept)\n");
#endif
}
+73
View File
@@ -0,0 +1,73 @@
/******************************************************************************
* @file app_nvm.h
* @brief CBR3 VM 持久化(P1 起):mode/fan_gear/条件/定时/使能
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.1.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 仅接口与 stub)
* - V1.1.0, 2026.08.31, cyWu, P1 实现:3 个 VM item(状态/条件/定时)+ 500ms
* 防抖写入 + 立即写接口
*
* @note 存:mode、fan_gear、cond_en、cond_config1~7、time_en、time_config1~7、
* fault_en、alarm_en。不存:DP0、条件作废标志、模式已运行秒数、原始采样。
* 写 VM 时机:APP 改配置后、单次定时自毁、软关机前、档位/模式稳定变化
* 500ms 防抖)。
******************************************************************************/
#ifndef __APP_NVM_H__
#define __APP_NVM_H__
#include <stdint.h>
/**
* @brief 初始化(校验 VM 有效性,仅日志;数据加载在 app_nvm_load
* @return 无
*/
void app_nvm_init(void);
/**
* @brief 立即保存全部业务字段到 VM(软关机前等关键时机调用)
* @note syscfg 写入不能在裸中断里调用;本函数由业务时基回调触发,
* 与 SDK norflash 缓存同步(sys_timeout 回调)同级,可安全执行
* @return 无
*/
void app_nvm_save(void);
/**
* @brief 从 VM 加载业务字段并应用(开机 3s 后调用):
* mode/fan_gear 恢复、条件/定时配置回填 gDevData;mode 从周期头开跑
* @return 无
*/
void app_nvm_load(void);
/**
* @brief 业务节拍(中断上下文):变化检测 + 防抖计时,**不执行写入**
* @param dt 距上次调用的毫秒数
* @note ⚠ 跑在 usr_timer 20ms 回调 = 硬件定时器中断上下文,栈极小,
* 只允许轻量比较计时;禁止在此调 syscfg_write(会栈溢出复位)。
* @return 无
*/
void app_nvm_tick(uint16_t dt);
/**
* @brief 任务上下文落盘:防抖满后真正写 VM(由 usr_timer_loop 500ms 调用)
* @note 必须且只能在非中断上下文调用(VM 擦写 flash 栈用量大)
* @return 无
*/
void app_nvm_flush(void);
/**
* @brief 标记业务字段有变化,防抖计时归零(档位/模式/配置变化时调用)
* @return 无
*/
void app_nvm_mark_dirty(void);
/**
* @brief 出厂重置:清全部业务 VM(保留 BLE 配对,规格书第 14 章)
* @note 由 jb_product DP25 调用;RAM 复位由调用方完成
* @return 无
*/
void app_nvm_reset(void);
#endif /* __APP_NVM_H__ */
+76
View File
@@ -0,0 +1,76 @@
/******************************************************************************
* @file app_pair.c
* @brief 配对会话计时实现
* @author cyWu <1917507415@qq.com>
* @date 2026.08.25
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现配对会话计时与成功/超时判定
******************************************************************************/
#include "app_pair.h"
#include "board_pin.h"
#include "usr_le_api.h"
#include "system/includes.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_PAIR]"
#define LOG_INFO_ENABLE
#include "debug.h"
/** 配对会话状态,替代原来散落的 s_pairing / s_pair_ms */
typedef struct {
uint8_t active;
uint32_t elapsed_ms;
} AppPairCtx_t;
static AppPairCtx_t s_pair;
/**
* @brief 开始一次配对会话(计时清零)
* @return 无
*/
void app_pair_start(void)
{
s_pair.active = 1;
s_pair.elapsed_ms = 0;
}
/**
* @brief 推进配对计时并检查结果,每个业务 tick 调用一次
* @param dt 距上次调用的毫秒数
* @return APP_PAIR_EVT_OK=配对成功,APP_PAIR_EVT_FAIL=超时,否则 NONE
*/
AppPairEvt_t app_pair_tick_1ms(uint16_t dt)
{
if (!s_pair.active) {
return APP_PAIR_EVT_NONE;
}
/* BLE 侧绑定成功:结束本次配对会话 */
if (usr_get_pair_flag()) {
s_pair.active = 0;
s_pair.elapsed_ms = 0;
log_info("pair ok\n");
return APP_PAIR_EVT_OK;
}
s_pair.elapsed_ms += dt;
if (s_pair.elapsed_ms >= BOARD_PAIR_TIMEOUT_MS) {
s_pair.active = 0;
s_pair.elapsed_ms = 0;
log_info("pair timeout 120s, stay on\n");
return APP_PAIR_EVT_FAIL;
}
return APP_PAIR_EVT_NONE;
}
/**
* @brief 查询是否仍在配对会话中
* @return 1=进行中,0=未在配对
*/
uint8_t app_pair_is_active(void)
{
return s_pair.active;
}
+45
View File
@@ -0,0 +1,45 @@
/******************************************************************************
* @file app_pair.h
* @brief 配对会话计时:开机配对窗口是否成功/超时,与 BLE 配对本身解耦
* @author cyWu <1917507415@qq.com>
* @date 2026.08.25
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现配对会话计时与成功/超时判定
*
* @note 真正的 BLE 广播/绑定逻辑在 usr_le_code;本模块只负责“进入配对后
* 120s 内 usr_get_pair_flag() 是否变 1”这件事,纯状态,不碰灯/按键。
******************************************************************************/
#ifndef __APP_PAIR_H__
#define __APP_PAIR_H__
#include <stdint.h>
/** 配对结果边沿事件,仅在状态变化那一次返回一次 */
typedef enum {
APP_PAIR_EVT_NONE = 0,
APP_PAIR_EVT_OK,
APP_PAIR_EVT_FAIL
} AppPairEvt_t;
/**
* @brief 开始一次配对会话(计时清零)
* @return 无
*/
void app_pair_start(void);
/**
* @brief 推进配对计时并检查结果,每个业务 tick 调用一次
* @param dt 距上次调用的毫秒数
* @return APP_PAIR_EVT_OK=配对成功,APP_PAIR_EVT_FAIL=超时,否则 NONE
*/
AppPairEvt_t app_pair_tick_1ms(uint16_t dt);
/**
* @brief 查询是否仍在配对会话中
* @return 1=进行中,0=未在配对
*/
uint8_t app_pair_is_active(void);
#endif /* __APP_PAIR_H__ */
+100
View File
@@ -0,0 +1,100 @@
/******************************************************************************
* @file app_power.c
* @brief CBR3 软关机生命周期实现:无电池、无充电
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹精简:删 USB/充电/电量/低电)
******************************************************************************/
#include "app_power.h"
#include "system/includes.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_PWR]"
#define LOG_INFO_ENABLE
#include "debug.h"
/** 电源模块总状态 */
typedef struct {
uint8_t req_poweroff;
AppPowerHook_t on_request;
AppPowerHook_t on_finalize;
} AppPowerCtx_t;
static AppPowerCtx_t s_power;
static void power_do_poweroff(void *priv);
/**
* @brief 初始化电源模块状态
* @return 无
*/
void app_power_init(void)
{
s_power.req_poweroff = 0;
s_power.on_request = NULL;
s_power.on_finalize = NULL;
}
/**
* @brief 注册深睡请求的两段式回调
* @param on_request 刚请求深睡时调用(停风扇;灯由调用方按关机类型决定)
* @param on_finalize 延时到点后调用(真正进深睡,不会返回)
* @return 无
*/
void app_power_set_hooks(AppPowerHook_t on_request, AppPowerHook_t on_finalize)
{
s_power.on_request = on_request;
s_power.on_finalize = on_finalize;
}
/**
* @brief 请求深睡(按键 3s 等):立即触发 on_request 钩子,50ms 后触发 on_finalize
* @return 无
*/
void app_power_request_poweroff(void)
{
if (s_power.req_poweroff) {
return; /* 已经在关机流程里,不要重复挂定时器 */
}
s_power.req_poweroff = 1;
log_info("request deep off\n");
if (s_power.on_request) {
s_power.on_request(); /* 立刻停风扇、灯全灭 */
}
sys_timeout_add(NULL, power_do_poweroff, 50); /* 给 GPIO 落稳一点再真正掉电 */
}
/**
* @brief 查询深睡请求是否已发起
* @return 1=已发起,0=未发起
*/
uint8_t app_power_poweroff_pending(void)
{
return s_power.req_poweroff;
}
/**
* @brief 当前这次关机是否为空电路径(本产品无电池,恒 0)
* @return 0
*/
uint8_t app_power_empty_off_pending(void)
{
return 0;
}
/**
* @brief 50ms 延时到点:真正进入深睡
* @param priv 未使用
* @return 无
*/
static void power_do_poweroff(void *priv)
{
(void)priv;
log_info("enter deep off\n");
if (s_power.on_finalize) {
s_power.on_finalize();
}
}
+55
View File
@@ -0,0 +1,55 @@
/******************************************************************************
* @file app_power.h
* @brief CBR3 软关机生命周期:无电池、无充电,只保留 request/finalize
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹精简:删 USB/充电/电量/低电)
*
* @note 本产品 USB 常电、无电池,插电不会自动开机,必须长按 3s。
* SDK 层 TCFG_CHARGE 相关配置保留为板级模板默认,但业务层完全不
* 参与充电/电量逻辑(规格书第 0 章第 3 条)。
******************************************************************************/
#ifndef __APP_POWER_H__
#define __APP_POWER_H__
#include <stdint.h>
/** 深睡请求的钩子:on_request 立刻停执行体并按关机类型处理灯;on_finalize 真正掉电 */
typedef void (*AppPowerHook_t)(void);
/**
* @brief 初始化电源模块状态
* @return 无
*/
void app_power_init(void);
/**
* @brief 注册深睡请求的两段式回调
* @param on_request 刚请求深睡时调用(停风扇;灯由调用方按关机类型决定)
* @param on_finalize 延时到点后调用(真正进深睡,不会返回)
* @return 无
*/
void app_power_set_hooks(AppPowerHook_t on_request, AppPowerHook_t on_finalize);
/**
* @brief 请求深睡(按键 3s 等):立即触发 on_request 钩子,50ms 后触发 on_finalize
* @return 无
*/
void app_power_request_poweroff(void);
/**
* @brief 查询深睡请求是否已发起(发起后其它模块应停止响应新操作)
* @return 1=已发起,0=未发起
*/
uint8_t app_power_poweroff_pending(void);
/**
* @brief 当前这次关机是否为空电路径(本产品无电池,恒 0,保留接口)
* @return 0
*/
uint8_t app_power_empty_off_pending(void);
#endif /* __APP_POWER_H__ */
+171
View File
@@ -0,0 +1,171 @@
/******************************************************************************
* @file app_th.c
* @brief CBR3 温湿度采集实现
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布
******************************************************************************/
#include "app_th.h"
#include "bsp_aht20.h"
#include "board_pin.h"
#include "system/includes.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_TH]"
#define LOG_ERROR_ENABLE
#define LOG_INFO_ENABLE
#include "debug.h"
extern u32 timer_get_ms(void);
extern void clr_wdt(void);
/** 采样状态机 */
typedef enum {
TH_ST_IDLE = 0, /**< 空闲,等待下一个采样周期 */
TH_ST_WAIT_DATA /**< 命令已发出,等 ≥80ms 后读数 */
} ThState_t;
typedef struct {
uint8_t inited;
ThState_t state;
uint16_t poll_ms; /**< 距上次采样的累计毫秒 */
uint32_t wait_t0; /**< 测量命令发出时刻 */
int16_t temp_x10; /**< 最近有效温度 ×10 */
uint8_t humi; /**< 最近有效湿度 % */
uint8_t valid; /**< 1=有有效采样 */
uint8_t fail_cnt; /**< 连续失败次数 */
uint8_t last_ok; /**< 最近一次采样是否成功(P2 条件引擎 8.4 用) */
} AppThCtx_t;
static AppThCtx_t s_th;
/**
* @brief 初始化温湿度模块
* @return 无
*/
void app_th_init(void)
{
#if !BOARD_TH_ENABLE
log_info("th disabled\n");
return;
#endif
if (s_th.inited) {
return;
}
bsp_aht20_init();
s_th.state = TH_ST_IDLE;
s_th.poll_ms = 0;
s_th.valid = 0;
s_th.fail_cnt = 0;
s_th.last_ok = 0;
s_th.inited = 1;
log_info("th init period=%ums\n", (unsigned)BOARD_TH_PERIOD_MS);
}
/**
* @brief 业务节拍:内部 2s 才真正采一次;IO 分离后采集与显示互不干扰
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_th_poll(uint16_t dt)
{
#if !BOARD_TH_ENABLE
(void)dt;
return;
#endif
if (!s_th.inited) {
return;
}
if (s_th.state == TH_ST_IDLE) {
s_th.poll_ms = (uint16_t)(s_th.poll_ms + dt);
if (s_th.poll_ms >= BOARD_TH_PERIOD_MS) {
s_th.poll_ms = 0;
if (bsp_aht20_measure_start() == 0) {
s_th.state = TH_ST_WAIT_DATA;
s_th.wait_t0 = timer_get_ms();
}
/* measure_start 非 0=通讯失败,跳过本次采样 */
}
} else if (s_th.state == TH_ST_WAIT_DATA) {
clr_wdt();
if ((timer_get_ms() - s_th.wait_t0) >= BOARD_AHT20_WAIT_MS) {
int16_t t = 0;
uint8_t h = 0;
int8_t ret = bsp_aht20_measure_finish(&t, &h);
/* IO 分离后无总线竞争,finish 不会返回 -2;0=成功,-1=通讯失败 */
s_th.state = TH_ST_IDLE;
if (ret == 0) {
s_th.temp_x10 = t;
s_th.humi = h;
s_th.fail_cnt = 0;
s_th.valid = 1;
s_th.last_ok = 1;
log_info("th %d.%dC %d%%\n",
(int)(t / 10), (int)((t < 0) ? (-t % 10) : (t % 10)), h);
} else {
s_th.fail_cnt++;
s_th.last_ok = 0;
log_error("th sample fail %d\n", s_th.fail_cnt);
}
}
}
}
/**
* @brief 是否已有有效采样
* @return 1=有效,0=从未成功
*/
uint8_t app_th_get_valid(void)
{
return s_th.valid;
}
/**
* @brief 最近一次有效温度
* @return 温度 ×10
*/
int16_t app_th_get_temp_x10(void)
{
return s_th.temp_x10;
}
/**
* @brief 最近一次有效湿度
* @return 湿度 0~100 整数 %
*/
uint8_t app_th_get_humi(void)
{
return s_th.humi;
}
/**
* @brief 连续失败次数
* @return 连续失败次数
*/
uint8_t app_th_get_fail_cnt(void)
{
return s_th.fail_cnt;
}
/**
* @brief 传感器故障(连续失败 ≥ BOARD_TH_FAIL_MAX
* @return 1=故障(显示 --、fault_code=2),0=正常
*/
uint8_t app_th_is_fault(void)
{
return (s_th.fail_cnt >= BOARD_TH_FAIL_MAX) ? 1 : 0;
}
/**
* @brief 最近一次采样是否成功(P2 条件引擎 8.4:时长只在采样成功时累加)
* @return 1=成功,0=失败/未采样过
*/
uint8_t app_th_last_sample_ok(void)
{
return s_th.last_ok;
}
+69
View File
@@ -0,0 +1,69 @@
/******************************************************************************
* @file app_th.h
* @brief CBR3 温湿度采集:2s 周期、失败计数、最近有效值
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布
*
* @note 规格书 8.4:条件"持续时长"只在本次采样成功且比较成立时累加;
* 采样失败不累加、不清零;连续 3 次失败显示 "--" 且 fault_code=2。
* P0 不做条件比较,只做采样与失败计数。
******************************************************************************/
#ifndef __APP_TH_H__
#define __APP_TH_H__
#include <stdint.h>
/**
* @brief 初始化温湿度模块
* @return 无
*/
void app_th_init(void);
/**
* @brief 业务节拍:内部 2s 才真正采一次;采的时候走总线互斥
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_th_poll(uint16_t dt);
/**
* @brief 是否已有有效采样
* @return 1=有效,0=从未成功
*/
uint8_t app_th_get_valid(void);
/**
* @brief 最近一次有效温度
* @return 温度 ×10(如 25.3℃ → 253
*/
int16_t app_th_get_temp_x10(void);
/**
* @brief 最近一次有效湿度
* @return 湿度 0~100 整数 %
*/
uint8_t app_th_get_humi(void);
/**
* @brief 连续失败次数
* @return 连续失败次数
*/
uint8_t app_th_get_fail_cnt(void);
/**
* @brief 传感器故障(连续失败 ≥ BOARD_TH_FAIL_MAX
* @return 1=故障(显示 --、fault_code=2),0=正常
*/
uint8_t app_th_is_fault(void);
/**
* @brief 最近一次采样是否成功(P2 条件引擎用,规格书 8.4)
* @return 1=成功,0=失败/未采样过
*/
uint8_t app_th_last_sample_ok(void);
#endif /* __APP_TH_H__ */
+264
View File
@@ -0,0 +1,264 @@
/******************************************************************************
* @file app_timer_task.c
* @brief CBR3 定时引擎(P2):7 组 8 字节定时窗,RTC 门闩
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V2.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 stub
* - V1.1.0, 2026.08.31, cyWu, P1set_en/set_config 存 VM(引擎仍关)
* - V2.0.0, 2026.08.31, cyWu, P2 实现完整定时引擎(规格书第 11 章):
* RTC 门闩(未对时一律不跑)/对时补窗/7 组窗判断(跨午夜、星期位图)/
* 窗内切模式/结束收口(风扇 0 mode 0)/单次自毁清使能写 VM
*
* @note 运行上下文:app_timer_run 由 usr_timer_loop500ms,任务上下文)
* 调用——窗判断分钟级精度足够,且单次自毁要写 VM(禁止中断上下文)。
* 字节布局(规格书 11.1):Byte0=使能 Byte1=模式 Byte2=类型(0单次/
* 1每天/2每周) Byte3=星期位图(Bit0=周一…Bit6=周日,仅类型2)
* Byte4-5=开始时分 Byte6-7=结束时分。
* 12 章仲裁:定时 > 条件;手动介入后本窗剩余时间不自动抢回,
* 等下一窗开始沿或对时补窗;窗结束时若定时仍在占用才收口。
******************************************************************************/
#include "app_timer_task.h"
#include "app_arbiter.h"
#include "app_mode.h"
#include "app_fan.h"
#include "app_nvm.h"
#include "usr_rtc.h"
#include "jb_product.h"
#include "board_pin.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_TMR]"
#define LOG_INFO_ENABLE
#include "debug.h"
/** 定时引擎运行时状态 */
typedef struct {
uint8_t taken; /**< 1=本窗已接管过(手动打断后也不抢回,直到窗结束) */
uint8_t win_idx; /**< 当前/最近命中组 0~60xFF=无 */
uint8_t last_rtc_ok; /**< 上次 RTC 门闩状态(对时补窗沿检测) */
} TimerState_t;
static TimerState_t s_timer;
/**
* @brief 取第 idx 组定时配置(gDevData 里是 7 个独立数组字段)
* @param idx 0~6
* @return 配置指针;越界返回 NULL
*/
static uint8_t *timer_cfg_ptr(uint8_t idx)
{
switch (idx) {
case 0: return gDevData.time_config1;
case 1: return gDevData.time_config2;
case 2: return gDevData.time_config3;
case 3: return gDevData.time_config4;
case 4: return gDevData.time_config5;
case 5: return gDevData.time_config6;
case 6: return gDevData.time_config7;
default: return NULL;
}
}
/**
* @brief 计算某日期是星期几
* @param y 年
* @param m 月 1~12
* @param d 日
* @return 0=周一 … 6=周日(规格书 11.1 位图 Bit0=周一)
*/
static uint8_t timer_calc_dow(uint16_t y, uint8_t m, uint8_t d)
{
uint8_t mm = m;
uint16_t yy = y;
uint16_t k;
int h;
if (mm < 3) {
mm += 12;
yy--;
}
k = yy % 100;
/* 蔡勒公式(格里高利历):h=0 周六 … 6 周五 */
h = (d + 13 * (mm + 1) / 5 + k + k / 4 + (yy / 100) / 4 + 5 * (yy / 100)) % 7;
return (uint8_t)((h + 5) % 7); /* 0=周一 … 6=周日 */
}
/**
* @brief 当前时刻是否在窗口内
* @param cfg 8 字节配置
* @param t 本地时间
* @return 1=在窗内
*/
static uint8_t timer_in_window(const uint8_t *cfg, const struct sys_time *t)
{
uint8_t type = cfg[2];
int16_t start = (int16_t)cfg[4] * 60 + cfg[5];
int16_t end = (int16_t)cfg[6] * 60 + cfg[7];
int16_t now = (int16_t)t->hour * 60 + t->min;
if (start == end) {
return 0; /* 非法窗(零时长) */
}
if (type == 2) {
/* 每周:星期位图;跨午夜窗以"开始时刻所在本地星期"判断(11.1) */
uint8_t dow = timer_calc_dow(t->year, t->month, t->day);
uint8_t dow_bit;
if (start > end) {
/* 跨午夜窗:凌晨 [0,end) 属于昨晚的窗,用昨天星期 */
if (now < end) {
dow = (uint8_t)((dow + 6) % 7); /* 昨天 */
}
}
dow_bit = (uint8_t)(1u << dow);
if (!(cfg[3] & dow_bit)) {
return 0;
}
}
if (start < end) {
return ((now >= start) && (now < end)) ? 1 : 0;
}
return ((now >= start) || (now < end)) ? 1 : 0; /* 跨午夜 */
}
/**
* @brief 窗结束收口:释放定时占用 + 风扇 0 / mode 0 + 单次自毁
* @note 仅定时仍在占用时才收口(手动打断后不干预,12 章规则 2);
* 单次(类型 0)无论谁占用,结束时刻后清 Byte0 并写 VM(11.1
* @return 无
*/
static void timer_window_close(void)
{
uint8_t idx = s_timer.win_idx;
s_timer.taken = 0;
s_timer.win_idx = 0xFF;
if (app_arbiter_get_owner() == ARB_OWNER_TIMER) {
app_arbiter_timer_release();
app_mode_stop();
app_fan_set_gear(0);
log_info("timer window out, stop (mode=0 fan=0)\n");
} else {
log_info("timer window out (manual taken)\n");
}
/* 单次自毁:结束时刻过后 Byte0 清 0 并写 VM,不再跑(11.1 */
if ((idx != 0xFF) && (timer_cfg_ptr(idx)[2] == 0)) {
timer_cfg_ptr(idx)[0] = 0;
app_nvm_mark_dirty();
log_info("timer[%d] once done, en cleared\n", idx + 1);
}
}
/**
* @brief 初始化定时引擎
* @return 无
*/
void app_timer_init(void)
{
#if !BOARD_TIMER_ENABLE
log_info("timer engine disabled\n");
return;
#endif
s_timer.taken = 0;
s_timer.win_idx = 0xFF;
s_timer.last_rtc_ok = 0;
log_info("timer init\n");
}
/**
* @brief 推进定时引擎(500ms,任务上下文,由 usr_timer_loop 调用)
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_timer_run(uint16_t dt)
{
#if !BOARD_TIMER_ENABLE
(void)dt;
return;
#endif
struct sys_time t;
uint8_t rtc_ok = usr_rtc_get_time(&t);
uint8_t win_idx = 0xFF;
uint8_t i;
(void)dt;
/* 对时补窗(11.2):RTC 从无效→有效瞬间立即重判窗口 */
if (rtc_ok && !s_timer.last_rtc_ok) {
log_info("timer rtc synced, re-check window\n");
}
s_timer.last_rtc_ok = rtc_ok;
/* RTC 门闩 / time_en=0:等同所有窗立刻结束(11.2) */
if (!rtc_ok || !gDevData.time_en) {
if (s_timer.taken) {
timer_window_close();
}
return;
}
/* 找第一个命中的使能窗 */
for (i = 0; i < 7; i++) {
const uint8_t *cfg = timer_cfg_ptr(i);
if (!cfg || (cfg[0] == 0)) {
continue;
}
if (timer_in_window(cfg, &t)) {
win_idx = i;
break;
}
}
if (win_idx != 0xFF) {
/* ── 窗内 ── */
if (!s_timer.taken) {
/* 窗开始沿:接管(定时优先级最高,条件让位由条件引擎感知) */
const uint8_t *cfg = timer_cfg_ptr(win_idx);
app_arbiter_timer_take();
app_mode_set_manual(cfg[1]); /* 切 Byte1 模式(通风循环/强力一轮) */
s_timer.taken = 1;
s_timer.win_idx = win_idx;
log_info("timer[%d] window in, mode=%d\n", win_idx + 1, cfg[1]);
}
/* 已接管(或手动打断后不抢回,12 章规则 2):保持;
* 通风循环由 app_mode 自理;强力只一轮 15min 自停(11.3),
* 结束时刻由本函数窗结束沿收口 */
} else {
if (s_timer.taken) {
timer_window_close();
}
}
}
/**
* @brief APP 写 DP13 定时总开关:存 gDevData + 触发 VM 持久化
* @param en 0=关,1=开
* @note 引擎关闭时仍保存(P1 验收);置 1 后下个 tick 补窗判断
* @return 无
*/
void app_timer_set_en(uint8_t en)
{
app_nvm_mark_dirty();
log_info("time_en=%d (stored)\n", en);
}
/**
* @brief APP 写 DP14~20 定时配置:触发 VM 持久化(数据已在 gDevData
* @param idx 1~7
* @note 引擎关闭时仍保存
* @return 无
*/
void app_timer_set_config(uint8_t idx)
{
app_nvm_mark_dirty();
log_info("time_config[%d] stored\n", idx);
}
+49
View File
@@ -0,0 +1,49 @@
/******************************************************************************
* @file app_timer_task.h
* @brief CBR3 定时引擎(P2):7 组 8 字节定时窗,RTC 门闩
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 仅接口与 stub)
*
* @note 文件名避开 usr_rtc。字节布局见规格书 11.1Byte0=使能 Byte1=模式
* Byte2=类型(0单次/1每天/2每周) Byte3=星期位图 Byte4-5=开始时/分
* Byte6-7=结束时/分。RTC 未对时一律不执行(规格书 11.2)。
* ⚠ app_timer_run 由 usr_timer_loop500ms,任务上下文)调用
* (单次自毁要写 VM,禁止放 20ms 中断上下文)。
******************************************************************************/
#ifndef __APP_TIMER_TASK_H__
#define __APP_TIMER_TASK_H__
#include <stdint.h>
/**
* @brief 初始化定时引擎
* @return 无
*/
void app_timer_init(void);
/**
* @brief 推进定时引擎,每个业务 tick 调用一次
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_timer_run(uint16_t dt);
/**
* @brief APP 写 DP13 定时总开关
* @param en 0=关,1=开
* @return 无
*/
void app_timer_set_en(uint8_t en);
/**
* @brief APP 写 DP14~20 定时配置(8 字节)
* @param idx 1~7
* @return 无
*/
void app_timer_set_config(uint8_t idx);
#endif /* __APP_TIMER_TASK_H__ */
+267
View File
@@ -0,0 +1,267 @@
/******************************************************************************
* @file key.c
* @brief 按键状态机实现
* @author cyWu <1917507415@qq.com>
* @date 2026.08.25
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现按键短按/长按/释放状态机
******************************************************************************/
#include "key.h"
#include <stddef.h>
static uint32_t s_key_count_time;
static uint8_t s_key_num = USR_KEY_COUNT;
keyCategory_t keyTable[USR_KEY_COUNT];
/**
* @brief 读按键电平到 fsm
* @param key 状态机
* @return 1=已读,0=被屏蔽
*/
static uint8_t get_key_level(keyFSM_t *key)
{
if (key->keyShield == KEY_DISABLE) {
return 0;
}
if (key->keyReadValue == NULL) {
return 0;
}
key->keyLevel = (key->keyReadValue() == key->keyDownLevel) ? Bit_SET : Bit_RESET;
return 1;
}
/**
* @brief 单击 / 双击间隔判定
* @param click 状态机
* @return 1=可以出事件
*/
static uint8_t key_click_frequency(keyFSM_t *click)
{
click->keyInterval++;
switch (click->keyFrequency) {
case CLICK:
if (click->keyInterval >= (KEY_INTERVAL / (DEBOUNCE_TIME * KEY_TIMER_MS))) {
click->eventType = SHORT_Event;
return 1;
}
break;
case DbLCLICK:
if (click->keyInterval >= (KEY_INTERVAL / (DEBOUNCE_TIME * KEY_TIMER_MS))) {
click->eventType = DBCL_Event;
return 1;
}
break;
default:
return 1;
}
return 0;
}
/**
* @brief 按键状态机步进
* @param key_buf 状态机
* @return 1=已处理
*/
static uint8_t read_key_status(keyFSM_t *key_buf)
{
if (!get_key_level(key_buf)) {
return 0;
}
switch (key_buf->keyStatus) {
case KEY_ST_NULL:
if (key_buf->keyLevel == Bit_SET) {
key_buf->keyStatus = KEY_ST_SURE;
}
break;
case KEY_ST_SURE:
if (key_buf->keyLevel == Bit_SET) {
key_buf->eventType = DOWN_Event;
key_buf->keyStatus = KEY_ST_DOWN;
key_buf->keyCount = 0;
key_buf->keyLongFlag = 1;
} else {
key_buf->keyStatus = KEY_ST_NULL;
}
break;
case KEY_ST_DOWN:
if (key_buf->keyLevel != Bit_SET) {
if (key_buf->keyShortFlag == 0) {
key_buf->keyShortFlag = 1;
key_buf->keyFrequency++;
key_buf->keyInterval = 0;
}
if (key_click_frequency(key_buf)) {
key_buf->keyFrequency = 0;
key_buf->keyStatus = KEY_ST_NULL;
}
if ((key_buf->keyCount >= key_buf->keyLongTime / DEBOUNCE_TIME)
&& (key_buf->keyCount < key_buf->keyLastTime / DEBOUNCE_TIME)) {
key_buf->keyFrequency = 0;
key_buf->keyStatus = KEY_ST_NULL;
key_buf->eventType = RELEASE_Event;
}
key_buf->keyCount = 0;
key_buf->keyLongFlag = 1;
} else {
if ((++key_buf->keyCount >= key_buf->keyLastTime / DEBOUNCE_TIME)) {
key_buf->keyCount = 0;
key_buf->keyFrequency = 0;
key_buf->keyStatus = KEY_ST_LONG;
key_buf->eventType = LAST_Event;
key_buf->keyLongFlag = 1;
}
if ((key_buf->keyCount >= key_buf->keyLongTime / DEBOUNCE_TIME)
&& (key_buf->keyCount < key_buf->keyLastTime / DEBOUNCE_TIME)) {
key_buf->keyFrequency = 0;
if (key_buf->keyLongFlag == 1) {
key_buf->eventType = LONG_Event;
key_buf->keyLongFlag = 0;
}
}
key_buf->keyShortFlag = 0;
}
break;
case KEY_ST_LONG:
if (key_buf->keyLevel != Bit_SET) {
key_buf->keyStatus = KEY_ST_NULL;
key_buf->eventType = RELEASE_Event;
}
break;
default:
break;
}
return 1;
}
/**
* @brief 扫描全部按键状态机
* @return 无
*/
static void key_event_process(void)
{
size_t i;
for (i = 0; i < s_key_num; i++) {
(void)read_key_status(&keyTable[i].fsm);
}
}
/**
* @brief 1ms 调用一次;满 DEBOUNCE_TIME 才真正采样
* @return 无
*/
void keyCheckProcess(void)
{
s_key_count_time++;
if (s_key_count_time >= (DEBOUNCE_TIME / KEY_TIMER_MS)) {
s_key_count_time = 0;
key_event_process();
}
}
/**
* @brief 拷贝产品按键表
* @param keys 按键数组,长度 USR_KEY_COUNT
* @return 无
*/
void keyParaInit(keyCategory_t *keys)
{
if (keys == NULL) {
return;
}
if (USR_KEY_COUNT >= KEY_MAX_NUMBER) {
s_key_num = KEY_MAX_NUMBER;
}
memcpy(keyTable, keys, sizeof(keyCategory_t) * s_key_num);
}
/**
* @brief 运行中改长按/连按时长并复位状态
* @param key_event 按键索引
* @param key_param 新参数
* @return 无
*/
void setKeyEventParams(uint8_t key_event, keyCategory_t key_param)
{
if (key_event >= s_key_num) {
return;
}
keyTable[key_event].fsm.keyShield = key_param.fsm.keyShield;
keyTable[key_event].fsm.keyLongTime = key_param.fsm.keyLongTime;
keyTable[key_event].fsm.keyLastTime = key_param.fsm.keyLastTime;
keyTable[key_event].fsm.keyStatus = KEY_ST_NULL;
keyTable[key_event].fsm.eventType = NULL_Event;
keyTable[key_event].fsm.keyCount = 0;
}
/**
* @brief 派发按键事件回调
* @return 无
*/
void keyHandle(void)
{
size_t i;
for (i = 0; i < s_key_num; i++) {
if (keyTable[i].fsm.eventType == NULL_Event) {
continue;
}
switch (keyTable[i].fsm.eventType) {
case RELEASE_Event:
if (keyTable[i].func.releasePressCb != NULL) {
keyTable[i].func.releasePressCb();
}
keyTable[i].fsm.eventType = NULL_Event;
break;
case SHORT_Event:
if (keyTable[i].func.ShortPressCb != NULL) {
keyTable[i].func.ShortPressCb();
}
keyTable[i].fsm.eventType = NULL_Event;
break;
case DOWN_Event:
if (keyTable[i].func.downPressCb != NULL) {
keyTable[i].func.downPressCb();
}
keyTable[i].fsm.eventType = NULL_Event;
break;
case LONG_Event:
if (keyTable[i].func.longPressCb != NULL) {
keyTable[i].func.longPressCb();
}
keyTable[i].fsm.eventType = NULL_Event;
break;
case LAST_Event:
if (keyTable[i].func.lastPressCb != NULL) {
keyTable[i].func.lastPressCb();
}
break;
case DBCL_Event:
if (keyTable[i].func.dbclPressCb != NULL) {
keyTable[i].func.dbclPressCb();
}
keyTable[i].fsm.eventType = NULL_Event;
break;
default:
break;
}
}
}
/**
* @brief 复位指定按键状态机
* @param key_index 按键索引
* @return 无
*/
void reset_key_Status(keyList key_index)
{
if (key_index >= USR_KEY_COUNT) {
return;
}
keyTable[key_index].fsm.keyStatus = KEY_ST_NULL;
keyTable[key_index].fsm.eventType = NULL_Event;
keyTable[key_index].fsm.keyCount = 0;
}
+100
View File
@@ -0,0 +1,100 @@
/******************************************************************************
* @file key.h
* @brief 按键状态机:短按 / 长按 / 释放
* @author cyWu <1917507415@qq.com>
* @date 2026.08.25
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现按键短按/长按/释放状态机
******************************************************************************/
#ifndef __KEY_H__
#define __KEY_H__
#include <stdint.h>
#include <string.h>
/*============================================================================*/
/* 宏定义 */
/*============================================================================*/
#define KEY_TIMER_MS 1
#define KEY_MAX_NUMBER 12
#define DEBOUNCE_TIME 30
#define KEY_INTERVAL 30
#define CLICK 1
#define DbLCLICK 2
typedef enum {
USR_KEY_USER = 0,
USR_KEY_COUNT
} keyList;
typedef enum {
Bit_RESET = 0,
Bit_SET
} BitAction_t;
typedef enum {
KEY_ST_NULL = 0,
KEY_ST_RELEASE,
KEY_ST_SURE,
KEY_ST_UP,
KEY_ST_DOWN,
KEY_ST_LONG
} keyStatus_t;
typedef enum {
NULL_Event = 0,
DOWN_Event,
SHORT_Event,
LONG_Event,
LAST_Event,
DBCL_Event,
RELEASE_Event
} keyEvent_t;
typedef enum {
KEY_DISABLE = 0,
KEY_ENABLE = !KEY_DISABLE
} keyEnable_t;
typedef struct {
uint8_t keyShortFlag;
uint8_t keyLongFlag;
uint8_t keyInterval;
uint8_t keyFrequency;
uint16_t keyLongTime;
uint16_t keyLastTime;
uint32_t keyCount;
keyEnable_t keyShield;
BitAction_t keyLevel;
BitAction_t keyDownLevel;
keyStatus_t keyStatus;
keyEvent_t eventType;
uint8_t (*keyReadValue)(void);
} keyFSM_t;
typedef struct {
void (*nullPressCb)(void);
void (*releasePressCb)(void);
void (*downPressCb)(void);
void (*ShortPressCb)(void);
void (*longPressCb)(void);
void (*lastPressCb)(void);
void (*dbclPressCb)(void);
} keyFunc_t;
typedef struct {
keyFSM_t fsm;
keyFunc_t func;
} keyCategory_t;
void keyParaInit(keyCategory_t *keys);
void setKeyEventParams(uint8_t key_event, keyCategory_t key_param);
void keyCheckProcess(void);
void keyHandle(void);
void reset_key_Status(keyList key_index);
#endif /* __KEY_H__ */
+148
View File
@@ -0,0 +1,148 @@
/******************************************************************************
* @file key_manager.c
* @brief CBR3 按键:读 PB1,短按切档、长按 3s 关机交给 usr_jb_main
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,短按语义改切档)
******************************************************************************/
#include "system/includes.h"
#include "key_manager.h"
#include "bsp_hw.h"
#include "board_pin.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[KEY]"
#define LOG_INFO_ENABLE
#include "debug.h"
/** 模块状态,替代原来 2 个散落的 static */
typedef struct {
uint8_t initialized;
uint8_t boot_key_lock; /**< 1=开机判定时键还按着,等松开才认短按/长按 */
} KeyManagerCtx_t;
static KeyManagerCtx_t s_km;
/**
* @brief 读按键:1=按下
* @return 1=按下,0=松开
*/
static uint8_t key_user_read(void)
{
return bsp_key_is_pressed();
}
/**
* @brief 松开:清掉开机按着键的锁
* @return 无
*/
static void key_user_release(void)
{
if (s_km.boot_key_lock) {
s_km.boot_key_lock = 0;
log_info("boot key released\n");
}
}
/**
* @brief 短按:开机未松手前忽略
* @return 无
*/
static void key_user_short(void)
{
if (s_km.boot_key_lock) {
s_km.boot_key_lock = 0; /* 开机松手当成释放,不当短按切档 */
log_info("boot key released\n");
return;
}
usr_on_key_short();
}
/**
* @brief 长按 3s:开机未松手前忽略,避免开机手还按着就关机
* @return 无
*/
static void key_user_long(void)
{
if (s_km.boot_key_lock) {
return; /* 开机手还按着,忽略这次 3s 长按,避免一开机就关机 */
}
usr_on_key_long();
}
static keyCategory_t s_keys[USR_KEY_COUNT] = {
[USR_KEY_USER] = {
.fsm.keyShield = KEY_ENABLE,
.fsm.keyDownLevel = Bit_SET,
.fsm.eventType = NULL_Event,
.fsm.keyLongTime = BOARD_KEY_POWER_MS,
.fsm.keyLastTime = (BOARD_KEY_POWER_MS + 7000), /* 大于长按即可,不使用持续按 */
.fsm.keyReadValue = key_user_read,
.func.ShortPressCb = key_user_short,
.func.longPressCb = key_user_long,
.func.releasePressCb = key_user_release,
},
};
/**
* @brief 装载按键表
* @return 无
*/
void user_key_init(void)
{
if (s_km.initialized) {
return;
}
keyParaInit(s_keys);
s_km.boot_key_lock = 0;
s_km.initialized = 1;
}
/**
* @brief 开机判定结束后:键还按着则锁住,松开才认短按/长按
* @return 无
*/
void user_key_lock_if_held(void)
{
reset_key_Status(USR_KEY_USER);
if (bsp_key_is_pressed()) {
s_km.boot_key_lock = 1; /* 开机手还按着,等松开才认短按/长按,避免立刻关机 */
log_info("boot key held, wait release\n");
}
}
/**
* @brief 按真实毫秒推进状态机
* @param dt 距上次调用的毫秒数
* @return 无
*/
void user_key_scan(uint16_t dt)
{
uint16_t i;
if (!s_km.initialized) {
return;
}
if (dt == 0) {
dt = 1;
}
/* 按键库按 1ms 步进,业务节拍是 20ms,这里循环 dt 次对齐真实时间 */
for (i = 0; i < dt; i++) {
keyCheckProcess();
}
}
/**
* @brief 派发按键回调
* @return 无
*/
void user_key_handle(void)
{
if (!s_km.initialized) {
return;
}
keyHandle();
}
+53
View File
@@ -0,0 +1,53 @@
/******************************************************************************
* @file key_manager.h
* @brief CBR3 单按键:短按切档,长按 3s 关机;开机 3s/5s/12s 不走本库
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,语义改切档/关机)
******************************************************************************/
#ifndef __KEY_MANAGER_H__
#define __KEY_MANAGER_H__
#include "key.h"
/**
* @brief 业务层短按回调(app_function 实现)
* @return 无
*/
void usr_on_key_short(void);
/**
* @brief 业务层长按 3s 回调(app_function 实现)
* @return 无
*/
void usr_on_key_long(void);
/**
* @brief 装载按键表
* @return 无
*/
void user_key_init(void);
/**
* @brief 开机判定结束后:若键还按着则锁住,松开才认短按/长按
* @return 无
*/
void user_key_lock_if_held(void);
/**
* @brief 按真实毫秒推进状态机(20ms 节拍会循环 dt 次)
* @param dt 距上次调用的毫秒数
* @return 无
*/
void user_key_scan(uint16_t dt);
/**
* @brief 派发按键回调
* @return 无
*/
void user_key_handle(void);
#endif /* __KEY_MANAGER_H__ */
+328
View File
@@ -0,0 +1,328 @@
/******************************************************************************
* @file led.c
* @brief LED 事件框架实现:只改 ledMask 对应通道,红绿互不抢
* @author cyWu <1917507415@qq.com>
* @date 2026.08.25
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现按通道掩码独立开关的 LED 事件框架
******************************************************************************/
#include "led.h"
#include <stddef.h>
static LED_EventTableItem_t s_led_priority_table[PRIORITY_MAX];
static LED_EventTableItem_t s_led_event_table[LED_EVENT_MAX];
static LED_SetState_t s_led_set_state;
static TIME_Get_t s_time_get;
static uint32_t get_tick_diff(uint32_t meiosis);
static uint8_t led_lighting_update(LED_Config_t *led);
#if (BLINK_ENABLE)
static uint8_t led_blinking_update(LED_Config_t *led);
#endif
#if (ALTERNATING_BLINK_ENABLE)
static uint8_t led_alternating_update(LED_Config_t *led);
#endif
/**
* @brief 注册写灯回调,只对掩码里的通道动手
* @param setState 回调,led=通道掩码,state=LED_ON/OFF
* @return 无
*/
void LED_SetStateInit(LED_SetState_t setState)
{
s_led_set_state = setState;
}
/**
* @brief 注册毫秒时间戳回调
* @param getTime 回调
* @return 无
*/
void SET_TimeGetInit(TIME_Get_t getTime)
{
s_time_get = getTime;
}
/**
* @brief 拷贝产品事件表
* @param ledEvent 长度必须为 LED_EVENT_MAX
* @return 无
*/
void LED_EventTableInit(LED_EventTableItem_t ledEvent[])
{
if (ledEvent == NULL) {
return;
}
memset(s_led_priority_table, 0, sizeof(s_led_priority_table));
memset(s_led_event_table, 0, sizeof(s_led_event_table));
memcpy(s_led_event_table, ledEvent, sizeof(s_led_event_table));
}
/**
* @brief 把事件挂到对应优先级槽;同优先级会被覆盖
* @param ledEvent 事件号
* @return 无
*/
void LED_EventAdd(LED_Event_t ledEvent)
{
size_t i;
LED_EventTableItem_t *item;
LED_EventPriority_t prio;
if ((s_time_get == NULL) || (s_led_set_state == NULL)) {
return;
}
if (ledEvent >= LED_EVENT_MAX) {
return;
}
for (i = 0; i < LED_EVENT_MAX; i++) {
item = &s_led_event_table[i];
if (item->event != ledEvent) {
continue;
}
prio = item->priority;
if (prio >= PRIORITY_MAX) {
return;
}
if (s_led_priority_table[prio].event != ledEvent) {
item->config.currentTime = s_time_get();
if (s_led_priority_table[prio].event != LED_EVENT_NONE) {
/* 同优先级换事件:只关掉旧灯里新事件用不到的通道 */
s_led_set_state((uint16_t)(s_led_priority_table[prio].config.ledMask
^ item->config.ledMask),
LED_OFF);
}
}
s_led_priority_table[prio] = *item;
return;
}
}
/**
* @brief 清空优先级表(不改 GPIO,由调用方灭灯)
* @return 无
*/
void LED_EventAllDelete(void)
{
memset(s_led_priority_table, 0, sizeof(s_led_priority_table));
}
/**
* @brief 删除指定事件并关掉它占用的通道
* @param ledEvent 事件号
* @return 无
*/
void LED_EventDelete(LED_Event_t ledEvent)
{
size_t i;
for (i = 0; i < PRIORITY_MAX; i++) {
if (s_led_priority_table[i].event != ledEvent) {
continue;
}
memset(&s_led_priority_table[i], 0, sizeof(LED_EventTableItem_t));
if ((s_led_set_state != NULL) && (ledEvent < LED_EVENT_MAX)) {
s_led_set_state(s_led_event_table[ledEvent].config.ledMask, LED_OFF);
}
return;
}
}
/**
* @brief 从高优先级往下跑事件
* @note handler 返回 1 会拦住更低优先级。红绿要共存时 handler 必须返回 0
* @return 无
*/
void LED_EventHandle(void)
{
int i;
uint8_t ret;
for (i = (int)PRIORITY_MAX - 1; i >= 0; i--) {
if (s_led_priority_table[i].event == LED_EVENT_NONE) {
continue;
}
if (s_led_priority_table[i].ledEventHandler == NULL) {
continue;
}
ret = s_led_priority_table[i].ledEventHandler(&s_led_priority_table[i].config);
if (ret) {
return;
}
}
}
/**
* @brief 是否有事件在跑
* @return 1=有,0=无
*/
uint8_t LED_IsEventActive(void)
{
size_t i;
for (i = 0; i < PRIORITY_MAX; i++) {
if (s_led_priority_table[i].event != LED_EVENT_NONE) {
return 1;
}
}
return 0;
}
/**
* @brief 指定事件是否挂在优先级表里
* @param ledEvent 事件号
* @return 1=在,0=不在
*/
uint8_t LED_IsEventExist(LED_Event_t ledEvent)
{
size_t i;
for (i = 0; i < PRIORITY_MAX; i++) {
if (s_led_priority_table[i].event == ledEvent) {
return 1;
}
}
return 0;
}
/**
* @brief 按配置刷新一次灯效
* @param led 配置,不可为空
* @return 1=还在跑,0=次数用完
*/
uint8_t LED_Update(LED_Config_t *led)
{
if (led == NULL) {
return 0;
}
switch (led->state) {
case LED_LIGHT:
return led_lighting_update(led);
#if (BLINK_ENABLE)
case LED_BLINKING:
return led_blinking_update(led);
#endif
#if (ALTERNATING_BLINK_ENABLE)
case LED_ALTERNATING_BLINK:
return led_alternating_update(led);
#endif
default:
return 0;
}
}
/**
* @brief 毫秒时间差,兼容溢出
* @param meiosis 起点时间戳
* @return 差值 ms
*/
static uint32_t get_tick_diff(uint32_t meiosis)
{
uint32_t now;
if (s_time_get == NULL) {
return 0;
}
now = s_time_get();
if (now >= meiosis) {
return now - meiosis;
}
return (0xFFFFFFFFu - meiosis) + now;
}
/**
* @brief 常亮;runNum 按秒计,0=一直亮
* @param led 配置
* @return 1=还在亮,0=到点灭
*/
static uint8_t led_lighting_update(LED_Config_t *led)
{
uint16_t sec_count;
if (s_led_set_state == NULL) {
return 0;
}
sec_count = (uint16_t)(get_tick_diff(led->currentTime) / 1000u);
if ((led->runNum > 0) && (sec_count >= led->runNum)) {
s_led_set_state(led->ledMask, LED_OFF);
return 0;
}
s_led_set_state(led->ledMask, LED_ON);
return 1;
}
#if (BLINK_ENABLE)
/**
* @brief 闪烁;runNum 按完整周期计,0=一直闪
* @param led 配置
* @return 1=还在闪,0=到点灭
*/
static uint8_t led_blinking_update(LED_Config_t *led)
{
uint32_t run_cycle;
uint16_t blink_count;
uint32_t sec_left;
if (s_led_set_state == NULL) {
return 0;
}
run_cycle = led->blinkParams.onTime + led->blinkParams.offTime;
if (run_cycle == 0) {
return 0;
}
blink_count = (uint16_t)(get_tick_diff(led->currentTime) / run_cycle);
sec_left = get_tick_diff(led->currentTime) % run_cycle;
if ((led->runNum > 0) && (blink_count >= led->runNum)) {
s_led_set_state(led->ledMask, LED_OFF);
return 0;
}
if (sec_left <= led->blinkParams.onTime) {
s_led_set_state(led->ledMask, led->blinkParams.orDer ? LED_OFF : LED_ON);
} else {
s_led_set_state(led->ledMask, led->blinkParams.orDer ? LED_ON : LED_OFF);
}
return 1;
}
#endif
#if (ALTERNATING_BLINK_ENABLE)
/**
* @brief 两组灯按时间片轮流亮
* @param led 配置
* @return 1=还在跑,0=到点灭
*/
static uint8_t led_alternating_update(LED_Config_t *led)
{
uint16_t blink_count;
uint32_t sec_left;
uint32_t time_slice;
size_t i;
if (s_led_set_state == NULL) {
return 0;
}
if (led->alternatingBlinkParams.Cycle == 0) {
return 0;
}
blink_count = (uint16_t)(get_tick_diff(led->currentTime)
/ led->alternatingBlinkParams.Cycle);
sec_left = get_tick_diff(led->currentTime) % led->alternatingBlinkParams.Cycle;
if ((led->runNum > 0) && (blink_count >= led->runNum)) {
s_led_set_state(led->ledMask, LED_OFF);
return 0;
}
time_slice = led->alternatingBlinkParams.Cycle / ALTERNAT_GROUP;
s_led_set_state(led->ledMask, LED_OFF);
for (i = 0; i < ALTERNAT_GROUP; i++) {
if ((sec_left > (time_slice * i)) && (sec_left <= (time_slice * (i + 1)))) {
s_led_set_state(led->alternatingBlinkParams.Group[i], LED_ON);
break;
}
}
return 1;
}
#endif
+120
View File
@@ -0,0 +1,120 @@
/******************************************************************************
* @file led.h
* @brief LED 事件框架:按通道掩码独立开关
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V1.2.0
* @history
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现按通道掩码独立开关的 LED 事件框架
* - V1.1.0, 2026.08.27, cyWu, 新增 LED_EVENT_BOOT_REJECT
* - V1.2.0, 2026.08.31, cyWu, CBR3 单灯:裁掉充电/低电/空电等门夹事件
******************************************************************************/
#ifndef __LED_H__
#define __LED_H__
#include <stdint.h>
#include <string.h>
/*============================================================================*/
/* 宏定义 */
/*============================================================================*/
#define BLINK_ENABLE 1
#define ALTERNATING_BLINK_ENABLE 0
#define BREATH_ENABLE 0
#define RUNNING_ENABLE 0
#define MARQUEE_ENABLE 0
#define LED_ON 1
#define LED_OFF 0
#define LED_CHANNEL(user_led) (1u << (user_led))
#define ledPrintf(level, fmt, ...) ((void)0)
/*============================================================================*/
/* 类型 */
/*============================================================================*/
typedef enum {
LED_LIGHT = 0, /**< 常亮 */
LED_BLINKING, /**< 闪烁 */
LED_ALTERNATING_BLINK /**< 交替闪(本产品未用) */
} LED_State_t;
typedef enum {
LED_EVENT_NONE = 0,
LED_EVENT_STANDBY, /**< 开机常亮 */
LED_EVENT_PAIRING, /**< 配对快闪 */
LED_EVENT_PAIR_OK, /**< 配对成功常亮若干秒 */
LED_EVENT_PAIR_FAIL, /**< 配对失败闪若干秒 */
LED_EVENT_OTA, /**< OTA 闪,直到升级结束/复位 */
LED_EVENT_MAX
} LED_Event_t;
/**
* 同优先级会互相覆盖
*/
typedef enum {
PRIORITY_1 = 0, /**< 开机常亮 */
PRIORITY_2, /**< 配对中 / 配对结果 */
PRIORITY_3, /**< OTA */
PRIORITY_MAX
} LED_EventPriority_t;
#if (BLINK_ENABLE)
typedef struct {
uint8_t orDer; /**< 1=先灭后亮,0=先亮后灭 */
uint32_t onTime; /**< 亮时间 ms */
uint32_t offTime; /**< 灭时间 ms */
} LED_BlinkParams_t;
#endif
#if (ALTERNATING_BLINK_ENABLE)
#define ALTERNAT_GROUP 2
typedef struct {
uint16_t Group[ALTERNAT_GROUP]; /**< 交替两组的通道掩码 */
uint32_t Cycle; /**< 一整轮交替周期 ms */
} LED_AlternatingBlinkParams_t;
#endif
typedef struct {
LED_State_t state;
uint16_t ledMask; /**< 本事件只动这些通道,其它灯不受影响 */
uint16_t runNum; /**< 0=无限;常亮按秒计,闪烁按周期计 */
uint32_t currentTime;
#if (BLINK_ENABLE)
LED_BlinkParams_t blinkParams;
#endif
#if (ALTERNATING_BLINK_ENABLE)
LED_AlternatingBlinkParams_t alternatingBlinkParams;
#endif
} LED_Config_t;
typedef struct {
LED_Event_t event;
LED_EventPriority_t priority;
LED_Config_t config;
uint8_t (*ledEventHandler)(LED_Config_t *config);
} LED_EventTableItem_t;
typedef void (*LED_SetState_t)(uint16_t led, uint8_t state);
typedef uint32_t (*TIME_Get_t)(void);
/*============================================================================*/
/* 外部接口 */
/*============================================================================*/
void LED_SetStateInit(LED_SetState_t setState);
void SET_TimeGetInit(TIME_Get_t getTime);
void LED_EventTableInit(LED_EventTableItem_t ledEvent[]);
void LED_EventAdd(LED_Event_t ledEvent);
void LED_EventDelete(LED_Event_t ledEvent);
void LED_EventAllDelete(void);
void LED_EventHandle(void);
uint8_t LED_IsEventActive(void);
uint8_t LED_IsEventExist(LED_Event_t ledEvent);
uint8_t LED_Update(LED_Config_t *led);
#endif /* __LED_H__ */
+193
View File
@@ -0,0 +1,193 @@
/******************************************************************************
* @file led_manager.c
* @brief CBR3 灯事件:单灯 PC2,高优先级覆盖低优先级
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,删红绿双通道改单灯)
*
* @note 灯效优先级(见规格书第 6 章,高者覆盖):
* OTA(200ms 闪) > 配对中(100ms 快闪) / 配对结果 > 正常开机常亮。
* 单灯无充电/低电/空电事件。
******************************************************************************/
#include "led_manager.h"
#include "system/includes.h"
#include "bsp_hw.h"
#include "board_pin.h"
static uint8_t led_keep_handler(LED_Config_t *led_config);
static uint8_t led_standby_handler(LED_Config_t *led_config);
static uint8_t led_pairing_handler(LED_Config_t *led_config);
static uint8_t led_pair_ok_handler(LED_Config_t *led_config);
static uint8_t led_pair_fail_handler(LED_Config_t *led_config);
static uint8_t led_ota_handler(LED_Config_t *led_config);
/**
* @brief 只改掩码里的通道(单灯即唯一通道)
* @param led 通道掩码
* @param state LED_ON / LED_OFF
* @return 无
*/
static void set_led_multi(uint16_t led, uint8_t state)
{
if (led & LED_CHANNEL(LED_CH_ON)) {
bsp_led_set(state);
}
}
/**
* @brief 常亮/闪烁一直跑:返回 1 独占,单灯不允许叠加
* @param led_config 配置
* @return 1
*/
static uint8_t led_keep_handler(LED_Config_t *led_config)
{
(void)LED_Update(led_config);
return 1;
}
/**
* @brief 待机常亮;runNum>0 时亮满后删事件
* @param led_config 配置
* @return 1
*/
static uint8_t led_standby_handler(LED_Config_t *led_config)
{
if (LED_Update(led_config) == 0) {
LED_EventDelete(LED_EVENT_STANDBY);
}
return 1;
}
/**
* @brief 配对快闪(无限)
* @param led_config 配置
* @return 1
*/
static uint8_t led_pairing_handler(LED_Config_t *led_config)
{
(void)LED_Update(led_config);
return 1;
}
/**
* @brief 配对成功常亮若干秒后删事件
* @param led_config 配置
* @return 1
*/
static uint8_t led_pair_ok_handler(LED_Config_t *led_config)
{
if (LED_Update(led_config) == 0) {
LED_EventDelete(LED_EVENT_PAIR_OK);
}
return 1;
}
/**
* @brief 配对失败 200ms 闪若干秒后删事件
* @param led_config 配置
* @return 1
*/
static uint8_t led_pair_fail_handler(LED_Config_t *led_config)
{
if (LED_Update(led_config) == 0) {
LED_EventDelete(LED_EVENT_PAIR_FAIL);
}
return 1;
}
/**
* @brief OTA 200ms 闪(无限):最高优先级
* @param led_config 配置
* @return 1
*/
static uint8_t led_ota_handler(LED_Config_t *led_config)
{
(void)LED_Update(led_config);
return 1;
}
/**
* @brief 注册回调并装载 CBR3 事件表(GPIO 已在 bsp_hw_init
* @return 无
*/
void user_led_init(void)
{
static LED_EventTableItem_t led_event_category[LED_EVENT_MAX] = {
[LED_EVENT_STANDBY] = {
/* 正常开机常亮 */
.event = LED_EVENT_STANDBY,
.priority = PRIORITY_1,
.config.ledMask = LED_CHANNEL(LED_CH_ON),
.config.state = LED_LIGHT,
#if BOARD_LED_STANDBY_ON
.config.runNum = 0, /* 0=一直亮 */
#else
.config.runNum = 1,
#endif
.ledEventHandler = led_standby_handler,
},
[LED_EVENT_PAIRING] = {
/* 配对中 100ms 快闪(无限) */
.event = LED_EVENT_PAIRING,
.priority = PRIORITY_2,
.config.ledMask = LED_CHANNEL(LED_CH_ON),
.config.state = LED_BLINKING,
.config.blinkParams.orDer = 0,
.config.blinkParams.onTime = BOARD_LED_FAST_ON_MS,
.config.blinkParams.offTime = BOARD_LED_FAST_OFF_MS,
.config.runNum = 0,
.ledEventHandler = led_pairing_handler,
},
[LED_EVENT_PAIR_OK] = {
/* 配对成功常亮若干秒(允许直接回到常亮) */
.event = LED_EVENT_PAIR_OK,
.priority = PRIORITY_2,
.config.ledMask = LED_CHANNEL(LED_CH_ON),
.config.state = LED_LIGHT,
.config.runNum = (uint16_t)((BOARD_LED_PAIR_OK_MS + 999u) / 1000u),
.ledEventHandler = led_pair_ok_handler,
},
[LED_EVENT_PAIR_FAIL] = {
/* 配对失败 200ms 闪提示若干秒 */
.event = LED_EVENT_PAIR_FAIL,
.priority = PRIORITY_2,
.config.ledMask = LED_CHANNEL(LED_CH_ON),
.config.state = LED_BLINKING,
.config.blinkParams.orDer = 0,
.config.blinkParams.onTime = BOARD_LED_OTA_ON_MS,
.config.blinkParams.offTime = BOARD_LED_OTA_OFF_MS,
.config.runNum = (uint16_t)(BOARD_LED_PAIR_FAIL_MS
/ (BOARD_LED_OTA_ON_MS + BOARD_LED_OTA_OFF_MS)),
.ledEventHandler = led_pair_fail_handler,
},
[LED_EVENT_OTA] = {
/* OTA:200ms 闪一直闪,直到升级结束或复位 */
.event = LED_EVENT_OTA,
.priority = PRIORITY_3,
.config.ledMask = LED_CHANNEL(LED_CH_ON),
.config.state = LED_BLINKING,
.config.blinkParams.orDer = 0,
.config.blinkParams.onTime = BOARD_LED_OTA_ON_MS,
.config.blinkParams.offTime = BOARD_LED_OTA_OFF_MS,
.config.runNum = 0, /* 0=无限 */
.ledEventHandler = led_ota_handler,
},
};
LED_SetStateInit(set_led_multi);
SET_TimeGetInit((TIME_Get_t)timer_get_ms);
LED_EventTableInit(led_event_category);
bsp_led_set(0);
}
/**
* @brief 跑一轮 LED 事件
* @return 无
*/
void user_led_handle(void)
{
LED_EventHandle();
}
+33
View File
@@ -0,0 +1,33 @@
/******************************************************************************
* @file led_manager.h
* @brief CBR3 单 LEDPC2)通道与事件表
* @author cyWu <1917507415@qq.com>
* @date 2026.08.29
* @version V1.0.0
* @history
* - V1.0.0, 2026.08.29, cyWu, 首次发布(单灯版,删红绿双通道)
******************************************************************************/
#ifndef __LED_MANAGER_H__
#define __LED_MANAGER_H__
#include "led.h"
typedef enum {
LED_CH_ON = 0, /**< 单灯唯一通道 */
LED_CH_MAX
} LedChannel_t;
/**
* @brief 注册 GPIO 回调并装载 CBR3 事件表
* @return 无
*/
void user_led_init(void);
/**
* @brief 跑一轮 LED 事件
* @return 无
*/
void user_led_handle(void);
#endif /* __LED_MANAGER_H__ */