From 73941da6b189fd14e8b1a77a261a01b9998fe280 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 1 Oct 2026 06:57:01 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9C=8B=E9=97=A8=E7=8B=97=E5=88=A4=E5=AE=9A?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9C=BA=E5=A2=9E=E5=8A=A0=E4=B8=BB=E6=9C=BA?= =?UTF-8?q?=E4=BE=A7=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=EF=BC=88L0.5=20?= =?UTF-8?q?=E5=B1=82=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 task_watchdog.c 的判定逻辑(位图取走清零 / 必需掩码 0x37 判定 / miss_streak 连续 8 次停喂 / 100ms 抽样节流 / tick 回绕安全)补一层 主机侧纯逻辑单元测试,弥补此前只能靠真机或模拟器才能发现回归的空白。 架构(沿用项目既有"共享核心 + 平台后端"做法,参照 cfg_file_store): - task_watchdog.h 末段新增"平台后端(内部)"声明 3 个钩子 (wd_platform_feed / _start / _tick_ms),MCU 端以 __weak 提供默认实现, 行为与重构前逐字一致(@build 0 错误 0 警告,Flash +20B)。 - 主机侧单测以同名强符号覆盖三钩子,即可离线注入/观测,无需碰 MCU 代码。 - 测试文件 #include 生产源文件本身,保证测的是真代码而非副本。 - 测试专用复位 task_wd_test_reset() 仅 TASK_WATCHDOG_UNIT_TEST 下编入, MCU 构建零代码零符号。 测试(test/host/watchdog/,MSVC 离线编译,纯标准库驱动): - 39 项断言全过(退出码 0);注入故障可复现退出码 1。 - run_unit_test.py 直接拼 INCLUDE/LIB 调 cl.exe,绕开本机被禁的 reg.exe。 - stub/ 三桩头覆盖 main.h / iwdg.h / dbg_log.h。 门禁:pre-commit 新增 watchdog-unit-test 本地钩子(L0.5 层),仅被测文件 或其测试变动时触发;测试开发标准流程.md 新增 L0.5 层说明。 顺带改善:task_watchdog.c 的节流基线 s_last_tick 由函数内 static 提为文件 static(可测性 + 模块状态归一)。 --- .cubemx_survival_baseline.json | 2 +- .gitignore | 7 + .pre-commit-config.yaml | 13 + App/util/README.md | 11 + App/util/task_watchdog.c | 83 +++++- App/util/task_watchdog.h | 49 ++++ MDK-ARM/STM32F407-Demo.uvprojx | 379 ++++++++++++++++++------- docs/内存管理/RAM使用分析与优化方案.md | 4 +- docs/子系统设计/看门狗设计说明.md | 27 +- docs/待处理内容.md | 1 + docs/测试说明/测试开发标准流程.md | 29 +- docs/测试说明/看门狗单元测试说明.md | 178 ++++++++++++ test/host/README.md | 20 +- test/host/watchdog/run_unit_test.py | 170 +++++++++++ test/host/watchdog/stub/dbg_log.h | 27 ++ test/host/watchdog/stub/iwdg.h | 34 +++ test/host/watchdog/stub/main.h | 28 ++ test/host/watchdog/watchdog_test.c | 313 ++++++++++++++++++++ 18 files changed, 1268 insertions(+), 107 deletions(-) create mode 100644 docs/测试说明/看门狗单元测试说明.md create mode 100644 test/host/watchdog/run_unit_test.py create mode 100644 test/host/watchdog/stub/dbg_log.h create mode 100644 test/host/watchdog/stub/iwdg.h create mode 100644 test/host/watchdog/stub/main.h create mode 100644 test/host/watchdog/watchdog_test.c diff --git a/.cubemx_survival_baseline.json b/.cubemx_survival_baseline.json index f8eabee1..f41d1d0f 100644 --- a/.cubemx_survival_baseline.json +++ b/.cubemx_survival_baseline.json @@ -10,7 +10,7 @@ "assert-not-deadloop": "d4a00718ab2bd071", "assert-impl": "b7fd1fadb4522edf", "stack-overflow-hook-impl": "b7fd1fadb4522edf", - "watchdog-module": "3c70ea43ff03523e", + "watchdog-module": "e8fb07637a214d03", "reset-cause-latch": "52fc71acbb0f1aed", "iwdg-hal-driver": "4b902875a84554e9", "hal-iwdg-enabled": "c8c9b916b05464b9", diff --git a/.gitignore b/.gitignore index d8ee8c98..1d709809 100644 --- a/.gitignore +++ b/.gitignore @@ -111,6 +111,13 @@ simulator/SDL2/ # Binary / large files *.bin + +# 主机侧单元测试产物(MSVC 中间物;test/host/**/run_unit_test.py 正常写 %TEMP%, +# 但手工直接调 cl.exe 调试时可能落在 CWD。统一兜底忽略) +*.obj +*.exe +*.pdb +*.ilk # 内容页黄金参考(ref/*.bin)与目检 PNG 均为本地生成物,不入库: # 首次/换机后先跑 check_content_pages.py --update 生成 ref/*.bin,再跑 regen_ref_png.py 出 PNG simulator/tests/ref/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4209956..308dcd18 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -148,3 +148,16 @@ repos: language: system files: ^(STM32F407-Demo\.ioc|Src/.*\.(c|h)|Inc/.*\.h|App/util/task_watchdog\.(c|h)|MDK-ARM/.*\.uvprojx|scripts/check_cubemx_survival\.py)$ pass_filenames: false + + # 7) 看门狗判定状态机主机侧单元测试(2026-10-01 加) + # 背景:task_watchdog.c 的判定逻辑(位图取走清零 / 掩码 / miss_streak + # 累计 / 100ms 节流 / tick 回绕)是纯逻辑,板端套件无法穷举边界;本钩子 + # 用 MSVC 离线编译真实源文件(#include 纳入)并用可观测桩驱动,秒级、零 + # 硬件、不占 MCU RAM。仅在被测文件或其平台层 / 测试变动时触发。 + # 详见 docs/测试说明/看门狗单元测试说明.md + - id: watchdog-unit-test + name: 看门狗判定状态机单测(主机侧) + entry: python test/host/watchdog/run_unit_test.py + language: system + files: ^(App/util/task_watchdog\.(c|h)|test/host/watchdog/.*)$ + pass_filenames: false diff --git a/App/util/README.md b/App/util/README.md index 7d54139c..4f9051e3 100644 --- a/App/util/README.md +++ b/App/util/README.md @@ -9,6 +9,7 @@ | `crc.c/h` | 校验算法(CRC16/CRC32 等,供 RS-485 协议与数据校验使用) | | `ringbuf.c/h` | 环形缓冲(SPSC 无锁环形队列,用于串口/数据流收发) | | `sys_clock.c/h` | **墙钟时间管理**:RTC 基准、tick 回绕防护、每 30 天惰性再基准化 | +| `task_watchdog.c/h` | **独立看门狗(IWDG)判定状态机**:任务心跳位图 + defaultTask 单一喂狗者 | ## 设计要点 @@ -24,6 +25,16 @@ - 单生产者单消费者(SPSC)无锁设计,配合 DMA/中断收发 - 容量为 2 的幂,用掩码取模(避免除法开销) +### task_watchdog + +- **任务心跳位图**:各任务只置位(`task_wd_beat`),校验者取走并清零(单字原子,无锁) +- **单一喂狗者**:`defaultTask` 每 100 ms 抽样判定,仅当位图等于必需掩码时才喂狗 +- **硬件访问收敛**:`IWDG` 寄存器与 `HAL_GetTick()` 经 `wd_platform_*` 弱钩子访问 + (声明在 `task_watchdog.h` 末段),MCU 端为 `__weak` 默认实现,主机侧单测以同名强符号覆盖 → 可在 PC 上跑纯逻辑回归 +- 设计详见 `docs/子系统设计/看门狗设计说明.md`;单测见 `docs/测试说明/看门狗单元测试说明.md` + ## 相关文档 - `docs/子系统设计/系统墙钟时间维护说明.md` +- `docs/子系统设计/看门狗设计说明.md` +- `docs/测试说明/看门狗单元测试说明.md` diff --git a/App/util/task_watchdog.c b/App/util/task_watchdog.c index 2b854559..ff55d50e 100644 --- a/App/util/task_watchdog.c +++ b/App/util/task_watchdog.c @@ -17,6 +17,13 @@ * - §5.3 为何"停喂"而不直接 NVIC_SystemReset(保留 IWDGRSTF 权威归因) * * ⚠ 本模块不创建任何 FreeRTOS/CMSIS-RTOS 对象,可安全用于内核启动前后的上下文。 + * + * === 可测性设计(2026-10-01)=== + * 硬件访问(IWDG 寄存器读写 / HAL_GetTick)下沉到 task_watchdog.h 末段声明的 + * 三个 __weak 钩子(wd_platform_feed / _start / _tick_ms);MCU 端默认实现见 + * 文件末尾,行为与重构前逐字一致。判定状态机因此成为纯逻辑,主机侧单测 + * (test/host/watchdog/)以强符号覆盖这三个钩子后即可离线穷举验证。 + * 详见 docs/测试说明/看门狗单元测试说明.md。 */ #include "task_watchdog.h" @@ -54,6 +61,7 @@ static uint32_t s_miss_streak; /* 连续未达标轮数(达到容忍上限 static uint32_t s_miss_total; /* 累计未达标轮数(观测用) */ static uint32_t s_last_mask; /* 上次抽样到的位图(定位卡死任务用) */ static uint32_t s_started; /* 1 = IWDG 已启动 */ +static uint32_t s_last_tick; /* 上次抽样时刻(100ms 节流基准) */ /* Private function prototypes -----------------------------------------------*/ @@ -121,8 +129,8 @@ void task_wd_start(void) { /* 启动前先喂一次:让首个抽样窗口从"启动时刻"起算, * 而非从 MX_IWDG_Init 的时刻起算(避免启动后立即超时) */ - IWDG->KR = 0x0000AAAAu; /* 重装计数器(喂狗) */ - IWDG->KR = 0x0000CCCCu; /* 启动看门狗 */ + wd_platform_feed(); /* 重装计数器(喂狗) */ + wd_platform_start(); /* 启动看门狗 */ DBG_INFO("[WD] armed: timeout ~2498ms (worst ~1701ms), miss_tol=800ms"); } @@ -139,7 +147,6 @@ void task_wd_start(void) { * IWDGRSTF,而不是 NVIC_SystemReset 留下的 SFTRSTF —— §5.3)。 */ void task_wd_poll(void) { - static uint32_t s_last_tick; uint32_t now; uint32_t snapshot; @@ -147,7 +154,7 @@ void task_wd_poll(void) { return; /* 未启动:不做任何判定 */ } - now = HAL_GetTick(); + now = wd_platform_tick_ms(); if ((now - s_last_tick) < WD_POLL_PERIOD_MS) { return; /* 节流:非 100ms 边界直接返回 */ } @@ -159,7 +166,7 @@ void task_wd_poll(void) { if (snapshot == wd_required_mask()) { s_miss_streak = 0u; - IWDG->KR = 0x0000AAAAu; /* 全员健康:喂狗 */ + wd_platform_feed(); /* 全员健康:喂狗 */ return; } @@ -199,3 +206,69 @@ uint32_t task_wd_miss_count(void) { uint32_t task_wd_is_armed(void) { return (s_started != 0u) ? 1u : 0u; } + +/* Platform layer (MCU defaults, weak) ---------------------------------------*/ + +/* + * 以下三个 __weak 为 MCU 端默认实现,行为与重构前逐字一致。 + * 主机侧单测提供同名强符号覆盖(链接器优先取强符号)以实现离线可观测。 + * 声明见 task_watchdog.h 末段"平台后端(内部)";详见 + * docs/测试说明/看门狗单元测试说明.md。 + * + * 主机侧单测(同编译单元 #include 本文件)需回避这组默认定义与自身桩重名, + * 故由 TASK_WATCHDOG_UNIT_TEST 守卫:该宏下不编入默认实现,仅留桩。 + */ +#ifndef TASK_WATCHDOG_UNIT_TEST + +/* + * 函数功能:重装 IWDG 计数器(喂狗)——MCU 端默认实现 + * 入口参数:无 + * 返回值:无 + * 函数说明:写 IWDG->KR = 0xAAAA。100ms 级高频路径,故不经 HAL 封装。 + */ +__weak void wd_platform_feed(void) { + IWDG->KR = 0x0000AAAAu; /* 重装计数器(喂狗) */ +} + +/* + * 函数功能:启动 IWDG——MCU 端默认实现 + * 入口参数:无 + * 返回值:无 + * 函数说明:写 IWDG->KR = 0xCCCC(不可逆,之后只能靠复位停止)。 + */ +__weak void wd_platform_start(void) { + IWDG->KR = 0x0000CCCCu; /* 启动看门狗(不可逆) */ +} + +/* + * 函数功能:读单调毫秒时基——MCU 端默认实现 + * 入口参数:无 + * 返回值:HAL 时基毫秒计数(32 位回绕安全,调用方用无符号差比较) + * 函数说明:HAL 时基 = TIM7(1ms),非 SysTick。 + */ +__weak uint32_t wd_platform_tick_ms(void) { + return (uint32_t)HAL_GetTick(); +} + +#endif /* !TASK_WATCHDOG_UNIT_TEST */ + +/* Test-only hooks (compiled out unless TASK_WATCHDOG_UNIT_TEST) --------------*/ + +/* + * 函数功能:复位判定状态机到"刚上电"(仅主机侧单元测试使用) + * 入口参数:无 + * 返回值:无 + * 限定条件:仅当定义 TASK_WATCHDOG_UNIT_TEST 时存在 + * 函数说明:把 s_started / 位图 / 计数 / 节流基线全部清零,使各用例互相独立。 + * MCU 构建不定义该宏 → 零代码、零符号。 + */ +#ifdef TASK_WATCHDOG_UNIT_TEST +void task_wd_test_reset(void) { + s_hb_bits = 0u; + s_miss_streak = 0u; + s_miss_total = 0u; + s_last_mask = 0u; + s_started = 0u; + s_last_tick = 0u; +} +#endif diff --git a/App/util/task_watchdog.h b/App/util/task_watchdog.h index c1c9b889..b9add5de 100644 --- a/App/util/task_watchdog.h +++ b/App/util/task_watchdog.h @@ -69,6 +69,55 @@ uint32_t task_wd_last_mask(void); /* 上次抽样到的位图(0 位 = 缺 uint32_t task_wd_miss_count(void); /* 累计未达标轮数 */ uint32_t task_wd_is_armed(void); /* 1 = IWDG 已启动 */ +/* + * ============================ 平台后端(内部) ============================ + * 把判定状态机中的硬件访问收敛到三个薄钩子,使"取走清零 / 掩码判定 / 缺失 + * 累计 / 节流"成为纯逻辑,可在主机侧用桩替换后离线穷举验证 + * (见 test/host/watchdog/ 与 docs/测试说明/看门狗单元测试说明.md)。 + * + * 与 App/cfg/cfg_file_store.h 的"共享核心 + 平台后端"同款做法: + * - 三者由 task_watchdog.c 以 __weak 提供 MCU 默认实现(读写 IWDG 寄存器 / + * 调 HAL_GetTick),**行为与不抽钩子时逐字一致**; + * - 主机侧单测提供同名**强符号**覆盖 → 不碰 MCU 代码即可观测/注入; + * - MCU 构建零行为变化。 + * + * ⚠ 非模块对外接口,业务代码**不要**直接调用(由 task_wd_* 间接使用)。 + * ======================================================================== + */ + +/* + * 函数功能:重装 IWDG 计数器(喂狗) + * 返回值:无 + * 函数说明:MCU 默认实现 = `IWDG->KR = 0x0000AAAAu;` + * 主机侧单测覆盖此符号以记录"何时被喂"。 + */ +void wd_platform_feed(void); + +/* + * 函数功能:启动 IWDG(不可逆,启动后只能靠复位停止) + * 返回值:无 + * 函数说明:MCU 默认实现 = `IWDG->KR = 0x0000CCCCu;` + */ +void wd_platform_start(void); + +/* + * 函数功能:读单调毫秒时基(用于 100ms 抽样节流) + * 返回值:自启动以来的毫秒计数(32 位回绕安全,用无符号差比较) + * 函数说明:MCU 默认实现 = `HAL_GetTick()` + * 主机侧单测覆盖此符号以驱动时间推进。 + */ +uint32_t wd_platform_tick_ms(void); + +/* + * 函数功能:复位判定状态机(**仅主机侧单元测试使用**) + * 返回值:无 + * 函数说明:仅当定义 TASK_WATCHDOG_UNIT_TEST 时编入(MCU 构建无此符号)。 + * 用于在用例之间清空位图/计数/节流基线,保证用例互不污染。 + */ +#ifdef TASK_WATCHDOG_UNIT_TEST +void task_wd_test_reset(void); +#endif + #ifdef __cplusplus } #endif diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index c591098f..665d0a6c 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -1,7 +1,10 @@ - - + + + 2.1 +
### uVision Project, (C) Keil Software
+ STM32F407-Demo @@ -16,28 +19,28 @@ Keil.STM32F4xx_DFP.3.1.1 https://www.keil.com/pack/ IRAM(0x20000000-0x2001BFFF) IRAM2(0x2001C000-0x2001FFFF) IROM(0x8000000-0x80FFFFF) CLOCK(25000000) FPU2 CPUTYPE("Cortex-M4") TZ - - - + + + 0 - - - - - - - - - - + + + + + + + + + + $$Device:STM32F407ZGTx$CMSIS\SVD\STM32F407.svd 0 0 - - - - - + + + + + 0 0 @@ -52,15 +55,15 @@ 1 1 1 - + 1 0 0 0 0 - - + + 0 0 0 @@ -69,8 +72,8 @@ 0 0 - - + + 0 0 0 @@ -79,15 +82,15 @@ 0 1 - - + + 0 0 0 0 1 - + 0 @@ -101,8 +104,8 @@ 0 0 3 - - + + 0 @@ -136,10 +139,10 @@ 1 BIN\UL2CM3.DLL "" () - - - - + + + + 0 @@ -172,7 +175,7 @@ 0 0 "Cortex-M4" - + 0 0 0 @@ -183,6 +186,7 @@ 2 0 0 + 0 1 0 8 @@ -306,7 +310,7 @@ 0x0 - + 1 @@ -335,7 +339,7 @@ --diag_suppress=1,68,111,188,191,546,1293 USE_HAL_DRIVER,STM32F407xx,NDEBUG - + ../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/NET;../Drivers/BSP/NET/lftpd;../Drivers/BSP/NETCFG;../App;../App/app;../App/cfg;../App/log;../App/sync;../App/task;../App/util;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F;../Lib/dhara;../Lib/FatFs;../test/board;../Lib/lvgl;../Drivers/BSP/UI;../Lib @@ -351,9 +355,9 @@ 0 1 - - - + + + ../Drivers/CMSIS/Include @@ -364,15 +368,15 @@ 0 1 1 - - - + + + STM32F407-Demo\STM32F407-Demo.sct - - - - - + + + + + @@ -417,6 +421,8 @@ 2 2 11 + + 1 @@ -444,6 +450,12 @@ 2 2 2 + + + + + + @@ -465,6 +477,8 @@ 2 2 11 + + 1 @@ -492,6 +506,12 @@ 2 2 2 + + + + + + @@ -513,6 +533,8 @@ 2 2 11 + + 1 @@ -540,6 +562,12 @@ 2 2 2 + + + + + + @@ -561,6 +589,8 @@ 2 2 11 + + 1 @@ -588,19 +618,13 @@ 2 2 2 + + + + + + - - 2 - 2 - 2 - 2 - 2 - 2 - 2 - 2 - 2 - 1 - @@ -621,6 +645,8 @@ 2 2 11 + + 1 @@ -648,6 +674,12 @@ 2 2 2 + + + + + + @@ -669,6 +701,8 @@ 2 2 11 + + 1 @@ -696,6 +730,12 @@ 2 2 2 + + + + + + @@ -727,6 +767,8 @@ 2 2 11 + + 1 @@ -754,6 +796,12 @@ 2 2 2 + + + + + + @@ -840,6 +888,8 @@ 2 2 11 + + 1 @@ -867,6 +917,12 @@ 2 2 2 + + + + + + @@ -888,6 +944,8 @@ 2 2 11 + + 1 @@ -915,6 +973,12 @@ 2 2 2 + + + + + + @@ -1001,6 +1065,8 @@ 2 2 11 + + 1 @@ -1028,6 +1094,12 @@ 2 2 2 + + + + + + @@ -1049,6 +1121,8 @@ 2 2 11 + + 1 @@ -1076,6 +1150,12 @@ 2 2 2 + + + + + + @@ -1097,6 +1177,8 @@ 2 2 11 + + 1 @@ -1124,19 +1206,13 @@ 2 2 2 + + + + + + - - 2 - 2 - 2 - 2 - 2 - 2 - 2 - 2 - 2 - 1 - @@ -1157,6 +1233,8 @@ 2 2 11 + + 1 @@ -1184,6 +1262,12 @@ 2 2 2 + + + + + + @@ -1205,6 +1289,8 @@ 2 2 11 + + 1 @@ -1232,6 +1318,12 @@ 2 2 2 + + + + + + @@ -1338,6 +1430,8 @@ 2 2 11 + + 1 @@ -1365,6 +1459,12 @@ 2 2 2 + + + + + + 2 @@ -1377,6 +1477,12 @@ 2 2 1 + + + + + + @@ -1398,6 +1504,8 @@ 2 2 11 + + 1 @@ -1425,6 +1533,12 @@ 2 2 2 + + + + + + @@ -1446,6 +1560,8 @@ 2 2 11 + + 1 @@ -1473,6 +1589,12 @@ 2 2 2 + + + + + + @@ -1494,6 +1616,8 @@ 2 2 11 + + 1 @@ -1521,6 +1645,12 @@ 2 2 2 + + + + + + @@ -1542,6 +1672,8 @@ 2 2 11 + + 1 @@ -1569,6 +1701,12 @@ 2 2 2 + + + + + + @@ -1590,6 +1728,8 @@ 2 2 11 + + 1 @@ -1617,6 +1757,12 @@ 2 2 2 + + + + + + @@ -1638,6 +1784,8 @@ 2 2 11 + + 1 @@ -1665,6 +1813,12 @@ 2 2 2 + + + + + + @@ -1686,6 +1840,8 @@ 2 2 11 + + 1 @@ -1713,6 +1869,12 @@ 2 2 2 + + + + + + @@ -1734,6 +1896,8 @@ 2 2 11 + + 1 @@ -1761,6 +1925,12 @@ 2 2 2 + + + + + + @@ -1782,6 +1952,8 @@ 2 2 11 + + 1 @@ -1809,6 +1981,12 @@ 2 2 2 + + + + + + @@ -1830,6 +2008,8 @@ 2 2 11 + + 1 @@ -1857,6 +2037,12 @@ 2 2 2 + + + + + + @@ -1878,8 +2064,8 @@ 2 2 11 - - + + 1 @@ -1909,9 +2095,9 @@ 2 --diag_suppress=177 - - - + + + @@ -1926,10 +2112,10 @@ 2 1 - - - - + + + + @@ -2017,8 +2203,8 @@ 0 0 3 - - + + 0 @@ -2047,10 +2233,10 @@ 2 2 - - - - + + + + @@ -2065,10 +2251,10 @@ 2 0 - - - - + + + + @@ -3177,18 +3363,20 @@ + - + - + - + - + + @@ -3197,4 +3385,5 @@ +
diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index b2a0f9ff..6c5add21 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 265,016 B (258.80 KB) | 1,048,576 B (1024 KB) | 25.3% | +| ER_IROM1 | 265,036 B (258.82 KB) | 1,048,576 B (1024 KB) | 25.3% | | RW_IRAM1 | 87,080 B (85.04 KB) | 131,072 B (128 KB) | 66.4% | | RW_CCM | 49,184 B (48.03 KB) | 65,536 B (64 KB) | 75.0% | -| **Flash (ROM 合计)** | **265,772 B (259.5 KB)** | 1,048,576 B (1024 KB) | **25.3%** | +| **Flash (ROM 合计)** | **265,792 B (259.6 KB)** | 1,048,576 B (1024 KB) | **25.3%** | 各区的容量与常量来源: diff --git a/docs/子系统设计/看门狗设计说明.md b/docs/子系统设计/看门狗设计说明.md index 59d674a5..5e4f8f8b 100644 --- a/docs/子系统设计/看门狗设计说明.md +++ b/docs/子系统设计/看门狗设计说明.md @@ -438,6 +438,7 @@ IWDG 复位常伴随 `LPWRRSTF` 若同时发生低压)。按**因果强度** |---|---|---| | `App/util/task_watchdog.c` | 新建 | 心跳位图、喂狗判定、**IWDG 寄存器级初始化与刷新**(§2.2 路径 C) | | `App/util/task_watchdog.h` | 新建 | 任务 ID 枚举、`task_wd_beat()` / `task_wd_poll()` / `task_wd_start()` 接口 | +| `test/host/watchdog/{watchdog_test.c,run_unit_test.py,stub/*}` | 新建 | 主机侧单元测试:`#include` 生产源码 + 强符号覆盖钩子(§7.3) | | `Src/main.c` | 修改 | 复位原因锁存(`HAL_Init()` 后,`USER CODE` 区内) | | `Src/freertos.c` | 修改 | ◆ `vApplicationAssertFailed()` 实现 + `configASSERT` 改造;◆ 见下方"埋点方式" | | `App/task/*.c` | 修改 | 各任务主循环插入 `task_wd_beat(TASK_WD_XXX)` | @@ -703,6 +704,28 @@ external pin → "pin reset" (新增) > IWDG 硬件行为在模拟器不可建模,S1 只验证**位图与判定逻辑**(纯 RAM 部分)。 +### 7.3 主机侧单元测试(L0.5,**2026-10-01 已落地**) + +在不依赖真机与模拟器的前提下,把判定状态机**直接编进主机进程**做纯逻辑回归: + +| # | 场景 | 命令 | 期望 | +|---|---|---|---| +| U1 | 判定状态机全用例 | `python test/host/watchdog/run_unit_test.py` | 39 项断言全过,退出码 0 | +| U2 | 门禁接入 | 改动 `App/util/task_watchdog*.{c,h}` 或 `test/host/watchdog/**` 后提交 | pre-commit `watchdog-unit-test` 钩子触发并通过 | + +**架构**:`App/util/task_watchdog.c` 的硬件访问经 3 个 `__weak` 钩子收敛到 +`task_watchdog.h` 末段"平台后端(内部)"(`wd_platform_feed` / `wd_platform_start` / +`wd_platform_tick_ms`)。主机侧测试文件 `#include "task_watchdog.c"` 原样纳入生产源码, +并提供同名**强符号**覆盖这三个钩子(注入 `IWDG->KR` 影子寄存器与虚拟 tick), +故测的是**生产代码本身**而非副本。 + +**覆盖范围**:置位/清零语义、必需掩码 `0x37` 判定、`WD_MISS_TOLERANCE=8`(800 ms) +停喂、`WD_POLL_PERIOD_MS=100` 节流、tick 回绕安全、`task_wd_start()` 先喂一次等。 + +> 用例清单与扩展方法见 [`../测试说明/看门狗单元测试说明.md`](../测试说明/看门狗单元测试说明.md)。 +> 本层取代(并优于)原 S1 计划中"位图与判定逻辑"部分——**无需模拟器即可回归**; +> S1 保留意义仅剩"在模拟器全链路中联调",优先级可降。 + --- ## 8. 内存与资源预算 @@ -760,8 +783,9 @@ external pin → "pin reset" (新增) | **4** | 各任务埋点(含 `timeSyncTask` **切片**而非拆 delay) | 编译 0/0;串口无 `miss` | ✅ 完成 | | **5** | `task_wd_start()` 由 `defaultTask` 在 bring-up 完成后调用 | `[WD] armed:` 出现 | ✅ 完成 | | **6** | `configASSERT` 改造 + `vApplicationAssertFailed()` | 编译 0/0 | ✅ 完成 | +| **6.5** | **(新增)** 硬件访问收敛到薄平台层(`task_watchdog.h` 末段,3 个 `__weak` 钩子)+ 主机侧单元测试(`test/host/watchdog/`,39 用例)+ pre-commit 钩子 | `run_unit_test.py` 退出码 0;`@build` 0/0(行为不变) | ✅ **2026-10-01 完成** | | **7** | 故障注入(V2/V4):人为制造任务卡死 | 复位 + `reset cause: IWDG` | ⏳ **待台架** | -| **8** | 模拟器 `--watchdogtest` + stub | S1 | ⏳ 待做 | +| **8** | 模拟器 `--watchdogtest` + stub | S1 | ⏳ 待做(位图逻辑已由 U1 覆盖,优先级降低) | | **9** | 长跑(V5,≥30 min,真实负载) | **零复位** | ⏳ **待台架** | | **10** | 本文档改写为"现状设计说明";`待处理内容.md` N-03 销项;`README.md` 登记 | 门禁 | ⏳ 待第 7~9 步结论 | @@ -1078,3 +1102,4 @@ python scripts/check_cubemx_survival.py --write # 重建基线(确认改动 | 2026-09-29 | **第二次重生成验证 + 机制修正** | 审计脚本**实测有效**(准确检出 5 处覆盖)。⚠️ **关键发现**:CubeMX 换了一代中断函数模板(旧 `HardFault_Handler`/`HardFault_Report` 自造区 → 新函数体内 `HardFault_IRQn 0`/`W1_HardFault_IRQn 0`),**"生成区外的自造标记区"同样不被保留**——上一轮假设被证伪。据此升级恢复方案:报告实现迁入 `USER CODE BEGIN 0`(标准保护位);Handler 本体改为生成区整函数替换(`#define` 改名保留生成实体,链接器自动移除,`.map` 已验证)+ `HARD-FAULT-ANCHOR` 锚点。同步修正审计脚本(`hardfault-handler` 改 `wholefile` + 锚点)、`UNPROTECTED_LEGACY` 清单、§10.1/§10.2 全文。`@build` 0 错误 0 警告 | | 2026-09-29 | **3 个 Fault 升级为崩溃采集** | `MemManage` / `BusFault` / `UsageFault` 由空 `while(1)`(死机静默)升级为与 `HardFault` 同一套采集:生成区整函数替换(各带独立锚点 `MEMFAULT-ANCHOR` / `BUSFAULT-ANCHOR` / `USAGEFAULT-ANCHOR`)+ `#define X X_Gen` 改名保留生成实体(链接器各移除 2 字节)。新增 `fault_handler_enable()` 使能 `SHCSR` 后三者(默认只开 HardFault,不开则故障被升级、看不出根因),声明入 `Inc/stm32f4xx_it.h`、调用在 `app_main_init()` 第一行。因 ARMCC `__asm` 取不到 C 数据符号,改为 per-fault C 入口 `fault_entry_{hard,mem,bus,usage}` 带故障名调 `hard_fault_report(tag, frame)`。**刻意不启用** `UNALIGN_TRP` / `DIV_0_TRP`(避免把普通 bug 升级成死机)。审计脚本新增 `fault-handlers-3` / `fault-handler-enable` 两项、`UNPROTECTED_LEGACY` 清掉 3 个 Fault(WARN 2→1)、清单 16→18 项。`@build` 0 错误 0 警告;审计 18 通过 / 1 警告 / 0 失败 | | 2026-10-01 | **查证 `fault_handler_enable()` 的 CubeMX 可行性** | 用户问"能否通过 CubeMX 配置"→ **否**。查 CubeMX 安装目录模板源码得三重证据:① `tpl_main_c.ftl` 的 `MX_NVIC_Init()` 只在 `systemHandler=="false"` 时生成 `HAL_NVIC_EnableIRQ`,而 `NVIC-STM32F417_Modes.xml` 中三个 Fault 都标 `Y`(=systemHandler)→ 永不生成使能代码;② **全部模板中 `SHCSR` 只出现 1 次**且是 MPU 清理的 `&= ~MEMFAULTENA`(禁用),**无任何 `\|=` 置位**;③ `.ioc` 无 MPU(`Mcu.IP` 列表),且 `HAL_MPU_Enable` 只置 `MEMFAULTENA` 一位。实测印证:`HAL_MspInit()` 里三个 Fault 一行代码都没生成。结论:只能手工写,但属 **L2**(`USER CODE BEGIN 1` 区)→ 重生成不丢,有审计项兜底。另新增警示:`.ioc` 的 `NVIC.MemoryManagement_IRQn=true` **不代表已使能**(第 1 段只是"向量可见")。写入 §10.2 与四个 Fault 采集小节 | +| 2026-10-01 | **新增 §7.3 主机侧单元测试(L0.5)** | 硬件访问收敛到薄平台层(`App/util/task_watchdog.h` 末段声明 3 个 `__weak` 钩子:喂狗/启动/取 tick),MCU 端行为零变化(`@build` 0/0)。新建 `test/host/watchdog/`(`watchdog_test.c` 39 用例 + `run_unit_test.py` MSVC 运行器 + `stub/` 三桩头),`#include` 生产源码以保证测的是真代码。挂 pre-commit `watchdog-unit-test` 钩子。新增 `docs/测试说明/看门狗单元测试说明.md`;`App/util/README.md`、`test/host/README.md`、`测试开发标准流程.md`(新增 L0.5 层)同步。顺带改善:`s_last_tick` 由函数局部 static 提为文件 static(可测性 + 模块状态归一)。§5.1 文件表与 §10 步骤表登记;§7.2 S1 优先级降低(位图逻辑已由 U1 覆盖) | diff --git a/docs/待处理内容.md b/docs/待处理内容.md index 187a808f..17a1a50e 100644 --- a/docs/待处理内容.md +++ b/docs/待处理内容.md @@ -34,4 +34,5 @@ | N-03a | `configASSERT(x)` = `taskDISABLE_INTERRUPTS(); for(;;);`(`Inc/FreeRTOSConfig.h`)→ 断言时无任何输出 | ✅ **已修(2026-09-29)**:改为 `vApplicationAssertFailed(__FILE__, __LINE__)`,打印文件名+行号后主动复位(`Src/freertos.c`);宏已迁入 `USER CODE BEGIN 1` 区防重生成丢失 | 同上 N-03b | | N-03b | 启动段(`app_main_init` + `defaultTask` bring-up)在新方案下**无看门狗保护** | 显式代价(未变):`看门狗设计说明.md` §4.5 决策 C + §9 R1;该段死机表现为"无输出 + LED 不闪" | 同上 N-03c | | N-03c | **CubeMX 重生成会覆盖 `USER CODE` 标记区之外的定制** | ⚠️ **2026-09-29 两次重生成实测确认**:首次覆盖 4 处、第二次覆盖 5 处(P0 `Src/iwdg.c` 自动启动、P1 `HardFault_Handler` 打回空死循环、P1 `FreeRTOSConfig.h` 两个加固宏被删、P2 `freertos.c` 中文注释)。✅ **已建立自动检出**:`scripts/check_cubemx_survival.py`(18 项清单 + 哈希基线,已挂 pre-commit)——第二次实测**准确检出全部 5 处**。⚠️ 关键发现:CubeMX **换了一代中断模板**,"生成区外的自造标记区"同样不被保留(详见设计文档 §10.1)。**今后每次重生成后跑一次该脚本即可** | 本次实施记录 | +| N-03e | 判定状态机**缺主机侧纯逻辑回归**(改动后只能靠真机/模拟器才发现回归) | ✅ **已落地(2026-10-01)**:硬件访问收敛到薄平台层(`App/util/task_watchdog.h` 末段声明 3 个 `__weak` 钩子),新建 `test/host/watchdog/` 单元测试(39 用例,`#include` 生产源码 + 强符号覆盖钩子),挂 pre-commit `watchdog-unit-test` 钩子。MCU 端行为零变化(`@build` 0/0)。新增测试分层 **L0.5** | 见 [`测试说明/看门狗单元测试说明.md`](测试说明/看门狗单元测试说明.md) | | N-03d | 3 个次要 Fault(`MemManage`/`BusFault`/`UsageFault`)为空 `while(1)`,死机静默 | ✅ **已修(2026-09-29)**:升级为与 `HardFault` 同一套崩溃采集(手工 `__asm` Handler → per-fault C 入口 → `hard_fault_report(tag, frame)`,日志打 `[ERR] MEMFAULT/BUSFAULT/USAGEFAULT + 全量现场`);新增 `fault_handler_enable()` 使能 `SHCSR` 后三者(默认只开 HardFault,否则故障被升级、看不出根因),在 `app_main_init()` 第一行调用。**刻意不启用** `UNALIGN_TRP`/`DIV_0_TRP`(避免把普通 bug 升级成死机)。`@build` 0/0;审计 18 通过 / 1 警告 / 0 失败 | 见 [`子系统设计/看门狗设计说明.md`](子系统设计/看门狗设计说明.md) §10.2 | diff --git a/docs/测试说明/测试开发标准流程.md b/docs/测试说明/测试开发标准流程.md index 929bd6c4..53154cec 100644 --- a/docs/测试说明/测试开发标准流程.md +++ b/docs/测试说明/测试开发标准流程.md @@ -25,16 +25,23 @@ | 层 | 名称 | 载体 | 抓住什么 | |---|---|---|---| | L0 | 静态门禁 | pre-commit 钩子(cppcheck / 代码规范 / 乱码 / 空白 / gitlint)+ `@build` | 编码规范、静态缺陷、任何编译警告 | +| **L0.5** | **主机侧纯逻辑单测** | `test/host/<模块>/run_unit_test.py` + MSVC 离线编译真实源码(`#include` 纳入)+ 平台桩 | **纯逻辑模块的边界穷举**(节流窗口、计数阈值、整数回绕)——板端无法覆盖、无需硬件 | | L1 | 板端测试套件 | testTask + `test/board/test_config.h` 套件 | 驱动 / 协议功能(串口 `[PASS]/[FAIL]` + `=== ... PASSED` 汇总) | | L2 | 主机侧回归 | `@test` 串口日志标记 + LVGL 模拟器像素 / 池水位回归 | 启动序列、UI 渲染、LVGL 内存池水位 | | L3 | 人工目检 | 上机屏幕、PC 端交互工具 | 视觉内容、自动化无法覆盖的场景 | +> **L0.5 与 L2 的区别**:L2 的模拟器回归依赖 SDL / LVGL 与图形输出;L0.5 是**纯计算**单测 +> (无窗口、无硬件、秒级),专测"判定逻辑"。目前仅看门狗模块使用,见 +> [`看门狗单元测试说明.md`](看门狗单元测试说明.md)。适格对象:算法 / 状态机 / 协议编解码 / +> 参数校验等**无副作用**的模块。 + ### 1.1 门禁决策表(改动类型 → 必跑门禁) | 改动类型 | 必跑门禁 | |---|---| | 任何源码提交 | G0 pre-commit(提交时自动触发)+ G1 `@build` | | App / Drivers 功能代码(板端行为可观测) | G0 + G1 + G2 `@test`(模块标记) | +| **有 L0.5 单测的模块(如看门狗)** | G0(含该模块单测钩子,自动)+ G1 `@build`(+ 台架项另按需 G2) | | UI(菜单 / 内容页 / 字体 / 样式 / 几何) | G0 + G1 + G2 + G3 ui-verify 五步 | | UI 新增中文文案 | 另需 font-regen(字体重生成 + 回归),**先于提交** | | `test/` 套件或 PC 端工具代码 | G1 + G2(套件标题标记 / 工具自测) | @@ -187,11 +194,31 @@ static void phaseN_run(void) { | Python 串口助手(serial_tool) | `tools/serial_tool/serial_tool.py`(.venv + pyserial) | 常驻 daemon + 薄 CLI:列端口 / 开关 / 收发 / 全量日志 / wait-idle | | `test/host/run_boot_test.py` | `test/host/` | 一键闭环:采集 → 烧录 → 提取日志 → 校验 | | `test/host/verify_log.py` | `test/host/` | 日志标记校验(Required / Forbidden → PASS / FAIL) | +| **`test/host/watchdog/run_unit_test.py`** | `test/host/watchdog/` | **L0.5 主机侧单测**:MSVC 离线编译真实 `task_watchdog.c` + 可观测桩,穷举判定状态机(零硬件、秒级) | | `.opencode/serial_config.json` | 仓库 | **串口端口单一配置源**(当前 COM6 / 115200);换端口只改这个文件 | | `test/host/ftp_*.py`(4 个)/ `ch395f_socket_test.py` / `time_sync_server.py` | `test/host/` | PC 端交互测试工具(FTP 客户端 / socket 回显 / 对时推送) | | `simulator/tests/*.py`(4 个) | `simulator/tests/` | LVGL 模拟器回归脚本(见 §5) | -### 4.2 新工具约定 +### 4.2 L0.5 主机侧单元测试(纯逻辑,无硬件) + +**适用对象**:**无副作用**的纯逻辑模块——算法、状态机、协议编解码、参数校验等 +(例:看门狗的位图判定 / 掩码 / 计数阈值 / 整数回绕)。 + +**做法**(以看门狗为范式,见 [`看门狗单元测试说明.md`](看门狗单元测试说明.md)): + +1. 模块的**硬件访问**收敛到薄平台层(`__weak` 钩子或后端函数)——参照 + `App/cfg/cfg_file_store.h` 的"共享核心 + 平台后端"; +2. 测试单元 `#include` **生产源文件**(保证测的是真代码,非副本),用桩替换平台层; +3. 用 MSVC 离线编译运行(`run_unit_test.py` 自动探测工具链); +4. 挂 pre-commit 钩子,被改动时自动跑(**只 commit 不烧录**,秒级)。 + +**约束**: + +- 平台层的抽取必须**零行为变化、零额外开销**(MCU 端默认实现即原逻辑;`@build` 须 0/0); +- 测试专用钩子(如静态态复位)用 `#ifdef <模块>_UNIT_TEST` 守卫,MCU 构建不编入(零符号); +- 单测**不能替代**板端验证:真机寄存器 / 复位原因 / 时序仍须 L1/L2 台架闭环。 + +### 4.3 新工具约定 - **test/ 自动化脚本为纯 Python 标准库**(2026-09-20 由 PowerShell 迁移:绕开控制台代码页/执行策略陷阱,全部 I/O 显式 UTF-8/字节级处理;`requirements.txt` 仍须纯 ASCII——pip 按 GBK 代码页解码); - **Python 统一用仓库 `.venv`**;第三方依赖必须进 `requirements.txt`(纯 ASCII); diff --git a/docs/测试说明/看门狗单元测试说明.md b/docs/测试说明/看门狗单元测试说明.md new file mode 100644 index 00000000..20a89c69 --- /dev/null +++ b/docs/测试说明/看门狗单元测试说明.md @@ -0,0 +1,178 @@ +# 看门狗单元测试说明(主机侧) + +> **模块**:Task Watchdog(IWDG + 任务心跳位图,`App/util/task_watchdog.c`) +> **测试类型**:**主机侧纯逻辑单元测试**(L0.5 层,见 §1.5)——在 PC 上离线验证 +> 判定状态机,零硬件、零烧录、秒级、不占 MCU 的 RAM。 +> **状态**:✅ 已落地(39 用例,`@build` 0/0,已挂 pre-commit 钩子) +> **日期**:2026-10-01 +> **关联文档**: +> [`../子系统设计/看门狗设计说明.md`](../子系统设计/看门狗设计说明.md)(被测行为的设计依据)、 +> [`测试开发标准流程.md`](测试开发标准流程.md)(测试分层与门禁总览)、 +> [`测试文档模板规范.md`](测试文档模板规范.md)(本文命名规范) + +--- + +## 1. 概述 + +### 1.1 为什么单独做主机侧单测 + +`task_watchdog.c` 分两半: + +| 半边 | 内容 | 可测性 | +|---|---|---| +| **判定状态机**(被测) | 位图取走清零、必需掩码比较、`miss_streak` 累计(容忍 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 调用,行为逐字一致, +> 无额外分支、无额外符号(`@build` 0/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`。`run_unit_test.py` 因此**直接拼 `INCLUDE`/`LIB` 环境变量**并用绝对 +> 路径调 `cl.exe`(只需 MSVC 版本目录 + Windows SDK 的 include/lib),无副作用、可重复。 + +### 1.4 运行方式 + +```powershell +# 仓库根目录(统一用 .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/**` 变动 | +| **门禁层级** | **L0.5 层**(介于 L0 静态门禁与 L1 板端套件之间):主机侧、秒级、纯逻辑,比板端套件便宜、比静态检查更强 | +| **pre-commit 钩子** | `watchdog-unit-test`(本地钩子,`.pre-commit-config.yaml` §7) | +| **汇总行** | `=== Task Watchdog Tests: N/N PASSED (failed=0) ===` | +| **与板端测试的关系** | 互补:本单测保证**判定逻辑**正确;板端 `@test` 故障注入(`docs/待处理内容.md` N-03 V2/V4)保证**真机复位链路**正确 | + +--- + +## 2. 测试用例 + +> 命名 `TC-WD-NNN`(WD = Watchdog)。全部在 `watchdog_test.c` 内,主机侧运行。 + +| 用例 | 断言 | 验证的设计点 | +|---|---|---| +| **TC-WD-001** | 未启动时 `poll` 不喂狗、`is_armed()==0` | 启动前不判定(`s_started` 门卫) | +| **TC-WD-016** | 未启动时即使全员置位也不喂、不更新 `last_mask` | 同上(位图无关) | +| **TC-WD-014** | 重复 `task_wd_start()` 只启动 IWDG 一次 | 启动幂等(IWDG 不可逆,重复启动无意义) | +| **TC-WD-015** | `start` 内部先喂一次(`feed_count==1`) | 首个抽样窗口从启动时刻起算,避免启动即超时 | +| **TC-WD-002** | 50 ms 内 `poll` 不喂;累计 100 ms 喂 1 次 | 100 ms 抽样节流 | +| **TC-WD-003** | 全员置位 → 喂狗 1 次;`last_mask==0x37` | 全员到位判定 + 掩码取值 | +| **TC-WD-004** | 不重新置位则第二轮不喂 | **取走清零**语义(旧位不残留) | +| **TC-WD-005** | 缺 UI(bit4)→ 不喂;`last_mask` 精确记录缺失位 | 必需掩码比较 + 缺失定位 | +| **TC-WD-006** | 连续 1..7 次缺席不报;**第 8 次**打 `[WD] task stall`;缺席期间不喂 | `WD_MISS_TOLERANCE=8`(800 ms)停喂触发点 | +| **TC-WD-007** | 停喂后**每 100 ms 复报**一条 stall(现状行为) | 见 §4 已知行为("最后遗言"持续到复位) | +| **TC-WD-008** | 恢复全员到位 → 立即喂狗 + `streak` 归零 | 停顿可恢复(非锁死状态机) | +| **TC-WD-009** | 99 ms 不判 / 100 ms 判 / 再 99 ms 不判 / 累计 100 ms 判 | 节流窗口**边界**(易错点) | +| **TC-WD-010** | `tick` 跨 2^32 回绕后 100 ms 仍正确判 | 无符号差比较(回绕安全) | +| **TC-WD-011** | `is_armed`/`last_mask`/`miss_count` 语义 | 观测接口正确性 | +| **TC-WD-013** | 非法任务 ID(`>= TASK_WD_ID_MAX`)被忽略,不污染位图 | 防位图越界(`task_wd_beat` 的守卫) | + +**当前结果**:39/39 PASSED(`failed=0`)。 + +--- + +## 3. 如何扩展用例 + +1. 在 `watchdog_test.c` 加一个 `static void tc_0NN_xxx(void)`; +2. 用 `fresh()` 开头(复位模块静态态 + 清观测计数 + 时钟归零); +3. 用 `start_and_clear()` 启动(并清掉 `start` 自身那一次喂狗); +4. `beat_all()` / `task_wd_beat(...)` 置位,`poll_advance(ms)` 推进时间并抽样; +5. `CHECK(cond, "描述")` 断言; +6. 在 `main()` 里 `fresh(); tc_0NN_xxx();` 注册。 + +> ⚠ **别忘 `fresh()`**:`task_wd_poll` 的节流基准 `s_last_tick` 与位图 / 计数都是模块 +> 静态态,跨用例不隔离会导致假失败(本项目开发时即踩到,见 §4)。 + +--- + +## 4. 已知行为与可优化点 + +### 4.1 stall 日志在停喂后复报(TC-WD-007,现状) + +实现(`task_watchdog.c`): + +```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-WD-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` | ⏳ 待台架(`docs/待处理内容.md` N-03 V2/V4) | +| 长跑(L2) | ≥30 min 无假复位(`miss_count` 稳定) | ⏳ 待台架(N-03 V5) | + +> 本单测**不能**替代板端验证:IWDG 实际溢出时间、LSI 频率漂移、`SHCSR`/复位原因 +> 寄存器的真机行为,仍需 `@test` 台架闭环确认。 + +--- + +## 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-01 | 创建:看门狗判定状态机主机侧单元测试(39 用例)+ 平台层抽取(`task_watchdog.h` 末段三个 `__weak` 钩子)+ pre-commit 钩子 `watchdog-unit-test`;记录两处已知行为(stall 复报、测试隔离) | diff --git a/test/host/README.md b/test/host/README.md index 791fd626..01b51f6c 100644 --- a/test/host/README.md +++ b/test/host/README.md @@ -30,11 +30,24 @@ | `latency_peer.py` | 链路时延测试的 PC 对端(peer) | | `time_sync_server.py` | 时间同步推送服务(对时源) | +### 主机侧单元测试(纯逻辑,无硬件) + +| 脚本 | 作用 | +|---|---| +| `watchdog/run_unit_test.py` | **看门狗判定状态机单测**:用 MSVC 离线编译真实 `App/util/task_watchdog.c`(可观测桩驱动),39 用例穷举边界。零硬件、秒级(退出码 0=PASS/1=FAIL/2=环境错误) | +| `watchdog/watchdog_test.c` | 测试单元本体(`#include` 生产源码 + 平台钩子桩) | +| `watchdog/stub/` | 桩头文件(`main.h`/`iwdg.h`/`dbg_log.h`),覆盖 MCU 版以在 PC 上编译 | + +> 详见 `docs/测试说明/看门狗单元测试说明.md`。已挂 pre-commit 钩子 `watchdog-unit-test`。 + ## 运行方式 ```bash -# @test 一键闭环(推荐) +# @test 一键闭环(推荐,需真机) & '.venv\Scripts\python.exe' test/host/run_boot_test.py + +# 看门狗判定状态机单测(纯逻辑,无需真机) +& '.venv\Scripts\python.exe' test/host/watchdog/run_unit_test.py ``` > 串口端口配置在 `.opencode/serial_config.json`(单一配置源,当前 COM6/115200)。 @@ -42,11 +55,14 @@ ## ⚠️ 注意 - **PYTHONPATH/路径敏感**:脚本按仓库根相对路径读写,**从仓库根执行** -- 本目录**被 pre-commit 门禁排除** +- 本目录**被 pre-commit 门禁的 cppcheck / 代码规范排除**(`test/` 豁免); + 但 `watchdog/run_unit_test.py` 自身**作为独立钩子**参与门禁(见上) - 台架探针族(TCP 复连 / FTP 中止等)在 `MDK-ARM/*.py`,与本目录分工不同 +- `watchdog/` 单测需要本机装有 MSVC(VS2019/2022 的 C++ 工具链);无则脚本报环境错误(退出码 2) ## 相关文档 - `docs/测试说明/自动化测试流程.md` — `@test` 细节与失败模式 - `docs/测试说明/测试开发标准流程.md` — 测试分层与门禁 +- `docs/测试说明/看门狗单元测试说明.md` — 主机侧单测(用例 / 平台层 / 扩展方法) - `docs/测试说明/CH395F_Test_Guide.md`、`FTP_Test_Guide.md` diff --git a/test/host/watchdog/run_unit_test.py b/test/host/watchdog/run_unit_test.py new file mode 100644 index 00000000..526c5033 --- /dev/null +++ b/test/host/watchdog/run_unit_test.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""看门狗判定状态机的主机侧单元测试运行器(纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/watchdog/watchdog_test.c(它以 #include 纳入 + **生产源文件** App/util/task_watchdog.c),在 PC 上离线跑完整判定状态机, + 失败即非 0 退出。零硬件、零烧录、毫秒级。 + +为什么要主机侧单测(而不是只用板端套件): + task_watchdog.c 的判定逻辑(位图取走清零 / 掩码 / miss_streak 累计 / 节流) + 是纯逻辑,与硬件无关;板端套件只能"在真机上跑一遍",无法穷举边界(节流窗口 + 99/100ms、tick 回绕、连续 8 次缺席的第 8/9 次分界)。主机侧单测把这些一次性 + 钉死,且不占 MCU 的 RAM(项目 RAM 已 95%)。 + +实现要点(为何不用 vcvars64.bat): + vcvars64.bat 内部调用 reg.exe 探测环境;本机策略禁止 reg.exe。故这里**直接 + 拼 INCLUDE/LIB 环境变量**并绝对路径调用 cl.exe——只需 3 个已知目录 + (MSVC 版本 / Windows SDK include / SDK lib),无副作用、可重复。 + +用法(仓库根目录): + python test/host/watchdog/run_unit_test.py + 或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/watchdog/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败)。 + +设计依据:docs/测试说明/看门狗单元测试说明.md +创建日期:2026-10-01 +""" +import glob +import os +import subprocess +import sys +import tempfile + +# ----------------------------- 路径 ----------------------------- +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +STUB = os.path.join(HERE, "stub") +SRC = os.path.join(HERE, "watchdog_test.c") +APP_UTIL = os.path.join(ROOT, "App", "util") # task_watchdog.c 所在目录 + +VS_ROOTS = [ + r"C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools", + r"C:\Program Files\Microsoft Visual Studio\2022\BuildTools", + r"C:\Program Files\Microsoft Visual Studio\2022\Community", + r"C:\Program Files\Microsoft Visual Studio\2022\Professional", +] +SDK_ROOTS = [r"C:\Program Files (x86)\Windows Kits\10"] + + +def _first(pattern): + hits = sorted(glob.glob(pattern)) + return hits[-1] if hits else None + + +def _decode(b): + """把 subprocess 字节输出解码为字符串:本机 MSVC 输出为 GBK(代码页 936), + 测试程序自身输出为 UTF-8;按 UTF-8 优先、失败回退 GBK 处理。""" + if isinstance(b, str): + return b + try: + return b.decode("utf-8") + except UnicodeDecodeError: + return b.decode("gbk", errors="replace") + + +def locate_toolchain(): + """返回 (cl.exe, include_dirs, lib_dirs);找不到返回 (None, [], [])。""" + cl = None + for vs in VS_ROOTS: + c = _first(os.path.join(vs, r"VC\Tools\MSVC\*\bin\Hostx64\x64\cl.exe")) + if c: + cl = c + break + if not cl: + return None, [], [] + + msvc_ver_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(cl)))) + # cl.exe = \VC\Tools\MSVC\\bin\Hostx64\x64\cl.exe + msvc_ver_dir = os.path.dirname(os.path.dirname(os.path.dirname( + os.path.dirname(cl)))) # -> ...\MSVC\ + inc = [ + os.path.join(msvc_ver_dir, "include"), + ] + lib = [ + os.path.join(msvc_ver_dir, "lib", "x64"), + ] + + for sdk in SDK_ROOTS: + sdk_inc = _first(os.path.join(sdk, r"Include\*")) + sdk_lib = _first(os.path.join(sdk, r"Lib\*")) + if sdk_inc: + inc += [ + os.path.join(sdk_inc, "ucrt"), + os.path.join(sdk_inc, "shared"), + os.path.join(sdk_inc, "um"), + ] + if sdk_lib: + lib += [ + os.path.join(sdk_lib, "ucrt", "x64"), + os.path.join(sdk_lib, "um", "x64"), + ] + return cl, inc, lib + + +def main(): + cl, inc, lib = locate_toolchain() + if not cl: + print("[ENV] 找不到 MSVC cl.exe(需要 VS2019/2022 的 C++ 工具链)", + file=sys.stderr) + return 2 + + tmp = tempfile.gettempdir() + objdir = os.path.join(tmp, "wd_test_obj") + os.makedirs(objdir, exist_ok=True) + exe = os.path.join(tmp, "watchdog_unit_test.exe") + + env = dict(os.environ) + env["INCLUDE"] = os.pathsep.join(inc) + env["LIB"] = os.pathsep.join(lib) + env["PATH"] = os.path.dirname(cl) + os.pathsep + env.get("PATH", "") + + cmd = [ + cl, "/nologo", "/W4", "/std:c11", "/utf-8", + # C4819("源代码包含不能在当前代码页中表示的字符")是**纯编码诊断**: + # 本文件确为 UTF-8(带 BOM,已核验),且已显式传 /utf-8,字节解码无误、 + # 编译产物正确。但在"只设 INCLUDE/LIB、不调 vcvars"的调用方式下, + # MSVC 的代码页探测会退化为 `代码页(0)` → 恒定误报 10 条。 + # 该文件仅主机侧编译(ARMCC 从不编译 test/),故显式屏蔽,保持门禁输出干净。 + "/wd4819", + "/I", STUB, # stub 头优先于真实头(覆盖 main.h/iwdg.h/dbg_log.h) + "/I", APP_UTIL, # task_watchdog.c / task_watchdog.h + "/I", HERE, # (保留)本目录 + SRC, + "/Fe:" + exe, + "/Fo:" + objdir + os.sep, + ] + + print("=== 编译并运行看门狗单元测试 ===") + cp = subprocess.run(cmd, env=env, capture_output=True) + cout = _decode(cp.stdout) + cerr = _decode(cp.stderr) + if cout: + sys.stdout.write(cout) + if cerr: + sys.stderr.write(cerr) + if cp.returncode != 0: + print("[ENV] 编译失败", file=sys.stderr) + return 2 + + run = subprocess.run([exe], env=env, capture_output=True) + out = _decode(run.stdout) + sys.stdout.write(out) + errout = _decode(run.stderr) + if errout: + sys.stderr.write(errout) + + if "=== Task Watchdog Tests:" not in out: + print("[ENV] 未产生测试汇总行", file=sys.stderr) + return 2 + if run.returncode == 0: + print("[OK] 单元测试全部通过") + return 0 + print("[FAIL] 有测试用例未通过", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/host/watchdog/stub/dbg_log.h b/test/host/watchdog/stub/dbg_log.h new file mode 100644 index 00000000..bf1ea919 --- /dev/null +++ b/test/host/watchdog/stub/dbg_log.h @@ -0,0 +1,27 @@ +/* + * 主机侧单测桩:dbg_log.h + * 模块功能:为 task_watchdog.c 提供主机可编译的日志宏(映射到 printf / 计数)。 + * + * ⚠ 仅供 test/host/watchdog/ 使用,不参与 MCU 构建。 + * + * 与 simulator/dbg_log.h 同思路:MCU 版依赖 sys_clock(RTC 时间戳)与 USART, + * 主机侧替换为 printf,并把 DBG_ERROR 计入计数器供单测断言("停喂时是否打了 + * [WD] task stall" 是可观测行为的一部分)。 + * + * 创建日期:2026-10-01 + */ + +#ifndef DBG_LOG_H +#define DBG_LOG_H + +#include + +/* 测试期望的"最后遗言"计数(在 watchdog_test.c 实现,单测断言其增量) */ +extern int g_wdtest_err_count; + +/* 断言:停喂时应恰好出现若干条 DBG_ERROR([WD] task stall) */ +#define DBG_INFO(fmt, ...) do { } while (0) +#define DBG_DEBUG(fmt, ...) do { } while (0) +#define DBG_ERROR(fmt, ...) do { g_wdtest_err_count++; } while (0) + +#endif /* DBG_LOG_H */ diff --git a/test/host/watchdog/stub/iwdg.h b/test/host/watchdog/stub/iwdg.h new file mode 100644 index 00000000..424de74a --- /dev/null +++ b/test/host/watchdog/stub/iwdg.h @@ -0,0 +1,34 @@ +/* + * 主机侧单测桩:iwdg.h + * 模块功能:为 task_watchdog.c 提供主机可编译、可观测的 IWDG 模型。 + * + * ⚠ 仅供 test/host/watchdog/ 使用,不参与 MCU 构建。 + * + * 说明:真实代码写 `IWDG->KR = 0xAAAAu;`(重装/喂狗)与 0xCCCC(启动)。 + * 本桩把 IWDG 指向 watchdog_test.c 中的影子对象,写入落到普通内存; + * 单测通过平台钩子(wd_platform_feed/start)计次观测,无需读寄存器。 + * + * 创建日期:2026-10-01 + */ + +#ifndef IWDG_H +#define IWDG_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 与真实芯粒同名的结构(本模块仅用到 KR) */ +typedef struct { + volatile uint32_t KR; +} IWDG_TypeDef; + +extern IWDG_TypeDef *IWDG; + +#ifdef __cplusplus +} +#endif + +#endif /* IWDG_H */ diff --git a/test/host/watchdog/stub/main.h b/test/host/watchdog/stub/main.h new file mode 100644 index 00000000..8da1a0db --- /dev/null +++ b/test/host/watchdog/stub/main.h @@ -0,0 +1,28 @@ +/* + * 主机侧单测桩:main.h + * 模块功能:为 task_watchdog.c 提供主机可编译的最小替代,仅需 HAL_GetTick() 声明。 + * + * ⚠ 仅供 test/host/watchdog/ 使用,不参与 MCU 构建。 + * MCU 构建走真实 Inc/main.h(含 stm32f4xx_hal.h);本文件因 include 路径 + * 优先级被优先命中(见 run 脚本 -I 顺序)。 + * + * 创建日期:2026-10-01 + */ + +#ifndef __MAIN_H +#define __MAIN_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* HAL 毫秒时基(MCU 端 = TIM7 中断累加;主机侧由桩提供可控时钟) */ +uint32_t HAL_GetTick(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __MAIN_H */ diff --git a/test/host/watchdog/watchdog_test.c b/test/host/watchdog/watchdog_test.c new file mode 100644 index 00000000..157bde7e --- /dev/null +++ b/test/host/watchdog/watchdog_test.c @@ -0,0 +1,313 @@ +/* + * 模块名称: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 +#include + +/* + * 先纳入桩头文件,再纳入生产源码——保证 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; +} + +/* 全员置位(默认必需掩码 0x37 = bit0|1|2|4|5) */ +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); +} + +/* ===================== 用例 ===================== */ + +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() == 0x37u, "last_mask == 0x37(bit0|1|2|4|5)"); +} + +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); + poll_advance(100); + CHECK(s_feed_count == 0, "缺 UI 位 → 不喂狗"); + CHECK(task_wd_last_mask() == (0x37u & ~(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() == 0x37u, "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; +}