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 序号倒退:锁存不可自愈,代价是永久闭锁",