Files
STM32F4-Base/App/algo/sample_ring.c
T
wandering1 de512a0db8 docs(algo): 补 sample_ring_window 注释,详解窗首下标推导
- 补环内布局示意与 s_head 语义(下一待写位置,非最后写入位置),
  据此推出窗首 = s_head - n、窗尾 = s_head - 1。
- 重点解释 start 为何写成 (s_head + DEPTH - n) % DEPTH:
  加 DEPTH 不改变同余类;规避 16 位下溢后再取模不等价(65536 不是
  40 的倍数);规避有符号负取模(-17 % 40 == -17)。
- 补限定条件:仅 adcTask 上下文,写者与读者同任务串行,故不加锁。
- .h 只加一行指针指向 .c 的推导,避免同一事实两处详述。
- 纯注释改动:去注释后代码 token 与 HEAD 完全一致;@build 0 错 0 警。
2026-10-29 17:06:28 +08:00

204 lines
10 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:采样环缓冲(全周傅氏相量计算的输入)
* 模块功能:按语义通道保存最近 2 个周波(40 点)采样值,供 全周傅氏相量计算按窗取数。
* 适用平台:STM32F407ZGTx(纯算法,无寄存器/无 RTOS 依赖)
* 作者:王建锋
* 创建日期:2026-10-10
* 修改记录:
* v1.0 2026-10-10 王建锋 创建:全周傅氏相量输入环缓冲(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 - 参数错
* 限定条件:同一帧内各通道须同刻(同族量同刻性由上游 语义取数与有效性负责)
* 函数说明:整帧写入完毕后**统一**推进写指针——单帧内不存在"半新半旧",
* 消费侧取到的窗永远落在整帧边界上(无帧内撕裂)。
*/
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 - 语义通道(0 .. ADC_SEM_COUNT-1)
* n - 取点数(1 .. SAMPLE_RING_DEPTH)
* out - 输出缓冲(调用方保证长度 >= n,本层不校验容量)
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 已累积点数不足 n
* 限定条件:**仅 adcTask 上下文**调用,且须在同拍 sample_ring_push_frame() 之后。
* 全仓唯一写者 = adcTask 的压环语句,读者(phasor_run / meas_tick)与之
* 同任务串行执行,写读之间不存在并发 —— 故本函数**不加锁、不用 volatile、
* 不关中断**。若将来把读者搬到别的任务,必须先在本层补双区/序号快照,
* 否则会读到"半新半旧"的窗(旧点已被覆盖、新点尚未写入)。
* 函数说明:语义 = "以当前最新一点为终点、向前回溯 n 点"的一周波窗。
* out[0] 是最老一点、out[n-1] 是最新一点(时间序 旧 → 新),
* 与 phasor_run() 的傅氏求和方向一致,调用方不必再反转。
* 环内布局与窗首下标的推导见函数体内的分步注释。
*/
int sample_ring_window(adc_semantic_t ch, uint8_t n, float *out) {
uint16_t start;
uint16_t i;
/* ── 环内布局(以 DEPTH = 40、n = 20、s_head = 30 为例,此时 start = 10)──
*
* 下标 0 .. 9 | 10 ........ 29 | 30 .. 39
* 内容 旧数据 | <-- 本窗 20 点 --> | 更旧(下一拍起被覆盖)
*
* 三个关键位置:start = 10(窗首/最老点)、s_head - 1 = 29(窗尾/最新点)、
* s_head = 30(下一拍写入处,也是环满时最先被覆盖的那一点)。
*
* ⚠ s_head 是**下一个待写入**的位置,不是最后写入的位置 —— 这一点是理解
* 下面 start 公式的全部关键:最后写入的一点 = s_head - 1,于是
* "以最新点为终点的 n 点窗" = 下标 [s_head-n, s_head-1],共 n 点;
* 窗首(最老点)就是 s_head - n。窗可能跨过环尾(start + i 越过
* DEPTH - 1 时回绕到 0),故取数必须逐点取模。 */
/* 参数校验(通道号):adc_semantic_t 是枚举,C 标准允许其底层类型为有符号整型;
* 若调用方传负值/越界值,直接写 ch >= ADC_SEM_COUNT 会因整数提升变成有符号比较,
* 负值能"骗过"上界判断 → 索引到 s_ring 之外。故先转 unsigned int 再比:
* 负值转成极大无符号数,必然被拦下。(同时覆盖 out == NULL 的空指针。) */
if ((out == NULL) || ((unsigned int)ch >= (unsigned int)ADC_SEM_COUNT)) {
return -1;
}
/* 参数校验(取点数):
* n == 0 → 空窗无物理意义,且此时 start 会等于 s_head,返回"最新 0 点",
* 容易被调用方当成一次成功取窗 → 显式拒收;
* n > DEPTH → 一个环装不下连续 n 点,逐点回绕会把同一点重复读出,
* 拼出来的窗不再是真实时序(会伪造出"周期性"假象)→ 显式拒收。 */
if ((n == 0u) || (n > SAMPLE_RING_DEPTH)) {
return -1;
}
/* 数据够不够:s_filled 饱和于 DEPTH(见 sample_ring_push_frame),故对
* n <= DEPTH 而言,本判据等价于"自 init 起累计入环帧数 >= n"。
* 返回 -2 而非 -1 是有意区分:-2 表示"再等几拍自然会好",调用方应跳过本拍,
* 不应把它当故障上报(首周波启动期必然出现)。 */
if (s_filled < (uint16_t)n) {
return -2;
}
/* ── 窗首(最老点)下标:start = (s_head - n) mod DEPTH 的安全写法 ──────
*
* 数学目标很直白:最老点 = 最新点(s_head-1) 往前退 n-1 步 = s_head - n,
* 因为环是循环的,结果要落在 [0, DEPTH - 1],所以取模。但 C 里**不能**直接
* 写成 (s_head - n) % DEPTH —— 有两个独立的坑,任一个都会让整窗错位:
*
* 坑 1(16 位无符号下溢后再取模,不等价):
* 若写成 (uint16_t)(s_head - n) % DEPTH,s_head < n 时减法先回绕成
* 65536 - (n - s_head)。而 (x mod 65536) mod 40 **不等于** x mod 40
* —— 65536 只含因子 2,不是 40(= 8 × 5)的整数倍,模运算不满足这种
* "分两步取模"的分解。反例:s_head = 3、n = 20,正确 start = 23,
* 而回绕后得 65519,65519 % 40 = 39 → 读到的不是窗首而是环尾附近,
* 整个窗错位(表面现象是相量相位乱跳,极易误判成"采样通道接反")。
*
* 坑 2(有符号负取模):
* 在 32 位 int 平台,两个 uint16_t 参与运算前先做整数提升 → s_head - n
* 得到 int 且可能为负;C99 规定 % 向零截断,于是 (-17) % 40 == -17
* (不是数学上的 23)。这个负值再强转 uint16_t 变成 65519,
* 拿去索引 s_ring[ch][65519] 就是**越界读**(可能读到相邻通道或别的全局量,
* 不报错、不 HardFault,数据看着"像那么回事")。
*
* 修法:**先加一个 DEPTH,再减 n,最后取模**。三件事同时被解决:
* ① 同余不变:DEPTH ≡ 0 (mod DEPTH),加它不改变结果所在的同余类,
* 数学值与 (s_head - n) mod DEPTH 完全一致,不是"凑数";
* ② 恒非负、不溢出:0 <= s_head <= DEPTH - 1、1 <= n <= DEPTH,
* 故 s_head + DEPTH - n ∈ [0, 2·DEPTH - 2] = [0, 78],恒非负且远小于 65535,
* 两种整数提升规则下都不会下溢/溢出,% 的结果就是唯一的 [0, DEPTH - 1] 解;
* ③ 顺带护住后面的 (start + i):start <= DEPTH - 1、i <= n-1,
* 加法最大 2·DEPTH - 2,同样不会在 16 位域回绕。
*
* 边界自检:n == DEPTH 时 start == s_head —— 环满时"下一拍将被覆盖的那一点"
* 恰好就是最老点,这是唯一自洽的解,不是特例。环尚未写满(s_filled < DEPTH)时
* 有 s_head == s_filled,故 start = s_filled - n >= 0,此时窗一定不跨环尾。
*
* 性能备注:SAMPLE_RING_DEPTH = 40 不是 2 的幂,**不能**用 & (DEPTH - 1) 代替 %;
* Cortex-M4 有硬件 UDIV/SDIV,编译器会生成一次除法指令,本函数每拍最多 n 次
* 取模(n <= 40),代价可接受。若将来把 DEPTH 改成 2 的幂,这里可换成位与,
* 但须同步核查全文件所有 % SAMPLE_RING_DEPTH 的使用点。 */
start = (uint16_t)((s_head + SAMPLE_RING_DEPTH - (uint16_t)n) % SAMPLE_RING_DEPTH);
/* 逐点顺序拷贝:从 start 起向前走 n 步,终点正好是 s_head-1(最新点)。
* 每步单独取模,是因为窗可能**跨过环尾**(start + i > DEPTH - 1 时回绕到 0);
* 环形缓冲在内存里首尾并不相连,故不能用一次 memcpy 拷完 —— 要么先判边界再
* 分两段拷,要么像这里逐点取模。DEPTH 只有 40,逐点拷贝比"两段 + 分支"更短更稳。
* 拷贝顺序即时间顺序:out[0] 最老、out[n-1] 最新。 */
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;
}
/*
* 函数功能:读累计入环帧数(不饱和)
* 入口参数:无
* 返 回 值:累计入环帧数
* 限定条件:无
* 函数说明:供心跳日志 / 丢帧观测(采样有效性校验 判据之一)。
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
*/
uint32_t sample_ring_frames(void) {
return s_frames;
}