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:
@@ -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
@@ -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
@@ -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
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 控制台输出,模拟器无消费路径"),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user