fix(protect): 采样陈旧时独立闭锁保护(判定层失明的兜底门)

问题:L5-4 闭锁读的 adc_health_flags() 只有 adcTask 每拍跑
adc_health_check() 才可能置位。TIM3 一停,adcTask 就永久阻塞在
osThreadFlagsWait(刻意的 fail-safe),check() 不再执行 → stall 位
永远是 0,而 protectTask 仍每 20 ms 拿冻在最后一帧的相量继续判据。
窗口最坏约 800 ms(停喂阈值)+ 2498 ms(IWDG 标称)≈ 3.3 s。

方案:保护侧建一条与消费者存活完全解耦的兜底新鲜度门。
- adc_health.c/h 新增 adc_health_age_stale():无模块状态的纯判据,
  参照系 adc_health_age_t 由调用方自存。故意不让 protectTask 去调
  adc_health_check()——那是「每拍一次、单写者」的契约,两个上下文
  同写会互相刷新差值基线,把现有判定层一起弄坏。
- 新增 ADC_HF_STALE = bit8(与判定层 bit0..bit7 位段错开)。本位
  不由判定层写、不进 sticky、不进编排层 logged 位图。
- protect_task.c 用自己的时基直接读两个**中断侧递增**的计数器
  (tpafe5160_frame_seq / adc_input_stm32_seq),并入掩码。两条车道
  各自一份 primed:共用会让先观测的那条把标志置 1,另一条首拍拿
  adv_ms = 0 算年龄 → 开机即误锁保护。
- 门限 PROT_STALE_AGE_MS = 100 + 20 = 120 ms,买的是时序阶梯:
  判定层活着时它先报 0x1/0x2(带日志与自检留痕),死了只出现
  0x100。闭锁最迟 140 ms,仍远小于最小动作延时(默认 1000 ms)。

实测:
- @build 0 错 0 警:Code 213424 → 213520(+96 B);ZI 131480 →
  131504(+24 B = 两份 age 结构);RW-data 1184 不变
- stack_report:protectTask 静态最坏链 872 B 不变(余 1176 B / 57%)、
  adcTask 1320 B 不变——新判据无日志,不进最坏链
- 主机单测 45/45(新增 TC-ADCHEALTH-012..017 共 14 条断言:首拍只建
  基线 / 超拍不判老 / 门限含等号 / 跨 tick 回绕 / 倒退按分工不判老 /
  双观察者互不污染 / 空参数不判老)
- 台架注入(一次性停 TIM3 300 ms 探针,验完已撤;探针固件与最终
  固件 Program Size 逐字节相同):
  [+5462ms] [ERR][PROT] sampling FAULT -> protection blocked (hf=0x00000100)
  [+5757ms] [PROT] sampling recovered -> protection resumed
  同次开机 [ADC_TASK] health: 出现 0 次;MISS 快照缺的正是 bit6
  (0x1F7 - 0x1B7 = 0x40,实证 adcTask 当时确实阻塞,不是"在跑但没报");
  reset cause: 仅 1 次(300 ms 被 800 ms 容忍吸收,闭锁不靠复位)
  证据 docs/测试说明/boot_log_data/20261021_probe_stale_gate_boot_log.txt
- 字节卫生:BOM 无 / CR 0 / U+FFFD 0;新增行无非法字面量

gate: only mem-budget/mem-numbers pending lead ledger backfill
pre-commit run --all-files 失败仅这两条(台账受管快照与本树 .map 的
Flash/RAM 水位漂移,即本次 +96 B Code、+24 B ZI)。按并行分支约定
本分支不跑 --write,台账由 Lead 合并后统一回填。

