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 干净。
309 lines
12 KiB
C
309 lines
12 KiB
C
/*
|
||
* 模块名称:遥测快照发布层(全周傅氏相量派生·发布侧)主机侧单元测试
|
||
* 被测对象:真实源文件(#include 原样纳入,测生产代码而非副本):
|
||
* App/algo/meas.c + App/algo/phasor.c + App/algo/sample_ring.c
|
||
* 三源均为纯算法(无寄存器/RTOS/文件依赖)→ **无需任何桩**。
|
||
*
|
||
* 为什么要主机侧(而不是只等台架):遥测快照发布的判据一半是"并发与结构"类——
|
||
* 双区切换无撕裂、seq 只增、布局与 §4.5 一致、品质汇总正确——台架上要么不可
|
||
* 观测(撕裂窗 200 ms 量级,人工比对读不到)、要么无法注入(削顶位图由两点标定
|
||
* 完成后才真实出现);主机侧可合成任意幅值/相位的 9 通道帧序列把它们钉死。
|
||
* 读数与注入源比对类判据 = 待台架实测(本套件不覆盖,见「数据可信」遥测快照发布)。
|
||
*
|
||
* 覆盖点(TC-MEAS-xxx;属主登记处 = 本目录,族 TC-HOST-MEAS):
|
||
* TC-MEAS-001 init 语义:seq=0 / get=-2 且清零填充 / 发布后可见 / init 幂等复位
|
||
* TC-MEAS-002 布局钉死(docs/任务规划/数据可信.md §4.5):sizeof=72 + 八字段偏移
|
||
* TC-MEAS-003 窗不足发布:seq=1 照常发布,电气 8 路 + TEMP 全 IV|NT,valid=0
|
||
* TC-MEAS-004 基波有效值透传:50 Hz 整周波窗 yc = A/√2(≤0.1 %),零幅路=0 且品质 0
|
||
* TC-MEAS-005 同拍同值(§4.5"从相量使用区取"口径钉死):yc[UA/UN/IA] 与
|
||
* phasor_get(基波实例).fAmSQ 逐位相等
|
||
* TC-MEAS-006 品质汇总(遥测快照发布 DoD 原句"任一点 CLIPPED → 该点 qds 置位"):
|
||
* mask=0xE6 → UB/UC/IB/IC/IN 置 CLIPPED 且值不清零;UA 不置;
|
||
* mask 位 8 对 TEMP 无效(TEMP 恒 NT)
|
||
* TC-MEAS-007 seq 递增 + 最新帧:50 连发,get 恒为最新(seq/tick_ms/幅值三元组
|
||
* 互洽 → 双区切换无撕裂、无陈旧读)
|
||
* TC-MEAS-008 频率注入透传:fv/f 原样进快照;fv=0 时 f 仍保持(读数保持语义在
|
||
* freq 层,快照只搬运)
|
||
* TC-MEAS-009 TEMP 透传最新一点:yc[TEMP] = 最近一拍原始帧值(非均值非滞后)
|
||
* TC-MEAS-010 采样环回绕连续:60+60 拍穿环深 40 后读数恒定(≤0.1 %)
|
||
* TC-MEAS-011 get 参数守卫:NULL → -1
|
||
*
|
||
* 通过:全部 TC PASS(main 返回 0)。
|
||
* 创建日期:2026-10-25(道D 遥测快照发布)
|
||
*/
|
||
|
||
#include <math.h>
|
||
#include <stddef.h>
|
||
#include <stdint.h>
|
||
#include <stdio.h>
|
||
|
||
#define PI 3.14159265358979323846
|
||
#define W50 (2.0 * PI * 50.0 / 1000.0) /* 50 Hz @ 1 kHz:0.1π/拍(20 点/周波) */
|
||
#define SQRT2 1.41421356237309505
|
||
|
||
/* ===================== 断言 ===================== */
|
||
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 "meas.c"
|
||
|
||
/* ===================== 工具 ===================== */
|
||
static double s_ph = 0.0; /* 全局正弦相位(拍推进) */
|
||
|
||
/* 各电气路取 sin(ph + s*0.7) 错相,避免 9 路同值互相掩盖;TEMP = 拍序号 */
|
||
static void push_beats(int n, double amp) {
|
||
float f[ADC_SEM_COUNT];
|
||
int i;
|
||
int s;
|
||
|
||
for (i = 0; i < n; i++) {
|
||
for (s = 0; s < (int)ADC_SEM_TEMP; s++) {
|
||
f[s] = (float)(amp * sin(s_ph + (double)s * 0.7));
|
||
}
|
||
f[ADC_SEM_TEMP] = (float)(1000.0 + s_ph * 100.0);
|
||
s_ph += W50;
|
||
(void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT);
|
||
}
|
||
}
|
||
|
||
/* 每开机一段用例的干净起点:环 + 相量(内含环复位)+ 快照全复位 */
|
||
static void t_reset(void) {
|
||
(void)phasor_init();
|
||
(void)meas_init();
|
||
s_ph = 0.0;
|
||
}
|
||
|
||
static int rms_close(float got, double want) {
|
||
double tol = (want > 1.0 ? want : 1.0) * 0.001; /* 0.1 % 容差 */
|
||
return (fabs((double)got - want) <= tol) ? 1 : 0;
|
||
}
|
||
|
||
static int all_iv_nt(const meas_snap_t *s, int upto) {
|
||
int i;
|
||
for (i = 0; i < upto; i++) {
|
||
if (s->qds[i] != (uint16_t)(MEAS_QDS_IV | MEAS_QDS_NT)) {
|
||
return 0;
|
||
}
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* ===================== TC ===================== */
|
||
static void tc_001_init(void) {
|
||
meas_snap_t s;
|
||
|
||
t_reset();
|
||
CHECK(meas_seq() == 0u, "001a init: seq=0");
|
||
CHECK(meas_get(&s) == -2, "001b init: get=-2");
|
||
CHECK(s.seq == 0u && s.tick_ms == 0u && s.valid == 0u &&
|
||
s.freq_valid == 0u && s.freq_hz == 0.0f, "001c init: -2 时 out 清零填充");
|
||
{
|
||
int i; int z = 1;
|
||
for (i = 0; i < (int)ADC_SEM_COUNT; i++) {
|
||
if ((s.yc[i] != 0.0f) || (s.qds[i] != 0u)) { z = 0; }
|
||
}
|
||
CHECK(z == 1, "001d init: yc/qds 全清零");
|
||
}
|
||
push_beats(20, 100.0);
|
||
meas_tick(200u, 0u, 0.0f, 0u);
|
||
CHECK(meas_seq() == 1u && meas_get(&s) == 0, "001e 发布后可读");
|
||
(void)meas_init();
|
||
CHECK(meas_seq() == 0u && meas_get(&s) == -2 && s.seq == 0u,
|
||
"001f init 幂等:复位序号/就绪/双区");
|
||
}
|
||
|
||
static void tc_002_layout(void) {
|
||
CHECK(sizeof(meas_snap_t) == 72u, "002a sizeof=72B(双区 2x72=144B 卡面预算)");
|
||
CHECK(offsetof(meas_snap_t, seq) == 0u, "002b off seq=0");
|
||
CHECK(offsetof(meas_snap_t, tick_ms) == 4u, "002c off tick_ms=4");
|
||
CHECK(offsetof(meas_snap_t, valid) == 8u, "002d off valid=8");
|
||
CHECK(offsetof(meas_snap_t, rsv) == 9u, "002e off rsv=9");
|
||
CHECK(offsetof(meas_snap_t, yc) == 12u, "002f off yc=12");
|
||
CHECK(offsetof(meas_snap_t, qds) == 48u, "002g off qds=48");
|
||
CHECK(offsetof(meas_snap_t, freq_valid) == 66u, "002h off freq_valid=66");
|
||
CHECK(offsetof(meas_snap_t, freq_hz) == 68u, "002i off freq_hz=68");
|
||
}
|
||
|
||
static void tc_003_warmup(void) {
|
||
meas_snap_t s;
|
||
|
||
t_reset();
|
||
meas_tick(200u, 1u, 49.9f, 0xE6u);
|
||
CHECK(meas_get(&s) == 0, "003a 窗不足照常发布(品质节拍性刷新)");
|
||
CHECK(s.seq == 1u && s.tick_ms == 200u, "003b seq/tick_ms 落戳");
|
||
CHECK(s.valid == 0u, "003c 整帧 valid=0");
|
||
CHECK(all_iv_nt(&s, (int)ADC_SEM_COUNT), "003d 电气 8 + TEMP 全 IV|NT");
|
||
{
|
||
int i; int z = 1;
|
||
for (i = 0; i < (int)ADC_SEM_COUNT; i++) {
|
||
if (s.yc[i] != 0.0f) { z = 0; }
|
||
}
|
||
CHECK(z == 1, "003e IV 路值位保持 0(不产伪值)");
|
||
}
|
||
CHECK(s.freq_valid == 1u && s.freq_hz == 49.9f,
|
||
"003f 频率注入与窗不足解耦(新鲜度独立)");
|
||
}
|
||
|
||
static void tc_004_rms(void) {
|
||
meas_snap_t s;
|
||
int i;
|
||
|
||
t_reset();
|
||
push_beats(19, 100.0);
|
||
meas_tick(190u, 0u, 0.0f, 0u);
|
||
CHECK(meas_get(&s) == 0 && s.valid == 0u, "004a 19 拍窗不足:不产出值");
|
||
push_beats(1, 100.0);
|
||
meas_tick(200u, 0u, 0.0f, 0u);
|
||
CHECK(meas_get(&s) == 0 && s.valid == 1u, "004b 满 20 拍:整帧 valid=1");
|
||
for (i = 0; i < (int)ADC_SEM_TEMP; i++) {
|
||
CHECK(rms_close(s.yc[i], 100.0 / SQRT2), "004c 各路 yc = A/sqrt2(有效值口径)");
|
||
CHECK(s.qds[i] == 0u, "004d 正常路品质 0");
|
||
}
|
||
CHECK(s.qds[ADC_SEM_TEMP] == (uint16_t)MEAS_QDS_NT, "004e TEMP = NT(非交流量语义)");
|
||
}
|
||
|
||
static void tc_005_use_zone_equiv(void) {
|
||
meas_snap_t s;
|
||
phasor_t p;
|
||
uint8_t k;
|
||
|
||
t_reset();
|
||
push_beats(20, 80.0);
|
||
phasor_run();
|
||
meas_tick(400u, 0u, 0.0f, 0u);
|
||
CHECK(meas_get(&s) == 0, "005a 快照可读");
|
||
for (k = 0u; k < phasor_inst_count(); k++) {
|
||
const phasor_inst_t *d = phasor_inst(k);
|
||
if ((d != NULL) && (d->harm == 1u) && (phasor_get(k, &p) == 0)) {
|
||
CHECK(s.yc[d->sem] == p.fAmSQ,
|
||
"005b yc[基波路] 与使用区 fAmSQ 逐位相等(同拍同核)");
|
||
}
|
||
}
|
||
}
|
||
|
||
static void tc_006_clip_quality(void) {
|
||
meas_snap_t s;
|
||
uint8_t clip[5] = { ADC_SEM_UB, ADC_SEM_UC, ADC_SEM_IB, ADC_SEM_IC, ADC_SEM_IN };
|
||
int i;
|
||
|
||
t_reset();
|
||
push_beats(20, 60.0);
|
||
meas_tick(200u, 0u, 0.0f, 0xE6u); /* 0xE6 = bit1/2/5/6/7(同 cal: 行 cl 位图) */
|
||
CHECK(meas_get(&s) == 0, "006a 可读");
|
||
for (i = 0; i < 5; i++) {
|
||
CHECK((s.qds[clip[i]] & (uint16_t)MEAS_QDS_CLIPPED) != 0u,
|
||
"006b 削顶路 qds 置 CLIPPED");
|
||
CHECK(s.yc[clip[i]] > 30.0f, "006c 削顶路值不清零(监视语义保留)");
|
||
CHECK(s.qds[clip[i]] == (uint16_t)MEAS_QDS_CLIPPED,
|
||
"006d 削顶路仅 CLIPPED(未叠 IV/NT)");
|
||
}
|
||
CHECK(s.qds[ADC_SEM_UA] == 0u, "006e UA 不受位图外影响");
|
||
CHECK(s.qds[ADC_SEM_UN] == 0u && s.qds[ADC_SEM_IA] == 0u, "006f UN/IA 干净");
|
||
CHECK(s.valid == 1u, "006g CLIPPED 不拉低整帧 valid(品质在点级)");
|
||
meas_tick(400u, 0u, 0.0f, 0x1E6u); /* bit8(TEMP)应被忽略 */
|
||
CHECK(meas_get(&s) == 0 && s.qds[ADC_SEM_TEMP] == (uint16_t)MEAS_QDS_NT,
|
||
"006h TEMP 品质语义独立:mask 位 8 无效");
|
||
}
|
||
|
||
static void tc_007_seq_latest(void) {
|
||
meas_snap_t s;
|
||
int n;
|
||
|
||
t_reset();
|
||
push_beats(20, 50.0); /* 先攒窗,之后每轮再推 20 拍换幅值 */
|
||
for (n = 1; n <= 50; n++) {
|
||
push_beats(20, (double)n * 10.0);
|
||
meas_tick((uint32_t)n * 1000u, 0u, 0.0f, 0u);
|
||
CHECK(meas_get(&s) == 0 && s.seq == (uint32_t)n,
|
||
"007a seq 逐拍 +1 无丢帧");
|
||
CHECK(s.tick_ms == (uint32_t)n * 1000u,
|
||
"007b tick_ms 与 seq 同戳(读到的帧自洽)");
|
||
CHECK(rms_close(s.yc[ADC_SEM_UA], (double)n * 10.0 / SQRT2),
|
||
"007c 幅值与 seq 匹配(无陈旧区误读)");
|
||
}
|
||
CHECK(meas_seq() == 50u, "007d meas_seq 与 get 一致收敛");
|
||
}
|
||
|
||
static void tc_008_freq(void) {
|
||
meas_snap_t s;
|
||
|
||
t_reset();
|
||
push_beats(20, 100.0);
|
||
meas_tick(300u, 1u, 50.125f, 0u);
|
||
CHECK(meas_get(&s) == 0 && s.freq_valid == 1u && s.freq_hz == 50.125f,
|
||
"008a 有效频率透传");
|
||
meas_tick(500u, 0u, 49.875f, 0u);
|
||
CHECK(meas_get(&s) == 0 && s.freq_valid == 0u && s.freq_hz == 49.875f,
|
||
"008b fv=0 时读数仍搬运(保持语义在 freq 层)");
|
||
}
|
||
|
||
static void tc_009_temp_passthrough(void) {
|
||
meas_snap_t s;
|
||
|
||
t_reset();
|
||
push_beats(20, 100.0);
|
||
meas_tick(200u, 0u, 0.0f, 0u);
|
||
CHECK(meas_get(&s) == 0, "009a 可读");
|
||
{
|
||
float t1 = s.yc[ADC_SEM_TEMP];
|
||
CHECK(t1 > 0.0f && s.qds[ADC_SEM_TEMP] == (uint16_t)MEAS_QDS_NT,
|
||
"009b TEMP 值在场 + NT");
|
||
push_beats(21, 100.0);
|
||
meas_tick(400u, 0u, 0.0f, 0u);
|
||
CHECK(meas_get(&s) == 0 && s.yc[ADC_SEM_TEMP] != t1,
|
||
"009c TEMP = 最新一点(非均值、非首点)");
|
||
CHECK(rms_close(s.yc[ADC_SEM_UA], 100.0 / SQRT2),
|
||
"009d TEMP 换值不影响电气路");
|
||
}
|
||
}
|
||
|
||
static void tc_010_ring_wrap(void) {
|
||
meas_snap_t s1;
|
||
meas_snap_t s2;
|
||
|
||
t_reset();
|
||
push_beats(60, 100.0); /* 穿环深 40 一次 */
|
||
meas_tick(600u, 0u, 0.0f, 0u);
|
||
CHECK(meas_get(&s1) == 0 && s1.valid == 1u, "010a 首次回绕后发布正常");
|
||
push_beats(60, 100.0); /* 再穿一次 */
|
||
meas_tick(1200u, 0u, 0.0f, 0u);
|
||
CHECK(meas_get(&s2) == 0 && s2.valid == 1u, "010b 二次回绕后发布正常");
|
||
CHECK(rms_close(s1.yc[ADC_SEM_UA], 100.0 / SQRT2) &&
|
||
rms_close(s2.yc[ADC_SEM_UA], 100.0 / SQRT2),
|
||
"010c 回绕前后读数恒定(≤0.1 %)");
|
||
CHECK(s2.seq == s1.seq + 1u, "010d 回绕不影响 seq");
|
||
}
|
||
|
||
static void tc_011_get_guard(void) {
|
||
t_reset();
|
||
CHECK(meas_get(NULL) == -1, "011a NULL -> -1");
|
||
}
|
||
|
||
int main(void) {
|
||
printf("=== Meas Layer Tests ===\n");
|
||
|
||
tc_001_init();
|
||
tc_002_layout();
|
||
tc_003_warmup();
|
||
tc_004_rms();
|
||
tc_005_use_zone_equiv();
|
||
tc_006_clip_quality();
|
||
tc_007_seq_latest();
|
||
tc_008_freq();
|
||
tc_009_temp_passthrough();
|
||
tc_010_ring_wrap();
|
||
tc_011_get_guard();
|
||
|
||
printf("\n=== Meas Layer Tests: %d/%d PASSED (failed=%d) ===\n",
|
||
s_passed, s_total, s_failed);
|
||
return (s_failed == 0) ? 0 : 1;
|
||
}
|