Files
wandering1 728d60675f feat(UI/采集/104): 通讯设置页 + 开机账本 + adc_cal 可诊断化——三车道同树一次构建入库
三条道在同一棵树上交付,故合并成**一次权威构建**(@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,交台架道补。
2026-10-27 11:29:23 +08:00

375 lines
16 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:开机账本(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;
}