Files
STM32F4-Base/docs/子系统设计/ADC采样节拍设计说明.md
T
wandering1 4c74cfa825 refactor(编号·批次二): 文档侧 194 处保护功能号改成功能名,定义表 7 张首格改语义标识
批次二文档半(道 gate-docs 执行)。先纠三个数的口径账,别再拿任何旧数当准:**234 处是准数** —— 卡片写
52(Lead 用了门禁 Checker 的引用集合,它**刻意排除属主文档内部的定义位**)、道里先报 265(裸正则把
`SDL2-2.32.10` 里的 `L2-2` 当成功能号,误报 10 处)。最终边界口径 = 带左边界 + 跳过反引号代码段。

· 19 只文件 HEAD 共 234 处 → 现 40 处,**40 处 100% 落在变更记录行**(冻结历史,按 Q3 一条没动),
  非变更记录残留 **0**;改写 163 行 / 16 只文档。站点最多:规划文档 110→37、相量层单测说明 25→1、
  采集与数据处理短板评测 21→0、App/algo/README 26→0。
  (`App/algo/README.md` 由上一笔代码提交带上车,路径归属已在该笔正文注明,内容是本批改写)
· **定义表首列由编号改成语义标识**(编号规范 §3.3「表首格就是短语」的完全形态):首列原为编号的表
  共 **7 张**(不是 5 张),删掉与之重复的「语义名」列;表头标签按域词区分——三张 L3 保护**元件**表
  保留 `| 元件 |`,四张 L1/L2 表用 `| 功能名 |`(把「元件」统一洗成功能名会丢掉领域信息)。
  七张表逐张核:表头竖线数 == 分隔行 == 首条数据行 == 列数+1;与 HEAD 逐块对拍**净新增不一致块 0**,
  并且正是这个对拍抓到第一版「三张表数据行已删列、表头没删」的破表。
· 7 处删号留下的残词/重复解释括号人工收口(如「L1-5(通道标定)」→「通道标定(一次系数 + 零漂)」),
  复扫待收口站点 0;措辞逐字取自映射表,无第二种说法。
· tools/arch_diagram 三只生成器 4 处(3 处 docstring + 1 处图形文案)与产物同批:重跑后**只有位图那张
  变 2 个 text 节点的换行位置**,画布 1580x740 与总行数不变,另两张逐字节未变;改前先干跑对照证明三张
  svg 原本与生成器一致(不在 stale 基线里)——`check_generated_fresh --only docs-svg` PASS。

本笔另含 **F-* 半批的文档面**(同一轮 `git add docs tools/arch_diagram AGENTS.md` 一并收进
,非误伤):BSD
函数调用关系里 9 处反引号紧邻指针、属主文档 §7 的停发句与导读行(**12 条定义行原样保留、不删登记族**)、
以及 `gen_net_instability_svg.py` 与它的产物 `网络不稳根因总览.svg`(「唯一读者红线」那条的框宽自适应
526 → 551,右缘 1411 未越界,画布 1560x1468 不变)——该半批的**代码注释面**在下一笔。

⚠ 因 `docs/任务规划/登记簿.md` 与 `docs/任务规划/UI补齐.md` 正被另一张卡(UI 叶数复核)改动,本笔**不含
那两只文件**,批次二的 §9 存量收尾与它们同批提交。
2026-10-27 09:05:24 +08:00

24 KiB
Raw Blame History

ADC 采样节拍设计说明(TIM3 1 kHz 同源硬件节拍)

模块名称:ADC 采样节拍与消费链——TIM3 节拍源(Src/tim.c)、更新事件回调(Src/main.c)、 L1 采集层(App/algo/adc_input.c)、消费者 adcTask(App/task/adc_task.c)。 文档定位:现状设计说明——这条链当前由谁产生节拍、同一个硬件事件如何驱动两片转换器、 STM32 侧怎么搬运、消费者怎么取数、为什么这样配置、边界与代价在哪里。历史演进、方案对比、 门禁与台架复测记录、修订日志一律不写进本文(需要时查 git 历史)。 状态:已落地并经真机验证;下游 = L2 相量 / 测频 → L3 保护判据(docs/算法/采集与算法层设计说明.md)。 相关文档: docs/硬件平台/ADC通道映射说明.md(网名 ↔ 板级 ADCn ↔ 转换器通道号,唯一权威) docs/算法/继电保护功能复现规划.md §3.1「周期 CONVST 重触发」 Drivers/BSP/TPAFE5160/README.md(驱动契约:BUSY / FRSTDATA / 原子快照) docs/子系统设计/看门狗设计说明.md §11.4([PROF] 字段语义与耗时基线)

