Files
STM32F4-Base/test/host/prot_elm/prot_elm_test.c
T
wandering1 b7c0889cc7 refactor(编号·批次二): 代码与测试侧 215 处保护功能号改成功能名,零行为改动
用户裁定「不要用字母加数字的编号,语义标识是最好的」后的批次二代码半(道 ui-param 执行,逐文件站点数与
自证见其交付)。唯一口径 = 映射表 ci-artifacts/lprot_map.tsv 的功能名列原文,**零自造同义词**;L2.5 与
L-TEST 经实测各 0 处(不是本族),TC 族一字未动。

固件 22 文件 128 处(adc_task.c 21、phasor.c 22、phasor.h 13…),主机测试 13 文件 87 处(pha
sor_test.c
57…);设计文档指针新增 19 行,另 16 只文件本来就写着规划文档 ⇒ 不重复加。

**两条只有真改过 215 处才会踩到的坑,都写进注释规范实践**:
· 指针**不能接在原句尾部**——那会替原文一个未闭合的半括号封口,实测咬坏 App/algo/sample_ring.h 这类
  跨行句 ⇒ 指针一律**单独成行**;
· 号删掉会留下**叠词**(「自检失败闭锁 闭锁判定」「定值校验和 定值巡检」)⇒ 复扫并收口 4 处,措辞只用
  映射名本身;具体对象需要留住时写成「自检失败闭锁(采样故障闭锁)」。

自证(不靠嘴说):改前把 35 只文件原文备份到仓外 temp,逐文件比 `/*`、`*/` 计数、三类括号计数、行数增量
恰等于指针行数、全角括号增删相等、残留号 0 ⇒ **35/35 通过**;Lead 复核后跑权威 @build **0 错 0 警**,
Program Size `Code=233900 RO=26104 RW=1536 ZI=136808` —— **与三条道各自改前改后测的四数逐
字节同数**,
这就是「只动注释与文本」的机器证据;八套主机单测 exit 0,cppcheck 按钩子参数零发现,coding-standard 0。

文件头「修改记录」里的号**一并换名、不还原**(Lead 裁定):这些号是全仓退役的,留一个查无此号的引用比
留一条改过名的条目更坏——「冻结历史不回改」保护的是**别替历史改口**,不是保护一个再也查不到的字符串;
id-check 对变更记录行本就豁免,两种选择都不影响门禁。
2026-10-27 08:59:26 +08:00

