Files
STM32F4-Base/test/host/adc_health/adc_health_test.c
T
wandering1 94b0d79026 feat(ui)+chore(docs): UI 补齐清单清空;数据可信销项与「卡」号清退
本笔含两组收尾工作(同一工作区内交错在 6 个共享文档/门禁配置里,
拆开会产生"文档声称的页数与树里黄金数不一致"的中间提交,故合为一笔):

一、UI 补齐(清单清空)
- 新建 6 个内容页模块:ui_page_accident / oper / alarm / start / yk
  复用 ui_w_records 共享组件(分页、空态"无记录"、按实例读盘缓存),
  记录层与五个写入点已由道F task-10 / 道G task-12 落地,本批只补页面;
  ui_page_runreport 为 13 行 2 页只读统计,四个数逐个挂范围标签
  (自电池周期 / 本次开机),boot_ready=0 显 "--" 不显 0。
- 数据面:ui_data_info_t 尾部追加 9 字段,ui_data_feed 用
  boot_stats_get() + iec104_serv_get_stats() 纯搬运;模拟器填确定值假数据
  (差异登记 docs/测试说明/模拟器差距清单.md §2.3 两行)。
- 接线:菜单树 6 叶绑 #page=,绑定叶 22→28、UI_PAGE_MAP_COUNT=28;
  字库 185→218 字 / 280→313 字形;双端注册 uvprojx +30、CMakeLists +6。
- 回归:内容页黄金 53→60(含 runreport_p2 真拍第 2 页),文本断言
  425 正向 / 36 反向;存量 53 张逐字节零漂移;记录 5 页各用独立空目录
  (模拟器把操作留痕写进 cwd 的 sim_oper.log,共用 tmp 会让空态拍不到)。

二、数据可信销项 + 「数据可信」任务卡号整族清退
- 删除已闭环的"已知电压注入验证"行与闭环结论块(证据在
  docs/硬件平台/ADC通道映射说明.md §8.1 与
  docs/测试说明/ADC前端注入验证流程.md §5~§7),§5 标识列改语义短语。
- 卡 0/0b/1~12 共 14 个号全仓 288 站点 / 56 文件清退成语义短语,
  代码注释、测试、门禁配置与文档一并改;换算表入 docs/编号规范.md §9.3。
  这一族是中文前缀,不进 check_ids 语料,刻意不入 id_aliases.json。

验证(实测)
- @build 0 错 0 警:Code=236456 RO=28124 RW=1876 ZI=137084;
  RAM +464 B(RW +304 / ZI +160),RW_IRAM1 62.2% / RW_CCM 87.9%,
  体积同码凭证与 RAM 台账已回填。
- check_menu_rows 8 状态 rc=0(池 5368/5448 常量未动);
  check_content_pages 60 页 rc=0;check_font_charset / registration /
  coding-standard / bom-drift / mojibake / doc-links --all / layer-deps /
  nested-comment 全 rc=0;check_ids --strict 无错误;主机单测
  ui_data 295 / boot_stats 21 / sys_log_mcu 141 / soe 32 / soe_store 27。

未做(不是遗漏)
- 六页真机判据(真记录翻页、掉电保持、清除后归零、boot_count 自增、
  拔 RTC 电池落 RESTARTED、IWDG 复位落 rst_cause、主站连一次 conn_up+1)
  已登记 docs/任务规划/登记簿.md §7;模拟器结构上照不到。
- docs/产品需求/产品需求设计文档.md 属另一条在飞改动,本笔不含;
  其中我已同步 6 处现状数字(28 叶 / 60 页 / 218 字),随该笔入库。
2026-10-28 07:23:16 +08:00

