adc: 节拍源 TIM6->TIM3 + ADC1 走 DMA 乒乓双缓冲,消除 2 ms 忙等(P1-5 单拍硬上界办结)

用户选定最彻底方案(B2):DMA + 定时器 TRGO 硬件触发 + 消费上一拍
已完成的半区。对应 P1-5 拆出的"单拍硬上界",现已办结。

硬件事实更正(查 CubeMX 器件库 + 本仓 HAL 头证实):F4 上 ADC1 的
外部触发清单里没有 TIM6/TIM7 TRGO(只接 DAC)。故 1 kHz 节拍源由
TIM6 改为 TIM3(PSC 83 / ARR 999,TRGO=UPDATE),同一个更新事件
同时 ① ISR 脉冲 TPAFE CONVST ② 经 TRGO 触发 ADC1 一轮 9 通道
③ 置位 ADC_WAKE_FLAG 唤醒 adcTask。TIM6 从 .ioc 整体移除。

代码:
- Src/tim.c/h:新增 MX_TIM3_Init(含 MasterSlaveMode),删 htim6
- Src/adc.c:ExternalTrigConv=T3_TRGO + EDGE_RISING +
  DMAContinuousRequests=ENABLE;MspInit 挂 hdma_adc1(DMA2_Stream4
  循环模式)。DDS 位关掉 = 一轮只发一次请求,静默丢 8 路
- Src/dma.c:DMA2_Stream4 NVIC 优先级 6(CubeMX 换流会把它重置为 0,
  优先级 0 会抢占 FreeRTOS 临界区,违反"外设中断 ≥5")
- Src/main.c:周期回调加 TIM3 分支;stm32f4xx_it.c 向量同步
- App/algo/adc_input.c:stm32_scan() → stm32_take(),逐通道
  PollForConversion 全删;乒乓半区由完成计数奇偶推出(只读一个
  32 位量 → 与 ISR 无竞态);STM32 侧数据固定滞后 1 拍
- App/task/adc_task.c:心跳日志拆出四类计数

观测性:adc1_seq(完成计数)/ adc1_gap(seq 前进 >1 = 丢帧)/
adc1_err(连续 ≥2 拍无新帧 = 停摆)/ adc1_hw(HAL 错误回调)。
单拍无新帧是消费者相位伪影(DMA 完成落在唤醒后 ≈67 µs),不计故障。

验证:
- 主机单测 79 → 101 全绿;反向验证四连均咬(半区奇偶取反 11 条、
  停摆拍置 valid 4 条、STALL_RUN_MAX 改 1 为 6 条、gap 阈值 1 条)
- @build 0 错 0 警,Code 256872 → 257660
- @test PASS:adcp 122 µs → 8 µs、adc1_err=0、adc1_hw=0、
  adc1_gap=27/20002 拍、adc1_seq 与 seq 恒定偏移 2
- stack_report 7/7 PASS,adcTask 832 B / 余 59%(不变)
- cubemx-survival 新增 5 项审计(TIM3 分支 / T3_TRGO / DDS /
  DMA 挂载 / 流优先级),28 通过 0 失败

文档:ADC 采样节拍设计说明新增 §11;看门狗停摆分析 §4.4 与 §8 收口;
算法规划 L1-1/L1-3、通道映射、测试说明、各 README 同步。
This commit is contained in:
2026-10-11 00:31:33 +08:00
parent dd8210867a
commit 9bbe117f97
32 changed files with 958 additions and 430 deletions
+49
View File
@@ -307,6 +307,55 @@ CHECKS = [
"`USER CODE BEGIN 1` 区。本仓库 HAL 无 HAL_ADCEx_Calibration_Start、"
"CMSIS 头无 ADC_CR2_CAL,两者都是裁剪造成的,勿以为『标准 HAL 里有』。",
),
# ---------------- ADC 采样节拍 + DMA(2026-10-10 v2.8) ----------------
# 节拍源 TIM3:同一个更新事件 ① ISR 脉冲 TPAFE CONVST ② 经 TRGO 触发 ADC1
# ③ 置位 ADC_WAKE_FLAG。丢了 ② 不报编译错,只是两片转换器不再同源(相位差自由漂移)。
dict(
id="adc-tick-tim3-branch",
kind="protected",
path="Src/main.c",
region="Callback 1",
must_contain=["TIM3", "tpafe5160_start_conv_irq()", "adc_tick_irq_notify()"],
hint="HAL_TIM_PeriodElapsedCallback 的 TIM3 分支必须同时做三件事:判 TIM3、脉冲 CONVST、"
"通知消费者。⚠ 节拍源**不能**换回 TIM6/TIM7——F4 上 ADC1 的外部触发清单里没有 "
"TIM6/TIM7 TRGO(只接 DAC),TIM3 的 TRGO 才是 ADC1 能收到的那个。",
),
dict(
id="ioc-adc-trig-t3trgo",
kind="ioc",
key="ADC1.ExternalTrigConv",
want="ADC_EXTERNALTRIGCONV_T3_TRGO",
hint="ADC1 外部触发必须是 T3_TRGO(与节拍源 TIM3 配对)。改回软件触发 = 每拍由任务启动"
"转换,DMA 与 TPAFE 不再同源对齐,且重新引入忙等。",
severity="FAIL",
),
dict(
id="ioc-adc-dma-continuous",
kind="ioc",
key="ADC1.DMAContinuousRequests",
want="ENABLE",
hint="DMAContinuousRequests(DDS 位)必须 ENABLE:关掉后一轮 9 通道只发**一次** DMA 请求"
"→ 静默丢 8 路,不报任何编译/运行错误。",
severity="FAIL",
),
dict(
id="adc1-dma-link",
kind="generated",
path="Src/adc.c",
must_contain=["hdma_adc1", "__HAL_LINKDMA", "DMA2_Stream4", "DMA_CIRCULAR"],
hint="ADC1 的 DMA 挂载(Stream4 + 循环模式 + __HAL_LINKDMA)须在位。"
"ADC1 在 DMA2 上只挂 Stream0 或 Stream4;选 Stream0 会连锁搬走 SPI1_RX/USART1_RX。",
),
dict(
id="adc1-dma-irq-priority",
kind="generated",
path="Src/dma.c",
must_contain=["HAL_NVIC_SetPriority(DMA2_Stream4_IRQn, 6, 0)"],
hint="⚠ CubeMX 在**换 DMA 流**时会把该流的 NVIC 优先级静默重置为 **0**(实测两轮)。"
"优先级 0 高于 configMAX_SYSCALL_INTERRUPT_PRIORITY(5) → 会抢占 FreeRTOS 临界区,"
"违反本仓『外设中断 ≥5』。每次改 DMA 流后必须回 NVIC 页核对。",
severity="FAIL",
),
]
+1 -1
View File
@@ -75,7 +75,7 @@ ROLE = {
"testTask": "硬件测试入口",
"uiTask": "LVGL UI 界面",
"timeSyncTask": "墙钟时间同步",
"adcTask": "TPAFE5160 采样(TIM6 1kHz 事件驱动 + 原子快照)",
"adcTask": "TPAFE5160 采样(TIM3 1kHz 事件驱动 + 原子快照 + STM32 ADC DMA)",
}