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,交台架道补。
241 lines
10 KiB
C
241 lines
10 KiB
C
/*
|
||
* 模块名称: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));
|
||
}
|