620 lines
24 KiB
C
Raw 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.
/*
* 模块名称:ADC 采样有效性校验主机侧单元测试
* 模块功能:在 PC 上对 App/algo/adc_health.c 做离线验证。
*
* 被测对象:真实源文件(以 #include 原样纳入,保证测的是生产代码而非副本):
* ../../../App/algo/adc_health.c
* 本层为**纯逻辑**(仅 stdint.h),**无需任何桩**。
*
* 为什么要主机侧单测(而不是只用板端闭环):
* 1. 静默/倒退/错误增量在健康台架上**不可复现**(复现 = 拔芯片/改驱动),
* 板端只能证明"健康时不误报",证不了"故障时必报";
* 2. 边界(恰好 100ms 门限 / tick 回绕 / 序号倒退后再前进 / 事件只发一拍)
* 只有合成计数序列能精确构造;
* 3. 不占 MCU 的 RAM,毫秒级完成。
*
* 覆盖点(对应 docs/算法/继电保护功能复现规划.md §3.1 采样有效性校验):
* TC-ADCHEALTH-001 init + 首拍只建基线(不误报)
* TC-ADCHEALTH-002 正常 1 kHz 推进 2000 拍:零故障零事件(健康不误报)
* TC-ADCHEALTH-003 TPAFE 静默:99ms 不报 / 100ms 报 / 恢复清除电平位、锁存位留痕
* TC-ADCHEALTH-004 STM32 静默:TPAFE 正常时只报 STM32 侧
* TC-ADCHEALTH-005 TPAFE 丢帧:d=3 → tgap+=2、事件仅当拍(边沿语义)
* TC-ADCHEALTH-006 STM32 gap 计数增量 → 事件(不入锁存:超拍非故障)
* TC-ADCHEALTH-007 序号倒退 → SEQ_BACKWARD 锁存(前进后仍留痕)
* TC-ADCHEALTH-008 FRSTDATA / STM32_ERR / HW_ERR 增量 → 事件位与锁存位编号一致
* 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 对象互不干扰;空指针不判老
* —— 采样通道自检 残余「码值越界(贴轨)检出」(2026-10-25 道A,标定折算落地分支加入)——
* TC-ADCHEALTH-018 释放沿后连续 20 拍贴上轨 -> 锁存 + 边沿只报一次
* TC-ADCHEALTH-019 无释放沿(开机/空载恒 0)永不锁存(合法工况不误报)
* TC-ADCHEALTH-020 中带样本清零连续计数(19+19 不锁);触轨含等号边界
* TC-ADCHEALTH-021 上下轨交替互清对侧计数,永不锁存
* TC-ADCHEALTH-022 下轨带对称锁存(STM32 削顶 = 贴底签名)
* TC-ADCHEALTH-023 rail_init 清 sticky 与释放沿记录;health_init 内含复位;
* 通道越界忽略不污染
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-11
*/
#include <stdint.h>
#include <stdio.h>
/* ===================== 断言 ===================== */
static int s_total = 0, s_passed = 0, s_failed = 0;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
/* ===================== 被测代码 ===================== */
/* 原样纳入生产源文件(纯逻辑,无需桩) */
#include "adc_health.c"
/* ===================== 工具 ===================== */
/* 构造一份"健康"计数快照模板:两侧序号同拍号 seq,错误计数全 0 */
static adc_health_in_t mk_in(uint32_t seq, uint32_t now_ms) {
adc_health_in_t in;
in.tpafe_seq = seq;
in.tpafe_frst_err = 0u;
in.stm32_seq = seq;
in.stm32_errs = 0u;
in.stm32_gaps = 0u;
in.stm32_hw_errs = 0u;
in.now_ms = now_ms;
return in;
}
/* ===================== TC ===================== */
/* TC-ADCHEALTH-001:init + 首拍只建基线 */
static void tc_prime(void) {
adc_health_in_t in;
adc_health_status_t st;
adc_health_init();
in = mk_in(12345u, 1000u); /* 开机时序号已非 0 */
adc_health_check(&in, &st);
CHECK((st.flags == 0u) && (st.events == 0u) && (st.sticky == 0u) &&
(st.tpafe_gaps == 0u),
"TC-ADCHEALTH-001 first check only primes: no flags/events/sticky");
/* 第二拍序号未动但仅 1ms(< 门限)→ 仍不报 */
in.now_ms = 1001u;
adc_health_check(&in, &st);
CHECK(st.flags == 0u, "TC-ADCHEALTH-001 1ms without advance is not a stall");
}
/* TC-ADCHEALTH-002:正常 1 kHz 推进 2000 拍零误报 */
static void tc_healthy(void) {
adc_health_status_t st;
adc_health_in_t in;
uint32_t seq = 0u, now = 0u;
int i, ok = 1;
adc_health_init();
for (i = 0; i < 2000; i++) {
in = mk_in(seq, now);
adc_health_check(&in, &st);
if ((st.flags != 0u) || (st.events != 0u) || (st.sticky != 0u)) {
ok = 0;
}
seq++;
now++;
}
CHECK(ok, "TC-ADCHEALTH-002 healthy 1kHz sweep 2000 beats: zero flags/events");
}
/* TC-ADCHEALTH-003:TPAFE 静默 99/100ms 门限 + 恢复留痕 */
static void tc_tpafe_stall(void) {
adc_health_status_t st;
adc_health_in_t in;
uint32_t seq = 500u, now = 500u;
int i;
adc_health_init();
/* 先跑 10 拍健康节拍建立"前进中"基线 */
for (i = 0; i < 10; i++) {
in = mk_in(seq, now);
adc_health_check(&in, &st);
seq++; now++;
}
/* 仅 TPAFE 冻结(STM32 继续走):前 100 拍(elapsed 0..99ms)不报。
* 注意第 1 拍会消费掉 healthy 循环末尾"未消费的那次 +1",
* 故静默计时从 now=510 起算,第 i 拍 elapsed = i。 */
for (i = 0; i < 100; i++) {
in = mk_in(seq, now);
in.stm32_seq = seq + (uint32_t)i + 1u; /* STM32 侧照常前进 */
adc_health_check(&in, &st);
now++;
}
CHECK((st.flags & ADC_HF_TPAFE_STALL) == 0u,
"TC-ADCHEALTH-003 no TPAFE stall before 100ms");
/* 第 100ms:报 */
in = mk_in(seq, now);
in.stm32_seq = seq + 100u;
adc_health_check(&in, &st);
CHECK((st.flags & ADC_HF_TPAFE_STALL) != 0u,
"TC-ADCHEALTH-003 TPAFE stall at 100ms");
CHECK((st.sticky & ADC_HF_TPAFE_STALL) != 0u,
"TC-ADCHEALTH-003 stall latched into sticky");
CHECK(st.tpafe_stall_ms >= ADC_HEALTH_STALL_MS,
"TC-ADCHEALTH-003 stall_ms reported >= threshold");
/* 恢复:TPAFE 序号重新前进 → 电平位清除、sticky 留痕 */
in = mk_in(seq + 1u, now + 1u);
in.stm32_seq = seq + 101u;
adc_health_check(&in, &st);
CHECK((st.flags & ADC_HF_TPAFE_STALL) == 0u,
"TC-ADCHEALTH-003 stall level clears on recovery");
CHECK((adc_health_sticky() & ADC_HF_TPAFE_STALL) != 0u,
"TC-ADCHEALTH-003 sticky keeps the fault trace after recovery");
}
/* TC-ADCHEALTH-004:STM32 静默(TPAFE 正常) */
static void tc_stm32_stall(void) {
adc_health_status_t st;
adc_health_in_t in;
uint32_t seq = 100u, now = 100u;
int i;
adc_health_init();
for (i = 0; i < 10; i++) {
in = mk_in(seq, now);
adc_health_check(&in, &st);
seq++; now++;
}
/* STM32 冻结(TPAFE 继续走)。⚠ 第 1 拍消费 healthy 循环末尾的"未消费 +1"
* 并把静默计时起点重置为该拍 now → 第 i 拍 elapsed = i,需 101 拍才到 100ms */
for (i = 0; i < 101; i++) {
in = mk_in(seq + (uint32_t)i + 1u, now + (uint32_t)i + 1u);
in.stm32_seq = seq; /* 冻结在旧值 */
adc_health_check(&in, &st);
}
CHECK((st.flags & ADC_HF_STM32_STALL) != 0u,
"TC-ADCHEALTH-004 STM32 stall at 100ms");
CHECK((st.flags & ADC_HF_TPAFE_STALL) == 0u,
"TC-ADCHEALTH-004 healthy TPAFE not falsely stalled");
}
/* TC-ADCHEALTH-005:TPAFE 丢帧计数 + 边沿语义 */
static void tc_tpafe_gap(void) {
adc_health_status_t st;
adc_health_in_t in;
adc_health_init();
in = mk_in(10u, 10u);
adc_health_check(&in, &st); /* 基线 */
in.tpafe_seq = 13u; in.stm32_seq = 11u; in.now_ms = 11u; /* TPAFE d=3 */
adc_health_check(&in, &st);
CHECK((st.events & ADC_HE_TPAFE_GAP) != 0u,
"TC-ADCHEALTH-005 gap event on d>1");
CHECK(st.tpafe_gaps == 2u, "TC-ADCHEALTH-005 d=3 accumulates 2 lost points");
in.tpafe_seq = 14u; in.stm32_seq = 12u; in.now_ms = 12u; /* 恢复正常 d=1 */
adc_health_check(&in, &st);
CHECK(st.events == 0u, "TC-ADCHEALTH-005 gap event is edge-only (next beat clean)");
CHECK(st.tpafe_gaps == 2u, "TC-ADCHEALTH-005 gap counter is cumulative");
}
/* TC-ADCHEALTH-006:STM32 gap 计数增量 → 事件、不入锁存 */
static void tc_stm32_gap(void) {
adc_health_status_t st;
adc_health_in_t in;
adc_health_init();
in = mk_in(1u, 1u);
adc_health_check(&in, &st);
in.stm32_gaps = 1u; in.tpafe_seq = 2u; in.stm32_seq = 2u; in.now_ms = 2u;
adc_health_check(&in, &st);
CHECK((st.events & ADC_HE_STM32_GAP) != 0u,
"TC-ADCHEALTH-006 STM32 gap increment raises event");
CHECK((st.sticky & (ADC_HF_FRSTDATA | ADC_HF_STM32_ERR |
ADC_HF_HW_ERR)) == 0u,
"TC-ADCHEALTH-006 gap does NOT latch sticky (overrun is not a fault)");
}
/* TC-ADCHEALTH-007:序号倒退 → 锁存 */
static void tc_backward(void) {
adc_health_status_t st;
adc_health_in_t in;
uint32_t s, t;
int i;
adc_health_init();
in = mk_in(1000u, 100u);
adc_health_check(&in, &st);
in.tpafe_seq = 995u; in.stm32_seq = 995u; in.now_ms = 101u; /* 倒退 */
adc_health_check(&in, &st);
CHECK((st.flags & ADC_HF_SEQ_BACKWARD) != 0u,
"TC-ADCHEALTH-007 backward seq raises latched flag");
CHECK((st.sticky & ADC_HF_SEQ_BACKWARD) != 0u,
"TC-ADCHEALTH-007 backward latched into sticky");
/* 恢复正常推进 50 拍:SEQ_BACKWARD 不清(锁存语义) */
s = 996u; t = 102u;
for (i = 0; i < 50; i++) {
in = mk_in(s, t);
adc_health_check(&in, &st);
s++; t++;
}
CHECK((adc_health_flags() & ADC_HF_SEQ_BACKWARD) != 0u,
"TC-ADCHEALTH-007 SEQ_BACKWARD persists after normal advance");
}
/* TC-ADCHEALTH-008:三类错误计数增量 → 事件位与锁存位编号一致 */
static void tc_err_counters(void) {
adc_health_status_t st;
adc_health_in_t in;
adc_health_init();
in = mk_in(1u, 1u);
adc_health_check(&in, &st);
in.tpafe_seq = 2u; in.stm32_seq = 2u; in.now_ms = 2u;
in.tpafe_frst_err = 1u;
adc_health_check(&in, &st);
CHECK(((st.events & ADC_HE_FRSTDATA) != 0u) &&
((st.sticky & ADC_HF_FRSTDATA) != 0u),
"TC-ADCHEALTH-008 FRSTDATA increment: event + sticky (same bit number)");
in.tpafe_seq = 3u; in.stm32_seq = 3u; in.now_ms = 3u;
in.stm32_errs = 1u;
adc_health_check(&in, &st);
CHECK(((st.events & ADC_HE_STM32_ERR) != 0u) &&
((st.sticky & ADC_HF_STM32_ERR) != 0u),
"TC-ADCHEALTH-008 STM32 err increment: event + sticky (same bit number)");
in.tpafe_seq = 4u; in.stm32_seq = 4u; in.now_ms = 4u;
in.stm32_hw_errs = 1u;
adc_health_check(&in, &st);
CHECK(((st.events & ADC_HE_HW_ERR) != 0u) &&
((st.sticky & ADC_HF_HW_ERR) != 0u),
"TC-ADCHEALTH-008 HAL err increment: event + sticky (same bit number)");
/* 计数不再变化 → 事件不再重复触发 */
in.tpafe_seq = 5u; in.stm32_seq = 5u; in.now_ms = 5u;
adc_health_check(&in, &st);
CHECK(st.events == 0u, "TC-ADCHEALTH-008 steady error counters raise no repeat events");
}
/* TC-ADCHEALTH-009:tick 回绕安全 */
static void tc_tick_wrap(void) {
adc_health_status_t st;
adc_health_in_t in;
uint32_t seq = 70000u, now = 0xFFFFFF00u;
int i, ok = 1;
adc_health_init();
/* 跨 0xFFFFFFFF 正常推进 512 拍:不得误报静默 */
for (i = 0; i < 512; i++) {
in = mk_in(seq, now);
adc_health_check(&in, &st);
if (st.flags != 0u) {
ok = 0;
}
seq++; now++; /* uint32 自然回绕 */
}
CHECK(ok, "TC-ADCHEALTH-009 tick wrap sweep: no false stall");
/* 回绕后两侧同时冻结。⚠ 第 1 拍消费 sweep 末尾的"未消费 +1"并重置计时起点
* → 第 i 拍 elapsed = i,需 101 拍才到 100ms */
for (i = 0; i < 101; i++) {
in = mk_in(seq, now);
adc_health_check(&in, &st);
now++;
}
CHECK(((st.flags & ADC_HF_TPAFE_STALL) != 0u) &&
((st.flags & ADC_HF_STM32_STALL) != 0u),
"TC-ADCHEALTH-009 stall still detected after tick wrap");
}
/* TC-ADCHEALTH-010:NULL 守卫 */
static void tc_null_guard(void) {
adc_health_status_t st;
adc_health_in_t in;
int i;
adc_health_init();
in = mk_in(1u, 1u);
adc_health_check(&in, &st);
st.flags = 0xA5A5A5A5u;
adc_health_check(NULL, &st);
CHECK(st.flags == 0xA5A5A5A5u, "TC-ADCHEALTH-010 in=NULL leaves out untouched");
/* out=NULL:内部状态照常更新(两侧冻结 101 拍 → 静默位应出现) */
for (i = 0; i < 101; i++) {
in = mk_in(1u, (uint32_t)(2 + i));
adc_health_check(&in, NULL);
}
CHECK((adc_health_flags() & ADC_HF_TPAFE_STALL) != 0u,
"TC-ADCHEALTH-010 out=NULL still updates internal state");
}
/* TC-ADCHEALTH-011:init 幂等 / 故障后全清 */
static void tc_reinit(void) {
adc_health_status_t st;
adc_health_in_t in;
int i;
adc_health_init();
in = mk_in(1u, 1u);
adc_health_check(&in, &st);
for (i = 0; i < 150; i++) { /* 制造双侧静默 */
in = mk_in(1u, (uint32_t)(2 + i));
adc_health_check(&in, &st);
}
CHECK(st.sticky != 0u, "TC-ADCHEALTH-011 pre: sticky non-zero before re-init");
adc_health_init();
CHECK((adc_health_flags() == 0u) && (adc_health_sticky() == 0u) &&
(adc_health_tpafe_gaps() == 0u),
"TC-ADCHEALTH-011 re-init clears flags/sticky/gaps");
in = mk_in(9u, 9u); /* 序号跳变也无妨:首拍只建基线 */
adc_health_check(&in, &st);
CHECK((st.flags == 0u) && (st.events == 0u),
"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)");
}
/* ===================== 采样通道自检 贴轨观察器(2026-10-25 道A) ===================== */
/* 便捷喂数:连发 n 拍同值,返回最后一次 watch 的边沿结果 */
static uint32_t feed(uint8_t ch, int32_t code, int n) {
uint32_t edge = 0u;
int i;
for (i = 0; i < n; i++) {
edge = adc_health_rail_watch(ch, code, -32112, 32112);
}
return edge;
}
/* TC-ADCHEALTH-018:释放沿后连续 20 拍贴上轨 -> 锁存,边沿只报一次 */
static void tc_rail_latch_after_release(void) {
uint32_t edge;
adc_health_rail_init();
(void)feed(0, 0, 1);
edge = feed(0, 32500, 19);
CHECK(edge == 0u && adc_health_rail_sticky() == 0u,
"018: 19 hi beats after release -> no latch");
edge = feed(0, 32600, 1);
CHECK(edge == 1u, "018: 20th consecutive hi beat -> edge");
CHECK(adc_health_rail_sticky() == (1uL << 0), "018: sticky bit0");
edge = feed(0, 32700, 5);
CHECK(edge == 0u, "018: no repeat edge after latched");
}
/* TC-ADCHEALTH-019:无释放沿(恒 0)永不锁存——空载/开机合法工况 */
static void tc_rail_static_zero_ok(void) {
adc_health_rail_init();
(void)feed(3, 0, 30);
CHECK(adc_health_rail_sticky() == 0u,
"019: static in-band zero (no release) never latches");
}
/* TC-ADCHEALTH-020:计数被中带清零;再连续 20 才锁存(含触轨带含等号边界) */
static void tc_rail_reset_by_mid(void) {
uint32_t edge;
adc_health_rail_init();
(void)feed(1, 0, 1);
(void)feed(1, 32113, 19);
(void)feed(1, 0, 1);
edge = feed(1, 32112, 19);
CHECK(edge == 0u && adc_health_rail_sticky() == 0u,
"020: mid sample resets run counter (19+19 never hits 20)");
edge = feed(1, 32112, 1);
CHECK(edge == 1u && adc_health_rail_sticky() == (1uL << 1),
"020: fresh 20-run latches; boundary code == hi_th counts as rail");
}
/* TC-ADCHEALTH-021:上下轨交替互不清锁存但各自清零对侧计数 */
static void tc_rail_alt_no_latch(void) {
int i;
adc_health_rail_init();
(void)feed(2, 0, 1);
for (i = 0; i < 60; i++) {
(void)adc_health_rail_watch(2, ((i & 1) != 0) ? 32500 : -32500, -32112, 32112);
}
CHECK(adc_health_rail_sticky() == 0u,
"021: alternating rails clear each other, never latch");
}
/* TC-ADCHEALTH-022:下轨对称路径(STM32 削顶 = 贴底签名,同样先要释放沿) */
static void tc_rail_low_side(void) {
uint32_t edge = 0u;
int i;
adc_health_rail_init();
(void)adc_health_rail_watch(5, 2000, 81, 4013);
for (i = 0; i < 20; i++) {
edge |= adc_health_rail_watch(5, 0, 81, 4013);
}
CHECK(edge == 1u && adc_health_rail_sticky() == (1uL << 5),
"022: low rail band latches symmetrically (ch5)");
}
/* TC-ADCHEALTH-023:rail_init 全清(含释放沿记录);health_init 内含;越界通道忽略 */
static void tc_rail_reset_and_oob(void) {
adc_health_rail_init();
(void)feed(0, 0, 1);
(void)feed(0, 32500, 20);
CHECK(adc_health_rail_sticky() != 0u, "023: pre-check latch happens");
adc_health_rail_init();
CHECK(adc_health_rail_sticky() == 0u, "023: rail_init clears sticky");
(void)feed(0, 32500, 25);
CHECK(adc_health_rail_sticky() == 0u, "023: release history cleared too");
(void)feed(0, 0, 1);
(void)feed(0, 32500, 20);
CHECK(adc_health_rail_sticky() != 0u, "023: pre-check relatch for health_init test");
adc_health_init();
CHECK(adc_health_rail_sticky() == 0u, "023: adc_health_init() also clears rail");
CHECK(adc_health_rail_watch(9, 32767, -32112, 32112) == 0u,
"023: ch OOB ignored, no sticky pollution");
}
int main(void) {
printf("=== ADC Health Tests ===\n");
printf("-- prime / healthy --\n");
tc_prime();
tc_healthy();
printf("-- stall (TPAFE / STM32) --\n");
tc_tpafe_stall();
tc_stm32_stall();
printf("-- gap / backward / err counters --\n");
tc_tpafe_gap();
tc_stm32_gap();
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();
printf("-- 采样通道自检 rail watch --\n");
tc_rail_latch_after_release();
tc_rail_static_zero_ok();
tc_rail_reset_by_mid();
tc_rail_alt_no_latch();
tc_rail_low_side();
tc_rail_reset_and_oob();
printf("\n=== ADC Health Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}