3b1d0308d8
Task #32 UI 看门狗停摆(docs/问题记录/UI任务看门狗停摆分析.md) P0 adc_task.c(已上板验证): - 仅 r==1 才压环+重算;上板印证 ring==frames(修前 ring=frames+idle) - osDelayUntil 返回值重同步,落后拉回基准,解除永久空转死锁 P1-4a ui_task.c v1.3: task_wd_beat 提到 ui_net_sink_poll 之前 → 心跳测"UI 活着"而非"netTask 响应",解开 §4.2 心跳依赖 → 未上 P1-3(提优先级会掩盖 §4.2/§4.4);P1-5 暂缓 P2 新增 App/util/task_prof.c/.h: DWT 常开探针,测各任务周期/耗时/ 推流往返/ADC 轮询/控制台排空/syslog 落盘/net_poll 最坏值;心跳位图 1→0 边沿裸 UART 直发 [PROF]/[PROF2] 快照;uvprojx 已登记 doc: 分析 doc §7 补良性启动 episode 与复位非诊断项;§8 加处置落地 验证: P0+P1 累计 32 次上板启动 0/32 复现(原 L2 基线 3/10),构建 0/0
283 lines
16 KiB
C
283 lines
16 KiB
C
/*
|
||
* 模块名称: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);本任务**不再自行触发**,退化为**纯消费者**,
|
||
* 1ms 轮询(osDelayUntil)仅作消费节奏。详见
|
||
* docs/子系统设计/ADC采样节拍设计说明.md。
|
||
* **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)
|
||
*/
|
||
|
||
#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)。采样节拍由 TIM6(1 kHz)提供,本周期仅作消费节奏 */
|
||
#define ADC_TASK_BEAT_MS 1U
|
||
/* 心跳日志周期(ms):周期性上报帧统计,作为采集链路存活的观测面 */
|
||
#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;
|
||
|
||
/*
|
||
* 函数功能:打印全部相量实例(×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() 已调用;内核已启动
|
||
* 函数说明:每 1ms 采集一拍(adc_input_poll():TPAFE 原子快照 + STM32 ADC1 9 通道
|
||
* 顺序扫描)并统计。采样节拍由 TIM6 硬件定时器提供(ISR 内触发 CONVST),
|
||
* 本任务不再触发。稳态下 consumed 每拍 +1、idle 不再增长;idle 持续增长 =
|
||
* EXTI 未在读帧或 TIM6 未启动(异常),由周期心跳日志暴露。
|
||
*/
|
||
void adc_task_func(void *arg) {
|
||
uint32_t next = osKernelGetTickCount();
|
||
uint32_t report_at = next + ADC_TASK_REPORT_MS;
|
||
uint8_t first_logged = 0u;
|
||
uint8_t first_phasor_logged = 0u;
|
||
|
||
(void)arg;
|
||
|
||
for (;;) {
|
||
int r;
|
||
|
||
/* P2 探针:单拍起点。名义节拍 1 ms = 168000 周期;若实测远超,
|
||
* 本任务(osPriorityNormal)会持续抢占 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),是单拍耗时的主要来源,必须与压环/相量分开看 */
|
||
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) 等下一个节拍(固定周期;落后则立即补齐,不累积漂移)
|
||
* ⚠ 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();
|
||
}
|
||
}
|
||
}
|