Files
STM32F4-Base/test/host/adc_health/adc_health_test.c
T
wandering1 13a4e61c27 L1-6 采样有效性校验:adc_health 判定层 + 告警编排 + 31 用例单测 + 台架闭环
新增 App/algo/adc_health.c/h(纯逻辑判定层,零平台依赖免桩单测):
- 静默:两侧帧序号连续 >=100ms 无前进(电平位可自愈,flags() 为 L5-4 闭锁预留)
- 丢帧:TPAFE 序号一次前进 >1 计 tgap(与 STM32 侧 adc1_gap 对称,边沿事件)
- 单调性:序号倒退锁存位;tick/计数差值法 (int32_t)(a-b) 回绕安全
- 错误增量:FRSTDATA / HAL 错误 / STM32 停摆计数增量,事件位与锁存位编号对齐,
  消费者按位图做"每类每次开机留痕一次"编排

adc_task.c v2.9 告警编排(判定与编排分离):严重事件/电平上升沿 DBG_ERROR +
SYSLOG_SELFTEST(每类每次开机一次防刷屏),下降沿 DBG_INFO 恢复;gap 不告警
(消费者超拍是已知现象,告警会淹没真故障);心跳行增 tgap 观测字段。

主机单测 test/host/adc_health/ 31 用例全绿(100ms 门限两侧 / 边沿语义 /
倒退锁存 / tick 回绕 / NULL 守卫 / init 幂等),登记 adc-health-unit-test 钩子。

台架 @test PASS:零 [ERR](健康不误报)、tgap=1->2 与 adc1_gap 同步可见、
frames 1000.0 Hz;证据 boot_log_data/20261011_prod_adc_health_boot_log.txt。

抬栈:adc_health_report->syslog_post 控制台回显尾使 adcTask 静态最坏链
832->1080 B(512 words 余 968 B < 1024 B ISR 底线)-> 512->640 words,
复测余 1480 B / 57%,堆 min_ever 7152->6640 B(-512 B 符合预算)。
AGENTS/README 任务表、RAM 方案 §2.3、规划文档 §3.1 L1-6 已同步置完成。
2026-10-11 01:28:20 +08:00

375 lines
13 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 采样有效性校验(L1-6)主机侧单元测试
* 模块功能:在 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 L1-6):
* TC-AH-001 init + 首拍只建基线(不误报)
* TC-AH-002 正常 1 kHz 推进 2000 拍:零故障零事件(健康不误报)
* TC-AH-003 TPAFE 静默:99ms 不报 / 100ms 报 / 恢复清除电平位、锁存位留痕
* TC-AH-004 STM32 静默:TPAFE 正常时只报 STM32 侧
* TC-AH-005 TPAFE 丢帧:d=3 → tgap+=2、事件仅当拍(边沿语义)
* TC-AH-006 STM32 gap 计数增量 → 事件(不入锁存:超拍非故障)
* TC-AH-007 序号倒退 → SEQ_BACKWARD 锁存(前进后仍留痕)
* TC-AH-008 FRSTDATA / STM32_ERR / HW_ERR 增量 → 事件位与锁存位编号一致
* TC-AH-009 tick 回绕:跨 0xFFFFFFFF 的正常推进不误报静默;回绕后冻结仍报
* TC-AH-010 NULL 守卫:in=NULL 跳过;out=NULL 只更新内部状态
* TC-AH-011 init 幂等:故障后 re-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-AH-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-AH-001 first check only primes: no flags/events/sticky");
/* 第二拍序号未动但仅 1ms(< 门限)→ 仍不报 */
in.now_ms = 1001u;
adc_health_check(&in, &st);
CHECK(st.flags == 0u, "TC-AH-001 1ms without advance is not a stall");
}
/* TC-AH-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-AH-002 healthy 1kHz sweep 2000 beats: zero flags/events");
}
/* TC-AH-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-AH-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-AH-003 TPAFE stall at 100ms");
CHECK((st.sticky & ADC_HF_TPAFE_STALL) != 0u,
"TC-AH-003 stall latched into sticky");
CHECK(st.tpafe_stall_ms >= ADC_HEALTH_STALL_MS,
"TC-AH-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-AH-003 stall level clears on recovery");
CHECK((adc_health_sticky() & ADC_HF_TPAFE_STALL) != 0u,
"TC-AH-003 sticky keeps the fault trace after recovery");
}
/* TC-AH-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-AH-004 STM32 stall at 100ms");
CHECK((st.flags & ADC_HF_TPAFE_STALL) == 0u,
"TC-AH-004 healthy TPAFE not falsely stalled");
}
/* TC-AH-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-AH-005 gap event on d>1");
CHECK(st.tpafe_gaps == 2u, "TC-AH-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-AH-005 gap event is edge-only (next beat clean)");
CHECK(st.tpafe_gaps == 2u, "TC-AH-005 gap counter is cumulative");
}
/* TC-AH-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-AH-006 STM32 gap increment raises event");
CHECK((st.sticky & (ADC_HF_FRSTDATA | ADC_HF_STM32_ERR |
ADC_HF_HW_ERR)) == 0u,
"TC-AH-006 gap does NOT latch sticky (overrun is not a fault)");
}
/* TC-AH-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-AH-007 backward seq raises latched flag");
CHECK((st.sticky & ADC_HF_SEQ_BACKWARD) != 0u,
"TC-AH-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-AH-007 SEQ_BACKWARD persists after normal advance");
}
/* TC-AH-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-AH-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-AH-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-AH-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-AH-008 steady error counters raise no repeat events");
}
/* TC-AH-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-AH-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-AH-009 stall still detected after tick wrap");
}
/* TC-AH-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-AH-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-AH-010 out=NULL still updates internal state");
}
/* TC-AH-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-AH-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-AH-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-AH-011 re-prime after re-init is clean");
}
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_tick_wrap();
tc_null_guard();
tc_reinit();
printf("\n=== ADC Health Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}