⚠ 网名 AINn ≠ 板级 ADCn ≠ 转换器通道号(17 路里 13 路置换)→ 取数必须查通道映射文档。

目录


1. 节拍链路总览

一句话链:TIM3 每 1 ms 产生一个更新事件,同一个事件同时驱动两片转换器,并唤醒唯一的消费者任务。

flowchart TD
    T["TIM3 更新事件(1 kHz)"]
    T --> ISR["TIM3 更新中断(抢占优先级 6)"]
    T --> TRGO["TRGO_UPDATE → ADC1 外部触发(上升沿)"]

    ISR --> CONV["PC9 CONVST 脉冲"]
    ISR --> WAKE["adc_tick_irq_notify()<br/>就绪门 + 置 ADC_WAKE_FLAG"]

    CONV --> BUSY["TPAFE 转换 → BUSY 拉低"]
    BUSY --> EXTI["PC7 EXTI(优先级 6)读 8 通道<br/>帧缓冲 + seq++"]

    TRGO --> SCAN["ADC1 一轮 9 通道"]
    SCAN --> DMA["DMA2_Stream4 循环搬运<br/>乒乓双缓冲 2×9 半字"]

    WAKE --> TASK["adcTask(AboveNormal)被唤醒即抢占"]
    EXTI --> POLL["adc_input_poll():TPAFE 原子快照"]
    DMA --> TAKE["stm32_take():取上一拍完成的半区"]
    TASK --> POLL
    TASK --> TAKE
    POLL --> L2["L2 相量 / 测频 → 采样环<br/>仅确有新帧时压环"]
    TAKE --> L2
    L2 --> OUT["下游:L3 保护判据(protectTask)"]
    L2 --> HB["task_wd_beat(TASK_WD_ADC) + 10 s 心跳"]

关键性质:

  • 一个更新事件驱动两片转换器:TPAFE 的 CONVST 由 ISR 内 GPIO 脉冲给出,STM32 ADC1 由同一事件 的 TRGO 硬件触发 → 两者的相对相位固定,与软件调度无关。
  • 节拍精度是硬件级:84 MHz ÷ (PSC+1) ÷ (ARR+1),与 FreeRTOS tick(TIM7)无关;硬件自动重载, 任务被长时抢占不漏拍。
  • 节拍与唤醒同源:消费者由同一个更新事件的 ISR 唤醒 → 改采样率时消费节拍自动跟随(§10)。
  • 消费者是纯消费者:不触发转换、不自旋等转换、不做保护运算;只做"取一拍 → 压环 → 算相量 → 心跳"。
  • 零新任务、零动态分配;这条链新增的静态 RAM 只有 s_stm32_dma 36 B(§9)。

⚠ 同一事件驱动 ≠ 8 路电气量同刻采样:两片转换器之间没有共同采样触发点(§10 第一条)。


2. 实现落点

