Files
STM32F4-Base/test/host/prot_elm/prot_elm_test.c
T
wandering1 77779d49d6 feat(ui+记录): 整定写路径与四类页落地,五类记录写入方补齐
UI 面:ui_data 增加深度 4 参数写队列(请求 168 B、队列 .bss 672 B)与状态机
OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY,ui_data_feed 出队后按注入 ops 走「闭锁 → 整批
预检 → 逐项 prot_set_set_sealed」,只提交改动位图;par 帧尾追加 cal_block
(口径 = UA/UN/IA 三路 ADC_CAL_F_VALID,与 protect_task 判据同构,不用 rt.eng
反推,V0.1 下 eng 恒 0)、UI_DATA_VER 升到 2。参数/定值/控制字/软压板四叶绑定
内容页(UI_PAGE_MAP_COUNT 12→16,复用四个查看模块,零新增 .c),编辑态按
is_editable 分派,LOCK(写不进去)与「未标定」(仍可改值)两源两通道两词。

记录面:syslog_request_clear/state/apply 三段式删除 API(UI 只发请求,落地在
defaultTask 的 drain 尾部且必须后置,卷锁一律 try,幂等只认 FR_NO_FILE);
事故/告警/启动/操作/遥控五类各有一个真实写入点,记录类型 = 日志 ID 本身
(一类一个 /log 严格 8.3 文件;64B 定长体没有类型字段位,塞字段会砸掉既有读者)。
为守 protectTask 872 B 静态最坏链另拆 syslog_post_quiet()(无 DBG 的投递核,
丢弃留痕挪到 drain 侧差值补发),syslog_post 对外语义逐字不变。

为什么两道合成一笔而不是各一笔(实测,不是省事):simulator/main.c 同时消费
两道新增 API(整定 pump 与 logtest 清除用例),按道切文件会让中间提交编不过;
RAM 受管快照与体积同码凭证描述的是合并后的 .map,拆开必落在「台账与 .map 不同
码」这条红线上。pre-commit 跑钩子前会 stash 未 staged 部分,这类耦合没法绕。

实测:@build 0 错误 0 警告;Code 228,260 / RO 24,960 / RW 1,440 / ZI 135,976 B;
RW_IRAM1 79,992 B = 61.0%、RW_CCM 87.9%(阈值 92%);九个任务静态最坏链全 PASS,
protectTask 872 B / 57% 改前后逐字节不变(记录投递没垫栈)。主机单测 ui_data 82→
164、sys_log_mcu 106→137、prot_elm 47→60、iec104 141→154、iec104_serv 28→34,
既有断言零删改;48 个钩子 --all-files 全绿;编号门禁可判定 86.34%(基线 86.0%)、
不可判定 1,153 处低于上限 1,166;doc-links 无法判定 0;UI 黄金 18 页扩到 27 页
连跑两次逐字节一致;字库 diff 空。

顺手修掉三处既有缺陷(都是本轮第一个消费者才暴露):sim_syslog.c 的 s_paths 行宽
16 而 "sim_selftest.log" 正好 16 字符没有 NUL 位(stat/remove 读越过数组尾,此前
靠后面 .rodata 碰巧为 0);ui_w_setlist 二态行 ENTER 不回绕(控制字与软压板出厂值
全在最后一项,按了像没反应而叠层里已是越界值);生成物 ui_menu_tab.h 与 gen_menu.py
模板漂移(上一笔改了模板没重跑生成器),现随生成器输出对齐。

未收口(不算完成):保存后重启值保持与主站 C_RD 读回两条只能上板,台架窗口执行;
LOCK 屏显与越界反显的页面侧在模拟器造不出来(模拟器不跑定值自检、无 NAND),
已由生成侧单测与代码注释点名差距;同类页数与钩子数陈述残留另立待办。
2026-10-26 15:51:12 +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 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-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 (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)");
}
/* 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;
}