# 开机账本单元测试说明(主机侧) > **模块**:Boot Stats(`App/util/boot_stats.c` + `App/util/boot_stats.h`) > **测试类型**:**主机侧纯逻辑单元测试**——在 PC 上离线验证"上电次数"这块 > RTC 用户 SRAM 账本的**四态判定、记录校验、I2C 失败分支、幂等与饱和**,以及复位原因 > 优先级链的唯一出处。零硬件、零烧录、毫秒级。 > **状态**:✅ 已落地(21 断言点,MSVC `/W4` 与 cppcheck 均 0 告警) > **日期**:2026-10-27(道G task-12) > **关联文档**: > [`../../../AGENTS.md`](../../../AGENTS.md)(RTOS 与内存铁律、`[ERR]` 通道口径)、 > [`测试开发标准流程.md`](../流程与规范/测试开发标准流程.md)(测试分层与门禁总览)、 > [`主机单测覆盖率口径.md`](主机单测覆盖率口径.md)(覆盖率总闸如何收编本套)、 > [`../../子系统设计/系统墙钟时间维护说明.md`](../../子系统设计/系统墙钟时间维护说明.md) > §13.1(同一块 70 B 用户 SRAM 的**占用分配表**,本模块占 0..7)、 > [`../../子系统设计/IEC104上行设计说明.md`](../../子系统设计/IEC104上行设计说明.md) > (同一张运行报告页的另一半数据源:104 观测计数) --- ## 1. 为什么要单独做这一套 被测量的用途是回答"这台装置上了多少次电"。这类账本**错的时候不像错**:屏幕上永远 有一个数字,格式正常、量级正常,没有任何一行日志会说它是假的。四条只能靠构造才能 复现的失效路径: 1. **I2C 读失败被当成"没有记录"**:一次总线抖动就把计数当 0 重来,从此从 1 往上数。 更坏的是读失败还顺手回写——那会把可能完好无损的旧记录**覆盖掉**,故障从"这次读不到" 升级成"永久少一笔账"。 2. **"本模块首次启用"与"RTC 电池域掉过电"被合并成一态**:这两件事的文案完全不同 (前者是"账本从这一版固件开始",后者是"换过电池,历史没了")。合并就是拿前者 冒充后者。 3. **只信 magic、不信校验和**:电池欠压造成的坏区常是整片全 0 或整片全 FF,magic 那一关自然过不去;但更常见的失效是**个别字节翻转**——那时 magic 可能仍然是好的, 而次数会错 256 倍。这种数字最容易被人当成真的。 4. **到顶回绕而不是饱和**:回绕成个位数会被读成"刚上电几次";停在上限则一眼能看出 到头了。 真机只能覆盖"一切正常"那一条:SRAM 完好、I2C 通、计数往下加。上面四条在板端**构造不 出来**(要真的把 I2C 拉死、真的换电池、真的翻转一个字节),而主机侧只要给假件注入一个 返回值。这是本套存在的全部理由。 --- ## 2. 用例清单(本文是这些号的**定义位**) 跑法见 §4。断言全部只盯结构体字段与假 SRAM 的字节,不依赖打印。 | 号 | 判据 | 为什么是这一条 | |---|---|---| | `TC-BOOTSTAT-001` | SRAM 成片全 1(magic 过不去)且 RTC 未报欠压 ⇒ 判 FRESH、计数从 1 起、落盘一笔 | 首次启用与掉电必须分开;第一格是 1 不是 0 | | `TC-BOOTSTAT-002` | 合法记录 count=7 ⇒ 判 CONTINUED、count=8,且回写后介质里也是 8 | 账要真的写回去,不是只写在 RAM | | `TC-BOOTSTAT-002b` | 介质不动、只重开被测模块 ⇒ 继续往上数(9) | 这就是"跨掉电保持"的本体 | | `TC-BOOTSTAT-003` | 记录无效且 BLF 置位 ⇒ 判 RESTARTED,绝不判 FRESH | 换过电池要说换过电池 | | `TC-BOOTSTAT-004` | 读失败 ⇒ 判 UNKNOWN、计数归零显示、**零次回写**,假 SRAM 逐字节不变 | 读不到不许覆盖账本 | | `TC-BOOTSTAT-004b` | 同一状态里 `ready=1` 且 `seq=UNKNOWN` | "不可信"必须与"零次""还没初始化"三种都能分辨 | | `TC-BOOTSTAT-005` | 写失败 ⇒ 本次仍报数但 `write_failed=1` | 这条账只会少 1,不会重复计 | | `TC-BOOTSTAT-006` | 同一次开机连调两次 init ⇒ 只 +1、只写一笔 | bring-up 重试不能把一次上电数成两次 | | `TC-BOOTSTAT-007` | magic 与版本都好、中段漂一位 ⇒ 被校验和挡下,报 FRESH 而不是错账 | 挡住"256 倍的像真数字" | | `TC-BOOTSTAT-008` | 格式版本不是当前值 ⇒ 整条作废并按当前版本重写(校验同步重算) | 将来改布局时旧记录不能错位读 | | `TC-BOOTSTAT-009` | 计数已达上限 ⇒ 不再自增、不再写回 | 饱和而不是回绕 | | `TC-BOOTSTAT-010` | BLF 读不到但记录有效 ⇒ 仍判 CONTINUED、`rtc_blf=0` | 读不到佐证不该把好账判坏 | | `TC-BOOTSTAT-011` | SRAM 整片全 0(另一种擦除形态)⇒ FRESH 且第一格是 1 | 出厂态与掉电态的字节形态不同,两态都要认 | | `TC-BOOTSTAT-020` | `IWDG \| SFT` ⇒ IWDG | 崩溃钩子常以软件复位收尾,两态同置时报看门狗才有价值 | | `TC-BOOTSTAT-020b` | `WWDG \| SFT` ⇒ WWDG;单独 SFT ⇒ SFT | 同上,且不能把纯软件复位误判成狗 | | `TC-BOOTSTAT-020c` | `POR \| PIN` ⇒ POR;`PIN \| BOR` ⇒ PIN;只有 BOR ⇒ BOR;全零 ⇒ UNKNOWN | 全零必须承认"不知道",不许猜一个 | | `TC-BOOTSTAT-020d` | 每个编码都有可读串、全纯 ASCII、未知编码回落 `unknown` | 无 BOM 文件里字符串必须 ASCII;开机极早期不能因编码挡启动 | | `TC-BOOTSTAT-020e` | 快照暴露的就是 `g_rcc_csr_latched` 里那次复位原因 | 串口与屏幕不可能各说一套(链已收成一份真源) | | `TC-BOOTSTAT-030` | 未 init ⇒ 整表零且 `ready=0` | 页面该显示占位,而不是"上电 0 次" | | `TC-BOOTSTAT-030b` | `boot_stats_get(NULL)` 无害 | 观测口不该因调用者手滑而炸 | | `TC-BOOTSTAT-030c` | init 后连读 100 次,三类 I2C 调用计数**纹丝不动** | "任意上下文可读"不是口头承诺 | > ⚠ 这些号是**主机侧用例号**,不是板端用例号,也不是需求号;族口径沿用 > `docs/编号规范.md` 的 TC 族(既有号只读),所以**本文就是定义位**: > 新增/删除用例必须同步改本表——`id-check` 已于 2026-10-28 退役,悬空与孤儿**不再有机器警报**,只靠评审; > 新增用例本身不再配号(`docs/编号规范.md` §4.1 一律停发),用语义化测试函数名。 --- ## 3. 桩的边界 | 头 | 取真实还是桩 | 原因 | |---|---|---| | `stub/main.h` | **桩** | 只给六个 `RCC_CSR_*RSTF` 位掩码;位号照抄 CMSIS(BOR25 / PIN26 / POR27 / SFT28 / IWDG29 / WWDG30)。真实 `main.h` 会拉整棵 HAL 头树,PC 上编不动。缺宏 ⇒ 编译直接失败,不会出现"桩悄悄放过"。 | | `sd2506.h` / `bsp_err.h` / `boot_stats.h` / `app_main.h` | **真实头** | 都不依赖 HAL 类型,用真头才不会出现"假件与真契约漂移" | | `sd2506_read_sram` / `sd2506_write_sram` / `sd2506_read_ctr1` | **假件(本套实现)** | 一片 70 B 假 SRAM + 三个可注入的错误码;地址越界按真驱动契约返 `BSP_ERR_PARAM` | | `g_rcc_csr_latched` | 本套给出定义 | 真定义在 `App/app/app_main.c`,本套不编译它 | --- ## 4. 怎么跑 ```powershell & '.venv\Scripts\python.exe' test/host/boot_stats/run_unit_test.py ``` 退出码 0=PASS / 1=测试失败 / 2=环境错误。汇总行是 `s_passed/s_total`——**分子必须是 真过了几条**;曾经写成 `s_total/s_total`,那种情况下失败时会打印 "21/21 PASSED (failed=1)" 这种自相矛盾的串,等于把门禁的失败显示成成功。 --- ## 5. 注入反证台账(这套断言有没有牙) 逐个注入、跑套件、逐字节还原(还原后复跑全绿): | 注入的退化写法 | 变红的号 | 说明 | |---|---|---| | 读失败改成"当成读出一片空白"继续往下 | `TC-BOOTSTAT-004`、`TC-BOOTSTAT-004b` | 会回写、把完好账本覆盖掉 | | 合并 FRESH 与 RESTARTED(不看 BLF) | `TC-BOOTSTAT-003` | 拿首次上电冒充 RTC 掉电 | | 只判 magic、丢掉校验和 | `TC-BOOTSTAT-007` | 256 倍错账照发 | | 去掉饱和判据(改成回绕) | `TC-BOOTSTAT-009` | 到顶变小数字 | | BLF 读失败就早退并把记录判坏 | `TC-BOOTSTAT-010` | 判定顺序写反的代价 | > ⚠ 第一条**最初是一处假变异**:注入的是未声明标识符,套件 `exit 2`(编译错), > 看着也"红"。那不算证据——**断言变红才是**。上表全部用能编过、能跑起来的退化写法重做过。 --- ## 6. 主机侧测不出、必须上板的部分 如实登记,别在文档里当成已验证: - **真掉电保持**:断电 ≥5 min 再上电,`boot_count` 应 +1 且旧值保持(同时留证 BLF 与电池电压)——主机侧的"假 SRAM 不动"只是模拟,不能证明电池域真的保住了。 - **换电池**:应落 `RESTARTED` 而不是 `FRESH`。 - **复位原因对照**:IWDG 触发 → 快照 = IWDG;软复位 → SFT;掉电上电 → POR/BOR。 三者与串口那条 `reset cause:` 必须同源(同一份判定链,见 `TC-BOOTSTAT-020e`)。 - **写点不落在喂狗点**:`boot_stats_init()` 在内核启动前执行,主机侧无法验证"不占 喂狗预算",需板端结合 `[STACK]`/`task_wd` 水位看。