L2 相量层:采样环缓冲 + 全周傅氏 + 主机单测(台架间歇复位待查)
新增(均已通过主机侧单测与构建,**台架存在未定位的间歇性回归,见下**): - App/algo/sample_ring.c/h:按语义通道的采样环缓冲(2 周波 = 40 点), 整帧压入 + 帧计数,供 L2 按窗取数。 - App/algo/phasor.c/h:全周傅氏相量层,覆盖规划 §3.2 的 L2-1/2/3/6/7—— 系数表按 docs/算法/傅氏算法详解.md §2.3 预乘 √2/N(fAmSQ 直接即有效值); 5 个数据驱动实例(IA/UA/UN 基波 + IA 二/五次谐波);暴力滑动窗; 计算区/使用区双区原子切换。 - test/host/phasor/:主机侧单测(52/52 PASSED,无需桩)——系数表逐项复核、 幅值/有效值/相位、直流与谐波抑制、任意次谐波提取、频率敏感性(±2Hz≤3%)、 环缓冲时序与回绕、实例层与双区交换。 - 集成:adc_task 每拍压帧 + 重算(v2.4);app_main 调 phasor_init(); Keil 工程挂 phasor.c/sample_ring.c(App/algo 已在 IncludePath)。 验证: - 主机单测 52/52 PASSED。 - @build 0 错误 0 警告;内存快照已刷新(RW_IRAM1 89,320→90,936 B)。 - 台架:L2 链路数据正常(ring=10001、skip=0、回读一致、相量日志可见), 但 10 次运行中 3 次出现 UI 任务看门狗停喂(mask=0x67 缺 TASK_WD_UI) → IWDG 复位;改动前基线 6 次运行 0 次。属**未定位的间歇性回归**。 未完成:定位并修复该间歇性复位;L2 文档状态与登记簿条目尚未按结论更新。
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* 模块名称:相量计算层(L2 相量计算)
|
||||
* 模块功能:全周傅氏算法(单频 DFT)——系数表预乘 √2/N,fAmSQ 直接即有效值。
|
||||
* 覆盖 L2-1 系数表 / L2-2 实例 / L2-3 有效值 / L2-6 滑动窗 / L2-7 双区交换。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:L2-1/2/3/6/7
|
||||
*/
|
||||
|
||||
#include "phasor.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "sample_ring.h"
|
||||
|
||||
/* ===== L2-1 全周傅氏系数表 =====
|
||||
* g_fFFTSin20[k] = sin(2πk/N)·(√2/N)
|
||||
* g_fFFTCos20[k] = cos(2πk/N)·(√2/N) N = PHASOR_N = 20
|
||||
* 权威口径:docs/算法/傅氏算法详解.md §2.3("把 √2 藏进系数表")。
|
||||
* 本表为**离线生成**的 float 常量(不在运行期调 sinf/cosf):确定、无 libm 精度依赖。
|
||||
* 主机侧单测 test/host/phasor/ 会**逐项按上式复核本表**(防转录错),
|
||||
* 并以合成正弦做端到端复核(读数误差 ≤ 1%)。
|
||||
* 精确零点(sin(π)=0、cos(π/2)=0)直接写 0.0f,不保留浮点噪声。 */
|
||||
static const float k_fft_sin[PHASOR_N] = {
|
||||
+0.000000000e+00f, +2.185080200e-02f, +4.156269506e-02f, +5.720613897e-02f, +6.724984944e-02f,
|
||||
+7.071068138e-02f, +6.724984944e-02f, +5.720613897e-02f, +4.156269506e-02f, +2.185080200e-02f,
|
||||
+0.000000000e+00f, -2.185080200e-02f, -4.156269506e-02f, -5.720613897e-02f, -6.724984944e-02f,
|
||||
-7.071068138e-02f, -6.724984944e-02f, -5.720613897e-02f, -4.156269506e-02f, -2.185080200e-02f,
|
||||
};
|
||||
|
||||
static const float k_fft_cos[PHASOR_N] = {
|
||||
+7.071068138e-02f, +6.724984944e-02f, +5.720613897e-02f, +4.156269506e-02f, +2.185080200e-02f,
|
||||
+0.000000000e+00f, -2.185080200e-02f, -4.156269506e-02f, -5.720613897e-02f, -6.724984944e-02f,
|
||||
-7.071068138e-02f, -6.724984944e-02f, -5.720613897e-02f, -4.156269506e-02f, -2.185080200e-02f,
|
||||
+0.000000000e+00f, +2.185080200e-02f, +4.156269506e-02f, +5.720613897e-02f, +6.724984944e-02f,
|
||||
};
|
||||
|
||||
/* √2:幅值 = √2 × 有效值(与系数表的 √2 折算互逆) */
|
||||
#define PHASOR_SQRT2 1.4142135623730951f
|
||||
|
||||
/* ===== L2-2 相量实例表 =====
|
||||
* 参考项目为 **4 个实例**(C01 基波 I/U、C02 零序基波、C03 二次谐波、C04 五次谐波,
|
||||
* `APP/CLS100_Dsp/InstCalCell.c`)。本项目按 V0.1 **有效通道**(`UA`/`UN`/`IA`,
|
||||
* 见规划 §1.6:`UB/UC/IB/IC/IN` 无中点偏置 → 削顶不可用)展开——C01 的"I/U"
|
||||
* 拆为两行(IA 电流 / UA 电压),并补出 C04 的五次谐波轨(L1-9)。
|
||||
* 新增实例只需在表尾加一行,其余代码自动适配。 */
|
||||
static const phasor_inst_t k_inst[] = {
|
||||
{ "C01-IA-F1", ADC_SEM_IA, 1u }, /* A 相电流基波(L3-1 过流 / L3-3 过负荷) */
|
||||
{ "C02-UA-F1", ADC_SEM_UA, 1u }, /* A 相电压基波(L3-4 过压/欠压、L3-5 频率) */
|
||||
{ "C03-UN-F1", ADC_SEM_UN, 1u }, /* 零序电压基波(L3-6 零序过压) */
|
||||
{ "C04-IA-F2", ADC_SEM_IA, 2u }, /* 二次谐波(L3-7 涌流制动) */
|
||||
{ "C05-IA-F5", ADC_SEM_IA, 5u }, /* 五次谐波(L1-9 选线) */
|
||||
};
|
||||
|
||||
#define PHASOR_INST_N (sizeof(k_inst) / sizeof(k_inst[0]))
|
||||
|
||||
/* ===== L2-7 双区(计算区 / 使用区)=====
|
||||
* 本拍结果先写入"计算区",全部实例写完后**一次性**切换 s_use_zone
|
||||
* (单字节 store,Cortex-M 上原子),消费侧读"使用区" → 无锁、无撕裂。
|
||||
* s_use_zone 声明 volatile:阻止编译器把结果写入挪到切换之后
|
||||
* (本平台单核顺序执行,无需额外内存屏障)。
|
||||
* 今日唯一消费者是 adcTask 自身(同拍读写、实际无竞争),本机制为后续
|
||||
* 独立保护任务(L3)预留。 */
|
||||
static phasor_t s_zone[2][PHASOR_INST_N];
|
||||
static volatile uint8_t s_use_zone;
|
||||
static uint8_t s_ready; /* 使用区是否已产出过有效结果 */
|
||||
|
||||
/*
|
||||
* 函数功能:清零一个相量
|
||||
* 入口参数:p - 目标相量
|
||||
* 返 回 值:无
|
||||
* 限定条件:p 非空
|
||||
* 函数说明:静态辅助。
|
||||
*/
|
||||
static void phasor_zero(phasor_t *p) {
|
||||
p->fRe = 0.0f;
|
||||
p->fIm = 0.0f;
|
||||
p->fAm = 0.0f;
|
||||
p->fAmSQ = 0.0f;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:对 n 点窗口算 harm 次相量(L2-1 系数表 + L2-3 有效值)
|
||||
* 入口参数:x - 采样窗(按时间序,旧 → 新)
|
||||
* n - 窗长(必须等于 PHASOR_N)
|
||||
* harm - 谐波次数(>=1)
|
||||
* out - 输出相量
|
||||
* 返 回 值:无
|
||||
* 限定条件:无
|
||||
* 函数说明:纯函数。索引步进 = 谐波次数,故同一张表可算任意次谐波。
|
||||
*/
|
||||
void phasor_calc(const float *x, uint8_t n, uint8_t harm, phasor_t *out) {
|
||||
float re = 0.0f;
|
||||
float im = 0.0f;
|
||||
uint8_t i;
|
||||
|
||||
if ((x == NULL) || (out == NULL) || (n != (uint8_t)PHASOR_N) || (harm == 0u)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0u; i < n; i++) {
|
||||
uint8_t idx = (uint8_t)(((unsigned int)i * harm) % n);
|
||||
re += x[i] * k_fft_sin[idx];
|
||||
im += x[i] * k_fft_cos[idx];
|
||||
}
|
||||
|
||||
out->fRe = re;
|
||||
out->fIm = im;
|
||||
out->fAmSQ = sqrtf((re * re) + (im * im)); /* 系数已折算 √2/N → 直接即有效值 */
|
||||
out->fAm = out->fAmSQ * PHASOR_SQRT2; /* 幅值 = √2 × 有效值 */
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:相量层初始化
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 成功
|
||||
* 限定条件:内核启动前调用
|
||||
* 函数说明:清双区结果与有效标志,并复位采样环缓冲;可重复调用(幂等)。
|
||||
*/
|
||||
int phasor_init(void) {
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < (uint8_t)PHASOR_INST_N; i++) {
|
||||
phasor_zero(&s_zone[0][i]);
|
||||
phasor_zero(&s_zone[1][i]);
|
||||
}
|
||||
s_use_zone = 0u;
|
||||
s_ready = 0u;
|
||||
sample_ring_init();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取相量实例数(L2-2)
|
||||
* 入口参数:无
|
||||
* 返 回 值:实例个数
|
||||
* 限定条件:无
|
||||
* 函数说明:供遍历 / 日志打印。
|
||||
*/
|
||||
uint8_t phasor_inst_count(void) {
|
||||
return (uint8_t)PHASOR_INST_N;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取相量实例描述
|
||||
* 入口参数:idx - 实例下标
|
||||
* 返 回 值:指向静态实例表的指针;idx 越界返回 NULL
|
||||
* 限定条件:无
|
||||
* 函数说明:返回静态存储地址,调用方不得修改。
|
||||
*/
|
||||
const phasor_inst_t *phasor_inst(uint8_t idx) {
|
||||
if (idx >= (uint8_t)PHASOR_INST_N) {
|
||||
return NULL;
|
||||
}
|
||||
return &k_inst[idx];
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:重算全部实例 + 双区原子切换(L2-6 / L2-7)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅任务上下文调用;须先 sample_ring_push_frame() 压入本拍数据
|
||||
* 函数说明:任一实例数据不足一周波 → 本拍整体不产出(不切换使用区),
|
||||
* 避免出现"部分新部分旧"的混合结果。
|
||||
*/
|
||||
void phasor_run(void) {
|
||||
float win[PHASOR_N];
|
||||
uint8_t w = (uint8_t)(1u - s_use_zone); /* 计算区 = 非使用区 */
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < (uint8_t)PHASOR_INST_N; i++) {
|
||||
if (sample_ring_window(k_inst[i].sem, (uint8_t)PHASOR_N, win) != 0) {
|
||||
return; /* 尚未攒满一周波:本拍不产出 */
|
||||
}
|
||||
phasor_calc(win, (uint8_t)PHASOR_N, k_inst[i].harm, &s_zone[w][i]);
|
||||
}
|
||||
|
||||
s_use_zone = w; /* 原子切换(单字节 store) */
|
||||
s_ready = 1u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读实例结果(使用区,无锁)
|
||||
* 入口参数:idx - 实例下标
|
||||
* out - 输出相量
|
||||
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 尚无有效结果
|
||||
* 限定条件:无
|
||||
* 函数说明:先取一次使用区下标再索引,避免读写期间下标变化。
|
||||
*/
|
||||
int phasor_get(uint8_t idx, phasor_t *out) {
|
||||
uint8_t z;
|
||||
|
||||
if ((out == NULL) || (idx >= (uint8_t)PHASOR_INST_N)) {
|
||||
return -1;
|
||||
}
|
||||
if (s_ready == 0u) {
|
||||
return -2;
|
||||
}
|
||||
z = s_use_zone;
|
||||
*out = s_zone[z][idx];
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 模块名称:相量计算层(L2 相量计算)
|
||||
* 模块功能:全周傅氏(全周波傅里叶)算法——对一周波 20 点采样做单频 DFT,
|
||||
* 产出基波/谐波相量。系数表**预乘 √2/N**(口径见
|
||||
* `docs/算法/傅氏算法详解.md` §2.3),故 `fAmSQ = √(fRe²+fIm²)`
|
||||
* **直接就是有效值**,无需再除 √2。
|
||||
* 覆盖规划 §3.2:L2-1 系数表 / L2-2 实例 / L2-3 有效值输出 /
|
||||
* L2-6 每 1 ms 滑动窗 / L2-7 双区指针原子交换。
|
||||
* ⚠ 本层**纯算法**:不碰寄存器、不碰任务调度、不自己取数(输入由
|
||||
* `sample_ring` 提供)→ 主机侧可单测(L0.5,落实登记簿 A-4)。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:L2-1/2/3/6/7(基波 + 谐波相量)
|
||||
*/
|
||||
|
||||
#ifndef PHASOR_H
|
||||
#define PHASOR_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "adc_input.h" /* adc_semantic_t:实例按语义通道绑定 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 每周波采样点数(1000 Hz @ 50 Hz,见 docs/子系统设计/ADC采样节拍设计说明.md) */
|
||||
#define PHASOR_N 20U
|
||||
|
||||
/* 相量结果(**有效值口径**)。字段名沿用参考系统 `{fRe, fIm, fAm, fAmSQ}`
|
||||
* (规划 §10.2 术语表:实部/虚部/幅值/有效值):
|
||||
* fRe - 实部 = a1/√2(有效值相量实部)
|
||||
* fIm - 虚部 = b1/√2(有效值相量虚部)
|
||||
* fAm - 幅值 = √2·√(fRe²+fIm²)(正弦峰值 A1)
|
||||
* fAmSQ - 有效值 = √(fRe²+fIm²)(系数已折算 √2/N,故直接即有效值)
|
||||
* 相位(L2-4)按需由 `atan2(fIm, fRe)` 导出,不常驻本结构。 */
|
||||
typedef struct {
|
||||
float fRe;
|
||||
float fIm;
|
||||
float fAm;
|
||||
float fAmSQ;
|
||||
} phasor_t;
|
||||
|
||||
/* 相量实例(L2-2):**数据驱动**——新增实例只需在实例表尾加一行 */
|
||||
typedef struct {
|
||||
const char *name; /* 实例名(日志 / UI 用) */
|
||||
adc_semantic_t sem; /* 语义通道 */
|
||||
uint8_t harm; /* 谐波次数(1=基波,2=二次,5=五次…) */
|
||||
} phasor_inst_t;
|
||||
|
||||
/*
|
||||
* 函数功能:相量层初始化
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 成功
|
||||
* 限定条件:内核启动前调用(无 RTOS 依赖)
|
||||
* 函数说明:清双区结果与有效标志,并复位采样环缓冲;可重复调用(幂等)。
|
||||
*/
|
||||
int phasor_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取相量实例数(L2-2)
|
||||
* 入口参数:无
|
||||
* 返 回 值:实例个数
|
||||
* 限定条件:无
|
||||
* 函数说明:供遍历 / 日志打印。
|
||||
*/
|
||||
uint8_t phasor_inst_count(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取相量实例描述
|
||||
* 入口参数:idx - 实例下标
|
||||
* 返 回 值:指向静态实例表的指针;idx 越界返回 NULL
|
||||
* 限定条件:无
|
||||
* 函数说明:返回静态存储地址,调用方不得修改。
|
||||
*/
|
||||
const phasor_inst_t *phasor_inst(uint8_t idx);
|
||||
|
||||
/*
|
||||
* 函数功能:重算全部实例并从计算区原子切换到使用区(L2-6 滑动窗 + L2-7 双区交换)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅任务上下文调用;须先经 `sample_ring_push_frame()` 压入本拍数据
|
||||
* 函数说明:以"当前时刻为终止的前一周波"为窗,**20 点全部重算**(暴力法,
|
||||
* 无累积误差)。**任一实例数据不足一周波则本拍整体不产出**(不切换
|
||||
* 使用区),避免部分新部分旧的混合结果。每拍更新,故相量刷新周期 = 1 ms。
|
||||
*/
|
||||
void phasor_run(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读实例结果(使用区,无锁)
|
||||
* 入口参数:idx - 实例下标
|
||||
* out - 输出相量
|
||||
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 尚无有效结果(未攒满一周波)
|
||||
* 限定条件:无
|
||||
* 函数说明:读的是"使用区",与正在写的"计算区"物理隔离 → 无撕裂、无锁。
|
||||
*/
|
||||
int phasor_get(uint8_t idx, phasor_t *out);
|
||||
|
||||
/*
|
||||
* 函数功能:对 n 点窗口算 harm 次相量(L2-1 系数表 + L2-3 有效值)
|
||||
* 入口参数:x - 采样窗(按时间序,旧 → 新;长度须 = PHASOR_N)
|
||||
* n - 窗长(必须等于 PHASOR_N)
|
||||
* harm - 谐波次数(>=1)
|
||||
* out - 输出相量
|
||||
* 返 回 值:无(参数非法时 out 保持不动)
|
||||
* 限定条件:无
|
||||
* 函数说明:**纯函数**(无状态),可脱离采样环独立调用,便于单测与复用。
|
||||
* 索引步进 = 谐波次数即可取任意次谐波(sin 以 2π 为周期,
|
||||
* 故 `sin(2π·((n·h) mod N)/N) ≡ sin(2π·n·h/N)`)。
|
||||
*/
|
||||
void phasor_calc(const float *x, uint8_t n, uint8_t harm, phasor_t *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PHASOR_H */
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 模块名称:采样环缓冲(L2 相量计算的输入)
|
||||
* 模块功能:按语义通道保存最近 2 个周波(40 点)采样值,供 L2 相量计算按窗取数。
|
||||
* 适用平台:STM32F407ZGTx(纯算法,无寄存器/无 RTOS 依赖)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:L2 输入环缓冲(20 点/周波 × 2 周波)
|
||||
*/
|
||||
|
||||
#include "sample_ring.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* 采样环:[语义通道][点]。按语义通道索引(0..ADC_SEM_COUNT-1),
|
||||
* 故消费侧"按语义取数",不依赖任何物理通道号或转换器归属。 */
|
||||
static float s_ring[ADC_SEM_COUNT][SAMPLE_RING_DEPTH];
|
||||
/* 写指针:下一个待写入的点下标(0..SAMPLE_RING_DEPTH-1) */
|
||||
static uint16_t s_head;
|
||||
/* 已累积点数(饱和于 SAMPLE_RING_DEPTH) */
|
||||
static uint16_t s_filled;
|
||||
/* 累计入环帧数(不饱和,供心跳/丢帧观测) */
|
||||
static uint32_t s_frames;
|
||||
|
||||
/*
|
||||
* 函数功能:采样环缓冲初始化
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:无
|
||||
* 函数说明:清空全部通道历史与计数;可重复调用(幂等)。
|
||||
*/
|
||||
void sample_ring_init(void) {
|
||||
uint8_t c;
|
||||
uint16_t i;
|
||||
|
||||
for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) {
|
||||
for (i = 0u; i < SAMPLE_RING_DEPTH; i++) {
|
||||
s_ring[c][i] = 0.0f;
|
||||
}
|
||||
}
|
||||
s_head = 0u;
|
||||
s_filled = 0u;
|
||||
s_frames = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:压入一拍(一帧)全部语义通道的采样值
|
||||
* 入口参数:values - 按语义通道顺序排列的一帧采样值(长度须 = ADC_SEM_COUNT)
|
||||
* n - 实际长度(必须等于 ADC_SEM_COUNT)
|
||||
* 返 回 值:0 - 成功;-1 - 参数错
|
||||
* 限定条件:同一帧内各通道须同刻(同族量同刻性由上游 L1 负责)
|
||||
* 函数说明:整帧写入完毕后**统一**推进写指针——单帧内不存在"半新半旧",
|
||||
* 消费侧取到的窗永远落在整帧边界上(无帧内撕裂)。
|
||||
*/
|
||||
int sample_ring_push_frame(const float *values, uint8_t n) {
|
||||
uint8_t c;
|
||||
|
||||
if ((values == NULL) || (n != (uint8_t)ADC_SEM_COUNT)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) {
|
||||
s_ring[c][s_head] = values[c];
|
||||
}
|
||||
|
||||
s_head = (uint16_t)((s_head + 1u) % SAMPLE_RING_DEPTH);
|
||||
if (s_filled < SAMPLE_RING_DEPTH) {
|
||||
s_filled++;
|
||||
}
|
||||
s_frames++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取指定语义通道最近 n 点的窗(按时间序,旧 → 新)
|
||||
* 入口参数:ch - 语义通道
|
||||
* n - 取点数(1..SAMPLE_RING_DEPTH)
|
||||
* out - 输出缓冲(长度须 >= n)
|
||||
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 已累积点数不足 n
|
||||
* 限定条件:无
|
||||
* 函数说明:out[n-1] 为最新一点。最老点下标 = head - n(模 DEPTH)。
|
||||
*/
|
||||
int sample_ring_window(adc_semantic_t ch, uint8_t n, float *out) {
|
||||
uint16_t start;
|
||||
uint16_t i;
|
||||
|
||||
if ((out == NULL) || ((unsigned int)ch >= (unsigned int)ADC_SEM_COUNT)) {
|
||||
return -1;
|
||||
}
|
||||
if ((n == 0u) || (n > SAMPLE_RING_DEPTH)) {
|
||||
return -1;
|
||||
}
|
||||
if (s_filled < (uint16_t)n) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
start = (uint16_t)((s_head + SAMPLE_RING_DEPTH - (uint16_t)n) % SAMPLE_RING_DEPTH);
|
||||
for (i = 0u; i < (uint16_t)n; i++) {
|
||||
out[i] = s_ring[ch][(uint16_t)((start + i) % SAMPLE_RING_DEPTH)];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读已累积点数(饱和于 SAMPLE_RING_DEPTH)
|
||||
* 入口参数:无
|
||||
* 返 回 值:已累积点数(0..SAMPLE_RING_DEPTH)
|
||||
* 限定条件:无
|
||||
* 函数说明:供上层判断"数据是否够一周波"。
|
||||
*/
|
||||
uint16_t sample_ring_filled(void) {
|
||||
return s_filled;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读累计入环帧数(不饱和)
|
||||
* 入口参数:无
|
||||
* 返 回 值:累计入环帧数
|
||||
* 限定条件:无
|
||||
* 函数说明:供心跳日志 / 丢帧观测(L1-6 判据之一)。
|
||||
*/
|
||||
uint32_t sample_ring_frames(void) {
|
||||
return s_frames;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 模块名称:采样环缓冲(L2 相量计算的输入)
|
||||
* 模块功能:按**语义通道**保存最近若干周波的采样值,供 L2 相量计算按窗取数。
|
||||
* V0.1 上 8 路电气量分属两片转换器(硬件缺陷 N-18),本层只做"按时序
|
||||
* 存/取",**不做任何跨通道运算**——同刻性由上游(L1)保证,本层不假设。
|
||||
* 容量取 **2 个周波(40 点)**,与参考系统 `CN_BUFSAM_CYCLE = 2`
|
||||
* (规划 §10.1)一致,为后续加长数据窗(1.5~2 周波,抗衰减直流,
|
||||
* 见 `傅氏算法详解.md` §9 Q4)预留空间。
|
||||
* 适用平台:STM32F407ZGTx(**纯算法**:不碰寄存器、不碰 RTOS → 可主机侧单测 L0.5)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:L2 输入环缓冲(20 点/周波 × 2 周波)
|
||||
*/
|
||||
|
||||
#ifndef SAMPLE_RING_H
|
||||
#define SAMPLE_RING_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "adc_input.h" /* adc_semantic_t / ADC_SEM_COUNT:本层按语义通道存取 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 每周波采样点数(1000 Hz @ 50 Hz,见 docs/子系统设计/ADC采样节拍设计说明.md) */
|
||||
#define SAMPLE_RING_CYCLE_N 20U
|
||||
/* 环容量(周波数)——与参考系统 CN_BUFSAM_CYCLE 一致 */
|
||||
#define SAMPLE_RING_CYCLES 2U
|
||||
/* 环深度(点):一周波点数 × 周波数 */
|
||||
#define SAMPLE_RING_DEPTH (SAMPLE_RING_CYCLE_N * SAMPLE_RING_CYCLES)
|
||||
|
||||
/*
|
||||
* 函数功能:采样环缓冲初始化
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:内核启动前调用(无 RTOS 依赖)
|
||||
* 函数说明:清空全部通道历史与计数;可重复调用(幂等)。
|
||||
*/
|
||||
void sample_ring_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:压入一拍(一帧)全部语义通道的采样值
|
||||
* 入口参数:values - 按语义通道顺序排列的一帧采样值(浮点工程量,长度须 = ADC_SEM_COUNT)
|
||||
* n - 实际长度(必须等于 ADC_SEM_COUNT,否则拒收)
|
||||
* 返 回 值:0 - 成功;-1 - 参数错(空指针或长度不符)
|
||||
* 限定条件:调用方保证同一帧内各通道**同刻**(同族量同刻性由上游 L1 负责)
|
||||
* 函数说明:整帧一次性写入,写完统一推进写指针——单帧内不可能出现"半新半旧",
|
||||
* 即天然无帧内撕裂。消费侧读到的窗永远是**整帧**边界。
|
||||
*/
|
||||
int sample_ring_push_frame(const float *values, uint8_t n);
|
||||
|
||||
/*
|
||||
* 函数功能:取指定语义通道最近 n 点的窗(按时间序,旧 → 新)
|
||||
* 入口参数:ch - 语义通道
|
||||
* n - 取点数(1..SAMPLE_RING_DEPTH)
|
||||
* out - 输出缓冲(长度须 >= n)
|
||||
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 已累积点数不足 n
|
||||
* 限定条件:无
|
||||
* 函数说明:out[n-1] 为最新一点。相量层以本函数取"以当前时刻为终止的一周波"。
|
||||
*/
|
||||
int sample_ring_window(adc_semantic_t ch, uint8_t n, float *out);
|
||||
|
||||
/*
|
||||
* 函数功能:读已累积点数(饱和于 SAMPLE_RING_DEPTH)
|
||||
* 入口参数:无
|
||||
* 返 回 值:已累积点数(0..SAMPLE_RING_DEPTH)
|
||||
* 限定条件:无
|
||||
* 函数说明:供上层判断"数据是否够一周波"。
|
||||
*/
|
||||
uint16_t sample_ring_filled(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读累计入环帧数(**不饱和**,供心跳/丢帧观测)
|
||||
* 入口参数:无
|
||||
* 返 回 值:累计入环帧数(32 位,1 kHz 下约 49.7 天回绕)
|
||||
* 限定条件:无
|
||||
* 函数说明:与 L1 的 `tpafe5160_frame_seq()` 对照可判丢帧(L1-6 采样有效性校验
|
||||
* 的判据之一,本层只提供计数,不做判定)。
|
||||
*/
|
||||
uint32_t sample_ring_frames(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SAMPLE_RING_H */
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "gd5f2gq5ue.h"
|
||||
#include "tpafe5160.h"
|
||||
#include "adc_input.h" /* L1 采集层:adc_input_init()(语义映射 + STM32 ADC1 扫描) */
|
||||
#include "phasor.h" /* L2 相量层:phasor_init()(系数表 + 实例 + 双区交换) */
|
||||
#include "sys_log.h"
|
||||
#include "task_watchdog.h" /* task_wd_seg_report():round 8 复位不丢"进段打点"上报 */
|
||||
|
||||
@@ -194,6 +195,13 @@ int app_main_init(void) {
|
||||
DBG_INFO("ADC input layer init: OK");
|
||||
}
|
||||
|
||||
/* 4c) 相量层(L2)初始化:清双区结果与有效标志 + 复位采样环缓冲。
|
||||
* 纯算法层(无外设 / 无 RTOS 依赖,见 docs/算法/继电保护功能复现规划.md
|
||||
* §3.2),须在启动采样节拍(步骤 6)之前。幂等、当前无失败路径
|
||||
* (返回值保留供后续自检扩展)。 */
|
||||
(void)phasor_init();
|
||||
DBG_INFO("phasor layer init: OK (inst=%u)", (unsigned)phasor_inst_count());
|
||||
|
||||
/* 5) SD2506 RTC 诊断信息 */
|
||||
{
|
||||
/* 读取 ID */
|
||||
|
||||
+97
-9
@@ -1,7 +1,9 @@
|
||||
/*
|
||||
* 模块名称:ADC Sampling Task
|
||||
* 模块功能:ADC 采集消费——TPAFE5160 快照 + STM32 ADC1 顺序扫描(经 L1 层
|
||||
* App/algo/adc_input.c 按语义取数)+ 帧统计(节拍由 TIM6 硬件定时器提供)
|
||||
* 模块功能:ADC 采集消费 + L2 相量计算——TPAFE5160 快照 + STM32 ADC1 顺序扫描
|
||||
* (经 L1 层 App/algo/adc_input.c 按语义取数)→ 压入采样环
|
||||
* (App/algo/sample_ring.c)→ 全周傅氏相量重算(App/algo/phasor.c)
|
||||
* + 帧统计(节拍由 TIM6 硬件定时器提供)
|
||||
* 状态说明:**B1 已落地(2026-10-10)**——本任务从空桩转为真实消费者:
|
||||
* 用 tpafe5160_snapshot() 原子取数(AGENTS 铁律),统计帧数/无新帧次数,
|
||||
* 并纳入看门狗 required_mask(TASK_WD_ADC)。
|
||||
@@ -35,11 +37,22 @@
|
||||
* v2.3 2026-10-10 王建锋 L1-3/L1-4:取数改走 App/algo/adc_input.c(语义映射 +
|
||||
* STM32 ADC1 9 通道扫描);首帧日志补全 8 路电气量 + 温度,
|
||||
* 心跳日志增 adc1_rc / adc1_err 观测 STM32 侧健康度。
|
||||
* v2.4 2026-10-10 王建锋 L2:接入相量层——每拍把 9 路语义值压入采样环
|
||||
* (sample_ring)并重算全部相量实例(phasor_run);
|
||||
* 攒满一周波后**首拍即落一次相量日志**(开机证据,不等
|
||||
* 10 s 心跳),心跳日志增 ring / skip 计数与各实例相量
|
||||
* (Re/Im/有效值,×0.1 定点打印,因 MicroLIB 的 snprintf
|
||||
* 无 %f)。
|
||||
* ⚠ L1-5 通道标定未实现(缺标准源)→ 暂以**原始码值**
|
||||
* 充作工程量(系数 = 1),故相量读数只证明**链路通**,
|
||||
* 不可作幅值精度判据(同 L1-3 的替代口径)。
|
||||
*/
|
||||
|
||||
#include "adc_task.h"
|
||||
#include "cmsis_os.h"
|
||||
#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 "tpafe5160.h"
|
||||
#include "task_watchdog.h"
|
||||
#include "console_log.h"
|
||||
@@ -56,6 +69,34 @@
|
||||
* 原始码值缓冲由 L1 层 App/algo/adc_input.c 持有(消费侧按语义取数,不写死下标) */
|
||||
static uint32_t s_frames_consumed;
|
||||
static uint32_t s_frames_idle;
|
||||
/* 因通道尚未首次成功而**整拍跳过入环**的次数(不向相量窗注入 0 点) */
|
||||
static uint32_t s_frames_skip;
|
||||
|
||||
/*
|
||||
* 函数功能:打印全部相量实例(×0.1 定点)
|
||||
* 入口参数:tag - 前缀标记("first" 首次产出 / "hb" 周期心跳)
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅任务上下文;调用方须保证 dbg_console_writer_ready() 为真
|
||||
* 函数说明:MicroLIB 的 snprintf **不支持 %f** → 以 ×0.1 定点整数打印
|
||||
* (打印值 = 实际值 × 10,分辨率 0.1)。逐实例数据驱动输出,
|
||||
* 实例表加行即自动覆盖。⚠ L1-5 标定未实现 → 读数单位是**原始码值**。
|
||||
*/
|
||||
static void log_phasors(const char *tag) {
|
||||
uint8_t k;
|
||||
|
||||
for (k = 0u; k < phasor_inst_count(); k++) {
|
||||
const phasor_inst_t *d = phasor_inst(k);
|
||||
phasor_t p;
|
||||
|
||||
if ((d != NULL) && (phasor_get(k, &p) == 0)) {
|
||||
DBG_INFO("phasor(%s) %s: Re=%ld Im=%ld rms=%ld (x0.1)",
|
||||
tag, d->name,
|
||||
(long)(p.fRe * 10.0f),
|
||||
(long)(p.fIm * 10.0f),
|
||||
(long)(p.fAmSQ * 10.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:ADC 任务初始化
|
||||
@@ -67,6 +108,7 @@ static uint32_t s_frames_idle;
|
||||
int adc_task_init(void) {
|
||||
s_frames_consumed = 0u;
|
||||
s_frames_idle = 0u;
|
||||
s_frames_skip = 0u;
|
||||
DBG_INFO("adc_task_init");
|
||||
return 0;
|
||||
}
|
||||
@@ -85,6 +127,7 @@ void adc_task_func(void *arg) {
|
||||
uint32_t next = osKernelGetTickCount();
|
||||
uint32_t report_at = next + ADC_TASK_REPORT_MS;
|
||||
uint8_t first_logged = 0u;
|
||||
uint8_t first_phasor_logged = 0u;
|
||||
|
||||
(void)arg;
|
||||
|
||||
@@ -103,7 +146,36 @@ void adc_task_func(void *arg) {
|
||||
/* r == -1:参数错。调用点 count 为常量,不可达;不计入 idle 以区分异常 */
|
||||
}
|
||||
|
||||
/* 2) 首帧码值上报:**写者就绪后**才落日志。写者未注册时 con_append 走
|
||||
/* 2) L2 输入 + 相量重算:把本拍 9 路语义值转浮点帧压入采样环,再重算全部
|
||||
* 相量实例(L2-6 滑动窗:每拍以"当前时刻为终止的前一周波"整窗重算)。
|
||||
* 任一通道读不到(尚未首次成功)→ 本拍**整帧跳过**:不向窗内注入 0 点
|
||||
* (单个 0 点会显著污染 20 点窗),并计入 skip 观测。 */
|
||||
{
|
||||
float frame[ADC_SEM_COUNT];
|
||||
uint8_t s;
|
||||
uint8_t all_ok = 1u;
|
||||
|
||||
for (s = 0u; s < (uint8_t)ADC_SEM_COUNT; s++) {
|
||||
int32_t code = 0;
|
||||
|
||||
if (adc_input_raw((adc_semantic_t)s, &code) != 0) {
|
||||
all_ok = 0u;
|
||||
break;
|
||||
}
|
||||
/* ⚠ L1-5 通道标定尚未实现(缺标准源)→ 暂以**原始码值**充作工程量
|
||||
* (系数 = 1)。标定落地后此处改为 工程量 = (code + b) × k(定点 2^10)。 */
|
||||
frame[s] = (float)code;
|
||||
}
|
||||
|
||||
if (all_ok != 0u) {
|
||||
(void)sample_ring_push_frame(frame, (uint8_t)ADC_SEM_COUNT);
|
||||
phasor_run();
|
||||
} else {
|
||||
s_frames_skip++;
|
||||
}
|
||||
}
|
||||
|
||||
/* 3) 首帧码值上报:**写者就绪后**才落日志。写者未注册时 con_append 走
|
||||
* 无锁直发路径,与 defaultTask 的存储初始化日志会字节级交错
|
||||
* (2026-10-10 台架实测);此时挂起上报,就绪后补报最近一帧。
|
||||
* 8 路电气量 + 温度全部经 adc_input_raw() **按语义**取数(L1-4),
|
||||
@@ -129,29 +201,45 @@ void adc_task_func(void *arg) {
|
||||
(long)ia, (long)ib, (long)ic, (long)in, (long)temp);
|
||||
}
|
||||
|
||||
/* 3) 看门狗心跳(本任务已纳入 required_mask) */
|
||||
/* 4) 首拍相量上报:攒满一周波(20 ms)后立即落一次,作为 L2 链路的开机证据。
|
||||
* 不依赖 10 s 心跳 → 台架 @test 的 settle 窗口内即可见(否则会被静默判据
|
||||
* 截断,见 docs/测试说明/自动化测试流程.md 的 settle 口径)。 */
|
||||
if ((first_phasor_logged == 0u) && (dbg_console_writer_ready() != 0u)) {
|
||||
phasor_t p;
|
||||
|
||||
if (phasor_get(0u, &p) == 0) {
|
||||
first_phasor_logged = 1u;
|
||||
log_phasors("first");
|
||||
}
|
||||
}
|
||||
|
||||
/* 5) 看门狗心跳(本任务已纳入 required_mask) */
|
||||
task_wd_beat(TASK_WD_ADC);
|
||||
|
||||
/* 4) 周期心跳日志:帧统计 + 驱动侧 FRSTDATA 错误计数。
|
||||
* 写者就绪前抑制(同 2) 的交错理由);到点无论是否落日志都推进下个上报点 */
|
||||
/* 6) 周期心跳日志:帧统计 + 驱动侧 FRSTDATA 错误计数 + L2 相量。
|
||||
* 写者就绪前抑制(同 3) 的交错理由);到点无论是否落日志都推进下个上报点 */
|
||||
{
|
||||
uint32_t now = osKernelGetTickCount();
|
||||
|
||||
if ((int32_t)(now - report_at) >= 0) {
|
||||
if (dbg_console_writer_ready() != 0u) {
|
||||
DBG_INFO("frames=%lu idle=%lu seq=%lu frst_err=%u adc1_rc=%d adc1_err=%lu",
|
||||
DBG_INFO("frames=%lu idle=%lu skip=%lu seq=%lu frst_err=%u adc1_rc=%d adc1_err=%lu ring=%lu",
|
||||
(unsigned long)s_frames_consumed,
|
||||
(unsigned long)s_frames_idle,
|
||||
(unsigned long)s_frames_skip,
|
||||
(unsigned long)tpafe5160_frame_seq(),
|
||||
(unsigned)tpafe5160_frstdata_err(),
|
||||
adc_input_stm32_rc(),
|
||||
(unsigned long)adc_input_stm32_errs());
|
||||
(unsigned long)adc_input_stm32_errs(),
|
||||
(unsigned long)sample_ring_frames());
|
||||
|
||||
log_phasors("hb");
|
||||
}
|
||||
report_at = now + ADC_TASK_REPORT_MS;
|
||||
}
|
||||
}
|
||||
|
||||
/* 5) 等下一个节拍(固定周期;落后则立即补齐,不累积漂移) */
|
||||
/* 7) 等下一个节拍(固定周期;落后则立即补齐,不累积漂移) */
|
||||
next += ADC_TASK_BEAT_MS;
|
||||
(void)osDelayUntil(next);
|
||||
}
|
||||
|
||||
@@ -942,6 +942,16 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\algo\adc_input.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>sample_ring.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\algo\sample_ring.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>phasor.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\algo\phasor.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
<Group>
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| ER_IROM1 | 269,940 B (263.61 KB) | 1,048,576 B (1024 KB) | 25.7% |
|
||||
| RW_IRAM1 | 89,320 B (87.23 KB) | 131,072 B (128 KB) | 68.1% |
|
||||
| ER_IROM1 | 271,440 B (265.08 KB) | 1,048,576 B (1024 KB) | 25.9% |
|
||||
| RW_IRAM1 | 90,936 B (88.80 KB) | 131,072 B (128 KB) | 69.4% |
|
||||
| RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% |
|
||||
| **Flash (ROM 合计)** | **270,712 B (264.4 KB)** | 1,048,576 B (1024 KB) | **25.8%** |
|
||||
| **Flash (ROM 合计)** | **272,228 B (265.8 KB)** | 1,048,576 B (1024 KB) | **26.0%** |
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
各区的容量与常量来源:
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
/*
|
||||
* 模块名称:相量层(L2)主机侧单元测试
|
||||
* 模块功能:在 PC 上对 App/algo/phasor.c + App/algo/sample_ring.c 做离线验证。
|
||||
*
|
||||
* 被测对象:真实源文件(以 #include 原样纳入,保证测的是生产代码而非副本):
|
||||
* ../../../App/algo/phasor.c
|
||||
* ../../../App/algo/sample_ring.c
|
||||
* 本层为**纯算法**(无寄存器、无 RTOS、无文件/外设依赖),故**无需任何桩**。
|
||||
*
|
||||
* 为什么要主机侧单测(而不是只用板端套件):
|
||||
* 1. 相量算法是纯数学变换,与硬件无关;板端**无信号源**,无法注入已知幅值/相位/
|
||||
* 频率的标准波形,因而无法验证"读数 = 设定值"这一核心判据(同 L1-3 的替代口径)。
|
||||
* 主机侧可合成任意波形,把幅值精度/谐波抑制/频率敏感性一次性钉死。
|
||||
* 2. 可精确构造边界:恰好一周波 / 不足一周波 / 环形回绕 / 任意次谐波 / 频偏。
|
||||
* 3. 不占 MCU 的 RAM,毫秒级完成。
|
||||
*
|
||||
* 覆盖点(对应 docs/算法/继电保护功能复现规划.md §3.2):
|
||||
* TC-PH-001 L2-1 系数表逐项等于 sin(2πk/N)·(√2/N) / cos(2πk/N)·(√2/N)(防转录错)
|
||||
* TC-PH-002 L2-1 系数表精确零点(sin@0,10 / cos@5,15)与归一化 Σcoe² = 1/N
|
||||
* TC-PH-003 L2-3 纯基波:fAmSQ = A/√2(有效值),fAm = √2·fAmSQ(幅值)
|
||||
* TC-PH-004 L2-3/L2-4 实部虚部与相位:atan2(fIm,fRe) = 注入相角
|
||||
* TC-PH-005 直流 + 2/3/5 次谐波被完全滤除(基波读数不变)
|
||||
* TC-PH-006 任意次谐波提取(索引步进 = 谐波次数):k=2 / k=5
|
||||
* TC-PH-007 频率偏离 50 Hz 的幅值误差(±1 Hz ≤ 1.5%,±2 Hz ≤ 3%,见 §9 Q3)
|
||||
* TC-PH-010 采样环:入环/取窗时序、回绕、饱和、帧计数、参数守卫
|
||||
* TC-PH-020 L2-2 实例层:实例数/实例表/未攒满一周波不产出
|
||||
* TC-PH-021 L2-6 滑动窗:连续滑动 20 拍基波读数恒定
|
||||
* TC-PH-022 L2-7 双区交换:使用区交替 + get 返回**最后一次** run 的结果(非陈旧)
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
*
|
||||
* 创建日期:2026-10-10
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define PH_PI 3.14159265358979323846
|
||||
/* 采样率(Hz):1000 Hz @ 50 Hz → 20 点/周波 */
|
||||
#define PH_FS 1000.0
|
||||
#define PH_F0 50.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"
|
||||
|
||||
/* ===================== 工具 ===================== */
|
||||
|
||||
/*
|
||||
* 函数功能:生成一段正弦窗
|
||||
* 入口参数:x - 输出(长度 >= n)
|
||||
* n - 点数
|
||||
* amp - 正弦峰值
|
||||
* f - 频率(Hz)
|
||||
* phase - 初相(度)
|
||||
* 返 回 值:无
|
||||
* 限定条件:无
|
||||
* 函数说明:以 t = i/PH_FS 采样,即"以当前时刻为终止的前 n 点窗"。
|
||||
*/
|
||||
static void gen_sine(float *x, uint8_t n, double amp, double f, double phase) {
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < n; i++) {
|
||||
x[i] = (float)(amp * sin(2.0 * PH_PI * f * (double)i / PH_FS + phase * PH_PI / 180.0));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按时刻 t 构造一整帧(9 路语义通道同值)
|
||||
* 入口参数:f - 输出帧(长度 = ADC_SEM_COUNT)
|
||||
* t - 采样序号(t/PH_FS 秒)
|
||||
* amp - 正弦峰值
|
||||
* phase - 初相(度)
|
||||
* 返 回 值:无
|
||||
* 限定条件:无
|
||||
* 函数说明:仅用于驱动实例层/采样环;**不代表真实硬件**(V0.1 上各通道不同刻,
|
||||
* 此处只为验证相量层本身)。
|
||||
*/
|
||||
static void frame_at(float *f, uint32_t t, double amp, double phase) {
|
||||
uint8_t c;
|
||||
double v = amp * sin(2.0 * PH_PI * PH_F0 * (double)t / PH_FS + phase * PH_PI / 180.0);
|
||||
|
||||
for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) {
|
||||
f[c] = (float)v;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== TC-PH-001/002:L2-1 系数表 ===================== */
|
||||
|
||||
static void tc_table(void) {
|
||||
const double scale = sqrt(2.0) / (double)PHASOR_N;
|
||||
double sum_sin = 0.0, sum_cos = 0.0;
|
||||
int ok_sin = 1, ok_cos = 1, ok_norm = 1;
|
||||
uint8_t k;
|
||||
|
||||
for (k = 0u; k < (uint8_t)PHASOR_N; k++) {
|
||||
double arg = 2.0 * PH_PI * (double)k / (double)PHASOR_N;
|
||||
double es = sin(arg) * scale;
|
||||
double ec = cos(arg) * scale;
|
||||
|
||||
if (fabs((double)k_fft_sin[k] - es) > 1e-7) { ok_sin = 0; }
|
||||
if (fabs((double)k_fft_cos[k] - ec) > 1e-7) { ok_cos = 0; }
|
||||
|
||||
/* 归一化:sin²+cos² = (√2/N)² = 2/N² */
|
||||
if (fabs((double)k_fft_sin[k] * (double)k_fft_sin[k]
|
||||
+ (double)k_fft_cos[k] * (double)k_fft_cos[k] - 2.0 / (PHASOR_N * PHASOR_N)) > 1e-9) {
|
||||
ok_norm = 0;
|
||||
}
|
||||
sum_sin += (double)k_fft_sin[k] * (double)k_fft_sin[k];
|
||||
sum_cos += (double)k_fft_cos[k] * (double)k_fft_cos[k];
|
||||
}
|
||||
|
||||
CHECK(ok_sin, "L2-1 coeff sin[k] == sin(2*pi*k/N)*(sqrt2/N) for all k");
|
||||
CHECK(ok_cos, "L2-1 coeff cos[k] == cos(2*pi*k/N)*(sqrt2/N) for all k");
|
||||
CHECK(ok_norm, "L2-1 per-k sin^2+cos^2 == 2/N^2 (normalization)");
|
||||
/* Σsin² = Σcos² = (√2/N)²·(N/2) = 1/N:这就是"fAmSQ 直接是有效值"的根 */
|
||||
CHECK(fabs(sum_sin - 1.0 / PHASOR_N) < 1e-7, "L2-1 sum(sin^2) == 1/N (=> RMS, not amplitude)");
|
||||
CHECK(fabs(sum_cos - 1.0 / PHASOR_N) < 1e-7, "L2-1 sum(cos^2) == 1/N (=> RMS, not amplitude)");
|
||||
|
||||
CHECK((k_fft_sin[0] == 0.0f) && (k_fft_sin[10] == 0.0f), "L2-1 sin exact zeros at k=0,10");
|
||||
CHECK((k_fft_cos[5] == 0.0f) && (k_fft_cos[15] == 0.0f), "L2-1 cos exact zeros at k=5,15");
|
||||
CHECK(fabs((double)k_fft_sin[5] - sqrt(2.0) / PHASOR_N) < 1e-7,
|
||||
"L2-1 sin[5] == sqrt(2)/N (peak coefficient)");
|
||||
}
|
||||
|
||||
/* ===================== TC-PH-003/004:基波幅值 / 有效值 / 相位 ===================== */
|
||||
|
||||
static void tc_fundamental(void) {
|
||||
static const struct { double amp; double ph; } k_case[] = {
|
||||
{ 57.7, 0.0 },
|
||||
{ 57.7, 30.0 },
|
||||
{ 57.7, -45.0 },
|
||||
{ 100.0, 90.0 },
|
||||
{ 1.0, 0.0 },
|
||||
{ 32767.0, 137.0 },
|
||||
};
|
||||
const unsigned int ncase = (unsigned int)(sizeof(k_case) / sizeof(k_case[0]));
|
||||
unsigned int i;
|
||||
int ok_rms = 1, ok_amp = 1, ok_ph = 1;
|
||||
float x[PHASOR_N];
|
||||
|
||||
for (i = 0u; i < ncase; i++) {
|
||||
phasor_t p;
|
||||
double exp_rms = k_case[i].amp / sqrt(2.0);
|
||||
double ph_meas;
|
||||
|
||||
gen_sine(x, (uint8_t)PHASOR_N, k_case[i].amp, PH_F0, k_case[i].ph);
|
||||
phasor_calc(x, (uint8_t)PHASOR_N, 1u, &p);
|
||||
|
||||
if (fabs((double)p.fAmSQ - exp_rms) / exp_rms > 0.01) { ok_rms = 0; }
|
||||
if (fabs((double)p.fAm - exp_rms * sqrt(2.0)) / (exp_rms * sqrt(2.0)) > 0.01) { ok_amp = 0; }
|
||||
|
||||
ph_meas = atan2((double)p.fIm, (double)p.fRe) * 180.0 / PH_PI;
|
||||
if (fabs(ph_meas - k_case[i].ph) > 0.5) {
|
||||
printf(" (amp=%.1f ph=%.1f -> measured %.4f)\n",
|
||||
k_case[i].amp, k_case[i].ph, ph_meas);
|
||||
ok_ph = 0;
|
||||
}
|
||||
}
|
||||
|
||||
CHECK(ok_rms, "L2-3 pure fundamental: fAmSQ == A/sqrt(2) (RMS) within 1%");
|
||||
CHECK(ok_amp, "L2-3 fAm == sqrt(2)*fAmSQ (amplitude) within 1%");
|
||||
CHECK(ok_ph, "L2-4/L2-3 atan2(fIm,fRe) == injected phase (6 amp/phase cases)");
|
||||
}
|
||||
|
||||
/* ===================== TC-PH-005:直流与谐波被滤除 ===================== */
|
||||
|
||||
static void tc_filtering(void) {
|
||||
const double amp = 57.7, exp_rms = amp / sqrt(2.0);
|
||||
static const struct { double dc, h2, h3, h5; const char *name; } k_case[] = {
|
||||
{ 0.0, 0.0, 0.0, 0.0, "pure" },
|
||||
{ 1.0, 0.0, 0.0, 0.0, "with 1V DC" },
|
||||
{ 0.0, 0.0, 30.0, 0.0, "with 30% 3rd harmonic" },
|
||||
{ 0.0, 0.0, 0.0, 11.5, "with 20% 5th harmonic" },
|
||||
{ 1.0, 20.0, 15.0, 11.5, "DC + 2nd + 3rd + 5th (doc 6 case)" },
|
||||
};
|
||||
const unsigned int ncase = (unsigned int)(sizeof(k_case) / sizeof(k_case[0]));
|
||||
unsigned int i;
|
||||
int ok = 1;
|
||||
float x[PHASOR_N];
|
||||
|
||||
for (i = 0u; i < ncase; i++) {
|
||||
phasor_t p;
|
||||
uint8_t k;
|
||||
double err;
|
||||
|
||||
for (k = 0u; k < (uint8_t)PHASOR_N; k++) {
|
||||
double arg = 2.0 * PH_PI * (double)k / (double)PHASOR_N;
|
||||
x[k] = (float)(amp * sin(arg + 30.0 * PH_PI / 180.0)
|
||||
+ k_case[i].dc
|
||||
+ k_case[i].h2 * sin(2.0 * arg)
|
||||
+ k_case[i].h3 * sin(3.0 * arg)
|
||||
+ k_case[i].h5 * sin(5.0 * arg));
|
||||
}
|
||||
|
||||
phasor_calc(x, (uint8_t)PHASOR_N, 1u, &p);
|
||||
err = fabs((double)p.fAmSQ - exp_rms) / exp_rms;
|
||||
if (err > 0.01) {
|
||||
printf(" (%s -> fAmSQ=%.4f expect=%.4f err=%.3f%%)\n",
|
||||
k_case[i].name, (double)p.fAmSQ, exp_rms, err * 100.0);
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
|
||||
CHECK(ok, "L2-3 DC + 2nd/3rd/5th harmonics fully rejected (5 cases, err <= 1%)");
|
||||
}
|
||||
|
||||
/* ===================== TC-PH-006:任意次谐波提取 ===================== */
|
||||
|
||||
static void tc_harmonic(void) {
|
||||
/* 基波 100 + 二次 25 + 五次 12(峰值);三根谱线应各自独立读出 */
|
||||
static const struct { uint8_t harm; double amp; } k_case[] = {
|
||||
{ 1u, 100.0 },
|
||||
{ 2u, 25.0 },
|
||||
{ 5u, 12.0 },
|
||||
};
|
||||
const unsigned int ncase = (unsigned int)(sizeof(k_case) / sizeof(k_case[0]));
|
||||
unsigned int i;
|
||||
int ok = 1;
|
||||
float x[PHASOR_N];
|
||||
uint8_t k;
|
||||
|
||||
for (k = 0u; k < (uint8_t)PHASOR_N; k++) {
|
||||
double arg = 2.0 * PH_PI * (double)k / (double)PHASOR_N;
|
||||
x[k] = (float)(100.0 * sin(arg) + 25.0 * sin(2.0 * arg) + 12.0 * sin(5.0 * arg));
|
||||
}
|
||||
|
||||
for (i = 0u; i < ncase; i++) {
|
||||
phasor_t p;
|
||||
double exp_rms = k_case[i].amp / sqrt(2.0);
|
||||
|
||||
phasor_calc(x, (uint8_t)PHASOR_N, k_case[i].harm, &p);
|
||||
if (fabs((double)p.fAmSQ - exp_rms) / exp_rms > 0.01) {
|
||||
printf(" (harm=%u -> fAmSQ=%.4f expect=%.4f)\n",
|
||||
(unsigned)k_case[i].harm, (double)p.fAmSQ, exp_rms);
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
|
||||
CHECK(ok, "L2-1 harmonic extraction k=1/2/5 from a mixed signal (index step = order)");
|
||||
|
||||
/* 纯基波时谐波轨应≈0(选择性) */
|
||||
gen_sine(x, (uint8_t)PHASOR_N, 100.0, PH_F0, 0.0);
|
||||
{
|
||||
phasor_t p2, p5;
|
||||
phasor_calc(x, (uint8_t)PHASOR_N, 2u, &p2);
|
||||
phasor_calc(x, (uint8_t)PHASOR_N, 5u, &p5);
|
||||
CHECK(((double)p2.fAmSQ < 1e-3) && ((double)p5.fAmSQ < 1e-3),
|
||||
"L2-1 pure fundamental leaves 2nd/5th harmonic bins ~0");
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== TC-PH-007:频率敏感性 ===================== */
|
||||
|
||||
static void tc_freqdev(void) {
|
||||
const double amp = 57.7, exp_rms = amp / sqrt(2.0);
|
||||
/* 最坏相角下的幅值误差(实测:±1 Hz ≤1.1%,±2 Hz ≤2.33%;口径见 §9 Q3) */
|
||||
static const struct { double f; double limit_pct; } k_case[] = {
|
||||
{ 50.0, 0.10 },
|
||||
{ 49.0, 1.50 },
|
||||
{ 51.0, 1.50 },
|
||||
{ 48.0, 3.00 },
|
||||
{ 52.0, 3.00 },
|
||||
};
|
||||
const unsigned int ncase = (unsigned int)(sizeof(k_case) / sizeof(k_case[0]));
|
||||
unsigned int i;
|
||||
int ok = 1;
|
||||
float x[PHASOR_N];
|
||||
|
||||
for (i = 0u; i < ncase; i++) {
|
||||
double worst = 0.0;
|
||||
int ph;
|
||||
|
||||
for (ph = 0; ph < 360; ph += 15) {
|
||||
phasor_t p;
|
||||
double err;
|
||||
|
||||
gen_sine(x, (uint8_t)PHASOR_N, amp, k_case[i].f, (double)ph);
|
||||
phasor_calc(x, (uint8_t)PHASOR_N, 1u, &p);
|
||||
err = fabs((double)p.fAmSQ - exp_rms) / exp_rms * 100.0;
|
||||
if (err > worst) { worst = err; }
|
||||
}
|
||||
|
||||
printf(" (f=%5.1f Hz worst err over phase = %.2f%%, limit %.2f%%)\n",
|
||||
k_case[i].f, worst, k_case[i].limit_pct);
|
||||
if (worst > k_case[i].limit_pct) { ok = 0; }
|
||||
}
|
||||
|
||||
CHECK(ok, "L2-3 frequency sensitivity: 50Hz<=0.1%, +-1Hz<=1.5%, +-2Hz<=3% (worst phase)");
|
||||
}
|
||||
|
||||
/* ===================== TC-PH-010:采样环 ===================== */
|
||||
|
||||
static void tc_ring(void) {
|
||||
float f[ADC_SEM_COUNT];
|
||||
float w[SAMPLE_RING_DEPTH];
|
||||
uint8_t c;
|
||||
int i;
|
||||
int ok;
|
||||
|
||||
sample_ring_init();
|
||||
CHECK(sample_ring_filled() == 0u, "L2-in ring: init clears fill");
|
||||
CHECK(sample_ring_frames() == 0u, "L2-in ring: init clears frame counter");
|
||||
CHECK(sample_ring_window(ADC_SEM_UA, 20u, w) == -2, "L2-in ring: window before data -> -2");
|
||||
|
||||
/* 前 3 帧:UA = 100/200/300 */
|
||||
ok = 1;
|
||||
for (i = 0; i < 3; i++) {
|
||||
for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) { f[c] = 0.0f; }
|
||||
f[ADC_SEM_UA] = (float)(100 * (i + 1));
|
||||
if (sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT) != 0) { ok = 0; }
|
||||
}
|
||||
CHECK(ok, "L2-in ring: 3 frames pushed OK");
|
||||
CHECK(sample_ring_filled() == 3u, "L2-in ring: filled == 3");
|
||||
CHECK((sample_ring_window(ADC_SEM_UA, 3u, w) == 0)
|
||||
&& (w[0] == 100.0f) && (w[1] == 200.0f) && (w[2] == 300.0f),
|
||||
"L2-in ring: window is chronological (old -> new)");
|
||||
/* 通道隔离:其它语义通道不应被 UA 的写入污染 */
|
||||
CHECK((sample_ring_window(ADC_SEM_IB, 3u, w) == 0)
|
||||
&& (w[0] == 0.0f) && (w[1] == 0.0f) && (w[2] == 0.0f),
|
||||
"L2-in ring: channels are independent");
|
||||
|
||||
/* 推到 45 帧(> DEPTH=40)→ 回绕 */
|
||||
for (i = 3; i < 45; i++) {
|
||||
for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) { f[c] = 0.0f; }
|
||||
f[ADC_SEM_UA] = (float)i;
|
||||
(void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT);
|
||||
}
|
||||
CHECK(sample_ring_filled() == SAMPLE_RING_DEPTH, "L2-in ring: fill saturates at DEPTH");
|
||||
CHECK(sample_ring_frames() == 45u, "L2-in ring: frame counter is NOT saturated");
|
||||
|
||||
ok = (sample_ring_window(ADC_SEM_UA, 20u, w) == 0);
|
||||
for (i = 0; i < 20; i++) { if (w[i] != (float)(25 + i)) { ok = 0; } }
|
||||
CHECK(ok, "L2-in ring: wrapped window == last 20 values, in order");
|
||||
|
||||
ok = (sample_ring_window(ADC_SEM_UA, (uint8_t)SAMPLE_RING_DEPTH, w) == 0);
|
||||
for (i = 0; i < (int)SAMPLE_RING_DEPTH; i++) { if (w[i] != (float)(5 + i)) { ok = 0; } }
|
||||
CHECK(ok, "L2-in ring: full-depth window == last DEPTH values, in order");
|
||||
|
||||
CHECK(sample_ring_window(ADC_SEM_UA, 0u, w) == -1, "L2-in ring: n=0 rejected");
|
||||
CHECK(sample_ring_window(ADC_SEM_UA, (uint8_t)(SAMPLE_RING_DEPTH + 1u), w) == -1,
|
||||
"L2-in ring: n > DEPTH rejected");
|
||||
CHECK(sample_ring_window((adc_semantic_t)ADC_SEM_COUNT, 20u, w) == -1,
|
||||
"L2-in ring: out-of-range channel rejected");
|
||||
CHECK(sample_ring_window(ADC_SEM_UA, 20u, NULL) == -1, "L2-in ring: NULL out rejected");
|
||||
CHECK(sample_ring_push_frame(NULL, (uint8_t)ADC_SEM_COUNT) == -1, "L2-in ring: NULL frame rejected");
|
||||
CHECK(sample_ring_push_frame(f, 3u) == -1, "L2-in ring: wrong frame length rejected");
|
||||
}
|
||||
|
||||
/* ===================== TC-PH-020/021/022:实例层 + 滑动窗 + 双区 ===================== */
|
||||
|
||||
static void tc_instances(void) {
|
||||
const double amp = 100.0, exp_rms = amp / sqrt(2.0);
|
||||
float f[ADC_SEM_COUNT];
|
||||
phasor_t p;
|
||||
uint32_t t;
|
||||
int ok;
|
||||
uint8_t i;
|
||||
|
||||
CHECK(phasor_inst_count() == 5u, "L2-2 instance count == 5");
|
||||
CHECK((phasor_inst(0u) != NULL) && (phasor_inst(0u)->name != NULL), "L2-2 instance name available");
|
||||
CHECK(phasor_inst((uint8_t)phasor_inst_count()) == NULL, "L2-2 out-of-range instance -> NULL");
|
||||
|
||||
phasor_init();
|
||||
CHECK(s_use_zone == 0u, "L2-7 use zone starts at 0");
|
||||
CHECK(phasor_get(0u, &p) == -2, "L2-2 no result before a full cycle");
|
||||
CHECK(phasor_get((uint8_t)phasor_inst_count(), &p) == -1, "L2-2 out-of-range index -> -1");
|
||||
CHECK(phasor_get(0u, NULL) == -1, "L2-2 NULL out -> -1");
|
||||
|
||||
/* 喂 19 帧(不足一周波)→ 仍不产出 */
|
||||
for (t = 0u; t < 19u; t++) {
|
||||
frame_at(f, t, amp, 0.0);
|
||||
(void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT);
|
||||
phasor_run();
|
||||
}
|
||||
CHECK(phasor_get(0u, &p) == -2, "L2-2 still no result after 19 frames");
|
||||
CHECK(s_use_zone == 0u, "L2-7 use zone unchanged while not producing");
|
||||
|
||||
/* 第 20 帧 → 攒满一周波,产出 */
|
||||
frame_at(f, 19u, amp, 0.0);
|
||||
(void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT);
|
||||
phasor_run();
|
||||
CHECK(phasor_get(0u, &p) == 0, "L2-2 result available after 20 frames");
|
||||
CHECK(s_use_zone == 1u, "L2-7 use zone toggles to 1 after first production");
|
||||
CHECK(fabs((double)p.fAmSQ - exp_rms) / exp_rms < 0.01,
|
||||
"L2-2/L2-3 instance 0 (C01-IA-F1) RMS == A/sqrt(2)");
|
||||
{
|
||||
phasor_t p2, p5;
|
||||
int ok_sel = 1;
|
||||
if (phasor_get(3u, &p2) != 0) { ok_sel = 0; }
|
||||
if (phasor_get(4u, &p5) != 0) { ok_sel = 0; }
|
||||
CHECK(ok_sel && ((double)p2.fAmSQ < 1e-3) && ((double)p5.fAmSQ < 1e-3),
|
||||
"L2-2 harmonic instances (C04-IA-F2 / C05-IA-F5) ~0 on pure fundamental");
|
||||
}
|
||||
|
||||
/* L2-6 滑动窗:继续推 20 帧,每拍重算,基波读数应恒定 */
|
||||
ok = 1;
|
||||
for (t = 20u; t < 40u; t++) {
|
||||
frame_at(f, t, amp, 0.0);
|
||||
(void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT);
|
||||
phasor_run();
|
||||
if (phasor_get(0u, &p) != 0) { ok = 0; }
|
||||
else if (fabs((double)p.fAmSQ - exp_rms) / exp_rms > 0.01) { ok = 0; }
|
||||
}
|
||||
CHECK(ok, "L2-6 sliding window: RMS constant across 20 consecutive slides");
|
||||
|
||||
/* 实例表一致性:逐实例复核"实例窗 == 独立 phasor_calc 结果" */
|
||||
ok = 1;
|
||||
for (i = 0u; i < phasor_inst_count(); i++) {
|
||||
const phasor_inst_t *d = phasor_inst(i);
|
||||
float win[PHASOR_N];
|
||||
phasor_t ref, got;
|
||||
|
||||
if (d == NULL) { ok = 0; break; }
|
||||
if (sample_ring_window(d->sem, (uint8_t)PHASOR_N, win) != 0) { ok = 0; break; }
|
||||
phasor_calc(win, (uint8_t)PHASOR_N, d->harm, &ref);
|
||||
if (phasor_get(i, &got) != 0) { ok = 0; break; }
|
||||
if ((ref.fRe != got.fRe) || (ref.fIm != got.fIm) || (ref.fAmSQ != got.fAmSQ)) { ok = 0; break; }
|
||||
}
|
||||
CHECK(ok, "L2-2/L2-6 instance results == phasor_calc() on the same ring window");
|
||||
}
|
||||
|
||||
/* ===================== TC-PH-022:L2-7 双区指针原子交换 ===================== */
|
||||
|
||||
static void tc_dualzone(void) {
|
||||
const double amp = 100.0;
|
||||
float f[ADC_SEM_COUNT];
|
||||
phasor_t p;
|
||||
uint32_t t;
|
||||
int ok;
|
||||
|
||||
/* 1) 使用区逐次产出交替(0 -> 1 -> 0 -> 1) */
|
||||
phasor_init();
|
||||
for (t = 0u; t < 20u; t++) {
|
||||
frame_at(f, t, amp, 0.0);
|
||||
(void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT);
|
||||
phasor_run();
|
||||
}
|
||||
CHECK(s_use_zone == 1u, "L2-7 use zone == 1 after 1st production");
|
||||
|
||||
frame_at(f, 20u, amp, 0.0);
|
||||
(void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT);
|
||||
phasor_run();
|
||||
CHECK(s_use_zone == 0u, "L2-7 use zone == 0 after 2nd production");
|
||||
|
||||
frame_at(f, 21u, amp, 0.0);
|
||||
(void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT);
|
||||
phasor_run();
|
||||
CHECK(s_use_zone == 1u, "L2-7 use zone == 1 after 3rd production");
|
||||
|
||||
/* 2) 每拍产出后 get() 必须等于"当前环窗的独立重算"。
|
||||
* 幅值每 20 拍切换(100 <-> 10),故"读到陈旧区"会立刻不相等——
|
||||
* 连测 60 拍,覆盖两个区的来回切换,比单次比较强得多。 */
|
||||
phasor_init();
|
||||
ok = 1;
|
||||
for (t = 0u; t < 60u; t++) {
|
||||
float win[PHASOR_N];
|
||||
phasor_t ref, got;
|
||||
double a = (((t / 20u) % 2u) == 0u) ? amp : (amp / 10.0);
|
||||
|
||||
frame_at(f, t, a, 0.0);
|
||||
(void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT);
|
||||
phasor_run();
|
||||
|
||||
if (sample_ring_window(ADC_SEM_IA, (uint8_t)PHASOR_N, win) != 0) {
|
||||
continue; /* 尚未攒满一周波:本拍按契约不产出,无需比较 */
|
||||
}
|
||||
phasor_calc(win, (uint8_t)PHASOR_N, 1u, &ref);
|
||||
if (phasor_get(0u, &got) != 0) { ok = 0; break; }
|
||||
if ((ref.fRe != got.fRe) || (ref.fIm != got.fIm) || (ref.fAmSQ != got.fAmSQ)) { ok = 0; break; }
|
||||
}
|
||||
CHECK(ok, "L2-7 dual-zone: get() == fresh recompute of the live window, all 60 beats");
|
||||
CHECK(phasor_get(0u, &p) == 0, "L2-7 result still readable after the sweep");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== Phasor Tests ===\n");
|
||||
|
||||
printf("-- L2-1 coefficient table --\n");
|
||||
tc_table();
|
||||
printf("-- L2-3/L2-4 fundamental amplitude/RMS/phase --\n");
|
||||
tc_fundamental();
|
||||
printf("-- DC/harmonic rejection --\n");
|
||||
tc_filtering();
|
||||
printf("-- arbitrary harmonic extraction --\n");
|
||||
tc_harmonic();
|
||||
printf("-- frequency sensitivity --\n");
|
||||
tc_freqdev();
|
||||
printf("-- L2 input sample ring --\n");
|
||||
tc_ring();
|
||||
printf("-- L2-2/L2-6/L2-7 instance layer --\n");
|
||||
tc_instances();
|
||||
printf("-- L2-7 dual-zone swap --\n");
|
||||
tc_dualzone();
|
||||
|
||||
printf("\n=== Phasor Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
return (s_failed == 0) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""相量层(L2)的主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/phasor/phasor_test.c(它以 #include 纳入**生产源文件**
|
||||
App/algo/phasor.c 与 App/algo/sample_ring.c),在 PC 上离线跑完整的相量算法验证:
|
||||
系数表逐项复核 / 幅值-有效值-相位 / 直流与谐波抑制 / 任意次谐波提取 /
|
||||
频率敏感性 / 采样环时序与回绕 / 实例层与双区交换,失败即非 0 退出。
|
||||
零硬件、零烧录、毫秒级。
|
||||
|
||||
工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness
|
||||
`test/host/common/unit_test_harness.py`(本文件只做声明)。
|
||||
|
||||
为什么要主机侧单测(而不是只用板端套件):
|
||||
相量算法是纯数学变换,与硬件无关。**板端无信号源**,无法注入已知幅值/相位/频率
|
||||
的标准波形,因而无法验证"读数 = 设定有效值"这一 L2-3 核心判据(与 L1-3 的替代口径
|
||||
同源)。主机侧可合成任意波形并精确构造边界(恰好一周波 / 不足一周波 / 环形回绕 /
|
||||
任意次谐波 / 频偏),且不占 MCU 的 RAM。
|
||||
|
||||
本层为**纯算法**:无寄存器、无 RTOS、无文件/外设依赖,故**不需要任何桩目录**。
|
||||
|
||||
用法(仓库根目录):
|
||||
python test/host/phasor/run_unit_test.py
|
||||
或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/phasor/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
|
||||
|
||||
设计依据:docs/算法/继电保护功能复现规划.md §3.2(L2-1/2/3/4/6/7);
|
||||
docs/算法/傅氏算法详解.md §2.3(系数口径);
|
||||
docs/测试说明/测试开发标准流程.md §4.2(L0.5 主机侧单测)。
|
||||
创建日期:2026-10-10
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
|
||||
# harness 位于 test/host/common/
|
||||
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
|
||||
|
||||
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
|
||||
|
||||
# 无桩目录:被测代码为纯算法(phasor.c / sample_ring.c 仅依赖 math.h 与 App/algo 内的头)。
|
||||
# App/algo 提供 phasor.h / sample_ring.h / adc_input.h(后者为纯枚举 + 声明,可主机编译)。
|
||||
BUILD = TestBuild(
|
||||
name="Phasor Tests",
|
||||
src=os.path.join(HERE, "phasor_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(ROOT, "App", "algo"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== Phasor Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
Reference in New Issue
Block a user