Files
STM32F4-Base/docs/测试说明/相量层单元测试说明.md
T
wandering1 4c74cfa825 refactor(编号·批次二): 文档侧 194 处保护功能号改成功能名,定义表 7 张首格改语义标识
批次二文档半(道 gate-docs 执行)。先纠三个数的口径账,别再拿任何旧数当准:**234 处是准数** —— 卡片写
52(Lead 用了门禁 Checker 的引用集合,它**刻意排除属主文档内部的定义位**)、道里先报 265(裸正则把
`SDL2-2.32.10` 里的 `L2-2` 当成功能号,误报 10 处)。最终边界口径 = 带左边界 + 跳过反引号代码段。

· 19 只文件 HEAD 共 234 处 → 现 40 处,**40 处 100% 落在变更记录行**(冻结历史,按 Q3 一条没动),
  非变更记录残留 **0**;改写 163 行 / 16 只文档。站点最多:规划文档 110→37、相量层单测说明 25→1、
  采集与数据处理短板评测 21→0、App/algo/README 26→0。
  (`App/algo/README.md` 由上一笔代码提交带上车,路径归属已在该笔正文注明,内容是本批改写)
· **定义表首列由编号改成语义标识**(编号规范 §3.3「表首格就是短语」的完全形态):首列原为编号的表
  共 **7 张**(不是 5 张),删掉与之重复的「语义名」列;表头标签按域词区分——三张 L3 保护**元件**表
  保留 `| 元件 |`,四张 L1/L2 表用 `| 功能名 |`(把「元件」统一洗成功能名会丢掉领域信息)。
  七张表逐张核:表头竖线数 == 分隔行 == 首条数据行 == 列数+1;与 HEAD 逐块对拍**净新增不一致块 0**,
  并且正是这个对拍抓到第一版「三张表数据行已删列、表头没删」的破表。
· 7 处删号留下的残词/重复解释括号人工收口(如「L1-5(通道标定)」→「通道标定(一次系数 + 零漂)」),
  复扫待收口站点 0;措辞逐字取自映射表,无第二种说法。
· tools/arch_diagram 三只生成器 4 处(3 处 docstring + 1 处图形文案)与产物同批:重跑后**只有位图那张
  变 2 个 text 节点的换行位置**,画布 1580x740 与总行数不变,另两张逐字节未变;改前先干跑对照证明三张
  svg 原本与生成器一致(不在 stale 基线里)——`check_generated_fresh --only docs-svg` PASS。

本笔另含 **F-* 半批的文档面**(同一轮 `git add docs tools/arch_diagram AGENTS.md` 一并收进
,非误伤):BSD
函数调用关系里 9 处反引号紧邻指针、属主文档 §7 的停发句与导读行(**12 条定义行原样保留、不删登记族**)、
以及 `gen_net_instability_svg.py` 与它的产物 `网络不稳根因总览.svg`(「唯一读者红线」那条的框宽自适应
526 → 551,右缘 1411 未越界,画布 1560x1468 不变)——该半批的**代码注释面**在下一笔。

⚠ 因 `docs/任务规划/登记簿.md` 与 `docs/任务规划/UI补齐.md` 正被另一张卡(UI 叶数复核)改动,本笔**不含
那两只文件**,批次二的 §9 存量收尾与它们同批提交。
2026-10-27 09:05:24 +08:00

216 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 相量层单元测试说明(主机侧)
> **模块**:Phasor Layer(L2 相量计算,`App/algo/phasor.c` + `App/algo/sample_ring.c`)
> **测试类型**:**主机侧纯逻辑单元测试**(L0.5 层)——在 PC 上离线验证全周傅氏相量计算与
> 采样环缓冲,零硬件、零烧录、毫秒级、不占 MCU 的 RAM。
> **状态**:✅ 已落地(57 用例,`/W4` 0 告警,已挂 pre-commit 钩子)
> **日期**:2026-10-10
> **关联文档**:
> [`../../AGENTS.md`](../../AGENTS.md)(平台约束)、
> [`测试开发标准流程.md`](测试开发标准流程.md)(测试分层与门禁总览)、
> [`ADC输入层单元测试说明.md`](ADC输入层单元测试说明.md)(同批落地的 L1 姊妹篇)、
> [`../算法/傅氏算法详解.md`](../算法/傅氏算法详解.md)(**系数约定权威口径** §2.3)、
> [`../算法/继电保护功能复现规划.md`](../算法/继电保护功能复现规划.md)(§3.2 L2 设计点 / §10.2 术语表)、
> [`../硬件平台/ADC通道映射说明.md`](../硬件平台/ADC通道映射说明.md)(L1 输入口径 / 「双转换器无共同采样触发」约束)
---
## 1. 概述
### 1.1 为什么单独做主机侧单测
`App/algo/` 是本项目**首个纯算法层**(L1/L2 落点)——**无寄存器、无 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/),原样纳入(采样环,L2 输入)
└── #include "phasor.c" ← 生产源文件(App/algo/),原样纳入(相量计算,L2 核心)
```
`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(与 L1 采样节拍同源)。
> 系数表为**离线生成的 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` 变动 |
| **门禁层级** | **L0.5 层**(介于 L0 静态门禁与 L1 板端套件之间):主机侧、秒级、纯逻辑 |
| **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)。用例内的输出标签使用 `L2-x` 前缀,对应
> `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 L2 输入采样环(`sample_ring.c`)
| 用例 | 断言 | 设计点 |
|---|---|---|
| **TC-PHASOR-010** | `init` 清填充与帧计数;未攒满取窗 → -2;入环后取窗**按时序**(老 → 新);通道之间**互相独立**;填充量饱和于 `DEPTH` 但帧计数**不**饱和;回绕后取窗 == 最后 N 个值(有序);参数守卫:`n=0` / `n>DEPTH` / 越界通道 / `NULL out` / `NULL frame` / 错帧长 均拒绝 | L2 输入 |
### 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 路语义通道灌的是**同值**波形,仅为驱动 L2 本身。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"与
"逐实例复核"两条断言会**自动扩展到新实例**——但需同步更新该常量断言。
---
## 5. 变更记录
| 日期 | 变更 |
|---|---|
| 2026-10-10 | 创建;52 用例(TC-PH-001..022)全绿;覆盖系数表 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 实例层 / 滑动窗 / 双区交换 |
| 2026-10-10 | 补 TC-PH-030..034(`phasor_zero` 四字段清零 + `phasor_init` 双区清零 / 使用区与就绪标志复位 / 幂等)→ 57 用例;函数级覆盖审计 `App/algo` 三源共 20 函数、0 未覆盖;挂 pre-commit 钩子 `phasor-unit-test` |
| 2026-10-10 | §4.3 由"与规划表的一处待对齐"改为"实例数 4 → 5:规划表已对齐"——规划 §3.2 的 L2-2 行同步为 **5 个实例**(并加注 4 → 5 的来历),`傅氏算法详解.md` §7 的"4 个实例"标注为**参考项目**口径 |