L2 相量层:采样环缓冲 + 全周傅氏 + 主机单测(台架间歇复位待查)

新增(均已通过主机侧单测与构建,**台架存在未定位的间歇性回归,见下**):
- App/algo/sample_ring.c/h:按语义通道的采样环缓冲(2 周波 = 40 点),
  整帧压入 + 帧计数,供 L2 按窗取数。
- App/algo/phasor.c/h:全周傅氏相量层,覆盖规划 §3.2 的 L2-1/2/3/6/7——
  系数表按 docs/算法/傅氏算法详解.md §2.3 预乘 √2/N(fAmSQ 直接即有效值);
  5 个数据驱动实例(IA/UA/UN 基波 + IA 二/五次谐波);暴力滑动窗;
  计算区/使用区双区原子切换。
- test/host/phasor/:主机侧单测(52/52 PASSED,无需桩)——系数表逐项复核、
  幅值/有效值/相位、直流与谐波抑制、任意次谐波提取、频率敏感性(±2Hz≤3%)、
  环缓冲时序与回绕、实例层与双区交换。
- 集成:adc_task 每拍压帧 + 重算(v2.4);app_main 调 phasor_init();
  Keil 工程挂 phasor.c/sample_ring.c(App/algo 已在 IncludePath)。

验证:
- 主机单测 52/52 PASSED。
- @build 0 错误 0 警告;内存快照已刷新(RW_IRAM1 89,320→90,936 B)。
- 台架:L2 链路数据正常(ring=10001、skip=0、回读一致、相量日志可见),
  但 10 次运行中 3 次出现 UI 任务看门狗停喂(mask=0x67 缺 TASK_WD_UI)
  → IWDG 复位;改动前基线 6 次运行 0 次。属**未定位的间歇性回归**。

未完成:定位并修复该间歇性复位;L2 文档状态与登记簿条目尚未按结论更新。
This commit is contained in:
2026-10-10 17:39:37 +08:00
parent 66186e1dae
commit 3762c03bb4
10 changed files with 1222 additions and 12 deletions
+124
View File
@@ -0,0 +1,124 @@
/*
* 模块名称:采样环缓冲(L2 相量计算的输入)
* 模块功能:按语义通道保存最近 2 个周波(40 点)采样值,供 L2 相量计算按窗取数。
* 适用平台:STM32F407ZGTx(纯算法,无寄存器/无 RTOS 依赖)
* 作者:王建锋
* 创建日期:2026-10-10
* 修改记录:
* v1.0 2026-10-10 王建锋 创建:L2 输入环缓冲(20 点/周波 × 2 周波)
*/
#include "sample_ring.h"
#include <stddef.h>
/* 采样环:[语义通道][点]。按语义通道索引(0..ADC_SEM_COUNT-1),
* 故消费侧"按语义取数",不依赖任何物理通道号或转换器归属。 */
static float s_ring[ADC_SEM_COUNT][SAMPLE_RING_DEPTH];
/* 写指针:下一个待写入的点下标(0..SAMPLE_RING_DEPTH-1) */
static uint16_t s_head;
/* 已累积点数(饱和于 SAMPLE_RING_DEPTH) */
static uint16_t s_filled;
/* 累计入环帧数(不饱和,供心跳/丢帧观测) */
static uint32_t s_frames;
/*
* 函数功能:采样环缓冲初始化
* 入口参数:无
* 返 回 值:无
* 限定条件:无
* 函数说明:清空全部通道历史与计数;可重复调用(幂等)。
*/
void sample_ring_init(void) {
uint8_t c;
uint16_t i;
for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) {
for (i = 0u; i < SAMPLE_RING_DEPTH; i++) {
s_ring[c][i] = 0.0f;
}
}
s_head = 0u;
s_filled = 0u;
s_frames = 0u;
}
/*
* 函数功能:压入一拍(一帧)全部语义通道的采样值
* 入口参数:values - 按语义通道顺序排列的一帧采样值(长度须 = ADC_SEM_COUNT)
* n - 实际长度(必须等于 ADC_SEM_COUNT)
* 返 回 值:0 - 成功;-1 - 参数错
* 限定条件:同一帧内各通道须同刻(同族量同刻性由上游 L1 负责)
* 函数说明:整帧写入完毕后**统一**推进写指针——单帧内不存在"半新半旧",
* 消费侧取到的窗永远落在整帧边界上(无帧内撕裂)。
*/
int sample_ring_push_frame(const float *values, uint8_t n) {
uint8_t c;
if ((values == NULL) || (n != (uint8_t)ADC_SEM_COUNT)) {
return -1;
}
for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) {
s_ring[c][s_head] = values[c];
}
s_head = (uint16_t)((s_head + 1u) % SAMPLE_RING_DEPTH);
if (s_filled < SAMPLE_RING_DEPTH) {
s_filled++;
}
s_frames++;
return 0;
}
/*
* 函数功能:取指定语义通道最近 n 点的窗(按时间序,旧 → 新)
* 入口参数:ch - 语义通道
* n - 取点数(1..SAMPLE_RING_DEPTH)
* out - 输出缓冲(长度须 >= n)
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 已累积点数不足 n
* 限定条件:无
* 函数说明:out[n-1] 为最新一点。最老点下标 = head - n(模 DEPTH)。
*/
int sample_ring_window(adc_semantic_t ch, uint8_t n, float *out) {
uint16_t start;
uint16_t i;
if ((out == NULL) || ((unsigned int)ch >= (unsigned int)ADC_SEM_COUNT)) {
return -1;
}
if ((n == 0u) || (n > SAMPLE_RING_DEPTH)) {
return -1;
}
if (s_filled < (uint16_t)n) {
return -2;
}
start = (uint16_t)((s_head + SAMPLE_RING_DEPTH - (uint16_t)n) % SAMPLE_RING_DEPTH);
for (i = 0u; i < (uint16_t)n; i++) {
out[i] = s_ring[ch][(uint16_t)((start + i) % SAMPLE_RING_DEPTH)];
}
return 0;
}
/*
* 函数功能:读已累积点数(饱和于 SAMPLE_RING_DEPTH)
* 入口参数:无
* 返 回 值:已累积点数(0..SAMPLE_RING_DEPTH)
* 限定条件:无
* 函数说明:供上层判断"数据是否够一周波"。
*/
uint16_t sample_ring_filled(void) {
return s_filled;
}
/*
* 函数功能:读累计入环帧数(不饱和)
* 入口参数:无
* 返 回 值:累计入环帧数
* 限定条件:无
* 函数说明:供心跳日志 / 丢帧观测(L1-6 判据之一)。
*/
uint32_t sample_ring_frames(void) {
return s_frames;
}