Files
STM32F4-Base/test/host/prot_elm/prot_elm_test.c
T
wandering1 0264b49489 feat(algo): B8 频率测量层 + 过频/低频保护(L3-5)
新增 App/algo/freq.c/h:由 L2 基波相量导出系统频率。相邻两拍共轭
乘积 P=Z(m)·conj(Z(m-1)) 取角 = 每拍相位增量(免显式解缠绕),再按
自适应窗长 K_eff=round(8·Fs/(2·f_hat))(45~55 Hz 段 73~89 拍)做滑动
平均。定长窗受 20 点矩形窗负序泄漏纹波限制(实测扫频最坏 0.317 Hz;
60 拍 Kay 复数和 0.197 Hz,交叉项和角不按 1/K^2 衰减),自适应窗使
窗长恰为纹波周期 fs/(2f) 整数倍 -> 最坏 0.0101 Hz。推导与两轮弯路
写在 freq.c 文件头。

假值钳制带取 35~65 Hz,刻意宽于系统合理带 40.5~60.5:单拍相位差本身
带 ±5 Hz 泄漏纹波,贴边钳制会非对称丢弃纹波半周 -> 均值偏置(实测
45 Hz 点 +0.16 Hz)。系统合理带由 OF/UF 定值范围在消费侧把关。
幅值门 FREQ_AMP_MIN=50 码(拟定,待 L1-5 标定复核):低于门判无有效
波形,停更新、valid 清零、读数保持 -> 浮空/PT 断线不发散。源优先级
UA(C02)->IA(C01),按名前缀解析、每拍动态回退,切换即废相位基准。

prot_set:尾追加 8 项(PS_ID_COUNT 32->40,编号不回收)
OF_EN/OF_HZ/OF_DLY/UF_EN/UF_HZ/UF_DLY/RET_OF/RET_UF。OF/UF 逐项通讯
地址不在本仓库 -> 全部 comm_addr=0 本地项(104 C_SE 不可寻址,待对照
参考表补)。频率返回系数独立(0.99/1.01),勿与电气量 0.95/1.05 共用。
bad_mask 随项数扩为 uint64_t。PROT_SET_BLOB_VER 1->2。

prot_set_store:换代与损坏分流。load 返回 2 = 版本/项数换代(本文件
打 INFO "prot set blob version change, defaults in use"),-1 = 真损坏
(defaultTask 按 CORRUPT 报 ERROR + 自检留痕)。B7 旧档(ver=1/32 项)
升级首启属预期路径,不得报成损坏。语义分流收在属主文件内,
default_task.c 不动。

prot_elm:实例表加 src 字段(0=phasor rms,1=freq),PE_OF/PE_UF 尾
追加(PE_COUNT 7->9)。freq 无有效读数按方向填"必不启动"值(过量 0 /
欠量 1e30 -> 滞回比较恒假 -> 元件复位),不整拍跳过——频率源缺失不得
饿死电流/电压类元件(B5 行为不回归)。OF/UF 暂未映射独立 104 遥信点,
动作只经 YX15 保护总聚合可见(送检点表 = B10)。

板端接线:adcTask 在 phasor_run() 之后同拍调 freq_run();心跳/首拍日志
追加 freq(<tag>): f=%ld valid=%u (x0.01)(MicroLIB 无 %f)。app_main
内核前 freq_init()。

主机单测:freq 46 用例(J1 扫频 45~55 Hz <=0.02 Hz 实测最坏 0.0101;
J2 噪声 1% <=0.10 实测 0.0160;J3 启动收敛 <=40 ms 实测 20;J4 阶跃
50->51 <=120 ms 实测 98;J5 浮空 valid=0 不发散;J6 跳变 60 ms 内回
±0.05;另直流 8000 码 / 3-5 次谐波 20%/10% / UA->IA 回退)。
prot_set 90(+TC-PS-011 OF/UF 行 + 64 位 bad_mask bit39 + 旧档拒绝)、
prot_elm 47(+OF/UF 滞回与方向隔离 + run() 测频接线)。回归:phasor
57、iec104 94 全绿。

