Files
STM32F4-Base/App/util/task_prof.c
T
wandering1 54d6d2d5d5 docs: 删除 Task #32 停摆分析(已结案),可复用结论并入看门狗设计说明 §11
按登记簿"完成即销项"规则删除 docs/问题记录/UI任务看门狗停摆分析.md
(560 行)。结案依据:累计 38 次上板启动 0 复现(原 L2 基线 3/10),
P0/P1-4a/P1-5/P2/方案 A 全部落地,方案 B 已评估决定不做。

并入 docs/子系统设计/看门狗设计说明.md 新增 §11(只留可复用部分):
- §11.1 三条易误读的判读口径:启动期 MISS start 0x67 是良性 episode;
  miss= 是累计不是连续(连续数饱和在 WD_MISS_TOLERANCE=8);
  reset cause 不能区分 IWDG 溢出与台架软件复位
- §11.2 诊断四步:先看掩码缺几位(只缺 1 位 = 定点打击,多位 = 全局
  饥饿)→ 逐项量化占空比(实测 10%~13%,否掉"CPU 饱和"直觉)→
  用计数速率反证空转(ring 恰好 1000/s)→ 用 [PROF] 指认占用者
- §11.3 三条结构性脆弱点(uiTask 唯一 BelowNormal/心跳曾依赖 netTask
  往返/自定截止期不可恢复)+ 两个顺带修掉的正确性缺陷(无新帧仍压环
  ≈3% 频率失配、失败拍静默沿用旧值)
- §11.4 [PROF]/[PROF2] 全字段语义 + v2.8 实测基线(netp=0 是真实值、
  adcm 峰值来自心跳日志、uip 地板 = LVGL 帧周期)与三机理判读口诀
- §11.5 三条教训:门控类优化必须保留低频兜底;门控会顺手消灭暴露路径;
  事件驱动要核对节拍源启动时刻 vs 消费者创建时刻
- §11.6 明确不做的四条及理由(提 uiTask 优先级会掩盖真缺陷、net_* 有界
  等待版会串回执、推流搬出 uiTask、推流塞进 netTask)

UI 推流门控与方案 B 否决理由收进 RemoDispBus 协议说明 §3(该处原本已有
摘要,改为自洽不再回指);就绪门控 API 的约束已在 BSD Socket 指南 §10.1。

引用改指 12 处:ui_task.c、task_prof.c/.h、ui_net_sink.c/.h、docs/README、
RemoDispBus、看门狗设计说明 R7 行、ADC 采样节拍设计说明、BSD Socket 指南
×2、项目审查报告 N-10 ×2。

⚠ ui_task.c / ui_net_sink.c / ui_net_sink.h 是带 BOM 文件,Edit 剥掉了
BOM,已按原样补回 3 字节 EF BB BF(未整文件重写)。

验证:check_doc_links --all 通过(127 个文档,无悬空引用);
@build 0 错 0 警,Code=257660 与删除前一字不差(纯注释改动)。
2026-10-11 00:38:50 +08:00

