728d60675f
三条道在同一棵树上交付,故合并成**一次权威构建**(@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,交台架道补。
134 lines
8.8 KiB
C
134 lines
8.8 KiB
C
/*
|
||
* 模块名称: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 */
|