J4=120 ms 是"精度优先于响应"的显式取舍(Lead 批准):4 周波窗换
0.317->0.0101 Hz,测频响应 98 ms << OF/UF 定时限默认 1000 ms(占
总动作时间 <10%),暂态另有 J6 <=60 ms 兜底。

注册:freq.c 入 uvprojx algo 组 + check_registration.py SIM_EXEMPT
(纯算法、模拟器无消费路径,照 prot_set.c 先例)。

@build 0 错误 0 警告(Code=207468 RO=18304 RW=1068 ZI=153084)。
RAM:RW_IRAM1 96096->96728 B(+632 B,73.3%->73.8%)、RW_CCM 不变
57376 B(87.9%)、Flash 224596->226840 B(+2244 B,21.4%->21.6%);
台账随合并由 Lead 回填。stack_report:adcTask 静态最坏链 1320 B /
余量 1240 B(48%)改前后完全不变(freq_run 帧未进最坏链,无需抬栈);
protectTask 链 872 B / 余 1176 B(57%)同样不变。

gate: only mem-budget/mem-numbers pending lead ledger backfill
--------------------------------------------------------------
门禁实跑(pre-commit run --all-files,Lead 批准 --no-verify 提交):
28 Passed / 2 Failed,失败仅 mem-numbers 与 mem-budget 两条——均为
docs/内存管理 受管快照与本 .map 不一致(台账由 Lead 每次合并后
--write 回填,避免多分支同改受管表冲突)。其余含 doc-links /
coding-standard / no-mojibake / cppcheck / registration-check /
13 条主机单测钩子全 Passed(gitlint 属 commit-msg 阶段,不在该次
--all-files 内)。ci_local L0.5 全绿(freq 46/46 等)。
字节卫生自查(--no-verify 跳过卫生钩子,故手工核):17 个暂存文件
全部 BOM=无 / CR=0(纯 LF)/ U+FFFD=0;自研 .c/.h 去注释后字符串
字面量 100% ASCII(唯一非 ASCII 字面量在 test/host/iec104/iec104_test.c
的既有用例名,非本次改动行;test/ 不在 coding-standard 钩子范围)。
2026-10-12 19:16:37 +08:00

