Files
STM32F4-Base/docs/测试说明/单元测试/相量层单元测试说明.md
wandering1 f754d59218 docs(测试说明): 按用途分六个子目录并同步全库引用
分层:流程与规范/ 单元测试/ 板级测试/ 专项验证/ 测试工具/ 测试数据/,
顶层只留新增的目录导航 README.md(归层判据 + 硬约束 + 本次处置记录)。

- 搬迁 28 个路径:测试数据三个目录 58 个文件为纯改名(R100,内容逐字节不变)
- 全库引用同步 124 个文件:AGENTS / docs 索引 / skills / 门禁注释 / scripts
- 门禁面:scripts/check_test_traceability.py 的 glob 改 rglob
  (分层后不递归扫描=追溯门禁直接瞎掉)
- scripts/rename_ref.py:收尾的 ℹ 提示在 GBK 代码页抛 UnicodeEncodeError,
  让进程以退出码 1 收场而改写其实已落盘(RC 与真实结果相反);改为把
  stdout/stderr reconfigure 成 UTF-8,与 check_doc_links.py 等同法
- docs/文档规范.md §1.1 补「子目录文件过多时再分一层」;§3.6 补 rename_ref
  搬整目录的两个实测坑:① 会改写目录内部文件的内容 ② 内部文件的 ../ 出链
  不补层级(本次 23 个被误改的证据文件已按 HEAD 原始字节逐字节还原)
- docs/README.md §6 改为与六个子目录一一对应,并补登原先漏登记的
  「启动日志原子性复验流程」一行
- .cubemx_survival_baseline.json:App/util/task_watchdog.c 的注释路径也随分层更新,
  该文件是存活审计的 watchdog-module 漂移锚点,按脚本自己的提示跑 --write 重建基线
  (实测重建后仅 watchdog-module 一条哈希变化)
- 铁律 4 例外(本次在任务树里改了单写者文件):AGENTS.md 与
  .pre-commit-config.yaml 只被 rename_ref 改写路径字面量(后者全在注释行),
  任务表块与钩子声明一字未动

验证:check_doc_links.py --sections-strict 覆盖全部 88 个改动 .md 全绿;
check_test_traceability.py 输出与主树逐行一致;196 个文本文件无 BOM、无新增
CR、无 U+FFFD、无行尾空白;陈旧引用面由主树 25 处降到 6 处(无新增断链)。
2026-10-30 07:16:07 +08:00

12 KiB
Raw Permalink Blame History

相量层单元测试说明(主机侧)

模块:Phasor Layer(全周傅氏相量计算,App/algo/phasor.c + App/algo/sample_ring.c) 测试类型:主机侧纯逻辑单元测试——在 PC 上离线验证全周傅氏相量计算与 采样环缓冲,零硬件、零烧录、毫秒级、不占 MCU 的 RAM。 状态:✅ 已落地(57 用例,/W4 0 告警,已挂 pre-commit 钩子) 日期:2026-10-10 关联文档: ../../../AGENTS.md(平台约束)、 测试开发标准流程.md(测试分层与门禁总览)、 ADC输入层单元测试说明.md(同批落地的 语义取数与有效性姊妹篇)、 ../../算法/傅氏算法详解.md(系数约定权威口径 §2.3)、 ../../算法/继电保护功能复现规划.md(§3.2 全周傅氏相量设计点 / §10.2 术语表)、 ../../硬件平台/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, 本模块 run_unit_test.py 只声明 TestBuild(源文件 / 汇总行标记)。

1.5 运行方式

# 仓库根目录(统一用 .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"与 "逐实例复核"两条断言会自动扩展到新实例——但需同步更新该常量断言。