位置 当前职责 改动边界
MDK-ARM/STM32F407-Demo.ioc TIM3 使能与参数(PSC / ARR / TRGO / NVIC 6)、ADC1 触发源与 DMA 绑定、DMA2_Stream4 改这里,重生成才不丢
Src/tim.c / Inc/tim.h htim3、MX_TIM3_Init()、HAL_TIM_Base_MspInit()(TIM3_IRQn 优先级 6) CubeMX 生成,勿手改
Src/stm32f4xx_it.c TIM3_IRQHandler() → HAL_TIM_IRQHandler(&htim3) 生成
Src/main.c 的 HAL_TIM_PeriodElapsedCallback() TIM3 分支:CONVST 脉冲 + adc_tick_irq_notify() 只写 USER CODE 区(§3.5)
App/app/app_main.c(步骤 6) 唯一的节拍启动点 HAL_TIM_Base_Start_IT(&htim3) 手写;顺序约束见 §3.4
Src/adc.c ADC1 触发与 DMA 参数 + adc1_calibrate() 参数在生成区;自校准在 USER CODE 区(§4.2)
Src/dma.c DMA2_Stream4 参数与 NVIC 优先级 生成区;改流后必回本页核对(§4.3)
App/algo/adc_input.c L1:adc_input_poll() / adc_input_raw()、半区推导、DMA 完成回调、四类故障计数 手写
App/task/adc_task.c / adc_task.h 消费者主循环、adc_tick_irq_notify() 与就绪门、10 s 心跳统计 手写
Src/freertos.c adcTask 栈 640 words、优先级 osPriorityAboveNormal 手写于 USER CODE 区(不在 .ioc);改栈前后各跑一次 stack_report.py
App/task/protect_task.c 下游消费者:20 ms 判据节拍取 L2 相量快照 → docs/算法/采集与算法层设计说明.md §9

3. 节拍源:TIM3 配置与启动

3.1 当前配置

项 值 依据 / 后果
计数时钟 84 MHz(APB1 定时器时钟 = 42 MHz × 2) SYSCLK 168 MHz / HCLK 168 MHz / APB1 ÷4
Prescaler (PSC) 83 → 1 MHz 计数,1 µs 分辨率
Counter Period (ARR) 999 → 1.000 kHz(1000 µs 周期)
Counter Mode Up —
Clock Division No Division 与采样无关
auto-reload preload Enable 运行期写 ARR 在下一个更新事件生效 → 改频无毛刺(§10)
Trigger Event Selection Update(TRGO = 更新事件) 硬件触发 ADC1(§4.1)
Master-Slave Mode MSM = ENABLE(从模式 SMS = 0 内部时钟) 软件 CEN 启动不受影响(已核对 RM0090 与生成代码)
NVIC TIM3_IRQn 抢占 6 / 子优先级 0 理由见 §3.3
ADC1 触发 ADC_EXTERNALTRIGCONV_T3_TRGO,边沿 RISING §4.1

MX_TIM3_Init() 只配置不启动;HAL_TIM_Base_Start_IT(&htim3) 是唯一启动点(§3.4)。

3.2 为什么是 TIM3(不是 TIM6 / TIM7)

F4 上 ADC1 的外部触发清单里没有 TIM6 / TIM7 的 TRGO——它们的 TRGO 只接 DAC。ADC1 的可用外部触发 (RM0090 的 EXTSEL 表、本仓 stm32f4xx_hal_adc.h、CubeMX 器件库 db/mcu/IP/ADC-STM32F417_Modes.xml):

T1_CC1/2/3、T2_CC2/3/4、T2_TRGO、T3_CC1、T3_TRGO、T4_CC4、T5_CC1/2/3、T8_CC1、T8_TRGO、EXTI15

想让同一个硬件事件同时驱动两片转换器,TRGO 型候选只剩 TIM2 / TIM3 / TIM4 / TIM5 / TIM8; TIM7 已被 HAL 时基占用,TIM3 空闲且同为 APB1 通用定时器 → 用 TIM3。PSC / ARR 与 HAL 时基 TIM7 同构(84 MHz / 1 MHz / 1000 计数),对照排查时两边可直接互参。

换节拍源前先核三条:① 该定时器的 TRGO 在 F4 上确实能触发 ADC1;② 定时器空闲(TIM7 = HAL 时基); ③ 是否仍在 APB1 时钟域(否则 PSC / ARR 要按新时钟重算)。

3.3 优先级为什么是 6

  • 与 TPAFE BUSY 的 EXTI(EXTI9_5_IRQn)同抢占优先级 → 互不抢占:读帧的 8 次 RD 翻转不会 被打断,避免"读到一半被新转换刷新数据总线"。
  • ≥ configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY(= 5)→ ISR 内允许调内核 API(osThreadFlagsSet() 实为 xTaskNotifyFromISR() + portYIELD_FROM_ISR(),会立即触发上下文切换)。
  • 5 是 USART、7 是 SPI / DMA 等 → 被更高优先级 ISR 挡住的时长以 µs 计(§7)。
  • DMA2_Stream4_IRQn 同为 6(Src/dma.c)。