301 lines
12 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.
/*
* 模块名称:Task Profiler(任务耗时最坏值探针)实现
* 模块功能:接口见 task_prof.h。本文件只做三件事:
* ① 惰性使能 DWT CYCCNT 并读值;② 维护每探针的 count/max/last/glitch
* 与"在段内"标志;③ 把全部字段格式化成一整行 `[PROF]` 交给控制台单写者。
* 适用平台:STM32F407ZGTx(仅 MCU 端)
* 作者:王建锋
* 创建日期:2026-10-10
* 修改记录:
* v1.0 2026-10-10 王建锋 创建(Task #32 的 P2 测量埋点)
*
* 设计依据:docs/子系统设计/看门狗设计说明.md §11.4(探针字段语义与实测基线)
*/
#include "task_prof.h"
#include <stdio.h>
#include "main.h" /* DWT / CoreDebug / SystemCoreClock / HAL_GetTick() */
#include "console_log.h" /* dbg_console_write_line / dbg_console_write_raw */
/* Private defines -----------------------------------------------------------*/
/* 报告节奏:首次延后(bring-up 深链期间不占串口),此后周期一行。
* 2 s 与 defaultTask 的 [STACK] 报告同节奏,便于两条序列交叉比对。 */
#define TASK_PROF_FIRST_MS 5000u
#define TASK_PROF_PERIOD_MS 2000u
/* 合理耗时上界(周期数)。超过即判为"CYCCNT 中途被重置"造成的无符号回绕
* (差值 ~2^32),丢弃而不污染 max。取 10 s 对应周期数:远高于真实最坏段
* 耗时(停摆窗口 < 2.5 s 即复位),又远低于 2^32,不会误杀真实数据。
* 168 MHz 下 10 s = 1.68e9 < 2^32,本身不溢出。 */
#define TASK_PROF_GLITCH_CYC (SystemCoreClock * 10u)
/* Private types -------------------------------------------------------------*/
typedef struct {
volatile uint32_t count; /* 已结算样本数 */
volatile uint32_t max; /* 最坏耗时(周期数) */
volatile uint32_t last; /* 最近一次耗时(周期数) */
volatile uint32_t glitch; /* 丢弃的样本数(DWT 不可用 / 计数器被重置) */
volatile uint32_t t_start; /* 进段时刻(周期数;仅 enter/exit 类探针使用) */
volatile uint32_t in_seg; /* 1 = 此刻仍在段内(读侧据此算"已停留多久") */
} task_prof_slot_t;
/* Private variables ---------------------------------------------------------*/
static task_prof_slot_t s_prof[TASK_PROF_ID_COUNT];
static uint8_t s_dwt_ok; /* 1 = DWT CYCCNT 已使能 */
static uint8_t s_prof_reported; /* 1 = 已首次报告(节流状态) */
static uint32_t s_prof_last; /* 上次报告时刻(ms) */
static char s_prof_line[DBG_CON_LINE_MAX]; /* 报告行缓冲(仅 defaultTask 用) */
/* Private function prototypes -----------------------------------------------*/
static uint32_t prof_cyc_to_us(uint32_t cyc);
static void prof_send(int n, uint32_t raw);
static void prof_emit(uint32_t raw);
/*
* 函数功能:周期数 → 微秒
* 入口参数:cyc - 周期数
* 返回值:微秒数(SystemCoreClock 低于 1 MHz 时返回 0)
* 限定条件:任意上下文可调(纯算术)
* 函数说明:MicroLIB 的 snprintf 不支持 %f → 一律以整数微秒上报。
*/
static uint32_t prof_cyc_to_us(uint32_t cyc) {
uint32_t per_us = SystemCoreClock / 1000000u;
if (per_us == 0u) {
return 0u;
}
return cyc / per_us;
}
/*
* 函数功能:把已格式化在静态行缓冲里的行按指定通道发出
* 入口参数:n - snprintf 返回的长度(<= 0 = 格式化失败,丢弃)
* raw - 0 = 入环形缓冲;非 0 = 裸 UART 直发
* 返回值:无
* 限定条件:仅 defaultTask 调用(唯一串口写者)
* 函数说明:长度按静态缓冲容量夹取,防 snprintf 截断后越界。
*/
static void prof_send(int n, uint32_t raw) {
const unsigned cap = (unsigned)sizeof(s_prof_line);
unsigned len;
if (n <= 0) {
return;
}
len = ((unsigned)n >= cap) ? (cap - 1u) : (unsigned)n;
if (raw != 0u) {
dbg_console_write_raw(s_prof_line, len);
} else {
(void)dbg_console_write_line(s_prof_line, len);
}
}
/*
* 函数功能:格式化并输出 `[PROF]` + `[PROF2]` 两行(静态行缓冲,零栈开销)
* 入口参数:raw - 0 = 入环形缓冲(周期报告);非 0 = 裸 UART 直发(停摆现场)
* 返回值:无
* 限定条件:仅 defaultTask 调用(唯一串口写者,无并发;故可复用静态缓冲)
* 函数说明:字段含义(括号内为打印单位):
* [PROF] t HAL tick(ms)
* uic/uip/uipn/uil uiTask 循环次数 / 周期最坏 / 本轮已进行 / 周期最近(us)
* uiw/uis/uisn uiTask 单轮工作最坏 / 推流往返最坏 / 仍在推流段内(us)
* adc/adcm/adcn adcTask 单拍次数 / 单拍最坏 / 仍在拍内(us)
* [PROF2] defd/defs defaultTask 控制台排空最坏 / syslog 落盘最坏(us)
* netp/netn netTask 驱动轮询最坏 / 仍在轮询内(us)
* adcp/adcq adcTask ADC 单次最坏 / 仍在 ADC 内(us)
* 两行的 gl 丢弃样本数(DWT 不可用或计数器被重置)
* "仍在段内"为 0 表示不在该段内——某任务若永久阻塞在某段,该段的
* count 不再增长,只有这个字段能证明它卡在哪里。
*/
static void prof_emit(uint32_t raw) {
const uint32_t t = HAL_GetTick();
const uint32_t now = task_prof_cyc();
uint32_t uipn = 0u;
uint32_t uisn = 0u;
uint32_t adcn = 0u;
uint32_t netn = 0u;
uint32_t adcq = 0u;
uint32_t gl = 0u;
uint32_t i;
int n;
if (s_prof[TASK_PROF_UI_PERIOD].in_seg != 0u) {
uipn = now - s_prof[TASK_PROF_UI_PERIOD].t_start;
}
if (s_prof[TASK_PROF_UI_SINK].in_seg != 0u) {
uisn = now - s_prof[TASK_PROF_UI_SINK].t_start;
}
if (s_prof[TASK_PROF_ADC_BEAT].in_seg != 0u) {
adcn = now - s_prof[TASK_PROF_ADC_BEAT].t_start;
}
if (s_prof[TASK_PROF_NET_POLL].in_seg != 0u) {
netn = now - s_prof[TASK_PROF_NET_POLL].t_start;
}
if (s_prof[TASK_PROF_ADC_POLL].in_seg != 0u) {
adcq = now - s_prof[TASK_PROF_ADC_POLL].t_start;
}
for (i = 0u; i < (uint32_t)TASK_PROF_ID_COUNT; i++) {
gl += s_prof[i].glitch;
}
/* 行 1:任务级(uiTask / adcTask) */
n = snprintf(s_prof_line, sizeof(s_prof_line),
"[PROF] t=%lu uic=%lu uip=%lu uipn=%lu uil=%lu uiw=%lu uis=%lu "
"uisn=%lu adc=%lu adcm=%lu adcn=%lu gl=%lu\r\n",
(unsigned long)t,
(unsigned long)s_prof[TASK_PROF_UI_PERIOD].count,
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_UI_PERIOD].max),
(unsigned long)prof_cyc_to_us(uipn),
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_UI_PERIOD].last),
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_UI_WORK].max),
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_UI_SINK].max),
(unsigned long)prof_cyc_to_us(uisn),
(unsigned long)s_prof[TASK_PROF_ADC_BEAT].count,
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_ADC_BEAT].max),
(unsigned long)prof_cyc_to_us(adcn),
(unsigned long)gl);
prof_send(n, raw);
/* 行 2:段级(占用者指认:defaultTask 排空/落盘、netTask 驱动、adcTask ADC) */
n = snprintf(s_prof_line, sizeof(s_prof_line),
"[PROF2] t=%lu defd=%lu defs=%lu netp=%lu netn=%lu adcp=%lu "
"adcq=%lu gl=%lu\r\n",
(unsigned long)t,
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_DEF_DRAIN].max),
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_DEF_SYSLOG].max),
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_NET_POLL].max),
(unsigned long)prof_cyc_to_us(netn),
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_ADC_POLL].max),
(unsigned long)prof_cyc_to_us(adcq),
(unsigned long)gl);
prof_send(n, raw);
}
/*
* 函数功能:读 DWT 周期计数器(懒使能;见 task_prof.h 的契约说明)
* 入口参数:无
* 返回值:当前 CYCCNT 值(周期数)
* 限定条件:任意上下文可调(幂等;纯寄存器读写)
* 函数说明:只置使能位、**不清零** CYCCNT——本模块只用差值,清零无意义,
* 且会与 console_log.c 的同名惰性使能互相干扰(该模块对 CYCCNT
* 清零后,在途的差值会变成一次无符号回绕,由 glitch 守卫丢弃)。
*/
uint32_t task_prof_cyc(void) {
if (s_dwt_ok == 0u) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
if ((DWT->CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0u) {
s_dwt_ok = 1u;
}
}
return DWT->CYCCNT;
}
/*
* 函数功能:记录一次耗时样本(周期数)
* 入口参数:id - 探针 ID;cyc - 耗时(周期数)
* 返回值:无
* 限定条件:单写者(每个 ID 只由一个任务调用)
* 函数说明:不可用样本(cyc 为 0 或超出合理上界)只累加 glitch,不更新
* max/last/count——否则一次计数器重置就会把最坏值抬到 ~2^32 周期。
*/
void task_prof_sample(task_prof_id_t id, uint32_t cyc) {
task_prof_slot_t *p;
if ((uint32_t)id >= (uint32_t)TASK_PROF_ID_COUNT) {
return;
}
p = &s_prof[id];
if ((cyc == 0u) || (cyc >= TASK_PROF_GLITCH_CYC)) {
p->glitch++;
return;
}
p->count++;
p->last = cyc;
if (cyc > p->max) {
p->max = cyc;
}
}
/*
* 函数功能:标记"进入某段"(观测"此刻仍在段内已多久")
* 入口参数:id - 探针 ID
* 返回值:无
* 限定条件:单写者;须与 task_prof_exit 成对
* 函数说明:写序为先 t_start 后 in_seg——读侧见 in_seg 为 1 时 t_start 必已有效。
*/
void task_prof_enter(task_prof_id_t id) {
task_prof_slot_t *p;
if ((uint32_t)id >= (uint32_t)TASK_PROF_ID_COUNT) {
return;
}
p = &s_prof[id];
p->t_start = task_prof_cyc();
p->in_seg = 1u;
}
/*
* 函数功能:结算一段耗时(与 task_prof_enter 成对)
* 入口参数:id - 探针 ID
* 返回值:无
* 限定条件:单写者;须与 task_prof_enter 成对
* 函数说明:先清"在段内"标志再结算,避免读侧把已结算的段误报为"仍在段内"。
* **未 enter 过则直接返回**:让"先 exit 再 enter"的周期测量写法
* 在首轮不把 0 当作起点(否则会记下一次虚假的超大样本)。
*/
void task_prof_exit(task_prof_id_t id) {
uint32_t t0;
if ((uint32_t)id >= (uint32_t)TASK_PROF_ID_COUNT) {
return;
}
if (s_prof[id].in_seg == 0u) {
return;
}
t0 = s_prof[id].t_start;
s_prof[id].in_seg = 0u;
task_prof_sample(id, task_prof_cyc() - t0);
}
/*
* 函数功能:周期性打印 [PROF] 行(内部节流)
* 入口参数:无
* 返回值:无
* 限定条件:仅 defaultTask 主循环调用
* 函数说明:首次 5 s 后报告(bring-up 深链已跑过),此后每 2 s 一行。
*/
void task_prof_report(void) {
const uint32_t now = HAL_GetTick();
if (s_prof_reported == 0u) {
if (now < TASK_PROF_FIRST_MS) {
return;
}
s_prof_reported = 1u;
} else if ((uint32_t)(now - s_prof_last) < TASK_PROF_PERIOD_MS) {
return;
}
s_prof_last = now;
prof_emit(0u);
}
/*
* 函数功能:停摆现场快照(同一 [PROF] 行,走裸 UART 直发)
* 入口参数:无
* 返回值:无
* 限定条件:仅 defaultTask 调用(此时它就是唯一串口写者,无并发)
* 函数说明:用于"首次心跳缺席"瞬间;缺席窗口内环形缓冲可能积压,
* 裸直发保证这一行能出去。
*/
void task_prof_report_raw(void) {
prof_emit(1u);
}