Files
STM32F4-Base/test/host/watchdog/watchdog_test.c
T
wandering1 2d16fdddc0 fix(看门狗): 停喂判定按位分账——复合串窗不再误停喂(2026-10-25 大窗判读 F2)
道F task-23。动因 = 2026-10-25 台架大窗判读:9 条 stall 行里 16:24/16:28 两起为
复合判据误升级——旧逻辑喂狗前置 = 全必需位同窗跑过,任一窗缺任一位即断喂,
复合连续 8 窗申报 stall;UI 一窗缺、IEC104 下一窗缺、再下窗 FTP 缺这类互不相干
的亚秒抖动被串成 800ms 假停摆(mg=877/918,两事件中无任何任务达单任务容忍线)。
16:39 那起才是真 UI 单任务串(~1.2s,bit4 到 8 连击——受害者面修复 = F3 票)。

【改造】stall/停喂唯一判据 = 任一必需位自己的连续缺席窗数(s_miss_bits[] 分账,
+9B BSS):同一任务 8 窗(800ms)没跑过 → 申报 + 停喂 → IWDG 自然溢出
(IWDGRSTF 权威归因不变);所有位连击 <8 的缺席窗照常喂狗;位出现即该位清零,
掩码外位不入账(wd_required_mask 随构建变化自动跟随、不留旧账)。`MISS start`
保留为复合边沿诊断行(不再代表停喂,判读口径以设计说明 §6.1/§11.1 为准)。
代价:真停摆从挂死到复位总时长最坏 +约 0.7s(≈2.5s 最坏 / ≈3.3s 标称),V2
台架判据「声明 stall 后 ≤2.5s 复位」不受影响(申报时点未变,停喂起点即申报)。

【测试】test/host/watchdog 断言按新判据改写 5 处(004/005/006/008/013:缺席窗
照常喂),新增 TC-WATCHDOG-017(20 窗 UI/NET 交替缺席:零 stall + 20 窗持续喂——
旧复合判据必在第 8 窗误报,回归钉)与 TC-WATCHDOG-018(同位 8/9/10 连击必报停
+ 停喂;恢复窗喂、账本重启,第二段亚容忍串重新从零计)。47/47 全绿。

【文档】看门狗设计说明 §0/§2.2(停喂链加「v1.5 起字面成立」注)/§3.2/§5.3/
§6.1/§11.1/§11.6 大窗补记 + 变更记录;看门狗单元测试说明(用例表 + 47 断言 +
变更记录,顺带修正 TC-WATCHDOG-003 行 10-12 后未跟的 `0x37`→`0x1F7`)。

【越界申报】① docs/测试说明/看门狗单元测试说明.md——票面写域外,但该文档
§4 自要求「行为变更须同步改单测与本文」,执行性同步;② 根目录
.cubemx_survival_baseline.json——cubemx-survival 钩子对 task_watchdog.c(watchdog-
module 审计项 = 全文哈希基线)明示「有意改动请 --write 重建基线」,重建后
31 通过 / 0 失败。均非他人并行改动面。

【门禁】@build 0 错误 0 警告(Code=224768 RO=24640 RW=1328 ZI=135096)。
--all-files 43 绿;红仅 mem-numbers/mem-budget = 台账漂移(vs 2026-10-25 行:
Code +80B / ER_IROM1 249328→249408 / ZI +8B(s_miss_bits)/ IRAM1 约 +8B,待回填)。
id-check 增量拦截通过(首跑拦下变更记录里 TC-003/TC-004 裸形,已改全 ID)、
test-coverage 通过、cubemx-survival 通过。
gate: only mem-budget/mem-numbers pending lead ledger backfill

【台架】等你排窗两轮(各 ≥30min):单独 relisten_stress :7003 / 探针轮。判据按
v1.1 口径 = stall 行零为硬判据;预期本修复后 16:24/16:28 型复合误升级归零,
MISS start 仍会出现(诊断量);16:39 型 UI 真单串在 F3 落地前仍可能照报——
那属于「判据抓对了人」,请连同 mask 一起记给我。
2026-10-25 17:58:18 +08:00

