feat(遥测快照): 卡8 meas 双区快照模块 + adc_task 200ms 唯一发布点(道D M5)

卡 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
This commit is contained in:
2026-10-25 10:02:10 +08:00
parent 13ef0715bb
commit 48bc50e794
10 changed files with 782 additions and 4 deletions
+308
View File
@@ -0,0 +1,308 @@
/*
* 模块名称:遥测快照发布层(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;
}
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""遥测快照层(L2 发布侧,meas)主机侧单元测试运行器(L0.5,纯标准库)。
做什么:
用本机 MSVC 编译 test/host/meas/meas_test.c(它以 #include 纳入
**生产源文件** App/algo/meas.c + phasor.c + sample_ring.c),在 PC 上离线
跑 200 ms 遥测快照的:§4.5 布局钉死 / seq 只增 / 双区切换无撕裂 /
基波有效值与 phasor 使用区同拍同值 / 品质汇总(CLIPPED 位图)/ TEMP 透传 /
采样环回绕连续性,失败即非 0 退出。零硬件、零烧录、毫秒级。
工具链探测 / 编译运行全部委托 test/host/common/unit_test_harness.py。
为什么要主机侧单测(而不是只等台架):
卡 8(docs/任务规划/数据可信.md)判据一半是并发与结构类——双区撕裂窗在
台架上不可观测,削顶品质要等卡 12 标定才真实出现;读数与注入源比对类
判据仍归台架(实测采样类,本套件不覆盖)。
本层为纯算法:无寄存器、无 RTOS、无文件/外设依赖,**不需要任何桩目录**。
用法(仓库根目录):
python test/host/meas/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误。
设计依据:docs/任务规划/数据可信.md 卡 8(道D,§4.5 快照布局);
创建日期:2026-10-25
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
# 无桩目录:meas.c / phasor.c / sample_ring.c 仅依赖 stdint/stddef/math 与
# App/algo 内的头(adc_input.h 为纯枚举 + 声明,可主机编译)。
BUILD = TestBuild(
name="Meas Layer Tests",
src=os.path.join(HERE, "meas_test.c"),
include_dirs=[
os.path.join(ROOT, "App", "algo"),
HERE,
],
summary_marker="=== Meas Layer Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))