# 相量层单元测试说明(主机侧) > **模块**:Phasor Layer(全周傅氏相量计算,`App/algo/phasor.c` + `App/algo/sample_ring.c`) > **测试类型**:**主机侧纯逻辑单元测试**——在 PC 上离线验证全周傅氏相量计算与 > 采样环缓冲,零硬件、零烧录、毫秒级、不占 MCU 的 RAM。 > **状态**:✅ 已落地(57 用例,`/W4` 0 告警,已挂 pre-commit 钩子) > **日期**:2026-10-10 > **关联文档**: > [`../../../AGENTS.md`](../../../AGENTS.md)(平台约束)、 > [`测试开发标准流程.md`](../流程与规范/测试开发标准流程.md)(测试分层与门禁总览)、 > [`ADC输入层单元测试说明.md`](ADC输入层单元测试说明.md)(同批落地的 语义取数与有效性姊妹篇)、 > [`../../算法/傅氏算法详解.md`](../../算法/傅氏算法详解.md)(**系数约定权威口径** §2.3)、 > [`../../算法/继电保护功能复现规划.md`](../../算法/继电保护功能复现规划.md)(§3.2 全周傅氏相量设计点 / §10.2 术语表)、 > [`../../硬件平台/ADC通道映射说明.md`](../../硬件平台/ADC通道映射说明.md)(语义取数与有效性输入口径 / 「双转换器无共同采样触发」约束) --- ## 1. 概述 ### 1.1 为什么单独做主机侧单测 `App/algo/` 是本项目**首个纯算法层**(语义取数与有效性/全周傅氏相量落点)——**无寄存器、无 RTOS、无文件 / 外设依赖**, 因此天然适合离线穷举。板端套件(`test/board/`)**无法验证本层的核心判据**: - 板端**无信号源**,无法注入已知**幅值 / 相位 / 频率**的标准波形,因而无法验证 "读数 = 设定值"这一根本判据(同「STM32 ADC1 启动与扫描」的替代口径,见 `docs/硬件平台/ADC通道映射说明.md` §8); - 谐波抑制、频率敏感度、滑动窗恒定性、双区无撕裂等特性在板上**无稳定激励源**可造。 主机侧可**合成任意波形**(含直流、任意次谐波、频偏、任意初相),把上述特性一次性钉死, 且**不占 MCU 的 RAM**(本项目 RAM 预算见 `docs/内存管理/RAM使用分析与优化方案.md`)。 ### 1.2 被测对象(**无需任何桩**) 两个源文件都是纯算法,测试单元直接 `#include` 即可,**没有桩目录**: ``` test/host/phasor/phasor_test.c ├── #include "sample_ring.c" ← 生产源文件(App/algo/),原样纳入(采样环,全周傅氏相量输入) └── #include "phasor.c" ← 生产源文件(App/algo/),原样纳入(相量计算,全周傅氏相量核心) ``` `include_dirs` 只需 `App/algo` + 本目录。源文件被纳入同一编译单元,故**静态量亦可直接观测** (白盒):`s_zone[2][N]`、`s_use_zone`、`s_ready`、`k_fft_sin` / `k_fft_cos`、`phasor_zero()`。 ### 1.3 关键约定:系数表已预乘 √2/N("直接即有效值") 权威口径 `docs/算法/傅氏算法详解.md` §2.3:系数为 `sin(2πk/N)·(√2/N)`、`cos(2πk/N)·(√2/N)`(N = 20)。因此 `fRe = a₁/√2`、`fIm = b₁/√2`,于是 - **`fAmSQ = √(fRe² + fIm²)` 直接就是有效值(RMS)**,无需再除 √2; - `fAm = √2 · fAmSQ` 是正弦峰值。 判据是归一化恒等式 **`Σsin² = Σcos² = 1/N`**(`TC-PHASOR-001/002` 逐项校验,防转录错)。 术语权威见 `docs/算法/继电保护功能复现规划.md` §10.2: `{fRe, fIm, fAm, fAmSQ}` = 实部 / 虚部 / **幅值** / **有效值**。 > 采样参数:N = 20 点/周波 @ 50 Hz → 1000 Hz,保护节拍 1 ms(与 语义取数与有效性采样节拍同源)。 > 系数表为**离线生成的 float 常量**(运行期不调 `sinf` / `cosf`),故主机侧与 MCU 侧 > 使用**逐位相同**的系数——这是本单测结论能外推到真机的前提。 ### 1.4 测试环境 | 依赖 | 版本 / 说明 | |---|---| | 编译器 | 本机 MSVC(VS2019/2022/2026 任一),由 `run_unit_test.py` 自动探测 | | Python | 仓库 `.venv` 或任意 python3(**纯标准库**) | | 硬件 | 无(不需要开发板 / 串口 / ST-LINK / 信号源) | 工具链探测 / `INCLUDE`-`LIB` 拼接 / 编译运行 / 退出码判定全部委托通用 harness [`../../../test/host/common/unit_test_harness.py`](../../../test/host/common/unit_test_harness.py), 本模块 `run_unit_test.py` 只**声明** `TestBuild`(源文件 / 汇总行标记)。 ### 1.5 运行方式 ```powershell # 仓库根目录(统一用 .venv) & '.venv\Scripts\python.exe' test/host/phasor/run_unit_test.py ``` 退出码:`0`=PASS / `1`=测试失败 / `2`=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 ### 1.6 回归接入 | 项 | 说明 | |---|---| | **触发范围** | `App/algo/phasor.c` / `phasor.h`、`App/algo/sample_ring.c` / `sample_ring.h`,或 `test/host/phasor/**`、`test/host/common/unit_test_harness.py` 变动 | | **门禁层级** | **主机侧纯逻辑单测**(介于静态门禁与板端测试套件之间):主机侧、秒级、纯逻辑 | | **pre-commit 钩子** | `phasor-unit-test`(本地钩子,`.pre-commit-config.yaml` §7d) | | **汇总行** | `=== Phasor Tests: 57/57 PASSED (failed=0) ===` | | **与板端测试的关系** | 互补:本单测保证**算法正确性**;板端保证 **1 kHz 采样节拍与 TPAFE 快照链路**(「周期 CONVST 重触发」/「TPAFE 快照消费」)正常 | --- ## 2. 测试用例 > 命名 `TC-PH-NNN`(PH = PHasor)。用例内的输出标签使用相量输出标签前缀,对应 > `docs/算法/继电保护功能复现规划.md` §3.2 的设计点编号。部分用例直接观测静态量(白盒)。 ### 2.1 全周傅氏系数表 | 用例 | 断言 | 设计点 | |---|---|---| | **TC-PHASOR-001** | 逐项:`sin[k] == sin(2πk/N)·(√2/N)`、`cos[k] == cos(2πk/N)·(√2/N)`(k = 0..19) | 「全周傅氏系数表」防转录错 | | **TC-PHASOR-002** | 归一化 `Σsin² == Σcos² == 1/N`;精确零点 `sin@0,10`、`cos@5,15`;`sin[5] == √2/N` | 「全周傅氏系数表」判据基础 | ### 2.2 「相量有效值输出」/「相位输出」基波幅值 / 有效值 / 相位 | 用例 | 断言 | 设计点 | |---|---|---| | **TC-PHASOR-003** | 纯基波:`fAmSQ == A/√2`(**有效值**)、`fAm == √2·fAmSQ`(幅值),误差 ≤ 1% | 相量有效值输出 | | **TC-PHASOR-004** | `atan2(fIm, fRe)` == 注入相角(6 组幅值 / 相位) | 相位输出 | ### 2.3 直流与谐波抑制 / 任意次谐波提取 | 用例 | 断言 | 设计点 | |---|---|---| | **TC-PHASOR-005** | 直流 + 2 / 3 / 5 次谐波被**完全滤除**(基波读数不变,误差 ≤ 1%,5 例) | 全周傅氏系数表 | | **TC-PHASOR-006** | 任意次谐波提取(**索引步进 = 谐波次数**):k = 1 / 2 / 5 从混合信号中提取;纯基波下 2 / 5 次分量 ≈ 0 | 「全周傅氏系数表」/「二次谐波/五次谐波提取入口」入口 | ### 2.4 频率敏感度 | 用例 | 断言 | 设计点 | |---|---|---| | **TC-PHASOR-007** | 幅值误差:50 Hz ≤ 0.1%、±1 Hz ≤ 1.5%、±2 Hz ≤ 3%(**最坏相位**) | 「相量有效值输出」/「等周波采样(软件测频 + 调频闭环)」依据 | ### 2.5 全周傅氏相量输入采样环(`sample_ring.c`) | 用例 | 断言 | 设计点 | |---|---|---| | **TC-PHASOR-010** | `init` 清填充与帧计数;未攒满取窗 → -2;入环后取窗**按时序**(老 → 新);通道之间**互相独立**;填充量饱和于 `DEPTH` 但帧计数**不**饱和;回绕后取窗 == 最后 N 个值(有序);参数守卫:`n=0` / `n>DEPTH` / 越界通道 / `NULL out` / `NULL frame` / 错帧长 均拒绝 | 全周傅氏相量输入 | ### 2.6 「基波相量实例」实例层 | 用例 | 断言 | 设计点 | |---|---|---| | **TC-PHASOR-020** | 实例数 == 5;实例表可取 `name`;越界实例 → `NULL`;未攒满一周波 `phasor_get()` → -2;越界 index / `NULL out` → -1;攒满 20 帧后 `get()` == 0 且实例 0 的 RMS == `A/√2`;谐波实例(`C04-IA-F2` / `C05-IA-F5`)在纯基波下 ≈ 0;**逐实例复核"实例窗 == 独立 `phasor_calc()` 结果"** | 基波相量实例 | ### 2.7 「每 1 ms 滑动窗刷新」滑动窗 | 用例 | 断言 | 设计点 | |---|---|---| | **TC-PHASOR-021** | 连续滑动 20 拍,基波读数**恒定**(误差 ≤ 1%) | 每 1 ms 滑动窗刷新 | ### 2.8 双区指针原子交换 | 用例 | 断言 | 设计点 | |---|---|---| | **TC-PHASOR-022** | 使用区逐次产出**交替** 0 → 1 → 0 → 1;**连测 60 拍**:每拍 `get()` == 当前环窗的独立重算(幅值每 20 拍在 100 / 10 之间切换 → 读到陈旧区会立刻不相等);扫描后结果仍可读 | 双区指针原子交换 | ### 2.9 `phasor_zero` / `phasor_init` 清零与幂等 | 用例 | 断言 | 设计点 | |---|---|---| | **TC-PHASOR-030** | 直接白盒调 `phasor_zero()` → 四字段(含 `fAm` / `fAmSQ`)全清零 | 静态辅助 | | **TC-PHASOR-031** | 先把双区写脏 → `phasor_init()` 后**双区全零** | init 清零 | | **TC-PHASOR-032** | `phasor_init()` 后使用区复位为 0 | init 复位 | | **TC-PHASOR-033** | `phasor_init()` 后就绪标志清(`phasor_get()` → -2) | init 复位 | | **TC-PHASOR-034** | 重复 `phasor_init()` 幂等 | 可重复调用 | **当前结果**:57/57 PASSED(`failed=0`)。 --- ## 3. 如何扩展用例 1. 加一个 `static void tc_xxx(void)`;用 `gen_sine()` 造正弦窗、`frame_at()` 造整帧(喂实例层); 2. 驱动实例层三步:`sample_ring_push_frame()` → `phasor_run()` → `phasor_get()`; 3. `CHECK(cond, "描述")` 断言;在 `main()` 里注册(并同步更新本文档 §2)。 > ⚠ 实例层是**全局态**(双区 + 采样环),跨用例必须先 `phasor_init()`——它会同时 > `sample_ring_init()`,等价于"刚上电"。否则上一用例的环内容会串进来。 --- ## 4. 已知边界与设计取舍 ### 4.1 采样环用"暴力重算",不做迭代 每来一个新点,`phasor_run()` 对 20 点**整体重算**(滑动窗)。迭代法(加减首尾项)会累积 浮点误差且难以保证与"整体重算"逐位一致;暴力法的代价是每拍 `N × 实例数` 次乘加 + 取模, 在 1 ms 节拍下仍有余量。本单测的 `TC-PHASOR-022`(每拍 `get()` == 独立重算)正是这条取舍的 **回归防线**——一旦有人改成迭代法,逐位一致性断言会立刻失败。 ### 4.2 主机侧合成波形**不代表真实硬件** `frame_at()` 给 9 路语义通道灌的是**同值**波形,仅为驱动 全周傅氏相量本身。V0.1 上各通道**不同刻** (V0.1 硬件缺陷:TPAFE 与 STM32 无共同采样触发),跨通道相量运算在本平台上不成立——本单测**不**做 任何跨通道合成断言(「线/负序/零序合成」受限,见 `docs/算法/继电保护功能复现规划.md` §1.6)。 ### 4.3 实例数 4 → 5:规划表已对齐(2026-10-10) **参考项目**(`APP/CLS100_Dsp/InstCalCell.c`)为 **4 个实例**——C01 基波 I/U、C02 零序基波、 C03 二次谐波、C04 五次谐波;**本项目**按 V0.1 **有效通道**(`UA`/`UN`/`IA`,见规划 §1.6: `UB/UC/IB/IC/IN` 无中点偏置 → 削顶不可用)展开为 **5 个实例** (`C01-IA-F1` / `C02-UA-F1` / `C03-UN-F1` / `C04-IA-F2` / `C05-IA-F5`)。 `docs/算法/继电保护功能复现规划.md` §3.2 的「基波相量实例」行已于 2026-10-10 同步为 **5 个实例** (并加注说明 4 → 5 的来历);本单测按**实现**断言(实例数 == 5),两者口径现已一致。 `docs/算法/傅氏算法详解.md` §7 的"4 个实例"已标注为**参考项目**口径。 ### 4.4 实例表用"数据驱动"而非硬编码 `k_inst[]` 每项为 `{name, sem, harm}`,`phasor_run()` 遍历该表取窗并计算。因此新增一个 相量实例(如「遥测谐波轨(可选)」的遥测谐波轨)**只需在表里加一行**,本单测的"实例数 == 5"与 "逐实例复核"两条断言会**自动扩展到新实例**——但需同步更新该常量断言。