Files
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

134 lines
8.9 KiB
C
Raw Permalink 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 → 2026-10-28 已落地 ui_page_runreport.c)
*
* ┌── 四个数的分类台账(页面文案必须逐项挂类别,"不得含糊"是 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]` 在本仓是"设备出故障"的唯一信号通道(/serial-boot-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 */