593c5d0c84
用户裁定(2026-10-28):L1/L1.5/L2/L2.5/L3 这套层号「谁能看懂」,改用语义名。 映射(同一批定稿,含 L4/L5): L1 → 语义取数与有效性 L1.5 → 标定折算 L2 → 全周傅氏相量 L2.5 → 测频与遥测快照 L3 → 保护判据 L4 → 出口与记录 L5 → 动作留痕 实测规模:384 站点 / 62 文件(App/algo、App/task、test/host 六个套件、docs/算法、 产品需求、架构总览、启动顺序、ADC 采样节拍、IEC104 上行、采集短板评测、内存管理、 AGENTS.md、README.md、docs/README.md、.pre-commit-config.yaml 与 scripts 里的注释)。 改法(不是全局替换,逐模式有上下文规则): - 先吃同位语再落裸词:L1 有效性校验 → 语义取数与有效性校验、L2 相量层 → 全周傅氏 相量层、L3 保护 → 保护判据、L2.5 遥测快照 → 测频与遥测快照,避免「保护判据保护判据」 这类重复(第一版就是这么坏掉的,已整体回滚重做)。 - .c/.h 只改注释:先把字符串字面量切出去(引号数为奇数的跨行注释按整行处理), C 字符串里的 L2-in ring 之类原样保留——AGENTS 铁律「字符串字面量必须纯 ASCII」。 - 层内条目号(L1-3 / L2-3 这类)**本批不动**,属批次 2;另外三族(UI 菜单层级 L0~L3、链路延迟分段 L1~L8、CubeMX 防丢失三层)也不动,属批次 3。 - 顺手修的措辞:范围写法 L1~L3 → 「分层(语义取数与有效性 → 保护判据)」、 CI 段「不含 L1/L2/L3」→「不含采集链分层」、目录地图里同一处并列表述。 验证:@build 0 错 0 警(Code=236456,与改前逐字节同尺寸——纯注释改动); 内存布局配图已按新脚本重生成(自检通过);doc-links --all --sections-strict 通过 (195 文档);mojibake / bom-guard / coding-standard / 新编号门禁自查 全 rc=0; git diff --check 干净。
375 lines
14 KiB
C
375 lines
14 KiB
C
/*
|
||
* 模块名称:频率测量层(全周傅氏相量的派生量,B8)主机侧单元测试
|
||
* 模块功能:在 PC 上对 App/algo/freq.c + phasor.c + sample_ring.c 做离线验证。
|
||
*
|
||
* 被测对象:真实源文件(#include 原样纳入,纯算法无需桩):
|
||
* ../../../App/algo/sample_ring.c
|
||
* ../../../App/algo/phasor.c
|
||
* ../../../App/algo/freq.c
|
||
*
|
||
* 精度判据(与 docs/算法/采集与算法层设计说明.md §7.2 同口径,测频范围见规划 §4.2):
|
||
* J1 纯音扫频 45.00~55.00 Hz(0.5 Hz 步进):|f_meas - f_true| <= 0.02 Hz
|
||
* (实测最坏 0.0101 Hz @55.0 Hz,判据留 2 倍裕度)
|
||
* J2 噪声工况(幅值 1000 码、噪声 rms = 1% 幅值):|f_meas - f_true| <= 0.10 Hz
|
||
* (实测最坏 0.016 Hz,判据留 ~6 倍裕度)
|
||
* J3 启动收敛:首个有效相量后 <= 40 ms 出有效读数
|
||
* J4 频率阶跃 50->51 Hz:<= 120 ms 进入 ±0.05 Hz 并保持(自适应窗
|
||
* K_eff≈73~89 拍,阶跃后窗冲刷决定物理下限;OF/UF 定时限 >=100 ms
|
||
* 叠加其上,总动作时间仍 <220 ms,符合定时限保护常规)
|
||
* J5 零幅/浮空(rms < 幅值门):valid=0、读数保持、不发散(无 NaN/无跳变;
|
||
* 信号消失过渡段 <=20 ms 窗内残窗参与均值,读数漂移有界 <=0.3 Hz)
|
||
* J6 假值钳制:相位/幅值跳变拍产生的单拍折算越出 35~65 Hz 假值带 -> 丢弃,
|
||
* 读数在跳变后 60 ms 内回到 ±0.05 Hz
|
||
*
|
||
* 覆盖点:
|
||
* TC-FREQ-001 50 Hz 收敛时序(J3)+ 稳态精度(J1 子集)
|
||
* TC-FREQ-002 扫频 45~55 Hz @0.5 Hz(J1,逐点判 + 最坏值打印)
|
||
* TC-FREQ-003 噪声鲁棒(J2,最坏值打印)
|
||
* TC-FREQ-004 频率阶跃 50->51(J4)
|
||
* TC-FREQ-005 零幅浮空不发散(J5)+ 恢复后重新收敛
|
||
* TC-FREQ-006 直流偏置抑制(全周傅氏滤直流 -> 测频不受影响)
|
||
* TC-FREQ-007 3/5 次谐波污染抑制(相量基波选择性)
|
||
* TC-FREQ-008 相位跳变假值钳制(J6)
|
||
* TC-FREQ-009 UA 无幅值 -> 自动落 IA 源
|
||
* TC-FREQ-010 init 幂等 / get 参数守卫 / 未收敛 -2
|
||
* TC-FREQ-011 幅值门中途失效 -> valid 清零 + 读数保持(J5 中途版)
|
||
*
|
||
* 通过:全部 TC PASS(main 返回 0)。
|
||
* 创建日期:2026-10-12(B8)
|
||
*/
|
||
|
||
#include <math.h>
|
||
#include <stdint.h>
|
||
#include <stdio.h>
|
||
|
||
#define FR_PI 3.14159265358979323846
|
||
#define FR_FS 1000.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"
|
||
#include "freq.c"
|
||
|
||
/* ===================== 波形驱动 ===================== */
|
||
|
||
/* 全局采样时刻(拍):驱动函数按此生成"当前时刻"的样本,跨调用连续 */
|
||
static uint32_t s_t = 0u;
|
||
|
||
/* 确定性伪随机(LCG):噪声可复现 */
|
||
static uint32_t s_rng = 0x12345678u;
|
||
static double s_noise_unit(void) { /* [-0.5, 0.5) */
|
||
s_rng = s_rng * 1103515245u + 12345u;
|
||
return (double)((s_rng >> 8) & 0xFFFFFFu) / (double)0x1000000u - 0.5;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:合成一拍整帧并跑一遍 全周傅氏相量+测频链
|
||
* 入口参数:f - 信号频率(Hz)
|
||
* amp - 基波峰值(码值)
|
||
* dc - 直流偏置(码值)
|
||
* h3, h5 - 3/5 次谐波峰值(码值)
|
||
* noise_rms- 高斯样近似噪声 rms(码值,均匀分布近似)
|
||
* ua_en - UA 通道是否带波形(0 = 只挂 0 码,测 IA 回退)
|
||
* 返 回 值:无
|
||
* 限定条件:无
|
||
* 函数说明:9 路语义通道同波形(除 ua_en=0 时 UA 置 0);每拍
|
||
* push_frame -> phasor_run -> freq_run,与 adcTask 同序。
|
||
*/
|
||
static void drive_beat(double f, double amp, double dc, double h3, double h5,
|
||
double noise_rms, uint8_t ua_en) {
|
||
float frame[ADC_SEM_COUNT];
|
||
double t = (double)s_t / FR_FS;
|
||
double v = amp * sin(2.0 * FR_PI * f * t)
|
||
+ h3 * sin(2.0 * FR_PI * 3.0 * f * t)
|
||
+ h5 * sin(2.0 * FR_PI * 5.0 * f * t)
|
||
+ dc
|
||
+ s_noise_unit() * noise_rms * 1.7320508; /* 均匀 -> rms 折算 */
|
||
uint8_t c;
|
||
|
||
for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) {
|
||
frame[c] = (float)v;
|
||
}
|
||
if (ua_en == 0u) {
|
||
frame[ADC_SEM_UA] = 0.0f; /* UA 无波形(门失败),IA 仍有 */
|
||
}
|
||
if (f == 0.0) {
|
||
for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) {
|
||
frame[c] = 0.0f; /* 浮空:全 0 帧 */
|
||
}
|
||
}
|
||
(void)sample_ring_push_frame(frame, (uint8_t)ADC_SEM_COUNT);
|
||
phasor_run();
|
||
freq_run();
|
||
s_t++;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:连续驱动 n 拍
|
||
*/
|
||
static void drive_n(uint32_t n, double f, double amp, double dc, double h3,
|
||
double h5, double noise_rms, uint8_t ua_en) {
|
||
uint32_t i;
|
||
|
||
for (i = 0u; i < n; i++) {
|
||
drive_beat(f, amp, dc, h3, h5, noise_rms, ua_en);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:复位整条链(采样时刻 + 三层状态)
|
||
*/
|
||
static void chain_reset(void) {
|
||
s_t = 0u;
|
||
s_rng = 0x12345678u;
|
||
(void)phasor_init(); /* 连带 sample_ring_init */
|
||
(void)freq_init();
|
||
}
|
||
|
||
/* ===================== TC-FREQ-001:50 Hz 收敛时序 + 稳态精度 ===================== */
|
||
static void tc_converge_50(void) {
|
||
float f = 0.0f;
|
||
uint32_t first_valid_t = 0u, phasor_ready_t = 0u;
|
||
uint32_t i;
|
||
int got = 0;
|
||
|
||
chain_reset();
|
||
for (i = 0u; i < 200u; i++) {
|
||
drive_beat(50.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
|
||
if (phasor_ready_t == 0u) {
|
||
phasor_t p;
|
||
if (phasor_get(1u, &p) == 0) {
|
||
phasor_ready_t = i; /* 首个有效相量拍 */
|
||
}
|
||
}
|
||
if (!got && (freq_valid() != 0u) && (freq_get(&f) == 0)) {
|
||
first_valid_t = i;
|
||
got = 1;
|
||
}
|
||
}
|
||
CHECK(got, "TC-FREQ-001 freq becomes valid at 50 Hz");
|
||
CHECK((first_valid_t >= phasor_ready_t)
|
||
&& ((first_valid_t - phasor_ready_t) <= 40u),
|
||
"TC-FREQ-001 J3: first valid reading <= 40 ms after first phasor");
|
||
(void)freq_get(&f);
|
||
printf(" [DATA] 50 Hz steady: f=%.4f err=%.4f Hz (first valid @%u ms, phasor @%u ms)\n",
|
||
(double)f, (double)f - 50.0, (unsigned)first_valid_t, (unsigned)phasor_ready_t);
|
||
CHECK(fabsf(f - 50.0f) <= 0.01f, "TC-FREQ-001 J1(50Hz): |err| <= 0.01 Hz");
|
||
}
|
||
|
||
/* ===================== TC-FREQ-002:扫频 45~55 Hz @0.5 Hz(J1) ===================== */
|
||
static void tc_sweep(void) {
|
||
double worst = 0.0, worst_f = 0.0;
|
||
int ok = 1;
|
||
int k;
|
||
|
||
for (k = 0; k <= 20; k++) {
|
||
double f_true = 45.0 + 0.5 * (double)k;
|
||
float f_meas = 0.0f;
|
||
double err;
|
||
|
||
chain_reset();
|
||
drive_n(150u, f_true, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
|
||
CHECK(freq_get(&f_meas) == 0, "TC-FREQ-002 freq valid at each sweep point");
|
||
err = fabs((double)f_meas - f_true);
|
||
if (err > worst) { worst = err; worst_f = f_true; }
|
||
if (err > 0.02) { ok = 0; }
|
||
}
|
||
printf(" [DATA] sweep 45.00~55.00 Hz step 0.5: worst |err| = %.4f Hz @ %.1f Hz (J1 <= 0.02)\n",
|
||
worst, worst_f);
|
||
CHECK(ok, "TC-FREQ-002 J1: |f_meas - f_true| <= 0.02 Hz over 45~55 Hz");
|
||
}
|
||
|
||
/* ===================== TC-FREQ-003:噪声鲁棒(J2) ===================== */
|
||
static void tc_noise(void) {
|
||
double worst = 0.0;
|
||
int ok = 1;
|
||
int k;
|
||
|
||
for (k = 0; k < 5; k++) {
|
||
double f_true = 45.0 + 2.5 * (double)k; /* 45/47.5/50/52.5/55 */
|
||
float f_meas = 0.0f;
|
||
double err;
|
||
|
||
chain_reset();
|
||
drive_n(200u, f_true, 1000.0, 0.0, 0.0, 0.0, 10.0, 1u); /* rms = 1% 幅值 */
|
||
(void)freq_get(&f_meas);
|
||
err = fabs((double)f_meas - f_true);
|
||
if (err > worst) { worst = err; }
|
||
if (err > 0.10) { ok = 0; }
|
||
}
|
||
printf(" [DATA] noise rms=1%% amp: worst |err| = %.4f Hz (J2 <= 0.10)\n", worst);
|
||
CHECK(ok, "TC-FREQ-003 J2: |err| <= 0.10 Hz with 1% noise");
|
||
}
|
||
|
||
/* ===================== TC-FREQ-004:频率阶跃 50->51(J4) ===================== */
|
||
static void tc_step(void) {
|
||
float f = 0.0f;
|
||
uint32_t settle = 0u;
|
||
uint32_t i;
|
||
int ok = 1;
|
||
|
||
chain_reset();
|
||
drive_n(150u, 50.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
|
||
(void)freq_get(&f);
|
||
printf(" [DATA] pre-step f=%.4f\n", (double)f);
|
||
|
||
/* 阶跃拍 = t=150;判"进入 ±0.05 且保持" */
|
||
for (i = 0u; i < 100u; i++) {
|
||
drive_beat(51.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
|
||
(void)freq_get(&f);
|
||
if (settle == 0u) {
|
||
if (fabsf(f - 51.0f) <= 0.05f) {
|
||
settle = i + 1u; /* 阶跃后第 N 拍进入带内 */
|
||
}
|
||
} else if (fabsf(f - 51.0f) > 0.05f) {
|
||
ok = 0; /* 进入后又出带 */
|
||
}
|
||
}
|
||
printf(" [DATA] step 50->51: settle = %u ms, final f=%.4f err=%.4f\n",
|
||
(unsigned)settle, (double)f, (double)f - 51.0);
|
||
CHECK((settle != 0u) && (settle <= 120u), "TC-FREQ-004 J4: settle <= 120 ms");
|
||
CHECK(ok, "TC-FREQ-004 J4: stays within +/-0.05 Hz after settle");
|
||
CHECK(fabsf(f - 51.0f) <= 0.01f, "TC-FREQ-004 J1(51Hz): steady |err| <= 0.01 Hz");
|
||
}
|
||
|
||
/* ===================== TC-FREQ-005:零幅浮空不发散(J5) ===================== */
|
||
static void tc_zero_amp(void) {
|
||
float f = 123.0f;
|
||
uint32_t i;
|
||
int never_valid = 1, no_nan = 1;
|
||
|
||
chain_reset();
|
||
for (i = 0u; i < 500u; i++) {
|
||
drive_beat(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1u); /* 全 0 帧(浮空 rms=0) */
|
||
if (freq_valid() != 0u) { never_valid = 0; }
|
||
(void)freq_get(&f);
|
||
if (!(f == f)) { no_nan = 0; } /* NaN 自比较为假 */
|
||
}
|
||
CHECK(never_valid, "TC-FREQ-005 J5: zero-amplitude never asserts valid");
|
||
CHECK(no_nan && (f == 0.0f), "TC-FREQ-005 J5: reading stays 0, no divergence/NaN");
|
||
/* 恢复:重新注入波形后应再次收敛 */
|
||
drive_n(150u, 49.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
|
||
(void)freq_get(&f);
|
||
printf(" [DATA] recover after float: f=%.4f err=%.4f\n", (double)f, (double)f - 49.0);
|
||
CHECK((freq_valid() != 0u) && (fabsf(f - 49.0f) <= 0.01f),
|
||
"TC-FREQ-005 recovers and meets J1 after waveform returns");
|
||
}
|
||
|
||
/* ===================== TC-FREQ-006:直流偏置抑制 ===================== */
|
||
static void tc_dc_offset(void) {
|
||
float f = 0.0f;
|
||
|
||
chain_reset();
|
||
drive_n(150u, 50.3, 1000.0, 8000.0, 0.0, 0.0, 0.0, 1u); /* DC = 8000 码 */
|
||
(void)freq_get(&f);
|
||
printf(" [DATA] DC=8000: f=%.4f err=%.4f\n", (double)f, (double)f - 50.3);
|
||
CHECK(fabsf(f - 50.3f) <= 0.01f, "TC-FREQ-006 DC offset rejected: |err| <= 0.01 Hz");
|
||
}
|
||
|
||
/* ===================== TC-FREQ-007:3/5 次谐波污染 ===================== */
|
||
static void tc_harmonics(void) {
|
||
float f = 0.0f;
|
||
|
||
chain_reset();
|
||
drive_n(150u, 48.0, 1000.0, 0.0, 200.0, 100.0, 0.0, 1u); /* 20%/10% 谐波 */
|
||
(void)freq_get(&f);
|
||
printf(" [DATA] h3=20%% h5=10%%: f=%.4f err=%.4f\n", (double)f, (double)f - 48.0);
|
||
CHECK(fabsf(f - 48.0f) <= 0.01f, "TC-FREQ-007 harmonics rejected: |err| <= 0.01 Hz");
|
||
}
|
||
|
||
/* ===================== TC-FREQ-008:相位跳变假值钳制(J6) ===================== */
|
||
static void tc_phase_jump(void) {
|
||
float f = 0.0f;
|
||
uint32_t i;
|
||
int ok = 1;
|
||
|
||
chain_reset();
|
||
drive_n(150u, 50.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
|
||
/* 跳变:瞬时切到相位偏移 60 度的等价频率段——用 1 拍 55 Hz 高幅制造
|
||
* 窗内畸变(f_inst 出带),随后回 50 Hz,判 60 ms 内回带 */
|
||
drive_beat(55.0, 4000.0, 0.0, 0.0, 0.0, 0.0, 1u); /* 幅值/频率双跳变 */
|
||
for (i = 0u; i < 60u; i++) {
|
||
drive_beat(50.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
|
||
(void)freq_get(&f);
|
||
if (i >= 59u) {
|
||
if (fabsf(f - 50.0f) > 0.05f) { ok = 0; }
|
||
}
|
||
}
|
||
printf(" [DATA] after jump: f=%.4f err=%.4f (recover window 60 ms)\n",
|
||
(double)f, (double)f - 50.0);
|
||
CHECK(ok, "TC-FREQ-008 J6: recovers to +/-0.05 Hz within 60 ms after jump");
|
||
CHECK(freq_valid() != 0u, "TC-FREQ-008 J6: still valid after clamp path");
|
||
}
|
||
|
||
/* ===================== TC-FREQ-009:UA 无幅值 -> IA 回退 ===================== */
|
||
static void tc_src_fallback(void) {
|
||
float f = 0.0f;
|
||
|
||
chain_reset();
|
||
drive_n(150u, 52.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 0u); /* UA 置 0,IA 带波形 */
|
||
(void)freq_get(&f);
|
||
printf(" [DATA] UA=0 IA=1000: f=%.4f err=%.4f\n", (double)f, (double)f - 52.0);
|
||
CHECK((freq_valid() != 0u) && (fabsf(f - 52.0f) <= 0.01f),
|
||
"TC-FREQ-009 falls back to IA source when UA below amplitude gate");
|
||
}
|
||
|
||
/* ===================== TC-FREQ-010:init 幂等 / 参数守卫 ===================== */
|
||
static void tc_init_guards(void) {
|
||
float f = 5.0f;
|
||
|
||
chain_reset();
|
||
CHECK(freq_get(&f) == -2, "TC-FREQ-010 get before convergence -> -2");
|
||
CHECK((f == 0.0f), "TC-FREQ-010 get -2 writes 0 to out");
|
||
CHECK(freq_get(NULL) == -1, "TC-FREQ-010 NULL out -> -1");
|
||
CHECK(freq_valid() == 0u, "TC-FREQ-010 valid=0 before convergence");
|
||
CHECK(freq_last() == 0.0f, "TC-FREQ-010 last=0 before convergence");
|
||
(void)freq_init();
|
||
CHECK(freq_get(&f) == -2, "TC-FREQ-010 freq_init idempotent (still -2)");
|
||
}
|
||
|
||
/* ===================== TC-FREQ-011:幅值门中途失效 -> 保持 ===================== */
|
||
static void tc_gate_hold(void) {
|
||
float f_before = 0.0f, f_after = 0.0f;
|
||
|
||
chain_reset();
|
||
drive_n(150u, 50.0, 1000.0, 0.0, 0.0, 0.0, 0.0, 1u);
|
||
(void)freq_get(&f_before);
|
||
CHECK(freq_valid() == 1u, "TC-FREQ-011 valid before gate loss");
|
||
|
||
drive_n(50u, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1u); /* 波形消失 */
|
||
CHECK(freq_valid() == 0u, "TC-FREQ-011 J5: valid cleared on gate loss");
|
||
(void)freq_get(&f_after);
|
||
printf(" [DATA] hold: before=%.4f held=%.4f delta=%.4f\n",
|
||
(double)f_before, (double)f_after, (double)(f_after - f_before));
|
||
CHECK(fabsf(f_after - f_before) <= 0.3f,
|
||
"TC-FREQ-011 J5: reading bounded (|delta| <= 0.3 Hz) during gate loss");
|
||
}
|
||
|
||
int main(void) {
|
||
printf("=== Freq Tests (B8: phase-unwrap frequency measurement) ===\n");
|
||
tc_converge_50();
|
||
tc_sweep();
|
||
tc_noise();
|
||
tc_step();
|
||
tc_zero_amp();
|
||
tc_dc_offset();
|
||
tc_harmonics();
|
||
tc_phase_jump();
|
||
tc_src_fallback();
|
||
tc_init_guards();
|
||
tc_gate_hold();
|
||
|
||
printf("\n=== Freq Tests: %d/%d PASSED (failed=%d) ===\n",
|
||
s_passed, s_total, s_failed);
|
||
return (s_failed == 0) ? 0 : 1;
|
||
}
|