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++;
}