426 lines
13 KiB
C
426 lines
13 KiB
C
/******************************************************************************
|
||
* @file app_nvm.c
|
||
* @brief CBR1 VM 持久化:heat_switch / linkage 20B / time_en /
|
||
* time_config1~7 / run_record 31天 / fault_en / alarm_en
|
||
* @author cyWu <1917507415@qq.com>
|
||
* @date 2026.09.04
|
||
* @version V1.1.0
|
||
* @history
|
||
* - V1.1.0, 2026.09.04, cyWu, P1 完整实现(4 item 存取 + 加热请求恢复)
|
||
* - V1.0.0, 2026.09.04, cyWu, P0 空骨架
|
||
*
|
||
* @note VM item 分配在 syscfg 用户自定义区 [1~49](syscfg_id.h 已占用
|
||
* CFG_USER_PAIR_INFO=2 / TIM_FLAG=3 / VM_ALARM*=4~24 /
|
||
* DEVICE_STA=25 / PLAN_INFO1=26 / BATTERY_LEVEL=27),
|
||
* 本产品从 28 开始:
|
||
* CFG_USER_CBR1_STATE =28 状态(magic+ver+heat_switch+en,10B)
|
||
* CFG_USER_CBR1_LINKAGE =29 联动配置 linkage_config(20B,P2 写)
|
||
* CFG_USER_CBR1_TIME =30 定时配置 time_config[7][8](56B)
|
||
* CFG_USER_CBR1_RUNREC =31 运行记录 31×uint32 + slot0 日期(128B)
|
||
* 拆 4 个 item:改加热只擦写 10B 小 item,不重写配置区,减少 flash
|
||
* 磨损;首次上电(magic/ver 不符)保持 gDevData 全 0 默认。
|
||
*
|
||
* 落盘基准 = 加热**请求**(app_relay_get_request,规格书 13 章"上次
|
||
* 加热请求"),不是实际吸合态;上电按它恢复请求、等过零再吸合。
|
||
*
|
||
* ⚠ 上下文硬约束:usr_timer_add 回调在硬件定时器中断上下文
|
||
* (栈极小),app_nvm_tick 只能做变化检测与防抖计时,**绝对禁止
|
||
* 调 syscfg_write**(VM 擦写 flash 栈用量大 → 栈溢出软复位)。
|
||
* 真正写入由 app_nvm_flush() 在 usr_timer_loop(任务上下文)执行。
|
||
* nvm_write_item 的比较缓冲必须 static(放栈上会撑爆中断栈)。
|
||
******************************************************************************/
|
||
|
||
#include <string.h>
|
||
|
||
#include "app_nvm.h"
|
||
#include "app_relay.h"
|
||
#include "app_defer.h"
|
||
#include "app_runrec.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_CBR1_STATE 28
|
||
#define CFG_USER_CBR1_LINKAGE 29
|
||
#define CFG_USER_CBR1_TIME 30
|
||
#define CFG_USER_CBR1_RUNREC 31
|
||
|
||
#define CBR1_VM_MAGIC 0xCB01
|
||
#define CBR1_VM_VER 1
|
||
#define CBR1_VM_DEBOUNCE_MS 500 /* 加热/使能稳定变化防抖(规格书 13 章) */
|
||
|
||
/** 状态 item(10 字节) */
|
||
typedef struct {
|
||
uint16_t magic; /**< CBR1_VM_MAGIC */
|
||
uint8_t ver; /**< CBR1_VM_VER */
|
||
uint8_t heat_switch;/**< 上次加热请求(DP0;上电按此恢复) */
|
||
uint8_t time_en; /**< DP9 定时总开关 */
|
||
uint8_t fault_en; /**< DP19 */
|
||
uint8_t alarm_en; /**< DP21 */
|
||
uint8_t rsv[2]; /**< 对齐到 10B */
|
||
} __packed Cbr1StateVm_t;
|
||
|
||
/** 联动配置 item(20 字节,DP8 原样落盘) */
|
||
typedef struct {
|
||
uint8_t linkage_config[BOARD_LINKAGE_CFG_LEN];
|
||
} __packed Cbr1LinkageVm_t;
|
||
|
||
/** 定时配置 item(56 字节) */
|
||
typedef struct {
|
||
uint8_t time_config[7][8];
|
||
} __packed Cbr1TimeVm_t;
|
||
|
||
/** 运行记录 item(124B 秒数 + 4B slot0 日期) */
|
||
typedef struct {
|
||
uint8_t run_record[APP_RUNREC_PACK_LEN]; /**< 31 × uint32 大端 */
|
||
uint32_t slot0_date; /**< slot[0] 对应本地日期 yyyymmdd */
|
||
} __packed Cbr1RunrecVm_t;
|
||
|
||
#if BOARD_NVM_ENABLE
|
||
/** NVM 运行时:防抖落盘 + 已落盘状态快照(变化检测基准) */
|
||
typedef struct {
|
||
uint8_t dirty; /**< 1=防抖计时中 */
|
||
uint16_t deb_ms; /**< 防抖累计毫秒 */
|
||
uint8_t last_req; /**< 上次已同步到 VM 的加热请求 */
|
||
uint8_t last_time_en; /**< 上次已同步到 VM 的定时总开关 */
|
||
} AppNvmCtx_t;
|
||
|
||
static AppNvmCtx_t s_nvm;
|
||
#endif
|
||
|
||
/**
|
||
* @brief 从 gDevData 收集定时配置到 VM 结构
|
||
* @param out 输出结构
|
||
* @return 无
|
||
*/
|
||
static void nvm_collect_time(Cbr1TimeVm_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 数据长度(≤128,比较缓冲大小)
|
||
* @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 写状态 item(heat_switch/time_en/使能)
|
||
* @return 无
|
||
*/
|
||
static void nvm_write_state(void)
|
||
{
|
||
Cbr1StateVm_t st;
|
||
|
||
memset(&st, 0, sizeof(st));
|
||
st.magic = CBR1_VM_MAGIC;
|
||
st.ver = CBR1_VM_VER;
|
||
st.heat_switch = app_relay_get_request();
|
||
st.time_en = gDevData.time_en ? 1 : 0;
|
||
st.fault_en = gDevData.fault_en;
|
||
st.alarm_en = gDevData.alarm_en;
|
||
nvm_write_item(CFG_USER_CBR1_STATE, &st, sizeof(st));
|
||
}
|
||
|
||
/**
|
||
* @brief 写联动配置 item(20B 原样落盘;P1 恒 0,P2 由 DP8 写入)
|
||
* @return 无
|
||
*/
|
||
static void nvm_write_linkage(void)
|
||
{
|
||
Cbr1LinkageVm_t lf;
|
||
|
||
memcpy(lf.linkage_config, gDevData.linkage_config, BOARD_LINKAGE_CFG_LEN);
|
||
nvm_write_item(CFG_USER_CBR1_LINKAGE, &lf, sizeof(lf));
|
||
}
|
||
|
||
/**
|
||
* @brief 写定时配置 item
|
||
* @return 无
|
||
*/
|
||
static void nvm_write_time(void)
|
||
{
|
||
Cbr1TimeVm_t tf;
|
||
|
||
nvm_collect_time(&tf);
|
||
nvm_write_item(CFG_USER_CBR1_TIME, &tf, sizeof(tf));
|
||
}
|
||
|
||
/**
|
||
* @brief 写运行记录 item(31 格秒数 + slot0 日期)
|
||
* @return 无
|
||
*/
|
||
static void nvm_write_runrec(void)
|
||
{
|
||
Cbr1RunrecVm_t rf;
|
||
|
||
app_runrec_pack(rf.run_record);
|
||
rf.slot0_date = app_runrec_get_slot0_date();
|
||
nvm_write_item(CFG_USER_CBR1_RUNREC, &rf, sizeof(rf));
|
||
}
|
||
|
||
/**
|
||
* @brief 同步"已落盘状态"快照为当前值(save/load 后调用)
|
||
* @return 无
|
||
*/
|
||
static void nvm_sync_snapshot(void)
|
||
{
|
||
#if BOARD_NVM_ENABLE
|
||
s_nvm.last_req = app_relay_get_request();
|
||
s_nvm.last_time_en = gDevData.time_en ? 1 : 0;
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief 初始化:校验 VM 有效性并打日志(数据加载在 app_nvm_load)
|
||
* @return 无
|
||
*/
|
||
void app_nvm_init(void)
|
||
{
|
||
#if !BOARD_NVM_ENABLE
|
||
log_info("nvm disabled\n");
|
||
return;
|
||
#else
|
||
Cbr1StateVm_t st;
|
||
int ret = syscfg_read(CFG_USER_CBR1_STATE, &st, sizeof(st));
|
||
|
||
if ((ret == (int)sizeof(st)) && (st.magic == CBR1_VM_MAGIC) && (st.ver == CBR1_VM_VER)) {
|
||
log_info("nvm init, vm valid v%d (heat=%d time_en=%d)\n",
|
||
st.ver, st.heat_switch, st.time_en);
|
||
} else {
|
||
log_info("nvm init, vm invalid (ret=%d), use defaults\n", ret);
|
||
}
|
||
memset(&s_nvm, 0, sizeof(s_nvm));
|
||
app_defer_cancel(APP_DEFER_NVM_SAVE);
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief 立即保存全部业务字段
|
||
* @return 无
|
||
*/
|
||
void app_nvm_save(void)
|
||
{
|
||
#if !BOARD_NVM_ENABLE
|
||
return;
|
||
#else
|
||
nvm_write_state();
|
||
nvm_write_linkage();
|
||
nvm_write_time();
|
||
nvm_write_runrec();
|
||
s_nvm.dirty = 0;
|
||
s_nvm.deb_ms = 0;
|
||
app_defer_cancel(APP_DEFER_NVM_SAVE);
|
||
nvm_sync_snapshot();
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief 立即保存运行记录项(跨天旋转用,任务上下文)
|
||
* @return 无
|
||
*/
|
||
void app_nvm_save_runrec(void)
|
||
{
|
||
#if !BOARD_NVM_ENABLE
|
||
return;
|
||
#else
|
||
nvm_write_runrec();
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief 从 VM 加载并应用(开机由 usr_jb_init 调用)
|
||
* @note 加热恢复 = 按上次**请求**恢复(等过零吸合,规格书 13 章 / 5.1);
|
||
* 联动/定时/运行记录回填 gDevData。VM 无效(首次上电)保持全 0 默认。
|
||
* @return 无
|
||
*/
|
||
void app_nvm_load(void)
|
||
{
|
||
#if !BOARD_NVM_ENABLE
|
||
return;
|
||
#else
|
||
Cbr1StateVm_t st;
|
||
Cbr1LinkageVm_t lf;
|
||
Cbr1TimeVm_t tf;
|
||
Cbr1RunrecVm_t rf;
|
||
uint8_t i;
|
||
|
||
if ((syscfg_read(CFG_USER_CBR1_STATE, &st, sizeof(st)) != (int)sizeof(st))
|
||
|| (st.magic != CBR1_VM_MAGIC) || (st.ver != CBR1_VM_VER)) {
|
||
log_info("nvm load: no valid vm, defaults (heat=0)\n");
|
||
return;
|
||
}
|
||
|
||
/* 联动配置回填 gDevData(引擎由 BOARD_LINKAGE_ENABLE 决定是否跑) */
|
||
if (syscfg_read(CFG_USER_CBR1_LINKAGE, &lf, sizeof(lf)) == (int)sizeof(lf)) {
|
||
memcpy(gDevData.linkage_config, lf.linkage_config, BOARD_LINKAGE_CFG_LEN);
|
||
}
|
||
|
||
/* 定时配置回填 gDevData(只存不跑时也要回填,P1 验收) */
|
||
if (syscfg_read(CFG_USER_CBR1_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);
|
||
}
|
||
}
|
||
|
||
/* 运行记录回填 RAM(13.1:掉电再上电先把 31 格灌回来) */
|
||
if (syscfg_read(CFG_USER_CBR1_RUNREC, &rf, sizeof(rf)) == (int)sizeof(rf)) {
|
||
app_runrec_unpack(rf.run_record);
|
||
app_runrec_set_slot0_date(rf.slot0_date);
|
||
}
|
||
|
||
gDevData.time_en = st.time_en ? 1 : 0;
|
||
gDevData.fault_en = st.fault_en;
|
||
gDevData.alarm_en = st.alarm_en;
|
||
|
||
/* 上电按上次加热请求恢复(开则等过零再吸合;无过零由 app_relay 超时兜底) */
|
||
app_relay_request(st.heat_switch ? 1 : 0);
|
||
nvm_sync_snapshot(); /* 恢复值即为已落盘值,同步基准 */
|
||
log_info("nvm load v%d: heat=%d time_en=%d\n",
|
||
st.ver, st.heat_switch, st.time_en);
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief 业务节拍:做加热请求/总开关变化检测 + 防抖计时,**不执行写入**
|
||
* @param dt 距上次调用的毫秒数
|
||
* @note ⚠ 本函数跑在 usr_timer_add 的 20ms 回调里 = **硬件定时器中断上下文**
|
||
* (异步接口,栈极小)。这里只允许做轻量比较与计时;真正的
|
||
* syscfg_write 由 app_nvm_flush() 在 usr_timer_loop(任务上下文)执行。
|
||
* 曾在此直接调 app_nvm_save 导致栈溢出软复位。
|
||
* @return 无
|
||
*/
|
||
void app_nvm_tick(uint16_t dt)
|
||
{
|
||
#if !BOARD_NVM_ENABLE
|
||
(void)dt;
|
||
return;
|
||
#else
|
||
uint8_t req = app_relay_get_request();
|
||
uint8_t time_en = gDevData.time_en ? 1 : 0;
|
||
|
||
if ((req != s_nvm.last_req) || (time_en != s_nvm.last_time_en)) {
|
||
s_nvm.last_req = req;
|
||
s_nvm.last_time_en = time_en;
|
||
s_nvm.dirty = 1;
|
||
s_nvm.deb_ms = 0; /* 变化重新计满防抖窗口 */
|
||
}
|
||
|
||
if (!s_nvm.dirty) {
|
||
return;
|
||
}
|
||
s_nvm.deb_ms = (uint16_t)(s_nvm.deb_ms + dt);
|
||
if (s_nvm.deb_ms >= CBR1_VM_DEBOUNCE_MS) {
|
||
s_nvm.deb_ms = 0;
|
||
s_nvm.dirty = 0;
|
||
app_defer_request(APP_DEFER_NVM_SAVE); /* 只置标志:真正落盘在 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 (!app_defer_take(APP_DEFER_NVM_SAVE)) {
|
||
return;
|
||
}
|
||
app_nvm_save();
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief 标记业务字段有变化,防抖计时归零(重新计满防抖窗口才写)
|
||
* @note 任务上下文调用(APP 下行改配置:定时/使能/故障告警使能)
|
||
* @return 无
|
||
*/
|
||
void app_nvm_mark_dirty(void)
|
||
{
|
||
#if BOARD_NVM_ENABLE
|
||
s_nvm.dirty = 1;
|
||
s_nvm.deb_ms = 0;
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief 出厂重置:清全部业务 VM(保留 BLE 配对,规格书第 14 章)
|
||
* @note 4 个 item 写全 0 → magic 失效,下次开机走首次上电默认值;
|
||
* BLE 配对三元组在 CFG_USER_PAIR_INFO(item 2),不受影响
|
||
* @return 无
|
||
*/
|
||
void app_nvm_reset(void)
|
||
{
|
||
#if !BOARD_NVM_ENABLE
|
||
return;
|
||
#else
|
||
Cbr1StateVm_t st;
|
||
Cbr1LinkageVm_t lf;
|
||
Cbr1TimeVm_t tf;
|
||
Cbr1RunrecVm_t rf;
|
||
|
||
memset(&st, 0, sizeof(st));
|
||
memset(&lf, 0, sizeof(lf));
|
||
memset(&tf, 0, sizeof(tf));
|
||
memset(&rf, 0, sizeof(rf));
|
||
syscfg_write(CFG_USER_CBR1_STATE, &st, sizeof(st));
|
||
syscfg_write(CFG_USER_CBR1_LINKAGE, &lf, sizeof(lf));
|
||
syscfg_write(CFG_USER_CBR1_TIME, &tf, sizeof(tf));
|
||
syscfg_write(CFG_USER_CBR1_RUNREC, &rf, sizeof(rf));
|
||
s_nvm.dirty = 0;
|
||
s_nvm.deb_ms = 0;
|
||
app_defer_cancel(APP_DEFER_NVM_SAVE);
|
||
log_info("nvm factory reset, business vm cleared (pairing kept)\n");
|
||
#endif
|
||
}
|