/* * 模块名称:任务耗时探针(task_prof)主机侧单元测试 * 模块功能:在 PC 上离线验证 App/util/task_prof.c 的**样本有效性闸门、enter/exit * 配对、上报节流与通道选择、周期→微秒换算、glitch 汇总**。 * * 被测对象:真实源文件 ../../../App/util/task_prof.c(#include 原样纳入)。桩: * - stub/main.h:DWT/CoreDebug/SystemCoreClock/HAL_GetTick(本目录, * 真实 main.h 拉整棵 HAL 头树无法在 PC 上编); * - console_log.h 用**真实头**(App/log),两个出口函数 * dbg_console_write_line / dbg_console_write_raw 由本文件实现并**留档 * 内容**——报告行是要被人读的,所以断言直接盯字符串本身。 * * 为什么要主机侧单测(而不是只靠真机日志): * 这个模块是"性能问题的唯一现场证据",它自己错了所有 PROF 行就都是假的: * 1. **无效样本闸门**(cyc==0 或 ≥ SystemCoreClock×10):漏了它,一次 DWT 复位 * 就把 max 抬到 ~2^32 周期,之后所有"最坏耗时"永久失真(真机上没人会去翻 * gl= 计数发现)。这条只能构造样本测。 * 2. **未 enter 就 exit 必须直接返回**:周期测量写成"先 exit 再 enter"时,首轮 * 会把 0 当起点,记下一次几秒的虚假超大样本——真机上表现为"偶发一条离谱 * PROF 行",查半年。 * 3. 节流契约(首次 5 s、之后每 2 s)与**两条通道的选择**(report 走环形缓冲、 * report_raw 走裸发):错了要么刷屏挤掉正事,要么停摆现场那一行发不出去。 * 4. 周期→微秒换算依赖 SystemCoreClock,改时钟树(比如 168→180 MHz)忘了同步 * 就会整表偏移;这条在真机上看不出来(没人对着示波器量 PROF)。 * * 覆盖点: * TC-TASKPROF-001 有效样本:count+1 / last=样本 / max=样本 * TC-TASKPROF-002 更小样本不改 max(max 只增不减) * TC-TASKPROF-003 cyc=0 只累加 glitch,不动 count/last/max(计数器复位防线) * TC-TASKPROF-004 cyc ≥ SystemCoreClock×10 同样只累加 glitch * TC-TASKPROF-005 id 越界(=COUNT 与 +1)安全返回,不碰任何槽 * TC-TASKPROF-006 首次取时基即惰性使能 DWT(TRCENA + CYCCNTENA 读回) * TC-TASKPROF-007 enter 置 in_seg 并记下起点 * TC-TASKPROF-008 exit 结算 Δ、清 in_seg、count+1 * TC-TASKPROF-009 **未 enter 直接 exit 不记账**(防首轮 0 起点造出假超大样本) * TC-TASKPROF-010 重复 exit 不重复记账 * TC-TASKPROF-011 首次 5 s 之前一条都不发 * TC-TASKPROF-012 到点后发两行([PROF] + [PROF2]),走环形缓冲通道 * TC-TASKPROF-013 之后不足 2 s 不发(节流) * TC-TASKPROF-014 满 2 s 再发两行 * TC-TASKPROF-015 report_raw 无视节流,恒发两行且走**裸发**通道 * TC-TASKPROF-016 行格式:以 [PROF]/[PROF2] 开头、以 \r\n 结尾 * TC-TASKPROF-017 周期→微秒换算按 SystemCoreClock(168 MHz 下 1680 周期 = 10 us) * TC-TASKPROF-018 gl= 是所有槽 glitch 的汇总,不是单槽 * TC-TASKPROF-019 段内进行时的 n= 字段非零(此刻仍在段内的可视性) * TC-TASKPROF-020 count 是累计值,上报不清零 * * 通过:全部 TC PASS(main 返回 0)。 * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 * * 创建日期:2026-10-23 */ #include #include #include "console_log.h" /* 真实头:DBG_CON_LINE_MAX + 两个出口 */ #include "task_prof.h" #include "task_prof.c" /* ===================== 断言框架 ===================== */ static int s_total; static int s_passed; static int s_failed; #define CHECK(cond, name) do { \ s_total++; \ if (cond) { s_passed++; printf(" PASS %s\n", name); } \ else { s_failed++; printf(" FAIL %s (line %d)\n", name, __LINE__); } \ } while (0) /* ===================== 桩实体 ===================== */ static tp_coredebug_t g_coredebug; static tp_dwt_t g_dwt; tp_coredebug_t *const CoreDebug = &g_coredebug; tp_dwt_t *const DWT = &g_dwt; uint32_t SystemCoreClock = 168000000u; /* 真机口径:168 MHz */ static uint32_t g_tick; /* HAL_GetTick 的可控值 */ uint32_t HAL_GetTick(void) { return g_tick; } /* 出口留档:两条通道分别计数;**每一行都单独留档**(一次上报会连发 * [PROF] 与 [PROF2] 两行,只存"最后一行"就没法断言第一行的字段) */ #define CAP_MAX 8 static int g_line_calls; static int g_raw_calls; static char g_line[CAP_MAX][DBG_CON_LINE_MAX]; static void cap(int slot, const char *src, unsigned len) { unsigned i; if (slot >= CAP_MAX) { return; } for (i = 0u; (i < len) && (i + 1u < DBG_CON_LINE_MAX); i++) { g_line[slot][i] = src[i]; } g_line[slot][i] = '\0'; } int dbg_console_write_line(const char *line, unsigned len) { cap(g_line_calls + g_raw_calls, line, len); g_line_calls++; return (int)len; } void dbg_console_write_raw(const char *buf, unsigned len) { cap(g_line_calls + g_raw_calls, buf, len); g_raw_calls++; } void dbg_console_writer_attach(void) { } uint8_t dbg_console_writer_ready(void) { return 1u; } /* ===================== 辅助 ===================== */ static void reset_all(void) { (void)memset(s_prof, 0, sizeof(s_prof)); (void)memset(&g_coredebug, 0, sizeof(g_coredebug)); (void)memset(&g_dwt, 0, sizeof(g_dwt)); s_dwt_ok = 0u; s_prof_reported = 0u; s_prof_last = 0u; g_tick = 0u; g_line_calls = 0; g_raw_calls = 0; (void)memset(g_line, 0, sizeof(g_line)); } static void set_cyc(uint32_t v) { g_dwt.CYCCNT = v; } /* 从指定行的键上取整数值;键不存在或其后不是数字则返回 -1(断言因此会响)。 * 手写解析而不用 sscanf:后者在 MSVC 下触发 C4996(弃用告警),门禁输出要干净 */ static long field_of(int slot, const char *p_key) { const char *p; long v = 0; int digits = 0; if (slot >= CAP_MAX) { return -1L; } p = strstr(g_line[slot], p_key); if (p == NULL) { return -1L; } p += strlen(p_key); while ((*p >= '0') && (*p <= '9')) { v = (v * 10) + (long)(*p - '0'); p++; digits++; } return (digits > 0) ? v : -1L; } /* ===================== 样本闸门 ===================== */ static void tc_sample_gate(void) { printf("-- TC-TASKPROF-001..005 sample gate --\n"); reset_all(); task_prof_sample(TASK_PROF_UI_PERIOD, 500u); CHECK((s_prof[TASK_PROF_UI_PERIOD].count == 1u) && (s_prof[TASK_PROF_UI_PERIOD].last == 500u) && (s_prof[TASK_PROF_UI_PERIOD].max == 500u), "TC-TASKPROF-001 valid sample records count/last/max"); task_prof_sample(TASK_PROF_UI_PERIOD, 300u); CHECK((s_prof[TASK_PROF_UI_PERIOD].max == 500u) && (s_prof[TASK_PROF_UI_PERIOD].last == 300u) && (s_prof[TASK_PROF_UI_PERIOD].count == 2u), "TC-TASKPROF-002 smaller sample does not change max"); reset_all(); task_prof_sample(TASK_PROF_UI_PERIOD, 0u); CHECK((s_prof[TASK_PROF_UI_PERIOD].glitch == 1u) && (s_prof[TASK_PROF_UI_PERIOD].count == 0u) && (s_prof[TASK_PROF_UI_PERIOD].max == 0u), "TC-TASKPROF-003 cyc=0 only accumulates glitch (DWT reset defence)"); reset_all(); task_prof_sample(TASK_PROF_UI_PERIOD, SystemCoreClock * 10u); CHECK((s_prof[TASK_PROF_UI_PERIOD].glitch == 1u) && (s_prof[TASK_PROF_UI_PERIOD].count == 0u), "TC-TASKPROF-004 over-upper-bound sample only accumulates glitch"); reset_all(); task_prof_sample((task_prof_id_t)TASK_PROF_ID_COUNT, 100u); task_prof_sample((task_prof_id_t)(TASK_PROF_ID_COUNT + 1), 100u); task_prof_enter((task_prof_id_t)TASK_PROF_ID_COUNT); task_prof_exit((task_prof_id_t)TASK_PROF_ID_COUNT); CHECK((s_prof[0].count == 0u) && (s_prof[TASK_PROF_ID_COUNT - 1].count == 0u), "TC-TASKPROF-005 out-of-range id returns safely, touches no slot"); } /* ===================== 时基与 enter/exit 配对 ===================== */ static void tc_enter_exit(void) { printf("-- TC-TASKPROF-006..010 dwt enable + enter/exit --\n"); reset_all(); set_cyc(1000u); (void)task_prof_cyc(); CHECK(((g_coredebug.DEMCR & CoreDebug_DEMCR_TRCENA_Msk) != 0u) && ((g_dwt.CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0u) && (s_dwt_ok == 1u), "TC-TASKPROF-006 first timebase read lazily enables DWT"); reset_all(); set_cyc(2000u); task_prof_enter(TASK_PROF_UI_WORK); CHECK((s_prof[TASK_PROF_UI_WORK].in_seg == 1u) && (s_prof[TASK_PROF_UI_WORK].t_start == 2000u), "TC-TASKPROF-007 enter sets in_seg and records the start point"); set_cyc(2640u); task_prof_exit(TASK_PROF_UI_WORK); CHECK((s_prof[TASK_PROF_UI_WORK].in_seg == 0u) && (s_prof[TASK_PROF_UI_WORK].last == 640u) && (s_prof[TASK_PROF_UI_WORK].count == 1u), "TC-TASKPROF-008 exit settles the delta and clears in_seg"); reset_all(); task_prof_exit(TASK_PROF_ADC_BEAT); CHECK((s_prof[TASK_PROF_ADC_BEAT].count == 0u) && (s_prof[TASK_PROF_ADC_BEAT].glitch == 0u), "TC-TASKPROF-009 exit without enter records nothing (guards fake huge sample from 0 start)"); reset_all(); set_cyc(100u); task_prof_enter(TASK_PROF_NET_POLL); set_cyc(150u); task_prof_exit(TASK_PROF_NET_POLL); task_prof_exit(TASK_PROF_NET_POLL); CHECK(s_prof[TASK_PROF_NET_POLL].count == 1u, "TC-TASKPROF-010 repeated exit does not double count"); } /* ===================== 上报节流与通道 ===================== */ static void tc_report_throttle(void) { printf("-- TC-TASKPROF-011..015 report throttle + channels --\n"); reset_all(); g_tick = 4999u; task_prof_report(); CHECK((g_line_calls == 0) && (g_raw_calls == 0), "TC-TASKPROF-011 nothing sent before the first 5 s"); g_tick = 5000u; task_prof_report(); CHECK((g_line_calls == 2) && (g_raw_calls == 0), "TC-TASKPROF-012 on schedule: sends two lines via the ring buffer"); g_tick = 6999u; task_prof_report(); CHECK(g_line_calls == 2, "TC-TASKPROF-013 less than 2 s apart sends nothing"); g_tick = 7000u; task_prof_report(); CHECK(g_line_calls == 4, "TC-TASKPROF-014 after a full 2 s sends two lines again"); g_line_calls = 0; g_raw_calls = 0; task_prof_report_raw(); CHECK((g_raw_calls == 2) && (g_line_calls == 0), "TC-TASKPROF-015 report_raw ignores throttling and uses the raw channel"); } /* ===================== 行内容与换算 ===================== */ static void tc_line_format(void) { printf("-- TC-TASKPROF-016..020 line content --\n"); /* 016:一次上报连发两行;行首前缀是采集脚本的抓取依据,行尾必须是 CRLF */ reset_all(); g_tick = 5000u; task_prof_report(); CHECK((g_line_calls == 2) && (strncmp(g_line[0], "[PROF] ", 7) == 0) && (strncmp(g_line[1], "[PROF2] ", 8) == 0), "TC-TASKPROF-016 the two lines start with [PROF] / [PROF2] respectively"); CHECK((strstr(g_line[0], "\r\n") != NULL) && (strstr(g_line[1], "\r\n") != NULL), "TC-TASKPROF-016b both lines end with CRLF"); CHECK(field_of(0, "uip=") == 0L, "TC-TASKPROF-016c an unsampled slot prints 0"); /* 017:打印的是**微秒**,分母是 SystemCoreClock —— 断言落在打印字段上 */ reset_all(); task_prof_sample(TASK_PROF_UI_PERIOD, 1680u); /* 168 MHz → 10 us */ task_prof_sample(TASK_PROF_ADC_BEAT, 3360u); /* 168 MHz → 20 us */ g_tick = 5000u; task_prof_report(); CHECK(field_of(0, "uip=") == 10L, "TC-TASKPROF-017 168 MHz: max of 1680 cycles prints as uip=10"); CHECK(field_of(0, "adcm=") == 20L, "TC-TASKPROF-017b same clock: max of 3360 cycles prints as adcm=20"); SystemCoreClock = 84000000u; /* 改时钟树而忘了同步 → 整表偏移,这里必须显式跟着变 */ g_tick = 7000u; task_prof_report(); CHECK(field_of(2, "uip=") == 20L, "TC-TASKPROF-017c at 84 MHz the same cycle counts print as 20"); SystemCoreClock = 168000000u; /* 018:gl= 是**所有槽**的 glitch 汇总,不是当前槽 */ reset_all(); task_prof_sample(TASK_PROF_UI_PERIOD, 0u); task_prof_sample(TASK_PROF_ADC_BEAT, 0u); task_prof_sample(TASK_PROF_NET_POLL, 0u); g_tick = 5000u; task_prof_report(); CHECK(field_of(0, "gl=") == 3L, "TC-TASKPROF-018 gl= sums the glitch of all slots"); /* 019:段未结算时,"此刻已在段内多久"要看得见(uipn / netn 两个字段) */ /* 字段口径 = 上报时刻的 CYCCNT 减去各自的进段时刻(**不是**样本值): * UI_PERIOD 进段 @1000、NET_POLL 进段 @10000,上报时 CYCCNT=51680 * uipn = (51680-1000)/168 = 301 netn = (51680-10000)/168 = 248 */ reset_all(); set_cyc(1000u); task_prof_enter(TASK_PROF_UI_PERIOD); set_cyc(10000u); task_prof_enter(TASK_PROF_NET_POLL); set_cyc(51680u); g_tick = 5000u; task_prof_report(); CHECK(field_of(0, "uipn=") == 301L, "TC-TASKPROF-019 inside UI_PERIOD segment: uip= reports the us elapsed so far"); CHECK(field_of(1, "netn=") == 248L, "TC-TASKPROF-019b inside NET_POLL segment: netn= of [PROF2] is visible too"); CHECK(field_of(0, "adcn=") == 0L, "TC-TASKPROF-019c ADC_BEAT not in segment prints 0 (no cross-slot leakage)"); CHECK(s_prof[TASK_PROF_UI_PERIOD].in_seg == 1u, "TC-TASKPROF-019d reporting does not consume segment state (observe only)"); /* 020:count 是累计量,两轮上报之间不能清零 */ reset_all(); task_prof_sample(TASK_PROF_UI_WORK, 100u); g_tick = 5000u; task_prof_report(); g_tick = 7000u; task_prof_report(); CHECK((s_prof[TASK_PROF_UI_WORK].count == 1u) && (field_of(2, "uic=") == 0L) && (field_of(0, "uic=") == 0L), "TC-TASKPROF-020 reporting does not clear counters (UI_WORK not in uic= field, still cumulative 1)"); } /* ===================== 入口 ===================== */ int main(void) { printf("=== Task Prof Tests ===\n"); tc_sample_gate(); tc_enter_exit(); tc_report_throttle(); tc_line_format(); printf("\n=== Task Prof Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; }