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
+15
View File
@@ -366,6 +366,21 @@ repos:
files: ^(App/algo/(adc_cal|prot_elm|prot_set|phasor|sample_ring|freq)\.(c|h)|App/task/(adc_task|protect_task)\.c|test/host/prot_dim/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 7h4) 遥测快照层(L2 发布侧,「数据可信」卡 8,道D)主机侧单元测试(2026-10-25 加)
# 背景:卡 8 的 DoD 一半是并发与结构类判据——双区切换无撕裂(撕裂窗
# 200 ms 量级,台架人工比对观测不到)、seq 只增、§4.5 布局与字段偏移
# (卡 9/UI 直接按整体拷贝消费,破布局 = 破协议)、CLIPPED 品质汇总
# (削顶位图要等卡 12 标定才真实出现)。主机侧以 #include 原样纳入
# meas/phasor/sample_ring 生产源,合成 9 通道帧序列一次性钉死;
# 读数与注入源比对类判据仍归台架(实测采样类)。files 覆盖被测源、
# 同链核(phasor/sample_ring)、编排方 adc_task 与 harness 本身。
- id: meas-unit-test
name: 遥测快照层单测(主机侧)
entry: python test/host/meas/run_unit_test.py
language: system
files: ^(App/algo/meas\.(c|h)|App/algo/(phasor|sample_ring)\.(c|h)|App/task/adc_task\.c|test/host/meas/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 7i) 保护定值模型(P1 / L5-1 载体)主机侧单元测试(2026-10-11 加)
# 定值是保护动作的"输入宪法":范围校验、整表装载 all-or-nothing、
+24 -4
View File
@@ -12,6 +12,7 @@
| `adc_input.h` / `.c` | L1 | 统一语义取数:两片转换器的码值按语义(`UA…IN` / 温度)暴露,并消费 STM32 ADC1 的 DMA 乒乓缓冲 | `adc_input_init()` `adc_input_poll()` `adc_input_raw()` | L1-2 / L1-3 / L1-4 |
| `adc_health.h` / `.c` | L1 | 采样有效性判定:吃计数快照,产出静默 / 丢帧 / 单调性 / 错误增量故障位(判定与告警编排分离);L5-2 残余的码值贴轨观察器同层 | `adc_health_check()` `adc_health_flags()` `adc_health_rail_watch()` | L1-6 / L5-2(残余) |
| `adc_cal.h` / `.c` | L1.5 | 通道标定折算(「数据可信」卡 2/卡 4):工程量 =(code+b)×k,Q16 系数/零点两段落盘 + 三态 flags + 合法带哨兵 + 两点公式 | `adc_cal_apply()` `adc_cal_flags()` `adc_cal_set()` | L1-5(M4 半边) |
| `meas.h` / `.c` | L2 发布 | 遥测快照(「数据可信」卡 8,M5 地基):200 ms 基波有效值 + 品质快照,双区无锁读,全仓唯一发布者 = adcTask | `meas_tick()` `meas_get()` `meas_seq()` | M5(卡 9/11 与 UI 是读者) |
| `sample_ring.h` / `.c` | L2 输入 | 按语义通道存最近 2 周波(40 点),供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | L2 输入(为 L2-6 供数) |
| `phasor.h` / `.c` | L2 | 全周傅氏:一周波 20 点 DFT → 基波 / 谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | L2-1 / L2-2 / L2-3 / L2-6 / L2-7 |
| `freq.h` / `.c` | L2 派生量 | 频率测量:相邻两拍基波相量共轭乘积取角 + 自适应窗滑动平均 + 假值钳制 + 幅值门(UA→IA 源回退) | `freq_run()` `freq_get()` `freq_valid()` | L3-5 测量侧(测频范围 → `docs/算法/继电保护功能复现规划.md` §4.2) |
@@ -19,7 +20,8 @@
| `prot_elm.h` / `.c` | L3 | 保护元件框架:通用"越限 + 返回系数滞回 + 定时限"核 + 数据驱动实例表(过流三段 / 过负荷 / 过压 / 欠压 / 零序过压 / 过频 / 低频) | `prot_elm_run()` `prot_elm_eval()` `prot_elm_active_mask()` | L3-1 / L3-3 / L3-4 / L3-5 / L3-6(软件侧) |
依赖(自下而上、无环):`adc_input.h`(纯枚举 + 声明)← `adc_cal`(另依赖 `cfg_file.h` 接口,
纯头文件级)← `sample_ring` ← `phasor` ← `freq`;
纯头文件级)← `sample_ring` ← `phasor` ← `freq`;`meas` ← `sample_ring` + `phasor`
(仅 `phasor_calc` 纯核;频率/品质由调用方注入,**不链** `freq`/`adc_cal`);
`prot_set` **独立**;`prot_elm` ← `prot_set` + `phasor` + `freq`。
`adc_health` 只吃调用方喂进来的计数 / 码值,不反向依赖 L1 实现(L5-2 贴轨门限由调用方
从 `adc_cal_rail_ths()` 取,本层仍零算法依赖)。
@@ -33,7 +35,7 @@
`adc_input_poll()` → `adc_input_raw()` ×9 → `adc_cal_apply()` ×9(L1.5 折算,现口径 =
ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(L5-2,码值域)→ `sample_ring_push_frame()`
→ `phasor_run()` → `freq_run()` → 消费者(`adcTask` 心跳与 L1 健康判定;`protectTask` 每 20 ms 取相量 / 频率跑
`prot_elm_run()`)。节拍 = TIM3 1 kHz 更新事件,两片转换器同源触发。
`prot_elm_run()`;**每 200 ms** `meas_tick()` 发布遥测快照供 iec104Task / uiTask 读,卡 8)。节拍 = TIM3 1 kHz 更新事件,两片转换器同源触发。
完整数据流图与初始化落点 → `docs/算法/采集与算法层设计说明.md` §2。
## 3. 逐文件要点(只留"改代码时必须知道"的;机制与理由见设计文档)
@@ -105,11 +107,28 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(L5-2,码值域)→ `s
- cfg 两段 `[adc_cal]` / `[adc_cal_ofs]` **不挂回调**(RAM 副本即生效值,引擎 load/poll
原地改写,逐点现读);注册在 `app_main_init()` 内核前完成,必须先于 `cfg_file_load`。
### 3.9 `meas.c` / `.h` — 遥测快照发布(L2 发布侧,「数据可信」卡 8)
- **唯一发布者 = adcTask**:`phasor_run()`/`freq_run()` 同拍、200 ms **tick 经过**分频
调 `meas_tick()`(非拍计数——idle/skip 拍拉长名义节拍)。docs/任务规划/数据可信.md §8
风险表"快照双写"行 = 评审判据:全仓 `meas_tick` 调用点数须为 1;iec104Task /
uiTask 只读。
- 值口径(批 2 卡 8 A-lite 裁决):**基波有效值瞬时透传**,与 phasor 使用区
**同拍同核逐位相等**(单测钉死);**不做** 4 点平均 / 谐波方和根——按
docs/任务规划/数据可信.md §4.5"上送节拍非平均窗,再平均引入滞后";谐波 k-bin 轨
(docs/算法/继电保护功能复现规划.md L2-8)扩展只许结构尾部追加,
docs/任务规划/数据可信.md §4.5 字段序 = 协议(布局由单测按偏移钉死,72 B × 双区)。
- 并发 = phasor 双区 + 单字节索引翻转(L2-7 同型,无锁);seq 只增供消费者判换帧。
- 品质注入:削顶位图唯一源 = `adc_cal_flags()`(口径 docs/任务规划/数据可信.md §4.2
三态表;由 adc_task 编排注入,本层不链 `adc_cal`);TEMP = 最新一拍原始值透传 +
`NT`(「数据可信」决策 5);`MEAS_QDS_*` 低字节与 104 QDS 线上字节同值
(本层**不** include 协议层头——分层方向)。
- 104 死区迁移 / 频率点绑定 = 卡 9/卡 11(道D 后续分支);读数与注入源比对 = **待台架实测**。
## 4. 构建与测试
### 4.1 挂载与模拟器边界
- 八个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `<IncludePath>` 内。
- 本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路):八个文件全部登记在
- 九个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `<IncludePath>` 内。
- 本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路):九个文件全部登记在
`scripts/check_registration.py` 的 `SIM_EXEMPT`(附理由),`registration-check` 钩子反向校验(N-23)。
### 4.2 主机侧单测(L0.5)——生产源文件以 `#include` 原样纳入
@@ -123,6 +142,7 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(L5-2,码值域)→ `s
| 元件框架 | `& '.venv\Scripts\python.exe' test/host/prot_elm/run_unit_test.py` | `prot-elm-unit-test` |
| 频率测量 | `& '.venv\Scripts\python.exe' test/host/freq/run_unit_test.py` | `freq-unit-test` |
| 通道标定(含真实 cfg_file 引擎集成) | `& '.venv\Scripts\python.exe' test/host/adc_cal/run_unit_test.py` | `adc-cal-unit-test` |
| 遥测快照(布局钉死/双区无撕裂/品质汇总) | `& '.venv\Scripts\python.exe' test/host/meas/run_unit_test.py` | `meas-unit-test` |
改本目录任一 `.c/.h`,提交时对应钩子自动跑。**用例清单与判据数字的属主**是各
`test/host/<模块>/` 与 `docs/测试说明/` 下的单测说明,本文不复制(三处抄数字必然打架)。
+170
View File
@@ -0,0 +1,170 @@
/*
* 模块名称:遥测快照发布层(L2 派生·发布侧,「数据可信」卡 8)
* 模块功能:实现见 meas.h——200 ms 基波有效值 + 品质快照,双区无锁读。
* 本文件纯算法:输入 = sample_ring 取窗 + 调用方注入(频率/削顶位图),
* 无 RTOS / 无寄存器 / 无日志(「数据可信」层内禁令:本目录不写日志)。
* 双区不变量(docs/任务规划/数据可信.md §4.5,L2-7 同型,先例 = App/algo/
* phasor.c 的双区注释):写侧只碰**非使用区**,全部字段填完后一次
* volatile 单字节 store 翻转使用区;读者先取索引再整体拷贝 → 拷贝期
* 间写者不可能改读者所在区(两帧间隔 200 ms 远大于 72 B 拷贝)。
* 单核顺序执行,无需内存屏障;volatile 阻编译器把字段写入挪到翻转后。
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
* 作者:DTU 项目组
* 创建日期:2026-10-25(道D 卡 8)
* 修改记录:
* v1.0 2026-10-25 DTU 项目组 创建:200 ms 遥测快照 + 双区无锁读
*/
#include "meas.h"
#include <stddef.h>
#include "phasor.h" /* phasor_calc():基波 DFT 纯函数核(与使用区同核) */
#include "sample_ring.h" /* sample_ring_window():以本拍为终止的一周波窗 */
/* ===== 双区(计算区 / 使用区),口径同 App/algo/phasor.c 的 L2-7 ===== */
static meas_snap_t s_snap[2];
static volatile uint8_t s_snap_use;
static uint8_t s_snap_ready; /* 使用区是否已产出过有效结果 */
static uint32_t s_snap_seq; /* 只增帧号(写者单写者,读者原子单字读) */
/*
* 函数功能:清零一份快照(含 rsv 与全部数组元素)
* 入口参数:p - 目标快照
* 返 回 值:无
* 限定条件:p 非空
* 函数说明:静态辅助;逐字段清零(不依赖 memset,行为与本目录 phasor_zero 同型)。
*/
static void meas_zero(meas_snap_t *p) {
uint8_t i;
p->seq = 0u;
p->tick_ms = 0u;
p->valid = 0u;
p->rsv[0] = 0u;
p->rsv[1] = 0u;
p->rsv[2] = 0u;
p->freq_valid = 0u;
p->freq_hz = 0.0f;
for (i = 0u; i < (uint8_t)ADC_SEM_COUNT; i++) {
p->yc[i] = 0.0f;
p->qds[i] = 0u;
}
}
/*
* 函数功能:遥测快照层初始化
* 入口参数:无
* 返 回 值:0 - 成功
* 限定条件:内核启动前或任务上下文皆可;幂等,可重复调用
* 函数说明:清双区 + 索引/就绪/序号复位。init 后到首次发布前,meas_get() 恒
* 返 -2(out 清零)、meas_seq() 恒返 0。
*/
int meas_init(void) {
meas_zero(&s_snap[0]);
meas_zero(&s_snap[1]);
s_snap_use = 0u;
s_snap_ready = 0u;
s_snap_seq = 0u;
return 0;
}
/*
* 函数功能:一拍遥测发布(被调即发布,节拍把关在调用方)
* 入口参数:tick_ms - 本拍时基(毫秒) uint32_t
* freq_valid_in - 频率读数新鲜度(freq_valid() 值) uint8_t
* freq_hz_in - 频率读数(freq_last() 值) float
* clip_mask - 削顶通道位图(bit s = 语义通道 s,来源唯一 =
* adc_cal_flags(),编排方注入) uint32_t
* 返 回 值:无
* 限定条件:仅 adcTask 上下文、全仓唯一调用点(meas.h 发布纪律);须在
* phasor_run() 之后调用(本拍窗含最新点,与使用区同窗同核)
* 函数说明:电气 8 路(语义序中 TEMP 之前的全部通道,映射表属主 =
* docs/硬件平台/ADC通道映射说明.md)逐路一周波基波 DFT;任一路取窗
* 不足(开机未满一周波)→ 该路 IV|NT + 整帧 valid=0,但照常发布
* (品质节拍性刷新,见 meas_tick 头注)。TEMP 只取最新一点透传 +
* NT(「数据可信」决策 5)。品质值不因 IV 清零——"值在场但不可信"
* 与"值缺失"分开,消费者按 qds 裁决。
*/
void meas_tick(uint32_t tick_ms, uint8_t freq_valid_in, float freq_hz_in,
uint32_t clip_mask) {
meas_snap_t *z = &s_snap[1u - s_snap_use]; /* 计算区 = 非使用区 */
float win[PHASOR_N];
phasor_t p;
uint8_t all_ok = 1u;
uint8_t s;
meas_zero(z);
z->seq = s_snap_seq + 1u;
z->tick_ms = tick_ms;
for (s = 0u; s < (uint8_t)ADC_SEM_TEMP; s++) {
if (sample_ring_window((adc_semantic_t)s, (uint8_t)PHASOR_N, win) == 0) {
phasor_calc(win, (uint8_t)PHASOR_N, 1u, &p);
z->yc[s] = p.fAmSQ; /* 有效值口径(系数已折算,同 phasor 层) */
if ((clip_mask & (1uL << s)) != 0u) {
z->qds[s] |= (uint16_t)MEAS_QDS_CLIPPED;
}
} else {
/* 本路窗不足:值缺位(yc 保持 0)+ IV|NT;整帧判不可信 */
z->qds[s] = (uint16_t)(MEAS_QDS_IV | MEAS_QDS_NT);
all_ok = 0u;
}
}
/* TEMP:非交流量语义(「数据可信」决策 5),最新一点原始帧值透传 + NT;
* clip_mask 位 8 不参与(adc_cal_flags(TEMP) 恒 0,无削顶语义) */
{
float tv = 0.0f;
if (sample_ring_window(ADC_SEM_TEMP, 1u, &tv) == 0) {
z->yc[ADC_SEM_TEMP] = tv;
z->qds[ADC_SEM_TEMP] = (uint16_t)MEAS_QDS_NT;
} else {
z->qds[ADC_SEM_TEMP] = (uint16_t)(MEAS_QDS_IV | MEAS_QDS_NT);
}
}
z->valid = all_ok;
z->freq_valid = (freq_valid_in != 0u) ? 1u : 0u;
z->freq_hz = freq_hz_in;
/* 全部字段就绪后才翻索引 + 置就绪(顺序即协议,勿交换) */
s_snap_seq = z->seq;
s_snap_use = (uint8_t)(1u - s_snap_use);
s_snap_ready = 1u;
}
/*
* 函数功能:读当前使用区快照(无锁整体拷贝)
* 入口参数:out - 输出快照;失败路径同样清零填充
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 从未发布过 int
* 限定条件:任意上下文;无(读 volatile 索引 + 普通拷贝)
* 函数说明:先取索引再索引拷贝,避免拷贝中途索引被翻转导致"半旧半新"。
* 返回 0 时帧内部自洽(同一发布拍的 seq/tick_ms/yc/qds 成套)。
*/
int meas_get(meas_snap_t *out) {
uint8_t z;
if (out == NULL) {
return -1;
}
if (s_snap_ready == 0u) {
meas_zero(out);
return -2;
}
z = s_snap_use;
*out = s_snap[z];
return 0;
}
/*
* 函数功能:读已发布帧数(只增)
* 入口参数:无
* 返 回 值:seq,0 = 从未发布(单字读,原子) uint32_t
* 限定条件:任意上下文
* 函数说明:轻量"是否换帧"探针,正式取数以 meas_get() 为准。
*/
uint32_t meas_seq(void) {
return s_snap_seq;
}
+136
View File
@@ -0,0 +1,136 @@
/*
* 模块名称:遥测快照发布层(L2 派生·发布侧,「数据可信」卡 8)
* 模块功能:每 **200 ms** 从 L2 层取一次基波有效值 + 品质,产出一份全仓唯一的
* 遥测快照(meas_snap_t,布局权威 = docs/任务规划/数据可信.md §4.5),
* 让 104 与 UI **共用同一份上游**——"什么时候算新值、品质怎么判"只此
* 一处决定(M5 遥测链的地基;104 改读快照属卡 9,UI 侧 ui_data_feed 属
* 「UI 扩展」波次,两者都只是本快照的读者)。
* 值口径(批 2 卡 8 的 A-lite 裁决,2026-10-25):**基波有效值瞬时透传**
* (同拍 phasor 使用区口径,系数已在 L1.5 折算进帧值——现单位 = ADC
* 引脚电压 V,二次值语义等 adc_cal_flags() 出 VALID,见
* docs/算法/采集与算法层设计说明.md §10 红线③);**不做** 4 点平均 /
* 谐波方和根——docs/任务规划/数据可信.md §4.5 明示"200 ms 是上送节拍
* 不是平均窗,再平均引入滞后";谐波 k-bin 轨(docs/算法/继电保护功能
* 复现规划.md L2-8)将来扩展时只许在本结构**尾部追加**或留在发布侧
* 内部状态,既有字段序不得变更。
* TEMP 通道(「数据可信」决策 5:不参与线性标定):yc = 最新一拍原始
* 帧值透传,qds 恒置 NT——值在场但非交流量语义,UI 按「数据可信」§4.2
* 三态表显示占位符,104 无对应点。
* 并发(docs/任务规划/数据可信.md §4.5 原文口径):沿用 phasor 的
* **双区 + 单字节活跃索引翻转**(L2-7 同型,不是新机制)——写侧填满
* 计算区后一次 store 翻转,读者先取索引再整体拷贝,全程无锁;
* seq 只增,消费者据此判"是否换帧"。
* 本层**纯算法**:不碰寄存器、不碰 RTOS、不自己取数(窗自 sample_ring、
* 频率/品质由调用方注入)→ 主机侧免桩单测 test/host/meas/(L0.5)。
* 发布纪律:唯一发布者 = adcTask(在 phasor_run()/freq_run() 之后 200 ms 分频
* 调 meas_tick())。评审判据 = 全仓 meas_tick 调用点数 == 1(「数据可信」
* §8 风险表"快照双写"行);其余任务/模块**只读**。
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
* 作者:DTU 项目组
* 创建日期:2026-10-25(道D 卡 8)
* 修改记录:
* v1.0 2026-10-25 DTU 项目组 创建:200 ms 遥测快照 + 双区无锁读
*/
#ifndef MEAS_H
#define MEAS_H
#include <stdint.h>
#include "adc_input.h" /* adc_semantic_t / ADC_SEM_COUNT:yc/qds 下标 = 语义通道 */
#ifdef __cplusplus
extern "C" {
#endif
/* 上送节拍(ms):adc_task 按本宏对 1 kHz 拍做**经过时间**分频(非拍计数——
* idle/skip 拍会把拍计数口径拉得比名义值长),落点见 App/task/adc_task.c */
#define MEAS_PERIOD_MS 200u
/* 每点品质位(uint16)。**低字节与线上 104 QDS 字节同值**(OV/NT/IV,数值
* 口径同 App/protocol/iec104/iec104_asdu.h 的 IEC104_QDS_*)——但本层**不得**
* include 协议层头(分层方向:算法层在协议层之下,反向依赖禁止),故在此自
* 定义同值常量;卡 9 的 104 侧消费直接截低字节上送,一致性由本目录单测与
* test/host/iec104/ 既有断言双向钉住。高字节为 meas 域扩展位(线上不直接
* 出现,由消费者映射): */
#define MEAS_QDS_OV (0x0001u) /* 溢出(留位,卡 8 不产出——判据待运行参数化) */
#define MEAS_QDS_NT (0x0020u) /* 非当前值:未就绪 / 语义未处理(TEMP) */
#define MEAS_QDS_IV (0x0080u) /* 无效:该路取窗失败,无有效读数 */
#define MEAS_QDS_CLIPPED (0x0100u) /* 硬件削顶(adc_cal_flags 位图直通,
* 「数据可信」§4.2 三态表;AC 量不可信,
* 消费者应映射为品质无效,如卡 9 的 104 IV) */
/* 遥测快照(docs/任务规划/数据可信.md §4.5 权威布局;实测 72 B,2×72 ≈ 144 B
* 双区 = 卡 8 DoD 预算口径,由 test/host/meas/ 的布局钉死用例锁字段偏移——
* 改字段 = 破协议,须同步卡 9 / UI) */
typedef struct {
uint32_t seq; /* 只增;消费者据此判断是否换帧 */
uint32_t tick_ms; /* 发布拍时基(osKernelGetTickCount 原值) */
uint8_t valid; /* 整帧有效:本拍 8 电气路取窗全部成功 */
uint8_t rsv[3]; /* 对齐填充,恒 0 */
float yc[ADC_SEM_COUNT]; /* 工程量(引脚电压 V 域;二次值等 VALID 标定) */
uint16_t qds[ADC_SEM_COUNT]; /* 每点品质 MEAS_QDS_*(值本身不因品质清零) */
uint8_t freq_valid; /* 频率读数新鲜度(freq_valid() 注入) */
float freq_hz; /* 频率读数(freq_last() 注入,未收敛为 0) */
} meas_snap_t;
/*
* 函数功能:遥测快照层初始化(清双区 / 索引 / 序号 / 就绪标志)
* 入口参数:无
* 返 回 值:0 - 成功 int
* 限定条件:内核启动前或任务上下文皆可(纯 .bss 状态,无 RTOS 依赖);
* 本仓落点 = adc_task_init()(每开机一次,幂等可重复调)
* 函数说明:init 后 meas_get() 返 -2、meas_seq() 返 0,直到首次 meas_tick()
* 发布——消费者据 -2 走"未就绪"分支,不读半初始化数据。
*/
int meas_init(void);
/*
* 函数功能:一拍遥测发布(200 ms 分频由调用方把关,本函数被调即发布一帧)
* 入口参数:tick_ms - 本拍时基(毫秒) uint32_t
* freq_valid_in - 频率读数新鲜度(freq_valid() 值) uint8_t
* freq_hz_in - 频率读数(freq_last() 值;从未收敛为 0.0) float
* clip_mask - 削顶通道位图(bit s = 语义通道 s;来源唯一 =
* adc_cal_flags() 的 ADC_CAL_F_CLIPPED,编排方注入,
* 本层不链 adc_cal——可用性属主在折算侧) uint32_t
* 返 回 值:无
* 限定条件:**仅 adcTask 上下文、全仓唯一调用点**(发布纪律见文件头);
* 须在 sample_ring_push_frame()/phasor_run() 之后调用,本拍窗才含最新点
* 函数说明:8 电气路逐路取"以本拍为终止的一周波"做基波 DFT(复用 phasor_calc
* 纯函数核,与 phasor 使用区同窗同核 → 同拍同值,见单测钉死),
* 任一路窗不足 → 该路 yc=0 + qds=IV|NT、整帧 valid=0(不产出半新半旧
* 混合帧;与 phasor_run 的差异:phasor 宁可不切区,meas **照常发布**
* 让品质位可见——消费者需要节拍性的品质刷新,seq 亦随之只增)。
* TEMP = 最新一点透传 + NT(见文件头)。末尾单字节翻转使用区,
* 读者无锁。
*/
void meas_tick(uint32_t tick_ms, uint8_t freq_valid_in, float freq_hz_in,
uint32_t clip_mask);
/*
* 函数功能:读当前使用区快照(无锁整体拷贝)
* 入口参数:out - 输出快照;失败(含 -2 未就绪)时**清零填充**,调用方可安全读
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 从未发布过(out 已清零,seq=0) int
* 限定条件:任意上下文(任务级读者:iec104Task / uiTask / protectTask)
* 函数说明:先取一次使用区索引再索引拷贝——拷贝期间写者最多改"**非使用区**",
* 本帧无撕裂(双区不变量,同 phasor_get 口径;写者两帧间隔 200 ms
* 远大于 72 B 拷贝耗时,读者被压超 200 ms 的竞态窗口按 §4.5 设计接受)。
* 判"新不新"用 seq:与上次不同则换帧。
*/
int meas_get(meas_snap_t *out);
/*
* 函数功能:读已发布帧数(只增)
* 入口参数:无
* 返 回 值:seq(0 = 从未发布;单字 32 位读,Cortex-M 原子) uint32_t
* 限定条件:任意上下文
* 函数说明:轻量观测入口(104 周期上送可先比 seq 省 72 B 拷贝);
* 与 meas_get 之间无原子性配对要求(读者以 get 返回的 seq 为准)。
*/
uint32_t meas_seq(void);
#ifdef __cplusplus
}
#endif
#endif /* MEAS_H */
+52
View File
@@ -92,6 +92,12 @@
* 门限 adc_cal_rail_ths,码值域与标定正交),锁存边沿 →
* DBG_ERROR + 每开机一次 SYSLOG;心跳增 cal: 行(mV 定点
* + valid/clipped 位图 + rail 锁存),台架注入口径见该行判读
* v3.2 2026-10-25 DTU 项目组 L2 遥测快照发布(「数据可信」卡 8,道D,M5):新模块
* App/algo/meas.c——本任务为唯一发布者,新帧拍内
* freq_run() 之后按 200 ms **tick 经过**分频调
* meas_tick()(全仓调用点 = 1,评审判据);品质注入源 =
* adc_cal_flags() 削顶位图,频率 = 同拍 freq 读数;心跳新增
* meas: 行(seq/v/ua·un·ia_mv/f/fv),读数比对类判据待台架实测
*/
#include "adc_task.h"
@@ -103,6 +109,7 @@
#include "freq.h" /* L2 派生量:freq_run()/freq_get()(测频,B8) */
#include "adc_health.h" /* L1-6:采样有效性校验(静默/丢帧/单调性/错误增量/贴轨) */
#include "adc_cal.h" /* L1.5:通道标定折算(「数据可信」卡 2;flags 供 cal 心跳行) */
#include "meas.h" /* L2 发布侧:遥测快照 meas_tick()(「数据可信」卡 8,道D) */
#include "tpafe5160.h"
#include "task_watchdog.h"
#include "console_log.h"
@@ -137,6 +144,11 @@ static uint32_t s_health_logged;
static uint32_t s_rail_edge_latched;
static uint8_t s_rail_syslogged;
/* 遥测快照发布闸门(「数据可信」卡 8):上次发布拍时基。分频用 **tick 经过**
* 对比 MEAS_PERIOD_MS,非拍计数——idle/skip 拍会把拍计数口径拉得超过名义
* 200 ms(与 v2.6 修"重复采样污染 DFT 窗"同族误差,勿"优化"成拍计数) */
static uint32_t s_meas_ms;
/* 消费者就绪门:由 adcTask 自己在进入等待前置 1;单写者 = 本任务,ISR 只读。
* ⚠ 为什么必须有这道门:TIM3 在 app_main_init()(Src/main.c:140)里就
@@ -265,7 +277,9 @@ int adc_task_init(void) {
s_frames_skip = 0u;
s_rail_edge_latched = 0u;
s_rail_syslogged = 0u;
s_meas_ms = 0u;
adc_health_init(); /* 内含 L5-2 贴轨观察器复位(rail_init) */
(void)meas_init(); /* 遥测快照双区/序号复位(幂等;发布状态属本任务,init 无日志) */
return 0;
}
@@ -375,6 +389,24 @@ void adc_task_func(void *arg) {
/* L2 派生量测频(B8):同拍紧随 phasor_run——本拍使用区已
* 切换,freq 读的就是本拍相量(1 ms 同上下文,无竞态) */
freq_run();
/* L2 遥测快照发布(「数据可信」卡 8,道D,M5):200 ms tick 经过分频。
* 唯一发布者 = 本任务(全仓 meas_tick 调用点数 = 1 为评审判据,
* 「数据可信」§8 风险表"快照双写"行);只在**压环成功的真新帧拍**发布
* ——idle/skip 拍压环未推进,发布 = 重复旧窗。品质注入唯一源 =
* adc_cal_flags()(§4.2 三态表;纯读,每 200 ms 才跑一遍)。 */
if ((uint32_t)(now - s_meas_ms) >= MEAS_PERIOD_MS) {
uint32_t cm = 0u;
uint8_t m;
s_meas_ms = now;
for (m = 0u; m < (uint8_t)ADC_SEM_COUNT; m++) {
if ((adc_cal_flags((adc_semantic_t)m) & ADC_CAL_F_CLIPPED) != 0u) {
cm |= (1uL << m);
}
}
meas_tick(now, freq_valid(), freq_last(), cm);
}
} else {
s_frames_skip++;
}
@@ -525,6 +557,26 @@ void adc_task_func(void *arg) {
(unsigned long)adc_health_rail_sticky(),
(unsigned long)s_rail_edge_latched);
}
/* M5 遥测快照台架观测面(定点打印,MicroLIB 无 %f):
* seq = 发布帧数(10 s 心跳间隔内应 +500±,不涨 = 发布闸门停)
* v = 整帧有效;ua/un/ia = 基波有效值毫伏(未标定域 = 引脚电压;
* 本行是 200 ms 快照轨,cal: 行是当拍瞬时码折算——同量级不同
* 相位,勿互为判据)
* f/fv = 频率读数与新鲜度(x0.01)
* 读数与注入源比对类判据 = **待台架实测**(实测采样类跳过口径) */
{
meas_snap_t ms;
(void)meas_get(&ms); /* 未就绪(-2) = 清零帧,以下打印全 0 */
DBG_INFO("meas: seq=%lu v=%u ua_mv=%ld un_mv=%ld ia_mv=%ld f=%ld fv=%u",
(unsigned long)ms.seq, (unsigned)ms.valid,
(long)(ms.yc[ADC_SEM_UA] * 1000.0f),
(long)(ms.yc[ADC_SEM_UN] * 1000.0f),
(long)(ms.yc[ADC_SEM_IA] * 1000.0f),
(long)(ms.freq_hz * 100.0f),
(unsigned)ms.freq_valid);
}
}
report_at = now + ADC_TASK_REPORT_MS;
}
+5
View File
@@ -1012,6 +1012,11 @@
<FileType>1</FileType>
<FilePath>..\App\algo\adc_cal.c</FilePath>
</File>
<File>
<FileName>meas.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\algo\meas.c</FilePath>
</File>
<File>
<FileName>prot_set.c</FileName>
<FileType>1</FileType>
+1
View File
@@ -95,6 +95,7 @@ SIM_EXEMPT = [
("App/log/soe_store.c", "SOE 落盘 MCU 胶水(队列+FatFs 单写者,B9),模拟器无消费路径"),
("App/algo/sample_ring.c", "L2 环形缓冲纯算法,模拟器无消费路径"),
("App/algo/adc_cal.c", "L1.5 标定折算纯算法 + cfg 段注册(消费者 = MCU 侧 adcTask 每拍;模拟器不跑采样任务),模拟器无消费路径"),
("App/algo/meas.c", "L2 遥测快照发布纯算法(唯一发布者 = MCU 侧 adcTask 200 ms 分频;104/UI 侧消费属卡 9/UI 波次另行接线,模拟器无消费路径;L0.5 单测 test/host/meas 覆盖),模拟器无消费路径"),
("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"),
("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"),
("App/log/console_log.c", "MCU 控制台输出,模拟器无消费路径"),
+22
View File
@@ -1730,6 +1730,28 @@
"owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCHEALTH/TC-HOST-ADCCAL 先例;2026-10-25 道A 卡7 建族):属主登记处 = 单测代码目录,不逐号补文档",
"cursor_note": "游标按建族时实测最大号收录(2026-10-25):越界即视为未登记的冒号牌"
},
{
"name": "TC-HOST-MEAS",
"class": "TC",
"title": "主机侧单测用例号:遥测快照层(test/host/meas,道D 卡8)",
"pattern": "\\bTC-MEAS-[0-9]{1,4}[a-z]?\\b",
"digit_width": [
2,
4
],
"owner_docs_glob": [
"test/host/meas/*"
],
"scope": "global",
"frozen": false,
"cursor": "TC-MEAS-011",
"cursor_owner_only": true,
"check_dangling": true,
"allow_orphans": true,
"subitem_ok": true,
"owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCHEALTH/TC-HOST-ADCCAL 先例;2026-10-25 道D 卡8 建族):属主登记处 = 单测代码目录,不逐号补文档",
"cursor_note": "游标按建族时实测最大号收录(2026-10-25):越界即视为未登记的冒号牌"
},
{
"name": "TC-HOST-IEC104",
"class": "TC",
+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))