/****************************************************************************** * @file app_runrec.c * @brief CBR3 31 天运行记录实现(规格书第 13.1 节) * @author cyWu <1917507415@qq.com> * @date 2026.09.02 * @version V1.0.0,随规格书 V3.0.0 新建 * * @note 累加源是 app_fan_get_gear():档位≠0 才是「实际在转」,通风停 * 5 分钟那段 gear=0 自然不计入(13.1)。 * * ⚠ 上下文分工(2026-08-31 事故定论,勿再踩): * - app_runrec_tick(20ms 中断):只把在转的毫秒折成 pending 秒, * **不碰 s_rr.rec[]、不打日志、不写 flash**(跨天旋转在任务侧)。 * - app_runrec_rotate_if_needed(500ms 任务):收下 pending 加到 * slot[0]、每拍灌 DP21、跨天旋转、满 10 分钟 / 跨天才写 VM。 * DP21 每 500ms 更新到 gDevData,供读请求和 10min 全量上报; * 差分变化上报不带 DP21(避免每秒推 124B)。 * 拔电没有软关机钩子,接受最多丢不满 10 分钟的秒数(13.1 已拍板)。 ******************************************************************************/ #include #include "system/includes.h" /* local_irq_disable / local_irq_enable */ #include "app_runrec.h" #include "app_fan.h" #include "app_nvm.h" #include "usr_rtc.h" #include "jb_product.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(如 20260902) * @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); /* DP21 报全 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_fan_get_gear() == 0) { /* 没在转:不累加,并丢弃不足 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、灌 DP21、跨天旋转、必要时写 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); } } } /* 每拍灌 DP21:读请求 / 10min 全量能拿到当前秒数;差分上报不带本 DP */ 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 打包成 DP21 的 124B 载荷(31 × uint32 大端) * @param out 输出缓冲 * @return 无 */ void app_runrec_pack(uint8_t *out) { uint8_t i; #if !BOARD_RUNREC_ENABLE memset(out, 0, APP_RUNREC_PACK_LEN); /* 13.1:关闭时报全 0 */ 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); /* 开机立刻灌 DP21,不等 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 }