3.4 启动位置与顺序

  1. MX_TIM3_Init() 由 main() 在 app_main_init() 之前调用(生成代码,只配置不计数)。
  2. HAL_TIM_Base_Start_IT(&htim3) 必须排在 tpafe5160_init() 成功之后(app_main.c 步骤 6): 否则 ISR 会在驱动首帧有效性自检期间乱发 CONVST,使自检结果不可信。
  3. 节拍启动仍早于内核:osKernelInitialize() / MX_FREERTOS_Init() / osKernelStart() 都在其后 → "节拍已在跑"与"消费者已存在"之间有约 100 ms 窗口,处置见 §6.1。

3.5 共用回调与生成区落点

HAL_TIM_PeriodElapsedCallback() 被 TIM7 与 TIM3 共用:TIM7 分支是 HAL 时基(HAL_IncTick())与 看门狗时基(task_wd_tick_probe()),TIM3 分支是采样节拍。三条分支并列,不得合并、不得删改 TIM7 那两条(改坏不报编译错,只在时钟与看门狗上出事)。TIM3 分支写在 USER CODE 保护区; 重生成存活方法论与审计脚本 scripts/check_cubemx_survival.py → docs/子系统设计/看门狗设计说明.md §10。


4. STM32 侧:ADC1 触发与 DMA 乒乓双缓冲

4.1 ADC1 当前配置

项 值 后果
ScanConvMode ENABLE 一轮 9 通道
ContinuousConvMode DISABLE 每轮由外部触发启动(§7)
ExternalTrigConv T3_TRGO,ExternalTrigConvEdge = RISING 与 CONVST 同一个更新事件
NbrOfConversion 9 通道清单与语义 → docs/硬件平台/ADC通道映射说明.md
DMAContinuousRequests ENABLE(DDS 位) ⚠ 关掉 = 一轮 9 通道只发一次 DMA 请求 → 静默丢 8 路,不报任何错

4.2 上电自校准

F4 的 ADC 上电必须自校准:缺它不报任何错,只是读数带失调。CubeMX 不生成这一步,且本仓库裁剪版 HAL 没有 HAL_ADCEx_Calibration_Start()、裁剪版 CMSIS 头没有 ADC_CR2_CAL(位值 3 在源内自定义) → adc1_calibrate() 手写于 Src/adc.c 的 USER CODE 区,在 MX_ADC1_Init() 末尾调用。 时序按 RM0090 §13.3.6:清 ADON → 等硬件清零 → 置 ADON 上电待稳定 → 置 CAL → 等硬件自动清 CAL(上限 10 ms)。存活由审计项 adc1-calibration-call / adc1-calibration-impl 兜底。

4.3 DMA 选型与四条硬约束

项 当前值 为什么 / 违反的代价
流 DMA2_Stream4 ADC1 在 DMA2 只挂 Stream0 或 Stream4(Stream1 = ADC3、Stream2/3 = ADC2)。取 Stream4 是为了不碰 SPI1_RX / USART1_RX 所在的流(NAND 与调试串口的 DMA)——让 CubeMX 自由分配会优先抢 Stream0,把这两路连锁搬家
NVIC 优先级 6 ⚠ 换流会把该流的 NVIC 优先级重置为 0,0 抢占 FreeRTOS 临界区(上限 5)→ 任何改流之后必须回 Src/dma.c 核对优先级
DMAContinuousRequests ENABLE 见 §4.1:违反时不报错,只是静默丢通道
缓冲地址 s_stm32_dma[18](uint16_t)落在 RW_IRAM1 ⚠ ADC1 走 DMA2,而 F4 的 DMA2 不可达 CCM(0x10000000)→ 放错不报编译错也不报运行错,数据永远搬不回来

模式:DMA_CIRCULAR + DMA_PRIORITY_LOW + halfword→halfword,长度 2 × 9 = 18(两个 9 通道半区)。

4.4 半区推导(无竞态写法)

