Files
STM32F4-Base/App/task/adc_task.c
T
wandering1 9231a3c22c 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 钩子全绿
2026-10-10 22:41:19 +08:00

333 lines
20 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:ADC Sampling Task
* 模块功能:ADC 采集消费 + L2 相量计算——TPAFE5160 快照 + STM32 ADC1 顺序扫描
* (经 L1 层 App/algo/adc_input.c 按语义取数)→ 压入采样环
* (App/algo/sample_ring.c)→ 全周傅氏相量重算(App/algo/phasor.c)
* + 帧统计(节拍由 TIM6 硬件定时器提供)
* 状态说明:**B1 已落地(2026-10-10)**——本任务从空桩转为真实消费者:
* 用 tpafe5160_snapshot() 原子取数(AGENTS 铁律),统计帧数/无新帧次数,
* 并纳入看门狗 required_mask(TASK_WD_ADC)。
* **采样节拍(v2.2,2026-10-10)**:由 **TIM6 更新中断(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` 的事件驱动**——唤醒与生产同源锁定,
* 消除 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),不再直接触碰驱动缓冲。
* V0.1 上只有 TPAFE 三路(UA/UN/IA)测量有效;本任务只取数,不做保护运算。
* 接入前必读:取数只用 adc_input_raw() / tpafe5160_snapshot()(临界区原子快照,
* 仅任务上下文;旧的 get_buf/data_ready/clear_ready 已删除);接消费者前的
* 强制门禁见根 AGENTS.md「接 ADC / RS-485 消费者前的强制门禁」+ 建任务前后
* 各跑 scripts/stack_report.py 做栈预算。
* 适用平台:STM32F407ZGTx + TPAFE5160
* 作者:王建锋
* 创建日期:2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
* v1.1 2026-10-05 王建锋 P3-11:注明"预留桩(未启用)",避免误认为已实现
* v2.0 2026-10-10 王建锋 B1:实现真实消费者(1ms 节拍 + 原子快照 + 帧统计 +
* 看门狗心跳),由 Src/freertos.c 创建为 adcTask
* v2.1 2026-10-10 王建锋 日志门控:首帧/周期统计仅在 dbg_console_writer_ready()
* 为真后落日志。写者未注册时控制台走无锁直发,与
* defaultTask 的存储初始化日志字节级交错(台架实测);
* 就绪前挂起上报,就绪后补报最近一帧。
* v2.2 2026-10-10 王建锋 采样节拍改由 TIM6 更新中断(1 kHz)硬件触发;本任务
* 删除自行触发,退化为纯消费者(见文件头"采样节拍")。
* v2.3 2026-10-10 王建锋 L1-3/L1-4:取数改走 App/algo/adc_input.c(语义映射 +
* STM32 ADC1 9 通道扫描);首帧日志补全 8 路电气量 + 温度,
* 心跳日志增 adc1_rc / adc1_err 观测 STM32 侧健康度。
* v2.4 2026-10-10 王建锋 L2:接入相量层——每拍把 9 路语义值压入采样环
* (sample_ring)并重算全部相量实例(phasor_run);
* 攒满一周波后**首拍即落一次相量日志**(开机证据,不等
* 10 s 心跳),心跳日志增 ring / skip 计数与各实例相量
* (Re/Im/有效值,×0.1 定点打印,因 MicroLIB 的 snprintf
* 无 %f)。
* ⚠ L1-5 通道标定未实现(缺标准源)→ 暂以**原始码值**
* 充作工程量(系数 = 1),故相量读数只证明**链路通**,
* 不可作幅值精度判据(同 L1-3 的替代口径)。
* v2.5 2026-10-10 王建锋 Task #32 的 P2 测量埋点:单拍加常开耗时探针
* (task_prof ADC_BEAT),用于判定本任务是否超拍
* (名义节拍 1 ms = 168000 周期)而挤占低优先级任务
* v2.6 2026-10-10 王建锋 Task #32 的 P0 修复(两处):
* §5 无新帧仍压环 → 改为**仅 r == 1 时**压环 + 重算
* (消除重复采样污染 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"
#include "cmsis_os.h"
#include "adc_input.h" /* L1 采集层:adc_input_poll()/adc_input_raw()(按语义取数) */
#include "sample_ring.h" /* L2 输入:sample_ring_push_frame()(采样环缓冲) */
#include "phasor.h" /* L2 相量层:phasor_run()/phasor_get()(全周傅氏) */
#include "tpafe5160.h"
#include "task_watchdog.h"
#include "console_log.h"
#include "task_prof.h" /* P2 常开探针:单拍耗时(Task #32) */
#define DBG_TAG "[ADC_TASK]"
#include "dbg_log.h"
/* 心跳日志周期(ms):周期性上报帧统计,作为采集链路存活的观测面。
* (原 ADC_TASK_BEAT_MS = 1U 的消费轮询周期已随 v2.7 事件驱动改造删除——
* 消费节拍改由 TIM6 ISR 置位 ADC_WAKE_FLAG 提供,与生产同源,不再自定周期) */
#define ADC_TASK_REPORT_MS 10000U
/* 统计:已消费帧数 / 无新帧次数(首拍必为无新帧,之后应稳定为 0 增量)。
* 原始码值缓冲由 L1 层 App/algo/adc_input.c 持有(消费侧按语义取数,不写死下标) */
static uint32_t s_frames_consumed;
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" 周期心跳)
* 返 回 值:无
* 限定条件:仅任务上下文;调用方须保证 dbg_console_writer_ready() 为真
* 函数说明:MicroLIB 的 snprintf **不支持 %f** → 以 ×0.1 定点整数打印
* (打印值 = 实际值 × 10,分辨率 0.1)。逐实例数据驱动输出,
* 实例表加行即自动覆盖。⚠ L1-5 标定未实现 → 读数单位是**原始码值**。
*/
static void log_phasors(const char *tag) {
uint8_t k;
for (k = 0u; k < phasor_inst_count(); k++) {
const phasor_inst_t *d = phasor_inst(k);
phasor_t p;
if ((d != NULL) && (phasor_get(k, &p) == 0)) {
DBG_INFO("phasor(%s) %s: Re=%ld Im=%ld rms=%ld (x0.1)",
tag, d->name,
(long)(p.fRe * 10.0f),
(long)(p.fIm * 10.0f),
(long)(p.fAmSQ * 10.0f));
}
}
}
/*
* 函数功能:ADC 任务初始化
* 入口参数:无
* 返 回 值:0 - 成功,-1 - 失败
* 限定条件:tpafe5160_init() 已在 app_main_init()(内核启动前)完成
* 函数说明:驱动初始化不在本任务(见限定条件);此处仅清帧统计。
*/
int adc_task_init(void) {
s_frames_consumed = 0u;
s_frames_idle = 0u;
s_frames_skip = 0u;
DBG_INFO("adc_task_init");
return 0;
}
/*
* 函数功能:ADC 任务主函数
* 入口参数:arg - FreeRTOS 传入参数(未使用)
* 返 回 值:无
* 限定条件:adc_task_init() 已调用;内核已启动
* 函数说明:每拍采集一次(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 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 周期;若实测**持续**超过,
* 本任务(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 探针:本段单独计时。实测常态 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);
if (r == 1) {
s_frames_consumed++;
} else if (r == 0) {
s_frames_idle++;
} else {
/* r == -1:参数错。调用点 count 为常量,不可达;不计入 idle 以区分异常 */
}
/* 2) L2 输入 + 相量重算:把本拍 9 路语义值转浮点帧压入采样环,再重算全部
* 相量实例(L2-6 滑动窗:每拍以"当前时刻为终止的前一周波"整窗重算)。
*
* ⚠ P0 修复(2026-10-10,Task #32 §5):**仅在本拍确有新帧(r == 1)时**才压环。
* 原实现在 r == 0(TPAFE 无新帧)时**仍然**压环 + phasor_run(),只有 r == -1
* 才跳过 —— 于是把**上一拍的重复采样值**塞进 20 点 DFT 窗,等效采样间隔被拉长。
* 证据是日志里的恒等式 `ring = frames + idle`(09:36:21: 9957+44=10001 ✓;
* 09:36:31: 19352+649=20001 ✓)。本例 idle 占 3.2% → 窗长从 20.0 ms 漂到约
* 20.6 ms → 对 50 Hz 信号相当于约 3% 的频率失配 → 幅值/相位**系统性**误差
* (不是随机噪声)。修好后 `ring` 只在真实新帧上增长(ring == frames)。
*
* 任一通道读不到(尚未首次成功)→ 本拍**整帧跳过**:不向窗内注入 0 点
* (单个 0 点会显著污染 20 点窗),并计入 skip 观测。 */
if (r == 1) {
float frame[ADC_SEM_COUNT];
uint8_t s;
uint8_t all_ok = 1u;
for (s = 0u; s < (uint8_t)ADC_SEM_COUNT; s++) {
int32_t code = 0;
if (adc_input_raw((adc_semantic_t)s, &code) != 0) {
all_ok = 0u;
break;
}
/* ⚠ L1-5 通道标定尚未实现(缺标准源)→ 暂以**原始码值**充作工程量
* (系数 = 1)。标定落地后此处改为 工程量 = (code + b) × k(定点 2^10)。 */
frame[s] = (float)code;
}
if (all_ok != 0u) {
(void)sample_ring_push_frame(frame, (uint8_t)ADC_SEM_COUNT);
phasor_run();
} else {
s_frames_skip++;
}
}
/* 3) 首帧码值上报:**写者就绪后**才落日志。写者未注册时 con_append 走
* 无锁直发路径,与 defaultTask 的存储初始化日志会字节级交错
* (2026-10-10 台架实测);此时挂起上报,就绪后补报最近一帧。
* 8 路电气量 + 温度全部经 adc_input_raw() **按语义**取数(L1-4),
* 用于验证"语义 → 物理通道"映射与两片转换器取数契约 */
if ((first_logged == 0u) && (s_frames_consumed != 0u) &&
(dbg_console_writer_ready() != 0u)) {
int32_t ua = 0, ub = 0, uc = 0, un = 0;
int32_t ia = 0, ib = 0, ic = 0, in = 0, temp = 0;
(void)adc_input_raw(ADC_SEM_UA, &ua);
(void)adc_input_raw(ADC_SEM_UB, &ub);
(void)adc_input_raw(ADC_SEM_UC, &uc);
(void)adc_input_raw(ADC_SEM_UN, &un);
(void)adc_input_raw(ADC_SEM_IA, &ia);
(void)adc_input_raw(ADC_SEM_IB, &ib);
(void)adc_input_raw(ADC_SEM_IC, &ic);
(void)adc_input_raw(ADC_SEM_IN, &in);
(void)adc_input_raw(ADC_SEM_TEMP, &temp);
first_logged = 1u;
DBG_INFO("first: UA=%ld UB=%ld UC=%ld UN=%ld IA=%ld IB=%ld IC=%ld IN=%ld T=%ld",
(long)ua, (long)ub, (long)uc, (long)un,
(long)ia, (long)ib, (long)ic, (long)in, (long)temp);
}
/* 4) 首拍相量上报:攒满一周波(20 ms)后立即落一次,作为 L2 链路的开机证据。
* 不依赖 10 s 心跳 → 台架 @test 的 settle 窗口内即可见(否则会被静默判据
* 截断,见 docs/测试说明/自动化测试流程.md 的 settle 口径)。 */
if ((first_phasor_logged == 0u) && (dbg_console_writer_ready() != 0u)) {
phasor_t p;
if (phasor_get(0u, &p) == 0) {
first_phasor_logged = 1u;
log_phasors("first");
}
}
/* 5) 看门狗心跳(本任务已纳入 required_mask) */
task_wd_beat(TASK_WD_ADC);
/* 6) 周期心跳日志:帧统计 + 驱动侧 FRSTDATA 错误计数 + L2 相量。
* 写者就绪前抑制(同 3) 的交错理由);到点无论是否落日志都推进下个上报点 */
{
uint32_t now = osKernelGetTickCount();
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",
(unsigned long)s_frames_consumed,
(unsigned long)s_frames_idle,
(unsigned long)s_frames_skip,
(unsigned long)tpafe5160_frame_seq(),
(unsigned)tpafe5160_frstdata_err(),
adc_input_stm32_rc(),
(unsigned long)adc_input_stm32_errs(),
(unsigned long)sample_ring_frames());
log_phasors("hb");
}
report_at = now + ADC_TASK_REPORT_MS;
}
}
/* 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);
}
}