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:
+1
-1
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user