/* * 模块名称:保护元件框架(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-PROTELEM-001~010 走 prot_elm_eval 纯判据路径,TC-PROTELEM-013 走 * run() 适配层 = 合成波形跑真实 L2+测频链) * * 覆盖点(docs/算法/继电保护功能复现规划.md §3.3 L3-1/3/4/6、§8 验收): * TC-PROTELEM-001 init:全状态清零、动作位图 0 * TC-PROTELEM-002 过量滞回 + 定时限:1.01× 定值启动、延时到点动作、 * 返回系数内保持、跌破返回门槛释放(1.2×/0.9× 验收口径的核) * TC-PROTELEM-003 延时 0:启动同拍即动作 * TC-PROTELEM-004 欠量方向(UV):低于定值启动、高于返回门槛释放 * TC-PROTELEM-005 投退=0:不判据、状态复位 * TC-PROTELEM-006 定值闭锁(L5-1 联动):locked → 全元件复位;解锁后恢复 * TC-PROTELEM-007 时基回绕:跨 0xFFFFFFFF 的延时判定仍正确 * TC-PROTELEM-008 动作位图聚合 * TC-PROTELEM-009 守卫:state(NULL)/越界编号、name 越界 * TC-PROTELEM-010 跨上下文复归请求(B5):request 不当拍生效、下一拍 eval 头部消费 * TC-PROTELEM-011 过频 OF(B8/L3-5):越限 + RET_OF 滞回 + 定时限 * TC-PROTELEM-012 低频 UF(B8/L3-5):欠量方向 + RET_UF 滞回 + 方向隔离 * TC-PROTELEM-013 run() 接线(B8):测频驱动 OF 动作;幅值门失效 OF 复位且 * 相量类元件仍被评估(不整拍跳过) * * 通过:全部 TC PASS(main 返回 0)。 * 创建日期:2026-10-11(B5);2026-10-12 扩 OF/UF(B8) */ #include #include #include 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-PROTELEM-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-PROTELEM-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-PROTELEM-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-PROTELEM-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-PROTELEM-002 not asserted at 99ms"); prot_elm_eval(1101u, m); /* 100 ms 整 */ prot_elm_state(PE_OC1, &st); CHECK(st.asserted != 0u, "TC-PROTELEM-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-PROTELEM-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-PROTELEM-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-PROTELEM-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-PROTELEM-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-PROTELEM-004 UV below set: pickup"); prot_elm_eval(2051u, m); /* 50 ms */ prot_elm_state(PE_UV, &st); CHECK(st.asserted != 0u, "TC-PROTELEM-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-PROTELEM-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-PROTELEM-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-PROTELEM-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-PROTELEM-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-PROTELEM-006 tamper detected by selfcheck"); prot_elm_eval(4100u, m); prot_elm_state(PE_OC1, &st); CHECK((st.enabled == 0u) && (st.asserted == 0u), "TC-PROTELEM-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-PROTELEM-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-PROTELEM-007 pickup near tick wrap"); prot_elm_eval(0x00000010u, m); /* 回绕后 32 ms */ prot_elm_state(PE_OC2, &st); CHECK(st.asserted == 0u, "TC-PROTELEM-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-PROTELEM-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-PROTELEM-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-PROTELEM-009 out-of-range id -> zero state"); prot_elm_state(PE_OC1, NULL); /* 不得崩溃 */ CHECK(prot_elm_name(PE_COUNT) != NULL, "TC-PROTELEM-009 name out-of-range -> fallback"); CHECK(prot_elm_name(PE_3U0) != NULL, "TC-PROTELEM-009 name valid id -> non-NULL"); prot_elm_eval(7000u, NULL); /* NULL 测量:按 0 处理,不得崩溃 */ CHECK(1, "TC-PROTELEM-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-PROTELEM-010 pre: OC1 asserted (default dly=0)"); prot_elm_request_reset(); /* 跨上下文请求:不当拍生效 */ prot_elm_state(PE_OC1, &st); CHECK(st.asserted != 0u, "TC-PROTELEM-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-PROTELEM-010 next eval consumes request -> cleared in hysteresis band"); } /* ===================== OF/UF(L3-5,B8)===================== */ /* TC-PROTELEM-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-PROTELEM-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-PROTELEM-011 OF 51.1: pickup before delay"); prot_elm_eval(1100u, m); prot_elm_state(PE_OF, &st); CHECK(st.asserted == 0u, "TC-PROTELEM-011 OF not asserted at 99ms"); prot_elm_eval(1101u, m); prot_elm_state(PE_OF, &st); CHECK(st.asserted != 0u, "TC-PROTELEM-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-PROTELEM-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-PROTELEM-011 OF below return threshold: drops out"); } /* TC-PROTELEM-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-PROTELEM-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-PROTELEM-012 UF 48.9 (<49): pickup"); prot_elm_eval(2051u, m); prot_elm_state(PE_UF, &st); CHECK(st.asserted != 0u, "TC-PROTELEM-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-PROTELEM-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-PROTELEM-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-PROTELEM-012 OF picks at 52 (over direction)"); prot_elm_state(PE_UF, &st); CHECK(st.picked == 0u, "TC-PROTELEM-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-PROTELEM-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-PROTELEM-013 run(): OF asserted from measured 52.5 Hz"); prot_elm_state(PE_UF, &st); CHECK(st.picked == 0u, "TC-PROTELEM-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-PROTELEM-013 run(): OF resets when freq source loses amplitude"); prot_elm_state(PE_UV, &st); CHECK(st.picked != 0u, "TC-PROTELEM-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; }