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 一起记给我。
This commit is contained in:
2026-10-25 17:58:18 +08:00
parent 138aebc6d6
commit 2d16fdddc0
5 changed files with 189 additions and 52 deletions
+68 -8
View File
@@ -163,12 +163,13 @@ static void tc_004_take_and_clear(void) {
beat_all();
poll_advance(100); /* 消费位图并喂狗 */
CHECK(s_feed_count == 1, "first round fed");
poll_advance(100); /* 不重新置位 */
CHECK(s_feed_count == 1, "no re-set -> second round does not feed (bitmap already taken and cleared)");
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: missing any bit -> no feed\n");
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);
@@ -179,13 +180,13 @@ static void tc_005_mask_required(void) {
task_wd_beat(TASK_WD_IEC104);
task_wd_beat(TASK_WD_PROTECT);
poll_advance(100);
CHECK(s_feed_count == 0, "UI bit missing -> no feed");
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 misses -> print stall and stop feeding\n");
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;
@@ -195,7 +196,7 @@ static void tc_006_stall_8(void) {
}
poll_advance(100); /* 第 8 次 */
CHECK(g_wdtest_err_count == err0 + 1, "8th miss prints [WD] task stall");
CHECK(s_feed_count == 1, "fed exactly once at the start only (nothing fed during the missing period)");
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) {
@@ -222,12 +223,69 @@ static void tc_008_recover(void) {
for (int i = 0; i < 8; i++) poll_advance(100); /* 触发 stall */
beat_all();
poll_advance(100);
CHECK(s_feed_count == 1, "feeds immediately after recovery");
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();
@@ -265,7 +323,7 @@ static void tc_013_bad_id(void) {
start_and_clear();
task_wd_beat((task_wd_id_t)(TASK_WD_ID_MAX + 3)); /* 非法:应被忽略 */
poll_advance(100);
CHECK(s_feed_count == 0, "invalid id does not pollute the bitmap (all required bits missing -> no feed)");
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)");
}
@@ -308,6 +366,8 @@ int main(void) {
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();