Files
STM32F4-Base/App/util/boot_stats.h
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

134 lines
8.8 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(开机账本:上电次数 + 本次开机复位原因)
* 模块功能:给"运行报告"这一页提供两个**没有现成正源**的量——① 上电次数(掉电保持,
* 落 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 */