adcTask 事件驱动消费(P1-5 方案 a)+ 补 F4 ADC 上电自校准 + 修正 2 ms 忙等过期表述
P1-5 方案 a(事件驱动消费,Task #32): - TIM6 1 kHz ISR 除触发 CONVST 外再调 adc_tick_irq_notify(),置位 ADC_WAKE_FLAG;adcTask 循环尾改 osThreadFlagsWait(osWaitForever), 删除 ADC_TASK_BEAT_MS 定时轮询 - adcTask 优先级 Normal(24) -> AboveNormal(32)(Src/freertos.c USER CODE 区) - 补消费者就绪门(规划未预见):TIM6 在 app_main_init 就启动,比调度器 早约 100 ms,未就绪时 ISR 不得 osThreadFlagsSet - 实测 6 次上板:idle 由 1~649 降到 0/20000 拍,frames 1000.0 Hz, seq-frames=7 正是就绪窗口;UI 侧 uip/uiw/uis 无退化 补 STM32F4 ADC 上电自校准(此前项目从未做过): - 核查证实裁剪版 HAL 无 HAL_ADCEx_Calibration_Start、裁剪版 CMSIS 头无 ADC_CR2_CAL -> 按 RM0090 13.3.6 在 Src/adc.c 的 USER CODE 区手写 adc1_calibrate(),MX_ADC1_Init() 末尾调用 - 新增 cubemx-survival 审计项 adc1-calibration-call / -impl(清单 21->23) - 实测校准前后同一拍 STM32 侧 6 路同向偏移 +10~+63 LSB,TPAFE 侧不变 修正两处过期表述(实测反证): - 原文 9 通道全超时最坏 = 18 ms 连占 有误:stm32_scan() 在首个超时处即 Stop+return,单拍上界 = 一次超时约 2~3 ms - adcm 约 2.1 ms 的峰值归因由 ADC 采集改判为每 10 s 一次的心跳日志; 6 次对照 adcp = 122 us(无超时)/ 2209 us(超时 1 次) - 同步修正 adc_task.c 两处过期注释;新发现超时拍 STM32 侧静默沿用 上一拍值的正确性代价,硬化排序记入 4.4 / 10.4(未实施) 验证:@build 0 错 0 警(Code 256728 -> 256868);@test 6/6 PASS; stack_report 7 任务全 PASS(adcTask 832 B / 余 59%);18 钩子全绿
This commit is contained in:
+4
-3
@@ -19,13 +19,14 @@
|
||||
`sample_ring.c` / `phasor.c` **零平台依赖**(只用 `math.h` / `stddef.h` 与本目录头);
|
||||
`adc_input.c` 另依赖 `adc.h`(`hadc1`)与 `tpafe5160.h`(快照),主机侧以桩头替身。
|
||||
|
||||
## 2. 数据流(一拍 = 1 ms)
|
||||
## 2. 数据流(一拍 = TIM6 1 kHz 节拍)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
TIM6["TIM6 更新中断 1 kHz<br/>Src/main.c → tpafe5160_start_conv_irq()"] --> TPAFE["TPAFE5160 转换<br/>BUSY EXTI 收帧"]
|
||||
TIM6["TIM6 更新中断 1 kHz<br/>Src/main.c → tpafe5160_start_conv_irq()<br/>+ adc_tick_irq_notify()"] --> TPAFE["TPAFE5160 转换<br/>BUSY EXTI 收帧"]
|
||||
TIM6 -. "ADC_WAKE_FLAG<br/>osThreadFlagsWait" .-> TASK
|
||||
TPAFE --> POLL
|
||||
TASK["adcTask(1 ms osDelayUntil)<br/>App/task/adc_task.c"] --> POLL["adc_input_poll()<br/>TPAFE 快照 + STM32 ADC1 9 通道扫描"]
|
||||
TASK["adcTask(事件驱动唤醒,非定时轮询)<br/>App/task/adc_task.c"] --> POLL["adc_input_poll()<br/>TPAFE 快照 + STM32 ADC1 9 通道扫描"]
|
||||
POLL --> RAW["adc_input_raw(ch) × 9<br/>按语义取原始码值"]
|
||||
RAW --> RING["sample_ring_push_frame()<br/>整帧入环(40 点窗)"]
|
||||
RING --> RUN["phasor_run()<br/>逐实例 20 点重算 + 双区切换"]
|
||||
|
||||
+78
-28
@@ -9,9 +9,12 @@
|
||||
* 并纳入看门狗 required_mask(TASK_WD_ADC)。
|
||||
* **采样节拍(v2.2,2026-10-10)**:由 **TIM6 更新中断(1 kHz)** 硬件触发——
|
||||
* ISR 内调 tpafe5160_start_conv_irq()(Src/main.c 的
|
||||
* HAL_TIM_PeriodElapsedCallback);本任务**不再自行触发**,退化为**纯消费者**,
|
||||
* 1ms 轮询(osDelayUntil)仅作消费节奏。详见
|
||||
* docs/子系统设计/ADC采样节拍设计说明.md。
|
||||
* HAL_TIM_PeriodElapsedCallback);本任务**不再自行触发**,退化为**纯消费者**。
|
||||
* **消费唤醒(v2.7,2026-10-10,Task #32 P1-5 方案 a)**:由 1 ms `osDelayUntil`
|
||||
* 轮询改为 **TIM6 ISR 置位 `ADC_WAKE_FLAG` 的事件驱动**——唤醒与生产同源锁定,
|
||||
* 消除 0~1 ms 的 tick 量化相位抖动与"超拍永久落后 → 空转"风险;本任务优先级
|
||||
* 同步由 Normal 提到 **AboveNormal**(唤醒即抢占)。详见
|
||||
* docs/子系统设计/ADC采样节拍设计说明.md §10。
|
||||
* **L1 采集层(v2.3,2026-10-10)**:取数改走 App/algo/adc_input.c——
|
||||
* 本任务每拍调 adc_input_poll()(TPAFE 快照 + STM32 ADC1 9 通道扫描),
|
||||
* 经 adc_input_raw() **按语义**读通道(L1-3/L1-4),不再直接触碰驱动缓冲。
|
||||
@@ -54,6 +57,14 @@
|
||||
* (消除重复采样污染 20 点 DFT 窗的系统性误差);
|
||||
* §4.3 osDelayUntil 返回值被丢弃 → 落后时一次性重同步
|
||||
* (原写法会永久落后 → 不再阻塞 → 空转抢占 uiTask)
|
||||
* v2.7 2026-10-10 王建锋 Task #32 P1-5 方案 a(事件驱动消费):
|
||||
* 消费唤醒 osDelayUntil(1ms) → osThreadFlagsWait(ADC_WAKE_FLAG,
|
||||
* osWaitForever),标志由 TIM6 ISR 经 adc_tick_irq_notify() 置位;
|
||||
* 删除 ADC_TASK_BEAT_MS 与 next(P0-2 的重同步因此不再需要);
|
||||
* 新增"消费者就绪"门 s_consumer_ready——TIM6 在 app_main_init()
|
||||
* 就启动,**远早于** MX_FREERTOS_Init()/osKernelStart(),
|
||||
* ISR 不得在此之前对内核动手(同 Src/main.c 里 TIM7 的
|
||||
* 2026-10-02 实测坑)。优先级 Normal → AboveNormal(见 freertos.c)
|
||||
*/
|
||||
|
||||
#include "adc_task.h"
|
||||
@@ -69,9 +80,9 @@
|
||||
#define DBG_TAG "[ADC_TASK]"
|
||||
#include "dbg_log.h"
|
||||
|
||||
/* 消费轮询周期(ms)。采样节拍由 TIM6(1 kHz)提供,本周期仅作消费节奏 */
|
||||
#define ADC_TASK_BEAT_MS 1U
|
||||
/* 心跳日志周期(ms):周期性上报帧统计,作为采集链路存活的观测面 */
|
||||
/* 心跳日志周期(ms):周期性上报帧统计,作为采集链路存活的观测面。
|
||||
* (原 ADC_TASK_BEAT_MS = 1U 的消费轮询周期已随 v2.7 事件驱动改造删除——
|
||||
* 消费节拍改由 TIM6 ISR 置位 ADC_WAKE_FLAG 提供,与生产同源,不再自定周期) */
|
||||
#define ADC_TASK_REPORT_MS 10000U
|
||||
|
||||
/* 统计:已消费帧数 / 无新帧次数(首拍必为无新帧,之后应稳定为 0 增量)。
|
||||
@@ -81,6 +92,38 @@ static uint32_t s_frames_idle;
|
||||
/* 因通道尚未首次成功而**整拍跳过入环**的次数(不向相量窗注入 0 点) */
|
||||
static uint32_t s_frames_skip;
|
||||
|
||||
/* adcTask 的内核句柄,定义在 Src/freertos.c(引用方式同 App/task/default_task.c) */
|
||||
extern osThreadId_t adcTaskHandle;
|
||||
|
||||
/* 消费者就绪门:由 adcTask 自己在进入等待前置 1;单写者 = 本任务,ISR 只读。
|
||||
* ⚠ 为什么必须有这道门:TIM6 在 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、调度器也还没起。
|
||||
* osThreadFlagsSet() 对 NULL 句柄本身会安全返回 osErrorParameter(cmsis_os2.c 的入参
|
||||
* 检查),但"句柄已赋值、调度器未启动"这段(freertos.c 建任务 → osKernelStart)仍会让
|
||||
* ISR 版内核 API 在调度器外跑。本仓库对这类坑有实测记录:Src/main.c 的 TIM7 分支注释
|
||||
* (2026-10-02,port.c 优先级断言把系统卡死在 ISR 内)。就绪门把这条时序变成**显式约束**,
|
||||
* 而不是依赖 FreeRTOS 内部实现细节。 */
|
||||
static volatile uint8_t s_consumer_ready;
|
||||
|
||||
/*
|
||||
* 函数功能:TIM6 采样节拍的 ISR 入口——置位唤醒 adcTask 消费一拍
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅 ISR 上下文。TIM6 抢占优先级 = 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 时
|
||||
* 多个脉冲合并成一次唤醒,下一轮 adc_input_poll() 返回 r==0 记 idle,不会重复处理同一帧
|
||||
*/
|
||||
void adc_tick_irq_notify(void) {
|
||||
if (s_consumer_ready == 0u) {
|
||||
return;
|
||||
}
|
||||
(void)osThreadFlagsSet(adcTaskHandle, ADC_WAKE_FLAG);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:打印全部相量实例(×0.1 定点)
|
||||
* 入口参数:tag - 前缀标记("first" 首次产出 / "hb" 周期心跳)
|
||||
@@ -127,32 +170,40 @@ int adc_task_init(void) {
|
||||
* 入口参数:arg - FreeRTOS 传入参数(未使用)
|
||||
* 返 回 值:无
|
||||
* 限定条件:adc_task_init() 已调用;内核已启动
|
||||
* 函数说明:每 1ms 采集一拍(adc_input_poll():TPAFE 原子快照 + STM32 ADC1 9 通道
|
||||
* 顺序扫描)并统计。采样节拍由 TIM6 硬件定时器提供(ISR 内触发 CONVST),
|
||||
* 本任务不再触发。稳态下 consumed 每拍 +1、idle 不再增长;idle 持续增长 =
|
||||
* 函数说明:每拍采集一次(adc_input_poll():TPAFE 原子快照 + STM32 ADC1 9 通道顺序扫描)
|
||||
* 并统计。采样节拍由 TIM6 硬件定时器提供(ISR 内触发 CONVST),本任务不再触发;
|
||||
* **本任务的唤醒同样由该 TIM6 ISR 置位 ADC_WAKE_FLAG 驱动**(v2.7 事件驱动,
|
||||
* 不再自定 1 ms 周期)。稳态下 consumed 每拍 +1、idle 不再增长;idle 持续增长 =
|
||||
* EXTI 未在读帧或 TIM6 未启动(异常),由周期心跳日志暴露。
|
||||
* ⚠ 若 TIM6 停摆,本任务会**永久阻塞**在 osThreadFlagsWait → TASK_WD_ADC 缺席 →
|
||||
* IWDG 复位。这是刻意的 fail-safe("节拍源缺失"不被静默放过),不是缺陷。
|
||||
*/
|
||||
void adc_task_func(void *arg) {
|
||||
uint32_t next = osKernelGetTickCount();
|
||||
uint32_t report_at = next + ADC_TASK_REPORT_MS;
|
||||
uint32_t report_at = osKernelGetTickCount() + ADC_TASK_REPORT_MS;
|
||||
uint8_t first_logged = 0u;
|
||||
uint8_t first_phasor_logged = 0u;
|
||||
|
||||
(void)arg;
|
||||
|
||||
/* 开闸:此刻任务已存在、调度器必已运行 → ISR 的唤醒从此生效。
|
||||
* 放在进入循环之前:置位与等待之间不会漏唤醒(线程标志粘滞) */
|
||||
s_consumer_ready = 1u;
|
||||
|
||||
for (;;) {
|
||||
int r;
|
||||
|
||||
/* P2 探针:单拍起点。名义节拍 1 ms = 168000 周期;若实测远超,
|
||||
* 本任务(osPriorityNormal)会持续抢占 uiTask(osPriorityBelowNormal),
|
||||
* 是"UI 心跳缺席"的候选占用者之一 */
|
||||
/* P2 探针:单拍起点。名义节拍 1 ms = 168000 周期;若实测**持续**超过,
|
||||
* 本任务(v2.7 起为 osPriorityAboveNormal)会持续抢占 uiTask
|
||||
* (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 探针:本段单独计时——ADC 超时是 2ms 不让出的忙等(HAL_ADC_
|
||||
* PollForConversion),是单拍耗时的主要来源,必须与压环/相量分开看 */
|
||||
* P2 探针:本段单独计时。实测常态 adcp = 122 us(9 通道 x 7.4 us + TPAFE 快照);
|
||||
* 只有 STM32 侧超时才跳到 ~2.2 ms——HAL_ADC_PollForConversion 是 2 ms 不让出的忙等,
|
||||
* 而 stm32_scan() 在**首个**超时处即 Stop+return,故单拍上界是**一次**超时(不是 9 次)。
|
||||
* ⚠ adcm(整拍)的 ~2.1 ms 峰值来自每 10 s 一次的心跳日志,不是本段,勿混读 */
|
||||
task_prof_enter(TASK_PROF_ADC_POLL);
|
||||
r = adc_input_poll();
|
||||
task_prof_exit(TASK_PROF_ADC_POLL);
|
||||
@@ -266,17 +317,16 @@ void adc_task_func(void *arg) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 7) 等下一个节拍(固定周期;落后则立即补齐,不累积漂移)
|
||||
* ⚠ P0 修复(2026-10-10,Task #32 §4.3):osDelayUntil 在**目标时刻已过**
|
||||
* 时返回 osErrorParameter 并立即返回(cmsis_os2.c 的 "No delay or already
|
||||
* expired" 分支)。若像原实现那样只写 `(void)` 丢弃返回值,一次超拍会让
|
||||
* next 相对当前时刻**永久落后** → 本任务从此不再阻塞、以 Normal 优先级
|
||||
* 空转,直到某个"快拍"把差额追平(连续慢拍则锁死,全程抢占低优先级的
|
||||
* uiTask)。返回值是现成的重同步钩子:落后就一次性把基准拉回当前时刻。 */
|
||||
task_prof_exit(TASK_PROF_ADC_BEAT); /* P2 探针:单拍耗时结算(不含睡眠) */
|
||||
next += ADC_TASK_BEAT_MS;
|
||||
if (osDelayUntil(next) != osOK) {
|
||||
next = osKernelGetTickCount();
|
||||
}
|
||||
/* 7) 等下一次节拍唤醒(v2.7 事件驱动,Task #32 P1-5 方案 a)
|
||||
* 原实现是 osDelayUntil(next += 1ms):把消费唤醒**钉在 FreeRTOS tick(1 ms)边界**
|
||||
* → 与 TIM6 生产之间有 0~1 ms 随机相位抖动(日志里的 idle 正是这么来的),
|
||||
* 且一旦超拍,next 就永久落后 → 任务不再阻塞 → 空转抢占低优先级 uiTask
|
||||
* (P0-2 修的正是它)。现在唤醒与生产**同源**:TIM6 ISR 触发 CONVST 后置位
|
||||
* ADC_WAKE_FLAG,本任务被唤醒的延迟 = ISR + 调度(µs 级;本任务已提至
|
||||
* AboveNormal → 立即抢占),无 tick 量化、无累积漂移,也就不再需要重同步钩子。
|
||||
* 标志是命令位(等待即消费)→ osFlagsWaitAny,不加 osFlagsNoClear;
|
||||
* osWaitForever 无超时 = 刻意的 fail-safe(TIM6 停摆 → 无心跳 → IWDG 复位)。 */
|
||||
task_prof_exit(TASK_PROF_ADC_BEAT); /* P2 探针:单拍耗时结算(不含等待) */
|
||||
(void)osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
* 创建日期:2026-07-19
|
||||
* 修改记录:
|
||||
* 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 事件驱动唤醒
|
||||
*/
|
||||
|
||||
#ifndef ADC_TASK_H
|
||||
@@ -18,6 +21,17 @@ extern "C" {
|
||||
int adc_task_init(void);
|
||||
void adc_task_func(void *arg);
|
||||
|
||||
/* Task #32 P1-5(方案 a,2026-10-10):事件驱动消费。
|
||||
* TIM6 1 kHz 更新中断触发 CONVST 后,用本标志唤醒 adcTask 消费一拍。
|
||||
* 线程标志在此是**命令位**(等待即消费),不是状态位 → 用 osFlagsWaitAny,
|
||||
* 不加 osFlagsNoClear(AGENTS「就绪屏障位必须 osFlagsNoClear」只约束状态位) */
|
||||
#define ADC_WAKE_FLAG 0x0001U
|
||||
|
||||
/* ISR 入口:由 Src/main.c 的 HAL_TIM_PeriodElapsedCallback(TIM6 分支)调用。
|
||||
* 内部自带"消费者就绪"门与句柄判空 → 调度器启动前调用也安全(理由见 .c 注释)。
|
||||
* 收在 adc_task.c 而非 main.c 内联:main.c 不碰任何内核句柄,与 task_wd_tick_probe() 同构 */
|
||||
void adc_tick_irq_notify(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user