diff --git a/App/algo/adc_input.c b/App/algo/adc_input.c index 6496c6b6..e6499ebe 100644 --- a/App/algo/adc_input.c +++ b/App/algo/adc_input.c @@ -65,11 +65,17 @@ static uint32_t s_stm32_errs; /* STM32 扫描累计失败次数 */ * DISABLE),按 Rank 顺序逐通道轮询 EOC 并读 DR,结果按 ADC1 通道号 * 存入 s_stm32_raw[]。EOCSelection=SINGLE_CONV 且未用 DMA → 每个通道 * 产生一次 EOC,可逐通道轮询(见 HAL_ADC_PollForConversion 说明)。 + * 两条失败支路(启动失败 / 轮询超时)**都会清 s_stm32_valid**:本拍 + * STM32 侧判为无有效数据,绝不沿用上一拍旧值(详见支路内注释)。 + * 超时在**首个**失败通道处即 Stop+return,不轮询剩余通道 → 单拍耗时 + * 上界 = 一次超时(≈2~3 ms,HAL_GetTick 1 ms 粒度),不是 9 次。 */ static int stm32_scan(void) { uint8_t i; if (HAL_ADC_Start(&hadc1) != HAL_OK) { + /* 失败拍必须清有效标志,见下方超时分支说明(同一语义) */ + s_stm32_valid = 0u; s_stm32_rc = ADC_INPUT_RC_START; s_stm32_errs++; return ADC_INPUT_RC_START; @@ -78,6 +84,14 @@ static int stm32_scan(void) { for (i = 0u; i < ADC_INPUT_STM32_CH_NUM; i++) { if (HAL_ADC_PollForConversion(&hadc1, ADC_INPUT_POLL_TIMEOUT_MS) != HAL_OK) { (void)HAL_ADC_Stop(&hadc1); + /* ⚠ 失败拍**不得**沿用上一拍的旧值:本层只有一份 s_stm32_raw[], + * 超时前几个 Rank 已写入**本拍新值**、后面几个还是**上一拍旧值** → + * 若保持 s_stm32_valid=1,消费侧会拿到"半新半旧"的混合帧并照样压入 + * 采样环(adc_input_poll() 的返回值只由 TPAFE 决定,s_frames_skip + * 也抓不到它),直接污染 20 点 DFT 相量窗。清标志后 adc_input_raw() + * 对 STM32 语义通道返 -2 → adc_task 走既有的"整帧跳过"路径。 + * 下一拍扫描成功会重新置 1(自愈),无需额外恢复动作。 */ + s_stm32_valid = 0u; s_stm32_rc = ADC_INPUT_RC_TIMEOUT; s_stm32_errs++; return ADC_INPUT_RC_TIMEOUT; diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 3ef59067..cd6557d8 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 273,028 B (266.63 KB) | 1,048,576 B (1024 KB) | 26.0% | +| ER_IROM1 | 273,032 B (266.63 KB) | 1,048,576 B (1024 KB) | 26.0% | | RW_IRAM1 | 91,336 B (89.20 KB) | 131,072 B (128 KB) | 69.7% | | RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% | -| **Flash (ROM 合计)** | **273,836 B (267.4 KB)** | 1,048,576 B (1024 KB) | **26.1%** | +| **Flash (ROM 合计)** | **273,840 B (267.4 KB)** | 1,048,576 B (1024 KB) | **26.1%** | 各区的容量与常量来源: diff --git a/docs/子系统设计/ADC采样节拍设计说明.md b/docs/子系统设计/ADC采样节拍设计说明.md index d34ffeb8..f74d7839 100644 --- a/docs/子系统设计/ADC采样节拍设计说明.md +++ b/docs/子系统设计/ADC采样节拍设计说明.md @@ -239,6 +239,7 @@ t=1000 µs 下一个 TIM6 更新事件 | 2026-10-10 | **事件驱动消费方案 a 已实施(Task #32 P1-5)** | 四处改动落地:`Src/main.c` TIM6 分支调 `adc_tick_irq_notify()`;`adc_task.c` 循环尾改 `osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever)` 并删 `ADC_TASK_BEAT_MS`/`next`;`Src/freertos.c` 优先级 `Normal`→**`AboveNormal`**;`App/task/adc_task.h` 加 `ADC_WAKE_FLAG` 与 ISR 入口。实施时补**就绪门**(§10.6,规划未预见:TIM6 比调度器早起 ≈100 ms)。`@build` 0 错 0 警(Code 256368→256728);`@test` PASS:**`idle = 0`/19999 拍**(改前 1~649)、`frames` 1000.0 Hz、`seq−frames = 7` 正是就绪窗口、UI 侧 `uip/uiw/uis` 无退化 | | 2026-10-10 | **§10.1/§10.3/§10.4 复核修正(无功能改动)** | ① 2 ms 忙等的单拍上界由"9 通道 ×2 = 18 ms"更正为**一次超时 ≈2~3 ms**(`stm32_scan()` 在首个超时处即 `Stop`+`return`);② 6 次上板对照 `adcp` = **122 µs**(超时 0 次的 5 轮)/ **2209 µs**(超时 1 次的那轮)→ 2 ms 不是常态开销;③ `adcm` 2.1 ms 峰值**归因改判**为每 10 s 一次的心跳日志(`idle=0`/20000 反证无每拍 2 ms);④ 新增超时的**正确性**代价(超时拍 STM32 侧静默沿用上一拍值,`skip` 抓不到)与硬化排序 | | 2026-10-10 | **补 ADC1 上电自校准(§10.4)** | 发现 F4 必需的自校准在本项目**完全缺失**(CubeMX 不生成 + 裁剪 HAL 无 `HAL_ADCEx_Calibration_Start` + 裁剪 CMSIS 头无 `ADC_CR2_CAL`)→ 在 `Src/adc.c` 的 `USER CODE` 区实现 `adc1_calibrate()`(RM0090 §13.3.6 时序),`MX_ADC1_Init()` 末尾调用。新增审计项 `adc1-calibration-call`/`-impl`。`@build` 0 错 0 警(Code 256728→256868,+140 B);`@test` PASS,`adcp` 仍 122 µs、`adc1_err=0`;实测 STM32 侧 6 路同向偏移 +10~+63 LSB、TPAFE 侧不变 | +| 2026-10-10 | **修"失败拍沿用旧值"(§10.4 硬化 ①)** | `stm32_scan()` 两条失败支路都清 `s_stm32_valid` → 失败拍走既有 `skip` 整帧跳过,不再产出"半新半旧"混合帧污染相量窗;下一拍成功自愈。主机单测 70 → **79** 用例(新增 TC-AI-070..078),并做反向验证(改回旧行为 3 条立即 FAIL)。`@build` 0 错 0 警(Code 256868→256872)、`@test` PASS。硬化 ②(DMA 消忙等)仍未做,并更正原表述:**F4 的 ADC EOC 没有 EXTI 通道**,只有 `ADC1_2_IRQn` | --- @@ -314,9 +315,13 @@ t=1000 µs 下一个 TIM6 更新事件 而 `adc_input_poll()` 的返回值只由 TPAFE 决定 → 该拍照样 `r == 1`、照样压环,STM32 侧几路 (UB/UC/IB/IC/IN/温度)**静默沿用上一拍的值**(`adc_input.c:209-212`),`s_frames_skip` 只统计"从未成功过"、抓不到它。 - **硬化方向(待做,按性价比排序)**:① 超时即置 `s_stm32_valid = 0` → 本拍 STM32 侧判为无有效 - 数据,走既有 `skip` 整帧跳过路径(一行改动、无时序风险,先把"静默陈旧帧"这条正确性问题关掉); - ② EOC 改 EXTI 或 DMA + 非阻塞查询,彻底消除忙等;③ DMA 双缓冲属 V0.2 硬件项(§10.5)。 + **硬化排序**:① ✅ **已实施(2026-10-10)**——`stm32_scan()` 两条失败支路(启动失败 / 轮询 + 超时)都清 `s_stm32_valid` → 本拍 STM32 侧判为无有效数据,走既有 `skip` 整帧跳过路径, + 下一拍成功自动恢复;主机侧新增 **TC-AI-070..078** 钉死并做反向验证(改回旧行为 3 条立即 + FAIL)。② **消忙等 = DMA**:`HAL_ADC_Start_DMA(&hadc1, buf, 9)`,scan 模式下每个通道 EOC + 由 DMA 自动搬进缓冲,任务侧不再逐通道 `PollForConversion`(⚠ **F4 的 ADC EOC 没有 EXTI + 通道**,只有 `ADC1_2_IRQn`——"EOC 改 EXTI"在 F4 上不成立,别照 F3/G4 的做法抄)。 + ③ 双缓冲 / 等周波触发属 V0.2 硬件项(§10.5)。 - **已做的一项(2026-10-10):补 F4 ADC 上电自校准**。CubeMX **不生成**该步骤,且本仓库裁剪版 HAL **没有** `HAL_ADCEx_Calibration_Start` → 由 `Src/adc.c` 的 `adc1_calibrate()` 实现 (`USER CODE` 区 = L2,审计项 `adc1-calibration-call` / `adc1-calibration-impl` 兜底)。 diff --git a/docs/测试说明/ADC输入层单元测试说明.md b/docs/测试说明/ADC输入层单元测试说明.md index 39b60909..76e44443 100644 --- a/docs/测试说明/ADC输入层单元测试说明.md +++ b/docs/测试说明/ADC输入层单元测试说明.md @@ -3,7 +3,7 @@ > **模块**:ADC Input Layer(L1 采集与预处理,`App/algo/adc_input.c`) > **测试类型**:**主机侧纯逻辑单元测试**(L0.5 层)——在 PC 上离线验证语义通道映射、 > STM32 ADC1 顺序扫描、TPAFE 快照消费与取数守卫,零硬件、零烧录、毫秒级、不占 MCU 的 RAM。 -> **状态**:✅ 已落地(70 用例,`/W4` 0 告警,已挂 pre-commit 钩子) +> **状态**:✅ 已落地(79 用例,`/W4` 0 告警,已挂 pre-commit 钩子) > **日期**:2026-10-10 > **关联文档**: > [`../../AGENTS.md`](../../AGENTS.md)(平台约束 · CubeMX 生成区铁律)、 @@ -171,6 +171,16 @@ test/host/adc_input/adc_input_test.c | **TC-AI-039** | 超时后**立即退出**(只轮询 5 次,不继续扫完) | 不长时间阻塞 1 ms 保护节拍 | | **TC-AI-039b** | 超时后 STM32 侧无有效数据 | 不产出半帧 | | **TC-AI-03A** | 再失败一次 → 计数 = 2 | 跨次单调累计 | +| **TC-AI-070..078** | ⭐ **失败拍不得沿用上一拍旧值**:先成功一拍让 STM32 侧进入有效态(`UB`=108),再注入超时拍 / `Start` 失败拍 → `adc_input_raw()` 必须返 **-2**;恢复拍重新可取数 | **陈旧帧**(2026-10-10 真机实测发现的缺陷,见下注) | + +> ⚠ **为什么 TC-AI-034 / 039b 照不出这条缺陷**:两者都是"`adc_input_init()` 后**立刻**失败", +> 此时 `s_stm32_valid` 本来就是 0,断言 -2 恒成立。真正的缺陷路径是**成功过之后再失败**: +> 本层只有一份 `s_stm32_raw[]`,超时拍里"超时前几个 Rank 已写本拍新值、后面几个还是上一拍 +> 旧值",而有效标志此前**只在成功时置 1、失败时从不清零** → 消费侧照样取数、照样压入采样环 +> (`adc_input_poll()` 返回值只由 TPAFE 决定,`s_frames_skip` 抓不到),直接污染 20 点 DFT 窗。 +> 修复 = 两条失败支路都清 `s_stm32_valid`(`App/algo/adc_input.c`)。 +> **反向验证已做**:把超时支路改回旧行为,TC-AI-039b / 073 / 074 三条立即 FAIL(76/79)→ +> 证明这组用例真的咬得住。 ### 2.5 取数与参数守卫(`adc_input_raw`) @@ -263,3 +273,4 @@ V0.1 上 8 路电气量分属两片转换器且**无共同采样触发**(N-18 | 日期 | 变更 | |---|---| | 2026-10-10 | 创建;70 用例全绿(`/W4` 0 告警);覆盖 `adc_input.c` 全部 8 个函数(6 公开 + 2 静态);挂 pre-commit 钩子 `adc-input-unit-test` | +| 2026-10-10 | **新增 TC-AI-070..078(70 → 79 用例)**:钉死"失败拍不得沿用上一拍旧值"。配套修复 `App/algo/adc_input.c` 两条失败支路(清 `s_stm32_valid`)。已做**反向验证**:改回旧行为 → TC-AI-039b/073/074 立即 FAIL(76/79),证明用例有效 | diff --git a/docs/问题记录/UI任务看门狗停摆分析.md b/docs/问题记录/UI任务看门狗停摆分析.md index fd46cd97..82803d8b 100644 --- a/docs/问题记录/UI任务看门狗停摆分析.md +++ b/docs/问题记录/UI任务看门狗停摆分析.md @@ -230,11 +230,15 @@ TIM6 ISR 提供。**"超拍永久落后 → 空转"这一类缺陷在结构上 - 但 `adcTask` 现已在 **`AboveNormal`(32)**,一旦某拍真走到 2 ms 忙等,它挤的是 netTask / ftpTask / defaultTask / timeSyncTask(改前是同级竞争)→ **这是 P1-5 换来的新暴露, 记在账上**; -- ⚠ **超时的第二个代价是正确性,不只是耗时**:超时拍 `s_stm32_valid` 仍是上次成功留下的 1, - 而 `adc_input_poll()` 的返回值只由 TPAFE 决定 → 该拍照样 `r == 1`、照样压环,STM32 侧几路 - (UB/UC/IB/IC/IN/温度)**静默沿用上一拍的值**(`adc_input.c:209-212`);`s_frames_skip` - 只统计"从未成功过",抓不到它。→ 硬化第一步应是**超时即置 `s_stm32_valid = 0`**(走既有 - `skip` 整帧跳过路径,一行改动),而不是先去消忙等。 +- ✅ **超时的第二个代价是正确性,不只是耗时(2026-10-10 已修)**:超时拍 `s_stm32_valid` 此前 + 仍是上次成功留下的 1,而 `adc_input_poll()` 的返回值只由 TPAFE 决定 → 该拍照样 `r == 1`、 + 照样压环,STM32 侧几路(UB/UC/IB/IC/IN/温度)**静默沿用上一拍的值**(且超时前几个 Rank 已写 + 本拍新值 → 半新半旧的混合帧);`s_frames_skip` 只统计"从未成功过",抓不到它。 + **修复** = `stm32_scan()` 两条失败支路(`HAL_ADC_Start` 失败 / 轮询超时)**都清 + `s_stm32_valid`** → 本拍 `adc_input_raw()` 对 STM32 语义通道返 -2 → `adc_task` 走既有 + `skip` 整帧跳过路径,下一拍成功自动恢复。主机侧新增 **TC-AI-070..078** 钉死(旧用例 + TC-AI-034/039b 都是"init 后立刻失败",`valid` 本就是 0,照不出这条路径),并做**反向验证**: + 把支路改回旧行为 → 3 条立即 FAIL(76/79)。**剩余未做 = 消掉 2 ms 忙等本身(DMA)**。 --- @@ -544,3 +548,4 @@ A(门控 + 兜底)→ @build + @test + e2e → 看 [PROF]:uis 掉到 µs | 2026-10-10 | **P1-5 已实施(方案 a:事件驱动消费)**:`Src/main.c` TIM6 分支在 `tpafe5160_start_conv_irq()` 后调 `adc_tick_irq_notify()`;`App/task/adc_task.c` 循环尾 `osDelayUntil` → `osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever)` 并删 `ADC_TASK_BEAT_MS`/`next`;`Src/freertos.c` 的 `adcTask` 优先级 `Normal` → **`AboveNormal`**;`App/task/adc_task.h` 新增 `ADC_WAKE_FLAG` 与 ISR 入口。实施时补一条规划未预见的**就绪门** `s_consumer_ready`——TIM6 在 `app_main_init()` 就启动,比建任务与 `osKernelStart()` 早 ≈100 ms,ISR 不能对尚不存在的消费者动手(同本仓 TIM7 分支 2026-10-02 的实测坑)。验证:`@build` 0 错 0 警(Code 256368→**256728**)、**`@test` 6/6 PASS**、**`idle` 由 1~649 降到 0**、`frames` 严格 1000.0 Hz、`seq−frames = 7` 正是就绪窗口、UI 侧 `uip/uiw/uis` 一字不差、0 `task stall`。本文同步:§4.1 补优先级变化与可接受性论证、§4.3 补"再现状"(P0-2 那段已删)、§4.4 改为"忙等未改但敏感度上升"、§8 的 P1-5 行转 ✅ 并拆出仍未办的"单拍硬上界"、健康剖面按 6 连跑复测值更新 | | 2026-10-10 | **§4.4 复核修正(无代码改动)**:① 原文"9 通道全超时最坏 = **18 ms** 连占"**有误**——`stm32_scan()` 在首个超时处即 `HAL_ADC_Stop()`+`return`(`adc_input.c:79-84`),单拍上界 = **一次**超时 ≈2~3 ms;② 6 次上板对照证明 **2 ms 只在超时拍出现**:超时 1 次那轮 `adcp = 2209 µs`,超时 0 次的 5 轮 `adcp = 122 µs`;③ `adcm ≈ 2.1 ms` 的归因由"ADC 采集"**改判**为每 10 s 一次的心跳日志(`idle = 0`/20000 反证不存在"每拍 2 ms");④ 新增超时的**正确性**代价——超时拍 `s_stm32_valid` 仍为 1 → STM32 侧静默沿用上一拍值、`skip` 抓不到 → 硬化第一步应是"超时即置 `s_stm32_valid = 0`"。同步修正 `App/task/adc_task.c` 两处过期注释与 `ADC采样节拍设计说明.md` §10.1/§10.3/§10.4 | | 2026-10-10 | **补 F4 ADC 上电自校准(§7.2 候选根因之一)**:核查发现本项目**从未**做过 ADC 自校准——CubeMX 不生成该步骤,且仓库裁剪版 HAL 无 `HAL_ADCEx_Calibration_Start`、裁剪版 CMSIS 头无 `ADC_CR2_CAL`。已在 `Src/adc.c` 的 `USER CODE` 区实现 `adc1_calibrate()`(清 ADON→等生效→置 ADON→置 CAL→等 CAL 由硬件清零)并在 `MX_ADC1_Init()` 末尾调用;新增 `cubemx-survival` 审计项 `adc1-calibration-call` / `adc1-calibration-impl`。验证:`@build` 0 错 0 警(Code 256728→**256868**)、`@test` **PASS**(`adcp` 仍 122 µs、`adc1_err=0`、`idle=0`);**校准前后同一拍 STM32 侧 6 路同向偏移** `UB 898→961`/`UC 978→1038`/`IB 906→922`/`IC 1061→1071`/`IN 1017→1055`/`T 790→846`(+10~+63 LSB),TPAFE 侧 `UA/UN/IA` 一字不变 → 变化确来自 ADC1 校准。§4.4 的 2 ms 忙等与"超时沿用旧值"**仍未处理** | +| 2026-10-10 | **修 §4.4 的"超时拍静默沿用旧值"(正确性缺陷)**:`stm32_scan()` 两条失败支路(`HAL_ADC_Start` 失败 / 轮询超时)现都清 `s_stm32_valid` → 失败拍 `adc_input_raw()` 返 -2 → `adc_task` 走既有 `skip` 整帧跳过,不再把上一拍的值当新数据压入 20 点 DFT 窗;下一拍成功自动恢复。主机侧新增 **TC-AI-070..078**(70 → 79 用例)钉死——旧用例 TC-AI-034/039b 都是"init 后立刻失败"(`valid` 本就是 0),照不出"成功过之后再失败"这条路径。**反向验证已做**:把支路改回旧行为 → TC-AI-039b/073/074 立即 FAIL(76/79)。验证:`@build` 0 错 0 警(Code 256868→**256872**)、主机单测 **79/79**、`@test` **PASS**(本轮无超时:`adc1_err=0`、`skip=0`、`idle=0`)。**剩余未做 = 消掉 2 ms 忙等本身(DMA)** | diff --git a/test/host/README.md b/test/host/README.md index a93b2ccd..ed1fef8a 100644 --- a/test/host/README.md +++ b/test/host/README.md @@ -46,7 +46,7 @@ | `cfg_file/stub/` | 桩头文件(`dbg_log.h`),覆盖 MCU 版以在 PC 上编译 | | `phasor/run_unit_test.py` | **相量层(L2)单测**:离线编译真实 `App/algo/phasor.c` + `sample_ring.c`(**无需任何桩**,纯算法),57 用例覆盖傅氏系数表 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 双区原子交换 | | `phasor/phasor_test.c` | 测试单元本体(`#include` 两个生产源码 + 合成任意波形) | -| `adc_input/run_unit_test.py` | **ADC 输入层(L1)单测**:离线编译真实 `App/algo/adc_input.c`,70 用例给每个 Rank 注入互异码值**逐通道钉死映射**,并覆盖 HAL 启动失败 / 转换超时 / 参数错等真机难复现分支 | +| `adc_input/run_unit_test.py` | **ADC 输入层(L1)单测**:离线编译真实 `App/algo/adc_input.c`,79 用例给每个 Rank 注入互异码值**逐通道钉死映射**,并覆盖 HAL 启动失败 / 转换超时 / **失败拍陈旧帧** / 参数错等真机难复现分支 | | `adc_input/adc_input_test.c` | 测试单元本体(`#include` 生产源码 + HAL / TPAFE 可注入桩) | | `adc_input/stub/` | 桩头文件(`adc.h` / `tpafe5160.h`) | | `vol_lock/run_unit_test.py` | **卷锁单测**:离线编译真实 `App/fs/vol_lock.c`,33 断言点覆盖创建 / 获取 / 释放契约(**fail-open 不变量**:创建失败仍返回 1 且零阻塞)/ 幂等 / 超时透传 | diff --git a/test/host/adc_input/adc_input_test.c b/test/host/adc_input/adc_input_test.c index 7a431b8b..0a8cff39 100644 --- a/test/host/adc_input/adc_input_test.c +++ b/test/host/adc_input/adc_input_test.c @@ -29,7 +29,7 @@ * TC-AI-019/01A 越界(ADC_SEM_COUNT)与负数语义 -> NULL * —— map_check(TC-AI-020)—— * TC-AI-020 真实映射表自洽(反向路径结构上不可达,见文件末说明) - * —— stm32_scan(TC-AI-021..03A)—— + * —— stm32_scan(TC-AI-021..078)—— * TC-AI-021..025 全通道成功:返回 0 / 状态码 / 不计失败 / Start-Stop 各一次 / 轮询 9 次 * TC-AI-026..02B **Rank -> 通道号映射**:UB->CH14(rank9) / UC->CH3(rank4) / * IB->CH6(rank7) / IC->CH1(rank2) / IN->CH2(rank3) / TEMP->CH5(rank6) @@ -37,6 +37,11 @@ * TC-AI-030..034 HAL_ADC_Start 失败:返回 -1 / 状态码 RC_START / 计数 +1 / 不调 Stop * TC-AI-035..039 轮询超时:返回 -2 / 状态码 RC_TIMEOUT / 计数 +1 / 调 Stop 收尾 / 立即退出 * TC-AI-03A 失败计数跨次单调累计 + * TC-AI-070..078 **失败拍不得沿用上一拍旧值**(2026-10-10 补,真机实测发现的 + * 正确性缺陷):先成功一拍让 STM32 侧进入有效态,再注入超时拍 / + * Start 失败拍 -> adc_input_raw() 必须返 -2(而非上一拍的值); + * 恢复拍重新置有效。TC-AI-034/039b 只覆盖"冷启动即失败", + * 照不出"成功过之后失败 → 静默沿用旧值",故单独立一组。 * —— adc_input_raw(TC-AI-040..04B)—— * TC-AI-040..043 参数守卫:out=NULL / 越界 / 负数 -> -1,且不写 out * TC-AI-044/045 无有效数据 -> -2(两来源分别) @@ -318,6 +323,40 @@ static void tc_scan_timeout(void) { CHECK(adc_input_stm32_errs() == 2u, "TC-AI-03A 失败计数跨次单调累计 = 2"); } +/* ============ TC-AI-070..077:失败拍不得沿用上一拍旧值(陈旧帧) ============ + * 背景(2026-10-10 真机实测发现):本层只有一份 s_stm32_raw[],超时拍里"超时前 + * 几个 Rank 已写本拍新值、后面几个还是上一拍旧值",而 s_stm32_valid 此前只在 + * 成功时置 1、失败时**从不清零** → 消费侧照样取数、照样压入采样环, + * s_frames_skip 抓不到。TC-AI-034/039b 都是"init 后立刻失败"(valid 本就是 0), + * 因此照不出这条路径——必须先成功一拍。 */ + +static void tc_scan_stale(void) { + int32_t v = 0; + + /* --- 超时支路:成功拍 -> 超时拍 -> 恢复拍 --- */ + fakes_reset(); + (void)adc_input_init(); + CHECK(stm32_scan() == ADC_INPUT_RC_OK, "TC-AI-070 前置:首拍成功"); + CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-AI-071 前置:UB 取到 108(有效态)"); + + s_poll_fail_at = 4; /* 第 5 个 Rank 超时(前 4 个已写新值) */ + CHECK(stm32_scan() == ADC_INPUT_RC_TIMEOUT, "TC-AI-072 第二拍轮询超时"); + CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-073 超时拍不得沿用上一拍值 -> -2"); + CHECK(adc_input_raw(ADC_SEM_UC, &v) == -2, "TC-AI-074 超时拍连本拍已写入的 Rank 也不得出数"); + + s_poll_fail_at = -1; + CHECK(stm32_scan() == ADC_INPUT_RC_OK, "TC-AI-075 第三拍恢复成功"); + CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-AI-076 恢复拍重新可取数(自愈)"); + + /* --- 启动失败支路:同样不得沿用 --- */ + fakes_reset(); + (void)adc_input_init(); + (void)stm32_scan(); /* 先成功一拍 */ + s_start_rc = HAL_ERROR; + CHECK(stm32_scan() == ADC_INPUT_RC_START, "TC-AI-077 Start 失败拍"); + CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-078 Start 失败拍不得沿用上一拍值 -> -2"); +} + /* ===================== TC-AI-040..045:raw 参数守卫与无数据 ===================== */ static void tc_raw_guards(void) { @@ -448,6 +487,8 @@ int main(void) { tc_scan_start_fail(); printf("-- stm32 scan: poll timeout --\n"); tc_scan_timeout(); + printf("-- stm32 scan: stale frame on failure beat --\n"); + tc_scan_stale(); printf("-- raw: guards / no-data --\n"); tc_raw_guards(); printf("-- raw: tpafe index + two's complement --\n");