/****************************************************************************** * @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 #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 }