v2-on-ui #4
+14
-79
@@ -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)];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user