Files
STM32F4-Base/test/host/prot_elm/prot_elm_test.c
T
wandering1 90db528032 W1 功能主线完成:内存台账三件套(W1-0) + IEC104 上行(B4) + protectTask 板端接线(B5)
W1-0 台账三件套:scripts/ram_baseline.py(四模式)+ ram_baseline.tsv 趋势 +
docs/内存管理/RAM预算台账.md + pre-commit mem-budget 钩子(RW_IRAM1/RW_CCM >92%
即 FAIL + Top-10 附表);审查报告 §5.5 三条闭环。

B4 IEC 60870-5-104 上行(被控站,TCP 2404):App/protocol/iec104/ 三模块
(asdu 编解码 / pts 点表+数据源 vtable / link APCI+FSM,纯逻辑零 I/O)+
App/task/iec104_serv.c(iec104Task,NET_READY 屏障 100ms 分片喂位)。
82 断言 L0.5 全绿 + iec104-unit-test 钩子(第 31 钩子);
设计说明 docs/子系统设计/IEC104上行设计说明.md 全量回填。

B5 protectTask(L3 板端接线):App/task/protect_task.c——20ms 判据节拍
prot_elm_run + L5-4 采样故障闭锁(adc_health 电平位→元件复位)+ L5-1 定值
校验和巡检;新增 prot_elm_request_reset() 收口跨上下文复归(104 YK11 落请求、
eval 头部消费);定值/元件初始化移入 app_main_init(内核前)。TC-PE-010
(prot_elm 28 用例)。

门禁与内存:TASK_WD_PROTECT=8 入 required_mask(产品 0x77→0x1F7、HW 层
0x1F5,看门狗单测 39 断言同步);FreeRTOS 堆 48→56KB(.ioc L1 参数 +
FreeRTOSConfig.h 同步,RAM预算台账 §6 登记配额 48→56KB)。
ZI 144,008→152,204(+8,196 B = 堆 +8KB,B5 扩容);Code=204,348。
stack_report:protectTask 512 words 静态链 872 B/余 57%、iec104Task
640 words 1456 B/余 43% 均 PASS;RW_IRAM1 73.1% / RW_CCM 87.9%(<92%)。

验证:@build 0 错 0 警;ci_local 全绿(31 钩子 + 13 主机侧单测);
doc-links 135 文档 PASS。登记簿 B4/B5 销项、W1 波次完成;保护规划/看门狗
说明/AGENTS(9 任务)/RAM 三文档全部同步。

遗留(验收窗口,非代码空洞):台架第三方 104 主站往返(送检窗口)、
标准源注入动作判据、@test PROT 锚点 + protect 栈水位 + 新堆 min_ever 复测。
2026-10-12 12:36:23 +08:00

303 lines
10 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(prot_elm.c 链接依赖;本测试只走
* prot_elm_eval 纯判据路径,不依赖 run() 适配层)
*
* 覆盖点(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 PASS(main 返回 0)。
* 创建日期: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 "prot_set.c"
#include "sample_ring.c"
#include "phasor.c"
#include "prot_elm.c"
/* 中性测量基线:电流 1.0 A、电压 100 V、3U0 0 V(全部不越限) */
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;
}
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");
}
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();
printf("\n=== Prot Elm Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}