批次二文档半(道 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 存量收尾与它们同批提交。
24 KiB
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. 节拍链路总览
- 2. 实现落点
- 3. 节拍源:TIM3 配置与启动
- 4. STM32 侧:ADC1 触发与 DMA 乒乓双缓冲
- 5. TPAFE 侧:CONVST 脉冲与 BUSY 读帧
- 6. 消费者 adcTask:事件驱动唤醒
- 7. 单周期时序与中断交互
- 8. 可观测性:心跳计数与判读
- 9. 性能与资源特征
- 10. 设计边界与已知代价
- 11. 设计铁律(改这条链前必读)
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_dma36 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 启动位置与顺序
MX_TIM3_Init()由main()在app_main_init()之前调用(生成代码,只配置不计数)。HAL_TIM_Base_Start_IT(&htim3)必须排在tpafe5160_init()成功之后(app_main.c步骤 6): 否则 ISR 会在驱动首帧有效性自检期间乱发 CONVST,使自检结果不可信。- 节拍启动仍早于内核:
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无跳变"的帧计数——它是"每个周期BUSYEXTI 都真的在读帧"的直接观测面。
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. 设计铁律(改这条链前必读)
- ISR 只发唤醒,不做计算。相量、压环、保护判据全部留在任务上下文:忙等与算法不可入 ISR、
任务级共享数据不能在 ISR 里改写、
HAL_TIM_PeriodElapsedCallback()是生成区回调承载的业务代码。 - ISR 侧唤醒只能走
adc_tick_irq_notify()(就绪门,§6.1);直接osThreadFlagsSet()会在内核 启动前把调度器外代码跑进 ISR。 - 一个更新事件驱动两片转换器。另起定时器分别触发 = 重新引入跨转换器的相位不确定性(§10)。
- 取数只用
adc_input_poll()/adc_input_raw()/tpafe5160_snapshot(),按语义不按下标; 不得自行拼"两步取数"的等价物(§5)。 - 改节拍源、改 DMA 流、动
DMAContinuousRequests之前先读 §3.2 与 §4.3——这几类改动的共同点是 违反时不报任何错,只在数据上出事(静默丢通道、搬不回来、优先级被重置为 0)。 - 节拍源停摆必须导致复位(§6.3):不得给等待加超时来"绕过"看门狗。
- 动栈或加深调用链前后各跑一次
scripts/stack_report.py,新增缓冲前跑scripts/ram_report.py(§9)。