半传输与整传输都代表"一整帧就绪",两个回调共用同一个自增计数(HAL_ADC_ConvHalfCpltCallback() / HAL_ADC_ConvCpltCallback(),均带 hadc == &hadc1 句柄守卫);消费侧 stm32_take() 只读一个计数:

uint32_t       seq  = s_dma_seq;                  /* 单次 32 位读:与 ISR 自增不产生撕裂 */
uint8_t        half = (uint8_t)(1u - (seq & 1u));  /* 奇 = 刚完成前半区,偶 = 刚完成后半区 */
const uint16_t *src = &s_stm32_dma[half * ADC_INPUT_STM32_CH_NUM];
  • 判据只读一个 32 位量:不另存"当前半区"变量——否则会出现"读到新 half、读到旧 seq"的 组合窗口。
  • 代价:STM32 侧数据固定滞后一拍(1 ms)。两片转换器本就无同刻性(§10)→ 不新增误差。
  • 失败拍不沿用旧值:seq 未前进(本拍无新帧)或 HAL_ADC_Start_DMA() 挂载失败 → 清 s_stm32_valid → 本拍 STM32 侧判为无效,消费者整帧跳过(skip 计数,§8),下一拍成功自动恢复。
  • 取帧状态码:ADC_INPUT_RC_OK = 0 / ADC_INPUT_RC_START = -1 挂载失败 / ADC_INPUT_RC_STALL = -2 本拍无新帧 / ADC_INPUT_RC_NOT_RUN = -3 尚未取过帧。

5. TPAFE 侧:CONVST 脉冲与 BUSY 读帧

  • PC9 CONVST 是 GPIO_Output(不是定时器复用引脚),脉冲由 TIM3 ISR 内两次 GPIO 写产生 → 引脚配置、驱动 tpafe5160_init() 与首帧自检都不受影响。
  • 转换约 23 µs → PC7 BUSY 拉低 → EXTI9_5_IRQn(优先级 6)内读 8 通道(≈0.5 µs)→ 写帧缓冲、 seq++。转换与读帧时序的契约在 Drivers/BSP/TPAFE5160/README.md。
  • 取数只用原子快照:tpafe5160_snapshot()(临界区,仅任务上下文)。旧的 get_buf() / data_ready() / clear_ready() 已从驱动删除——两步之间被抢占会拷回两帧混合数据。
  • 消费者一律经 adc_input_poll() / adc_input_raw() 按语义取数,不得写死缓冲区下标 (buf[2]=IA 一类映射只写在 docs/硬件平台/ADC通道映射说明.md)。
  • frst_err(tpafe5160_frstdata_err())是"FRSTDATA 无跳变"的帧计数——它是"每个周期 BUSY EXTI 都真的在读帧"的直接观测面。

6. 消费者 adcTask:事件驱动唤醒

6.1 就绪门(ISR 侧的唯一入口)

TIM3 的节拍远早于调度器(§3.4):这约 100 ms 窗口里 adcTaskHandle 还是 NULL、调度器也还没起。

  • ISR 只调 adc_tick_irq_notify(),不得在回调里直接 osThreadFlagsSet()。前者内含就绪门 s_consumer_ready——由 adcTask 自己在进入等待前置 1(单写者 = 任务,ISR 只读)。
  • 就绪门把时序约束写成显式代码,不依赖 FreeRTOS 对 NULL 句柄的内部宽容——"句柄已赋值、 调度器未启动"这一段仍会让 ISR 版内核 API 跑在调度器之外(内核启动前不得调用内核 API 是平台级 约束,见 AGENTS.md「RTOS 与内存」)。
  • 置位与等待之间不漏唤醒:线程标志粘滞,开闸(s_consumer_ready = 1u)放在进入循环之前。
  • 把"定时轮询"换成"事件驱动"时,必须单独核对节拍源启动时刻与消费者创建时刻——否则第一次上电 就会踩到"ISR 对还不存在的消费者动手"。同类约束 → docs/子系统设计/看门狗设计说明.md §11.5。

6.2 等待语义与优先级

