Files
STM32F4-Base/App/util/boot_stats.c
T
wandering1 728d60675f feat(UI/采集/104): 通讯设置页 + 开机账本 + adc_cal 可诊断化——三车道同树一次构建入库
三条道在同一棵树上交付,故合并成**一次权威构建**(@build 0 错 0 警,Code=235236 RO=26308 RW=1572
ZI=136924)后同笔入库;台账已按该构建回填(`ram_baseline.py --update` + `--write`、`chec
k_mem_numbers.py
 --write`),凭证与实际 .map 同源。归属如下:

· **通讯设置页**(道G task-9):新增 `Drivers/BSP/UI/ui_page_comm.{c,h}`(通道六注入,与死
区页同骨架同纪
  律),`#page=comm` 绑定使 bound **21 → 22**、内容页黄金 **47 → 53**(旧 47 页字节零变化,
新增六态含
  WAIT/OK 两张);`iec104_comm` 脏位 + store_save(**无事件位**)挂 defaultTask 同点位
轮询;`port_104` 由
  宏改走 getter、`IEC104_SERV_PORT` 删除;字体 +听/监(185 字全覆)。**实机链已闭环**:FTP 外改
[comm]
  的 ca/port → 复位 → 串口出现 `section 'comm' (2 keys)` 与 `i104: listening o
n 9004` → 还原 2404,全程无
  FAIL ⇒「重启即生效 + 端口真值」不是推断是实测。上限核对 6/8 段、2/16 键,超限现象由 TC-CONFIG-056/057
  钉死(注册 -1 + `comm init: FAIL` 锚 + getter 回 ROM),**不抬宏**。
· **开机账本**(道F task-12):新增 `App/util/boot_stats.{c,h}`——上电次数落 SD2506 用户
 SRAM(magic +
  小端计数 + 版本 + 异或校验),一次上电 = 1 读 + ≤1 写,**零 NAND、零卷锁、零配置段**;四态互斥
  UNKNOWN / FRESH / CONTINUED / RESTARTED(BLF=1 才算电池域掉过电),**读失败绝不回写**;
文案口径「自 RTC
  电池周期起」,**禁写终身次数**。复位原因优先级链从 `app_main.c` 的 if-else 收进一份真源(串口与屏幕同
  源);104 四个只增不清计数 + `iec104_serv_get_stats()` 留在 serv、未塞进 link(会被 open
/close 洗掉)。
  运行时长按用户裁定 = **只报本次开机以来**,零新增代码。新套件 `test/host/boot_stats` 21/21,五处注入
  **全部有牙**(含「漂一字节被校验和挡下而不是报 256 倍错账」)。
· **adc_cal 可诊断化**(道E task-43):三个 volatile 计数 + `adc_cal_get_stats()`
/ `adc_cal_note_persisted()`
  (水位**盖写非自增**,因为一次 save 覆盖整档,数 save 会留假未落盘);「差额 × 脏位」三态语义唯一落在
  `adc_cal.h`,其中「差额 > 0 且脏位恒 0」= 绕过语义 setter 的静默丢失;TC-ADCCAL-019~023。

三条**必须留档的自纠**(都是执行道自己报的,我不替他们润色):
① stdint 那条断链是**宿主套件抓到、通讯道先修**,采集道承认自己是重复添加者并删掉自己那份;
② 「exit 2 不是证据,断言变红才是」——采集道第一版反证注入了未声明标识符导致编译错,差点当成功;
③ 套件汇总行原写 `s_total/s_total`,失败时会打印「21/21 PASSED (failed=1)」这种自相矛盾的行,已改。

另修:`check_registration.py` 的 `SIM_EXEMPT` 加一行豁免 `App/util/boot_stats.c
`(依赖 I2C 与 RCC 位掩码,
MCU 专属;核过 `sys_clock.c`/`sha256.c` 同形态先例),注册反向校验 exit 1 → 0。发号 TC-IEC1
04-034、
TC-UIDATA-029 已实抬游标(`TC-IEC104-033→034`、`TC-UIDATA-026→029`)。

门禁:id-check --strict 与 plain 均 rc=0、doc-links --all rc=0、注册校验 0、cppche
ck / coding-standard /
bom-drift 0;iec104 179/179、ui_data 295/295、boot_stats 21/21、adc_cal 11
7/117、run_par 67/67;菜单 8 状态
池 5368 不变、内容页 53/53。**已知遗留**:内容页数 47 → 53 的全仓文本同步尚未做(另卡跟踪),本笔只保
证代码与黄金一致;面板注键真机那一腿缺 CLI,交台架道补。
2026-10-27 11:29:23 +08:00

241 lines
10 KiB
C
Raw 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.
/*
* 模块名称: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 <string.h>
#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));
}