567 lines
22 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-PROTELEM-001~010 走 prot_elm_eval 纯判据路径,TC-PROTELEM-013 走
* run() 适配层 = 合成波形跑真实 L2+测频链)
*
* 覆盖点(docs/算法/继电保护功能复现规划.md §3.3 三段式相间过流 等、§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 定值闭锁(定值校验和 联动):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/过频 / 低频):越限 + RET_OF 滞回 + 定时限
* TC-PROTELEM-012 低频 UF(B8/过频 / 低频):欠量方向 + RET_UF 滞回 + 方向隔离
* TC-PROTELEM-013 run() 接线(B8):测频驱动 OF 动作;幅值门失效 OF 复位且
* 相量类元件仍被评估(不整拍跳过)
* TC-PROTELEM-014 启动位图与动作位图分离(道F task-10「保护启动」记录的前置):
* 延时段已启动未到点时两枚位图必须不同 + 不变式 asserted ⊆ picked
* TC-PROTELEM-015 动作边沿的记录分类(道F task-10「事故 / 保护告警」):
* 逐项独立读 PS_*_TRIP、改定值即改类别、无该投退项的五项恒告警
* (同时挡住 trip_ctl=0xFF 的越界读)、越界编号不落记录
*
* 通过:全部 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-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 (定值校验和 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(过频 / 低频,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)");
}
/* TC-PROTELEM-014(道F task-10):启动位图与动作位图**分离**。
* 「保护启动」记录的边沿检测靠 pick_mask,若它等于 active_mask(或干脆没实现,
* 让 protect_task 去轮询 state),"启动了但延时段还没到点"这一整段在记录里就是
* 空的——而那恰恰是这一页唯一比"动作记录"多出来的信息。所以这里把两者的
* **不等**关系钉死,并钉住不变式 asserted ⊆ picked。 */
static void tc_pick_mask(void) {
float m[PE_COUNT];
prot_set_init();
prot_elm_init();
(void)prot_set_set(PS_OC1_DLY, PROT_SET_Q10(100.0)); /* 延时段 */
(void)prot_set_set(PS_OC3_DLY, PROT_SET_Q10(0.0)); /* 瞬时段 */
mk_base(m);
m[PE_OC1] = 10.1f; /* 越限,但 100 ms 未到 */
m[PE_OC3] = 2.1f; /* 越限,dly=0 同拍动作 */
prot_elm_eval(6000u, m);
CHECK(prot_elm_pick_mask() == ((1uL << PE_OC1) | (1uL << PE_OC3)),
"TC-PROTELEM-014 pick mask carries the delayed element that has not tripped yet");
CHECK(prot_elm_active_mask() == (1uL << PE_OC3),
"TC-PROTELEM-014 active mask still excludes it (the two masks are genuinely different)");
CHECK((prot_elm_active_mask() & ~prot_elm_pick_mask()) == 0u,
"TC-PROTELEM-014 invariant: asserted is a subset of picked");
prot_elm_eval(6100u, m); /* OC1 定时限到点 */
CHECK(prot_elm_active_mask() == ((1uL << PE_OC1) | (1uL << PE_OC3)),
"TC-PROTELEM-014 after the delay the active mask catches up");
CHECK(prot_elm_pick_mask() == ((1uL << PE_OC1) | (1uL << PE_OC3)),
"TC-PROTELEM-014 pick mask unchanged while the level holds");
m[PE_OC1] = 9.4f; /* 跌破返回门槛 */
m[PE_OC3] = 1.0f;
prot_elm_eval(6200u, m);
CHECK((prot_elm_pick_mask() == 0u) && (prot_elm_active_mask() == 0u),
"TC-PROTELEM-014 both masks clear together below the return threshold");
}
/* TC-PROTELEM-015(道F task-10):动作上升沿该归"事故"还是"保护告警"。
* 判据的唯一来源是定值表里的跳闸投退项 PS_*_TRIP(枚举注释即"0 告警 / 1 跳闸"),
* 元件->投退项的对应关系在 k_pe[] 的 trip_ctl 列。这里要钉的是三件容易做错的事:
* a) **逐项独立**:OC1 投入、OC2 退出必须给出不同类别(做成一个全局开关、
* 或在本函数里另写一份"哪些元件算跳闸"的名单,都会在这里翻红——而那份
* 名单一旦与 g_prot_set_meta / k_pe[] 分叉,就是"整定视图与记录分类打架",
* setview-check 也只认既有表);
* b) **改定值即改类别**:同一元件同一状态,写 1 变事故、写 0 变告警,
* 证明它是查表而不是编译期常量;
* c) **没有该投退项的五项恒告警**,且**不会串到别人的字**:把全部 TRIP 项
* 写成 1,OV/UV/3U0/OF/UF 仍然是告警——这条同时是 trip_ctl=0xFF 那格的
* 越界读防线(若实现把 0xFF 直接喂给 prot_set_get_ctl(),读到的是定值区
* 之外,类别会随内存内容漂,这条必红)。
* 越界编号返回"不落任何记录"。 */
static void tc_record_kind(void) {
prot_set_init();
prot_elm_init();
/* a) 逐项独立 */
(void)prot_set_set(PS_OC1_TRIP, PROT_SET_Q10(1));
(void)prot_set_set(PS_OC2_TRIP, PROT_SET_Q10(0));
CHECK((prot_elm_record_kind(PE_OC1) == PROT_REC_TRIP)
&& (prot_elm_record_kind(PE_OC2) == PROT_REC_ALARM),
"TC-PROTELEM-015 the kind is read per element, not from one global switch");
/* b) 改定值即改类别(同元件、同状态) */
(void)prot_set_set(PS_OC1_TRIP, PROT_SET_Q10(0));
CHECK(prot_elm_record_kind(PE_OC1) == PROT_REC_ALARM,
"TC-PROTELEM-015 releasing the trip word turns the same element into an alarm");
(void)prot_set_set(PS_OC1_TRIP, PROT_SET_Q10(1));
CHECK(prot_elm_record_kind(PE_OC1) == PROT_REC_TRIP,
"TC-PROTELEM-015 re-engaging it turns it back into an accident (no latching)");
(void)prot_set_set(PS_OC3_TRIP, PROT_SET_Q10(1));
(void)prot_set_set(PS_OL_TRIP, PROT_SET_Q10(1));
CHECK((prot_elm_record_kind(PE_OC3) == PROT_REC_TRIP)
&& (prot_elm_record_kind(PE_OL) == PROT_REC_TRIP),
"TC-PROTELEM-015 OC3/OL follow their own trip words too");
/* c) 无跳闸投退项的五项:全表写 1 之后仍恒告警(也挡住 0xFF 越界读) */
(void)prot_set_set(PS_OC1_TRIP, PROT_SET_Q10(1));
(void)prot_set_set(PS_OC2_TRIP, PROT_SET_Q10(1));
(void)prot_set_set(PS_OC3_TRIP, PROT_SET_Q10(1));
(void)prot_set_set(PS_OL_TRIP, PROT_SET_Q10(1));
CHECK((prot_elm_record_kind(PE_OV) == PROT_REC_ALARM)
&& (prot_elm_record_kind(PE_UV) == PROT_REC_ALARM)
&& (prot_elm_record_kind(PE_3U0) == PROT_REC_ALARM)
&& (prot_elm_record_kind(PE_OF) == PROT_REC_ALARM)
&& (prot_elm_record_kind(PE_UF) == PROT_REC_ALARM),
"TC-PROTELEM-015 alarm-only elements stay alarms with every trip word engaged");
/* 守卫:越界编号 = 一条都不许落 */
CHECK(prot_elm_record_kind(PE_COUNT) == PROT_REC_NONE,
"TC-PROTELEM-015 out-of-range id -> no record at all");
/* 只读性:判定不改任何状态(protectTask 每拍都会问,问完还要跑判据) */
CHECK(prot_elm_pick_mask() == 0u,
"TC-PROTELEM-015 the classifier leaves the element states untouched");
}
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();
tc_pick_mask();
tc_record_kind();
printf("\n=== Prot Elm Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}