Files
STM32F4-Base/test/host/watchdog/watchdog_test.c
T
wandering1 883c9dd0cc feat(adc): B1 实现 ADC 采集链路(adcTask + 看门狗)
B1(ADC 采集链路)落地,消化 P1-1:
- adc_task.c:空桩 -> 真实消费者。每 1ms 触发一次 CONVST,
  用 tpafe5160_snapshot() 原子取数(AGENTS 铁律)+ 帧统计 +
  每 10s 心跳日志。只取数,不做保护运算。
- Src/freertos.c:手工创建 adcTask(512 words / Normal,落
  USER CODE 保护区,同 timeSyncTask 先例)。
- task_watchdog:新增 TASK_WD_ADC;required_mask 产品 0x37 ->
  0x77、ENABLE_HW_LAYER_TESTS 0x35 -> 0x75。
- default_task.c:栈水位探针增列 adcTask(extern + 表项)。
- 脚本:stack_report.py 增 ROOTS/RUNTIME_ALIAS;gen_task_table
  增 adcTask(README/AGENTS 任务表已 --write)。
- 单测:watchdog_test.c beat_all/掩码断言同步(39 断言全绿)。
- 文档:待处理内容/看门狗设计/RAM 方案/继保规划 同步。

验证:@build 0 错 0 警;stack_report 静态最坏链 504B(余 75%);
pre-commit 全绿。剩 @test 实机闭环。
2026-10-10 14:33:32 +08:00

