/****************************************************************************** * @file app_nvm.c * @brief CBR3 VM 持久化实现:mode/fan_gear/联动/定时/运行记录/使能 * @author cyWu <1917507415@qq.com> * @date 2026.09.02 * @version V2.0.0(规格书 V3.0.0):删除 CFG_USER_CBR3_COND(49B),改为 * CFG_USER_CBR3_LINKAGE(20B) + CFG_USER_CBR3_RUNREC(128B); * **CBR3_VM_VER 1→2**,旧条件数据不读不迁移(当新设备) * * @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_LINKAGE =29 联动配置 linkage_config(20B) * CFG_USER_CBR3_TIME =30 定时配置 time_config[7][8](56B) * CFG_USER_CBR3_RUNREC =31 运行记录 31×uint32 + slot0 日期(128B) * 拆 4 个 item:改档/改模式只擦写 10B 小 item,不重写配置区, * 减少 flash 磨损;首次上电(magic/ver 不符)保持 gDevData 全 0 默认。 * * 写入时机(规格书第 13 章):APP 改联动/定时、单次定时自毁、 * 档位/模式稳定变化(500ms 防抖);运行记录满 10 分钟 + 跨天立刻写。 * * ⚠⚠ 上下文硬约束(2026-08-31 复位事故定论,勿再踩): * usr_timer_add() 是**异步接口**,回调在**硬件定时器中断上下文**执行 * (栈极小);sys_timer_add() 是同步接口,回调在 add 它的线程执行。 * 因此: * - app_nvm_tick() 跑在 20ms 中断上下文 → **只能做变化检测与计时, * 绝对不能调 syscfg_write**(VM 擦写 flash 栈用量大且中断里做 * flash 操作 → 栈溢出软复位;实测 APP 下发数据后 500ms 必崩)。 * - 真正写入由 app_nvm_flush() / app_nvm_save_runrec() 在 * usr_timer_loop()(BLE 线程、任务上下文、栈充足)执行。 * 另:nvm_write_item 的比较缓冲必须是 static(放栈上会撑爆中断栈)。 ******************************************************************************/ #include #include "app_nvm.h" #include "app_mode.h" #include "app_fan.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_CBR3_STATE 28 #define CFG_USER_CBR3_LINKAGE 29 #define CFG_USER_CBR3_TIME 30 #define CFG_USER_CBR3_RUNREC 31 #define CBR3_VM_MAGIC 0xCB03 #define CBR3_VM_VER 2 /* V3.0.0 起:条件→联动+运行记录,旧数据不迁移 */ #define CBR3_VM_DEBOUNCE_MS 500 /* 档位/模式稳定变化防抖(规格书 13 章) */ /** 状态 item(10 字节) */ 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 time_en; /**< DP13 定时总开关 */ uint8_t fault_en; /**< DP23 */ uint8_t alarm_en; /**< DP25 */ uint8_t rsv[2]; /**< 对齐到 10B(原 cond_en 位置作废) */ } __packed Cbr3StateVm_t; /** 联动配置 item(20 字节,DP6 原样落盘) */ typedef struct { uint8_t linkage_config[BOARD_LINKAGE_CFG_LEN]; } __packed Cbr3LinkageVm_t; /** 定时配置 item(56 字节) */ typedef struct { uint8_t time_config[7][8]; } __packed Cbr3TimeVm_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 Cbr3RunrecVm_t; #if BOARD_NVM_ENABLE /** NVM 运行时:防抖落盘 + 已落盘档位/模式快照(变化检测基准) */ typedef struct { uint8_t dirty; /**< 1=防抖计时中 */ uint16_t deb_ms; /**< 防抖累计毫秒 */ uint8_t last_gear; /**< 上次已同步到 VM 的档位 */ uint8_t last_mode; /**< 上次已同步到 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(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 数据长度(≤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(mode/gear/使能) * @return 无 */ static void nvm_write_state(void) { Cbr3StateVm_t st; memset(&st, 0, sizeof(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.time_en = gDevData.time_en ? 1 : 0; st.fault_en = gDevData.fault_en; st.alarm_en = gDevData.alarm_en; nvm_write_item(CFG_USER_CBR3_STATE, &st, sizeof(st)); } /** * @brief 写联动配置 item(20B 原样落盘) * @return 无 */ static void nvm_write_linkage(void) { Cbr3LinkageVm_t lf; memcpy(lf.linkage_config, gDevData.linkage_config, BOARD_LINKAGE_CFG_LEN); nvm_write_item(CFG_USER_CBR3_LINKAGE, &lf, sizeof(lf)); } /** * @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 写运行记录 item(31 格秒数 + slot0 日期) * @return 无 */ static void nvm_write_runrec(void) { Cbr3RunrecVm_t rf; app_runrec_pack(rf.run_record); rf.slot0_date = app_runrec_get_slot0_date(); nvm_write_item(CFG_USER_CBR3_RUNREC, &rf, sizeof(rf)); } /** * @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 v%d (mode=%d gear=%d time_en=%d)\n", st.ver, st.mode, st.fan_gear, st.time_en); } else { /* ver 不符 = 旧条件固件留下的 VM(规格书 13 章:不迁移,当新设备) */ 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 同步"已落盘状态"快照为当前值(save/load 后调用) * @return 无 */ static void nvm_sync_snapshot(void) { #if BOARD_NVM_ENABLE s_nvm.last_gear = app_fan_get_gear(); s_nvm.last_mode = app_mode_get_active(); s_nvm.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_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 顺序:先恢复档位再恢复模式(app_mode_set_manual 内部会快照当前档、 * 并让通风/强力从周期头开跑,规格书第 9 章"开机恢复不存相位")。 * VM 无效(magic/ver 不符,含旧条件固件)时保持 gDevData 全 0 默认。 * @return 无 */ void app_nvm_load(void) { #if !BOARD_NVM_ENABLE return; #else Cbr3StateVm_t st; Cbr3LinkageVm_t lf; Cbr3TimeVm_t tf; Cbr3RunrecVm_t rf; 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(20B 原样;引擎由 BOARD_LINKAGE_ENABLE 决定是否跑) */ if (syscfg_read(CFG_USER_CBR3_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_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); } } /* 运行记录回填 RAM(13.1:掉电再上电先把 31 格灌回来) */ if (syscfg_read(CFG_USER_CBR3_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; /* 先档位后模式;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 v%d: mode=%d gear=%d time_en=%d\n", st.ver, st.mode, st.fan_gear, st.time_en); #endif } /** * @brief 业务节拍:做档位/模式/总开关变化检测 + 防抖计时,**不执行写入** * @param dt 距上次调用的毫秒数 * @note ⚠ 本函数跑在 usr_timer_add 的 20ms 回调里 = **硬件定时器中断上下文** * (异步接口,栈极小)。这里只允许做轻量比较与计时;真正的 * syscfg_write 由 app_nvm_flush() 在 usr_timer_loop(BLE 线程、 * 任务上下文)执行。曾在此直接调 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 time_en = gDevData.time_en ? 1 : 0; if ((gear != s_nvm.last_gear) || (mode != s_nvm.last_mode) || (time_en != s_nvm.last_time_en)) { s_nvm.last_gear = gear; s_nvm.last_mode = mode; 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 >= CBR3_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 标记业务字段有变化,防抖计时归零(重新计满 CBR3_VM_DEBOUNCE_MS 才写) * @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 Cbr3StateVm_t st; Cbr3LinkageVm_t lf; Cbr3TimeVm_t tf; Cbr3RunrecVm_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_CBR3_STATE, &st, sizeof(st)); syscfg_write(CFG_USER_CBR3_LINKAGE, &lf, sizeof(lf)); syscfg_write(CFG_USER_CBR3_TIME, &tf, sizeof(tf)); syscfg_write(CFG_USER_CBR3_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 }