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
+1 -1
View File
@@ -10,7 +10,7 @@
| `default_task.c/h` | defaultTask | Normal | FTL + FatFS 挂载、配置引擎属主、syslog 唯一写者、LED 指示 |
| `net_task.c/h` | netTask | Normal | `net_stack_bring_up` → 置 NET_READY → 循环 `net_poll` + `net_process_messages` |
| `ui_task.c/h` | uiTask | BelowNormal | LVGL 界面(菜单 / 内容页 / 待机)+ 推流 |
| `adc_task.c/h` | (ADC 任务) | — | ADC 采集消费:TPAFE5160 快照 + STM32 ADC1 顺序扫描,经 [`../algo/adc_input.c`](../algo/README.md) **按语义**取数(采样节拍由 TIM6 硬件触发) |
| `adc_task.c/h` | adcTask | AboveNormal | ADC 采集消费:TPAFE5160 快照 + 消费 STM32 ADC1 DMA 乒乓缓冲一帧,经 [`../algo/adc_input.c`](../algo/README.md) **按语义**取数(采样节拍由 TIM3 硬件触发) |
| `rs485_task.c/h` | (RS-485 任务) | — | 半双工 RS-485 协议通信 |
| `../sync/time_sync.c` | timeSyncTask | Normal | 墙钟时间同步(实现不在本目录,见 [`../sync/README.md`](../sync/README.md)) |
+45 -27
View File
@@ -1,17 +1,17 @@
/*
* 模块名称:ADC Sampling Task
* 模块功能:ADC 采集消费 + L2 相量计算——TPAFE5160 快照 + STM32 ADC1 顺序扫描
* 模块功能:ADC 采集消费 + L2 相量计算——TPAFE5160 快照 + STM32 ADC1 DMA 乒乓帧消费
* (经 L1 层 App/algo/adc_input.c 按语义取数)→ 压入采样环
* (App/algo/sample_ring.c)→ 全周傅氏相量重算(App/algo/phasor.c)
* + 帧统计(节拍由 TIM6 硬件定时器提供)
* + 帧统计(节拍由 TIM3 硬件定时器提供)
* 状态说明:**B1 已落地(2026-10-10)**——本任务从空桩转为真实消费者:
* 用 tpafe5160_snapshot() 原子取数(AGENTS 铁律),统计帧数/无新帧次数,
* 并纳入看门狗 required_mask(TASK_WD_ADC)。
* **采样节拍(v2.2,2026-10-10)**:由 **TIM6 更新中断(1 kHz)** 硬件触发——
* **采样节拍(v2.2,2026-10-10)**:由 **TIM3 更新中断(1 kHz)** 硬件触发——
* ISR 内调 tpafe5160_start_conv_irq()(Src/main.c 的
* HAL_TIM_PeriodElapsedCallback);本任务**不再自行触发**,退化为**纯消费者**。
* **消费唤醒(v2.7,2026-10-10,Task #32 P1-5 方案 a)**:由 1 ms `osDelayUntil`
* 轮询改为 **TIM6 ISR 置位 `ADC_WAKE_FLAG` 的事件驱动**——唤醒与生产同源锁定,
* 轮询改为 **TIM3 ISR 置位 `ADC_WAKE_FLAG` 的事件驱动**——唤醒与生产同源锁定,
* 消除 0~1 ms 的 tick 量化相位抖动与"超拍永久落后 → 空转"风险;本任务优先级
* 同步由 Normal 提到 **AboveNormal**(唤醒即抢占)。详见
* docs/子系统设计/ADC采样节拍设计说明.md §10。
@@ -35,7 +35,7 @@
* 为真后落日志。写者未注册时控制台走无锁直发,与
* defaultTask 的存储初始化日志字节级交错(台架实测);
* 就绪前挂起上报,就绪后补报最近一帧。
* v2.2 2026-10-10 王建锋 采样节拍改由 TIM6 更新中断(1 kHz)硬件触发;本任务
* v2.2 2026-10-10 王建锋 采样节拍改由 TIM3 更新中断(1 kHz)硬件触发;本任务
* 删除自行触发,退化为纯消费者(见文件头"采样节拍")。
* v2.3 2026-10-10 王建锋 L1-3/L1-4:取数改走 App/algo/adc_input.c(语义映射 +
* STM32 ADC1 9 通道扫描);首帧日志补全 8 路电气量 + 温度,
@@ -59,12 +59,17 @@
* (原写法会永久落后 → 不再阻塞 → 空转抢占 uiTask)
* v2.7 2026-10-10 王建锋 Task #32 P1-5 方案 a(事件驱动消费):
* 消费唤醒 osDelayUntil(1ms) → osThreadFlagsWait(ADC_WAKE_FLAG,
* osWaitForever),标志由 TIM6 ISR 经 adc_tick_irq_notify() 置位;
* osWaitForever),标志由 TIM3 ISR 经 adc_tick_irq_notify() 置位;
* 删除 ADC_TASK_BEAT_MS 与 next(P0-2 的重同步因此不再需要);
* 新增"消费者就绪"门 s_consumer_ready——TIM6 在 app_main_init()
* 新增"消费者就绪"门 s_consumer_ready——TIM3 在 app_main_init()
* 就启动,**远早于** MX_FREERTOS_Init()/osKernelStart(),
* ISR 不得在此之前对内核动手(同 Src/main.c 里 TIM7 的
* 2026-10-02 实测坑)。优先级 Normal → AboveNormal(见 freertos.c)
* v2.8 2026-10-10 王建锋 节拍源 TIM6 → **TIM3**(F4 上 ADC1 的外部触发没有
* TIM6 TRGO,只有 TIM2/TIM3/TIM8 系 → 换 TIM3 才能让
* 两片转换器由同一硬件事件驱动);STM32 侧改 DMA 双缓冲
* 消费,adc_input_poll() 不再忙等;心跳日志增
* adc1_seq / adc1_hw(adc1_err 语义改为"本拍无新帧")
*/
#include "adc_task.h"
@@ -82,7 +87,7 @@
/* 心跳日志周期(ms):周期性上报帧统计,作为采集链路存活的观测面。
* (原 ADC_TASK_BEAT_MS = 1U 的消费轮询周期已随 v2.7 事件驱动改造删除——
* 消费节拍改由 TIM6 ISR 置位 ADC_WAKE_FLAG 提供,与生产同源,不再自定周期) */
* 消费节拍改由 TIM3 ISR 置位 ADC_WAKE_FLAG 提供,与生产同源,不再自定周期) */
#define ADC_TASK_REPORT_MS 10000U
/* 统计:已消费帧数 / 无新帧次数(首拍必为无新帧,之后应稳定为 0 增量)。
@@ -96,7 +101,7 @@ static uint32_t s_frames_skip;
extern osThreadId_t adcTaskHandle;
/* 消费者就绪门:由 adcTask 自己在进入等待前置 1;单写者 = 本任务,ISR 只读。
* ⚠ 为什么必须有这道门:TIM6 在 app_main_init()(Src/main.c:140)里就
* ⚠ 为什么必须有这道门:TIM3 在 app_main_init()(Src/main.c:140)里就
* HAL_TIM_Base_Start_IT 启动,**远早于** osKernelInitialize()/MX_FREERTOS_Init()/
* osKernelStart()(Src/main.c:144/145/148)→ "节拍源已在跑"与"消费者已存在"之间有
* 约 100 ms 窗口,其间 adcTaskHandle 还是 NULL、调度器也还没起。
@@ -108,10 +113,10 @@ extern osThreadId_t adcTaskHandle;
static volatile uint8_t s_consumer_ready;
/*
* 函数功能:TIM6 采样节拍的 ISR 入口——置位唤醒 adcTask 消费一拍
* 函数功能:TIM3 采样节拍的 ISR 入口——置位唤醒 adcTask 消费一拍
* 入口参数:无
* 返 回 值:无
* 限定条件:仅 ISR 上下文。TIM6 抢占优先级 = 6 ≥ configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
* 限定条件:仅 ISR 上下文。TIM3 抢占优先级 = 6 ≥ configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
* (=5) → osThreadFlagsSet 走 xTaskNotifyFromISR + portYIELD_FROM_ISR,合法且立即切换
* 函数说明:Task #32 P1-5 方案 a——ISR **只发唤醒**,全部计算留在任务里(拒绝"ISR 内计算":
* adc_input_poll() 内含 2 ms 忙等,绝不可入 ISR)。线程标志粘滞 → 单拍偶超 1 ms 时
@@ -170,12 +175,12 @@ int adc_task_init(void) {
* 入口参数:arg - FreeRTOS 传入参数(未使用)
* 返 回 值:无
* 限定条件:adc_task_init() 已调用;内核已启动
* 函数说明:每拍采集一次(adc_input_poll():TPAFE 原子快照 + STM32 ADC1 9 通道顺序扫描)
* 并统计。采样节拍由 TIM6 硬件定时器提供(ISR 内触发 CONVST),本任务不再触发;
* **本任务的唤醒同样由该 TIM6 ISR 置位 ADC_WAKE_FLAG 驱动**(v2.7 事件驱动,
* 函数说明:每拍采集一次(adc_input_poll():TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 乒乓缓冲)
* 并统计。采样节拍由 TIM3 硬件定时器提供(ISR 内触发 CONVST),本任务不再触发;
* **本任务的唤醒同样由该 TIM3 ISR 置位 ADC_WAKE_FLAG 驱动**(v2.7 事件驱动,
* 不再自定 1 ms 周期)。稳态下 consumed 每拍 +1、idle 不再增长;idle 持续增长 =
* EXTI 未在读帧或 TIM6 未启动(异常),由周期心跳日志暴露。
* ⚠ 若 TIM6 停摆,本任务会**永久阻塞**在 osThreadFlagsWait → TASK_WD_ADC 缺席 →
* EXTI 未在读帧或 TIM3 未启动(异常),由周期心跳日志暴露。
* ⚠ 若 TIM3 停摆,本任务会**永久阻塞**在 osThreadFlagsWait → TASK_WD_ADC 缺席 →
* IWDG 复位。这是刻意的 fail-safe("节拍源缺失"不被静默放过),不是缺陷。
*/
void adc_task_func(void *arg) {
@@ -197,12 +202,13 @@ void adc_task_func(void *arg) {
* (osPriorityBelowNormal),是"UI 心跳缺席"的候选占用者之一 */
task_prof_enter(TASK_PROF_ADC_BEAT);
/* 1) 采集一拍:L1 层 adc_input_poll() = TPAFE 原子快照 + STM32 ADC1 顺序扫描
* (仅任务上下文;TPAFE 侧 1=有新帧 0=无新帧 -1=参数错)。
* 采样节拍由 TIM6 更新中断提供(ISR 内触发 CONVST),本任务不触发。
* P2 探针:本段单独计时。实测常态 adcp = 122 us(9 通道 x 7.4 us + TPAFE 快照);
* 只有 STM32 侧超时才跳到 ~2.2 ms——HAL_ADC_PollForConversion 是 2 ms 不让出的忙等,
* 而 stm32_scan() 在**首个**超时处即 Stop+return,故单拍上界是**一次**超时(不是 9 次)。
/* 1) 采集一拍:L1 层 adc_input_poll() = TPAFE 原子快照 + 消费一帧 STM32 ADC1
* DMA 双缓冲(仅任务上下文;TPAFE 侧 1=有新帧 0=无新帧 -1=参数错)。
* 采样节拍由 TIM3 更新事件提供(ISR 内触发 CONVST + TRGO 触发 ADC1),
* 本任务既不触发也不等待转换。
* P2 探针:本段单独计时。v2.0(DMA)起本段**零忙等**——原轮询版常态
* adcp=122 us、超时拍最坏 2~3 ms(HAL_ADC_PollForConversion 是不让出的
* 忙等),现在两档都降到 us 量级(只剩 TPAFE 快照临界区 + 拷 9 个半字)。
* ⚠ adcm(整拍)的 ~2.1 ms 峰值来自每 10 s 一次的心跳日志,不是本段,勿混读 */
task_prof_enter(TASK_PROF_ADC_POLL);
r = adc_input_poll();
@@ -301,15 +307,27 @@ void adc_task_func(void *arg) {
if ((int32_t)(now - report_at) >= 0) {
if (dbg_console_writer_ready() != 0u) {
DBG_INFO("frames=%lu idle=%lu skip=%lu seq=%lu frst_err=%u adc1_rc=%d adc1_err=%lu ring=%lu",
/* STM32 侧观测量(v2.0 DMA 版),四类各有独立成因:
* adc1_rc = 最近一拍取帧状态(0 OK / -2 本拍无新帧)
* adc1_seq = DMA 整帧完成计数(正常时 ≈ seq,恒定小偏移)
* adc1_gap = 丢帧:seq 一次前进 >1 → 本任务超拍(单拍 > 1 ms)
* adc1_err = 停摆:连续 >=2 拍无新帧 → 节拍源/DMA 真停
* adc1_hw = HAL ADC/DMA 错误回调
* 判读:adc1_seq 停增而 seq 仍涨 → STM32 侧停摆;
* adc1_gap 增长 → 本任务耗时超拍(看 adcm/adcn) */
DBG_INFO("frames=%lu idle=%lu skip=%lu seq=%lu frst_err=%u ring=%lu"
" adc1_rc=%d adc1_err=%lu adc1_seq=%lu adc1_gap=%lu adc1_hw=%lu",
(unsigned long)s_frames_consumed,
(unsigned long)s_frames_idle,
(unsigned long)s_frames_skip,
(unsigned long)tpafe5160_frame_seq(),
(unsigned)tpafe5160_frstdata_err(),
(unsigned long)sample_ring_frames(),
adc_input_stm32_rc(),
(unsigned long)adc_input_stm32_errs(),
(unsigned long)sample_ring_frames());
(unsigned long)adc_input_stm32_seq(),
(unsigned long)adc_input_stm32_gaps(),
(unsigned long)adc_input_stm32_hw_errs());
log_phasors("hb");
}
@@ -319,13 +337,13 @@ void adc_task_func(void *arg) {
/* 7) 等下一次节拍唤醒(v2.7 事件驱动,Task #32 P1-5 方案 a)
* 原实现是 osDelayUntil(next += 1ms):把消费唤醒**钉在 FreeRTOS tick(1 ms)边界**
* → 与 TIM6 生产之间有 0~1 ms 随机相位抖动(日志里的 idle 正是这么来的),
* → 与 TIM3 生产之间有 0~1 ms 随机相位抖动(日志里的 idle 正是这么来的),
* 且一旦超拍,next 就永久落后 → 任务不再阻塞 → 空转抢占低优先级 uiTask
* (P0-2 修的正是它)。现在唤醒与生产**同源**:TIM6 ISR 触发 CONVST 后置位
* (P0-2 修的正是它)。现在唤醒与生产**同源**:TIM3 ISR 触发 CONVST 后置位
* ADC_WAKE_FLAG,本任务被唤醒的延迟 = ISR + 调度(µs 级;本任务已提至
* AboveNormal → 立即抢占),无 tick 量化、无累积漂移,也就不再需要重同步钩子。
* 标志是命令位(等待即消费)→ osFlagsWaitAny,不加 osFlagsNoClear;
* osWaitForever 无超时 = 刻意的 fail-safe(TIM6 停摆 → 无心跳 → IWDG 复位)。 */
* osWaitForever 无超时 = 刻意的 fail-safe(TIM3 停摆 → 无心跳 → IWDG 复位)。 */
task_prof_exit(TASK_PROF_ADC_BEAT); /* P2 探针:单拍耗时结算(不含等待) */
(void)osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever);
}
+3 -3
View File
@@ -8,7 +8,7 @@
* v1.0 2026-07-19 王建锋 创建初始版本
* v1.1 2026-10-10 王建锋 Task #32 P1-5 方案 a:新增 ADC_WAKE_FLAG 与
* adc_tick_irq_notify()——消费节拍由 osDelayUntil(1ms)
* 轮询改为 TIM6 ISR 事件驱动唤醒
* 轮询改为 TIM3 ISR 事件驱动唤醒
*/
#ifndef ADC_TASK_H
@@ -22,12 +22,12 @@ int adc_task_init(void);
void adc_task_func(void *arg);
/* Task #32 P1-5(方案 a,2026-10-10):事件驱动消费。
* TIM6 1 kHz 更新中断触发 CONVST 后,用本标志唤醒 adcTask 消费一拍。
* TIM3 1 kHz 更新中断触发 CONVST 后,用本标志唤醒 adcTask 消费一拍。
* 线程标志在此是**命令位**(等待即消费),不是状态位 → 用 osFlagsWaitAny,
* 不加 osFlagsNoClear(AGENTS「就绪屏障位必须 osFlagsNoClear」只约束状态位) */
#define ADC_WAKE_FLAG 0x0001U
/* ISR 入口:由 Src/main.c 的 HAL_TIM_PeriodElapsedCallback(TIM6 分支)调用。
/* ISR 入口:由 Src/main.c 的 HAL_TIM_PeriodElapsedCallback(TIM3 分支)调用。
* 内部自带"消费者就绪"门与句柄判空 → 调度器启动前调用也安全(理由见 .c 注释)。
* 收在 adc_task.c 而非 main.c 内联:main.c 不碰任何内核句柄,与 task_wd_tick_probe() 同构 */
void adc_tick_irq_notify(void);