/* * 模块名称:Boot Stats(开机账本)实现 * 模块功能:见 boot_stats.h——上电次数落 SD2506 RTC 用户 SRAM,本次开机复位原因 * 在开机时从 RCC_CSR 锁存值翻成枚举驻留 RAM。 * 适用平台:STM32F407ZGTx * 作者:道G log-maint-writer * 创建日期:2026-10-27 * 修改记录: * v1.0 2026-10-27 道G 创建(task-12) */ #include "boot_stats.h" #include #include "main.h" /* RCC_CSR_IWDGRSTF 等位掩码(与 CMSIS 同名同值;主机单测走桩 main.h) */ #include "app_main.h" /* g_rcc_csr_latched:Src/main.c 在 HAL_Init() 后采得并清标志 */ #include "sd2506.h" /* sd2506_read_sram / sd2506_write_sram / sd2506_read_ctr1 + BLF 位 */ /* ---- RTC 用户 SRAM 记录布局(8 字节,偏移 0 = 寄存器 2CH)---------------------------- * [0] 0xB5 magic 低 * [1] 0x72 magic 高 ← 两个字节都对上才算本模块写过的记录 * [2..5] 上电次数,小端 u32 * [6] 0x01 记录格式版本 ← 以后改布局必须升它,否则旧记录会被当新格式读出错位 * [7] 校验 = [0]..[6] 逐字节异或 * 为什么带校验而不是只看 magic:电池欠压造成的坏区经常是**成片全 0 或成片全 FF**, * magic 那关就已经过不去;但"漂了一位数据"更常见的是个别字节翻转,那时 magic 可能 * 仍然好、次数却错了 256 倍。这一格是给人看"装置上了多少次电"的账,宁可报 * "不可信/重开",也不要报一个看起来很像样的错数。 * 为什么放偏移 0:SD2506 有 70 B 用户 SRAM(2CH~71H),本模块只用前 8 B,后面 * 留着——日后要放"上次结束原因"之类的另起一段,别在同一段里挤着改。 */ #define BS_SRAM_OFS 0u #define BS_REC_LEN 8u #define BS_REC_MAGIC_LO 0xB5u #define BS_REC_MAGIC_HI 0x72u #define BS_REC_VER 1u /* 计数饱和上限:到顶就不再自增也不再写回(宁可停在上限,也不许回绕成 1) */ #define BS_CNT_MAX 0xFFFFFFF0u static boot_stats_t s_bs; /* .bss 零初值 = ready 0,天然表达"还没 init" */ /* * 函数功能:把 RCC_CSR 锁存值翻成复位原因编码 * 入口参数:csr - RCC_CSR 锁存值 uint32_t * 返 回 值:BOOT_STATS_CAUSE_* 之一 uint8_t * 限定条件:任意上下文;纯位判定,不读外设、不写状态 * 函数说明:本函数是这条优先级链的**唯一一份**。App/app/app_main.c 的开机打印过去 * 自带一条同样顺序的 if-else 链,现在改成经本函数取编码再取串——两处各写 * 一条链,早晚有一天串口说"IWDG"而页面上写"software"。 */ uint8_t boot_stats_cause_from_csr(uint32_t csr) { if ((csr & RCC_CSR_IWDGRSTF) != 0u) { return BOOT_STATS_CAUSE_IWDG; } if ((csr & RCC_CSR_WWDGRSTF) != 0u) { return BOOT_STATS_CAUSE_WWDG; } if ((csr & RCC_CSR_SFTRSTF) != 0u) { return BOOT_STATS_CAUSE_SFT; } if ((csr & RCC_CSR_PORRSTF) != 0u) { return BOOT_STATS_CAUSE_POR; } if ((csr & RCC_CSR_PINRSTF) != 0u) { return BOOT_STATS_CAUSE_PIN; } if ((csr & RCC_CSR_BORRSTF) != 0u) { return BOOT_STATS_CAUSE_BOR; } return BOOT_STATS_CAUSE_UNKNOWN; } /* * 函数功能:取复位原因可读串 * 入口参数:cause - BOOT_STATS_CAUSE_* uint8_t * 返 回 值:静态常量串(永不 NULL) const char * * 限定条件:任意上下文;返回指针指向 .const,调用方不得写 * 函数说明:串内容与 App/app/app_main.c 过去打印的一致(纯 ASCII)。编码不认识时 * 回落 "unknown" 而不是断言——这条链跑在开机极早期,不能因为一个编码把 * 启动挡住。 */ const char *boot_stats_cause_str(uint8_t cause) { switch (cause) { case BOOT_STATS_CAUSE_IWDG: return "IWDG (independent watchdog)"; case BOOT_STATS_CAUSE_WWDG: return "WWDG (window watchdog)"; case BOOT_STATS_CAUSE_SFT: return "software (NVIC_SystemReset)"; case BOOT_STATS_CAUSE_POR: return "power-on (POR)"; case BOOT_STATS_CAUSE_PIN: return "external pin (NRST)"; case BOOT_STATS_CAUSE_BOR: return "brown-out (BOR)"; default: return "unknown"; } } /* * 函数功能:算记录校验([0] 到倒数第二字节逐字节异或) * 入口参数:p_rec - 记录缓冲(长度恒为 BS_REC_LEN,不作出参长度参数) const uint8_t * * 返 回 值:校验字节 uint8_t * 限定条件:调用方保证 p_rec 非空且长度 BS_REC_LEN(本文件内私有,两处调用点都传数组) * 函数说明:校验与写回**共用这同一个函数**——两边各写一份异或,改布局时必有一份忘改, * 症状是"记录永远读不出来"(写进去的校验和读的时候算的不一样)。 */ static uint8_t s_rec_sum(const uint8_t *p_rec) { uint8_t sum = 0u; uint8_t i; for (i = 0u; i < BS_REC_LEN - 1u; i++) { sum = (uint8_t)(sum ^ p_rec[i]); } return sum; } /* * 函数功能:判记录可信(magic 两字节 + 格式版本 + 校验,三者全中才算) * 入口参数:p_rec - 记录缓冲 const uint8_t * * 返 回 值:1 - 可信;0 - 不可信(按"无记录"另走首次/掉电两条分支) uint8_t * 限定条件:调用方保证 p_rec 已填入 BS_REC_LEN 字节 * 函数说明:**只信 magic 不够**:电池坏区常见整片全 0/全 FF(magic 自然过不去), * 但"个别字节翻转"时 magic 可能仍然好,次数会错 256 倍——那正是校验和 * 要挡的情形(机器判据 TC-BOOTSTAT-007)。 */ static uint8_t s_rec_valid(const uint8_t *p_rec) { if ((p_rec[0] != BS_REC_MAGIC_LO) || (p_rec[1] != BS_REC_MAGIC_HI)) { return 0u; } if (p_rec[6] != BS_REC_VER) { return 0u; } return (p_rec[7] == s_rec_sum(p_rec)) ? 1u : 0u; } /* * 函数功能:开机快照 + 上电次数自增(幂等) * 入口参数:无 void * 返 回 值:无 * 限定条件:SD2506 已 init 之后;唯一调用点在 App/app/app_main.c 的启动段(内核启动 * 前,单线程非 ISR)。本函数含 1 次 I2C 读 + 至多 1 次 I2C 写,I2C1 与 RTC * 同总线,严禁搬进 iec104/adc 关键链或 ISR * 函数说明:状态全留在 s_bs 里,一条日志都不打(可观测性事件不进 [ERR],理由见 * boot_stats.h 与 .c 里 s_bs 那几枚字段的注释)。 * 写失败的后果要算清楚:本次只在 RAM 里数上了,下次开机读回的还是旧值, * 账本**少 1**,不会重复计——所以 write_failed 是"少账"的记号,不是故障。 */ void boot_stats_init(void) { uint8_t rec[BS_REC_LEN]; uint8_t ctr1 = 0u; uint32_t cnt; bsp_err_t err; if (s_bs.ready != 0u) { return; /* 幂等:bring-up 重试不重复计数 */ } /* ① 复位原因快照——**必须先做**:RCC_CSR 的标志在 Src/main.c 里读后即清, * 本函数拿到的一直是那份锁存影子,与串口那条 "reset cause:" 同源同序。 */ s_bs.rst_cause = boot_stats_cause_from_csr(g_rcc_csr_latched); s_bs.ready = 1u; /* ② 读 RTC 用户 SRAM。I2C 失败 ≠ 没有记录:把这一格标成"不可信"就停手, * **绝不写回**——在只读到一半脏数据的情况下去写,等于把可能还完好的记录 * 覆盖掉(芯片刚上电、总线还没稳的时候最容易走这条路)。 */ err = sd2506_read_sram(BS_SRAM_OFS, rec, BS_REC_LEN); if (err != BSP_OK) { s_bs.seq_state = BOOT_STATS_SEQ_UNKNOWN; s_bs.boot_count = 0u; return; } /* ③ BLF(电池欠压标志):只用来把"记录无效"分成「首次启用」与「电池域掉过电」 * 两件事;读失败按 0 处理,且不参与有效性判定(读不到 BLF 不该把好记录判坏)。 * 该位是滞后的:置位后不清就一直在那儿,所以只在记录无效时才看它——记录还有效 * 就说明电池域没把 SRAM 洗掉,滞后无害。 */ (void)sd2506_read_ctr1(&ctr1); s_bs.rtc_blf = ((ctr1 & SD2506_CTR1_BLF) != 0u) ? 1u : 0u; if (s_rec_valid(rec) != 0u) { cnt = ((uint32_t)rec[2]) | ((uint32_t)rec[3] << 8) | ((uint32_t)rec[4] << 16) | ((uint32_t)rec[5] << 24); s_bs.seq_state = BOOT_STATS_SEQ_CONTINUED; } else { /* 记录无效:BLF 置位 → 电池周期换了,计数是从重开的;BLF 未置位 → 本模块 * 从来没写过这块 SRAM(第一次上电/首次刷入带本功能的固件)。两者**不许 * 合并**,合并就等于把"第一次上电"说成"RTC 掉过电"。 */ cnt = 0u; s_bs.seq_state = (s_bs.rtc_blf != 0u) ? BOOT_STATS_SEQ_RESTARTED : BOOT_STATS_SEQ_FRESH; } s_bs.write_failed = 0u; if (cnt >= BS_CNT_MAX) { /* 饱和:停在上限,不再写回。回绕成小数字比"停在 42 亿"恶劣得多——前者会让 * 人以为装置刚上电几次,后者一眼就能看出到头了。 */ s_bs.boot_count = cnt; return; } cnt = cnt + 1u; s_bs.boot_count = cnt; rec[0] = BS_REC_MAGIC_LO; rec[1] = BS_REC_MAGIC_HI; rec[2] = (uint8_t)(cnt & 0xFFu); rec[3] = (uint8_t)((cnt >> 8) & 0xFFu); rec[4] = (uint8_t)((cnt >> 16) & 0xFFu); rec[5] = (uint8_t)((cnt >> 24) & 0xFFu); rec[6] = BS_REC_VER; rec[7] = s_rec_sum(rec); err = sd2506_write_sram(BS_SRAM_OFS, rec, BS_REC_LEN); if (err != BSP_OK) { s_bs.write_failed = 1u; /* 少 1 笔账,不是故障:见函数头 */ } } /* * 函数功能:读开机账本快照 * 入口参数:p_out - 输出缓冲;NULL 时无害返回 boot_stats_t * * 返 回 值:无 void * 限定条件:任意上下文可读——不取锁、不读时刻、不碰 I2C、不阻塞 * 函数说明:整体一次拷(memcpy 结构体),跨字段不保证同一瞬间;表内没有任何"两字段 * 合起来才成立"的判据,混窗对显示无害(详见头文件)。 */ void boot_stats_get(boot_stats_t *p_out) { if (p_out == NULL) { return; } (void)memcpy(p_out, &s_bs, sizeof(boot_stats_t)); }