/****************************************************************************** * @file app_runrec.c * @brief CBR1 31 天运行记录(规格书 13.1 节,DP17 RO,124B) * @author cyWu <1917507415@qq.com> * @date 2026.09.04 * @version V1.1.0 * @history * - V1.1.0, 2026.09.04, cyWu, P2 完整实现(实际吸合折秒 + 跨天旋转落盘) * - V1.0.0, 2026.09.04, cyWu, P0 骨架 * * @note 什么叫在运行:过零后继电器**实际已吸合**(app_relay_get_actual==1), * pending 开还没过零不算(13.1)。日期轴:未对时只往 slot[0] 加、 * 不旋转;对时后按本地日历左移;掉电再上电先由 app_nvm_load 灌 VM。 * * ⚠ 上下文分工(勿再踩复位事故): * - app_runrec_tick(20ms 中断):只把在吸合的毫秒折成 pending 秒, * 不碰 s_rr.rec[]、不写 flash。 * - app_runrec_rotate_if_needed(500ms 任务):收下 pending 加到 * slot[0]、每拍灌 DP17、跨天旋转、满 600s / 跨天 / 首次对时才 * 写 VM。拔电没有软关机钩子,接受最多丢不满 10 分钟的秒数。 ******************************************************************************/ #include #include "system/includes.h" /* local_irq_disable / local_irq_enable */ #include "app_runrec.h" #include "app_relay.h" #include "app_nvm.h" #include "usr_rtc.h" #include "jb_product.h" #include "board_pin.h" #define LOG_TAG_CONST APP #define LOG_TAG "[APP_RREC]" #define LOG_INFO_ENABLE #include "debug.h" #if BOARD_RUNREC_ENABLE /** 运行记录上下文:31 格日历 + ISR/任务两侧的折秒状态 */ typedef struct { uint32_t rec[BOARD_RUN_RECORD_DAYS]; /**< slot[0]=今天 … slot[30]=前 30 天(任务侧) */ uint32_t ymd; /**< slot[0] 对应本地日期 yyyymmdd;0=未对时 */ uint16_t ms; /**< 不足 1 秒的毫秒零头(仅 ISR) */ volatile uint16_t pending_sec; /**< ISR 折秒,任务收下后加到 slot[0] */ uint16_t unsaved; /**< 距上次落盘累计的运行秒数(仅任务) */ } AppRunrecCtx_t; static AppRunrecCtx_t s_rr; #endif /** * @brief 公历日期 → 天数序号(Howard Hinnant 的 days_from_civil) * @param y 年(如 2026) * @param m 月 1~12 * @param d 日 1~31 * @return 距 1970-01-01 的天数(可为负) */ static int32_t runrec_days_from_civil(int y, int m, int d) { int era; int yoe; int doy; int doe; y -= (m <= 2) ? 1 : 0; era = ((y >= 0) ? y : (y - 399)) / 400; yoe = y - era * 400; /* [0,399] */ doy = (153 * (m + ((m > 2) ? -3 : 9)) + 2) / 5 + d - 1; /* [0,365] */ doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; /* [0,146096]*/ return (int32_t)(era * 146097L + (long)doe - 719468L); } /** * @brief yyyymmdd 压缩日期 → 天数序号 * @param ymd yyyymmdd(如 20260904) * @return 天数序号 */ static int32_t runrec_ymd_to_days(uint32_t ymd) { return runrec_days_from_civil((int)(ymd / 10000u), (int)((ymd / 100u) % 100u), (int)(ymd % 100u)); } /** * @brief 初始化运行记录 * @return 无 */ void app_runrec_init(void) { #if !BOARD_RUNREC_ENABLE log_info("runrec disabled\n"); return; #else memset(&s_rr, 0, sizeof(s_rr)); app_runrec_pack(gDevData.run_record); /* DP17 报全 0 */ log_info("runrec init, %u days\n", (unsigned)BOARD_RUN_RECORD_DAYS); #endif } #if BOARD_RUNREC_ENABLE /** * @brief 把 ISR 折好的秒数加到 slot[0],满 10 分钟则请求写 VM * @return 1=需要立刻写运行记录 item */ static uint8_t runrec_apply_pending(void) { uint16_t sec; uint8_t need_vm = 0; local_irq_disable(); sec = s_rr.pending_sec; s_rr.pending_sec = 0; local_irq_enable(); if (sec == 0) { return 0; } s_rr.rec[0] += sec; s_rr.unsaved = (uint16_t)(s_rr.unsaved + sec); if (s_rr.unsaved >= BOARD_RUN_RECORD_SAVE_SEC) { s_rr.unsaved = (uint16_t)(s_rr.unsaved % BOARD_RUN_RECORD_SAVE_SEC); need_vm = 1; } if (need_vm) { log_info("runrec %us reached, save vm\n", (unsigned)BOARD_RUN_RECORD_SAVE_SEC); } return need_vm; } #endif /* BOARD_RUNREC_ENABLE */ /** * @brief 20ms 中断上下文节拍:继电器实际吸合才折秒到 pending,不碰 s_rr.rec * @param dt 距上次调用的毫秒数 * @return 无 */ void app_runrec_tick(uint16_t dt) { #if !BOARD_RUNREC_ENABLE (void)dt; return; #else if (!app_relay_get_actual()) { /* 没在加热:不累加,并丢弃不足 1 秒的零头(13.1:未吸合不计入) */ s_rr.ms = 0; return; } s_rr.ms = (uint16_t)(s_rr.ms + dt); while (s_rr.ms >= 1000u) { s_rr.ms = (uint16_t)(s_rr.ms - 1000u); s_rr.pending_sec++; /* 任务侧再加到 slot[0],避免与跨天旋转打架 */ } #endif } /** * @brief 500ms 任务上下文:收下 pending、灌 DP17、跨天旋转、必要时写 VM * @return 无 */ void app_runrec_rotate_if_needed(void) { #if !BOARD_RUNREC_ENABLE return; #else struct sys_time t; uint32_t ymd; int32_t diff; int i; uint8_t need_vm = runrec_apply_pending(); if (usr_rtc_get_time(&t)) { ymd = (uint32_t)t.year * 10000u + (uint32_t)t.month * 100u + (uint32_t)t.day; if (s_rr.ymd == 0) { /* 首次对时:slot[0] 已是当天累计值,只登记日期,不旋转不清零 */ s_rr.ymd = ymd; need_vm = 1; log_info("runrec rtc synced, ymd=%lu\n", (unsigned long)ymd); } else if (ymd != s_rr.ymd) { diff = runrec_ymd_to_days(ymd) - runrec_ymd_to_days(s_rr.ymd); if (diff <= 0) { s_rr.ymd = ymd; /* 时钟回拨/异常:只纠正日期,不乱移数据 */ } else { if (diff > BOARD_RUN_RECORD_DAYS) { diff = BOARD_RUN_RECORD_DAYS; /* 跨太多天:整表作废 */ } /* 左移 N 天:old[i] → new[i+N],前面 N 格清零 */ for (i = (int)BOARD_RUN_RECORD_DAYS - 1; i >= diff; i--) { s_rr.rec[i] = s_rr.rec[i - diff]; } for (i = 0; i < diff; i++) { s_rr.rec[i] = 0; } s_rr.ymd = ymd; need_vm = 1; log_info("runrec rotate %ld day(s), ymd=%lu\n", (long)diff, (unsigned long)ymd); } } } /* 每拍灌 DP17:0x05 读立刻拿到当前秒;差分满 1 分钟才 0x07;10min 全量仍带 */ app_runrec_pack(gDevData.run_record); if (need_vm) { app_nvm_save_runrec(); /* 满 10 分钟或跨天 / 首次对时才写 flash */ } #endif } /** * @brief 取某天的运行秒数 * @param day 0=今天 … 30=前 30 天 * @return 秒数 */ uint32_t app_runrec_get(uint8_t day) { #if !BOARD_RUNREC_ENABLE (void)day; return 0; #else if (day >= BOARD_RUN_RECORD_DAYS) { return 0; } return s_rr.rec[day]; #endif } /** * @brief 打包成 DP17 的 124B 载荷(31 × uint32 大端) * @param out 输出缓冲 * @note 模块关闭时仍输出全 0(13.1:关闭时报全 0), * 避免 app_nvm 把未初始化栈内存写进 VM 的 RUNREC item * @return 无 */ void app_runrec_pack(uint8_t *out) { uint8_t i; #if !BOARD_RUNREC_ENABLE memset(out, 0, APP_RUNREC_PACK_LEN); return; #else for (i = 0; i < BOARD_RUN_RECORD_DAYS; i++) { uint32_t v = s_rr.rec[i]; out[(uint8_t)(i * 4u) + 0u] = (uint8_t)(v >> 24); out[(uint8_t)(i * 4u) + 1u] = (uint8_t)(v >> 16); out[(uint8_t)(i * 4u) + 2u] = (uint8_t)(v >> 8); out[(uint8_t)(i * 4u) + 3u] = (uint8_t)(v & 0xFFu); } #endif } /** * @brief 从 VM 的 124B 载荷灌回 RAM * @param in 输入缓冲 * @return 无 */ void app_runrec_unpack(const uint8_t *in) { uint8_t i; #if !BOARD_RUNREC_ENABLE (void)in; return; #else for (i = 0; i < BOARD_RUN_RECORD_DAYS; i++) { s_rr.rec[i] = ((uint32_t)in[(uint8_t)(i * 4u) + 0u] << 24) | ((uint32_t)in[(uint8_t)(i * 4u) + 1u] << 16) | ((uint32_t)in[(uint8_t)(i * 4u) + 2u] << 8) | (uint32_t)in[(uint8_t)(i * 4u) + 3u]; } app_runrec_pack(gDevData.run_record); /* 开机立刻灌 DP17,不等 500ms */ #endif } /** * @brief slot[0] 对应的本地日期 * @return yyyymmdd;0=未对时 */ uint32_t app_runrec_get_slot0_date(void) { #if !BOARD_RUNREC_ENABLE return 0; #else return s_rr.ymd; #endif } /** * @brief 设置 slot[0] 对应日期 * @param ymd yyyymmdd;0=未对时 * @return 无 */ void app_runrec_set_slot0_date(uint32_t ymd) { #if BOARD_RUNREC_ENABLE s_rr.ymd = ymd; #else (void)ymd; #endif } /** * @brief 清零 31 格与日期(出厂重置) * @return 无 */ void app_runrec_reset(void) { #if !BOARD_RUNREC_ENABLE return; #else memset(&s_rr, 0, sizeof(s_rr)); app_runrec_pack(gDevData.run_record); #endif }