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
+113 -20
View File
@@ -16,11 +16,12 @@
| `sample_ring.h` / `sample_ring.c` | L2 输入 | **采样环缓冲**:按语义通道保存最近 2 个周波(40 点)采样值,供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | L2 输入(为 L2-6 滑动窗供数) |
| `phasor.h` / `phasor.c` | L2 | **全周傅氏相量计算**:一周波 20 点单频 DFT → 基波/谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | L2-1 / L2-2 / L2-3 / L2-6 / L2-7 |
| `prot_set.h` / `prot_set.c` | L3 输入 | **保护定值模型**:Q10 定点 RAM 表(通讯地址对照参考 `g_tCommSetParaTab`)+ 范围校验 + 校验和封存自检闭锁 | `prot_set_init()` `prot_set_load_raw()` `prot_set_get_f()` `prot_set_selfcheck()` | §5 定值模型 / L5-1 |
| `prot_elm.h` / `prot_elm.c` | L3 | **保护元件框架**:通用"越限 + 返回系数滞回 + 定时限"核 + 数据驱动实例表(过流三段 / 过负荷 / 过压 / 欠压 / 零序过压) | `prot_elm_init()` `prot_elm_eval()` `prot_elm_run()` `prot_elm_active_mask()` | L3-1 / L3-3 / L3-4 / L3-6(软件侧) |
| `prot_elm.h` / `prot_elm.c` | L3 | **保护元件框架**:通用"越限 + 返回系数滞回 + 定时限"核 + 数据驱动实例表(过流三段 / 过负荷 / 过压 / 欠压 / 零序过压 / **过频 / 低频**) | `prot_elm_init()` `prot_elm_eval()` `prot_elm_run()` `prot_elm_active_mask()` | L3-1 / L3-3 / L3-4 / L3-5 / L3-6(软件侧) |
| `freq.h` / `freq.c` | L2 派生量 | **频率测量**:相邻两拍基波相量共轭乘积取角(每拍相位增量)+ 自适应窗长滑动平均 + 假值钳制 + 幅值门(UA→IA 源回退) | `freq_init()` `freq_run()` `freq_get()` `freq_valid()` | L3-5 测量侧(`docs/算法/继电保护功能复现规划.md` §4.2 测频范围) |
依赖关系(自下而上,无环):`adc_input.h`(纯枚举 + 声明)← `sample_ring` ← `phasor`;
`prot_set` **独立**(仅 `stdint.h`);`prot_elm` ← `prot_set` + `phasor`。
`sample_ring.c` / `phasor.c` / `adc_health.c` / `prot_set.c` / `prot_elm.c` **零平台依赖**
依赖关系(自下而上,无环):`adc_input.h`(纯枚举 + 声明)← `sample_ring` ← `phasor` ← `freq`;
`prot_set` **独立**(仅 `stdint.h`);`prot_elm` ← `prot_set` + `phasor` + `freq`。
`sample_ring.c` / `phasor.c` / `adc_health.c` / `prot_set.c` / `prot_elm.c` / `freq.c` **零平台依赖**
(只用 `math.h` / `stddef.h` / `stdint.h` 与本目录头);
`adc_input.c` 另依赖 `adc.h`(`hadc1`)与 `tpafe5160.h`(快照),主机侧以桩头替身。
@@ -38,12 +39,15 @@ flowchart TD
POLL --> RAW["adc_input_raw(ch) × 9<br/>按语义取原始码值"]
RAW --> RING["sample_ring_push_frame()<br/>整帧入环(40 点窗)"]
RING --> RUN["phasor_run()<br/>逐实例 20 点重算 + 双区切换"]
RUN --> FREQ["freq_run()<br/>共轭乘积相位增量 + 自适应窗均值(B8)"]
RUN --> GET["phasor_get(idx)"]
FREQ --> FGET["freq_get() / freq_valid()"]
GET --> CONS["消费者:adcTask 日志(现状)<br/>UI / 自检 / L3 保护任务(待接)"]
FGET --> PCONS["消费者:prot_elm_run() OF/UF 判据(B8)<br/>adcTask 心跳 freq 行"]
```
初始化落点:`adc_input_init()` 与 `phasor_init()` 由 `App/app/app_main.c` 的
`app_main_init()` 在**内核启动前**调用(三者 `*_init()` 均无 RTOS 依赖、幂等);
初始化落点:`adc_input_init()` 与 `phasor_init()`、`freq_init()` 由 `App/app/app_main.c` 的
`app_main_init()` 在**内核启动前**调用(各 `*_init()` 均无 RTOS 依赖、幂等);
`phasor_init()` 内部会连带 `sample_ring_init()`,等价于"刚上电"。
## 3. 逐文件说明
@@ -176,6 +180,19 @@ flowchart TD
(基址 0x8820);单测 TC-PS-006 保证地址不重复、`min ≤ def ≤ max`、投退项 0..1。
- ⚠ **默认值/范围为拟定值**(参考 EEPROM 出厂表不在本仓库),待对照修订——
见规划 §5.4 落实进度注。
- **B8 扩 8 项(32→40)**:`PS_OF_EN/OF_HZ/OF_DLY/UF_EN/UF_HZ/UF_DLY/RET_OF/RET_UF`
**尾追加**(编号不回收,枚举序 = 数组序 = 校验和覆盖序)。OF/UF 的逐项通讯地址
不在本仓库(参考保护定值段 0x8820~0x8882 仅示"含低压/低频"类目)→ 全部按
**本地项 comm_addr=0** 登记(104 C_SE 不可寻址),待对照参考表补。
频率返回系数**独立**(`RET_OF=0.99 / RET_UF=1.01`,返回门槛 = 定值×系数)——
频率滞回带与电气量 0.95/1.05 不同量级,勿共用。
- **B8 换代语义**:项数变 → blob 144→176 B、`PROT_SET_BLOB_VER` 1→2。B7 旧档
(ver=1/32 项)在新固件 parse 即被**版本/长度闸**拒绝 → 出厂默认;
`prot_set_store_load()` 返回码分流:**2 = 换代**(`prot_set_store.c` 打 INFO
"prot set blob version change, defaults in use",**不是** CORRUPT、不记自检),
−1 = 真损坏(defaultTask 打 ERROR + 自检留痕)。升级路径不得报成损坏。
- **bad_mask 随项数扩为 `uint64_t`**(bit i = 第 i 项越界;消费方仅主机单测,
板端闭锁走 `prot_set_locked()`)。
### 3.6 `prot_elm.c` / `prot_elm.h` — 保护元件框架(L3,P1 子集)
@@ -185,24 +202,88 @@ flowchart TD
| API | 干什么 | 注意 |
|---|---|---|
| `prot_elm_init()` | 复位状态 + 解析相量实例索引(按名前缀 `C01/C02/C03`) | 须在 `phasor_init()` 之后调 |
| `prot_elm_init()` | 复位状态 + 解析相量实例索引(按名前缀 `C01/C02/C03`) | 须在 `phasor_init()` 之后调;`src=FREQ` 行无相量绑定(`pidx=0xFF`,run 不查它) |
| `prot_elm_eval(now_ms, meas)` | **纯判据**:定值 RAM + 测量数组 → 全元件状态机 | 主机单测入口;NULL 测量按 0 |
| `prot_elm_run(now_ms)` | 适配层:`phasor_get()` 组测量 → eval | 任一相量未就绪 → **整拍跳过**(不引入陈旧判据) |
| `prot_elm_run(now_ms)` | 适配层:按实例表 `src` 组测量 → eval(`src=0` 取 `phasor_get()` rms,`src=1` 取 `freq_get()`) | 相量类任一未就绪 → **整拍跳过**(不引入陈旧判据);频率类无有效读数 → **按方向填"必不启动"值**(过量填 0 / 欠量填 `1e30` → 滞回比较恒假 → 元件复位),**不整拍跳过**——频率源缺失不得饿死电流/电压类元件 |
| `prot_elm_state()` / `prot_elm_active_mask()` | 状态快照 / 动作位图 | 供 L4 出口与心跳日志 |
**要点**
- **滞回**:已启动用返回门槛(`定值 × 返回系数`),未启动用动作门槛——
过量/欠量方向由实例表 `over` 位统一处理(`UV` 是唯一欠量实例)。
过量/欠量方向由实例表 `over` 位统一处理(`UV`/`UF` 为欠量实例)。
- **定时限**:`(uint32_t)(int32_t)(now - t0) >= dly` 差值法,tick 回绕安全;
`dly=0` → 启动同拍即动作。返回**即时**(参考带 10 ms 返回延时,P4 接出口硬件时再补)。
- **闭锁联动**:`prot_set_locked()`(L5-1)或投退=0 → 该元件状态整体复位、不判据。
- **V0.1 实例**(N-18):`OC1/OC2/OC3/OL`(IA)、`OV/UV`(UA)、`3U0`(UN 外部通道)。
零序过流(IN 走 STM32)与过频/低频(待频率算法)**故意不在表中**。
- **V0.1 实例**(N-18):`OC1/OC2/OC3/OL`(IA)、`OV/UV`(UA)、`3U0`(UN 外部通道)、
`OF/UF`(freq 层测频,B8)。零序过流(IN 走 STM32)**故意不在表中**。
- **OF/UF 出口可见性(B8)**:两元件**未单独映射 104 遥信点**(送检点表 = B10 口径),
其动作目前**只经 YX15 保护总聚合可见**(`iec104_pts` 的 fault-total 掩码含 OC/3U0,
OF/UF 动作会置总位但无独立分点)——README 与规划 §3.3 L3-5 行必须同此口径,勿写"独立遥信"。
- **板端接线已落地(B5,2026-10-12)**:`App/task/protect_task.c`(protectTask,
20 ms 节拍)消费 `prot_elm_run()` + L5-4 采样故障闭锁 + L5-1 校验和巡检;
跨上下文复归走 `prot_elm_request_reset()`(104 YK11 落请求、eval 头部消费)。
判据结果经 104 遥信上送(`iec104_pts` YX15~23/31)。→ `docs/算法/继电保护功能复现规划.md` §7
⚠ 实例数 7→9(B8):protectTask 启动锚点 `elm=` 与 `prot_elm_active_mask()` 位宽随之变,
`.serialtest` 锚点表未含 `elm=` 数字(已核),无需改配置。
### 3.7 `freq.c` / `freq.h` — 频率测量层(L2 派生量,L3-5 测量输入,B8)
**解决的问题**:过频/低频(L3-5)需要系统频率读数,而 L2 只出相量。本层把
"相邻两拍基波相量的相位差"变成"频率",并解决**非整周波窗长的 DFT 泄漏纹波**
这一决定精度的关键问题。
| API | 干什么 | 注意 |
|---|---|---|
| `freq_init()` | 清增量环/计数/读数/相位基准/有效标志(幂等) | 内核启动前可调,须在 `phasor_init()` 之后无要求 |
| `freq_run()` | 每拍步进:选源(UA→IA 过幅值门)→ 共轭乘积 `P=Z(m)·conj(Z(m-1))` 取角 → 假值钳制 → 自适应窗均值 | **仅 adcTask 上下文**,须在 `phasor_run()` 之后同拍调 |
| `freq_get(out)` | 读数(Hz) | 0 有读数(可能是保持值)/ −1 参数错 / −2 从未启动收敛(写 0) |
| `freq_valid()` | "本拍新鲜"标志 | 1 = 幅值门 OK 且已收敛;保护消费口径 = `get()==0 && valid()==1` |
| `freq_last()` | 最近有效读数 | 从未收敛返回 0.0 |
**算法要点(精度是实测驱动定稿的,两轮弯路记录在 freq.c 文件头)**
- **相位微分免解缠绕**:`arg(Z_now·conj(Z_prev))` 天然落在 (−π, π],纯音每拍
角**增大** 2πf/Fs(本层相量约定 φ=atan2(fIm,fRe)=窗首初相,窗每拍前滑一点);
45~55 Hz 段 |Δφ| ≈ 0.28~0.35 rad,距 ±π 裕度巨大。
- **自适应窗长**(核心):20 点矩形窗在非整周波时混入**负序泄漏项**
(45/55 Hz 处 |S₂/S₁| ≈ 3~5%),相量端轨迹成椭圆 → 逐拍相位差带周期
δ(φ) 纹波,周期 = fs/(2f) ≈ 9.1~11.1 拍。定长窗平均不整除纹波周期 →
扫频最坏误差 **0.317 Hz**(40 拍算术平均)/ **0.197 Hz**(60 拍复数和 Kay,
交叉项和角不随 K² 衰减,勿再尝试)。改 `K_eff = round(8·Fs/(2·f̂))`
(45~55 Hz 段 73~89 拍)使窗长恰为纹波周期整数倍 → 纹波相消,最坏 **0.0101 Hz**。
- **假值钳制带 35~65 Hz**(宽于系统合理带 40.5~60.5 = 参考 `CN_HERTZ_SOFTFR_MIN/MAX`,
规划 §10.1):单拍相位差本身带 ±5 Hz 纹波,钳制带贴边会**非对称丢弃纹波半周**
→ 均值偏置(实测 45 Hz 点 +0.16 Hz,已修)。35~65 仍拦得住窗滑 2 拍的 ≈2f
(90~110 Hz)与相位跳变假值;系统合理带由 OF/UF 定值范围在消费侧把关。
- **幅值门** `FREQ_AMP_MIN=50.0`(原始码值口径,**拟定待 L1-5 标定复核**):
低于门 = 无有效波形(浮空/PT 断线 rms≈0,相位无意义)→ 停更新、`valid` 清零、
读数保持 → 台架浮空不发散。信号消失过渡段(≤20 ms 残窗)读数漂移有界 ≤0.3 Hz。
- **源优先级 UA(C02)→IA(C01)**,按名前缀解析、每拍动态回退(台架只注电流亦可测频);
源切换即废相位基准重建(UA/IA 无共同相位基准,N-18)。
**精度判据(J1~J6,与 `test/host/freq/freq_test.c` 文件头同口径,数字为实测)**
| 判据 | 条件 | 门槛 | 实测最坏 |
|---|---|---|---|
| J1 扫频 | 纯音 45.00~55.00 Hz,0.5 Hz 步进(21 点) | **≤ 0.02 Hz** | **0.0101 Hz** @55.0 Hz |
| J2 噪声 | 幅值 1000 码、噪声 rms = 1% 幅值 | **≤ 0.10 Hz** | **0.0160 Hz** |
| J3 启动收敛 | 首个有效相量 → 首个有效读数 | **≤ 40 ms** | 20 ms(@39 ms 拍,相量 @19 ms) |
| J4 频率阶跃 | 50→51 Hz 进入 ±0.05 Hz 并保持 | **≤ 120 ms** | 98 ms(稳态 err 0.0053 Hz) |
| J5 零幅/浮空 | rms < 幅值门 | valid=0、不发散、保持漂移 ≤0.3 Hz | 漂移 0.13 Hz,无 NaN |
| J6 假值钳制 | 幅值+频率双跳变拍 | 60 ms 内回 ±0.05 Hz | 0.0000 Hz |
附加鲁棒性(同套件实测):直流偏置 8000 码 → err 0.0018 Hz;3/5 次谐波 20%/10%
→ err 0.0065 Hz;UA 无幅值落 IA → err 0.0020 Hz。
**J4 = 120 ms 是"精度优先于响应"的显式取舍(Lead 批准 2026-10-12)**:自适应窗
(M=8 ≈ 4 个泄漏纹波周期 ≈ 4 个信号周波)换来 0.317→0.0101 Hz,代价是频率骤变后
的窗冲刷(实测 98 ms)。OF/UF 定时限默认 **1000 ms** → 测频响应占保护总动作时间
**<10%**;暂态假值另有 J6(跳变后 ≤60 ms 回 ±0.05 Hz)兜底。故 120 ms 是工程整定,
不是判据失守——若将来要压响应,须同时重估精度(缩短窗 = 精度回退到 0.2~0.3 Hz 档)。
**板端可见性(DoD)**:adcTask 心跳/首拍日志追加一行
`freq(hb): f=5000 valid=1 (x0.01)`(MicroLIB 无 %f → ×0.01 定点,分辨率 0.01 Hz)。
正常带波形应 `valid=1` 且 `f≈5000`;浮空应 `valid=0`、`f` 为保持值。
## 4. 关键约定
@@ -224,17 +305,22 @@ flowchart TD
| `phasor.c` `s_zone[2][5]` `phasor_t` | 160 B |
| `adc_input.c` `s_stm32_raw[16]` + `s_tpafe_raw[8]` | 48 B |
| `adc_health.c` 内部状态(9 × uint32 + uint8) | ≈ 40 B |
| `prot_set.c` `s_val[31]` + 封存/位图/闭锁 | 136 B(**未接线前被链接器移除,实测 0**) |
| `prot_elm.c` `s_rt[7]`(16 B/实例) | 112 B(同上,实测 0) |
| `prot_set.c` `s_val[2][40]` + 封存/位图(64 位)/闭锁 | 336 B(B8:31→40 项) |
| `prot_elm.c` `s_rt[9]`(16 B/实例) | 144 B(B8:7→9 实例) |
| `freq.c` 相位增量环 `fq_d[96]` + 占位标志 + 状态 | **≈ 520 B**(B8 新增) |
| 运行期栈:`phasor_run()` 的 `win[20]` | 80 B/帧 |
| 运行期栈:`freq_run()` 局部 | ≈ 30 B/帧(无大局部数组) |
改容量(`SAMPLE_RING_CYCLES` / 实例数 / `ADC_SEM_COUNT`)前后各跑一次
改容量(`SAMPLE_RING_CYCLES` / 实例数 / `ADC_SEM_COUNT` / `FREQ_AVG_TAPS`)前后各跑一次
`& '.venv\Scripts\python.exe' scripts/stack_report.py`(口径见 `mem-budget` skill)。
**B8 实测(adcTask 静态最坏链)**:见 READY 消息与 `docs/内存管理/` 台账回填。
## 6. 构建与测试
- **挂载**:七个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`,`../App/algo` 已在 `<IncludePath>` 内。
本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路)。
- **挂载**:八个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`,`../App/algo` 已在 `<IncludePath>` 内。
本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路)——纯算法且模拟器无消费路径的
新文件(`prot_set.c` / `freq.c`)改在 `scripts/check_registration.py` 的 `SIM_EXEMPT` 登记豁免
(`registration-check` 钩子反向校验,N-23)。
- **主机侧单测(L0.5)**——生产源文件以 `#include` 原样纳入,无需改产品代码:
| 套件 | 命令 | pre-commit 钩子 |
@@ -242,19 +328,26 @@ flowchart TD
| 相量层(57 用例:系数表逐项复核 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 双区交换) | `& '.venv\Scripts\python.exe' test/host/phasor/run_unit_test.py` | `phasor-unit-test` |
| ADC 输入层(逐 Rank 注入互异码值钉死映射 + HAL 启动失败/超时分支) | `& '.venv\Scripts\python.exe' test/host/adc_input/run_unit_test.py` | `adc-input-unit-test` |
| 有效性校验(31 用例:100 ms 静默门限两侧 / 丢帧边沿 / 倒退锁存 / 错误增量 / tick 回绕 / NULL 守卫 / init 幂等) | `& '.venv\Scripts\python.exe' test/host/adc_health/run_unit_test.py` | `adc-health-unit-test` |
| 定值模型(30 用例:范围校验 / all-or-nothing 装载 / 封存-自检-闭锁 / Q10 往返 / 元数据表自检) | `& '.venv\Scripts\python.exe' test/host/prot_set/run_unit_test.py` | `prot-set-unit-test` |
| 元件框架(27 用例:滞回与定时限 / 欠量方向 / 投退 / 闭锁联动 / 时基回绕 / 位图聚合) | `& '.venv\Scripts\python.exe' test/host/prot_elm/run_unit_test.py` | `prot-elm-unit-test` |
| 定值模型(90 用例:范围校验 / all-or-nothing 装载 / 封存-自检-闭锁 / Q10 往返 / 元数据表自检 / B7 blob 往返与损坏拒绝 / **B8 OF-UF 行 + 64 位 bad_mask + 换代拒绝**) | `& '.venv\Scripts\python.exe' test/host/prot_set/run_unit_test.py` | `prot-set-unit-test` |
| 元件框架(47 用例:滞回与定时限 / 欠量方向 / 投退 / 闭锁联动 / 时基回绕 / 位图聚合 / **B8 OF-UF 滞回与方向隔离 + run() 测频接线**) | `& '.venv\Scripts\python.exe' test/host/prot_elm/run_unit_test.py` | `prot-elm-unit-test` |
| **频率测量(46 用例:J1 扫频 45~55 Hz / J2 噪声 / J3 启动收敛 / J4 阶跃 / J5 浮空不发散 / J6 假值钳制 / 直流·谐波抑制 / UA→IA 回退 / 守卫与幂等)** | `& '.venv\Scripts\python.exe' test/host/freq/run_unit_test.py` | `freq-unit-test`(**待登记**,合并时由 Lead 加钩子) |
改本目录任一 `.c/.h`,提交时对应钩子自动跑;详见 `docs/测试说明/相量层单元测试说明.md`。
- **板端**:`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍/心跳日志(`phasor(...)` 行、
`ring` / `skip` 计数、`adc1_rc` / `adc1_err`、`tgap`);健康判据 = 零 `[ERR]`
`freq(...)` 行、`ring` / `skip` 计数、`adc1_rc` / `adc1_err`、`tgap`);健康判据 = 零 `[ERR]`
(`adc_health_report` 的告警全部以 `[ERR]` 开头 → 误报会被闭环直接抓住)。
- **定值档升级 vs 损坏(B8,台架判读)**:`prot_set_store_load()` **rc=2 = 换代**
(旧固件/旧项数的档被版本闸拒绝)→ 只打 INFO `prot set blob version change, defaults in use`,
**不是** `[ERR] ... CORRUPT`、不记自检;**rc<0 = 真损坏** → `[ERR] prot set blob CORRUPT` + 自检留痕。
升级固件后首启看到"version change"一行属**预期**,勿当故障排查。
## 7. 新增一个算法模块的固定动作
1. 在本目录建 `xxx.c/h`,文件头写模块注释块(规范 V1.0),**尽量零平台依赖**以便免桩单测。
2. 用 `new-source-file` skill 注册到 `MDK-ARM/STM32F407-Demo.uvprojx`(目录已在 IncludePath,
若新建子目录则必须补 IncludePath)。
若新建子目录则必须补 IncludePath)。模拟器侧二选一:有消费路径 → 加
`simulator/CMakeLists.txt`;**纯算法无消费路径 → 加 `scripts/check_registration.py`
的 `SIM_EXEMPT`**(照 `prot_set.c` / `freq.c` 先例写理由),否则 `registration-check` 钩子拦。
3. 加 `test/host/<模块>/` 主机单测 + `.pre-commit-config.yaml` 钩子(照 `phasor-unit-test` 抄)。
4. 消费者写在 `App/task/`,本层只出"数";跨转换器运算的禁令见 §4。
+318
View File
@@ -0,0 +1,318 @@
/*
* 模块名称:频率测量层(L2 派生量)——实现
* 模块功能:见 freq.h。相邻两拍基波相量共轭乘积取角(= 每拍相位增量,免
* 显式解缠绕)+ **自适应窗长滑动平均** + 瞬时值钳制 + 幅值门。
* 输入 = phasor 使用区基波相量(UA 优先,UA 无幅值用 IA)。
*
* 精度问题的根与解(2026-10-12 单测实测驱动,数据见 test/host/freq 输出):
* 非整周波窗长让 20 点矩形窗 DFT 混入**负序泄漏项** S2(45/55 Hz 处
* |S2/S1| ≈ 3~5%),相量端轨迹是椭圆 → 逐拍相位差带周期纹波
* δ(φ),周期 = π/ω = fs/(2f) ≈ 9.1~11.1 拍。固定窗长平均时残差
* = [δ(m+K)-δ(m)]/K,K 不整除纹波周期 → 扫频最坏 0.2~0.32 Hz(两轮实测:
* 固定 40 拍算术平均 0.317 Hz;固定 60 拍复数和 Kay 0.197 Hz——Kay 的
* 交叉项和角同样不随 K² 衰减,勿再尝试)。
* **自适应窗长**:K_eff = round(M·Fs/(2·f_hat))(M=8,≈4 个泄漏纹波周期
* ≈4 个信号周波),使窗长恰为纹波周期的整数倍 → δ(m+K)≈δ(m),残差进入
* 0.01 Hz 量级。f_hat 用上一拍读数(收敛后误差 <0.1 Hz → K_eff 偏差 <1 拍,
* 自洽)。
* **精度优先于响应的显式取舍(Lead 批准 2026-10-12)**:4 周波窗换来
* 0.317→0.0101 Hz,代价是频率骤变后的窗冲刷(J4 阶跃整定 ≤120 ms,实测
* 98 ms)。OF/UF 定时限默认 1000 ms → 测频响应占保护总动作时间 <10%;
* 暂态假值另有 J6(跳变后 ≤60 ms 回 ±0.05 Hz)兜底。故 J4 的 120 ms 是
* 工程整定,不是判据失守。
* 另:单拍假值钳制带取 35~65 Hz(宽于系统合理带 40.5~60.5)——贴边钳制
* 会非对称丢弃纹波半周 → 均值偏置(实测 45 Hz 点 +0.16 Hz,已修)。
* 创建日期:2026-10-12(B8)
*/
#include "freq.h"
#include <math.h>
#include <stddef.h>
#include "phasor.h"
/* 圆周率(本层自带,避免依赖 math.h 的 M_* 扩展宏;ARMCC math.h 不保证定义) */
#define FREQ_PI 3.14159265358979f
/* 相位增量 -> 频率 换算系数:Fs/(2π)(Hz/rad) */
#define FREQ_HZ_PER_RAD (FREQ_FS_HZ / (2.0f * FREQ_PI))
/* 自适应窗长目标纹波周期数 M:K_eff = round(M·Fs/(2·f_hat))。
* 45~55 Hz 段 K_eff ∈ [73, 89] ⊂ [FREQ_AVG_MIN, FREQ_AVG_TAPS]。 */
#define FREQ_RIPPLE_M 8u
/* 源相量候选(优先级序):UA 基波 = "C02",IA 基波 = "C01"。
* 按**名字前缀**解析索引(同 prot_elm 口径,不写死下标),开机解析一次。 */
#define FREQ_SRC_N 2
static const char *const k_src_prefix[FREQ_SRC_N] = { "C02", "C01" };
/* 测频内部状态(BSS ≈ 96*(4+1) + 常数 ≈ 520 B) */
static float fq_d[FREQ_AVG_TAPS]; /* 每拍相位增量环(rad) */
static uint8_t fq_d_ok[FREQ_AVG_TAPS]; /* 对应拍是否有效(假值拍 = 0) */
static uint8_t fq_head; /* 环写指针 */
static uint8_t fq_count; /* 已入环样本数(饱和于 TAPS) */
static float fq_last; /* 最近有效读数(Hz) */
static float fq_prev_re; /* 上一拍源相量(实部) */
static float fq_prev_im; /* 上一拍源相量(虚部) */
static uint8_t fq_prev_valid; /* 上一拍是否有相量(首拍建基准) */
static uint8_t fq_src_idx[FREQ_SRC_N]; /* 候选实例索引;0xFF = 未找到 */
static uint8_t fq_src_resolved; /* 源已解析标志 */
static uint8_t fq_prev_src; /* 上一拍所用候选(切换即废基准) */
static uint8_t fq_started; /* 启动收敛完成(样本数 >= START_TAPS) */
static uint8_t fq_gate_ok; /* 本拍幅值门 OK 且已收敛 */
/*
* 函数功能:解析候选源相量实例索引(开机一次)
* 入口参数:无
* 返 回 值:无(结果写 fq_src_idx[],未找到 = 0xFF)
* 限定条件:phasor_init() 之后(开机一次)
* 函数说明:按实例名前缀匹配("C02"=UA 基波 / "C01"=IA 基波)且 harm==1,
* 与 prot_elm 同款朴素比较;**不写死实例下标**。
*/
static void fq_resolve_src(void) {
uint8_t p;
for (p = 0u; p < FREQ_SRC_N; p++) {
uint8_t i, n = phasor_inst_count();
fq_src_idx[p] = 0xFFu;
for (i = 0u; i < n; i++) {
const phasor_inst_t *pi = phasor_inst(i);
uint8_t j = 0u, hit = 1u;
if (pi == 0) {
continue;
}
while ((k_src_prefix[p][j] != '\0') && (pi->name[j] != '\0')) {
if (k_src_prefix[p][j] != pi->name[j]) {
hit = 0u;
break;
}
j++;
}
if ((hit != 0u) && (k_src_prefix[p][j] == '\0') && (pi->harm == 1u)) {
fq_src_idx[p] = i;
break;
}
}
}
fq_src_resolved = 1u;
}
/*
* 函数功能:把相位增量入环
* 入口参数:d - 本拍相位增量(rad);ok - 本拍是否有效(钳制失败 = 0)
* 返 回 值:无
* 限定条件:无
* 函数说明:假值拍也**占位**(ok=0),保持环时序 = 拍时序(自适应窗长
* 的"窗 = 最近 K_eff 拍"语义依赖这一点);求和时跳过占位拍。
*/
static void fq_push(float d, uint8_t ok) {
fq_d[fq_head] = d;
fq_d_ok[fq_head] = ok;
fq_head++;
if (fq_head >= FREQ_AVG_TAPS) {
fq_head = 0u;
}
if (fq_count < FREQ_AVG_TAPS) {
fq_count++;
if ((fq_started == 0u) && (fq_count >= FREQ_START_TAPS)) {
fq_started = 1u;
}
}
}
/*
* 函数功能:按自适应窗长求最近 K_eff 拍的相位增量均值 -> 频率
* 入口参数:无
* 返 回 值:频率读数(Hz);窗内无有效样本时返回 fq_last(保持)
* 限定条件:fq_started != 0
* 函数说明:K_eff = clamp(round(M·Fs/(2·f_hat)), FREQ_AVG_MIN, FREQ_AVG_TAPS)
* (f_hat = 上一读数,首轮用标称 50 Hz 起步)。窗 = 最近 K_eff 拍
* (含占位拍,占位不计和但计窗推进),恰为泄漏纹波周期整数倍
* -> δ(m+K)≈δ(m) 相消。每拍重算和(<=96 次加法,us 级)。
*/
static float fq_read_freq(void) {
float f_hat = (fq_last > 30.0f) ? fq_last : 50.0f;
uint16_t k_eff;
uint16_t i;
uint16_t idx;
float sum = 0.0f;
uint16_t n_ok = 0u;
/* K_eff = M*Fs/(2*f_hat):四舍五入 + 钳到环容量区间 */
k_eff = (uint16_t)(((float)FREQ_RIPPLE_M * FREQ_FS_HZ) / (2.0f * f_hat) + 0.5f);
if (k_eff < FREQ_AVG_MIN) {
k_eff = FREQ_AVG_MIN;
}
if (k_eff > FREQ_AVG_TAPS) {
k_eff = FREQ_AVG_TAPS;
}
if (k_eff > (uint16_t)fq_count) {
k_eff = (uint16_t)fq_count;
}
/* 从写指针回退 K_eff 拍求和(跳过占位拍) */
idx = fq_head;
for (i = 0u; i < k_eff; i++) {
if (idx == 0u) {
idx = FREQ_AVG_TAPS;
}
idx--;
if (fq_d_ok[idx] != 0u) {
sum += fq_d[idx];
n_ok++;
}
}
if (n_ok == 0u) {
return fq_last;
}
return (sum / (float)n_ok) * FREQ_HZ_PER_RAD;
}
/*
* 函数功能:频率测量层初始化(幂等)
* 入口参数:无
* 返 回 值:0 - 成功
* 限定条件:内核启动前调用(无 RTOS 依赖)
* 函数说明:清增量环/占位标志/计数/读数/相位基准/有效标志,并复位源解析。
*/
int freq_init(void) {
uint8_t i;
for (i = 0u; i < FREQ_AVG_TAPS; i++) {
fq_d[i] = 0.0f;
fq_d_ok[i] = 0u;
}
fq_head = 0u;
fq_count = 0u;
fq_last = 0.0f;
fq_prev_re = 0.0f;
fq_prev_im = 0.0f;
fq_prev_valid = 0u;
fq_src_idx[0] = 0xFFu;
fq_src_idx[1] = 0xFFu;
fq_src_resolved = 0u;
fq_prev_src = 0u;
fq_started = 0u;
fq_gate_ok = 0u;
return 0;
}
/*
* 函数功能:每拍测频步进
* 入口参数:无
* 返 回 值:无
* 限定条件:仅采集消费者任务上下文(adcTask),须在 phasor_run() 之后同拍调用
* 函数说明:流程 = 选源(UA->IA 过幅值门)-> 共轭乘积 P = Z_now·conj(Z_prev)
* 取角(免显式解缠绕)-> 钳制(假值拍占位 ok=0)-> 自适应窗均值。
* 相量未就绪/全部候选门失败:本拍不更新(读数保持)、清 gate、废基准。
* 基准在"断流"或**源切换**后重建(UA/IA 相位无共同基准)。
*/
void freq_run(void) {
phasor_t ph = { 0.0f, 0.0f, 0.0f, 0.0f };
float pr;
float pi;
float dphi;
float f_inst;
uint8_t pick = FREQ_SRC_N; /* FREQ_SRC_N = 无合格源 */
uint8_t p;
if (fq_src_resolved == 0u) {
fq_resolve_src();
}
/* 源选择(每拍动态):优先级 UA(C02) -> IA(C01),取第一个过幅值门的。
* 台架只注电流时 UA 门失败 -> 自动落 IA;两路都无幅值 = 无有效波形。 */
for (p = 0u; p < FREQ_SRC_N; p++) {
if ((fq_src_idx[p] == 0xFFu) || (phasor_get(fq_src_idx[p], &ph) != 0)) {
continue;
}
if (ph.fAmSQ >= FREQ_AMP_MIN) {
pick = p;
break;
}
}
if (pick >= FREQ_SRC_N) {
/* 相量未就绪或全部候选幅值门失败(浮空/断线 rms≈0 -> 相位无意义):
* 停更新、清 gate、废基准(读数保持) */
fq_prev_valid = 0u;
fq_gate_ok = 0u;
return;
}
if ((fq_prev_valid == 0u) || (pick != fq_prev_src)) {
/* 首拍 / 断流后首拍 / 源切换:只建基准,不出数 */
fq_prev_re = ph.fRe;
fq_prev_im = ph.fIm;
fq_prev_src = pick;
fq_prev_valid = 1u;
fq_gate_ok = 0u;
return;
}
/* P = Z_now * conj(Z_prev):arg(P) = 本拍相对上拍的相位增量
* (纯音 = +2*pi*f/Fs;|P| 大 = 信号强,钳制用角不受幅值影响) */
pr = (ph.fRe * fq_prev_re) + (ph.fIm * fq_prev_im);
pi = (ph.fIm * fq_prev_re) - (ph.fRe * fq_prev_im);
fq_prev_re = ph.fRe;
fq_prev_im = ph.fIm;
dphi = atan2f(pi, pr);
f_inst = dphi * FREQ_HZ_PER_RAD;
/* 假值钳制:单拍折算越出 35~65 Hz(采样间隙窗滑 2 拍 -> ≈2f=90~110、
* 相位跳变、深度畸变过渡段)-> 占位不入和,读数保持;基准已推进。
* ⚠ 钳制带刻意宽于系统合理带(见 freq.h FREQ_INST_* 注释):贴边会
* 非对称丢弃泄漏纹波半周造成均值偏置。 */
if ((f_inst < FREQ_INST_MIN_HZ) || (f_inst > FREQ_INST_MAX_HZ)) {
fq_push(0.0f, 0u);
} else {
fq_push(dphi, 1u);
}
if (fq_started != 0u) {
fq_last = fq_read_freq();
fq_gate_ok = 1u;
} else {
fq_gate_ok = 0u;
}
}
/*
* 函数功能:读测得频率(见 freq.h)
* 入口参数:out - 输出(NULL -> -1)
* 返 回 值:0 已有读数 / -1 参数错 / -2 从未启动收敛
* 限定条件:任意上下文(float 单字读原子)
*/
int freq_get(float *out) {
if (out == 0) {
return -1;
}
if (fq_started == 0u) {
*out = 0.0f;
return -2;
}
*out = fq_last;
return 0;
}
/*
* 函数功能:读"本拍有效"标志(见 freq.h)
* 入口参数:无
* 返 回 值:1 新鲜 / 0 未启动或幅值门失效
* 限定条件:无
*/
uint8_t freq_valid(void) {
return fq_gate_ok;
}
/*
* 函数功能:读最近一次有效读数(见 freq.h)
* 入口参数:无
* 返 回 值:Hz;从未收敛返回 0.0
* 限定条件:无
*/
float freq_last(void) {
return fq_last;
}
+121
View File
@@ -0,0 +1,121 @@
/*
* 模块名称:频率测量层(L2 派生量,L3-5 过频/低频的测量输入)
* 模块功能:由 L2 相量使用区的基波相量导出系统频率——相邻两拍基波相量取
* **共轭乘积** P = Z(m)·conj(Z(m-1))(其角 = 每拍相位增量,纯音 =
* +2πf/Fs,免显式解缠绕),再对**自适应窗长**的相位增量做滑动平均:
* K_eff = round(8·Fs/(2·f_hat)) ≈ 泄漏纹波周期 fs/(2f) 的整数倍
* -> 非整周波 DFT 负序泄漏造成的 ~10 ms 周期相位纹波相消
* (定长窗平均实测扫频误差 0.2~0.32 Hz,自适应窗进入 0.01 Hz 量级,
* 推导与实测见 freq.c 文件头与 test/host/freq 输出)。
* 覆盖规划 §3.3 L3-5 的测量侧与 §4.2 测频范围(45~55 Hz 建议段)。
*
* 原理与判据(详见 App/algo/README.md §3.7 与 test/host/freq/):
* - 采样率 Fs = 1000 Hz(TIM3 节拍,与 phasor 同源);45~55 Hz 段每拍
* 相位增量 ≈ 0.28~0.35 rad,距 ±π 缠绕边界裕度巨大,无歧义;
* - 瞬时值钳制:单拍相位差折算越出 35~65 Hz(**假值带**,宽于系统合理带
* 40.5~60.5=参考 CN_HERTZ_SOFTFR_MIN/MAX,规划 §10.1——贴边会非对称
* 丢弃泄漏纹波半周造成偏置,见 FREQ_INST_* 注释)判为采样间隙/相位
* 跳变假值,本拍不入窗;
* - 幅值门:源相量有效值 < FREQ_AMP_MIN 判"无有效波形"(浮空输入/PT 断线),
* 停止更新、valid 清零、读数保持 → 台架浮空 rms≈0 不发散;
* - 信号源优先级:UA 基波 → IA 基波(UA 无幅值时用 IA,台架只注电流亦可测频)。
*
* ⚠ 本层**纯算法**:不碰寄存器、不碰 RTOS、不自己取数(输入 = phasor 使用区)
* → 主机侧可免桩单测(L0.5,落实登记簿 A-4)。
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
* 作者:DTU 项目组
* 创建日期:2026-10-12(B8)
* 修改记录:
* v1.0 2026-10-12 DTU 项目组 创建:相位解缠绕测频 + 滑动平均 + 幅值门
*/
#ifndef FREQ_H
#define FREQ_H
#include <stdint.h>
/* 测频标称采样率(Hz):TIM3 1 kHz 节拍,与 phasor/sample_ring 同一时基 */
#define FREQ_FS_HZ 1000.0f
/* 单拍(逐拍相位差折算)**假值钳制带**(Hz):越出即判采样间隙/相位跳变/
* 深度畸变过渡段产生的粗大假值,本拍不入窗(读数保持)。
* ⚠ 与"系统合理频率带"分开定:非整周波 DFT 泄漏让单拍相位差本身带
* ±5 Hz 量级纹波(45/55 Hz 端实测),钳制带若贴住 40.5~60.5(参考
* CN_HERTZ_SOFTFR_MIN/MAX,规划 §10.1)会**非对称丢弃纹波半周** → 均值
* 偏置(实测 45 Hz 点 +0.16 Hz)。放宽到 35~65 Hz 后仍拦得住窗滑 2 拍的
* ≈2f(90~110 Hz)与相位跳变假值,又不吃掉正常纹波。
* 系统合理带 40.5~60.5 由消费侧把关(OF/UF 定值范围 50~55 / 45~50),
* 测量层不做截断——测量层只保证"测得准"。 */
#define FREQ_INST_MIN_HZ 35.0f
#define FREQ_INST_MAX_HZ 65.0f
/* 幅值门限(有效值,**原始码值口径**,L1-5 标定未实现):低于该值判
* "无有效波形"。⚠ 拟定值(远小于正常波形千级码值、远大于浮空噪声),
* 标定落地后须按工程量口径复核(同 prot_set 默认值"拟定"口径)。 */
#define FREQ_AMP_MIN 50.0f
/* 相位增量滑窗环容量(拍):96 拍 = 96 ms 上限。实际平均窗长**自适应**
* (freq.c fq_read_freq:K_eff = round(8·Fs/(2·f_hat)),45~55 Hz 段 73~89 拍),
* 使窗长恰为 DFT 负序泄漏纹波周期 fs/(2f) 的整数倍 -> 纹波相消。
* 保护定值延时 ≥100 ms,~90 ms 窗不影响动作时序判据。 */
#define FREQ_AVG_TAPS 96u
/* 自适应窗长下限(拍):K_eff 钳位区间下界(启动段窗未满时按已有拍数)。 */
#define FREQ_AVG_MIN 72u
/* 判"启动收敛完成"所需最少样本数(拍):20 拍 = 20 ms(首个有效相量后
* 21 拍内出首个读数)。窗未满时按已有样本求均值(部分窗),收敛快。 */
#define FREQ_START_TAPS 20u
/*
* 函数功能:频率测量层初始化
* 入口参数:无
* 返 回 值:0 - 成功
* 限定条件:内核启动前调用(无 RTOS 依赖);须在 phasor_init() 之后无要求
* (本层只在使用区就绪后才产出)
* 函数说明:清平均缓冲/计数/读数/有效标志;可重复调用(幂等)。
*/
int freq_init(void);
/*
* 函数功能:每拍测频步进(读 phasor 使用区 → 相位差 → 钳制 → 滑动平均)
* 入口参数:无
* 返 回 值:无
* 限定条件:仅采集消费者任务上下文(adcTask),**须在 phasor_run() 之后**
* 同拍调用(本拍相量已切换使用区)
* 函数说明:相量未就绪(-2)→ 本拍不更新;幅值门失效 → 停更新、valid 清零、
* 读数保持;单拍乘积角折算越出 40.5~60.5 Hz → 假值拍入 0(不进窗)。
* 窗未满 FREQ_AVG_TAPS 时按已有乘积求和(部分窗),收敛快。
*/
void freq_run(void);
/*
* 函数功能:读测得频率(工程量 Hz)
* 入口参数:out - 输出(NULL 直接返回 -1)
* 返 回 值:0 - 已有读数(out = 现值,可能为保持值);-1 参数错;
* -2 - 从未启动收敛(尚无任何有效读数,out 写 0)
* 限定条件:无(任意上下文;float 单字读,Cortex-M 原子)
* 函数说明:本函数判"**有没有数**";判"数新不新"(幅值门是否仍 OK)
* 用 freq_valid()。保护元件消费口径 = get==0 && valid==1。
*/
int freq_get(float *out);
/*
* 函数功能:读"本拍有效"标志(幅值门 OK 且已启动收敛)
* 入口参数:无
* 返 回 值:1 - 读数新鲜;0 - 未启动或幅值门失效(读数仍保持)
* 限定条件:无
* 函数说明:供心跳日志与保护元件判"测量可信"用。
*/
uint8_t freq_valid(void);
/*
* 函数功能:读最近一次有效读数(不判新鲜度)
* 入口参数:无
* 返 回 值:Hz;从未收敛返回 0.0
* 限定条件:无
* 函数说明:单字 float 读,原子。
*/
float freq_last(void);
#endif /* FREQ_H */
+39 -11
View File
@@ -8,6 +8,7 @@
#include "prot_elm.h"
#include "prot_set.h"
#include "phasor.h"
#include "freq.h"
#include <stddef.h>
@@ -15,21 +16,33 @@
typedef struct {
const char *name; /* 日志名 */
uint8_t over; /* 1 = 过量(越上限动作),0 = 欠量(越下限动作) */
uint8_t src; /* 测量源:0 = phasor 基波有效值,1 = freq 测频 */
prot_set_id_t en_id; /* 投退定值 */
prot_set_id_t set_id; /* 动作定值 */
prot_set_id_t dly_id; /* 延时定值(ms) */
prot_set_id_t ret_id; /* 返回系数定值(RET_OVER / RET_UNDER) */
const char *pprefix; /* 相量实例名前缀("C01"…),run() 适配层取数用 */
const char *pprefix; /* 相量实例名前缀("C01"…);src=1 时为 0(不用) */
} pe_cfg_t;
/* 测量源枚举(pe_cfg_t.src) */
#define PE_SRC_PHASOR 0u
#define PE_SRC_FREQ 1u
/* freq 无有效读数时给欠量元件的"必不启动"填充值(远大于任何欠量定值上限) */
#define PE_MEAS_BIG 1e30f
static const pe_cfg_t k_pe[PE_COUNT] = {
{ "OC1-IA", 1u, PS_OC1_EN, PS_OC1_CUR, PS_OC1_DLY, PS_RET_OVER, "C01" },
{ "OC2-IA", 1u, PS_OC2_EN, PS_OC2_CUR, PS_OC2_DLY, PS_RET_OVER, "C01" },
{ "OC3-IA", 1u, PS_OC3_EN, PS_OC3_CUR, PS_OC3_DLY, PS_RET_OVER, "C01" },
{ "OL-IA", 1u, PS_OL_EN, PS_OL_CUR, PS_OL_DLY, PS_RET_OVER, "C01" },
{ "OV-UA", 1u, PS_OV_EN, PS_OV_VOL, PS_OV_DLY, PS_RET_OVER, "C02" },
{ "UV-UA", 0u, PS_UV_EN, PS_UV_VOL, PS_UV_DLY, PS_RET_UNDER, "C02" },
{ "3U0-UN", 1u, PS_3U0_EN, PS_3U0_VOL, PS_3U0_DLY, PS_RET_OVER, "C03" },
{ "OC1-IA", 1u, PE_SRC_PHASOR, PS_OC1_EN, PS_OC1_CUR, PS_OC1_DLY, PS_RET_OVER, "C01" },
{ "OC2-IA", 1u, PE_SRC_PHASOR, PS_OC2_EN, PS_OC2_CUR, PS_OC2_DLY, PS_RET_OVER, "C01" },
{ "OC3-IA", 1u, PE_SRC_PHASOR, PS_OC3_EN, PS_OC3_CUR, PS_OC3_DLY, PS_RET_OVER, "C01" },
{ "OL-IA", 1u, PE_SRC_PHASOR, PS_OL_EN, PS_OL_CUR, PS_OL_DLY, PS_RET_OVER, "C01" },
{ "OV-UA", 1u, PE_SRC_PHASOR, PS_OV_EN, PS_OV_VOL, PS_OV_DLY, PS_RET_OVER, "C02" },
{ "UV-UA", 0u, PE_SRC_PHASOR, PS_UV_EN, PS_UV_VOL, PS_UV_DLY, PS_RET_UNDER, "C02" },
{ "3U0-UN", 1u, PE_SRC_PHASOR, PS_3U0_EN, PS_3U0_VOL, PS_3U0_DLY, PS_RET_OVER, "C03" },
/* 过频/低频(L3-5,B8):测量量 = freq 层测得频率(Hz),返回系数独立
* (RET_OF/RET_UF——频率滞回带与电气量不同量级,勿共用 0.95/1.05) */
{ "OF-F", 1u, PE_SRC_FREQ, PS_OF_EN, PS_OF_HZ, PS_OF_DLY, PS_RET_OF, 0 },
{ "UF-F", 0u, PE_SRC_FREQ, PS_UF_EN, PS_UF_HZ, PS_UF_DLY, PS_RET_UF, 0 },
};
/* 运行时状态(picked/asserted 的对外快照 + 延时起点 + 相量实例索引缓存) */
@@ -89,7 +102,8 @@ void prot_elm_init(void) {
s_rt[i].picked = 0u;
s_rt[i].asserted = 0u;
s_rt[i].t0 = 0u;
s_rt[i].pidx = pe_find_phasor(k_pe[i].pprefix);
/* src=FREQ 的行无相量绑定(pprefix=0)→ pidx 置未解析(run 不查它) */
s_rt[i].pidx = (k_pe[i].pprefix != 0) ? pe_find_phasor(k_pe[i].pprefix) : 0xFFu;
}
}
@@ -172,11 +186,15 @@ void prot_elm_eval(uint32_t now_ms, const float meas[PE_COUNT]) {
}
/*
* 函数功能:适配层——从 phasor 使用区取 rms 组装测量数组后调判据核
* 函数功能:适配层——按实例表 src 组装测量数组后调判据核
* 入口参数:now_ms - 单调时基(tick ms)
* 返 回 值:无
* 限定条件:仅保护消费者任务上下文;工程量↔码值换算 = 1(L1-5 标定未实现)
* 函数说明:任一相量未就绪(未满一周波)→ **整拍跳过**,不引入陈旧/半新测量。
* 函数说明:src=PHASOR 行任一相量未就绪(未满一周波)→ **整拍跳过**,
* 不引入陈旧/半新测量(B5 口径)。src=FREQ 行:freq 有有效读数
* (started 且幅值门 OK)→ 填测得频率;否则按方向填"必不启动"值
* (过量 0 / 欠量 PE_MEAS_BIG → 滞回比较恒假 → 元件复位),
* 不整拍跳过——频率源缺失不得饿死电流/电压类元件。
*/
void prot_elm_run(uint32_t now_ms) {
float meas[PE_COUNT];
@@ -184,6 +202,16 @@ void prot_elm_run(uint32_t now_ms) {
uint32_t i;
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
if (k_pe[i].src == PE_SRC_FREQ) {
float f;
if ((freq_get(&f) == 0) && (freq_valid() != 0u)) {
meas[i] = f;
} else {
meas[i] = (k_pe[i].over != 0u) ? 0.0f : PE_MEAS_BIG;
}
continue;
}
if ((s_rt[i].pidx == 0xFFu) || (phasor_get(s_rt[i].pidx, &p) != 0)) {
return;
}
+10 -2
View File
@@ -9,7 +9,13 @@
* OL 过负荷(A 相 IA,L3-3)
* OV/UV 过压/欠压(UA,L3-4)
* 3U0 零序过压(UN 外部零序电压通道,L3-6)
* ⚠ 过频/低频(L3-5)随频率测量算法(P2)落地,本表暂不含。
* OF/UF 过频/低频(测量量 = freq 层测得频率,L3-5,B8 落地)
*
* 测量数组口径:相量类实例 = 基波有效值(A/V);OF/UF = 测得频率(Hz)。
* run() 按实例表 src 字段取数(0=phasor rms,1=freq)。freq 无有效读数
* (未启动收敛或幅值门失效)→ 该行按方向填"必不启动"值(过量填 0、
* 欠量填极大值 → 滞回核比较恒假 → 元件复位不误动),**不整拍跳过**
* (否则浮空台架会把 OC/UV 等全部饿死,B5 行为回归)。
*
* 与参考系统的口径差异(有意简化,均已在规划文档登记):
* - 返回门槛 = 定值 × 返回系数(参考经 CN_SET_RET 独立定值,等价);
@@ -30,7 +36,7 @@
#include <stdint.h>
/* 元件实例编号(枚举序 = 实例表序 = 测量数组序) */
/* 元件实例编号(枚举序 = 实例表序 = 测量数组序;尾追加,编号不回收) */
typedef enum {
PE_OC1 = 0, /* 过流 Ⅰ 段(IA) */
PE_OC2, /* 过流 Ⅱ 段(IA) */
@@ -39,6 +45,8 @@ typedef enum {
PE_OV, /* 过压(UA) */
PE_UV, /* 欠压(UA,欠量方向) */
PE_3U0, /* 零序过压(UN) */
PE_OF, /* 过频(测量量 = freq 层测得频率,B8,L3-5) */
PE_UF, /* 低频(测量量 = freq 层测得频率,欠量方向,B8,L3-5) */
PE_COUNT
} prot_elm_id_t;
+17 -6
View File
@@ -47,6 +47,17 @@ const prot_set_meta_t g_prot_set_meta[PS_ID_COUNT] = {
{ 0x8825, 0u, PROT_SET_Q10(100.0), PROT_SET_Q10(1.0), PROT_SET_Q10(10000.0) },
{ 0x8826, 0u, PROT_SET_Q10(5.0), PROT_SET_Q10(1.0), PROT_SET_Q10(10.0) },
{ 0x0000, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* 全局软压板投退(YK12,B7) */
/* --- 过频 / 低频(L3-5,B8):地址全 0 = 本地项(参考逐项地址不在
* 本仓库,规划 §5.1 仅示"8820~8882 含低压/低频"类目);
* 默认值/范围**拟定**(DTU 馈线典型量级),待对照参考出厂表修订 --- */
{ 0x0000, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* OF 投退(本地) */
{ 0x0000, 0u, PROT_SET_Q10(51.0), PROT_SET_Q10(50.0), PROT_SET_Q10(55.0) }, /* OF 动作频率 Hz */
{ 0x0000, 0u, PROT_SET_Q10(1000.0), 0, PROT_SET_Q10(60000) }, /* OF 延时 ms */
{ 0x0000, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* UF 投退(本地) */
{ 0x0000, 0u, PROT_SET_Q10(49.0), PROT_SET_Q10(45.0), PROT_SET_Q10(50.0) }, /* UF 动作频率 Hz */
{ 0x0000, 0u, PROT_SET_Q10(1000.0), 0, PROT_SET_Q10(60000) }, /* UF 延时 ms */
{ 0x0000, 0u, PROT_SET_Q10(0.99), PROT_SET_Q10(0.9), PROT_SET_Q10(1.0) }, /* 过频返回系数 */
{ 0x0000, 0u, PROT_SET_Q10(1.01), PROT_SET_Q10(1.0), PROT_SET_Q10(1.1) }, /* 低频返回系数 */
};
static uint32_t s_val[2][PS_ID_COUNT]; /* 双缓冲值区(uint32 承载 int32,校验和按字覆盖)
@@ -55,7 +66,7 @@ static uint32_t s_val[2][PS_ID_COUNT]; /* 双缓冲值区(uint32 承载 int3
static volatile uint8_t s_act; /* 活跃区下标(0/1);交换 = 单字存储 */
static uint32_t s_sum; /* 封存的正码 */
static uint32_t s_sumn; /* 封存的反码 */
static uint32_t s_bad_mask; /* load_raw/parse 越界位图 */
static uint64_t s_bad_mask; /* load_raw/parse 越界位图(B8:40 项 → 64 位) */
static uint8_t s_locked; /* 定值非法 → 闭锁(L5-1) */
static uint8_t s_dirty; /* 未落盘定值改动(B7 持久化脏标志) */
@@ -105,7 +116,7 @@ void prot_set_init(void) {
* 入口参数:无
* 返 回 值:bit i = 第 i 项越界(load_raw 失败后有效)
*/
uint32_t prot_set_bad_mask(void) {
uint64_t prot_set_bad_mask(void) {
return s_bad_mask;
}
@@ -117,7 +128,7 @@ uint32_t prot_set_bad_mask(void) {
* 越界项经 prot_set_bad_mask() 指认。
*/
int prot_set_load_raw(const int32_t *vals) {
uint32_t mask = 0u;
uint64_t mask = 0u;
uint32_t cur_act;
uint32_t *shadow;
uint32_t i;
@@ -127,7 +138,7 @@ int prot_set_load_raw(const int32_t *vals) {
}
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
if ((vals[i] < g_prot_set_meta[i].min) || (vals[i] > g_prot_set_meta[i].max)) {
mask |= (1uL << i);
mask |= (1uLL << i);
}
}
s_bad_mask = mask;
@@ -390,7 +401,7 @@ int prot_set_store_parse(const uint8_t *p_buf, uint32_t len) {
uint32_t cur_act;
uint32_t *shadow;
uint32_t sum = 0u;
uint32_t mask = 0u;
uint64_t mask = 0u;
uint32_t i;
if ((p_buf == 0) || (len != PROT_SET_BLOB_SZ)) {
@@ -422,7 +433,7 @@ int prot_set_store_parse(const uint8_t *p_buf, uint32_t len) {
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
if (((int32_t)shadow[i] < g_prot_set_meta[i].min)
|| ((int32_t)shadow[i] > g_prot_set_meta[i].max)) {
mask |= (1uL << i);
mask |= (1uLL << i);
}
}
s_bad_mask = mask;
+22 -5
View File
@@ -70,6 +70,20 @@ typedef enum {
PS_CT_SEC, /* 0x8826 CT 二次额定(A) */
/* --- 全局软压板(B7):104 YK12 两段式投退;0 = 保护退出服务 --- */
PS_GLOBAL_EN, /* 本地项(无通讯地址,经 YK12 写、C_RD 不可读) */
/* --- 过频 / 低频(L3-5,B8):投退/动作频率/延时 + 各自返回系数 ---
* ⚠ 通讯地址:参考 g_tCommSetParaTab 的保护定值段(0x8820~0x8882)
* 含"低压/低频"类目(规划 §5.1),但 OF/UF 的**逐项地址不在本仓库**
* (参考 Comm 源文件未随仓库提供)→ 按"找不到对应即本地项"规则全部
* comm_addr=0 登记本地项(104 C_SE 不可寻址),待对照参考表后补。
* ⚠ 默认值/范围为**拟定**(同全表口径,规划 §5.4 落实进度注)。 */
PS_OF_EN, /* 过频投退(本地) */
PS_OF_HZ, /* 过频动作频率(Hz) */
PS_OF_DLY, /* 过频延时(ms) */
PS_UF_EN, /* 低频投退(本地) */
PS_UF_HZ, /* 低频动作频率(Hz) */
PS_UF_DLY, /* 低频延时(ms) */
PS_RET_OF, /* 过频返回系数(返回门槛 = 定值 × 系数,<1;本地) */
PS_RET_UF, /* 低频返回系数(返回门槛 = 定值 × 系数,>1;本地) */
PS_ID_COUNT
} prot_set_id_t;
@@ -97,8 +111,9 @@ void prot_set_init(void);
*/
int prot_set_load_raw(const int32_t *vals);
/* 越界位图(bit i = 第 i 项越界;load_raw 失败后有效) */
uint32_t prot_set_bad_mask(void);
/* 越界位图(bit i = 第 i 项越界;load_raw 失败后有效。B8:项数 32→40,
* 位图随项数扩为 64 位——消费方仅主机单测,板端闭锁走 prot_set_locked()) */
uint64_t prot_set_bad_mask(void);
/*
* 函数功能:单项写入(范围校验;**不自动重封校验和**——批量改完必须
@@ -137,10 +152,12 @@ void prot_set_dirty_mark(void); /* 写盘失败回置(等下轮重试)
/* ---------------- 持久化 blob(B7;纯逻辑,主机可单测) ----------------
* 格式(小端):magic 'PSET'(4B) + version u16 + count u16
* + values u32×count + sum32 u32 + ~sum32 u32
* 版本 1;count 必须等于 PS_ID_COUNT(定值表增项 → 旧 blob 整体作废,
* 编号不回收但格式换代,宁可用出厂默认也不半装载)。 */
* 版本 2(B8:定值表 32→40 项,OF/UF 尾追加);count 必须等于 PS_ID_COUNT
* (定值表增项 → 旧 blob 整体作废,编号不回收但格式换代,宁可用出厂默认
* 也不半装载)。ver 1 旧档在 parse 即被版本闸拒绝 → 出厂默认 + 日志
* (措辞 = "version change",非 CORRUPT,见 prot_set_store/default_task)。 */
#define PROT_SET_BLOB_MAGIC 0x50534554uL /* 字节序 P,S,E,T */
#define PROT_SET_BLOB_VER 1u
#define PROT_SET_BLOB_VER 2u
#define PROT_SET_BLOB_HDR_SZ 8u /* magic(4)+ver(2)+cnt(2) */
#define PROT_SET_BLOB_SZ (PROT_SET_BLOB_HDR_SZ + 4u * (uint32_t)PS_ID_COUNT + 8u)
+7
View File
@@ -25,6 +25,7 @@
#include "tpafe5160.h"
#include "adc_input.h" /* L1 采集层:adc_input_init()(语义映射 + STM32 ADC1 扫描) */
#include "phasor.h" /* L2 相量层:phasor_init()(系数表 + 实例 + 双区交换) */
#include "freq.h" /* L2 派生量:freq_init()(相位解缠绕测频,B8) */
#include "prot_set.h" /* L3 定值模型:prot_set_init()(B5,内核前封存) */
#include "prot_elm.h" /* L3 元件框架:prot_elm_init()(B5,解析相量索引) */
#include "sys_log.h"
@@ -242,6 +243,12 @@ int app_main_init(void) {
(void)phasor_init();
DBG_INFO("phasor layer init: OK (inst=%u)", (unsigned)phasor_inst_count());
/* 4c-2) 测频层(L2 派生量,B8)初始化:清平均窗/相位基准/有效标志。
* 纯算法(输入 = phasor 使用区),消费在 adcTask(freq_run 每拍)
* 与 protectTask(OF/UF 判据经 prot_elm_run 取数)。 */
(void)freq_init();
DBG_INFO("freq layer init: OK");
/* 4d) 保护定值 + 元件层初始化(B5):出厂默认定值封存校验和 + 元件状态
* 复位并解析相量索引(须在 phasor_init 之后)。内核前完成 → iec104
* 定值读/protectTask 首拍前定值必已装载。判据消费在 protectTask。 */
+43 -5
View File
@@ -13,21 +13,50 @@
#include <stddef.h>
#define DBG_TAG "[CFG]"
#include "dbg_log.h"
#include "cfg_file_store.h"
#include "prot_set.h"
#define PROT_SET_STORE_PATH "/prot_set.bin"
/* blob 收发缓冲(静态:defaultTask 栈深已是全项目最深链,勿再压 144 B 局部) */
/* blob 收发缓冲(静态:defaultTask 栈深已是全项目最深链,勿再压局部 blob;
* B8 定值表 32→40 项 → blob 144→176 B,随 PROT_SET_BLOB_SZ 自动适配) */
static uint8_t s_blob[PROT_SET_BLOB_SZ];
/*
* 函数功能:从小端缓冲读 16 位值(仅头部判别用,与 prot_set.c 内部同字节序)
* 入口参数:p - 读位置
* 返 回 值:值 uint16_t
*/
static uint16_t s_ld16(const uint8_t *p) {
return (uint16_t)((uint32_t)p[0] | ((uint32_t)p[1] << 8));
}
/*
* 函数功能:从小端缓冲读 32 位值
* 入口参数:p - 读位置
* 返 回 值:值 uint32_t
*/
static uint32_t s_ld32(const uint8_t *p) {
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16)
| ((uint32_t)p[3] << 24);
}
/*
* 函数功能:开机装载定值 blob
* 入口参数:无
* 返 回 值:0 已装载 / 1 文件缺失或读失败(保持默认)/ -1 损坏被拒
* 返 回 值:0 已装载 / 1 文件缺失或读失败(保持默认)/
* 2 版本或项数换代(旧档被拒,保持默认;本函数已落 INFO 日志)/
* -1 损坏被拒
* 函数说明:读失败与"文件不存在"同归 1——启动期卷锁无竞争(本任务独占
* bring-up),try-lock 失败即真 I/O 故障,此时保持出厂默认并让
* 调用方按"无档"处理是安全侧(定值仍封存自洽,L5-1 不误锁)。
* **换代与损坏分开**(B8):magic 完好但 ver/cnt 与当前固件不符 =
* 定值表升级(如 B8 32→40 项),是**预期升级路径**不是损坏 →
* 归 2 并打 INFO("version change"措辞),调用方不得按 CORRUPT
* 报 ERROR/自检留痕;magic 或校验对被破坏才是 -1。
*/
int prot_set_store_load(void) {
size_t nread = 0u;
@@ -35,10 +64,19 @@ int prot_set_store_load(void) {
if (cfg_store_read(PROT_SET_STORE_PATH, s_blob, sizeof(s_blob), &nread) != 0) {
return 1;
}
if (prot_set_store_parse(s_blob, (uint32_t)nread) != 0) {
return -1;
if (prot_set_store_parse(s_blob, (uint32_t)nread) == 0) {
return 0;
}
return 0;
/* 拒绝原因分流:头部 magic 完好 + 版本/项数与当前固件不符 = 换代 */
if ((nread >= (size_t)PROT_SET_BLOB_HDR_SZ)
&& (s_ld32(s_blob) == (uint32_t)PROT_SET_BLOB_MAGIC)
&& ((s_ld16(s_blob + 4u) != (uint16_t)PROT_SET_BLOB_VER)
|| (s_ld16(s_blob + 6u) != (uint16_t)PS_ID_COUNT))) {
DBG_INFO("prot set blob version change (ver=%u cnt=%u), defaults in use",
(unsigned)s_ld16(s_blob + 4u), (unsigned)s_ld16(s_blob + 6u));
return 2;
}
return -1;
}
/*
+15
View File
@@ -84,6 +84,7 @@
#include "adc_input.h" /* L1 采集层:adc_input_poll()/adc_input_raw()(按语义取数) */
#include "sample_ring.h" /* L2 输入:sample_ring_push_frame()(采样环缓冲) */
#include "phasor.h" /* L2 相量层:phasor_run()/phasor_get()(全周傅氏) */
#include "freq.h" /* L2 派生量:freq_run()/freq_get()(测频,B8) */
#include "adc_health.h" /* L1-6:采样有效性校验(静默/丢帧/单调性/错误增量) */
#include "tpafe5160.h"
#include "task_watchdog.h"
@@ -169,6 +170,17 @@ static void log_phasors(const char *tag) {
(long)(p.fAmSQ * 10.0f));
}
}
/* 测频行(B8,L3-5 测量侧台架可见性):同样 x0.01 定点(分辨率 0.01 Hz)。
* valid=0 = 未启动收敛或幅值门失效(浮空输入 rms≈0),此时 f 为保持值
* (从未收敛则 0)——判读:正常带波形应 valid=1 且 f≈5000。 */
{
float f = 0.0f;
(void)freq_get(&f);
DBG_INFO("freq(%s): f=%ld valid=%u (x0.01)",
tag, (long)(f * 100.0f), (unsigned)freq_valid());
}
}
/*
@@ -314,6 +326,9 @@ void adc_task_func(void *arg) {
if (all_ok != 0u) {
(void)sample_ring_push_frame(frame, (uint8_t)ADC_SEM_COUNT);
phasor_run();
/* L2 派生量测频(B8):同拍紧随 phasor_run——本拍使用区已
* 切换,freq 读的就是本拍相量(1 ms 同上下文,无竞态) */
freq_run();
} else {
s_frames_skip++;
}
+5
View File
@@ -1007,6 +1007,11 @@
<FileType>1</FileType>
<FilePath>..\App\algo\prot_elm.c</FilePath>
</File>
<File>
<FileName>freq.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\algo\freq.c</FilePath>
</File>
</Files>
</Group>
<Group>
+1
View File
@@ -81,6 +81,7 @@ SIM_EXEMPT = [
("App/algo/phasor.c", "L2 相量纯算法,模拟器无消费路径"),
("App/algo/prot_elm.c", "L3 保护元件纯算法,模拟器无消费路径"),
("App/algo/prot_set.c", "L5 保护定值纯算法,模拟器无消费路径"),
("App/algo/freq.c", "L2 派生测频纯算法(输入=phasor 使用区),模拟器无消费路径"),
("App/cfg/prot_set_store.c", "定值持久化 MCU 胶水(依赖 prot_set 纯算法),模拟器无消费路径"),
("App/algo/sample_ring.c", "L2 环形缓冲纯算法,模拟器无消费路径"),
("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"),
+374
View File
@@ -0,0 +1,374 @@
/*
* 模块名称:频率测量层(L2 派生量,B8)主机侧单元测试
* 模块功能:在 PC 上对 App/algo/freq.c + phasor.c + sample_ring.c 做离线验证。
*
* 被测对象:真实源文件(#include 原样纳入,纯算法无需桩):
* ../../../App/algo/sample_ring.c
* ../../../App/algo/phasor.c
* ../../../App/algo/freq.c
*
* 精度判据(与 App/algo/README.md §3.7 同口径,规划 §4.2 测频范围的落实):
* J1 纯音扫频 45.00~55.00 Hz(0.5 Hz 步进):|f_meas - f_true| <= 0.02 Hz
* (实测最坏 0.0101 Hz @55.0 Hz,判据留 2 倍裕度)
* J2 噪声工况(幅值 1000 码、噪声 rms = 1% 幅值):|f_meas - f_true| <= 0.10 Hz
* (实测最坏 0.016 Hz,判据留 ~6 倍裕度)
* J3 启动收敛:首个有效相量后 <= 40 ms 出有效读数
* J4 频率阶跃 50->51 Hz:<= 120 ms 进入 ±0.05 Hz 并保持(自适应窗
* K_eff≈73~89 拍,阶跃后窗冲刷决定物理下限;OF/UF 定时限 >=100 ms
* 叠加其上,总动作时间仍 <220 ms,符合定时限保护常规)
* J5 零幅/浮空(rms < 幅值门):valid=0、读数保持、不发散(无 NaN/无跳变;
* 信号消失过渡段 <=20 ms 窗内残窗参与均值,读数漂移有界 <=0.3 Hz)
* J6 假值钳制:相位/幅值跳变拍产生的单拍折算越出 35~65 Hz 假值带 -> 丢弃,
* 读数在跳变后 60 ms 内回到 ±0.05 Hz
*
* 覆盖点:
* TC-FR-001 50 Hz 收敛时序(J3)+ 稳态精度(J1 子集)
* TC-FR-002 扫频 45~55 Hz @0.5 Hz(J1,逐点判 + 最坏值打印)
* TC-FR-003 噪声鲁棒(J2,最坏值打印)
* TC-FR-004 频率阶跃 50->51(J4)
* TC-FR-005 零幅浮空不发散(J5)+ 恢复后重新收敛
* TC-FR-006 直流偏置抑制(全周傅氏滤直流 -> 测频不受影响)
* TC-FR-007 3/5 次谐波污染抑制(相量基波选择性)
* TC-FR-008 相位跳变假值钳制(J6)
* TC-FR-009 UA 无幅值 -> 自动落 IA 源
* TC-FR-010 init 幂等 / get 参数守卫 / 未收敛 -2
* TC-FR-011 幅值门中途失效 -> valid 清零 + 读数保持(J5 中途版)
*
* 通过:全部 TC PASS(main 返回 0)。
* 创建日期:2026-10-12(B8)
*/
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#define FR_PI 3.14159265358979323846
#define FR_FS 1000.0
/* ===================== 断言 ===================== */
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 "sample_ring.c"
#include "phasor.c"
#include "freq.c"
/* ===================== 波形驱动 ===================== */
/* 全局采样时刻(拍):驱动函数按此生成"当前时刻"的样本,跨调用连续 */
static uint32_t s_t = 0u;
/* 确定性伪随机(LCG):噪声可复现 */
static uint32_t s_rng = 0x12345678u;
static double s_noise_unit(void) { /* [-0.5, 0.5) */
s_rng = s_rng * 1103515245u + 12345u;
return (double)((s_rng >> 8) & 0xFFFFFFu) / (double)0x1000000u - 0.5;
}
/*
* 函数功能:合成一拍整帧并跑一遍 L2+测频链
* 入口参数:f - 信号频率(Hz)
* amp - 基波峰值(码值)
* dc - 直流偏置(码值)
* h3, h5 - 3/5 次谐波峰值(码值)
* noise_rms- 高斯样近似噪声 rms(码值,均匀分布近似)
* ua_en - UA 通道是否带波形(0 = 只挂 0 码,测 IA 回退)
* 返 回 值:无
* 限定条件:无
* 函数说明:9 路语义通道同波形(除 ua_en=0 时 UA 置 0);每拍
* push_frame -> phasor_run -> freq_run,与 adcTask 同序。
*/
static void drive_beat(double f, double amp, double dc, double h3, double h5,
double noise_rms, uint8_t ua_en) {
float frame[ADC_SEM_COUNT];
double t = (double)s_t / FR_FS;
double v = amp * sin(2.0 * FR_PI * f * t)
+ h3 * sin(2.0 * FR_PI * 3.0 * f * t)
+ h5 * sin(2.0 * FR_PI * 5.0 * f * t)
+ dc
+ s_noise_unit() * noise_rms * 1.7320508; /* 均匀 -> rms 折算 */
uint8_t c;
for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) {
frame[c] = (float)v;
}
if (ua_en == 0u) {
frame[ADC_SEM_UA] = 0.0f; /* UA 无波形(门失败),IA 仍有 */
}
if (f == 0.0) {
for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) {
frame[c] = 0.0f; /* 浮空:全 0 帧 */
}
}
(void)sample_ring_push_frame(frame, (uint8_t)ADC_SEM_COUNT);
phasor_run();
freq_run();
s_t++;
}
/*
* 函数功能:连续驱动 n 拍
*/
static void drive_n(uint32_t n, double f, double amp, double dc, double h3,
double h5, double noise_rms, uint8_t ua_en) {
uint32_t i;
for (i = 0u; i < n; i++) {
drive_beat(f, amp, dc, h3, h5, noise_rms, ua_en);
}
}
/*
* 函数功能:复位整条链(采样时刻 + 三层状态)
*/
static void chain_reset(void) {
s_t = 0u;
s_rng = 0x12345678u;
(void)phasor_init(); /* 连带 sample_ring_init */
(void)freq_init();
}
/* ===================== TC-FR-001:50 Hz 收敛时序 + 稳态精度 ===================== */
static void tc_converge_50(void) {
float f = 0.0f;
uint32_t first_valid_t = 0u, phasor_ready_t = 0u;
uint32_t i;
int got = 0;
chain_reset();
for (i = 0u; i < 200u; i++) {
drive_beat(50.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
if (phasor_ready_t == 0u) {
phasor_t p;
if (phasor_get(1u, &p) == 0) {
phasor_ready_t = i; /* 首个有效相量拍 */
}
}
if (!got && (freq_valid() != 0u) && (freq_get(&f) == 0)) {
first_valid_t = i;
got = 1;
}
}
CHECK(got, "TC-FR-001 freq becomes valid at 50 Hz");
CHECK((first_valid_t >= phasor_ready_t)
&& ((first_valid_t - phasor_ready_t) <= 40u),
"TC-FR-001 J3: first valid reading <= 40 ms after first phasor");
(void)freq_get(&f);
printf(" [DATA] 50 Hz steady: f=%.4f err=%.4f Hz (first valid @%u ms, phasor @%u ms)\n",
(double)f, (double)f - 50.0, (unsigned)first_valid_t, (unsigned)phasor_ready_t);
CHECK(fabsf(f - 50.0f) <= 0.01f, "TC-FR-001 J1(50Hz): |err| <= 0.01 Hz");
}
/* ===================== TC-FR-002:扫频 45~55 Hz @0.5 Hz(J1) ===================== */
static void tc_sweep(void) {
double worst = 0.0, worst_f = 0.0;
int ok = 1;
int k;
for (k = 0; k <= 20; k++) {
double f_true = 45.0 + 0.5 * (double)k;
float f_meas = 0.0f;
double err;
chain_reset();
drive_n(150u, f_true, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
CHECK(freq_get(&f_meas) == 0, "TC-FR-002 freq valid at each sweep point");
err = fabs((double)f_meas - f_true);
if (err > worst) { worst = err; worst_f = f_true; }
if (err > 0.02) { ok = 0; }
}
printf(" [DATA] sweep 45.00~55.00 Hz step 0.5: worst |err| = %.4f Hz @ %.1f Hz (J1 <= 0.02)\n",
worst, worst_f);
CHECK(ok, "TC-FR-002 J1: |f_meas - f_true| <= 0.02 Hz over 45~55 Hz");
}
/* ===================== TC-FR-003:噪声鲁棒(J2) ===================== */
static void tc_noise(void) {
double worst = 0.0;
int ok = 1;
int k;
for (k = 0; k < 5; k++) {
double f_true = 45.0 + 2.5 * (double)k; /* 45/47.5/50/52.5/55 */
float f_meas = 0.0f;
double err;
chain_reset();
drive_n(200u, f_true, 1000.0, 0.0, 0.0, 0.0, 10.0, 1u); /* rms = 1% 幅值 */
(void)freq_get(&f_meas);
err = fabs((double)f_meas - f_true);
if (err > worst) { worst = err; }
if (err > 0.10) { ok = 0; }
}
printf(" [DATA] noise rms=1%% amp: worst |err| = %.4f Hz (J2 <= 0.10)\n", worst);
CHECK(ok, "TC-FR-003 J2: |err| <= 0.10 Hz with 1% noise");
}
/* ===================== TC-FR-004:频率阶跃 50->51(J4) ===================== */
static void tc_step(void) {
float f = 0.0f;
uint32_t settle = 0u;
uint32_t i;
int ok = 1;
chain_reset();
drive_n(150u, 50.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
(void)freq_get(&f);
printf(" [DATA] pre-step f=%.4f\n", (double)f);
/* 阶跃拍 = t=150;判"进入 ±0.05 且保持" */
for (i = 0u; i < 100u; i++) {
drive_beat(51.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
(void)freq_get(&f);
if (settle == 0u) {
if (fabsf(f - 51.0f) <= 0.05f) {
settle = i + 1u; /* 阶跃后第 N 拍进入带内 */
}
} else if (fabsf(f - 51.0f) > 0.05f) {
ok = 0; /* 进入后又出带 */
}
}
printf(" [DATA] step 50->51: settle = %u ms, final f=%.4f err=%.4f\n",
(unsigned)settle, (double)f, (double)f - 51.0);
CHECK((settle != 0u) && (settle <= 120u), "TC-FR-004 J4: settle <= 120 ms");
CHECK(ok, "TC-FR-004 J4: stays within +/-0.05 Hz after settle");
CHECK(fabsf(f - 51.0f) <= 0.01f, "TC-FR-004 J1(51Hz): steady |err| <= 0.01 Hz");
}
/* ===================== TC-FR-005:零幅浮空不发散(J5) ===================== */
static void tc_zero_amp(void) {
float f = 123.0f;
uint32_t i;
int never_valid = 1, no_nan = 1;
chain_reset();
for (i = 0u; i < 500u; i++) {
drive_beat(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1u); /* 全 0 帧(浮空 rms=0) */
if (freq_valid() != 0u) { never_valid = 0; }
(void)freq_get(&f);
if (!(f == f)) { no_nan = 0; } /* NaN 自比较为假 */
}
CHECK(never_valid, "TC-FR-005 J5: zero-amplitude never asserts valid");
CHECK(no_nan && (f == 0.0f), "TC-FR-005 J5: reading stays 0, no divergence/NaN");
/* 恢复:重新注入波形后应再次收敛 */
drive_n(150u, 49.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
(void)freq_get(&f);
printf(" [DATA] recover after float: f=%.4f err=%.4f\n", (double)f, (double)f - 49.0);
CHECK((freq_valid() != 0u) && (fabsf(f - 49.0f) <= 0.01f),
"TC-FR-005 recovers and meets J1 after waveform returns");
}
/* ===================== TC-FR-006:直流偏置抑制 ===================== */
static void tc_dc_offset(void) {
float f = 0.0f;
chain_reset();
drive_n(150u, 50.3, 1000.0, 8000.0, 0.0, 0.0, 0.0, 1u); /* DC = 8000 码 */
(void)freq_get(&f);
printf(" [DATA] DC=8000: f=%.4f err=%.4f\n", (double)f, (double)f - 50.3);
CHECK(fabsf(f - 50.3f) <= 0.01f, "TC-FR-006 DC offset rejected: |err| <= 0.01 Hz");
}
/* ===================== TC-FR-007:3/5 次谐波污染 ===================== */
static void tc_harmonics(void) {
float f = 0.0f;
chain_reset();
drive_n(150u, 48.0, 1000.0, 0.0, 200.0, 100.0, 0.0, 1u); /* 20%/10% 谐波 */
(void)freq_get(&f);
printf(" [DATA] h3=20%% h5=10%%: f=%.4f err=%.4f\n", (double)f, (double)f - 48.0);
CHECK(fabsf(f - 48.0f) <= 0.01f, "TC-FR-007 harmonics rejected: |err| <= 0.01 Hz");
}
/* ===================== TC-FR-008:相位跳变假值钳制(J6) ===================== */
static void tc_phase_jump(void) {
float f = 0.0f;
uint32_t i;
int ok = 1;
chain_reset();
drive_n(150u, 50.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
/* 跳变:瞬时切到相位偏移 60 度的等价频率段——用 1 拍 55 Hz 高幅制造
* 窗内畸变(f_inst 出带),随后回 50 Hz,判 60 ms 内回带 */
drive_beat(55.0, 4000.0, 0.0, 0.0, 0.0, 0.0, 1u); /* 幅值/频率双跳变 */
for (i = 0u; i < 60u; i++) {
drive_beat(50.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
(void)freq_get(&f);
if (i >= 59u) {
if (fabsf(f - 50.0f) > 0.05f) { ok = 0; }
}
}
printf(" [DATA] after jump: f=%.4f err=%.4f (recover window 60 ms)\n",
(double)f, (double)f - 50.0);
CHECK(ok, "TC-FR-008 J6: recovers to +/-0.05 Hz within 60 ms after jump");
CHECK(freq_valid() != 0u, "TC-FR-008 J6: still valid after clamp path");
}
/* ===================== TC-FR-009:UA 无幅值 -> IA 回退 ===================== */
static void tc_src_fallback(void) {
float f = 0.0f;
chain_reset();
drive_n(150u, 52.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 0u); /* UA 置 0,IA 带波形 */
(void)freq_get(&f);
printf(" [DATA] UA=0 IA=1000: f=%.4f err=%.4f\n", (double)f, (double)f - 52.0);
CHECK((freq_valid() != 0u) && (fabsf(f - 52.0f) <= 0.01f),
"TC-FR-009 falls back to IA source when UA below amplitude gate");
}
/* ===================== TC-FR-010:init 幂等 / 参数守卫 ===================== */
static void tc_init_guards(void) {
float f = 5.0f;
chain_reset();
CHECK(freq_get(&f) == -2, "TC-FR-010 get before convergence -> -2");
CHECK((f == 0.0f), "TC-FR-010 get -2 writes 0 to out");
CHECK(freq_get(NULL) == -1, "TC-FR-010 NULL out -> -1");
CHECK(freq_valid() == 0u, "TC-FR-010 valid=0 before convergence");
CHECK(freq_last() == 0.0f, "TC-FR-010 last=0 before convergence");
(void)freq_init();
CHECK(freq_get(&f) == -2, "TC-FR-010 freq_init idempotent (still -2)");
}
/* ===================== TC-FR-011:幅值门中途失效 -> 保持 ===================== */
static void tc_gate_hold(void) {
float f_before = 0.0f, f_after = 0.0f;
chain_reset();
drive_n(150u, 50.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
(void)freq_get(&f_before);
CHECK(freq_valid() == 1u, "TC-FR-011 valid before gate loss");
drive_n(50u, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1u); /* 波形消失 */
CHECK(freq_valid() == 0u, "TC-FR-011 J5: valid cleared on gate loss");
(void)freq_get(&f_after);
printf(" [DATA] hold: before=%.4f held=%.4f delta=%.4f\n",
(double)f_before, (double)f_after, (double)(f_after - f_before));
CHECK(fabsf(f_after - f_before) <= 0.3f,
"TC-FR-011 J5: reading bounded (|delta| <= 0.3 Hz) during gate loss");
}
int main(void) {
printf("=== Freq Tests (B8: phase-unwrap frequency measurement) ===\n");
tc_converge_50();
tc_sweep();
tc_noise();
tc_step();
tc_zero_amp();
tc_dc_offset();
tc_harmonics();
tc_phase_jump();
tc_src_fallback();
tc_init_guards();
tc_gate_hold();
printf("\n=== Freq Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""频率测量层(L2 派生量,B8)主机侧单元测试运行器(L0.5,纯标准库)。
做什么:用本机 MSVC 离线编译 test/host/freq/freq_test.c(#include 纳入生产
源文件 App/algo/freq.c + phasor.c + sample_ring.c),验证相位解缠绕
测频的精度判据 J1~J6(45~55 Hz 扫频 / 噪声 / 收敛 / 阶跃 / 浮空 /
假值钳制 / 源回退),判据数字见 freq_test.c 文件头与 App/algo/README.md。
用法(仓库根目录):
& '.venv\\Scripts\\python.exe' test/host/freq/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误。
设计依据:docs/算法/继电保护功能复现规划.md §3.3(L3-5)、§4.2(测频范围)。
创建日期:2026-10-12(B8)
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
BUILD = TestBuild(
name="Freq Tests",
src=os.path.join(HERE, "freq_test.c"),
include_dirs=[
os.path.join(ROOT, "App", "algo"),
HERE,
],
summary_marker="=== Freq Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+1
View File
@@ -53,6 +53,7 @@ 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"
#include "iec104_pts.c"
#include "iec104_link.c"
+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);
+86 -4
View File
@@ -20,9 +20,12 @@
* (位精确 + 封存自洽 + parse 不置脏)
* TC-PS-010 损坏 blob 整表拒绝(all-or-nothing):值区位翻转/坏 magic/
* 截断/超长/坏版本/伪造自洽越界 → 全部拒绝且活跃值区一字不动
* TC-PS-011 OF/UF 定值行(B8):40 项 / blob 版本 2 / 本地项地址 0 /
* 默认值与范围 / 返回系数独立性 / 64 位 bad_mask(bit 39)/
* B7 旧档(32 项)换代拒绝
*
* 通过:全部 TC PASS(main 返回 0)。
* 创建日期:2026-10-11
* 创建日期:2026-10-11;2026-10-12 扩 OF/UF(B8)
*/
#include <stdint.h>
#include <stdio.h>
@@ -94,7 +97,7 @@ static void tc_load_raw(void) {
tbl[PS_OC3_DLY] = PROT_SET_Q10(60001.0); /* 越上限 */
tbl[PS_OL_CUR] = PROT_SET_Q10(2.5); /* 合法改动 */
CHECK(prot_set_load_raw(tbl) == -1, "TC-PS-004 bad table rejected");
CHECK(prot_set_bad_mask() == (1uL << PS_OC3_DLY),
CHECK(prot_set_bad_mask() == (1uLL << PS_OC3_DLY),
"TC-PS-004 bad_mask points at the offending item");
CHECK(prot_set_get_f(PS_OL_CUR) > 1.49f && prot_set_get_f(PS_OL_CUR) < 1.51f,
"TC-PS-004 rejected load keeps OLD table (all-or-nothing)");
@@ -136,7 +139,7 @@ static void tc_meta_sanity(void) {
CHECK(ok_range, "TC-PS-006 meta: min<=def<=max for all items");
CHECK(ok_ctl, "TC-PS-006 meta: ctl items are 0..1");
CHECK(ok_addr, "TC-PS-006 meta: comm addresses unique");
CHECK((uint32_t)PS_ID_COUNT <= 32u, "TC-PS-006 item count fits bad_mask bitmap");
CHECK((uint32_t)PS_ID_COUNT <= 64u, "TC-PS-006 item count fits 64-bit bad_mask (B8: 40)");
}
/* TC-PS-007 */
@@ -235,11 +238,89 @@ static void tc_store_corrupt(void) {
(uint32_t)(g_prot_set_meta[PS_OC1_CUR].max + 1));
s_forge_tail(blob, len);
CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-010 forged consistent out-of-range rejected");
CHECK(prot_set_bad_mask() == (1uL << PS_OC1_CUR), "TC-PS-010 bad_mask points at item");
CHECK(prot_set_bad_mask() == (1uLL << PS_OC1_CUR), "TC-PS-010 bad_mask points at item");
CHECK(prot_set_get(PS_OC1_CUR) == PROT_SET_Q10(10.0), "TC-PS-010 defaults still intact");
CHECK(prot_set_selfcheck() == 0, "TC-PS-010 active table still self-consistent after all rejects");
}
/* TC-PS-011(B8:OF/UF 定值行 + 40 项 + 64 位 bad_mask + 换代拒绝) */
static void tc_of_uf_meta(void) {
uint8_t blob[PROT_SET_BLOB_SZ];
uint32_t len = 0u;
prot_set_init();
/* 结构:40 项、blob 版本 2、尺寸随项数自适应 */
CHECK((uint32_t)PS_ID_COUNT == 40u, "TC-PS-011 PS_ID_COUNT == 40 (32 + 8 OF/UF)");
CHECK(PROT_SET_BLOB_VER == 2u, "TC-PS-011 blob version bumped to 2 (B8)");
CHECK((uint32_t)PROT_SET_BLOB_SZ == (PROT_SET_BLOB_HDR_SZ + 4u * 40u + 8u),
"TC-PS-011 blob size = hdr + 4*40 + tail");
/* 地址:参考逐项地址不在本仓库 -> 8 项全部本地项(comm_addr=0,104 不可寻址) */
CHECK((g_prot_set_meta[PS_OF_EN].comm_addr == 0u)
&& (g_prot_set_meta[PS_OF_HZ].comm_addr == 0u)
&& (g_prot_set_meta[PS_OF_DLY].comm_addr == 0u)
&& (g_prot_set_meta[PS_UF_EN].comm_addr == 0u)
&& (g_prot_set_meta[PS_UF_HZ].comm_addr == 0u)
&& (g_prot_set_meta[PS_UF_DLY].comm_addr == 0u)
&& (g_prot_set_meta[PS_RET_OF].comm_addr == 0u)
&& (g_prot_set_meta[PS_RET_UF].comm_addr == 0u),
"TC-PS-011 OF/UF items all local (comm_addr=0, pending reference table)");
/* 投退 = ctl 项(0/1),其余 = 模拟量 Q10 */
CHECK((g_prot_set_meta[PS_OF_EN].is_ctl == 1u) && (g_prot_set_meta[PS_UF_EN].is_ctl == 1u)
&& (g_prot_set_meta[PS_OF_HZ].is_ctl == 0u) && (g_prot_set_meta[PS_RET_UF].is_ctl == 0u),
"TC-PS-011 EN items are ctl, HZ/DLY/RET are Q10 analog");
/* 出厂默认(拟定值)与范围 */
CHECK(prot_set_get(PS_OF_EN) == PROT_SET_Q10(1.0), "TC-PS-011 OF EN default 1");
CHECK(prot_set_get(PS_UF_EN) == PROT_SET_Q10(1.0), "TC-PS-011 UF EN default 1");
CHECK(prot_set_get(PS_OF_HZ) == PROT_SET_Q10(51.0), "TC-PS-011 OF default 51.0 Hz");
CHECK(prot_set_get(PS_UF_HZ) == PROT_SET_Q10(49.0), "TC-PS-011 UF default 49.0 Hz");
CHECK(prot_set_get(PS_OF_DLY) == PROT_SET_Q10(1000.0), "TC-PS-011 OF default 1000 ms");
CHECK(prot_set_get(PS_UF_DLY) == PROT_SET_Q10(1000.0), "TC-PS-011 UF default 1000 ms");
CHECK(prot_set_get(PS_RET_OF) == PROT_SET_Q10(0.99), "TC-PS-011 RET_OF default 0.99");
CHECK(prot_set_get(PS_RET_UF) == PROT_SET_Q10(1.01), "TC-PS-011 RET_UF default 1.01");
CHECK((g_prot_set_meta[PS_OF_HZ].min == PROT_SET_Q10(50.0))
&& (g_prot_set_meta[PS_OF_HZ].max == PROT_SET_Q10(55.0)),
"TC-PS-011 OF range 50.0~55.0 Hz");
CHECK((g_prot_set_meta[PS_UF_HZ].min == PROT_SET_Q10(45.0))
&& (g_prot_set_meta[PS_UF_HZ].max == PROT_SET_Q10(50.0)),
"TC-PS-011 UF range 45.0~50.0 Hz");
/* 频率返回系数与电气量系数独立:返回**门槛** = 定值×系数(Q10 乘积
* /1024)——OF 门槛 < 动作门槛、UF 门槛 > 动作门槛(方向相反) */
CHECK(((int64_t)prot_set_get(PS_OF_HZ) * (int64_t)prot_set_get(PS_RET_OF)) / 1024
< (int64_t)prot_set_get(PS_OF_HZ),
"TC-PS-011 OF return threshold (set x 0.99) below pickup");
CHECK(((int64_t)prot_set_get(PS_UF_HZ) * (int64_t)prot_set_get(PS_RET_UF)) / 1024
> (int64_t)prot_set_get(PS_UF_HZ),
"TC-PS-011 UF return threshold (set x 1.01) above pickup");
/* 投退可经 set_sealed 投/退(YK 口径同 GLOBAL_EN;Q10(2)=2048 越界拒) */
CHECK(prot_set_set_sealed(PS_OF_EN, 0) == 0, "TC-PS-011 OF EN set_sealed OK");
CHECK(prot_set_get_ctl(PS_OF_EN) == 0u, "TC-PS-011 OF EN readable after seal");
CHECK(prot_set_set_sealed(PS_OF_EN, PROT_SET_Q10(2)) == -1, "TC-PS-011 OF EN rejects Q10(2)");
CHECK(prot_set_selfcheck() == 0, "TC-PS-011 no lock after OF/UF seal ops");
/* 64 位 bad_mask:末项(bit 39 > 31)越界可指认 */
(void)prot_set_set_sealed(PS_OC1_CUR, PROT_SET_Q10(11.0));
(void)prot_set_dirty_take();
CHECK(prot_set_store_serialize(blob, sizeof(blob), &len) == 0, "TC-PS-011 serialize ok");
s_put32(blob + PROT_SET_BLOB_HDR_SZ + 4u * (uint32_t)PS_RET_UF,
(uint32_t)(g_prot_set_meta[PS_RET_UF].max + 1));
s_forge_tail(blob, len);
CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-011 out-of-range RET_UF rejected");
CHECK(prot_set_bad_mask() == (1uLL << PS_RET_UF),
"TC-PS-011 bad_mask bit 39 addressable (64-bit mask, B8)");
CHECK(prot_set_get(PS_RET_UF) == PROT_SET_Q10(1.01), "TC-PS-011 default RET_UF intact");
/* 换代语义:B7 旧档(ver=1 / 32 项 = 144 B)在新固件必被拒 -> 出厂默认 */
CHECK(prot_set_store_parse(blob, 8u + 4u * 32u + 8u) == -1,
"TC-PS-011 legacy 32-item blob length rejected");
CHECK(prot_set_selfcheck() == 0, "TC-PS-011 active table intact after legacy reject");
}
int main(void) {
printf("=== Prot Set Tests ===\n");
tc_init();
@@ -252,6 +333,7 @@ int main(void) {
tc_global_plate_dirty();
tc_store_roundtrip();
tc_store_corrupt();
tc_of_uf_meta();
printf("\n=== Prot Set Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);