docs(algo): 5.3 节补窗首下标的圆周图(三种写法对照)

- 新增配图 采样环缓冲_窗首下标.svg:左侧 40 格圆环走一遍 head = 5、n = 20
  的「往回退 19 步」(跨过 0 与 39),右侧把直接减 / 16 位回绕后再取模 /
  先加 DEPTH 三种写法的结果并列,只有第三种落在 25。
- 与 5.2 的环形布局图分工:那张讲「窗在哪几格」,这张讲「下标怎么算出来」,
  不重复同一张图的语义。
- 生成器 gen_sample_ring_start_svg.py 顺带把 OVERFLOW 明细改到末尾统一打印
  (原先只在开头打,折行类告警只报计数、看不出是哪一条)。
- 门禁:doc-links --sections-strict 通过;id-check 新增行无未登记编号;
  generated-fresh 22 个生成器 17 一致、0 新漂移;产物连跑两次 sha1 不变。
This commit is contained in:
2026-10-29 17:29:15 +08:00
parent e71d623e60
commit 126c4bf57f
@@ -205,15 +205,18 @@ flowchart TD
代码里写成 `(s_head + SAMPLE_RING_DEPTH - n) % SAMPLE_RING_DEPTH` 而不是 `(s_head - n) % DEPTH`,
是因为后者有两个独立的坑:
- **16 位无符号下溢后再取模不等价**:`s_head < n` 时减法先回绕成 `65536-(n-s_head)`,
而 65536 不是 40 的倍数,`(x mod 65536) mod 40 ≠ x mod 40`。反例 `s_head = 3`、`n = 20`:
正确窗首是 23,回绕后得 65519,`65519 % 40 = 39` → 整窗错位。
- **有符号负取模**:两个 `uint16_t` 先提升成 `int`,`3 - 20 = -17`;C99 的 `%` 向零截断,
`(-17) % 40 == -17`(不是 23),再转回无符号就是一次**越界读**——不报错、不 HardFault。
- **16 位无符号下溢后再取模不等价**:`s_head < n` 时减法先回绕成 `65536-(n-s_head)` = 65521,
而 65536 不是 40 的倍数,`(x mod 65536) mod 40 ≠ x mod 40`。同一个例子:正确窗首是 25,
回绕后 `65521 % 40 = 1` → 整窗错位,读到的是环尾附近。
- **有符号负取模**:两个 `uint16_t` 先提升成 `int`,`5 - 20 = -15`;C99 的 `%` 向零截断,
`(-15) % 40 == -15`(不是 25),再转回无符号就是一次**越界读**——不报错、不 HardFault。
**先加一个 DEPTH** 同时解决两件事:`DEPTH ≡ 0 (mod DEPTH)`,加它不改变同余类、数学值不变;
而 `s_head + DEPTH - n ∈ [0, 2·DEPTH - 2]`,恒非负、远小于 65535,两种整数提升规则下都不下溢。
逐条反例与边界自检(`n == DEPTH` 时 `start == s_head`)见 `App/algo/sample_ring.c` 同名函数的注释。
边界自检:`n == DEPTH` 时 `start == s_head`——环满时「下一拍要覆盖的那格」就是最老点;
未写满时 `s_head == s_filled`,故 `start >= 0`,窗一定不跨环尾。
`App/algo/sample_ring.c` 里只留结论与指向本节的指针,推导以本节为准。
### 5.4 API 契约