/****************************************************************************** * @file app_nvm.c * @brief CBR2 VM 持久化:spray_config 8B / linkage 20B / plan_en / 7 条计划(各15B) / * fault_en / alarm_en / 运行记录(规格书第 14 章) * @author cyWu <1917507415@qq.com> * @date 2026.09.09 * @version V1.0.0, 首次发布 * * @note VM item 分配在 syscfg 用户自定义区 [1~49],本产品从 28 开始 * (每台设备 flash 独立,与其它产品 id 无冲突): * CFG_USER_CBR2_CFG =28 状态(magic+ver+spray_config+使能,16B) * CFG_USER_CBR2_PLAN =29 计划 spray_plan1 + time_config2~7(105B) * CFG_USER_CBR2_RUNREC =30 31 天运行记录 * CFG_USER_CBR2_LINKAGE =31 联动配置 20B * 拆多个 item:改喷淋参数只擦写 16B 小 item,不重写计划区,减少 * flash 磨损;首次上电(magic/ver 不符)保持 userInit 的默认值。 * * 打脏模型:本产品 P1 的全部写源都在 jbEventProcess(任务上下文), * 直接 app_nvm_mark_dirty() 即可,tick 不做变化检测、只跑防抖计时; * 真正写入由 app_nvm_flush() 在 usr_timer_loop(任务上下文)执行。 * * ⚠ 上下文硬约束:usr_timer_add 回调在硬件定时器中断上下文(栈极 * 小),绝对禁止调 syscfg_write(VM 擦写 flash 栈用量大 → 栈溢出 * 软复位)。nvm_write_item 的比较缓冲必须 static(放栈上会撑爆中断 * 栈)。 ******************************************************************************/ #include #include "app_nvm.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_CBR2_CFG 28 #define CFG_USER_CBR2_PLAN 29 #define CFG_USER_CBR2_RUNREC 30 #define CFG_USER_CBR2_LINKAGE 31 #define CBR2_VM_MAGIC 0xCB02 #define CBR2_VM_VER 1 #define CBR2_VM_DEBOUNCE_MS 500 /* 配置变化防抖(规格书 14 章) */ #define CBR2_PLAN_SLOTS 7 #define CBR2_PLAN_LEN 15 /** 状态 item(16 字节) */ typedef struct { uint16_t magic; /**< CBR2_VM_MAGIC */ uint8_t ver; /**< CBR2_VM_VER */ uint8_t spray_config[8];/**< DP7(大端:时长 u16 / 次数 u8 / 间隔 u16) */ uint8_t plan_en; /**< DP10 计划总开关 */ uint8_t fault_en; /**< DP20(原样存) */ uint8_t alarm_en; /**< DP22(原样存) */ uint8_t rsv[2]; /**< 对齐到 16B */ } __packed Cbr2CfgVm_t; /** 计划 item(105 字节:plan1 + time_config2~7) */ typedef struct { uint8_t plan[CBR2_PLAN_SLOTS][CBR2_PLAN_LEN]; } __packed Cbr2PlanVm_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 Cbr2RunrecVm_t; #if BOARD_NVM_ENABLE /** NVM 运行时:防抖计时(打脏由写源主动 mark) */ typedef struct { uint8_t dirty; /**< 1=防抖计时中 */ uint16_t deb_ms; /**< 防抖累计毫秒 */ } AppNvmCtx_t; static AppNvmCtx_t s_nvm; #endif /** * @brief 从 gDevData 收集 7 条计划到 VM 结构(plan1 + time_config2~7) * @param out 输出结构 * @return 无 */ static void nvm_collect_plan(Cbr2PlanVm_t *out) { const uint8_t *src[CBR2_PLAN_SLOTS] = { gDevData.spray_plan1, 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 < CBR2_PLAN_SLOTS; i++) { memcpy(out->plan[i], src[i], CBR2_PLAN_LEN); } } /** * @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:比较缓冲不放栈上;本函数只在任务上下文调用,无重入 */ 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(spray_config / plan_en / fault_en / alarm_en) * @return 无 */ static void nvm_write_cfg(void) { Cbr2CfgVm_t st; memset(&st, 0, sizeof(st)); st.magic = CBR2_VM_MAGIC; st.ver = CBR2_VM_VER; memcpy(st.spray_config, gDevData.spray_config, sizeof(st.spray_config)); st.plan_en = gDevData.spray_plan_en ? 1 : 0; st.fault_en = gDevData.fault_en; st.alarm_en = gDevData.alarm_en; nvm_write_item(CFG_USER_CBR2_CFG, &st, sizeof(st)); } /** * @brief 写计划 item(7 条 × 15B) * @return 无 */ static void nvm_write_plan(void) { Cbr2PlanVm_t pf; nvm_collect_plan(&pf); nvm_write_item(CFG_USER_CBR2_PLAN, &pf, sizeof(pf)); } /** * @brief 写运行记录 item(31 格秒数 + slot0 日期) * @return 无 */ static void nvm_write_runrec(void) { Cbr2RunrecVm_t rf; app_runrec_pack(rf.run_record); rf.slot0_date = app_runrec_get_slot0_date(); nvm_write_item(CFG_USER_CBR2_RUNREC, &rf, sizeof(rf)); } /** * @brief 写联动配置 20B * @return 无 */ static void nvm_write_linkage(void) { nvm_write_item(CFG_USER_CBR2_LINKAGE, gDevData.linkage_config, 20); } /** * @brief 初始化:校验 VM 有效性并打日志(数据加载在 app_nvm_load) * @return 无 */ void app_nvm_init(void) { #if !BOARD_NVM_ENABLE log_info("nvm disabled\n"); return; #else Cbr2CfgVm_t st; int ret = syscfg_read(CFG_USER_CBR2_CFG, &st, sizeof(st)); if ((ret == (int)sizeof(st)) && (st.magic == CBR2_VM_MAGIC) && (st.ver == CBR2_VM_VER)) { log_info("nvm init, vm valid v%d\n", st.ver); } 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_cfg(); nvm_write_plan(); nvm_write_linkage(); s_nvm.dirty = 0; s_nvm.deb_ms = 0; app_defer_cancel(APP_DEFER_NVM_SAVE); #endif } /** * @brief 从 VM 加载并回填 gDevData(开机由 usr_jb_init 调用) * @note 只回填配置:DP1/泵保持关,作业不恢复(上电绝不自动喷)。 * VM 无效(首次上电)保持 gDevData 现值 = userInit 默认。 * @return 无 */ void app_nvm_load(void) { #if !BOARD_NVM_ENABLE return; #else Cbr2CfgVm_t st; Cbr2PlanVm_t pf; Cbr2RunrecVm_t rf; uint8_t i; if ((syscfg_read(CFG_USER_CBR2_CFG, &st, sizeof(st)) != (int)sizeof(st)) || (st.magic != CBR2_VM_MAGIC) || (st.ver != CBR2_VM_VER)) { log_info("nvm load: no valid vm, keep defaults (dp7=10/3/3)\n"); return; } memcpy(gDevData.spray_config, st.spray_config, sizeof(st.spray_config)); gDevData.spray_plan_en = st.plan_en ? 1 : 0; gDevData.fault_en = st.fault_en; gDevData.alarm_en = st.alarm_en; if (syscfg_read(CFG_USER_CBR2_PLAN, &pf, sizeof(pf)) == (int)sizeof(pf)) { uint8_t *dst[CBR2_PLAN_SLOTS] = { gDevData.spray_plan1, gDevData.time_config2, gDevData.time_config3, gDevData.time_config4, gDevData.time_config5, gDevData.time_config6, gDevData.time_config7 }; for (i = 0; i < CBR2_PLAN_SLOTS; i++) { memcpy(dst[i], pf.plan[i], CBR2_PLAN_LEN); } } /* 运行记录回填 RAM(14.1:掉电再上电先把 31 格灌回来) */ if (syscfg_read(CFG_USER_CBR2_RUNREC, &rf, sizeof(rf)) == (int)sizeof(rf)) { app_runrec_unpack(rf.run_record); app_runrec_set_slot0_date(rf.slot0_date); } if (syscfg_read(CFG_USER_CBR2_LINKAGE, gDevData.linkage_config, 20) != 20) { memset(gDevData.linkage_config, 0, 20); } log_info("nvm load v%d: spray cfg %us x%u +%us, plan_en=%d\n", st.ver, (unsigned)(((uint16_t)st.spray_config[0] << 8) | st.spray_config[1]), (unsigned)st.spray_config[2], (unsigned)(((uint16_t)st.spray_config[3] << 8) | st.spray_config[4]), st.plan_en); #endif } /** * @brief 业务节拍:防抖计时,**不执行写入** * @param dt 距上次调用的毫秒数 * @note ⚠ 本函数跑在 usr_timer_add 的 20ms 回调里 = 硬件定时器中断上下文 * (栈极小)。只允许轻量计时;真正的 syscfg_write 由 app_nvm_flush() * 在 usr_timer_loop(任务上下文)执行。 * @return 无 */ void app_nvm_tick(uint16_t dt) { #if !BOARD_NVM_ENABLE (void)dt; return; #else if (!s_nvm.dirty) { return; } s_nvm.deb_ms = (uint16_t)(s_nvm.deb_ms + dt); if (s_nvm.deb_ms >= CBR2_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 必须且只能在非中断上下文调用(VM 擦写 flash 栈用量大) * @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 下行改配置:DP7/计划/plan_en/DP20/22) * @return 无 */ void app_nvm_mark_dirty(void) { #if BOARD_NVM_ENABLE s_nvm.dirty = 1; s_nvm.deb_ms = 0; #endif } /** * @brief 立即保存运行记录 item(满 600s / 跨天 / 首次对时,任务上下文) * @return 无 */ void app_nvm_save_runrec(void) { #if !BOARD_NVM_ENABLE return; #else nvm_write_runrec(); #endif } /** * @brief 出厂重置:清全部业务 VM(保留 BLE 配对,规格书第 15 章) * @note 3 个 item 写全 0 → magic 失效,下次开机走首次上电默认值; * BLE 配对三元组在 CFG_USER_PAIR_INFO(item 2),不受影响 * @return 无 */ void app_nvm_reset(void) { #if !BOARD_NVM_ENABLE return; #else Cbr2CfgVm_t st; Cbr2PlanVm_t pf; Cbr2RunrecVm_t rf; uint8_t linkage[20]; memset(&st, 0, sizeof(st)); memset(&pf, 0, sizeof(pf)); memset(&rf, 0, sizeof(rf)); memset(linkage, 0, sizeof(linkage)); syscfg_write(CFG_USER_CBR2_CFG, &st, sizeof(st)); syscfg_write(CFG_USER_CBR2_PLAN, &pf, sizeof(pf)); syscfg_write(CFG_USER_CBR2_RUNREC, &rf, sizeof(rf)); syscfg_write(CFG_USER_CBR2_LINKAGE, linkage, sizeof(linkage)); 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 }