项 当前实现 说明
等待 osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever) 无超时 = 刻意的 fail-safe(§6.3)。ADC_WAKE_FLAG 是命令位,等待即消费,不加 osFlagsNoClear(那是就绪屏障位的规则)
优先级 osPriorityAboveNormal 唤醒后立即抢占其余 Normal / BelowNormal 任务 → "唤醒 → 开始处理"的延迟固定在 µs 级,无 tick 量化、无累积漂移;代价见 §9、§10
超拍合并 多个 TIM3 脉冲合并成一次唤醒 下一轮 adc_input_poll() 返 r == 0 → 记 idle,不重复压环(只有 r == 1u 才压环)→ 20 点 DFT 窗不会被上一拍的重复值污染
看门狗 每拍 task_wd_beat(TASK_WD_ADC);required_mask 与心跳位集合一致 提优先级不改掩码
本任务不做 不触发转换、不 osDelay 轮询、不做保护运算 判据在 App/algo/prot_elm.c,消费者是 protectTask(20 ms 节拍)

6.3 节拍缺失是刻意的 fail-safe

TIM3 未启动或停摆 → ADC1 不再被触发、adcTask 再无唤醒 → 任务永久阻塞 → TASK_WD_ADC 缺席 → IWDG 复位。设计如此:节拍源缺失不允许被静默放过,不得为了"少一次复位"给等待加超时。


7. 单周期时序与中断交互

t = 0        TIM3 更新事件 → ISR:PC9 CONVST 上升沿脉冲 + adc_tick_irq_notify()
             同一更新事件经 TRGO 启动 ADC1 一轮 9 通道(DMA 逐通道搬运)
t ≈ 0+ε      adcTask 被唤醒(AboveNormal → 立即抢占)→ adc_input_poll()
t ≈ 23 µs    BUSY 拉低 → EXTI 读 8 通道(≈0.5 µs)→ 帧缓冲、seq++
t ≈ 67 µs    第 9 个通道搬完 → 半传输 / 整传输回调 s_dma_seq++
t = 1000 µs  下一个更新事件
交互 现状 判据
CONVST ↔ 读帧 读窗口与下一次 CONVST 相隔约 977 µs,互不重叠 转换只占约 23 µs
TIM3 ↔ BUSY EXTI 同抢占优先级 6 → 互不抢占 读 RD 时序不被打断(§3.3)
TIM3 ↔ 更高优先级 ISR USART(优先级 5)可挡住节拍 ISR,时长 µs 级 触发抖动占 1000 µs 的 < 0.5%
ISR 本体 两次 GPIO 写 + 一次线程标志置位 ISR 内不做任何采集与算法计算(§11 第一条)
数据就绪时刻 TPAFE 侧本拍 ISR 后约 23 µs;STM32 侧取上一拍已完成的半区 §4.4;因此 adc1_seq 与 seq 之间是一个恒定小偏移(§8)

8. 可观测性:心跳计数与判读

每 10 s 一条 [ADC_TASK] 心跳(ADC_TASK_REPORT_MS = 10000):

