docs(algo): sample_ring_window 注释收敛,长推导移到设计说明

- 设计说明第 5 节已详述「为什么需要 / 设计思想 / start 推导」,函数内不再复述:
  环内布局示意、两条坑的逐句论证、取值范围推导全部删去,改为一句结论 + 指针
  (docs/算法/采集与算法层设计说明.md §5 与 §5.3)。
- 留在代码里的都是「就地可判」的落地要点:通道号为何先转 unsigned、
  n 的两条拒收理由、-2 与 -1 的语义区分、先加 DEPTH 的同余理由、逐点取模。
- .h 的指针从 .c 改指设计说明,避免同一事实两处详述。
- 净减 81 行;去注释后代码 token 与 HEAD 逐字相同;@build 0 错 0 警。
This commit is contained in:
2026-10-29 17:19:42 +08:00
parent c377fc8c15
commit 929cacbd72
2 changed files with 16 additions and 81 deletions
+14 -79
View File
@@ -77,102 +77,37 @@ int sample_ring_push_frame(const float *values, uint8_t n) {
* 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() 的傅氏求和方向一致,调用方不必再反转。
* 环内布局与窗首下标的推导见函数体内的分步注释。
* 限定条件:仅 adcTask 上下文(写者与读者同任务串行,故不加锁、不用 volatile);
* 须在同拍 sample_ring_push_frame() 之后调用。
* 函数说明:以最新一点为终点、向前回溯 n 点;out[0] 最老、out[n-1] 最新,
* 与 phasor_run() 的求和方向一致。
* 为什么需要这一层、环内布局与窗首下标的推导 →
* docs/算法/采集与算法层设计说明.md §5(本文件只留落地要点)。
*/
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 的空指针。) */
/* 通道号先转 unsigned 再比:枚举底层可为有符号,负值会骗过 ch >= COUNT 的判据。 */
if ((out == NULL) || ((unsigned int)ch >= (unsigned int)ADC_SEM_COUNT)) {
return -1;
}
/* 参数校验(取点数):
* n == 0 → 空窗无物理意义,且此时 start 会等于 s_head,返回"最新 0 点",
* 容易被调用方当成一次成功取窗 → 显式拒收;
* n > DEPTH → 一个环装不下连续 n 点,逐点回绕会把同一点重复读出,
* 拼出来的窗不再是真实时序(会伪造出"周期性"假象)→ 显式拒收。 */
/* n = 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 表示"再等几拍自然会好",调用方应跳过本拍,
* 不应把它当故障上报(首周波启动期必然出现)。 */
/* -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 的使用点。 */
/* 窗首 = s_head - n(s_head 是"下一待写"位置,故窗尾 = s_head - 1)。
* 必须先加 DEPTH 再减:直接减在 s_head < n 时下溢或得负数,而
* (x mod 65536) mod 40 不等于 x mod 40、C99 的负数取模也不是同余解。
* 加 DEPTH 不改同余类,并把中间值锁在 [0, 2·DEPTH - 2]。详见设计说明 §5.3。 */
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] 最新。 */
/* 逐点取模拷贝:窗可能跨环尾,环在内存里首尾不相连,不能用一次 memcpy。 */
for (i = 0u; i < (uint16_t)n; i++) {
out[i] = s_ring[ch][(uint16_t)((start + i) % SAMPLE_RING_DEPTH)];
}
+2 -2
View File
@@ -60,8 +60,8 @@ int sample_ring_push_frame(const float *values, uint8_t n);
* 限定条件:无
* 函数说明:out[n-1] 为最新一点。相量层以本函数取"以当前时刻为终止的一周波"。
* ⚠ 仅 adcTask 上下文调用(写者与读者同任务串行,本层不加锁);
* 环内布局、窗首下标推导与"先加 DEPTH 再减 n"的理由见
* `App/algo/sample_ring.c` 同名函数的注释。
* 为什么需要这一层、环内布局与窗首下标的推导 →
* `docs/算法/采集与算法层设计说明.md` §5。
*/
int sample_ring_window(adc_semantic_t ch, uint8_t n, float *out);