77779d49d6
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), 已由生成侧单测与代码注释点名差距;同类页数与钩子数陈述残留另立待办。
567 lines
22 KiB
C
567 lines
22 KiB
C
/*
|
||
* 模块名称:保护元件框架(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;
|
||
}
|