fix(ui): Task #32 P0/P1/P2 — 解耦 UI 心跳、修 adc 压环与截止期、加测量探针
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
This commit is contained in:
+40
-4
@@ -46,6 +46,14 @@
|
||||
* ⚠ 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"
|
||||
@@ -56,6 +64,7 @@
|
||||
#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"
|
||||
@@ -134,10 +143,19 @@ void adc_task_func(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),本任务不触发。 */
|
||||
* 采样节拍由 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) {
|
||||
@@ -148,9 +166,18 @@ void adc_task_func(void *arg) {
|
||||
|
||||
/* 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;
|
||||
@@ -239,8 +266,17 @@ void adc_task_func(void *arg) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 7) 等下一个节拍(固定周期;落后则立即补齐,不累积漂移) */
|
||||
/* 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;
|
||||
(void)osDelayUntil(next);
|
||||
if (osDelayUntil(next) != osOK) {
|
||||
next = osKernelGetTickCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
* FTP 文件操作与 syslog/配置的卷访问自此串行化
|
||||
* (2026-10-02 台架:并发 FTL 写 → journal 竞争 →
|
||||
* 本任务主循环喂狗点饥饿 → IWDG 误复位)
|
||||
* v1.2 2026-10-10 王建锋 Task #32 的 P2 测量接线:主循环周期上报各任务最坏
|
||||
* 耗时(task_prof_report),并在**心跳位图出现 1→0 边沿**
|
||||
* 的瞬间留一份现场快照(task_prof_report_raw,裸 UART);
|
||||
* 另给控制台排空与 syslog 落盘各加一个耗时探针
|
||||
*/
|
||||
|
||||
#include "default_task.h"
|
||||
@@ -39,6 +43,7 @@
|
||||
#define DBG_LINE_BUF_MAX 128U
|
||||
#include "dbg_log.h"
|
||||
#include "task_watchdog.h"
|
||||
#include "task_prof.h" /* P2 测量:各任务最坏耗时上报(Task #32) */
|
||||
#include "vol_lock.h"
|
||||
|
||||
/* 任务句柄与属性块(CubeMX 生成全局,定义于 Src/freertos.c):
|
||||
@@ -185,6 +190,22 @@ void default_task_func(void *argument) {
|
||||
task_wd_seg(TASK_WD_SEG_POLL);
|
||||
/* 心跳抽样与喂狗判定(内部节流至 100ms;全员到位才喂,否则累计停喂) */
|
||||
task_wd_poll();
|
||||
/* P2 测量(Task #32):某位由"到过"变为"缺席"的瞬间留一份现场快照。
|
||||
* 走裸 UART 直发——缺席窗口内环形缓冲可能积压,这两行必须出去。
|
||||
*
|
||||
* ⚠ 判据用**位图边沿**,不能用 miss 累计数:`task_wd_miss_count()` 返回的是
|
||||
* 单调累计的 s_miss_total(永不清零),开机 bring-up 阶段任务尚未全部启动时
|
||||
* 就已经 >0 → 若以"归零"判恢复,则首次缺席之后再也不会触发。
|
||||
* 位图边沿(某位 1→0)天然"每个 episode 只报一次":此后 prev 也不含该位。 */
|
||||
{
|
||||
static uint32_t s_prof_prev_mask;
|
||||
const uint32_t mask = task_wd_last_mask();
|
||||
|
||||
if ((s_prof_prev_mask != 0u) && ((s_prof_prev_mask & ~mask) != 0u)) {
|
||||
task_prof_report_raw();
|
||||
}
|
||||
s_prof_prev_mask = mask;
|
||||
}
|
||||
/* 本任务自己的心跳 */
|
||||
task_wd_beat(TASK_WD_DEFAULT);
|
||||
/* 等待"保存网络配置"或"控制台待发":任一即醒;无事件 50ms 后返回
|
||||
@@ -197,7 +218,12 @@ void default_task_func(void *argument) {
|
||||
}
|
||||
/* 控制台唯一发送点:把环形缓冲中的日志按块发出(无待发时立即返回) */
|
||||
task_wd_seg(TASK_WD_SEG_DRAIN);
|
||||
/* P2 探针:控制台排空单独计时。它内部**忙等** UART TXE(单次预算
|
||||
* DBG_CON_DRAIN_BUDGET_MS=250ms),且本任务为 osPriorityNormal →
|
||||
* 是"某个 Normal 任务连续占用 CPU 把 UI 饿死"的头号候选 */
|
||||
task_prof_enter(TASK_PROF_DEF_DRAIN);
|
||||
dbg_console_drain();
|
||||
task_prof_exit(TASK_PROF_DEF_DRAIN);
|
||||
/* 控制台 TX 故障留痕:由本任务(syslog 唯一写者)在 drain **之外**补记 run.log。
|
||||
* console_log 内部刻意不调 syslog_post —— 那会构成日志调用图成环,
|
||||
* 让 armlink 静态栈深在每个任务上都多叠一条日志尾巴(见 console_log.c) */
|
||||
@@ -252,11 +278,21 @@ void default_task_func(void *argument) {
|
||||
#if !defined(TEST_SUITE_STORAGE)
|
||||
/* 排空运行期日志(上电/重启分类与自检故障落盘) */
|
||||
task_wd_seg(TASK_WD_SEG_SYSLOG);
|
||||
/* P2 探针:syslog 落盘(NAND/FTL 写,最坏数百 ms)单独计时 */
|
||||
task_prof_enter(TASK_PROF_DEF_SYSLOG);
|
||||
syslog_drain();
|
||||
task_prof_exit(TASK_PROF_DEF_SYSLOG);
|
||||
#endif
|
||||
/* 栈水位观测:首次 5s 后报告,此后每 2s 采样、出现更深峰值才打印 */
|
||||
task_wd_seg(TASK_WD_SEG_STACK);
|
||||
stack_probe_poll();
|
||||
/* P2 测量(Task #32):各任务最坏耗时 / 次数 / "仍在段内"快照。
|
||||
* 内部节流(首次 5s,此后每 2s);判据:
|
||||
* uisn 持续增长 → uiTask 卡在 ui_net_sink_poll 的往返里(阻塞)
|
||||
* uipn 持续增长而 uisn 为 0 → 被调度饿死(本轮迟迟跑不完)
|
||||
* uiw 大 → uiTask 自身渲染拖长
|
||||
* [PROF2] 的 defd/netp/adcp 谁在同一窗口抬起来 → 谁就是占用者 */
|
||||
task_prof_report();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
* freertos.c USER CODE 区重复创建已删除
|
||||
* v1.7 2026-10-02 王建锋 P1-8:初始组网失败/重建失败 → 自检记录 + 30s 周期重试
|
||||
* (原实现:初始失败 = netTask 无限驻留;重建失败 = 静默丢配置)
|
||||
* v1.8 2026-10-10 王建锋 Task #32 的 P2 测量埋点:net_poll() 单独计时——
|
||||
* 本任务每 ~1ms 分派一次消息队列,若驱动轮询被拉长,
|
||||
* 其它任务(尤其 UI 的 net_accept 往返)的等待随之拉长
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
@@ -31,6 +34,7 @@
|
||||
#include "sys_log.h"
|
||||
#include "test_config.h" /* 测试模式宏(ENABLE_HW_LAYER_TESTS);产品构建全未定义,无作用 */
|
||||
#include "task_watchdog.h"
|
||||
#include "task_prof.h" /* P2 探针:net_poll 耗时(Task #32) */
|
||||
|
||||
#define DBG_TAG "[NET_TASK]"
|
||||
#include "dbg_log.h"
|
||||
@@ -267,7 +271,11 @@ void net_task_func(void const *arg) {
|
||||
}
|
||||
}
|
||||
|
||||
/* P2 探针:驱动轮询单独计时。本循环每 ~1ms 一次,是 netTask 回执
|
||||
* (UI 的 net_accept 在等它)的节奏来源;此值变大 = UI 心跳被拖长 */
|
||||
task_prof_enter(TASK_PROF_NET_POLL);
|
||||
net_poll();
|
||||
task_prof_exit(TASK_PROF_NET_POLL);
|
||||
net_process_messages();
|
||||
osDelay(1);
|
||||
}
|
||||
|
||||
+43
-3
@@ -6,6 +6,18 @@
|
||||
* 创建日期:2026-08-30
|
||||
* 修改记录:
|
||||
* v1.1 2026-09-14 王建锋 新增待机显示页初始化(ui_standby_init,空闲 60s 切入)
|
||||
* v1.2 2026-10-10 王建锋 Task #32 的 P2 测量埋点:主循环加常开探针
|
||||
* (循环周期 / 单轮工作耗时 / ui_net_sink_poll 往返),
|
||||
* 用于把"心跳缺席"拆成 饿死 / 阻塞在往返 / 自身拖长
|
||||
* 三种互斥机理(见 docs/问题记录/UI任务看门狗停摆分析.md)
|
||||
* v1.3 2026-10-10 王建锋 Task #32 的 P1-4a(解耦心跳,§4.2):把 task_wd_beat
|
||||
* (TASK_WD_UI) 提到 ui_net_sink_poll() **之前**。原写法心跳
|
||||
* 在推流往返之后 → 心跳周期 = 渲染 + 一次 netTask 往返 + 10ms,
|
||||
* 实测 uiw≈uis≈50ms(心跳被 netTask 往返主导),心跳实际测的是
|
||||
* "netTask 响应性"而非"UI 活着"。提到往返之前后,心跳测的是
|
||||
* "UI 自己被调度到了"。注:若往返**永久**阻塞,本改仍不能救
|
||||
* (心跳只发一次)→ 真要彻底去掉依赖须走事件驱动 accept
|
||||
* (NET_EVENT_CONNECTED 回调),待复现快照确认阻塞机理后再定。
|
||||
*/
|
||||
#include "ui_task.h"
|
||||
|
||||
@@ -22,6 +34,7 @@
|
||||
#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */
|
||||
#include "net_latency_test.h" /* M3 探针:ui_loop(生产循环实测周期) */
|
||||
#include "task_watchdog.h"
|
||||
#include "task_prof.h" /* P2 常开探针:循环周期 / 工作耗时 / 推流往返 */
|
||||
|
||||
/*
|
||||
* 函数功能:待机屏底部时钟提供回调(平台差异:MCU = sys_clock_get_str)
|
||||
@@ -114,7 +127,21 @@ void ui_task_func(void *argument) {
|
||||
uint8_t tm_loop_valid = 0U;
|
||||
#endif
|
||||
|
||||
/* P2 常开探针(Task #32):TEST_LATENCY_PROBE 只在 CH395F 套件构建下打开,
|
||||
* 且样本消费者是 testTask(产品构建里不跑)→ 产品构建拿不到任何 UI 周期数据。
|
||||
* 故此处独立埋点,恒定生效、只写本模块的静态计数,由 defaultTask 周期上报。 */
|
||||
|
||||
while (1) {
|
||||
/* 循环周期用 enter/exit 测:exit 结算"上轮起点 → 本轮起点",enter 记本轮起点。
|
||||
* 首轮 exit 时尚未 enter 过 → 内部直接返回,不会拿 0 当起点。
|
||||
* 选 enter/exit 而非"自己存一个 prev"的额外收益:两次循环之间段内标志保持为 1
|
||||
* → 报告可读出"本轮已进行多久"。**这正是'uiTask 是否被饿住'的直接证据**:
|
||||
* 若该值在两次报告间持续增长而 uic 不动,就是被调度饿死/阻塞,而非渲染拖长。 */
|
||||
task_prof_exit(TASK_PROF_UI_PERIOD);
|
||||
task_prof_enter(TASK_PROF_UI_PERIOD);
|
||||
|
||||
const uint32_t prof_t0 = task_prof_cyc(); /* 单轮工作耗时基准 */
|
||||
|
||||
#if defined(TEST_LATENCY_PROBE)
|
||||
if (tm_loop_valid != 0U) {
|
||||
lat_probe_add_seg("ui_loop", lat_mark_us(&tm_loop));
|
||||
@@ -123,10 +150,23 @@ void ui_task_func(void *argument) {
|
||||
tm_loop_valid = 1U;
|
||||
#endif
|
||||
ui_port_step(10);
|
||||
ui_net_sink_poll();
|
||||
/* 心跳:ui_port_step 内含 LVGL 渲染,单轮耗时可能逼近 10ms 量级;
|
||||
* 100ms 抽样窗内可容纳多轮,此埋点足够 */
|
||||
/* 心跳**先于**推流往返发出(P1-4a,Task #32 §4.2):UI 心跳测的是
|
||||
* "UI 自身被调度到了",而非"netTask 回执快慢"。原写法心跳在 sink 之后,
|
||||
* 致实测 uiw≈uis≈50ms(心跳被 netTask 往返主导)。提到往返之前后,
|
||||
* 即使往返偶发变慢,心跳也不被其拖垮。
|
||||
* ⚠ 若往返**永久**阻塞(xTaskNotifyWait portMAX_DELAY 不返回),本改只能发
|
||||
* 一次心跳,救不了 → 彻底解耦须走事件驱动 accept,见文件头 v1.3 说明。 */
|
||||
task_wd_beat(TASK_WD_UI);
|
||||
/* 推流轮询段单独 enter/exit:它内部经 net_accept → net_wait_result 走到
|
||||
* xTaskNotifyWait(portMAX_DELAY) 的**无限期阻塞**。若 uiTask 永久卡在这里,
|
||||
* 样本永不结算,只有 task_prof 的"仍在段内已 X us"能证明它卡在何处 */
|
||||
task_prof_enter(TASK_PROF_UI_SINK);
|
||||
ui_net_sink_poll();
|
||||
task_prof_exit(TASK_PROF_UI_SINK);
|
||||
|
||||
/* 单轮工作耗时(不含 osDelay):与循环周期对比即得"调度延迟" */
|
||||
task_prof_sample(TASK_PROF_UI_WORK, task_prof_cyc() - prof_t0);
|
||||
|
||||
osDelay(10);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user