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,交台架道补。
375 lines
16 KiB
C
375 lines
16 KiB
C
/*
|
||
* 模块名称:开机账本(boot_stats)主机侧单元测试
|
||
* 模块功能:在 PC 上离线验证 App/util/boot_stats.c 的**上电次数四态判定、记录校验、
|
||
* I2C 失败分支(读失败绝不回写 / 写失败只少一笔账)、幂等、饱和**,
|
||
* 以及复位原因优先级链的唯一出处。
|
||
*
|
||
* 被测对象:真实源文件 App/util/boot_stats.c(#include 原样纳入)。桩:
|
||
* - stub/main.h:六个 RCC_CSR 复位标志位掩码(位号照抄 CMSIS);
|
||
* - sd2506.h / bsp_err.h / boot_stats.h / app_main.h 用**真实头**;
|
||
* - sd2506_read_sram / sd2506_write_sram / sd2506_read_ctr1 由本文件实现
|
||
* 为可注入错误的假件(含 70 B 假 SRAM,越界返 BSP_ERR_PARAM,与真驱动
|
||
* 的契约一致);g_rcc_csr_latched 也由本文件给出。
|
||
*
|
||
* 为什么这些判据必须在这儿测而不在真机上测(真机只覆盖"一切正常"那一条):
|
||
* 1. **读失败 ≠ 没有记录**:I2C 抖动一次就把计数当 0 重来,屏幕上是个很像样的小
|
||
* 数字、日志里一个字都没有;读失败还顺手回写的话,故障就从"这次读不到"升级成
|
||
* "永久少账"(004 用"假 SRAM 内容一字不许变"把这条钉住)。
|
||
* 2. **首次上电 ≠ RTC 掉过电**:合并成一态,文案就把前者冒充后者(001 vs 003)。
|
||
* 3. **只信 magic 不信校验**:坏区常是整片全 0/全 FF,但"个别字节翻转"时 magic
|
||
* 可能仍然好、次数错 256 倍(007)。
|
||
* 4. **饱和 vs 回绕**:停在 42 亿一眼看出到头;回绕成个位数会被读成"刚上电"(009)。
|
||
* 5. BLF 读不到不该把好记录判坏(012)——判定顺序写反的代价是"RTC 抖一下,
|
||
* 装置变成第一次上电"。
|
||
*
|
||
* 编号:TC-BOOTSTAT-001 起(小写子项 = 同一判据的另一面)。族需已登记进
|
||
* scripts/id_registry.json;未登记时 check_ids 校验 1 会报"未登记前缀"。
|
||
* 创建日期:2026-10-27(道G task-12)
|
||
*/
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
#include "boot_stats.h"
|
||
#include "sd2506.h" /* 真实头:bsp_err_t / SD2506_CTR1_BLF / 三个原型 */
|
||
#include "app_main.h" /* extern volatile uint32_t g_rcc_csr_latched */
|
||
|
||
/* 被测生产源(照仓内既有套件:include_dirs 里有 App/util) */
|
||
#include "boot_stats.c"
|
||
|
||
/* ---------------- g_rcc_csr_latched(真实定义在未编译的 app_main.c) ---------------- */
|
||
volatile uint32_t g_rcc_csr_latched;
|
||
|
||
/* ---------------- SD2506 假件 ---------------- */
|
||
#define FAKE_SRAM_SIZE 70u
|
||
|
||
static uint8_t s_sram[FAKE_SRAM_SIZE];
|
||
static uint8_t s_ctr1_val;
|
||
static int s_read_err; /* != BSP_OK ⇒ 注入读失败 */
|
||
static int s_write_err; /* != BSP_OK ⇒ 注入写失败 */
|
||
static int s_ctr1_err;
|
||
static int s_read_calls;
|
||
static int s_write_calls;
|
||
static int s_ctr1_calls;
|
||
|
||
static void fake_reset(void)
|
||
{
|
||
unsigned int i;
|
||
|
||
for (i = 0u; i < FAKE_SRAM_SIZE; i++) {
|
||
s_sram[i] = 0xFFu; /* 芯片出厂/掉电后的常见形态:成片全 1 */
|
||
}
|
||
s_ctr1_val = 0u;
|
||
s_read_err = BSP_OK;
|
||
s_write_err = BSP_OK;
|
||
s_ctr1_err = BSP_OK;
|
||
s_read_calls = 0;
|
||
s_write_calls = 0;
|
||
s_ctr1_calls = 0;
|
||
g_rcc_csr_latched = 0u;
|
||
/* 被测模块的静态状态也要归零——它在真实机器上"每 boot 一次",
|
||
* 测试里靠这条模拟"又开了一次机"(复位 SRAM 侧但不清假 SRAM = 跨掉电保持)。 */
|
||
(void)memset(&s_bs, 0, sizeof(s_bs));
|
||
}
|
||
|
||
/* 造一条合法记录(校验按生产同一算法算,别把算法抄第二份在这里) */
|
||
static void fake_put_count(uint32_t cnt, uint8_t ver)
|
||
{
|
||
uint8_t i;
|
||
uint8_t sum = 0u;
|
||
|
||
s_sram[0] = 0xB5u;
|
||
s_sram[1] = 0x72u;
|
||
s_sram[2] = (uint8_t)(cnt & 0xFFu);
|
||
s_sram[3] = (uint8_t)((cnt >> 8) & 0xFFu);
|
||
s_sram[4] = (uint8_t)((cnt >> 16) & 0xFFu);
|
||
s_sram[5] = (uint8_t)((cnt >> 24) & 0xFFu);
|
||
s_sram[6] = ver;
|
||
for (i = 0u; i < BS_REC_LEN - 1u; i++) {
|
||
sum = (uint8_t)(sum ^ s_sram[i]);
|
||
}
|
||
s_sram[7] = sum;
|
||
}
|
||
|
||
static uint32_t fake_get_count(void)
|
||
{
|
||
return ((uint32_t)s_sram[2]) | ((uint32_t)s_sram[3] << 8) |
|
||
((uint32_t)s_sram[4] << 16) | ((uint32_t)s_sram[5] << 24);
|
||
}
|
||
|
||
bsp_err_t sd2506_read_sram(uint8_t addr, uint8_t *p_buf, uint8_t len)
|
||
{
|
||
uint8_t i;
|
||
|
||
s_read_calls++;
|
||
if (s_read_err != BSP_OK) {
|
||
return (bsp_err_t)s_read_err;
|
||
}
|
||
if ((p_buf == 0) || (len == 0u) || (len > FAKE_SRAM_SIZE) ||
|
||
((uint16_t)addr + (uint16_t)len > FAKE_SRAM_SIZE)) {
|
||
return BSP_ERR_PARAM;
|
||
}
|
||
for (i = 0u; i < len; i++) {
|
||
p_buf[i] = s_sram[addr + i];
|
||
}
|
||
return BSP_OK;
|
||
}
|
||
|
||
bsp_err_t sd2506_write_sram(uint8_t addr, const uint8_t *p_buf, uint8_t len)
|
||
{
|
||
uint8_t i;
|
||
|
||
s_write_calls++;
|
||
if (s_write_err != BSP_OK) {
|
||
return (bsp_err_t)s_write_err;
|
||
}
|
||
if ((p_buf == 0) || (len == 0u) || (len > FAKE_SRAM_SIZE) ||
|
||
((uint16_t)addr + (uint16_t)len > FAKE_SRAM_SIZE)) {
|
||
return BSP_ERR_PARAM;
|
||
}
|
||
for (i = 0u; i < len; i++) {
|
||
s_sram[addr + i] = p_buf[i];
|
||
}
|
||
return BSP_OK;
|
||
}
|
||
|
||
bsp_err_t sd2506_read_ctr1(uint8_t *p_val)
|
||
{
|
||
s_ctr1_calls++;
|
||
if (s_ctr1_err != BSP_OK) {
|
||
return (bsp_err_t)s_ctr1_err;
|
||
}
|
||
if (p_val == 0) {
|
||
return BSP_ERR_PARAM;
|
||
}
|
||
*p_val = s_ctr1_val;
|
||
return BSP_OK;
|
||
}
|
||
|
||
/* sd2506_init 不测(属 SD2506 自己的套件),给个链接期够用的定义 */
|
||
bsp_err_t sd2506_init(void) { return BSP_OK; }
|
||
|
||
/* ---------------- 断言骨架 ---------------- */
|
||
static int s_total;
|
||
static int s_passed;
|
||
static int s_failed;
|
||
|
||
#define CHECK(cond, name) \
|
||
do { \
|
||
s_total++; \
|
||
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
|
||
else { s_failed++; printf(" [FAIL] %s\n", name); } \
|
||
} while (0)
|
||
|
||
static boot_stats_t snap(void)
|
||
{
|
||
boot_stats_t b;
|
||
|
||
boot_stats_get(&b);
|
||
return b;
|
||
}
|
||
|
||
/* ===================== 上电次数四态 ===================== */
|
||
static void tc_seq_states(void)
|
||
{
|
||
boot_stats_t b;
|
||
uint8_t before[BS_REC_LEN];
|
||
|
||
/* 001 首次启用:SRAM 成片全 1(magic 过不去)+ RTC 没报欠压 ⇒ FRESH,不是 RESTARTED */
|
||
fake_reset();
|
||
boot_stats_init();
|
||
b = snap();
|
||
CHECK((b.ready == 1u) && (b.seq_state == BOOT_STATS_SEQ_FRESH) && (b.boot_count == 1u)
|
||
&& (b.write_failed == 0u) && (s_write_calls == 1) && (fake_get_count() == 1u),
|
||
"TC-BOOTSTAT-001 blank SRAM without BLF starts the ledger at 1 and marks it FRESH, not RESTARTED");
|
||
|
||
/* 002 延续:合法记录 count=7 ⇒ count=8,且回写后介质里也是 8 */
|
||
fake_reset();
|
||
fake_put_count(7u, BS_REC_VER);
|
||
boot_stats_init();
|
||
b = snap();
|
||
CHECK((b.seq_state == BOOT_STATS_SEQ_CONTINUED) && (b.boot_count == 8u)
|
||
&& (fake_get_count() == 8u),
|
||
"TC-BOOTSTAT-002 a valid record continues the count and persists the incremented value");
|
||
|
||
/* 002b 跨掉电保持的本体:假 SRAM 不清、只重开被测模块(等价于"又上电一次") */
|
||
(void)memset(&s_bs, 0, sizeof(s_bs));
|
||
boot_stats_init();
|
||
b = snap();
|
||
CHECK((b.seq_state == BOOT_STATS_SEQ_CONTINUED) && (b.boot_count == 9u),
|
||
"TC-BOOTSTAT-002b re-initialising over an intact medium keeps counting up rather than restarting at 1");
|
||
|
||
/* 003 电池域掉过电:记录无效 + BLF 置位 ⇒ RESTARTED(与 001 是两件事) */
|
||
fake_reset();
|
||
s_ctr1_val = SD2506_CTR1_BLF;
|
||
boot_stats_init();
|
||
b = snap();
|
||
CHECK((b.seq_state == BOOT_STATS_SEQ_RESTARTED) && (b.boot_count == 1u) && (b.rtc_blf == 1u),
|
||
"TC-BOOTSTAT-003 an invalid record with the battery-low flag set is reported as RESTARTED, never as first power-on");
|
||
|
||
/* 004 I2C 读失败 ⇒ 不可信,且**绝不回写**(把可能完好的记录覆盖掉是不可接受的) */
|
||
fake_reset();
|
||
fake_put_count(41u, BS_REC_VER);
|
||
(void)memcpy(before, s_sram, BS_REC_LEN);
|
||
s_read_err = BSP_ERR_TIMEOUT;
|
||
boot_stats_init();
|
||
b = snap();
|
||
CHECK((b.seq_state == BOOT_STATS_SEQ_UNKNOWN) && (b.boot_count == 0u)
|
||
&& (s_write_calls == 0) && (memcmp(s_sram, before, BS_REC_LEN) == 0),
|
||
"TC-BOOTSTAT-004 an I2C read failure marks the ledger untrusted and writes nothing back, leaving the stored record byte-identical");
|
||
|
||
/* 004b 读不到时页面要能分辨"不可信"与"零次":同一张表里 ready=1 且 seq=UNKNOWN */
|
||
CHECK((b.ready == 1u) && (b.seq_state == BOOT_STATS_SEQ_UNKNOWN),
|
||
"TC-BOOTSTAT-004b the untrusted state is distinguishable from both a real count and an uninitialised table");
|
||
|
||
/* 005 写失败 ⇒ 本次仍报数,但打上"少一笔账"的记号(下次开机读旧值,只会少不会重) */
|
||
fake_reset();
|
||
s_write_err = BSP_ERR_IO;
|
||
boot_stats_init();
|
||
b = snap();
|
||
CHECK((b.boot_count == 1u) && (b.write_failed == 1u) && (s_write_calls == 1),
|
||
"TC-BOOTSTAT-005 a failed write-back still reports this boot but flags it, so the ledger can only lose one count");
|
||
|
||
/* 006 幂等:bring-up 重试不得重复计数(真实机器上 sd2506 之后可能重进一次) */
|
||
fake_reset();
|
||
boot_stats_init();
|
||
boot_stats_init();
|
||
b = snap();
|
||
CHECK((b.boot_count == 1u) && (s_write_calls == 1),
|
||
"TC-BOOTSTAT-006 a second init in the same boot neither counts twice nor writes twice");
|
||
|
||
/* 007 校验和不符(magic 与版本都好,中段漂了一位)⇒ 不当真账读 */
|
||
fake_reset();
|
||
fake_put_count(9u, BS_REC_VER);
|
||
s_sram[4] = (uint8_t)(s_sram[4] ^ 0x01u); /* 高位字节翻转:256 倍的错账 */
|
||
boot_stats_init();
|
||
b = snap();
|
||
CHECK((b.seq_state == BOOT_STATS_SEQ_FRESH) && (b.boot_count == 1u),
|
||
"TC-BOOTSTAT-007 a single flipped byte that leaves the magic intact is caught by the checksum instead of reporting a count off by 256");
|
||
|
||
/* 008 格式版本不符(将来改了布局)⇒ 整条作废,且回写时落回当前版本 */
|
||
fake_reset();
|
||
fake_put_count(1234u, 2u);
|
||
boot_stats_init();
|
||
b = snap();
|
||
CHECK((b.seq_state == BOOT_STATS_SEQ_FRESH) && (b.boot_count == 1u)
|
||
&& (s_sram[6] == BS_REC_VER) && (s_sram[7] == s_rec_sum(s_sram)),
|
||
"TC-BOOTSTAT-008 a record from a future layout is rejected outright and rewritten under the current version with a fresh checksum");
|
||
|
||
/* 009 饱和:到顶不再自增也不再写(回绕成个位数会被读成"刚上电") */
|
||
fake_reset();
|
||
fake_put_count(0xFFFFFFF0u, BS_REC_VER);
|
||
boot_stats_init();
|
||
b = snap();
|
||
CHECK((b.boot_count == 0xFFFFFFF0u) && (s_write_calls == 0) && (fake_get_count() == 0xFFFFFFF0u),
|
||
"TC-BOOTSTAT-009 the counter saturates at the ceiling and stops writing instead of wrapping to a small number");
|
||
|
||
/* 010 BLF 读不到但记录有效 ⇒ 仍按延续判(读不到佐证不该把好账判坏) */
|
||
fake_reset();
|
||
fake_put_count(20u, BS_REC_VER);
|
||
s_ctr1_err = BSP_ERR_IO;
|
||
boot_stats_init();
|
||
b = snap();
|
||
CHECK((b.seq_state == BOOT_STATS_SEQ_CONTINUED) && (b.boot_count == 21u) && (b.rtc_blf == 0u),
|
||
"TC-BOOTSTAT-010 an unreadable battery flag never invalidates an otherwise sound record");
|
||
|
||
/* 011 出厂全 0 的 SRAM(另一形态:不是全 FF)⇒ FRESH 且第一格就是 1,不报 0 次 */
|
||
fake_reset();
|
||
(void)memset(s_sram, 0, sizeof(s_sram));
|
||
boot_stats_init();
|
||
b = snap();
|
||
CHECK((b.seq_state == BOOT_STATS_SEQ_FRESH) && (b.boot_count == 1u),
|
||
"TC-BOOTSTAT-011 an all-zero SRAM (the other erase pattern) starts the ledger at one rather than showing zero boots");
|
||
}
|
||
|
||
/* ===================== 复位原因链 ===================== */
|
||
static uint8_t is_ascii(const char *p)
|
||
{
|
||
while (*p != '\0') {
|
||
if (((unsigned char)*p) > 127u) {
|
||
return 0u;
|
||
}
|
||
p++;
|
||
}
|
||
return 1u;
|
||
}
|
||
|
||
static void tc_cause_chain(void)
|
||
{
|
||
/* 020 优先级唯一出处:IWDG 压过软件复位(崩溃钩子常以 NVIC_SystemReset 收尾,
|
||
* 两个标志同时置位时报看门狗才是我们要看的自动恢复事件) */
|
||
CHECK(boot_stats_cause_from_csr(RCC_CSR_IWDGRSTF | RCC_CSR_SFTRSTF) == BOOT_STATS_CAUSE_IWDG,
|
||
"TC-BOOTSTAT-020 the independent watchdog outranks a software reset when both flags are set");
|
||
|
||
/* 020b WWDG 压过软件复位;单独的软件复位仍归软件 */
|
||
CHECK((boot_stats_cause_from_csr(RCC_CSR_WWDGRSTF | RCC_CSR_SFTRSTF) == BOOT_STATS_CAUSE_WWDG)
|
||
&& (boot_stats_cause_from_csr(RCC_CSR_SFTRSTF) == BOOT_STATS_CAUSE_SFT),
|
||
"TC-BOOTSTAT-020b the window watchdog likewise outranks a software reset while a lone software reset stays itself");
|
||
|
||
/* 020c POR 压过 PIN、PIN 压过 BOR;全零 ⇒ UNKNOWN */
|
||
CHECK((boot_stats_cause_from_csr(RCC_CSR_PORRSTF | RCC_CSR_PINRSTF) == BOOT_STATS_CAUSE_POR)
|
||
&& (boot_stats_cause_from_csr(RCC_CSR_PINRSTF | RCC_CSR_BORRSTF) == BOOT_STATS_CAUSE_PIN)
|
||
&& (boot_stats_cause_from_csr(RCC_CSR_BORRSTF) == BOOT_STATS_CAUSE_BOR)
|
||
&& (boot_stats_cause_from_csr(0u) == BOOT_STATS_CAUSE_UNKNOWN),
|
||
"TC-BOOTSTAT-020c power-on beats pin which beats brown-out, and no flag at all stays UNKNOWN rather than guessing");
|
||
|
||
/* 020d 每一格编码都有可读串、全是纯 ASCII(无 BOM 文件里字符串必须 ASCII),
|
||
* 且未知编码回落 unknown 而不是断言——这条链跑在开机极早期,不能挡启动 */
|
||
CHECK((boot_stats_cause_str(BOOT_STATS_CAUSE_UNKNOWN) != 0)
|
||
&& (boot_stats_cause_str(BOOT_STATS_CAUSE_BOR) != 0)
|
||
&& (strcmp(boot_stats_cause_str(BOOT_STATS_CAUSE_IWDG), "IWDG (independent watchdog)") == 0)
|
||
&& (strcmp(boot_stats_cause_str(200u), "unknown") == 0)
|
||
&& is_ascii(boot_stats_cause_str(BOOT_STATS_CAUSE_SFT)),
|
||
"TC-BOOTSTAT-020d every code maps to a pure-ASCII description and an out-of-range code falls back to unknown instead of blocking startup");
|
||
|
||
/* 020e 快照:开机那次读的就是锁存影子里的复位原因(与串口那条同源同序) */
|
||
fake_reset();
|
||
g_rcc_csr_latched = RCC_CSR_IWDGRSTF;
|
||
boot_stats_init();
|
||
CHECK(snap().rst_cause == BOOT_STATS_CAUSE_IWDG,
|
||
"TC-BOOTSTAT-020e the boot snapshot exposes the latched RCC_CSR cause so the screen and the serial line can never disagree");
|
||
}
|
||
|
||
/* ===================== 读口契约 ===================== */
|
||
static void tc_get_contract(void)
|
||
{
|
||
/* 030 未 init:整表零、ready=0(页面该显示占位而不是"0 次") */
|
||
fake_reset();
|
||
CHECK((snap().ready == 0u) && (snap().boot_count == 0u),
|
||
"TC-BOOTSTAT-030 before init the table reads zero with ready clear so a page can show a placeholder instead of zero boots");
|
||
|
||
/* 030b NULL 无害(观测口在任何上下文都不该因为调用者手滑而炸) */
|
||
boot_stats_get(0);
|
||
CHECK(1, "TC-BOOTSTAT-030b a null output pointer is a harmless no-op");
|
||
|
||
/* 030c 读口不碰总线:init 之后再读 100 次,I2C 调用数纹丝不动
|
||
* ——「任意上下文可读」不是口头承诺,是这条计数断言 */
|
||
fake_reset();
|
||
boot_stats_init();
|
||
s_read_calls = 0;
|
||
s_write_calls = 0;
|
||
s_ctr1_calls = 0;
|
||
{
|
||
int i;
|
||
for (i = 0; i < 100; i++) {
|
||
(void)snap();
|
||
}
|
||
}
|
||
CHECK((s_read_calls == 0) && (s_write_calls == 0) && (s_ctr1_calls == 0),
|
||
"TC-BOOTSTAT-030c the getter touches neither I2C nor the clock, so reading the ledger cannot become part of a fault");
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
printf("=== Boot Stats Tests ===\n");
|
||
printf("-- TC-BOOTSTAT-001..011 power-on ledger (four states, checksum, I2C failures, idempotence, saturation) --\n");
|
||
tc_seq_states();
|
||
printf("-- TC-BOOTSTAT-020..020e reset cause chain (single source of the priority order) --\n");
|
||
tc_cause_chain();
|
||
printf("-- TC-BOOTSTAT-030..030c read-only contract --\n");
|
||
tc_get_contract();
|
||
/* 分子必须是"真过了几条":写成 s_total/s_total 会出现
|
||
* 「21/21 PASSED (failed=1)」这种自相矛盾的汇总行——人只看前半就放行了。 */
|
||
printf("\n=== Boot Stats Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed);
|
||
return (s_failed == 0) ? 0 : 1;
|
||
}
|