feat(algo): B8 频率测量层 + 过频/低频保护(L3-5)

新增 App/algo/freq.c/h:由 L2 基波相量导出系统频率。相邻两拍共轭
乘积 P=Z(m)·conj(Z(m-1)) 取角 = 每拍相位增量(免显式解缠绕),再按
自适应窗长 K_eff=round(8·Fs/(2·f_hat))(45~55 Hz 段 73~89 拍)做滑动
平均。定长窗受 20 点矩形窗负序泄漏纹波限制(实测扫频最坏 0.317 Hz;
60 拍 Kay 复数和 0.197 Hz,交叉项和角不按 1/K^2 衰减),自适应窗使
窗长恰为纹波周期 fs/(2f) 整数倍 -> 最坏 0.0101 Hz。推导与两轮弯路
写在 freq.c 文件头。

假值钳制带取 35~65 Hz,刻意宽于系统合理带 40.5~60.5:单拍相位差本身
带 ±5 Hz 泄漏纹波,贴边钳制会非对称丢弃纹波半周 -> 均值偏置(实测
45 Hz 点 +0.16 Hz)。系统合理带由 OF/UF 定值范围在消费侧把关。
幅值门 FREQ_AMP_MIN=50 码(拟定,待 L1-5 标定复核):低于门判无有效
波形,停更新、valid 清零、读数保持 -> 浮空/PT 断线不发散。源优先级
UA(C02)->IA(C01),按名前缀解析、每拍动态回退,切换即废相位基准。

prot_set:尾追加 8 项(PS_ID_COUNT 32->40,编号不回收)
OF_EN/OF_HZ/OF_DLY/UF_EN/UF_HZ/UF_DLY/RET_OF/RET_UF。OF/UF 逐项通讯
地址不在本仓库 -> 全部 comm_addr=0 本地项(104 C_SE 不可寻址,待对照
参考表补)。频率返回系数独立(0.99/1.01),勿与电气量 0.95/1.05 共用。
bad_mask 随项数扩为 uint64_t。PROT_SET_BLOB_VER 1->2。

prot_set_store:换代与损坏分流。load 返回 2 = 版本/项数换代(本文件
打 INFO "prot set blob version change, defaults in use"),-1 = 真损坏
(defaultTask 按 CORRUPT 报 ERROR + 自检留痕)。B7 旧档(ver=1/32 项)
升级首启属预期路径,不得报成损坏。语义分流收在属主文件内,
default_task.c 不动。

prot_elm:实例表加 src 字段(0=phasor rms,1=freq),PE_OF/PE_UF 尾
追加(PE_COUNT 7->9)。freq 无有效读数按方向填"必不启动"值(过量 0 /
欠量 1e30 -> 滞回比较恒假 -> 元件复位),不整拍跳过——频率源缺失不得
饿死电流/电压类元件(B5 行为不回归)。OF/UF 暂未映射独立 104 遥信点,
动作只经 YX15 保护总聚合可见(送检点表 = B10)。

板端接线:adcTask 在 phasor_run() 之后同拍调 freq_run();心跳/首拍日志
追加 freq(<tag>): f=%ld valid=%u (x0.01)(MicroLIB 无 %f)。app_main
内核前 freq_init()。

主机单测:freq 46 用例(J1 扫频 45~55 Hz <=0.02 Hz 实测最坏 0.0101;
J2 噪声 1% <=0.10 实测 0.0160;J3 启动收敛 <=40 ms 实测 20;J4 阶跃
50->51 <=120 ms 实测 98;J5 浮空 valid=0 不发散;J6 跳变 60 ms 内回
±0.05;另直流 8000 码 / 3-5 次谐波 20%/10% / UA->IA 回退)。
prot_set 90(+TC-PS-011 OF/UF 行 + 64 位 bad_mask bit39 + 旧档拒绝)、
prot_elm 47(+OF/UF 滞回与方向隔离 + run() 测频接线)。回归:phasor
57、iec104 94 全绿。

J4=120 ms 是"精度优先于响应"的显式取舍(Lead 批准):4 周波窗换
0.317->0.0101 Hz,测频响应 98 ms << OF/UF 定时限默认 1000 ms(占
总动作时间 <10%),暂态另有 J6 <=60 ms 兜底。

注册:freq.c 入 uvprojx algo 组 + check_registration.py SIM_EXEMPT
(纯算法、模拟器无消费路径,照 prot_set.c 先例)。

@build 0 错误 0 警告(Code=207468 RO=18304 RW=1068 ZI=153084)。
RAM:RW_IRAM1 96096->96728 B(+632 B,73.3%->73.8%)、RW_CCM 不变
57376 B(87.9%)、Flash 224596->226840 B(+2244 B,21.4%->21.6%);
台账随合并由 Lead 回填。stack_report:adcTask 静态最坏链 1320 B /
余量 1240 B(48%)改前后完全不变(freq_run 帧未进最坏链,无需抬栈);
protectTask 链 872 B / 余 1176 B(57%)同样不变。

