/* * 模块名称:开机账本(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 #include #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; }