380 lines
16 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-WATCHDOG-001 未启动时 poll 不做任何判定(也不喂狗)
* TC-WATCHDOG-002 节流:< 100ms 的 poll 直接返回
* TC-WATCHDOG-003 全员到位置位 → 喂狗一次,且位图取走清零
* TC-WATCHDOG-004 取走清零语义:旧位不残留(下一轮必须重新置位)
* TC-WATCHDOG-005 掩码判定:缺任一位不喂,last_mask 精确记录
* TC-WATCHDOG-006 连续 1..7 次缺席不喂不报;第 8 次打 [WD] task stall
* TC-WATCHDOG-007 停喂后 stall 日志持续复报(现状行为,见用例内说明)
* TC-WATCHDOG-008 恢复:重新全员到位 → 喂狗 + streak 归零
* TC-WATCHDOG-009 节流窗口边界 99/100/199/200
* TC-WATCHDOG-010 tick 回绕安全(无符号差)
* TC-WATCHDOG-011 观测接口:last_mask / is_armed
* TC-WATCHDOG-013 非法任务 ID 被忽略(防位图越界)
* TC-WATCHDOG-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-WATCHDOG-015 单独断言;其余用例只关心
* "启动之后 poll 的喂狗行为",故此处把计数归零。
*/
static void start_and_clear(void) {
task_wd_start();
s_feed_count = 0;
}
/* 全员置位(默认必需掩码 0x1F7 = bit0|1|2|4|5|6|7|8,含 adc/iec104/protect) */
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);
task_wd_beat(TASK_WD_IEC104);
task_wd_beat(TASK_WD_PROTECT);
}
/* ===================== 用例 ===================== */
static void tc_001_not_armed(void) {
printf("TC-WATCHDOG-001 poll makes no decision before start\n");
beat_all();
poll_advance(1000);
CHECK(s_feed_count == 0, "poll does not feed before start");
CHECK(task_wd_is_armed() == 0, "is_armed == 0 (not started)");
}
static void tc_002_throttle(void) {
printf("TC-WATCHDOG-002 throttle: < 100ms returns directly\n");
start_and_clear();
beat_all();
poll_advance(50); /* 50ms < 100ms → 不抽样 */
CHECK(s_feed_count == 0, "poll within 50ms does not feed");
poll_advance(50); /* 累计 100ms → 抽样 */
CHECK(s_feed_count == 1, "after 100ms accumulated, fed exactly once");
}
static void tc_003_all_beat_feeds(void) {
printf("TC-WATCHDOG-003 all tasks present -> feed once\n");
start_and_clear();
beat_all();
poll_advance(100);
CHECK(s_feed_count == 1, "all present feeds exactly once");
CHECK(task_wd_last_mask() == 0x1F7u, "last_mask == 0x1F7 (bit0|1|2|4|5|6|7|8)");
}
static void tc_004_take_and_clear(void) {
printf("TC-WATCHDOG-004 take-and-clear: no stale bits left\n");
start_and_clear();
beat_all();
poll_advance(100); /* 消费位图并喂狗 */
CHECK(s_feed_count == 1, "first round fed");
poll_advance(100); /* 不重新置位:空位图窗(各位 streak=1 < 容忍) */
CHECK(s_feed_count == 2, "v1.5: an empty-bitmap single window still feeds (streak 1 < tol)");
CHECK(task_wd_last_mask() == 0u, "bitmap was taken and cleared (no stale bits survive)");
}
static void tc_005_mask_required(void) {
printf("TC-WATCHDOG-005 mask check: one-window miss (streak 1) feeds; ledger records it\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);
task_wd_beat(TASK_WD_IEC104);
task_wd_beat(TASK_WD_PROTECT);
poll_advance(100);
CHECK(s_feed_count == 1, "UI absent for ONE window (streak 1 < tol) -> still feeds (v1.5)");
CHECK(task_wd_last_mask() == (0x1F7u & ~(1u << TASK_WD_UI)),
"last_mask records the missing bit exactly");
}
static void tc_006_stall_8(void) {
printf("TC-WATCHDOG-006 8 consecutive all-miss windows -> stall at the 8th, feeding stops\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, "misses 1..7 do not report stall");
}
poll_advance(100); /* 第 8 次 */
CHECK(g_wdtest_err_count == err0 + 1, "8th miss prints [WD] task stall");
CHECK(s_feed_count == 8, "v1.5: windows 1..7 (streak < tol) kept feeding; the 8th stops");
}
static void tc_007_stall_repeats_until_reset(void) {
printf("TC-WATCHDOG-007 after feeding stops the stall log keeps repeating (current design: one per 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, "after the 8th miss exactly 1 stall line was reported");
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, "after feeding stops every sample re-reports stall (current behaviour)");
}
static void tc_008_recover(void) {
printf("TC-WATCHDOG-008 recovery: all present again -> feed + streak reset\n");
start_and_clear();
for (int i = 0; i < 8; i++) poll_advance(100); /* 触发 stall */
beat_all();
poll_advance(100);
CHECK(s_feed_count == 8, "feeds immediately after recovery (7 sub-tol windows fed + recovery)");
int err0 = g_wdtest_err_count;
poll_advance(100); /* 再缺一次 */
CHECK(g_wdtest_err_count == err0, "streak already reset: a single miss does not report stall");
}
/* 全员置位但跳过一位(按位连击用例的激励器;0x1F7 = 默认必需掩码,同 tc_005 用法) */
static void beat_all_except(task_wd_id_t skip) {
uint32_t bits = 0x1F7u;
uint32_t i;
for (i = 0u; i < (uint32_t)TASK_WD_ID_MAX; i++) {
if (((bits & (1u << i)) != 0u) && (i != (uint32_t)skip)) {
task_wd_beat((task_wd_id_t)i);
}
}
}
/* v1.5 按位分账·修复本体:交替缺席(本窗缺 UI、下窗缺 NET……)任何单位连击都
* 到不了容忍值——旧复合判据会在第 8 窗误报 stall 并停喂(2026-10-25 大窗
* 16:24/16:28 型),新判据必须全程零申报、持续喂狗 */
static void tc_017_mixed_alternating_no_false_stall(void) {
printf("TC-WATCHDOG-017 alternating per-bit misses never chain into a stall\n");
start_and_clear();
for (int i = 1; i <= 20; i++) {
if ((i % 2) != 0) {
beat_all_except(TASK_WD_UI); /* 奇数窗:UI 缺席 */
} else {
beat_all_except(TASK_WD_NET); /* 偶数窗:NET 缺席 */
}
poll_advance(100);
}
CHECK(g_wdtest_err_count == 0, "20 mixed-miss windows report ZERO stall (composite would have fired at 8th)");
CHECK(s_feed_count == 20, "feeding continues in every mixed window");
}
/* v1.5 按位分账·保留杀伤力:同一任务连续缺席到容忍值必须照报停摆、停喂;
* 恢复后账本清零,第二段亚容忍缺席重新从零计(16:39 型真单任务串仍会被抓) */
static void tc_018_single_bit_streak_stalls_and_restarts(void) {
printf("TC-WATCHDOG-018 same-bit streak reaches tol -> stall + feeding stops; ledger restarts after recovery\n");
start_and_clear();
for (int i = 1; i <= 7; i++) {
beat_all_except(TASK_WD_IEC104);
poll_advance(100);
}
CHECK(g_wdtest_err_count == 0 && s_feed_count == 7,
"7 windows missing the SAME bit stay below tol: no stall, feeding continues");
for (int i = 8; i <= 10; i++) {
beat_all_except(TASK_WD_IEC104);
poll_advance(100);
}
CHECK(g_wdtest_err_count == 3, "8th/9th/10th consecutive same-bit miss each report stall");
CHECK(s_feed_count == 7, "feeding stops from the 8th window onward");
beat_all();
poll_advance(100);
CHECK(s_feed_count == 8, "recovery window feeds and clears the per-bit ledger");
for (int i = 1; i <= 7; i++) {
beat_all_except(TASK_WD_IEC104);
poll_advance(100);
}
CHECK(g_wdtest_err_count == 3 && s_feed_count == 15,
"second sub-tol streak starts from zero again: no new stall");
}
static void tc_009_throttle_boundary(void) {
printf("TC-WATCHDOG-009 throttle window edges 99/100/199/200\n");
start_and_clear();
beat_all();
poll_advance(99); CHECK(s_feed_count == 0, "99ms: no decision");
poll_advance(1); CHECK(s_feed_count == 1, "100ms: decides (feed)");
beat_all();
poll_advance(99); CHECK(s_feed_count == 1, "another 99ms: no decision");
poll_advance(1); CHECK(s_feed_count == 2, "accumulated 100ms: decides");
}
static void tc_010_tick_wrap(void) {
printf("TC-WATCHDOG-010 tick wraparound is safe\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, "across the wraparound 100ms still decides correctly (unsigned diff)");
}
static void tc_011_observers(void) {
printf("TC-WATCHDOG-011 observation API semantics\n");
start_and_clear();
CHECK(task_wd_is_armed() == 1, "is_armed == 1 (started)");
beat_all();
poll_advance(100);
CHECK(task_wd_last_mask() == 0x1F7u, "last_mask reflects the most recent sample");
CHECK(task_wd_miss_count() == 0u, "all rounds met -> miss_count == 0");
}
static void tc_013_bad_id(void) {
printf("TC-WATCHDOG-013 invalid task id is ignored\n");
start_and_clear();
task_wd_beat((task_wd_id_t)(TASK_WD_ID_MAX + 3)); /* 非法:应被忽略 */
poll_advance(100);
CHECK(s_feed_count == 1, "invalid id ignored; one all-miss window still feeds (streak 1 < tol, v1.5)");
CHECK(task_wd_last_mask() == 0u, "bitmap stays 0 (invalid bit never set)");
}
static void tc_014_start_idempotent(void) {
printf("TC-WATCHDOG-014 start is idempotent\n");
task_wd_start();
task_wd_start();
CHECK(s_start_count == 1, "repeated start launches IWDG only once");
CHECK(task_wd_is_armed() == 1, "is_armed == 1");
}
static void tc_015_start_feeds_once(void) {
printf("TC-WATCHDOG-015 start feeds once first (first window counted from the start instant)\n");
task_wd_start();
CHECK(s_start_count == 1, "start launches IWDG once");
CHECK(s_feed_count == 1, "start feeds once internally (avoids a timeout right at boot)");
}
static void tc_016_stall_not_armed_no_feed(void) {
printf("TC-WATCHDOG-016 even with all tasks present, nothing feeds before start\n");
/* 未 start:poll 直接返回 */
beat_all();
poll_advance(500);
CHECK(s_feed_count == 0, "not started -> no feed (whatever the bitmap)");
CHECK(task_wd_last_mask() == 0u, "not started -> last_mask not updated");
}
int main(void) {
printf("=== Task Watchdog host-side unit tests ===\n");
printf("Target: App/util/task_watchdog.c (decision state machine)\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_017_mixed_alternating_no_false_stall();
fresh(); tc_018_single_bit_streak_stalls_and_restarts();
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;
}