316 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 Watchdog 主机侧单元测试
* 模块功能:在 PC 上对 App/util/task_watchdog.c 的**判定状态机**做穷举验证。
*
* 被测对象:真实源文件 ../App/util/task_watchdog.c(以 #include 原样纳入,
* 保证测的是生产代码而非副本)。硬件访问由本文件提供的三个强符号
* 平台钩子(wd_platform_feed/start/tick_ms)替换为可观测桩。
*
* 隔离:每个用例前调用 task_wd_test_reset()(生产文件中由
* TASK_WATCHDOG_UNIT_TEST 守卫的测试专用复位,MCU 构建零开销)
* 把模块静态态恢复到"刚上电"。
*
* 覆盖点(对应 docs/测试说明/看门狗单元测试说明.md 的 TC-WD-xxx):
* TC-WD-001 未启动时 poll 不做任何判定(也不喂狗)
* TC-WD-002 节流:< 100ms 的 poll 直接返回
* TC-WD-003 全员到位置位 → 喂狗一次,且位图取走清零
* TC-WD-004 取走清零语义:旧位不残留(下一轮必须重新置位)
* TC-WD-005 掩码判定:缺任一位不喂,last_mask 精确记录
* TC-WD-006 连续 1..7 次缺席不喂不报;第 8 次打 [WD] task stall
* TC-WD-007 停喂后 stall 日志持续复报(现状行为,见用例内说明)
* TC-WD-008 恢复:重新全员到位 → 喂狗 + streak 归零
* TC-WD-009 节流窗口边界 99/100/199/200
* TC-WD-010 tick 回绕安全(无符号差)
* TC-WD-011 观测接口:last_mask / is_armed
* TC-WD-013 非法任务 ID 被忽略(防位图越界)
* TC-WD-014 start 幂等
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-01
*/
/* 打开生产文件里的测试专用复位钩子(MCU 构建不定义此宏) */
#define TASK_WATCHDOG_UNIT_TEST 1
#include <stdint.h>
#include <stdio.h>
/*
* 先纳入桩头文件,再纳入生产源码——保证 IWDG_TypeDef / HAL_GetTick 等
* 类型与声明在使用前可见(生产源码内部是同名 include,走 include guard 不再重复)。
* - stub/main.h → HAL_GetTick 声明
* - stub/iwdg.h → IWDG_TypeDef + IWDG 外部声明
* - stub/dbg_log.h→ DBG_* 映射与 g_wdtest_err_count
*
* 注意:生产源码里的三个 __weak 平台默认实现由 TASK_WATCHDOG_UNIT_TEST 守卫
* 而不编入(避免与本文件桩重名),故主机侧无需处理 __weak 关键字。
*/
#include "main.h"
#include "iwdg.h"
#include "dbg_log.h"
/* ===================== 平台桩:时间与 IWDG ===================== */
/* 可控毫秒时钟(单测手动推进;真实 MCU 由 TIM7 累加) */
static uint32_t s_tick = 0u;
/* IWDG 影子寄存器:真实代码写 IWDG->KR = x,这里落到本对象 */
static IWDG_TypeDef s_iwdg_shadow;
IWDG_TypeDef *IWDG = &s_iwdg_shadow;
/* 喂狗 / 启动 计数(平台钩子观测) */
static int s_feed_count = 0;
static int s_start_count = 0;
/* DBG_ERROR 计数(停喂时应打 [WD] task stall) */
int g_wdtest_err_count = 0;
/* ---- 强符号覆盖:三个平台钩子(覆盖 task_watchdog.c 的 __weak 默认实现) ---- */
void wd_platform_feed(void) { s_feed_count++; s_iwdg_shadow.KR = 0x0000AAAAu; }
void wd_platform_start(void) { s_start_count++; s_iwdg_shadow.KR = 0x0000CCCCu; }
uint32_t wd_platform_tick_ms(void) { return s_tick; }
/* main.h 声明的 HAL 时基 */
uint32_t HAL_GetTick(void) { return s_tick; }
/* ===================== 测试框架(极简) ===================== */
static int s_total = 0, s_passed = 0, s_failed = 0;
#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)
/* ===================== 被测代码 ===================== */
/* 原样纳入生产源文件(stub/ 里的 main.h / iwdg.h / dbg_log.h 生效) */
#include "task_watchdog.c"
/* ===================== 辅助 ===================== */
/* 每个用例前的干净起点:复位模块静态态 + 清空观测计数 + 时钟归零 */
static void fresh(void) {
task_wd_test_reset();
s_tick = 0u;
s_feed_count = 0;
s_start_count = 0;
g_wdtest_err_count = 0;
s_iwdg_shadow.KR = 0u;
}
/* 推进时间并触发一次 poll */
static void poll_advance(uint32_t ms) {
s_tick += ms;
task_wd_poll();
}
/*
* 启动并清空喂狗计数:task_wd_start() 内部会先喂一次(让首个抽样窗口从启动
* 时刻起算,见生产代码注释),该次喂狗由 TC-WD-015 单独断言;其余用例只关心
* "启动之后 poll 的喂狗行为",故此处把计数归零。
*/
static void start_and_clear(void) {
task_wd_start();
s_feed_count = 0;
}
/* 全员置位(默认必需掩码 0x77 = bit0|1|2|4|5|6,含 adcTask) */
static void beat_all(void) {
task_wd_beat(TASK_WD_DEFAULT);
task_wd_beat(TASK_WD_NET);
task_wd_beat(TASK_WD_FTP);
task_wd_beat(TASK_WD_UI);
task_wd_beat(TASK_WD_TIMESYNC);
task_wd_beat(TASK_WD_ADC);
}
/* ===================== 用例 ===================== */
static void tc_001_not_armed(void) {
printf("TC-WD-001 未启动时 poll 不做判定\n");
beat_all();
poll_advance(1000);
CHECK(s_feed_count == 0, "未启动时 poll 不喂狗");
CHECK(task_wd_is_armed() == 0, "is_armed == 0(未启动)");
}
static void tc_002_throttle(void) {
printf("TC-WD-002 节流:< 100ms 直接返回\n");
start_and_clear();
beat_all();
poll_advance(50); /* 50ms < 100ms → 不抽样 */
CHECK(s_feed_count == 0, "50ms 内 poll 不喂狗");
poll_advance(50); /* 累计 100ms → 抽样 */
CHECK(s_feed_count == 1, "累计 100ms 后喂狗 1 次");
}
static void tc_003_all_beat_feeds(void) {
printf("TC-WD-003 全员到位 → 喂狗一次\n");
start_and_clear();
beat_all();
poll_advance(100);
CHECK(s_feed_count == 1, "全员到位喂狗 1 次");
CHECK(task_wd_last_mask() == 0x77u, "last_mask == 0x77(bit0|1|2|4|5|6)");
}
static void tc_004_take_and_clear(void) {
printf("TC-WD-004 取走清零:旧位不残留\n");
start_and_clear();
beat_all();
poll_advance(100); /* 消费位图并喂狗 */
CHECK(s_feed_count == 1, "第一轮喂狗");
poll_advance(100); /* 不重新置位 */
CHECK(s_feed_count == 1, "未重新置位 → 第二轮不喂狗(位图已取走清零)");
}
static void tc_005_mask_required(void) {
printf("TC-WD-005 掩码判定:缺任一位不喂\n");
start_and_clear();
/* 故意缺 UI(bit4) */
task_wd_beat(TASK_WD_DEFAULT);
task_wd_beat(TASK_WD_NET);
task_wd_beat(TASK_WD_FTP);
task_wd_beat(TASK_WD_TIMESYNC);
task_wd_beat(TASK_WD_ADC);
poll_advance(100);
CHECK(s_feed_count == 0, "缺 UI 位 → 不喂狗");
CHECK(task_wd_last_mask() == (0x77u & ~(1u << TASK_WD_UI)),
"last_mask 精确记录缺失位");
}
static void tc_006_stall_8(void) {
printf("TC-WD-006 连续 8 次缺席 → 打 stall 并停喂\n");
start_and_clear();
beat_all(); poll_advance(100); /* streak 清零 */
int err0 = g_wdtest_err_count;
for (int i = 1; i <= 7; i++) {
poll_advance(100);
CHECK(g_wdtest_err_count == err0, "第 1..7 次缺席不报 stall");
}
poll_advance(100); /* 第 8 次 */
CHECK(g_wdtest_err_count == err0 + 1, "第 8 次缺席打 [WD] task stall");
CHECK(s_feed_count == 1, "全程仅在开始时喂过 1 次(缺席期间未喂)");
}
static void tc_007_stall_repeats_until_reset(void) {
printf("TC-WD-007 停喂后 stall 日志持续复报(当前设计:每 100ms 一条)\n");
start_and_clear();
for (int i = 0; i < 8; i++) poll_advance(100); /* 第 8 次触发 */
int err8 = g_wdtest_err_count;
CHECK(err8 == 1, "第 8 次缺席后已报 1 条 stall");
poll_advance(100); /* 第 9 次 */
poll_advance(100); /* 第 10 次 */
/*
* 当前实现:streak 封顶 8,但判定条件 `streak >= 8` 在后续每轮仍成立
* → 每 100ms 复报一条("最后遗言"持续到 IWDG ~900ms 后复位)。
* 这是**刻意记录**的现状行为:它让复位前的日志窗口内信息不丢;
* 代价是日志冗余(约 9 条/停顿周期)。若改为"每停顿周期仅报一次",
* 需加一个 s_stall_reported 标志,见文档"已知可优化点"。
*/
CHECK(g_wdtest_err_count == err8 + 2, "停喂后每次抽样都复报 stall(现状)");
}
static void tc_008_recover(void) {
printf("TC-WD-008 恢复:重新全员到位 → 喂狗 + streak 归零\n");
start_and_clear();
for (int i = 0; i < 8; i++) poll_advance(100); /* 触发 stall */
beat_all();
poll_advance(100);
CHECK(s_feed_count == 1, "恢复后立即喂狗");
int err0 = g_wdtest_err_count;
poll_advance(100); /* 再缺一次 */
CHECK(g_wdtest_err_count == err0, "streak 已归零:单次缺席不报 stall");
}
static void tc_009_throttle_boundary(void) {
printf("TC-WD-009 节流窗口边界 99/100/199/200\n");
start_and_clear();
beat_all();
poll_advance(99); CHECK(s_feed_count == 0, "99ms 不判");
poll_advance(1); CHECK(s_feed_count == 1, "100ms 判(喂狗)");
beat_all();
poll_advance(99); CHECK(s_feed_count == 1, "再 99ms 不判");
poll_advance(1); CHECK(s_feed_count == 2, "累计 100ms 判");
}
static void tc_010_tick_wrap(void) {
printf("TC-WD-010 tick 回绕安全\n");
start_and_clear();
s_tick = 0xFFFFFF00u; /* 推到接近 2^32 回绕 */
task_wd_poll(); /* 建立基线(此处可能不判,取决节流) */
beat_all();
s_feed_count = 0;
poll_advance(0x100u); /* 跨越回绕:0xFFFFFF00 + 0x100 = 0 */
CHECK(s_feed_count == 1, "回绕处 100ms 仍正确判(无符号差)");
}
static void tc_011_observers(void) {
printf("TC-WD-011 观测接口语义\n");
start_and_clear();
CHECK(task_wd_is_armed() == 1, "is_armed == 1(已启动)");
beat_all();
poll_advance(100);
CHECK(task_wd_last_mask() == 0x77u, "last_mask 反映最近一次抽样");
CHECK(task_wd_miss_count() == 0u, "全程达标 → miss_count == 0");
}
static void tc_013_bad_id(void) {
printf("TC-WD-013 非法任务 ID 被忽略\n");
start_and_clear();
task_wd_beat((task_wd_id_t)(TASK_WD_ID_MAX + 3)); /* 非法:应被忽略 */
poll_advance(100);
CHECK(s_feed_count == 0, "非法 ID 未污染位图(必需位全缺 → 不喂)");
CHECK(task_wd_last_mask() == 0u, "位图保持 0(非法位未置)");
}
static void tc_014_start_idempotent(void) {
printf("TC-WD-014 start 幂等\n");
task_wd_start();
task_wd_start();
CHECK(s_start_count == 1, "重复 start 只启动 IWDG 一次");
CHECK(task_wd_is_armed() == 1, "is_armed == 1");
}
static void tc_015_start_feeds_once(void) {
printf("TC-WD-015 start 先喂一次(首个窗口从启动时刻起算)\n");
task_wd_start();
CHECK(s_start_count == 1, "start 启动 IWDG 一次");
CHECK(s_feed_count == 1, "start 内部先喂狗一次(避免启动即超时)");
}
static void tc_016_stall_not_armed_no_feed(void) {
printf("TC-WD-016 未启动时即便全员到位也不喂\n");
/* 未 start:poll 直接返回 */
beat_all();
poll_advance(500);
CHECK(s_feed_count == 0, "未启动 → 不喂狗(无论位图)");
CHECK(task_wd_last_mask() == 0u, "未启动 → 不更新 last_mask");
}
int main(void) {
printf("=== Task Watchdog 主机侧单元测试 ===\n");
printf("被测:App/util/task_watchdog.c(判定状态机)\n\n");
fresh(); tc_001_not_armed();
fresh(); tc_016_stall_not_armed_no_feed();
fresh(); tc_014_start_idempotent();
fresh(); tc_015_start_feeds_once();
fresh(); tc_002_throttle();
fresh(); tc_003_all_beat_feeds();
fresh(); tc_004_take_and_clear();
fresh(); tc_005_mask_required();
fresh(); tc_006_stall_8();
fresh(); tc_007_stall_repeats_until_reset();
fresh(); tc_008_recover();
fresh(); tc_009_throttle_boundary();
fresh(); tc_010_tick_wrap();
fresh(); tc_011_observers();
fresh(); tc_013_bad_id();
printf("\n=== Task Watchdog Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}