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,交台架道补。
This commit is contained in:
2026-10-27 11:29:23 +08:00
parent 0ae9c10f24
commit 728d60675f
47 changed files with 2364 additions and 123 deletions
+240
View File
@@ -0,0 +1,240 @@
/*
* 模块名称: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));
}
+133
View File
@@ -0,0 +1,133 @@
/*
* 模块名称:Boot Stats(开机账本:上电次数 + 本次开机复位原因)
* 模块功能:给"运行报告"这一页提供两个**没有现成正源**的量——① 上电次数(掉电保持,
* 落 SD2506 RTC 用户 SRAM,零 NAND 磨损、零卷锁、零配置段);② 本次开机的
* 复位原因(RCC_CSR 标志在 Src/main.c 里读后即清,故本模块在开机时把
* 锁存值翻成枚举**驻留 RAM**,否则第二次读就什么都没有了)。
* 适用平台:STM32F407ZGTx(仅 MCU 端:依赖 SD2506 I2C 与 RCC_CSR;主机单测用桩 SD2506
* 与桩 main.h,见 test/host/boot_stats/;模拟器不编译本模块)
* 作者:道G log-maint-writer
* 创建日期:2026-10-27
* 修改记录:
* v1.0 2026-10-27 道G 创建(task-12「运行报告计数器」的数据面;UI 页面归 task-13)
*
* ┌── 四个数的分类台账(页面文案必须逐项挂类别,"不得含糊"是 UI 补齐清单的原文判据)──┐
* │ 上电次数 = **掉电保持**:本模块,落 RTC 用户 SRAM;口径见下面 BOOT_STATS_SEQ_* 一段 │
* │ 运行时长 = **本次开机以来**:现成值 `ui_data_info_t.uptime_s`(写侧 │
* │ App/app/ui_data_feed.c),**本模块不重复提供**——重复就是第二真源。 │
* │ 用户裁定 2026-10-27:累计时长不做(装置无正常关机流程,任何"累计"都只能 │
* │ 周期写近似值)。⚠ 连续开机 49.7 天后 tick 回绕归零,所以那一栏必须写 │
* │ 「本次开机以来」,不能让人读成累计值。 │
* │ 复位原因 = **本次开机**:本模块 `rst_cause`(跨启动的"上一次结束原因"本卡**不做**, │
* │ 它需要额外一次落盘写点,且口径未裁,别在页面上口头承诺) │
* │ 104 统计 = **本次开机以来**(跨重连不清零):计数在 App/task/iec104_serv.c, │
* │ getter 在 App/task/iec104_serv.h——账本归 104,不并进本模块(分层) │
* └──────────────────────────────────────────────────────────────────────────────┘
*
* ⚠ 「上电次数」这一格**不许写成终身次数**:保持介质是 RTC 芯片的电池域 SRAM,
* 换电池 / 电池欠压都会把它清掉。可读出来的只有"自 RTC 电池周期起"这一件事,
* 且必须能把「本计数器首次启用」和「电池域掉过电」分开——见 `seq_state`:
* UNKNOWN = SRAM 读失败(I2C 超时/芯片故障):既不知道计数,也不知道是否延续;
* FRESH = 记录无效但 RTC 没报欠压(BLF=0)= **本固件第一次写它**;
* CONTINUED = 记录有效 = 接着上一轮数;
* RESTARTED = 记录无效且 BLF=1 = 电池域掉过电,计数是从重开的。
* 把 FRESH 与 RESTARTED 混成一件事 = 拿"第一次上电"冒充"RTC 掉电"(或反过来);
* 把 UNKNOWN 当成 FRESH = I2C 故障那天,屏幕上的次数悄悄归 1。
*/
#ifndef BOOT_STATS_H
#define BOOT_STATS_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
/* 复位原因编码。判定优先级(IWDG > WWDG > 软件 > POR > PIN > BOR > 未知)的
* **唯一出处**是 boot_stats_cause_from_csr():App/app/app_main.c 的开机打印改成调用
* 它取串,所以这里不再另列一遍顺序,两处也不许各写一条链(两处各写就会漂)。 */
#define BOOT_STATS_CAUSE_UNKNOWN 0u /* 六个标志都没置位(锁存值为 0 等) */
#define BOOT_STATS_CAUSE_IWDG 1u /* 独立看门狗:上一轮有任务卡死,自动恢复事件 */
#define BOOT_STATS_CAUSE_WWDG 2u /* 窗口看门狗 */
#define BOOT_STATS_CAUSE_SFT 3u /* 软件复位(崩溃钩子兜底的 NVIC_SystemReset) */
#define BOOT_STATS_CAUSE_POR 4u /* 上电复位 */
#define BOOT_STATS_CAUSE_PIN 5u /* 外部复位引脚 NRST */
#define BOOT_STATS_CAUSE_BOR 6u /* 掉电(brown-out) */
/* SRAM 记录的可信度状态,见文件头那一段。 */
#define BOOT_STATS_SEQ_UNKNOWN 0u
#define BOOT_STATS_SEQ_FRESH 1u
#define BOOT_STATS_SEQ_CONTINUED 2u
#define BOOT_STATS_SEQ_RESTARTED 3u
typedef struct {
uint32_t boot_count; /* 上电次数;seq_state == UNKNOWN 时**不可信**(此时为 0) */
uint8_t rst_cause; /* BOOT_STATS_CAUSE_*:本次开机 */
uint8_t seq_state; /* BOOT_STATS_SEQ_*:上电次数这一格的可信度 */
uint8_t write_failed; /* 1 = 本次自增没能写回 SRAM:这次只在 RAM 里数上了,
* 下次开机读到的还是旧值 ⇒ 账本少 1(只会少不会重) */
uint8_t rtc_blf; /* RTC 电池欠压标志原值(0/1);读不到时按 0,不参与判定 */
uint8_t ready; /* 0 = boot_stats_init() 还没跑:整表都是初值,别当数据用 */
} boot_stats_t;
/*
* 函数功能:把 RCC_CSR 锁存值翻成复位原因编码(优先级链的唯一出处)
* 入口参数:csr - RCC_CSR 的锁存值(Src/main.c 在 HAL_Init() 后采并清标志)uint32_t
* 返 回 值:BOOT_STATS_CAUSE_* 之一 uint8_t
* 限定条件:任意上下文(纯位判定,无副作用、不读外设、不取锁)
* 函数说明:IWDG/WWDG 排在软件复位之前是刻意的:看门狗复位才是我们要看的自动恢复
* 事件,而崩溃钩子常以 NVIC_SystemReset 收尾——两类标志同时置位时,报
* 看门狗更有价值(理由与 App/app/app_main.c 原注释一致,此处只搬家不改编)。
*/
uint8_t boot_stats_cause_from_csr(uint32_t csr);
/*
* 函数功能:取复位原因的可读串(纯 ASCII,供开机日志与页面共用)
* 入口参数:cause - BOOT_STATS_CAUSE_* 之一 uint8_t
* 返 回 值:静态常量串指针(永不 NULL;未知编码回落 "unknown") const char *
* 限定条件:任意上下文(只读 .const,不写缓冲、不需要调用方提供空间)
*/
const char *boot_stats_cause_str(uint8_t cause);
/*
* 函数功能:开机快照 + 上电次数自增(一次上电调一次,幂等)
* 入口参数:无 void
* 返 回 值:无
* 限定条件:**唯一调用点 = App/app/app_main.c 紧接 sd2506_init 之后**(内核启动前:
* 单线程、非 ISR、调度器未跑;本函数不调任何创建类内核 API)。之所以不放在
* defaultTask 的存储 bring-up:保持介质是 RTC 用户 SRAM 而不是文件系统,
* 不必等 STORAGE_READY,也就把 I2C 彻底挪出了喂狗点与卷锁之外。
* 本函数做 1 次 I2C 读 + 至多 1 次 I2C 写:I2C1 与 RTC 同总线,严禁再把它
* 搬进 iec104/adc 任何关键链或 ISR。重复调用直接返回,不重复计数。
* 函数说明:① 全程**不打日志**:I2C 失败/电池换过都是可观测性事件,不是装置故障;
* `[ERR]` 在本仓是"设备出故障"的唯一信号通道(@test 的 Forbidden 依赖它),
* 把可观测性事件塞进去就变成第二个信号源——与"日志让路不计 [ERR]"、
* 卷锁 fail-open 只投自检记录是同一条口径。状态一律留在结构体里。
* ② 落盘只写 8 字节到 RTC 用户 SRAM:无 FTL 写放大、不取卷锁、不进 /log;
* 一次上电一笔 I2C 写,失败**不做周期补偿**(否则"零写放大"方案会被做成
* 周期 I2C 轮询),后果就是 write_failed 那一条。
*/
void boot_stats_init(void);
/*
* 函数功能:读开机账本快照(纯拷字段)
* 入口参数:p_out - 输出缓冲;NULL 时无害返回 boot_stats_t *
* 返 回 值:无 void
* 限定条件:**任意上下文可读**——不取锁、不读时刻、不碰 I2C、不阻塞(照
* adc_cal_get_stats() / vol_lock_get_stats() 的形态:观测口一旦要去抢
* 总线或等锁,观测就成了故障的一部分)
* 函数说明:① 各字段都是 32 位对齐量或单字节,Cortex-M4 上单字/单字节读写原子;
* 跨字段**不保证同一瞬间**——本表里没有任何"两个字段合起来才成立"的判据,
* 所以混窗对显示无害。seq_state 与 boot_count 要一起看时,按"是否可信"
* 读,不要拿差值当账。
* ② ready == 0 表示 boot_stats_init() 还没跑(开机极早期或 SD2506 未就绪),
* 此时整表为 0,页面应显示占位而不是 0 次。
*/
void boot_stats_get(boot_stats_t *p_out);
#ifdef __cplusplus
}
#endif
#endif /* BOOT_STATS_H */