48bc50e794
卡 8(App/algo/meas.*,新增;布局权威 docs/任务规划/数据可信.md §4.5): - meas_snap_t = §4.5 布局(sizeof 72 B;字段偏移由 TC-MEAS-002 钉死——卡 9/ UI 按整体拷贝消费,破布局=破协议;谐波 k-bin 扩展(docs/算法/继电保护功能 复现规划.md L2-8)只许尾部追加,留口注释已写进 meas.h) - 并发 = phasor 双区 + 单字节索引翻转(L2-7 同型,无锁);seq 只增供判"换帧"; meas_get 失败路径清零填充(-2 未就绪时调用方可安全读) - 值口径 = A-lite 裁决(lead 2026-10-25 快裁,推翻票面全链):基波有效值瞬时 透传,复用 phasor_calc 纯核 → 与 phasor 使用区同拍同值(TC-MEAS-005 逐位 相等钉死);不做 4 点平均/方和根(§4.5"200 ms 是上送节拍不是平均窗") - 品质注入:CLIPPED 位图唯一源 = adc_cal_flags()(「数据可信」§4.2 三态表, 编排方注入、本层不链 adc_cal);取窗不足路 = IV|NT 且整帧 valid=0 但照常 发布(品质节拍性刷新);TEMP = 最新一点透传 + NT(「数据可信」决策 5); MEAS_QDS_* 低字节 = 104 QDS 线上值(本层不 include 协议层头——分层方向, 卡 9 直接截低字节映射);OV 留位不填(判据待运行参数化,卡 10 域) - adc_task.c 编排:新帧拍 freq_run() 后按 **tick 经过**分频(非拍计数,idle/ skip 拍会拉长名义节拍)调 meas_tick()——全仓调用点数 = 1(「数据可信」§8 风险表"快照双写"行评审判据);meas_init 挂 adc_task_init(不动 app_main.c); 心跳新增 meas: 行(seq/v/ua·un·ia_mv/f/fv)作 M5 台架观测面 - 死区 2 点确认状态机本卡不建(卡 9 迁移 104 语义时随建,lead 裁③);104 层 现有 deadband 常量未动;频率点绑定归卡 11 - 实测采样类判据(读数 vs 注入源)= 待台架实测,本卡零台架动作 测试与注册:新套件 test/host/meas(226/226,链真源 meas+phasor+sample_ring 免桩;覆盖 seq/双区无撕裂/布局/品质汇总/回绕/守卫);id_registry 建族 TC-HOST-MEAS(游标 011);meas-unit-test 钩子 + SIM_EXEMPT 一条 + uvprojx 仅增 meas.c 单条挂载(diff 已核零 UV4 抖动);App/algo/README.md §1/§2/ §3.9/§4 同步(设计说明大节与 AGENTS 口径留 lead 收口,同批 1 先例) gate:@build 0 错 0 警(Code=216352 RO=21476 RW=1232 ZI=132128);RAM 增量 RW+ZI = +152 B(快照双区恰 +144 B = 卡面 DoD;余 s_meas_ms 等编排状态), IRAM1 57.8%→约 57.9%;Flash(Code+RO+RW)+696 B;adcTask 静态最坏链 1408 B / margin 1152 B(45%)PASS(stack_report 实测,无抬栈);BOM/CR/U+FFFD/ 字面量 ASCII 逐文件自查通过(未触碰带 BOM 文件)。pre-commit run --all-files 实跑:其余钩子全绿(含 doc-links/id-check/ 覆盖率总闸/双端注册/ui-golden/全部主机单测套),仅 mem-budget + mem-numbers 两条红 = 台账受管区 vs .map 漂移(Flash/RAM 台账回填属 lead 单写者区,批 1 同口径): gate: only mem-budget/mem-numbers pending lead ledger backfill
309 lines
12 KiB
C
309 lines
12 KiB
C
/*
|
||
* 模块名称:遥测快照发布层(L2 派生·发布侧)主机侧单元测试
|
||
* 被测对象:真实源文件(#include 原样纳入,测生产代码而非副本):
|
||
* App/algo/meas.c + App/algo/phasor.c + App/algo/sample_ring.c
|
||
* 三源均为纯算法(无寄存器/RTOS/文件依赖)→ **无需任何桩**。
|
||
*
|
||
* 为什么要主机侧(而不是只等台架):卡 8 的判据一半是"并发与结构"类——
|
||
* 双区切换无撕裂、seq 只增、布局与 §4.5 一致、品质汇总正确——台架上要么不可
|
||
* 观测(撕裂窗 200 ms 量级,人工比对读不到)、要么无法注入(削顶位图由卡 12
|
||
* 标定后才真实出现);主机侧可合成任意幅值/相位的 9 通道帧序列把它们钉死。
|
||
* 读数与注入源比对类判据 = 待台架实测(本套件不覆盖,见「数据可信」卡 8)。
|
||
*
|
||
* 覆盖点(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 品质汇总(卡 8 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 卡 8)
|
||
*/
|
||
|
||
#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;
|
||
}
|