465 lines
16 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.
/*
* 模块名称:保护元件框架(L3,P1 子集)主机侧单元测试
* 模块功能:在 PC 上对 App/algo/prot_elm.c 的判据核做离线验证。
*
* 被测对象:真实源文件(#include 原样纳入):
* prot_elm.c + prot_set.c(判据吃定值 RAM)
* phasor.c + sample_ring.c + freq.c(prot_elm.c 链接依赖;
* TC-PE-001~010 走 prot_elm_eval 纯判据路径,TC-PE-013 走
* run() 适配层 = 合成波形跑真实 L2+测频链)
*
* 覆盖点(docs/算法/继电保护功能复现规划.md §3.3 L3-1/3/4/6、§8 验收):
* TC-PE-001 init:全状态清零、动作位图 0
* TC-PE-002 过量滞回 + 定时限:1.01× 定值启动、延时到点动作、
* 返回系数内保持、跌破返回门槛释放(1.2×/0.9× 验收口径的核)
* TC-PE-003 延时 0:启动同拍即动作
* TC-PE-004 欠量方向(UV):低于定值启动、高于返回门槛释放
* TC-PE-005 投退=0:不判据、状态复位
* TC-PE-006 定值闭锁(L5-1 联动):locked → 全元件复位;解锁后恢复
* TC-PE-007 时基回绕:跨 0xFFFFFFFF 的延时判定仍正确
* TC-PE-008 动作位图聚合
* TC-PE-009 守卫:state(NULL)/越界编号、name 越界
* TC-PE-010 跨上下文复归请求(B5):request 不当拍生效、下一拍 eval 头部消费
* TC-PE-011 过频 OF(B8/L3-5):越限 + RET_OF 滞回 + 定时限
* TC-PE-012 低频 UF(B8/L3-5):欠量方向 + RET_UF 滞回 + 方向隔离
* TC-PE-013 run() 接线(B8):测频驱动 OF 动作;幅值门失效 OF 复位且
* 相量类元件仍被评估(不整拍跳过)
*
* 通过:全部 TC PASS(main 返回 0)。
* 创建日期:2026-10-11(B5);2026-10-12 扩 OF/UF(B8)
*/
#include <math.h>
#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 "prot_set.c"
#include "sample_ring.c"
#include "phasor.c"
#include "freq.c"
#include "prot_elm.c"
/* 中性测量基线:电流 1.0 A、电压 100 V、3U0 0 V、频率 50 Hz(全部不越限)。
* OF/UF 填 50.0 = 标称:OF 定值范围下限 50.0(50>50 假)、UF 定值范围上限
* 50.0(50<50 假)-> 两侧都不启动(定值范围约束保证,见 prot_set 表)。 */
static void mk_base(float m[PE_COUNT]) {
uint32_t i;
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
m[i] = 1.0f;
}
m[PE_OV] = 100.0f;
m[PE_UV] = 100.0f;
m[PE_3U0] = 0.0f;
m[PE_OF] = 50.0f;
m[PE_UF] = 50.0f;
}
static void tc_init_clear(void) {
prot_set_init();
prot_elm_init();
CHECK(prot_elm_active_mask() == 0u, "TC-PE-001 init: active mask 0");
{
prot_elm_state_t st;
uint32_t i;
int clean = 1;
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
prot_elm_state((prot_elm_id_t)i, &st);
if ((st.picked != 0u) || (st.asserted != 0u) || (st.enabled != 0u)) {
clean = 0;
}
}
CHECK(clean, "TC-PE-001 init: all element states clear");
}
}
static void tc_over_hysteresis(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_OC1_DLY, PROT_SET_Q10(100.0)); /* 100 ms */
/* 默认 OC1_CUR=10.0 A,RET_OVER=0.95 → 返回门槛 ≈9.492 */
mk_base(m);
m[PE_OC1] = 9.9f;
prot_elm_eval(1000u, m);
prot_elm_state(PE_OC1, &st);
CHECK((st.picked == 0u) && (st.enabled != 0u),
"TC-PE-002 0.99x set: no pickup");
m[PE_OC1] = 10.1f;
prot_elm_eval(1001u, m);
prot_elm_state(PE_OC1, &st);
CHECK((st.picked != 0u) && (st.asserted == 0u),
"TC-PE-002 1.01x set: pickup before delay");
prot_elm_eval(1100u, m); /* 1100-1001 = 99 ms */
prot_elm_state(PE_OC1, &st);
CHECK(st.asserted == 0u, "TC-PE-002 not asserted at 99ms");
prot_elm_eval(1101u, m); /* 100 ms 整 */
prot_elm_state(PE_OC1, &st);
CHECK(st.asserted != 0u, "TC-PE-002 asserted at 100ms");
m[PE_OC1] = 9.7f; /* 返回门槛(9.492)与动作门槛(10)之间 */
prot_elm_eval(1200u, m);
prot_elm_state(PE_OC1, &st);
CHECK((st.picked != 0u) && (st.asserted != 0u),
"TC-PE-002 hysteresis: holds between return and pickup threshold");
m[PE_OC1] = 9.4f; /* 跌破返回门槛 */
prot_elm_eval(1300u, m);
prot_elm_state(PE_OC1, &st);
CHECK((st.picked == 0u) && (st.asserted == 0u),
"TC-PE-002 below return threshold: drops out");
}
static void tc_zero_delay(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_OC3_DLY, PROT_SET_Q10(0.0));
mk_base(m);
m[PE_OC3] = 2.1f; /* OC3 定值 2.0 */
prot_elm_eval(5000u, m);
prot_elm_state(PE_OC3, &st);
CHECK((st.picked != 0u) && (st.asserted != 0u),
"TC-PE-003 dly=0: asserted on the same beat as pickup");
}
static void tc_under_direction(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_UV_DLY, PROT_SET_Q10(50.0));
/* 默认 UV_VOL=20.0,RET_UNDER=1.05 → 返回门槛 ≈20.996 */
mk_base(m);
m[PE_UV] = 25.0f;
prot_elm_eval(2000u, m);
prot_elm_state(PE_UV, &st);
CHECK(st.picked == 0u, "TC-PE-004 UV above set: no pickup");
m[PE_UV] = 19.0f;
prot_elm_eval(2001u, m);
prot_elm_state(PE_UV, &st);
CHECK(st.picked != 0u, "TC-PE-004 UV below set: pickup");
prot_elm_eval(2051u, m); /* 50 ms */
prot_elm_state(PE_UV, &st);
CHECK(st.asserted != 0u, "TC-PE-004 UV asserted after delay");
m[PE_UV] = 20.5f; /* 20 与 20.996 之间:保持 */
prot_elm_eval(2100u, m);
prot_elm_state(PE_UV, &st);
CHECK(st.asserted != 0u, "TC-PE-004 UV holds below return threshold");
m[PE_UV] = 21.5f; /* 高于返回门槛:释放 */
prot_elm_eval(2200u, m);
prot_elm_state(PE_UV, &st);
CHECK((st.picked == 0u) && (st.asserted == 0u),
"TC-PE-004 UV above return threshold: drops out");
}
static void tc_disabled(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_OC2_EN, 0);
mk_base(m);
m[PE_OC2] = 999.0f;
prot_elm_eval(3000u, m);
prot_elm_state(PE_OC2, &st);
CHECK((st.enabled == 0u) && (st.picked == 0u) && (st.asserted == 0u),
"TC-PE-005 enable=0: no judgement even at 999x");
}
static void tc_set_lock(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
mk_base(m);
m[PE_OC1] = 10.1f;
(void)prot_set_set(PS_OC1_DLY, PROT_SET_Q10(0.0));
prot_elm_eval(4000u, m);
prot_elm_state(PE_OC1, &st);
CHECK(st.asserted != 0u, "TC-PE-006 pre: element operates while settings valid");
(void)prot_set_set(PS_OC1_CUR, PROT_SET_Q10(9.0)); /* 改值不封存 */
CHECK(prot_set_selfcheck() == -1, "TC-PE-006 tamper detected by selfcheck");
prot_elm_eval(4100u, m);
prot_elm_state(PE_OC1, &st);
CHECK((st.enabled == 0u) && (st.asserted == 0u),
"TC-PE-006 settings lock resets all elements (L5-1 gating)");
prot_set_seal();
prot_elm_eval(4200u, m);
prot_elm_state(PE_OC1, &st);
CHECK((st.enabled != 0u) && (st.asserted != 0u),
"TC-PE-006 unlock restores judgement");
}
static void tc_tick_wrap(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_OC2_DLY, PROT_SET_Q10(100.0));
mk_base(m);
m[PE_OC2] = 5.1f; /* OC2 定值 5.0 */
prot_elm_eval(0xFFFFFFF0u, m); /* 启动点贴近回绕 */
prot_elm_state(PE_OC2, &st);
CHECK(st.picked != 0u, "TC-PE-007 pickup near tick wrap");
prot_elm_eval(0x00000010u, m); /* 回绕后 32 ms */
prot_elm_state(PE_OC2, &st);
CHECK(st.asserted == 0u, "TC-PE-007 not asserted at 32ms after wrap");
prot_elm_eval(0x00000060u, m); /* 回绕后 112 ms */
prot_elm_state(PE_OC2, &st);
CHECK(st.asserted != 0u, "TC-PE-007 asserted at 112ms across wrap");
}
static void tc_active_mask(void) {
float m[PE_COUNT];
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_OC1_DLY, PROT_SET_Q10(0.0));
(void)prot_set_set(PS_OV_DLY, PROT_SET_Q10(0.0));
mk_base(m);
m[PE_OC1] = 10.1f;
m[PE_OV] = 130.0f; /* OV 定值 120 */
prot_elm_eval(6000u, m);
CHECK(prot_elm_active_mask() == ((1uL << PE_OC1) | (1uL << PE_OV)),
"TC-PE-008 active mask aggregates OC1+OV only");
}
static void tc_guards(void) {
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
prot_elm_state(PE_COUNT, &st);
CHECK((st.enabled == 0u) && (st.picked == 0u) && (st.asserted == 0u),
"TC-PE-009 out-of-range id -> zero state");
prot_elm_state(PE_OC1, NULL); /* 不得崩溃 */
CHECK(prot_elm_name(PE_COUNT) != NULL, "TC-PE-009 name out-of-range -> fallback");
CHECK(prot_elm_name(PE_3U0) != NULL, "TC-PE-009 name valid id -> non-NULL");
prot_elm_eval(7000u, NULL); /* NULL 测量:按 0 处理,不得崩溃 */
CHECK(1, "TC-PE-009 eval(NULL meas) survives");
}
static void tc_request_reset(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
mk_base(m);
m[PE_OC1] = 999.0f; /* OC1 默认 dly=0 → 启动同拍动作 */
prot_elm_eval(1000u, m);
prot_elm_state(PE_OC1, &st);
CHECK(st.asserted != 0u, "TC-PE-010 pre: OC1 asserted (default dly=0)");
prot_elm_request_reset(); /* 跨上下文请求:不当拍生效 */
prot_elm_state(PE_OC1, &st);
CHECK(st.asserted != 0u, "TC-PE-010 request alone does not reset");
/* 下一拍头部消费请求 → 复位;测量落在滞回保持区(9.6 ∈ [返回9.5, 动作10))
* ——不复位则 picked/asserted 保持,复位后保持清零,可区分 */
m[PE_OC1] = 9.6f;
prot_elm_eval(1100u, m);
prot_elm_state(PE_OC1, &st);
CHECK((st.picked == 0u) && (st.asserted == 0u),
"TC-PE-010 next eval consumes request -> cleared in hysteresis band");
}
/* ===================== OF/UF(L3-5,B8)===================== */
/* TC-PE-011:过频——越限 + RET_OF 滞回 + 定时限(判据核级) */
static void tc_of_hysteresis(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_OF_DLY, PROT_SET_Q10(100.0));
/* 默认 OF_HZ=51.0,RET_OF=0.99 -> 返回门槛 = 51*0.99 = 50.49 */
mk_base(m);
m[PE_OF] = 50.9f;
prot_elm_eval(1000u, m);
prot_elm_state(PE_OF, &st);
CHECK((st.picked == 0u) && (st.enabled != 0u),
"TC-PE-011 OF 50.9 (<51): no pickup");
m[PE_OF] = 51.1f;
prot_elm_eval(1001u, m);
prot_elm_state(PE_OF, &st);
CHECK((st.picked != 0u) && (st.asserted == 0u),
"TC-PE-011 OF 51.1: pickup before delay");
prot_elm_eval(1100u, m);
prot_elm_state(PE_OF, &st);
CHECK(st.asserted == 0u, "TC-PE-011 OF not asserted at 99ms");
prot_elm_eval(1101u, m);
prot_elm_state(PE_OF, &st);
CHECK(st.asserted != 0u, "TC-PE-011 OF asserted at 100ms");
m[PE_OF] = 50.7f; /* 50.49 与 51 之间:滞回保持 */
prot_elm_eval(1200u, m);
prot_elm_state(PE_OF, &st);
CHECK((st.picked != 0u) && (st.asserted != 0u),
"TC-PE-011 OF holds between return(50.49) and pickup(51)");
m[PE_OF] = 50.4f; /* 跌破返回门槛 */
prot_elm_eval(1300u, m);
prot_elm_state(PE_OF, &st);
CHECK((st.picked == 0u) && (st.asserted == 0u),
"TC-PE-011 OF below return threshold: drops out");
}
/* TC-PE-012:低频——欠量方向 + RET_UF 滞回(与 OF 系数独立,勿混 0.95/1.05) */
static void tc_uf_direction(void) {
float m[PE_COUNT];
prot_elm_state_t st;
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_UF_DLY, PROT_SET_Q10(50.0));
/* 默认 UF_HZ=49.0,RET_UF=1.01 -> 返回门槛 = 49*1.01 = 49.49 */
mk_base(m);
m[PE_UF] = 49.1f;
prot_elm_eval(2000u, m);
prot_elm_state(PE_UF, &st);
CHECK(st.picked == 0u, "TC-PE-012 UF 49.1 (>49): no pickup");
m[PE_UF] = 48.9f;
prot_elm_eval(2001u, m);
prot_elm_state(PE_UF, &st);
CHECK(st.picked != 0u, "TC-PE-012 UF 48.9 (<49): pickup");
prot_elm_eval(2051u, m);
prot_elm_state(PE_UF, &st);
CHECK(st.asserted != 0u, "TC-PE-012 UF asserted after 50ms");
m[PE_UF] = 49.3f; /* 49 与 49.49 之间:保持 */
prot_elm_eval(2100u, m);
prot_elm_state(PE_UF, &st);
CHECK(st.asserted != 0u, "TC-PE-012 UF holds below return threshold");
m[PE_UF] = 49.6f; /* 高于返回门槛:释放 */
prot_elm_eval(2200u, m);
prot_elm_state(PE_UF, &st);
CHECK((st.picked == 0u) && (st.asserted == 0u),
"TC-PE-012 UF above return threshold: drops out");
/* OF 不得被低频误启动(方向隔离);UF 不得被过频误启动 */
m[PE_OF] = 52.0f;
m[PE_UF] = 52.0f;
prot_elm_eval(2300u, m);
prot_elm_state(PE_OF, &st);
CHECK(st.picked != 0u, "TC-PE-012 OF picks at 52 (over direction)");
prot_elm_state(PE_UF, &st);
CHECK(st.picked == 0u, "TC-PE-012 UF NOT picked at 52 (direction isolation)");
}
/* ---- run() 适配层驱动:合成波形跑真实 L2+测频链(同 freq_test 口径) ---- */
static uint32_t s_t = 0u;
static void drive_beat(double f, double amp) {
float frame[ADC_SEM_COUNT];
double v = (f == 0.0) ? 0.0 : amp * sin(2.0 * 3.14159265358979 * f * (double)s_t / 1000.0);
uint8_t c;
for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) {
frame[c] = (float)v;
}
(void)sample_ring_push_frame(frame, (uint8_t)ADC_SEM_COUNT);
phasor_run();
freq_run();
s_t++;
}
/* TC-PE-013:run() 级——freq 有效读数驱动 OF 动作;幅值门失效后 OF 复位
* 且相量类元件仍被评估(不整拍跳过,B5 行为不回归) */
static void tc_run_freq_wiring(void) {
prot_elm_state_t st;
uint32_t i;
s_t = 0u;
(void)phasor_init();
(void)freq_init();
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_OF_DLY, PROT_SET_Q10(0.0)); /* 启动同拍动作 */
(void)prot_set_set(PS_UV_DLY, PROT_SET_Q10(0.0)); /* 观察相量类仍评估 */
/* 52.5 Hz(> OF 51)驱动到测频收敛(20 拍启动 + 窗)+ 判据拍 */
for (i = 0u; i < 140u; i++) {
drive_beat(52.5, 1000.0);
prot_elm_run(i);
}
prot_elm_state(PE_OF, &st);
CHECK((st.picked != 0u) && (st.asserted != 0u),
"TC-PE-013 run(): OF asserted from measured 52.5 Hz");
prot_elm_state(PE_UF, &st);
CHECK(st.picked == 0u, "TC-PE-013 run(): UF clear at 52.5 Hz");
/* 波形消失(浮空):freq 幅值门失效 -> OF 按方向填充复位;
* 同时相量 rms=0 -> UV(定值 20,dly=0)启动 = 证明未整拍跳过 */
for (i = 0u; i < 60u; i++) {
drive_beat(0.0, 0.0);
prot_elm_run(1000u + i);
}
prot_elm_state(PE_OF, &st);
CHECK((st.picked == 0u) && (st.asserted == 0u),
"TC-PE-013 run(): OF resets when freq source loses amplitude");
prot_elm_state(PE_UV, &st);
CHECK(st.picked != 0u,
"TC-PE-013 run(): UV still evaluated with zero-amplitude (no whole-beat skip)");
}
int main(void) {
printf("=== Prot Elm Tests ===\n");
tc_init_clear();
tc_over_hysteresis();
tc_zero_delay();
tc_under_direction();
tc_disabled();
tc_set_lock();
tc_tick_wrap();
tc_active_mask();
tc_guards();
tc_request_reset();
tc_of_hysteresis();
tc_uf_direction();
tc_run_freq_wiring();
printf("\n=== Prot Elm Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}