文档同步:设计说明新增 §15(原相关文档改 §16),§7 位表加 bit8 行、
§8 责任矩阵与「已知窗口」段改写为已闭合、§10 盲区第 3 条、§11 用例
表、§12 判读手册、§14 改前清单全部跟上;位图配图补 bit8 列,并修掉
生成器把列数硬编码成 8 列导致新列被备注框盖住的问题。
This commit is contained in:
2026-10-21 23:54:01 +08:00
parent 60a650c175
commit 9f826dbb4d
9 changed files with 718 additions and 199 deletions
+131
View File
@@ -25,6 +25,12 @@
* TC-ADCHEALTH-009 tick 回绕:跨 0xFFFFFFFF 的正常推进不误报静默;回绕后冻结仍报
* TC-ADCHEALTH-010 NULL 守卫:in=NULL 跳过;out=NULL 只更新内部状态
* TC-ADCHEALTH-011 init 幂等:故障后 re-init 全清
* TC-ADCHEALTH-012 帧年龄纯判据:首拍只建基线、不判老(开机不得误锁)
* TC-ADCHEALTH-013 帧年龄纯判据:正常推进恒不判老,一次前进 3 帧也不判老
* TC-ADCHEALTH-014 帧年龄纯判据:门限含等号(差 1 不判、到点判),恢复前进即清
* TC-ADCHEALTH-015 帧年龄纯判据:tick 与序号跨 0xFFFFFFFF 不误判、停滞仍判老
* TC-ADCHEALTH-016 帧年龄纯判据:序号倒退按「有变化」处理,不判老(判据分工)
* TC-ADCHEALTH-017 帧年龄纯判据:两个 age 对象互不干扰;空指针不判老
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
@@ -349,6 +355,125 @@ static void tc_reinit(void) {
"TC-ADCHEALTH-011 re-prime after re-init is clean");
}
/* ===================== 帧年龄纯判据(保护侧兜底门专用) ===================== */
/* 构造一个"未建基线"的参照系(本测试只用 stdint/stdio,不引 string.h) */
static adc_health_age_t mk_age(void) {
adc_health_age_t a;
a.seq = 0u;
a.adv_ms = 0u;
a.primed = 0u;
return a;
}
/* TC-ADCHEALTH-012:首拍只建基线 */
static void tc_age_prime(void) {
adc_health_age_t age = mk_age();
CHECK(adc_health_age_stale(&age, 4242u, 1000u, 120u) == 0u,
"TC-ADCHEALTH-012 first observation only primes");
CHECK((age.primed == 1u) && (age.seq == 4242u) && (age.adv_ms == 1000u),
"TC-ADCHEALTH-012 baseline recorded by first call");
CHECK(adc_health_age_stale(&age, 4242u, 1001u, 120u) == 0u,
"TC-ADCHEALTH-012 1ms idle is not stale");
}
/* TC-ADCHEALTH-013:正常推进与超拍都不该判老 */
static void tc_age_healthy(void) {
adc_health_age_t age = mk_age();
uint32_t seq = 0u, now = 0u;
int i, ok = 1;
for (i = 0; i < 2000; i++) {
if (adc_health_age_stale(&age, seq, now, 120u) != 0u) {
ok = 0;
}
seq += ((i % 7) == 6) ? 3u : 1u; /* 每 7 拍造一次「前进 3 帧」的超拍 */
now += 1u;
}
CHECK(ok, "TC-ADCHEALTH-013 advancing (incl. d=3 overrun) never goes stale");
CHECK(now > 120u, "TC-ADCHEALTH-013 sweep exceeded the threshold span");
}
/* TC-ADCHEALTH-014:门限含等号 + 恢复即清 */
static void tc_age_threshold(void) {
adc_health_age_t age = mk_age();
uint32_t t;
int ok = 1;
(void)adc_health_age_stale(&age, 10u, 0u, 120u); /* 建基线 */
(void)adc_health_age_stale(&age, 11u, 10u, 120u); /* 前进:adv_ms = 10 */
for (t = 11u; t < 130u; t++) {
uint8_t want = ((t - 10u) >= 120u) ? 1u : 0u;
if (adc_health_age_stale(&age, 11u, t, 120u) != want) {
ok = 0;
}
}
CHECK(ok, "TC-ADCHEALTH-014 stale exactly at age_ms (inclusive boundary)");
CHECK(adc_health_age_stale(&age, 12u, 200u, 120u) == 0u,
"TC-ADCHEALTH-014 advancing again clears staleness");
}
/* TC-ADCHEALTH-015:tick 与序号一起跨回绕 */
static void tc_age_wrap(void) {
adc_health_age_t age = mk_age();
uint32_t seq = 700000u, now = 0xFFFFFF00u;
int i, ok = 1;
for (i = 0; i < 512; i++) {
if (adc_health_age_stale(&age, seq, now, 120u) != 0u) {
ok = 0;
}
seq++; now++; /* uint32 自然回绕 */
}
CHECK(ok, "TC-ADCHEALTH-015 sweep across 0xFFFFFFFF never goes stale");
seq--; /* 回到最后一次被观测到的值:此后两侧冻结 */
for (i = 0; i < 121; i++) {
now++;
}
CHECK(adc_health_age_stale(&age, seq, now, 120u) != 0u,
"TC-ADCHEALTH-015 still detected stale after wrap");
}
/* TC-ADCHEALTH-016:倒退按「有变化」处理,不判老 */
static void tc_age_backward(void) {
adc_health_age_t age = mk_age();
(void)adc_health_age_stale(&age, 1000u, 100u, 120u);
CHECK(adc_health_age_stale(&age, 995u, 101u, 120u) == 0u,
"TC-ADCHEALTH-016 backward counts as change, not staleness");
/* 倒退已把参照系重新对齐到 995(adv_ms = 101):此后停在 995 上,
* 差 1 ms 不判老、到点才判老——把"重对齐后计时起点跟着走"钉住 */
CHECK(adc_health_age_stale(&age, 995u, 220u, 120u) == 0u,
"TC-ADCHEALTH-016 119ms after re-alignment is not stale yet");
CHECK(adc_health_age_stale(&age, 995u, 221u, 120u) != 0u,
"TC-ADCHEALTH-016 baseline re-aligned by backward step");
}
/* TC-ADCHEALTH-017:两个观察者互不干扰 + 空指针守卫 */
static void tc_age_two_observers(void) {
adc_health_age_t a = mk_age(); /* 模拟 adcTask:每拍观测 */
adc_health_age_t b = mk_age(); /* 模拟 protectTask:每 20 拍观测 */
uint32_t seq = 500u;
uint32_t t;
int ok = 1;
(void)adc_health_age_stale(&a, seq, 0u, 100u);
(void)adc_health_age_stale(&b, seq, 0u, 120u);
for (t = 1u; t <= 200u; t++) {
(void)adc_health_age_stale(&a, seq + t, t, 100u);
if ((t % 20u) == 0u) {
if (adc_health_age_stale(&b, seq + t, t, 120u) != 0u) {
ok = 0;
}
}
}
CHECK(ok, "TC-ADCHEALTH-017 two independent observers do not pollute each other");
CHECK(adc_health_age_stale(0, 1u, 1u, 120u) == 0u,
"TC-ADCHEALTH-017 NULL age never reports stale (no false lock)");
}
int main(void) {
printf("=== ADC Health Tests ===\n");
@@ -364,6 +489,12 @@ int main(void) {
tc_backward();
tc_err_counters();
printf("-- tick wrap / guards / re-init --\n");
tc_age_prime();
tc_age_healthy();
tc_age_threshold();
tc_age_wrap();
tc_age_backward();
tc_age_two_observers();
tc_tick_wrap();
tc_null_guard();
tc_reinit();