登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号) 一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、 采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。 门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏 undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8 reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF; id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。 test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字, 是本批唯一改变构建产物的文件集合。 为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged 部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、 不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。 实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01% (基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 / 告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。 基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处 引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使 识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。 ⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二 根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有), 今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。 固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0; App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889 断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿; check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带 BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。 .cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法: Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。 同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%, RW_CCM 57,376 B / 87.9%,阈值 92%。
12 KiB
看门狗单元测试说明(主机侧)
模块:Task Watchdog(IWDG + 任务心跳位图,
App/util/task_watchdog.c) 测试类型:主机侧纯逻辑单元测试(L0.5 层,见 §1.5)——在 PC 上离线验证 判定状态机,零硬件、零烧录、秒级、不占 MCU 的 RAM。 状态:✅ 已落地(47 断言,@build0/0,已挂 pre-commit 钩子) 日期:2026-10-01(2026-10-25 v1.5 按位分账改造,见变更记录) 关联文档:../子系统设计/看门狗设计说明.md(被测行为的设计依据)、测试开发标准流程.md(测试分层与门禁总览)、测试文档模板规范.md(本文命名规范)
1. 概述
1.1 为什么单独做主机侧单测
task_watchdog.c 分两半:
| 半边 | 内容 | 可测性 |
|---|---|---|
| 判定状态机(被测) | 位图取走清零、必需掩码比较、s_miss_bits[] 按位分账(容忍 = 同一位 8 连击)、100 ms 抽样节流、tick 回绕安全 |
纯逻辑,与硬件无关 → 可在 PC 上位穷举 |
| 硬件副作用 | IWDG->KR 写(喂狗 / 启动)、HAL_GetTick() |
需真机或桩 |
板端套件(test/board/)只能"在真机上完整跑一遍",无法穷举边界——例如:
- 节流窗口边界 99 ms / 100 ms / 199 ms / 200 ms 的分野;
- 连续缺席第 7 次 / 第 8 次 / 第 9 次的行为差异(停喂触发点);
HAL_GetTick()跨越 2^32 回绕后是否仍正确节流。
这些用主机侧单测一次性钉死,且不占 MCU 的 RAM(本项目 RAM 已 95%,见
docs/内存管理/)。设计上参照项目既有的"共享核心 + 平台后端"做法
(App/cfg/cfg_file_store.h:MCU 端 cfg_file_store_mcu.c / 模拟器端
cfg_file_store_sim.c)。
1.2 被测对象与桩
被测源文件以 #include 原样纳入测试单元,保证测的是生产代码而非副本:
test/host/watchdog/watchdog_test.c
├── #include "main.h" → stub/main.h (HAL_GetTick 声明)
├── #include "iwdg.h" → stub/iwdg.h (IWDG_TypeDef + IWDG 外部声明)
├── #include "dbg_log.h" → stub/dbg_log.h (DBG_* 映射 + 错误计数)
└── #include "task_watchdog.c" ← 生产源文件(App/util/),原样纳入
三个平台钩子(声明在 App/util/task_watchdog.h 末段"平台后端(内部)")在主机侧被替换为可观测桩:
| 钩子 | MCU 默认实现(__weak) |
主机侧桩 |
|---|---|---|
wd_platform_feed() |
IWDG->KR = 0x0000AAAAu |
计数 s_feed_count++ + 写影子寄存器 |
wd_platform_start() |
IWDG->KR = 0x0000CCCCu |
计数 s_start_count++ + 写影子寄存器 |
wd_platform_tick_ms() |
HAL_GetTick() |
返回单测可控的 s_tick |
生产端零增量:三个钩子在 MCU 构建下就是原来的寄存器写 / HAL 调用,行为逐字一致, 无额外分支、无额外符号(
@build0/0 已验)。平台默认实现由#ifndef TASK_WATCHDOG_UNIT_TEST守卫;测试专用复位task_wd_test_reset()仅在主机侧编译(MCU 构建零代码、零符号)。
1.3 测试环境
| 依赖 | 版本 / 说明 |
|---|---|
| 编译器 | 本机 MSVC(VS2019 BuildTools 14.29 / VS2022 任一),由 run_unit_test.py 自动探测 |
| Python | 仓库 .venv 或任意 python3(纯标准库) |
| 硬件 | 无(不需要开发板 / 串口 / ST-LINK) |
为何不调
vcvars64.bat:它内部调用reg.exe探测环境,本机安全策略禁止reg.exe。运行器因此直接拼INCLUDE/LIB环境变量并用绝对 路径调cl.exe(只需 MSVC 版本目录 + Windows SDK 的 include/lib),无副作用、可重复。该工具链逻辑不在本模块目录内(2026-10-01 抽取):已沉淀为通用 harness
../../test/host/common/unit_test_harness.py, 本模块的run_unit_test.py只声明TestBuild(源文件 / 桩目录 / 汇总行标记)。 新增 L0.5 单测时直接复用 harness,见../../test/host/common/README.md。
1.4 运行方式
# 仓库根目录(统一用 .venv)
& '.venv\Scripts\python.exe' test/host/watchdog/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
1.5 回归接入
| 项 | 说明 |
|---|---|
| 触发范围 | App/util/task_watchdog.c / task_watchdog.h,或 test/host/watchdog/**、test/host/common/unit_test_harness.py 变动 |
| 门禁层级 | L0.5 层(介于 L0 静态门禁与 L1 板端套件之间):主机侧、秒级、纯逻辑,比板端套件便宜、比静态检查更强 |
| pre-commit 钩子 | watchdog-unit-test(本地钩子,.pre-commit-config.yaml §7) |
| 汇总行 | === Task Watchdog Tests: N/N PASSED (failed=0) === |
| 与板端测试的关系 | 互补:本单测保证判定逻辑正确;板端 @test 故障注入(V2/V4,见 docs/子系统设计/看门狗设计说明.md §7 验证记录)保证真机复位链路正确 |
2. 测试用例
命名
TC-WD-NNN(WD = Watchdog)。全部在watchdog_test.c内,主机侧运行。
| 用例 | 断言 | 验证的设计点 |
|---|---|---|
| TC-WATCHDOG-001 | 未启动时 poll 不喂狗、is_armed()==0 |
启动前不判定(s_started 门卫) |
| TC-WATCHDOG-016 | 未启动时即使全员置位也不喂、不更新 last_mask |
同上(位图无关) |
| TC-WATCHDOG-014 | 重复 task_wd_start() 只启动 IWDG 一次 |
启动幂等(IWDG 不可逆,重复启动无意义) |
| TC-WATCHDOG-015 | start 内部先喂一次(feed_count==1) |
首个抽样窗口从启动时刻起算,避免启动即超时 |
| TC-WATCHDOG-002 | 50 ms 内 poll 不喂;累计 100 ms 喂 1 次 |
100 ms 抽样节流 |
| TC-WATCHDOG-003 | 全员置位 → 喂狗 1 次;last_mask==0x1F7 |
全员到位判定 + 掩码取值(掩码史:0x37→0x77→0x1F7,见设计说明变更记录 2026-10-10/12) |
| TC-WATCHDOG-004 | 不重新置位:第二窗(空位图,各位 streak=1)v1.5 照常喂;last_mask==0 证明位已取走 |
取走清零语义(旧位不残留);停喂只看按位连击 |
| TC-WATCHDOG-005 | 缺 UI(bit4)单窗(streak 1 < 容忍)→ 照常喂;last_mask 精确记录缺失位 |
按位分账(v1.5)+ 缺失定位 |
| TC-WATCHDOG-006 | 同一批缺席 1..7 窗不报且持续喂(feed_count==8);第 8 窗打 [WD] task stall 起停喂 |
WD_MISS_TOLERANCE=8(同一任务 800 ms)停喂触发点 |
| TC-WATCHDOG-007 | 停喂后每 100 ms 复报一条 stall(现状行为) | 见 §4 已知行为("最后遗言"持续到复位) |
| TC-WATCHDOG-008 | 恢复全员到位 → 立即喂狗 + 全部按位账本清零(v1.5:episode 内 1..7 窗已持续喂,恢复窗 feed_count==8) | 停顿可恢复(非锁死状态机) |
| TC-WATCHDOG-009 | 99 ms 不判 / 100 ms 判 / 再 99 ms 不判 / 累计 100 ms 判 | 节流窗口边界(易错点) |
| TC-WATCHDOG-010 | tick 跨 2^32 回绕后 100 ms 仍正确判 |
无符号差比较(回绕安全) |
| TC-WATCHDOG-011 | is_armed/last_mask/miss_count 语义 |
观测接口正确性 |
| TC-WATCHDOG-013 | 非法任务 ID(>= TASK_WD_ID_MAX)被忽略,不污染位图;空位图单窗(streak 1)v1.5 照常喂 |
防位图越界(task_wd_beat 的守卫) |
| TC-WATCHDOG-017 | UI/NET 交替缺席 20 窗 → 零 stall、20 窗全部持续喂(v1.5 回归钉:旧复合判据会在第 8 窗误申报) | 复合串窗根治(2026-10-25 大窗 16:24/16:28 型误升级) |
| TC-WATCHDOG-018 | 同一位(IEC104)缺席 7 窗:不报持续喂;8/9/10 窗连报 stall 且停喂;恢复窗喂并清零账本;第二段 7 窗亚容忍串再从零计、不误报 | 真单任务停摆保留杀伤力 + 账本重启(16:39 型真链仍可抓) |
当前结果:47/47 PASSED(failed=0)。
3. 如何扩展用例
- 在
watchdog_test.c加一个static void tc_0NN_xxx(void); - 用
fresh()开头(复位模块静态态 + 清观测计数 + 时钟归零); - 用
start_and_clear()启动(并清掉start自身那一次喂狗); beat_all()/task_wd_beat(...)置位,poll_advance(ms)推进时间并抽样;CHECK(cond, "描述")断言;- 在
main()里fresh(); tc_0NN_xxx();注册。
⚠ 别忘
fresh():task_wd_poll的节流基准s_last_tick与位图 / 计数都是模块 静态态,跨用例不隔离会导致假失败(本项目开发时即踩到,见 §4)。
4. 已知行为与可优化点
4.1 stall 日志在停喂后复报(TC-WATCHDOG-007,现状)
实现(task_watchdog.c):
if (s_miss_streak < WD_MISS_TOLERANCE) { s_miss_streak++; }
if (s_miss_streak >= WD_MISS_TOLERANCE) { DBG_ERROR("[WD] task stall: ..."); }
streak 封顶 8 后,判定条件 streak >= 8 在后续每轮仍成立 → 每 100 ms 复报一条,
直到 IWDG 约 900 ms 后复位(约 9 条/停顿周期)。
评估:这是刻意的"最后遗言"——复位前日志窗口内信息不丢。代价是日志冗余。
若改为"每停顿周期仅报一次",需加一个 s_stall_reported 标志(在 streak 归零时清除)。
当前保留现状(单测按现状断言,行为变更须同步改 TC-WATCHDOG-007 与本文)。
4.2 测试隔离依赖 task_wd_test_reset()
task_wd_poll 的节流基准 s_last_tick 曾是函数内 static,task_wd_test_reset()
够不到它 → 跨用例基线残留 → 假失败。已改为文件静态并纳入复位。
(这同时是更好的生产设计:模块状态应归于模块。)
5. 与板端验证的分工
| 层 | 覆盖 | 状态 |
|---|---|---|
| 本单测(L0.5) | 判定状态机全部逻辑分支与边界 | ✅ 已落地 |
| 板端故障注入(L1/L2) | 真机:单任务卡死 → 停喂 → IWDG 真复位 → RCC_CSR 记 IWDGRSTF |
✅ 已台架验收(V2/V4,2026-10-03) |
| 长跑(L2) | ≥30 min 无假复位(miss_count 稳定) |
✅ 已台架验收(V5,30 min / 120×FTP 零复位,2026-10-03) |
本单测不能替代板端验证:IWDG 实际溢出时间、LSI 频率漂移、
SHCSR/复位原因 寄存器的真机行为,仍需@test台架闭环确认。
变更记录
| 日期 | 变更 |
|---|---|
| 2026-10-25 | v1.5 按位分账同步:停喂判据从复合连击改为单个必需位连击(设计说明 §2.2/§3.2/§5.3),TC-WATCHDOG-004/005/006/008/013 断言按新语义改写(缺席窗在容忍下照常喂)、新增 TC-WATCHDOG-017(交替缺席零误报)/ TC-WATCHDOG-018(同位连击必报 + 账本重启),39→47 断言;顺带修正 TC-WATCHDOG-003 行陈旧的 0x37(B4/B5 后应为 0x1F7) |
| 2026-10-01 | §1.3/§1.4/§1.5 更新:工具链逻辑抽取到通用 harness test/host/common/unit_test_harness.py(T-3),本模块 run_unit_test.py 改为声明式 TestBuild(原 170 行 → 55 行);hooked 触发范围加 test/host/common/unit_test_harness.py。行为与退出码契约不变(39/39 PASSED) |
| 2026-10-01 | 创建:看门狗判定状态机主机侧单元测试(39 用例)+ 平台层抽取(task_watchdog.h 末段三个 __weak 钩子)+ pre-commit 钩子 watchdog-unit-test;记录两处已知行为(stall 复报、测试隔离) |