字段 来源 正常态 异常判读
frames adc_input_poll() == 1 的拍数 与 seq 同速 增速低于 seq → 消费者被挡或超拍
idle 无新帧拍数(r == 0) 稳态不增长 持续增长 → BUSY EXTI 未在读帧,或节拍源停摆
skip 无有效帧整帧跳过(STM32 侧失败拍,§4.4) 极少 持续涨 → 看 adc1_err / adc1_hw
seq tpafe5160_frame_seq() 每 10 s +10000(= 1000.0 Hz) 不涨 → TIM3 未启动或转换未触发
frst_err tpafe5160_frstdata_err() 0 > 0 → 读帧与转换未对齐
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 每次前进的超出部分) = 丢掉的帧点数(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()(「采样有效性校验」) 极少 TPAFE 侧丢帧,与 adc1_gap 同单位(帧点数),可直接对比

单拍耗时(adcp = TASK_PROF_ADC_POLL)与整拍耗时(adcm = TASK_PROF_ADC_BEAT)不在心跳里, 由 DWT 探针的 [PROF] 行给出,字段语义与基线 → docs/子系统设计/看门狗设计说明.md §11.4。

为什么"连续 ≥ 2 拍无新帧"才计 adc1_err(ADC_INPUT_STALL_RUN_MAX = 2):任务在 TIM3 触发瞬间 被唤醒,而该拍的 DMA 完成约落在 67 µs 之后;若上一拍被心跳日志或抢占拖过这 67 µs,本拍就会看到 "计数没前进"(那个完成已被上一拍消费掉)。这种相位伪影只会连续 1 拍——DMA 若真停,第 2 拍仍然 不会前进。故单拍无新帧不计故障,只记 ADC_INPUT_RC_STALL。


9. 性能与资源特征

指标 现状 来源 / 条件
节拍频率 1.000 kHz,硬件自动重载 84 MHz / 84 / 1000;心跳 seq 每 10 s +10000
单拍取数 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 + 相位伪影
静态 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「平台约束」
ISR 负担 1 kHz 一次,两次 GPIO 写 + 一次标志置位 §7

心跳拍的连带效应:adc1_gap 的根治手段是缩短心跳(每 10 s 一次的多行日志),不是提高 DMA 能力。 日志成本模型 → AGENTS.md「日志与工具链」。


10. 设计边界与已知代价

项 现状 说明 / 出口
8 路电气量分属两片转换器 跨转换器没有共同采样触发点 → 无同一时刻性、相位不确定 V0.1 硬件缺陷(登记簿条目「双转换器无共同采样触发」):V0.1 上禁止跨转换器做相位 / 功率 / 功率因数 / 矢量运算;根治要改板(8 路全接 TPAFE)
STM32 侧数据滞后 固定 1 拍(1 ms) 乒乓半区的结构代价;因上一条,不新增误差(§4.4)
TPAFE 侧仍是"脉冲 + 中断读" ISR 内 GPIO 脉冲、BUSY EXTI 逐通道读 8 次,未 DMA 化 单点成本约 0.5 µs,非当前瓶颈;DMA / FMC 化的前提未定:接口类型与 DRDY、读时序须先用逻辑分析仪确认
等周波调频 硬件侧已具备:1 µs 分辨率 + ARR 预装载 + 唤醒与生产同源 → 运行期 __HAL_TIM_SET_AUTORELOAD(&htim3, 周期µs - 1) 即可无毛刺改频,消费节拍自动跟随 采样率随动到相量窗与定值不在本层;测频见 App/algo/freq.c,上位需求见 docs/算法/继电保护功能复现规划.md §3.1
通道有效性 V0.1 只有部分通道测量有效 唯一权威 → docs/硬件平台/ADC通道映射说明.md
AboveNormal 的挤占面 单拍超过 1 ms 期间抢占 net / ftp / default / timeSync / ui 观测面 = adcm 与 adc1_gap;诊断方法与基线 → docs/子系统设计/看门狗设计说明.md §11

11. 设计铁律(改这条链前必读)

  1. ISR 只发唤醒,不做计算。相量、压环、保护判据全部留在任务上下文:忙等与算法不可入 ISR、 任务级共享数据不能在 ISR 里改写、HAL_TIM_PeriodElapsedCallback() 是生成区回调承载的业务代码。
  2. ISR 侧唤醒只能走 adc_tick_irq_notify()(就绪门,§6.1);直接 osThreadFlagsSet() 会在内核 启动前把调度器外代码跑进 ISR。
  3. 一个更新事件驱动两片转换器。另起定时器分别触发 = 重新引入跨转换器的相位不确定性(§10)。
  4. 取数只用 adc_input_poll() / adc_input_raw() / tpafe5160_snapshot(),按语义不按下标; 不得自行拼"两步取数"的等价物(§5)。
  5. 改节拍源、改 DMA 流、动 DMAContinuousRequests 之前先读 §3.2 与 §4.3——这几类改动的共同点是 违反时不报任何错,只在数据上出事(静默丢通道、搬不回来、优先级被重置为 0)。
  6. 节拍源停摆必须导致复位(§6.3):不得给等待加超时来"绕过"看门狗。
  7. 动栈或加深调用链前后各跑一次 scripts/stack_report.py,新增缓冲前跑 scripts/ram_report.py(§9)。