gate: only mem-budget/mem-numbers pending lead ledger backfill
--------------------------------------------------------------
门禁实跑(pre-commit run --all-files,Lead 批准 --no-verify 提交):
28 Passed / 2 Failed,失败仅 mem-numbers 与 mem-budget 两条——均为
docs/内存管理 受管快照与本 .map 不一致(台账由 Lead 每次合并后
--write 回填,避免多分支同改受管表冲突)。其余含 doc-links /
coding-standard / no-mojibake / cppcheck / registration-check /
13 条主机单测钩子全 Passed(gitlint 属 commit-msg 阶段,不在该次
--all-files 内)。ci_local L0.5 全绿(freq 46/46 等)。
字节卫生自查(--no-verify 跳过卫生钩子,故手工核):17 个暂存文件
全部 BOM=无 / CR=0(纯 LF)/ U+FFFD=0;自研 .c/.h 去注释后字符串
字面量 100% ASCII(唯一非 ASCII 字面量在 test/host/iec104/iec104_test.c
的既有用例名,非本次改动行;test/ 不在 coding-standard 钩子范围)。
This commit is contained in:
2026-10-12 19:16:37 +08:00
parent 4262bf6146
commit 0264b49489
17 changed files with 1376 additions and 57 deletions
+166 -4
View File
@@ -4,8 +4,9 @@
*
* 被测对象:真实源文件(#include 原样纳入):
* prot_elm.c + prot_set.c(判据吃定值 RAM)
* phasor.c + sample_ring.c(prot_elm.c 链接依赖;本测试只走
* prot_elm_eval 纯判据路径,不依赖 run() 适配层)
* phasor.c + sample_ring.c + freq.c(prot_elm.c 链接依赖;
* TC-PE-001~010 走 prot_elm_eval 纯判据路径,TC-PE-013 走
* run() 适配层 = 合成波形跑真实 L2+测频链)
*
* 覆盖点(docs/算法/继电保护功能复现规划.md §3.3 L3-1/3/4/6、§8 验收):
* TC-PE-001 init:全状态清零、动作位图 0
@@ -19,10 +20,15 @@
* TC-PE-008 动作位图聚合
* TC-PE-009 守卫:state(NULL)/越界编号、name 越界
* TC-PE-010 跨上下文复归请求(B5):request 不当拍生效、下一拍 eval 头部消费
* TC-PE-011 过频 OF(B8/L3-5):越限 + RET_OF 滞回 + 定时限
* TC-PE-012 低频 UF(B8/L3-5):欠量方向 + RET_UF 滞回 + 方向隔离
* TC-PE-013 run() 接线(B8):测频驱动 OF 动作;幅值门失效 OF 复位且
* 相量类元件仍被评估(不整拍跳过)
*
* 通过:全部 TC PASS(main 返回 0)。
* 创建日期:2026-10-11
* 创建日期:2026-10-11(B5);2026-10-12 扩 OF/UF(B8)
*/
#include <math.h>
#include <stdint.h>
#include <stdio.h>
@@ -37,9 +43,12 @@ static int s_total = 0, s_passed = 0, s_failed = 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(全部不越限) */
/* 中性测量基线:电流 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;
@@ -49,6 +58,8 @@ static void mk_base(float m[PE_COUNT]) {
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) {
@@ -283,6 +294,154 @@ static void tc_request_reset(void) {
"TC-PE-010 next eval consumes request -> cleared in hysteresis band");
}
/* ===================== OF/UF(L3-5,B8)===================== */
/* TC-PE-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-PE-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-PE-011 OF 51.1: pickup before delay");
prot_elm_eval(1100u, m);
prot_elm_state(PE_OF, &st);
CHECK(st.asserted == 0u, "TC-PE-011 OF not asserted at 99ms");
prot_elm_eval(1101u, m);
prot_elm_state(PE_OF, &st);
CHECK(st.asserted != 0u, "TC-PE-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-PE-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-PE-011 OF below return threshold: drops out");
}
/* TC-PE-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-PE-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-PE-012 UF 48.9 (<49): pickup");
prot_elm_eval(2051u, m);
prot_elm_state(PE_UF, &st);
CHECK(st.asserted != 0u, "TC-PE-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-PE-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-PE-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-PE-012 OF picks at 52 (over direction)");
prot_elm_state(PE_UF, &st);
CHECK(st.picked == 0u, "TC-PE-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-PE-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-PE-013 run(): OF asserted from measured 52.5 Hz");
prot_elm_state(PE_UF, &st);
CHECK(st.picked == 0u, "TC-PE-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-PE-013 run(): OF resets when freq source loses amplitude");
prot_elm_state(PE_UV, &st);
CHECK(st.picked != 0u,
"TC-PE-013 run(): UV still evaluated with zero-amplitude (no whole-beat skip)");
}
int main(void) {
printf("=== Prot Elm Tests ===\n");
tc_init_clear();
@@ -295,6 +454,9 @@ int main(void) {
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);