看门狗判定状态机增加主机侧单元测试(L0.5 层)
为 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(可测性 + 模块状态归一)。
This commit is contained in:
@@ -36,10 +36,10 @@
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| 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%** |
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
各区的容量与常量来源:
|
||||
|
||||
+26
-1
@@ -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 覆盖) |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
+28
-1
@@ -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);
|
||||
|
||||
@@ -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 复报、测试隔离) |
|
||||
Reference in New Issue
Block a user