Files
wuchuyuan e65b2b22b6 1、修复CBR0寿命与31天记录落盘失败(比较缓冲128→256)
2、APP换灯直接写DP3,DP4本版忽略;上电用restore灌RAM;读/全量打包权威剩余秒数
2026-09-08 12:29:02 +08:00

432 lines
14 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/******************************************************************************
* @file app_nvm.c
* @brief CBR0 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.08
* @version V1.2.0
* @history
* - V1.2.0, 2026.09.08, cyWu, runrec item 含寿命 132B,比较缓冲改为 256
* - V1.1.0, 2026.09.04, cyWu, P1 完整实现(4 item 存取 + 加热请求恢复)
* - V1.0.0, 2026.09.04, cyWu, P0 空骨架
*
* @note VM item 从 28 起:STATE=2810BLINKAGE=2920B
* TIME=3077BRUNREC=31124B 秒数 + 4B 日期 + 4B 寿命 = 132B)。
* 比较缓冲必须 ≥ 最大 item;加热垫是 128B,灯罩寿命多 4B,必须 256。
* 首次上电 magic 无效:开关/定时/记录为 0,剩余寿命由
* app_light_life_init 置额定 2,592,000(不是全 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_light_life.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_CBR0_STATE 28
#define CFG_USER_CBR0_LINKAGE 29
#define CFG_USER_CBR0_TIME 30
#define CFG_USER_CBR0_RUNREC 31
#define CBR0_VM_MAGIC 0xCB00
#define CBR0_VM_VER 1
#define CBR0_VM_DEBOUNCE_MS 500 /* 灯泡/使能稳定变化防抖(规格书 13 章) */
#define CBR0_VM_CMP_BUF_LEN 256 /* ≥ RUNREC 132B;加热垫 128 会把本 item 静默丢掉 */
/** 状态 item10 字节) */
typedef struct {
uint16_t magic; /**< CBR0_VM_MAGIC */
uint8_t ver; /**< CBR0_VM_VER */
uint8_t heat_switch;/**< 上次灯泡请求(DP0;上电按此恢复) */
uint8_t time_en; /**< DP12 定时总开关 */
uint8_t fault_en; /**< DP22 */
uint8_t alarm_en; /**< DP24 */
uint8_t rsv[2]; /**< 对齐到 10B */
} __packed Cbr0StateVm_t;
/** 联动配置 item20 字节,DP11 原样落盘) */
typedef struct {
uint8_t linkage_config[BOARD_LINKAGE_CFG_LEN];
} __packed Cbr0LinkageVm_t;
/** 定时配置 item(77 字节:7 组 × 11B,规格书 11.1 */
typedef struct {
uint8_t time_config[7][BOARD_TIME_CFG_LEN];
} __packed Cbr0TimeVm_t;
/** 运行记录 item124B 秒数 + 4B slot0 日期 + 4B 剩余寿命) */
typedef struct {
uint8_t run_record[APP_RUNREC_PACK_LEN]; /**< 31 × uint32 大端 */
uint32_t slot0_date; /**< slot[0] 对应本地日期 yyyymmdd */
uint32_t lighe_remain_time; /**< 灯泡剩余寿命(随 runrec 同写节奏,13.2 */
} __packed Cbr0RunrecVm_t;
typedef char cbr0_runrec_vm_fits[(sizeof(Cbr0RunrecVm_t) <= CBR0_VM_CMP_BUF_LEN) ? 1 : -1];
#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(Cbr0TimeVm_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], BOARD_TIME_CFG_LEN);
}
}
/**
* @brief 写单个 item:先读回比较,内容相同跳过,避免无谓擦写 flash
* @param item_id VM item id
* @param buf 新数据
* @param len 数据长度(≤CBR0_VM_CMP_BUF_LEN
* @return 无
*/
static void nvm_write_item(u16 item_id, void *buf, u16 len)
{
/* static:比较缓冲不放栈上;只在任务上下文调用,无重入 */
static uint8_t old[CBR0_VM_CMP_BUF_LEN];
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 写状态 itemheat_switch/time_en/使能)
* @return 无
*/
static void nvm_write_state(void)
{
Cbr0StateVm_t st;
memset(&st, 0, sizeof(st));
st.magic = CBR0_VM_MAGIC;
st.ver = CBR0_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_CBR0_STATE, &st, sizeof(st));
}
/**
* @brief 写联动配置 item20B 原样落盘;P1 恒 0P2 由 DP11 写入)
* @return 无
*/
static void nvm_write_linkage(void)
{
Cbr0LinkageVm_t lf;
memcpy(lf.linkage_config, gDevData.linkage_config, BOARD_LINKAGE_CFG_LEN);
nvm_write_item(CFG_USER_CBR0_LINKAGE, &lf, sizeof(lf));
}
/**
* @brief 写定时配置 item
* @return 无
*/
static void nvm_write_time(void)
{
Cbr0TimeVm_t tf;
nvm_collect_time(&tf);
nvm_write_item(CFG_USER_CBR0_TIME, &tf, sizeof(tf));
}
/**
* @brief 写运行记录 item31 格秒数 + slot0 日期 + 灯泡剩余寿命)
* @note 寿命字段随 runrec 同写节奏(600s / 边沿立刻),规格书 13.2
* @return 无
*/
static void nvm_write_runrec(void)
{
Cbr0RunrecVm_t rf;
app_runrec_pack(rf.run_record);
rf.slot0_date = app_runrec_get_slot0_date();
rf.lighe_remain_time = app_light_life_get_remain();
nvm_write_item(CFG_USER_CBR0_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
Cbr0StateVm_t st;
int ret = syscfg_read(CFG_USER_CBR0_STATE, &st, sizeof(st));
if ((ret == (int)sizeof(st)) && (st.magic == CBR0_VM_MAGIC) && (st.ver == CBR0_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,
* 剩余寿命保持 init 的额定值。
* @return 无
*/
void app_nvm_load(void)
{
#if !BOARD_NVM_ENABLE
return;
#else
Cbr0StateVm_t st;
Cbr0LinkageVm_t lf;
Cbr0TimeVm_t tf;
Cbr0RunrecVm_t rf;
uint8_t i;
if ((syscfg_read(CFG_USER_CBR0_STATE, &st, sizeof(st)) != (int)sizeof(st))
|| (st.magic != CBR0_VM_MAGIC) || (st.ver != CBR0_VM_VER)) {
log_info("nvm load: no valid vm, defaults (heat=0)\n");
return;
}
/* 联动配置回填 gDevData(引擎由 BOARD_LINKAGE_ENABLE 决定是否跑) */
if (syscfg_read(CFG_USER_CBR0_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_CBR0_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], BOARD_TIME_CFG_LEN);
}
}
/* 运行记录回填 RAM(13.1:掉电再上电先把 31 格灌回来) */
if (syscfg_read(CFG_USER_CBR0_RUNREC, &rf, sizeof(rf)) == (int)sizeof(rf)) {
app_runrec_unpack(rf.run_record);
app_runrec_set_slot0_date(rf.slot0_date);
/* 只灌 RAM,禁止走 set_remain(那会立刻再写 flash */
app_light_life_restore(rf.lighe_remain_time);
}
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(任务上下文)执行。
* 禁止在此调 syscfg_write(中断栈不够)。
* @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 >= CBR0_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_INFOitem 2),不受影响
* @return 无
*/
void app_nvm_reset(void)
{
#if !BOARD_NVM_ENABLE
return;
#else
Cbr0StateVm_t st;
Cbr0LinkageVm_t lf;
Cbr0TimeVm_t tf;
Cbr0RunrecVm_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_CBR0_STATE, &st, sizeof(st));
syscfg_write(CFG_USER_CBR0_LINKAGE, &lf, sizeof(lf));
syscfg_write(CFG_USER_CBR0_TIME, &tf, sizeof(tf));
syscfg_write(CFG_USER_CBR0_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
}