From e89fe54a978fe3ea48ac43375efe720b5fde8ccf Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sat, 10 Oct 2026 10:57:31 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=86=99=E7=9C=8B=E9=97=A8=E7=8B=97?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E8=AF=B4=E6=98=8E=E4=B8=BA=E7=8E=B0=E7=8A=B6?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=EF=BC=9A=E5=88=A0=E7=AB=8B=E9=A1=B9=E5=89=8D?= =?UTF-8?q?=E6=97=A7=E6=8F=8F=E8=BF=B0=E3=80=81=E8=A1=A5FTL=E5=BF=83?= =?UTF-8?q?=E8=B7=B3=E8=B7=AF=E7=94=B1=E4=B8=8E=E5=8F=96=E8=AF=81=E6=9C=BA?= =?UTF-8?q?=E5=88=B6=E3=80=81=E4=BF=AE=E6=8E=A9=E7=A0=81=E4=B8=8E=E5=A4=8D?= =?UTF-8?q?=E4=BD=8D=E5=BD=92=E5=9B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/task/default_task.c | 2 +- Src/freertos.c | 2 +- Src/iwdg.c | 2 +- docs/README.md | 2 +- docs/子系统设计/看门狗设计说明.md | 1314 ++++++++------------------- docs/测试说明/实机测试实施指南.md | 6 +- docs/测试说明/模拟器差距清单.md | 2 +- docs/测试说明/看门狗单元测试说明.md | 2 +- scripts/check_cubemx_survival.py | 4 +- 9 files changed, 382 insertions(+), 954 deletions(-) diff --git a/App/task/default_task.c b/App/task/default_task.c index 7c43fd60..17df1e72 100644 --- a/App/task/default_task.c +++ b/App/task/default_task.c @@ -169,7 +169,7 @@ void default_task_func(void *argument) { /* 启动看门狗(延后启动):此处位于全部长耗时初始化之后——存储 bring-up 深链 * (mount/mkfs/FAT 探针/syslog 挂载/配置加载)已完成,监视一开始即处于稳态。 * 这正是"启动段不喂狗、主循环段才喂"的落点(见 docs/子系统设计/看门狗设计说明.md - * §4.5 决策 C):MX_IWDG_Init() 只填参数不启动,启动延后到此时。 + * §4.1):MX_IWDG_Init() 只填参数不启动,启动延后到此时。 * 代价(显式声明):从复位到此处约数秒的窗口无看门狗保护,该段死机表现为 * "无任何输出 + LED 不闪",可人工辨识。 */ task_wd_start(); diff --git a/Src/freertos.c b/Src/freertos.c index 305d6c8b..dee4ead7 100644 --- a/Src/freertos.c +++ b/Src/freertos.c @@ -433,7 +433,7 @@ void StartUiTask(void *argument) * 断言失败钩子(2026-09-29,配合 IWDG;见 Inc/FreeRTOSConfig.h 的 configASSERT)。 * 与栈溢出钩子同为"内核状态已不可信"路径:裸 UART 有界输出 + 主动复位, * 不走 printf / 内核对象 / 控制台锁。 - * 设计取舍(docs/子系统设计/看门狗设计说明.md §4.6): + * 设计取舍(docs/子系统设计/看门狗设计说明.md §4.4): * - 上报 file/line 是断言唯一有价值的现场信息,故必须打印(早于 TCB 名之类); * - 打印后**主动 NVIC_SystemReset()** 而非空转:断言点未必能让 IWDG 抓到 * (若恰在启动段,IWDG 尚未启动),主动复位保证任何断言都能自恢复; diff --git a/Src/iwdg.c b/Src/iwdg.c index 880175b5..b139d102 100644 --- a/Src/iwdg.c +++ b/Src/iwdg.c @@ -37,7 +37,7 @@ * 的 HAL_IWDG_Init **改名到下面的空实现**。宏只作用于其后、本文件内的调用点 * (含生成区那一句),生成代码一行都不用动 → 重生成后自动失效,不必再手工删。 * 真正的启动移交 task_wd_start() → wd_platform_start()(App/util/task_watchdog.c), - * 由它解锁并写 PR/RLR、再写 KR=0xCCCC 启动(见 docs/子系统设计/看门狗设计说明.md §4.5 决策 C)。 + * 由它解锁并写 PR/RLR、再写 KR=0xCCCC 启动(见 docs/子系统设计/看门狗设计说明.md §4.1)。 * ⚠ 本宏只影响本编译单元;task_watchdog.c 那侧不经过它。 */ static HAL_StatusTypeDef iwdg_deferred_start(const IWDG_HandleTypeDef *a_h) diff --git a/docs/README.md b/docs/README.md index 8acaf87a..dab543d8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -85,7 +85,7 @@ docs/ | [`系统墙钟时间维护说明.md`](子系统设计/系统墙钟时间维护说明.md) | `sys_clock`:RTC 基准、tick 回绕防护、30 天惰性再基准化 | | [`控制台单写者日志设计说明.md`](子系统设计/控制台单写者日志设计说明.md) | 控制台日志单写者模型(避免多任务交错输出) | | [`运行与自检记录设计说明.md`](子系统设计/运行与自检记录设计说明.md) | `/log/run.log`·`selftest.log` 定长记录与待机显示 | -| [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | **实施中**:IWDG(CubeMX 配置)+ 任务心跳位图;代码已落地编译通过,待台架故障注入/长跑验证 | +| [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | IWDG(CubeMX 配置)+ 任务心跳位图:选型与超时推导、掩码与埋点、取证机制、验证记录;已台架闭环(V2/V4/V5) | | [`网络/BSD_Socket_API_使用指南.md`](网络/BSD_Socket_API_使用指南.md) | **上层视图**:BSD Socket API 公开语义、示例、常见问题 | | [`网络/BSD_Socket_API_函数调用关系.md`](网络/BSD_Socket_API_函数调用关系.md) | **内部视图**:netTask / 消息队列 / L3 挂起 / 重试对账的调用关系与设计意图 | | [`网络/Net层阻塞模型架构设计.md`](网络/Net层阻塞模型架构设计.md) | **等待模型**:pending 请求架构、五路完成语义、设计边界与性能特征 | diff --git a/docs/子系统设计/看门狗设计说明.md b/docs/子系统设计/看门狗设计说明.md index 670bc698..891112a2 100644 --- a/docs/子系统设计/看门狗设计说明.md +++ b/docs/子系统设计/看门狗设计说明.md @@ -1,194 +1,138 @@ # 看门狗设计说明 -> **模块名称**:独立看门狗(IWDG)+ 任务心跳位图 -> **文档定位**:**现状设计说明**——记录选型依据、在既有平台上落地的约束、实现现状与 -> 台架验证结论。**代码已落地并编译通过**(§10 第 0~6 步);**第 7~10 步(故障注入 / 长跑 / -> 本文档转为"现状设计说明")已于 2026-10-03 台架闭环**(见 §10 与文末变更记录)。 -> **状态**:✅ **台架验证已闭环(2026-10-03)**——V2 任务饿死声明 stall 后 ≤2.5s IWDG 复位、 -> V4 关中断死循环每 ~4.9s 稳定复位、V5 30 min 真实负载 120/120 FTP PASS 零复位。 -> **日期**:2026-09-29(2026-10-03 台架验收) +> **模块名称**:独立看门狗(IWDG)+ 任务心跳位图(`App/util/task_watchdog.{c,h}`) +> **文档定位**:**现状设计说明**——选型依据、当前实现、可观测性与判读方法、验证结论。 +> **状态**:✅ 已落地并台架闭环(2026-10-03 V2/V4/V5);未办项集中在 §9。 +> **日期**:2026-09-29 创建;**2026-10-10 按当前代码重写正文**(旧正文含大量立项前的 +> 描述,已清理;实施史与逐次决策见 §10 与文末变更记录,未删)。 > **相关文档**: -> [`../问题记录/项目审查与优化建议报告.md`](../问题记录/项目审查与优化建议报告.md)(N-03 缺陷原始登记)、 -> [`../待处理内容.md`](../待处理内容.md)(排期)、 -> [`运行与自检记录设计说明.md`](运行与自检记录设计说明.md)(§5.3 上电/重启分类)、 -> [`../架构设计/项目启动顺序管理.md`](../架构设计/项目启动顺序管理.md)(就此绪屏障)、 -> [`../测试说明/ST-LINK_CLI_复位使用说明.md`](../测试说明/ST-LINK_CLI_复位使用说明.md)(复位语义) +> [`../待处理内容.md`](../待处理内容.md)(未办项 N-17 / S-3 / N-4)、 +> [`运行与自检记录设计说明.md`](运行与自检记录设计说明.md)(复位记录与 `selftest.log`)、 +> [`../测试说明/看门狗单元测试说明.md`](../测试说明/看门狗单元测试说明.md)(L0.5 判定状态机单测)、 +> [`../测试说明/实机测试实施指南.md`](../测试说明/实机测试实施指南.md)(故障注入台架流程)、 +> [`../架构设计/项目启动顺序管理.md`](../架构设计/项目启动顺序管理.md)(启动时序与就绪屏障)、 +> [`../测试说明/ST-LINK_CLI_复位使用说明.md`](../测试说明/ST-LINK_CLI_复位使用说明.md)(复位语义)、 +> [`../问题记录/项目审查与优化建议报告.md`](../问题记录/项目审查与优化建议报告.md)(N-03 原始登记,已销项) --- -## 1. 问题域 +## 0. 一页速览 -### 1.1 现状:无任何硬件看门狗 - -| 事实 | 证据 | -|---|---| -| 全库无 IWDG/WWDG 初始化 | `grep -r "MX_IWDG_Init\|HAL_IWDG_Refresh"` 零命中 | -| HAL 模块未启用 | `Inc/stm32f4xx_hal_conf.h:58` `/* #define HAL_IWDG_MODULE_ENABLED */`(注释态) | -| **HAL IWDG 驱动源码不在仓库** | `find Drivers -iname "*iwdg*"` **零命中**——CubeMX 未勾选该外设,源文件从未拷入 | -| **MDK 工程未编入 IWDG** | `MDK-ARM/*.uvprojx` 的 `stm32f4xx_hal_*.c` 列表**无** `stm32f4xx_hal_iwdg.c` | -| `stm32f4xx_hal_conf.h:375` 却 include 了 | `#include "stm32f4xx_hal_iwdg.h"` 位于 `#ifdef HAL_IWDG_MODULE_ENABLED` 内,宏未定义故不生效 | -| `.ioc` 无看门狗配置 | `STM32F407-Demo.ioc` 无 `IWDG`/`WWDG` 条目 | -| 无复位原因读取 | `Src/main.c` / `App/util/*.c` 无 `__HAL_RCC_GET_FLAG(RCC_FLAG_*)` | - -**后果**:现场任何形态的死机(任务饿死、死锁、HardFault、初始化失败挂死) -都**不可自恢复**,必须人工断电。这是可靠性上的单点缺口。 - -> ⚠️ **实施路径因此改变**:`HAL_IWDG_MODULE_ENABLED` 不是一个"取消注释即可"的开关—— -> 取消后 `stm32f4xx_hal_conf.h` 会去 include 一个**不存在的头文件**,直接编译失败。 -> 故须**由 CubeMX 勾选 IWDG 后重生成**,让驱动源码与 HUD 开关一并就位(见 §2.2)。 - -### 1.2 现存的两处"假保护" - -项目已开两个 FreeRTOS 钩子,但它们的兜底动作并不完整: - -| 钩子 | 位置 | 当前动作 | 缺口 | -|---|---|---|---| -| `vApplicationStackOverflowHook` | `Src/freertos.c:323` | 裸串口打任务名 → `HAL_Delay(200)` → `NVIC_SystemReset()` | ✅ 会复位;但复位原因与"看门狗复位"混为一谈(都留 `SFTRSTF`) | -| `vApplicationMallocFailedHook` | `Src/freertos.c:355` | 裸串口告警,**不复位** | 堆耗尽后系统继续运行但已不可用,且**无人再喂狗时也不会被拉回** | -| `configASSERT(x)` | `Inc/FreeRTOSConfig.h:163` | `taskDISABLE_INTERRUPTS(); for(;;);` | ❌ **关中断死循环**:连日志都不打,且关中断后**即使 IWDG 已启动也照样能复位**(IWDG 独立于中断)——这是唯一一处"关中断也救得回来"的幸运 | - -> **`configASSERT` 的关中断死循环恰好是 IWDG 能覆盖的场景**:IWDG 由 LSI 独立计数, -> 不依赖 CPU 时钟与中断,`taskDISABLE_INTERRUPTS()` 对它无效。所以引入 IWDG 后, -> 该断言形态从"永久砖"降级为"1 次重启"。 - -### 1.3 设计目标 - -1. **可恢复**:任务级死锁 / 饿死 / 断言死循环 → 硬件复位,无需人工到场。 -2. **可诊断**:复位后能区分"上电 / 看门狗 / 软件 / 引脚 / 欠压",并落 `run.log`。 -3. **不误杀**:正常业务的长耗时窗口(NAND 大块擦写、FAT 挂载扫描、PHY 链路等待 3s、LVGL 大页重绘)**不得**触发复位。 -4. **低代价**:RAM/Flash 增量可核算,且不破坏既有 `osDelay` 节拍与"串口静默窗口"观测习惯。 +| 项 | 当前值 / 落点 | 事实来源 | +|---|---|---| +| 看门狗外设 | **IWDG**(独立 LSI ≈32 kHz),CubeMX 已勾选 | `.ioc`:`Mcu.IP4=IWDG`、`IWDG.Prescaler=IWDG_PRESCALER_64`、`IWDG.Reload=1249` | +| 超时 | 标称 **2498 ms**,最坏(LSI 47 kHz)**≈1701 ms** | `WD_IWDG_PRESCALER=4` / `WD_IWDG_RELOAD=1249`(`task_watchdog.c`) | +| 抽样周期 | **100 ms**(`WD_POLL_PERIOD_MS`) | 同上 | +| 缺失容忍 | **连续 8 轮 = 800 ms** 后停喂(`WD_MISS_TOLERANCE`) | 同上 | +| 必需掩码 | 产品构建 **`0x37`**(default/net/ftp/ui/timesync);`ENABLE_HW_LAYER_TESTS` 下 **`0x35`**(剔 net) | `wd_required_mask()` | +| 喂狗者 | **仅 defaultTask**(`task_wd_poll`,主循环每轮调用、内部节流) | `App/task/default_task.c` | +| 启动时刻 | **defaultTask 存储 bring-up 之后**(`task_wd_start()`);`MX_IWDG_Init()` 只填参数不启动 | `default_task.c:175`、`Src/iwdg.c` 的 `USER CODE BEGIN 0` | +| 复位归因 | 停喂 → IWDG 自然溢出 → `RCC_CSR.IWDGRSTF`(不用 `NVIC_SystemReset`) | `task_wd_poll()` | +| 复位原因上报 | `main.c` 锁存 `g_rcc_csr_latched` + 清 `RMVF` → `app_main.c` 打 `reset cause:` 并把看门狗复位记入 `selftest.log` | `app_main.c:117-130` | +| 主机侧单测 | 39 断言(L0.5,pre-commit `watchdog-unit-test`) | `test/host/watchdog/` | +| 重生成审计 | 21 项 / 1 警告 / 0 失败(含 iwdg 延后启动、看门狗挂载、复位锁存) | `scripts/check_cubemx_survival.py` | --- -## 2. 选型:为什么是 IWDG 而不是 WWDG +## 1. 问题域与目标 -STM32F407 有两个看门狗,本项目只能用其中一个。 +### 1.1 要覆盖的故障谱 + +| 故障形态 | 现有兜底 | 无看门狗时的后果 | +|---|---|---| +| 单任务死锁 / 饿死(SPI 等待、卷锁、事件屏障) | 无 | 现场必须人工断电 | +| `configASSERT` 触发(内核态误用) | 现为 `vApplicationAssertFailed()` 打印后软复位 | 旧形态是关中断死循环 = 永久砖 | +| 4 个 Fault(Hard/Mem/Bus/Usage) | 采集现场后 `NVIC_SystemReset()` | 旧形态空 `while(1)` = 静默死机 | +| 初始化深链挂死 | **不覆盖**(启动段无狗,见 §4.1) | 表现为"无输出 + LED 不闪",人工辨识 | +| NAND 侧真实卡死 | FTL 长路径直接喂狗 → **设备存活但功能冻结**(刻意边界,见 §9) | — | + +### 1.2 设计目标 + +1. **可恢复**:任务级死锁 / 饿死 / 断言挂死 → 硬件复位,无需人工到场。 +2. **可诊断**:复位后能区分"上电 / 看门狗 / 软件 / 引脚 / 欠压",并留下证据。 +3. **不误杀**:正常业务的长耗时窗口(NAND 擦写与 GC、FAT 挂载、PHY 链路等待、FTP 大文件传输、LVGL 重绘)**不得**触发复位。 +4. **低代价**:RAM/Flash 增量可核算(实测见 §8),不破坏既有 `osDelay` 节拍与串口观测习惯。 + +### 1.3 立项前状态(历史,2026-09-29) + +当时全库无 IWDG/WWDG 初始化、HAL IWDG 驱动源码不在仓库、`.ioc` 无看门狗条目、无复位原因读取——缺陷登记为 N-03(`../问题记录/项目审查与优化建议报告.md`,已销项)。**该节只作立项背景**:现状是 IWDG 已启用(`.ioc` + `HAL_IWDG_MODULE_ENABLED` + 驱动已挂载),复位原因每次开机都在上报。 + +--- + +## 2. 选型与参数 + +### 2.1 为什么是 IWDG 而不是 WWDG | 维度 | **IWDG**(独立) | **WWDG**(窗口) | |---|---|---| -| 时钟源 | **LSI ≈ 32 kHz**(独立 RC,`IWDG` 一启动即强制 LSI 开启且不可关闭) | APB1 时钟(168/4 = 42 MHz,依赖 PLL) | -| 依赖 CPU 时钟 | ❌ 不依赖 | ✅ 依赖;PLL 失锁/时钟切换期失效 | -| 依赖中断 | ❌ 不依赖 | ✅ 依赖(早期唤醒中断 EWI) | -| 复位类型 | `IWDGRSTF`(VDD 域) | `WWDGRSTF` | +| 时钟源 | **LSI ≈ 32 kHz**(独立 RC,启动后强制开启且不可关) | APB1(42 MHz,依赖 PLL) | +| 依赖 CPU 时钟 | ❌ | ✅(PLL 失锁/时钟切换期失效) | +| 依赖中断 | ❌ | ✅(早期唤醒中断 EWI) | +| 复位标志 | `IWDGRSTF` | `WWDGRSTF` | | 能否停 | **启动后只能靠复位停** | 可关 | -| 喂狗窗口 | 只有**下限**(太晚不喂就复位) | 有**上下限**(过早喂也复位) | +| 喂狗窗口 | 只有下限 | 有上下限(过早喂也复位) | -**结论:选 IWDG。** 核心理由是本项目的故障谱——死锁、饿死、HardFault、断言挂死。 -这类故障**可能伴随时钟异常或中断被关闭**(`configASSERT` 就是关中断), -而 WWDG 依赖 APB1 与中断,恰恰在这些场景下失效。IWDG 走独立 LSI,是全场景兜底。 +**结论:IWDG。** 本项目的故障谱(死锁、饿死、Fault、断言挂死)可能伴随时钟异常或中断被关闭,WWDG 恰在这些场景失效;IWDG 走独立 LSI,是全场景兜底。台架 V4(§7)实测:`__disable_irq()` 后死循环仍每 ~4.9 s 稳定复位,证实独立于中断。 -**代价**:LSI 精度差(datasheet 典型 32 kHz,实际可在 17–47 kHz 区间漂移,且随 -温度/电压变化)。项目 `.ioc` 已按 `RCC.LSI_VALUE=32000` 建模,超时需按**最坏情况** -留裕度(见 §4.3)。 +**代价**:LSI 精度差(可在 17–47 kHz 漂移,随温度/电压变化),`.ioc` 按 `RCC.LSI_VALUE=32000` 建模,**必须按最坏情况校验**(§2.2)。 -### 2.1 IWDG 超时计算 +### 2.2 超时计算 -STM32F4 的 IWDG 为 **12 位递减计数器**,超时公式(参考手册 §21.3): +IWDG 为 12 位递减计数器(RM0090 §21.3): ``` T_IWDG = (4 × 2^PR) × RLR / F_LSI ``` -其中 `PR` = 预分频寄存器值(0..7,对应分频 4/8/.../256),`RLR` = 重装值(0..4095)。 - -按 `F_LSI = 32 kHz` 标称值: - -| PR | 分频 | RLR | 标称超时 | **最坏超时(F_LSI=47 kHz)** | +| PR | 分频 | RLR | 标称(32 kHz) | **最坏(47 kHz)** | |---|---|---|---|---| -| 4 | 64 | 1249 | 2498 ms | ≈ 1701 ms | -| 4 | 64 | 2499 | 4998 ms | ≈ 3403 ms | -| 4 | 64 | 4999 | 9998 ms | ≈ 6807 ms | -| 5 | 128 | 1249 | 4996 ms | ≈ 3402 ms | -| 5 | 128 | 2499 | 9996 ms | ≈ 6806 ms | +| **4** | **64** | **1249** | **2498 ms** | **≈1701 ms** | +| 4 | 64 | 2499 | 4998 ms | ≈3403 ms | +| 5 | 128 | 1249 | 4996 ms | ≈3402 ms | -(表中数值经脚本按上述公式核算,非目测估计。) +**LSI 偏快时超时变短**,故按 47 kHz 校验。三个时长必须严格包含: -> ⚠️ **关键**:LSI 偏快时超时**变短**。设计必须按"最坏情况(LSI=47 kHz,即超时打约七折)" -> 校验,而不是按标称 32 kHz。上表"最坏超时"列即按 47 kHz 计算。 +``` +T_任务循环 < 监视窗口(100ms) < 停喂判定(800ms) < T_IWDG最坏(1701ms) +``` -**本方案选定(见 §4.3)**:`PR=4`(64 分频)/ `RLR=1249` → 标称 **2498 ms**、 -最坏 **≈1701 ms**。 +即:停喂后仍有约 **900 ms** 余量才到复位;实测 V2 从声明 stall 到复位 ≤2.5 s。 -### 2.2 实施路径:**走 CubeMX 生成**(原方案为直接寄存器,已变更) +### 2.3 配置落点 -> **变更说明(2026-09-29)**:本节初稿推荐"直接寄存器操作"(下称路径 C),依据是 -> HAL IWDG 驱动源码不在仓库。经与项目负责人确认,**改走 CubeMX 图形界面生成** -> (路径 A)——让 `.ioc` 成为配置的唯一事实源、与其他外设保持一致。路径 A 的 -> 原评估"高风险"已被本次实践证伪:重生成带来的定制覆盖是**有限且可枚举**的 -> (实测见 §10 第 2 步),处理方式已固化为"重生成 → 逐项恢复 → 编译验证"。 -> 以下保留路径比较,作为决策记录。 - -**约束事实**(§1.1 表的第 3、4 行):HAL IWDG 驱动源码**不在仓库**、也**不在 MDK 工程**中。 -CubeMX 只在勾选该外设时才会把 `stm32f4xx_hal_iwdg.c/h` 拷进 `Drivers/STM32F4xx_HAL_Driver/`。 - -因此 `#define HAL_IWDG_MODULE_ENABLED` 一旦取消注释,`stm32f4xx_hal_conf.h:375` -就会 include 一个**不存在的头文件** → 编译失败。 - -三条可选路径: - -| 路径 | 做法 | 评估 | +| 层 | 落点 | 内容 | |---|---|---| -| **A. 从 CubeMX 重新生成(勾选 IWDG)**(**实际采用**) | 用 `.ioc` 勾选 IWDG 后生成 | ✅ **配置单一事实源、与其他外设一致**;代价 = 需恢复被覆盖的既有定制(实测 4 处,见 §10 第 2 步),且须按 `AGENTS.md` 处理行尾空白抖动 | -| B. 手工补入 `stm32f4xx_hal_iwdg.c/h` | 从 STM32CubeF4 包拷贝两文件 + 注册进 MDK 工程 | ⚠️ 需**手改 `.uvprojx`**(XML 结构脆弱),且引入约 0.3 KB Flash 的 HAL 封装;`.ioc` 与代码脱节 | -| C. 直接寄存器操作(初稿推荐,**已放弃**) | 用 CMSIS 已提供的 `IWDG` 实例指针 + 寄存器位域宏 | ✅ 零新增依赖、零工程文件改动、约 20 行;❌ 但**破坏了"外设配置统一由 CubeMX 表达"的约定**,后续维护者无处可查"这台设备的 IWDG 配了什么" | +| **L1(`.ioc`)** | `IWDG.Prescaler=IWDG_PRESCALER_64` / `IWDG.Reload=1249` | CubeMX 生成 `Src/iwdg.c` 的 `hiwdg.Init.*`,重生成不丢 | +| **代码(判定侧)** | `WD_IWDG_PRESCALER=4` / `WD_IWDG_RELOAD=1249` | **真正写寄存器的是这两只宏**(见 §2.4) | +| **喂狗/启动** | 裸寄存器 `IWDG->KR`(不经 HAL 封装) | 100 ms 级高频路径,HAL 封装的 `State` 检查无收益 | -**选 A 的依据**:本项目的既有约定是"外设配置 = `.ioc`"(`AGENTS.md`「平台约束」: -`Inc/`/`Src/` 中 CubeMX 生成文件带 `USER CODE` 标记,自定义代码只写标记区内)。 -IWDG 若用裸寄存器,则分频/重载值只散落在 `App/util/task_watchdog.c` 的宏里, -与 `.ioc` 无关联——下一个维护者改超时值时**不会想到去改代码宏**。 -路径 A 让该配置出现在 CubeMX 界面里,符合项目惯例。 +IWDG 是"写键值"式外设(KR 只写,读回恒 0): -**CubeMX 侧配置值**(在 `.ioc` / CubeMX 图形界面填写): - -| 项 | 值 | 说明 | -|---|---|---| -| Activated | ✅ 勾选 | Peripheral 树 → IWDG | -| IWDG counter clock prescaler | **64** | 对应 `IWDG_PRESCALER_64`,即 `PR=4` | -| IWDG down-counter reload value | **1249** | 12 位,范围 0~4095 | -| Window value | —— | ⚠️ **本 HAL 版本 `IWDG_InitTypeDef` 无此字段**(仅 `Prescaler` / `Reload`),CubeMX 界面亦无该项;窗口寄存器保持复位默认 `0xFFF` = 禁用窗口,符合预期 | - -生成结果:`Src/iwdg.c` / `Inc/iwdg.h`(新文件)、`Drivers/STM32F4xx_HAL_Driver/{Inc,Src}/stm32f4xx_hal_iwdg.{h,c}`、 -`Inc/stm32f4xx_hal_conf.h` 中 `HAL_IWDG_MODULE_ENABLED` 解除注释、`.uvprojx` 挂载驱动、 -`Src/main.c` 的 `MX_*_Init()` 序列新增 `MX_IWDG_Init()`。 - -> ⚠️ **路径 A 的固有陷阱(本次实测踩中,P0)**:CubeMX 把 `MX_IWDG_Init()` 插在 -> `app_main_init()` **之前**,而 `HAL_IWDG_Init()` 内部第一句即 -> `__HAL_IWDG_START(hiwdg)`(立即启动、之后不可停)。启动段(NAND 初始化等)耗时 -> 可达数秒且无喂狗点 → 按 2.5s 超时必然复位 → **开机死循环**。 -> **CubeMX 界面无法关闭这个自动启动**(HAL 行为,无对应选项)。 -> 解决办法见 §4.5 决策 C:把 `Src/iwdg.c` 里那一行 `HAL_IWDG_Init(&hiwdg)` 换成 -> 只填参数的 USER CODE 注释块,启动动作移交 `task_wd_start()`。 -> 这是路径 A 下**唯一必须手工修改生成文件**的地方,已在文件内留注释说明恢复方法。 - -**参考:CMSIS 设备头已提供的寄存器定义**(路径 C 曾依赖,现仅作 `task_wd_beat`/`task_wd_poll` -里裸写 `IWDG->KR` 的依据): - -| 已有定义 | 位置 | 内容 | -|---|---|---| -| `IWDG_TypeDef` | `stm32f407xx.h:573-579` | `KR` / `PR` / `RLR` / `SR` 四寄存器结构体 | -| `IWDG_BASE` | `:944` | `APB1PERIPH_BASE + 0x3000` | -| `IWDG` 实例指针 | `:1068` | `((IWDG_TypeDef *) IWDG_BASE)` | -| `IWDG_KR_KEY_Msk` | `:9331` | 键值字段掩码 | -| `IWDG_PR_PR_Msk` / `IWDG_PR_PR_0..2` | `:9336-9340` | 分频字段 | -| `IWDG_RLR_RL_Msk` | `:9344` | 重装值字段(12 位) | -| `IWDG_SR_PVU` / `IWDG_SR_RVU` | `:9349-9353` | 更新进行中标志(须轮询等它清零) | - -**键值常量**(由 HAL 定义在 `stm32f4xx_hal_iwdg.h`,本方案在 `task_watchdog.c` 内裸写): - -| 用途 | 值 | +| 键值 | 用途 | |---|---| -| 喂狗(重装计数) | `0x0000AAAA` | -| 启动看门狗 | `0x0000CCCC` | -| 解锁 PR/RLR 写保护 | `0x00005555` | -| 锁定 PR/RLR(保护配置) | `0x00000000` | +| `0x00005555` | 解锁 `PR`/`RLR` 写保护 | +| `0x0000CCCC` | 启动(此后不可停,只能靠复位) | +| `0x0000AAAA` | 重装计数器(喂狗) | -IWDG 是**"写键值"式外设**:写 `0x5555` 解锁 → 配 `PR`/`RLR` → 写 `0xCCCC` 启动 -(启动后**不可停**,只能复位)→ 此后周期性写 `0xAAAA` 喂狗。 +⚠ `PR`/`RLR` 每只写完后须等 `SR.PVU`/`SR.RVU` 清零(同步到 LSI 域,≤5 个 LSI 周期),**否则后写的那只会被静默丢弃**(2026-10-02 SWD 实测)。 -> **喂狗为何仍走裸寄存器而非 `HAL_IWDG_Refresh()`**:喂狗是 100 ms 级高频操作, -> HAL 封装带 `IWDG_HandleTypeDef` 有效性检查(读 `State` 字段、判 `LOCKED`), -> 在高频路径上无收益。`Task_wd_poll()` 里写 `IWDG->KR = 0xAAAA` 是 1 条存储指令。 -> 这不是"绕过 HAL",而是"HAL 提供了该寄存器定义,直接用"——与路径选择无关。 +### 2.4 延后启动:`MX_IWDG_Init()` 只填参数 + +CubeMX 把 `MX_IWDG_Init()` 插在 `app_main_init()` **之前**,而 `HAL_IWDG_Init()` 第一句就是 `__HAL_IWDG_START()`(立即启动)。启动段(NAND/RTC/ADC)耗时可达数秒且无喂狗点 → 按最坏 1.7 s 必然复位 → **开机死循环**(2026-09-29 实测)。 + +**现做法(L2,2026-10-09 起)**:`Src/iwdg.c` 的 `USER CODE BEGIN 0` 里放空实现 + 宏改名,把**本编译单元内**那句生成调用变成空操作——生成区保持 CubeMX 原样,**重生成零 diff**,不必再手工删那一句: + +```c +static HAL_StatusTypeDef iwdg_deferred_start(const IWDG_HandleTypeDef *a_h) { (void)a_h; return HAL_OK; } +#define HAL_IWDG_Init(a_h) iwdg_deferred_start(a_h) +``` + +`.map` 证实 `Removing stm32f4xx_hal_iwdg.o(i.HAL_IWDG_Init), (94 bytes)`。 +⚠ **不能走"反初始化"**:IWDG 启动后无法停止(唯一手段是复位),只能在启动**之前**拦截。 + +> 🔴 **连带 P0 教训(2026-10-02)**:`HAL_IWDG_Init()` 是 HAL 里**唯一**把 `hiwdg.Init.Prescaler/Reload` 写进寄存器的路径。改名成空实现后若不在别处补写,`PR/RLR` 会停在**复位默认值**(`PR=0`→÷4、`RLR=4095` → 实测 349~512 ms,比设计短约 5 倍)→ 800 ms 容忍分支沦为死代码、任何 >400 ms 心跳缺口直接误复位。 +> 现由 **`wd_platform_start()`** 完成"解锁 → 写 PR → 等 PVU → 写 RLR → 等 RVU → 写 0xCCCC",是全固件唯一写这两只寄存器的点。 +> **通用教训:凡"把某个 HAL `Init()` 拆开/延后",必须逐一核对被拆掉的函数还有哪些副作用没搬到新位置。** --- @@ -196,12 +140,7 @@ IWDG 是**"写键值"式外设**:写 `0x5555` 解锁 → 配 `PR`/`RLR` → ### 3.1 为什么不能"每个任务各自喂狗" -最朴素的方案——`HAL_IWDG_Refresh()` 塞进每个任务循环——有致命缺陷: - -**只要还有任何一个任务在跑,狗就会被喂。** 那么真正的故障场景(`netTask` 死锁、 -`uiTask` 饿死)**永远检测不到**,因为别的任务还在正常喂。 - -它退化成"只检测全部任务同时挂死",而那种情况现实中几乎不发生。 +`HAL_IWDG_Refresh()` 塞进每个任务循环 → **只要还有任何一个任务在跑,狗就被喂**,单任务卡死永远检测不到,退化成"只检测全部任务同时挂死"(现实中几乎不发生)。 ### 3.2 采用方案:任务置位 → 校验者判定 → 校验者喂狗 @@ -210,919 +149,407 @@ IWDG 是**"写键值"式外设**:写 `0x5555` 解锁 → 配 `PR`/`RLR` → > 图源:`tools/arch_diagram/gen_watchdog_svg.py`(改图改脚本重跑,见 > [`../架构设计/图形绘制流程.md`](../架构设计/图形绘制流程.md))。 -**语义**:喂狗 = "**所有被监视的任务都在窗口内动过**"。 -任何单个任务卡死 → 位图长期残缺 → 喂狗停止 → 2.5 s 后 IWDG 复位。 +**语义**:喂狗 = "所有被监视的任务都在窗口内动过"。 -具体到代码(`App/util/task_watchdog.c`): - -| 阶段 | 代码 | +| 阶段 | 代码(`App/util/task_watchdog.c`) | |---|---| -| ① 任务置位(生产者) | `s_hb_bits \|= (1u << id)`(`task_wd_beat`,仅置位不清零) | -| ② 校验者取走清零 | `snapshot = s_hb_bits; s_hb_bits = 0u;`(`task_wd_poll`,单字原子) | -| ③ 掩码判定 | `snapshot == wd_required_mask()`(默认 `0x37` = bit0\|1\|2\|4\|5) | -| ④ 判定为真 → 喂狗 | `s_miss_streak = 0u; IWDG->KR = 0x0000AAAAu;` | -| ④ 判定为假 → 累计 | `s_miss_total++`;连续 8 次(800 ms)后打 `[WD] task stall` 并**停喂** | +| ① 任务置位 | `task_wd_beat(id)`:`s_hb_bits \|= (1u << id)`(仅置位) | +| ② 校验者取走清零 | `snapshot = s_hb_bits; s_hb_bits = 0u;`(单字原子) | +| ③ 掩码判定 | `snapshot == wd_required_mask()`(**精确相等**,见 §3.5) | +| ④ 达标 → 喂狗 | `s_miss_streak = 0; s_stall_evidenced = 0; wd_platform_feed();` | +| ④ 缺席 → 累计 | `s_miss_total++`;首轮打 `MISS start`;连续 8 轮打 `[WD] task stall` 并**停喂**(每 episode 只落一次盘上证据) | -> 图中时间轴的四个时刻(0 / 0.8 s / 1.7 s / 2.5 s)与约 900 ms 裕度的推导见 §4.3。 +### 3.3 为什么用"取走并清零" -### 3.3 为什么心跳位图用"取走并清零" - -``` -snapshot = s_hb_bits; /* 读 */ -s_hb_bits = 0; /* 清 */ -``` - -这是**单字读-改-写**,在 Cortex-M4 上天然原子(无锁)。相比之下: +单字读-改-写在 Cortex-M4 上天然原子(无锁)。对比: | 方案 | 问题 | |---|---| -| 每任务用独立 `volatile uint32_t` 计数器,校验者比较"是否增长" | 需额外临界区,且 32 位计数器回绕判定繁琐 | -| 用 `osEventFlags` 做心跳 | 需要 7 个事件位;`osEventFlagsSet` **可能阻塞/触发调度**,任务循环里代价过高 | -| **单字位图 + 取走清零** | ✅ 零临界区、单指令原子、一个静态字 | +| 每任务独立计数器,比较"是否增长" | 需临界区,32 位回绕判定繁琐 | +| `osEventFlags` 做心跳 | 需 6 位;`osEventFlagsSet` 可能触发调度,任务循环内代价过高 | +| **单字位图 + 取走清零** | ✅ 零临界区、单指令、一个静态字 | -**取走清零的语义优势**:校验者看到的位图 = "**自上次抽样以来**哪些任务动过"。 -若某任务卡死,它的位在每轮抽样里恒为 0 → 连续缺失可累计 → 可判"连续 N 轮未达标"。 +语义优势:校验者看到的位图 = "自上次抽样以来谁动过" → 缺席可连续累计 → 可判"连续 N 轮未达标"。 ### 3.4 为什么喂狗者在 defaultTask | 候选 | 评估 | |---|---| -| **defaultTask** | ✅ 已具"单写者/属主"文化(配置引擎属主、syslog 唯一写者、控制台唯一发送者);循环基准 50 ms,天然适合 100 ms 抽样;栈已 4096 words,加少量逻辑无压力 | -| netTask | ❌ 主循环 `osDelay(1)`,节拍最紧,但它是**最可能卡死的任务之一**(SPI 事务等待)——让被监视者喂狗语义错乱 | -| 独立看门狗任务 | ❌ 需新建任务(栈 + TCB),RAM 预算紧张;且它比 defaultTask 更"旁路",出问题时无人知道 | -| 中断/timer | ❌ FreeRTOS timer 任务优先级高、栈小,且它的存活本身不能代表业务健康 | +| **defaultTask** | ✅ 已是"系统管家"(配置引擎属主、syslog 唯一写者、控制台唯一发送者);50 ms 循环基准天然适配 100 ms 抽样 | +| netTask | ❌ 节拍最紧(`osDelay(1)`)且是最可能卡死的任务之一——让被监视者喂狗语义错乱 | +| 独立看门狗任务 | ❌ 新建任务(栈 + TCB)与 RAM 预算冲突;且"谁来监视监视者"无解 | +| 中断 / FreeRTOS timer | ❌ 其存活不代表业务健康 | -**defaultTask 是既有的"系统管家"角色**,喂狗职责与之一致。 +> ⚠ **对抗性风险即期望行为**:defaultTask 自身卡死 → 狗永不喂 → 必然复位(fail-safe)。 +> 但"喂狗点被长 I/O 阻塞"这一形态需要单独处置,见 §4.3。 -> ⚠️ **对抗性风险**:defaultTask 自身卡死时,狗**永远不会被喂** → 必然复位。 -> 这正是期望行为(fail-safe),无需额外设计。 +### 3.5 掩码与"精确匹配"语义 + +抽样判定是 **`snapshot == required`**,不是 `(snapshot & required) == required`。因此**掩码必须恰好等于"会置位的任务集合"**——多一个位同样判失败。 + +| 构建 | 掩码 | 会置位的任务 | +|---|---|---| +| 产品 | `0x37` = bit0,1,2,4,5 | default / net / **ftp** / ui / timesync | +| `ENABLE_HW_LAYER_TESTS`(CH395F 硬件层测试,netTask 入口自挂起) | `0x35` = bit0,2,4,5 | default / ftp / ui / timesync(屏障等待按 100 ms 分片喂位) | +| 测试套件(`TEST_SUITE_*`,创建 testTask) | 同上 `0x37` | **testTask 任何构建都不入掩码、也不置位**(它可长阻塞;置位反而引入多余位误杀) | + +> 2026-10-02 台架:旧版"仅剔 NET 位 + 屏障等待者不置位"会让测试构建陷入 IWDG 复位循环——**掩码与会置位集合必须成对维护**,改任一处都要同步。 --- -## 4. 关键设计决策 +## 4. 关键设计决策(现状) -### 4.1 决策一:监视谁 —— 是否包含 testTask +### 4.1 启动时刻:defaultTask bring-up 完成后 -`testTask` 仅在启用测试套件时创建(`configCHECK_FOR_STACK_OVERFLOW` 与 -`TEST_SUITE_*` 决定),且套件运行时**独占存储/CH395F**,其循环可能长时间 -阻塞(如 nand_ftl 掉电模拟)。 +`task_wd_start()` 位于 `default_task.c:175`——存储深链(`f_mount`/`f_mkfs`/卷探针/syslog 挂载/配置加载)之后,即全系统最长初始化链完成、系统进入稳态之时。启动前先喂一次,使首个抽样窗口从启动时刻起算。 -| 方案 | 说明 | -|---|---| -| **A. 位图掩码按构建配置编译期生成**(推荐) | `#if defined(TEST_SUITE_*)` 决定 required mask 是否含 testTask 位 | -| B. 运行时判定(句柄非 NULL 才要求) | 更灵活,但要求校验者接触 TCB,代价高 | +**显式代价**:从复位到此处约数秒的窗口**无看门狗保护**;该段死机表现为"无任何输出 + LED 不闪",可人工辨识(§9 R1)。 -选 **A**:编译期掩码是最简单且零运行时开销的做法,与项目"按套件条件编译"的既有风格一致。 +### 4.2 停喂,而不是直接 `NVIC_SystemReset()` -### 4.2 决策二:`ENABLE_HW_LAYER_TESTS`(netTask 自挂起)的处理 +软复位会把 `RCC_CSR` 置 `SFTRSTF`,丢失"是看门狗抓到的"这一权威归因。让 IWDG 自然溢出 → `IWDGRSTF`。代价是额外 ≤2.5 s 延迟(可接受)。 -`net_task_func` 在 `ENABLE_HW_LAYER_TESTS` 下入口即 `for(;;) osDelay(1000);` -(`App/task/net_task.c:166-171`)——**该任务在该构建下本就不干活**。 +### 4.3 长耗时窗口的三类处置 -若照常要求它置心跳位,则**该构建永不喂狗**(要么把 `osDelay(1000)` 加心跳, -要么从掩码剔除)。 +统一原则:**启动段不启动狗;主循环段用"分片心跳 / 心跳路由 / 直接喂狗"三类手段覆盖**。 -**决策**:与 `TEST_SUITE_*` 同样走编译期掩码——`ENABLE_HW_LAYER_TESTS` 下剔除 netTask 位, -并保留其 `osDelay(1000)` 自挂起形态(心跳位反而说明该构建下 netTask "活着",语义无意义)。 - -### 4.3 决策三:超时时长与监视窗口 - -三个时长必须满足**严格的包含关系**: - -``` -T_任务循环 < T_监视窗口 < T_IWDG超时 < T_最坏超时(打七折) -``` - -推荐取值: - -| 参数 | 值 | 依据 | -|---|---|---| -| 任务循环周期 | `netTask` 1 ms / `defaultTask` 50 ms / `uiTask` 10 ms / `timeSyncTask` ≤ 50 ms / `ftpTask` 事件驱动(可能长时间阻塞) | 各自既有 `osDelay` 节拍 | -| **监视窗口** | **100 ms**(抽样周期) | 是 `defaultTask` 50 ms 循环基准的整数倍;对 `netTask` 1 ms 有 100× 裕度 | -| **缺失容忍** | **连续 8 次**(= 800 ms 无心跳才停喂) | 容忍 NAND 擦写(最坏数百 ms)、PHY 等待 3 s 不在本窗口(见 §4.4) | -| **IWDG 超时** | **2498 ms**(PR=4 / RLR=1249) | 最坏超时 ≈1701 ms,仍 > 800 ms 停喂判定期 + 约 900 ms 复位余量 | -| 最坏超时校验 | 1701 ms > 800 ms ✅ | 即使 LSI 偏快 47 kHz,停喂后也在 1.7 s 内复位 | - -> **`ftpTask` 是最大变数**:它在 `osEventFlagsWait(..., osWaitForever)` 上无限阻塞 -> (`Src/freertos.c:247`),之后 `lftpd_start` 内部是长驻事件循环。 -> **处理**:`ftpTask` 的心跳位**不放在"每次循环"**上,而是放在它内部的 event loop 与 -> 文件传输分片处——这需要改 `Lib/lftpd` 或在 `lftpd` 调用的回调里埋点。 -> **若短期不想改 `liblftpd`**:把 `ftpTask` 从掩码中**暂时剔除**, -> 并在此文档记录为已知缺口(FTP 死锁不可自动恢复)。**推荐后者作为第一步。** - -### 4.4 决策四:长耗时窗口的白名单 - -以下窗口**天然超过 100 ms 监视窗口**,必须显式处理,否则必然误杀: - -| 场景 | 位置 | 时长 | 处理 | +| 场景 | 位置 | 时长 | 处置手段 | |---|---|---|---| -| NAND 大块擦写 / `f_mkfs` | `storage_bring_up` | 数百 ms ~ 数秒 | **启动段不喂狗**(IWDG 在 bring-up 完成后才启动,见 §5.2) | -| `dhara_map_resume` 全盘扫描 | `nand_ftl_init` | 可能数秒(首次) | 同上 | -| `f_mount` forced | `storage_bring_up` | 数百 ms | 同上 | -| PHY 链路等待 | `net_stack_bring_up` | 最长 3 s(60×50 ms) | **该循环内显式置心跳位**,或 `net_stack_bring_up` 完成后才开始监视 netTask | -| LVGL 大页重绘 | `ui_task_func` | 单帧数十 ms | 10 ms 循环 + 100 ms 窗口已有裕度 ✅ | -| 存储套件掉电模拟 | `testTask` | 秒级 | 由 §4.1 掩码剔除 ✅ | -| `syslog` 落盘 / 配置写盘 | `defaultTask` | 数十 ms | 50 ms 循环内完成,窗口 100 ms ✅ | +| NAND 初始化 / `f_mkfs` / `dhara_map_resume` / `f_mount` | 启动段 | 数百 ms ~ 数秒 | **① 启动段不启动狗**(§4.1) | +| PHY 链路等待(60×50 ms) | `net_stack_bring_up` | 最长 3 s | **② 分片心跳**:循环内每轮 `task_wd_beat` | +| `time_sync_handle_conn` 的 30 s 总预算 | `App/sync/time_sync.c` | 单次可睡满 30 s | **② 切片**:`TIME_SYNC_WAIT_SLICE_MS=200` 夹住每次 `net_recv_to` 的 `rem`,每片先喂位(总预算语义不变) | +| 就绪屏障等待(NET/STORAGE READY) | net / ui / ftp / timesync 入口 | 不定 | **② 分片**:等待按 100 ms 分片并喂位 | +| 卷锁等待(FatFs 多任务) | `lftpd_io.c`、`App/fs/vol_lock.h` | 不定 | **② 分片**:100 ms 分片阻塞 + 每片 `lftpd_task_wd_beat()` | +| FTP 事件循环与传输分片 | `Lib`外的 `Drivers/BSP/NET/lftpd/*` | 秒级 | **② 分片**:事件循环每轮 + RETR/STOR 每分片喂位 | +| NAND 块擦除等待(最长 5 s)/ GC / 日志写 | `gd5f2gq5ue.c` `gd5f_wait_busy` | 秒级 | **② 心跳路由** + **③ 直接喂狗**(每 100 ms 双管) | +| 长 FTL 操作发生在 defaultTask 上下文(喂狗点自身被阻塞) | 同上 | ~2 s | **③ `nand_ftl_wd_feed()` 直接写 `IWDG->KR`** | +| 控制台排空(僵尸通道自愈路径) | `console_log.c` | — | `DBG_CON_DRAIN_BUDGET_MS=250` 单次 drain 上限,让位喂狗轮询 | -**统一原则**:**启动段不喂狗,主循环段才喂**。 +**② 心跳路由**(`task_wd_bind()` + `nand_ftl_wd_beat()`):FTL 长操作运行在**调用者任务**上下文,驱动只调弱钩子 `nand_ftl_wd_beat()`,本模块按 `task_wd_bind()` 登记的句柄把它路由到调用者任务的心跳位——驱动层不向上依赖应用层。`testTask` 刻意不登记(不在掩码内)。 -### 4.5 决策五:IWDG 的启动时刻 +**③ 直接喂狗**的语义边界(显式声明):NAND 真实卡死时该路径持续喂狗 = **设备存活但存储功能冻结**(与控制台 ORE 自愈同类边界,见 §9 R5)。 -关键约束来自项目铁律「`osKernelStart` 前禁止任何 FreeRTOS/CMSIS-RTOS API 调用」, -以及 `app_main_init` 内的深链耗时(NAND 初始化、RTC 读取、ADC 校准,合计可能 > 1 s)。 +### 4.4 断言与崩溃钩子 -| 方案 | 评估 | -|---|---| -| **A. 在 `app_main_init` 最早处启动 IWDG** | ❌ 后续 bring-up 深链(NAND 初始化可达数秒)会超时复位——**必死** | -| **B. 在 `osKernelStart` 前、`app_main_init` 完成后启动** | ⚠️ 仍要保证 `osKernelStart` 到 `defaultTask` 首次喂狗 < 2.5 s,实测任务创建 + 首次调度耗时很小 ✅ | -| **C. 在 `defaultTask` 启动段末尾(bring-up 完成后)启动** | ✅ 最保守:所有长耗时初始化都已完成,监视一开始就处于稳态 | -| **D. 在 `app_main_init` 完成后 + 手动扩展超时** | ⚠️ 复杂(IWDG 分频/重装值一旦启动不可改,除非先复位再配) | - -**决策:C(defaultTask bring-up 完成后启动 IWDG)**,理由: - -1. 完全绕开"初始化深链超时"这一最大陷阱——**最简单即最可靠**。 -2. `defaultTask` 的 bring-up 包含 `f_mount` / `f_mkfs` / 卷探针 / 配置加载, - 是**全系统最长的初始化链**。它完成即代表系统进入稳态。 -3. 代价:**从复位到 bring-up 完成这段约数秒的窗口无保护**。 - 这段窗口内若死机,用户观察到"上电后无任何输出 + LED 不闪"——可通过 - §6 的复位原因分类与 LED 指示来区分(LED 不闪 = 启动段挂死)。 -4. **与 §4.4 白名单统一**:既然启动段本就不该被监视,干脆不启动。 - -> **代价显式声明**:启动段无看门狗保护,这是本方案的已知边界。 -> 若后续需要覆盖,可在 `app_main_init` 完成后启动一个"短超时 + 手动多喂" -> 的临时监视(复杂度显著上升,暂不采用)。 - -> ⚠️ **决策 C 的必配项(2026-10-02 P0 实测补记,曾漏做并致 5 倍超时偏差)**: -> 既然"启动"被延后到 `wd_platform_start()`,**PR/RLR 的写入也必须一并搬过去**。 -> 删 `HAL_IWDG_Init()` 的初衷只是阻止它"立即启动",但它同时是**唯一**写这两只 -> 寄存器的路径——漏搬的后果是寄存器停在**复位默认值**(`PR=0`→÷4、`RLR=4095`, -> 实测 ≈349~512ms,比设计短约 5 倍),使 §4.3 的"超时 ⊃ 监视窗口"包含关系失效、 -> §3.2 的停喂判定分支沦为**死代码**,任何 >400ms 心跳缺口都直接误复位。 -> 现已在 `wd_platform_start()` 内补写(见 §5.3 与变更记录)。 -> **通用教训:凡"把某个 HAL 初始化函数拆开/延后"的改动,必须逐一核对被拆掉的 -> 函数还有哪些副作用没有搬到新位置——HAL 的 `Init()` 往往不只是"赋值 + 调用"。** - -### 4.6 决策六:`configASSERT` 改为"先记录后复位" - -现状(`Inc/FreeRTOSConfig.h:163`): +`Inc/FreeRTOSConfig.h:165`(L2,`USER CODE BEGIN 1` 区): ```c -#define configASSERT( x ) if ((x) == 0) {taskDISABLE_INTERRUPTS(); for( ;; );} +#define configASSERT( x ) if ((x) == 0) { vApplicationAssertFailed(__FILE__, __LINE__); } ``` -改为: +`vApplicationAssertFailed()`(`Src/freertos.c` 的 `USER CODE Application` 区)经 `hook_ptr_ok()` 校验文件指针、取路径末段,裸 UART 打 `[ERR] ASSERT FAILED : (resetting)` 后 `NVIC_SystemReset()`。即使复位逻辑本身因内核态损坏而失效,**IWDG 仍会在最坏 1.7 s 后强制复位**(双保险)。 -```c -/* 断言失败:记录任务名后软复位(由 IWDG 兜底真死循环场景) */ -#define configASSERT( x ) \ - if ((x) == 0) { vApplicationAssertFailed( ( const char * )__FILE__, __LINE__ ); } -``` +**2026-10-10 栈整改**:钩子里的 `name[49]`/`num[16]` 改**函数级 static**(终点路径、打印完即复位、无并发),行号转换改原地反转,去掉 `HAL_Delay(200)`(`con_tx_bounded` 已等 TC)。原因:`configASSERT` 在 FreeRTOS 内部可达 → 本钩子挂在**每个任务**的最坏调用链尾,放栈上的 81 B 会出现在所有任务的静态最坏链上。 -在 `Src/freertos.c` 实现 `vApplicationAssertFailed()`,复用现有 -`hook_ptr_ok()` + `dbg_console_write_raw()` 的**裸 UART 有界发送**模式 -(与 `vApplicationStackOverflowHook` 同构),随后 `NVIC_SystemReset()`。 +4 个 Fault(Hard/Mem/Bus/Usage)的采集与使能(`fault_handler_enable()` 使能后三者,否则被升级成 HardFault 看不出根因)见 §10.2 与 `AGENTS.md`「平台约束」。 -**收益**:断言从"无日志永久砖"变为"有日志 + 自动复位"。 -即使复位逻辑本身因内核态损坏而失效,**IWDG 仍会在 2.5 s 后强制复位**(双保险)。 +### 4.5 复位原因锁存与优先级 -### 4.7 决策七:与既有钩子的复位原因区分 +`RCC_CSR` 的复位标志只由 POR 清除、读后不自动清 → 必须读并锁存后 `SET_BIT(RCC->CSR, RCC_CSR_RMVF)`,否则下次软复位会读回上次残留。 -`NVIC_SystemReset()` 置 `SFTRSTF`,IWDG 超时置 `IWDGRSTF`。 -两者在 `RCC_CSR` 中**不同位**,可区分。 +现状:`Src/main.c` 在 `HAL_Init()` 之后(`USER CODE BEGIN Init` 区)锁存到 `g_rcc_csr_latched` 并清标志;`app_main_init()` 早期翻译并打印。 -但要注意 **`RCC_CSR` 的复位标志只由 POR(上电)清除**, -且读后**不会自动清**——必须在读完并记录后手工 `SET_BIT(RCC->CSR, RCC_CSR_RMVF)` -清标志,否则下次软复位会看到"上一次的残留标志"。 +**代码实际优先级**(`app_main.c reset_cause_str()`,按"最可信归因"而非因果强度): -**统一方案**:在 `Src/main.c` 的 `HAL_Init()` 之后、`app_main_init()` 之前, -一次性读取并锁存复位原因到静态变量,随后清标志: +| 顺序 | 标志 | 描述串 | +|---|---|---| +| 1 | `IWDGRSTF` | `IWDG (independent watchdog)` | +| 2 | `WWDGRSTF` | `WWDG (window watchdog)` | +| 3 | `SFTRSTF` | `software (NVIC_SystemReset)` | +| 4 | `PORRSTF` | `power-on (POR)` | +| 5 | `PINRSTF` | `external pin (NRST)` | +| 6 | `BORRSTF` | `brown-out (BOR)` | +| 7 | 全 0 | `unknown` | -``` -读 RCC_CSR → 判定优先级(POR > PIN > IWDG > WWDG > SFT > LPWR > BOR) - → 锁存到 s_reset_cause - → SET_BIT(RCC->CSR, RCC_CSR_RMVF) ← 清标志 -``` +看门狗排在软件复位**之前**:两者同时置位时(崩溃钩子软复位 + 期间也超时),报告看门狗更有价值。 -然后由 `syslog_mcu_mounted()` 把 `s_reset_cause` 写进 `run.log` 的描述串 -(扩展 [`运行与自检记录设计说明.md`](运行与自检记录设计说明.md) §5.3 的 -"上电/重启"二分类为多分类)。 +### 4.6 与 `run.log` / `selftest.log` 的集成 -### 4.8 决策八:复位原因判定的优先级 - -多个复位标志可能同时置位(如 IWDG 复位也会置 `PINRSTF`? —— 不会,但 -IWDG 复位常伴随 `LPWRRSTF` 若同时发生低压)。按**因果强度**取优先级: - -| 优先级 | 标志 | 含义 | 描述串 | -|---|---|---|---| -| 1 | `PORRSTF` / `BORRSTF` | 上电 / 欠压(真cold start) | `power on` | -| 2 | `PINRSTF` | 外部复位引脚(人工按复位/调试器) | `pin reset` | -| 3 | `IWDGRSTF` | **看门狗超时**(本次引入的核心目标) | `watchdog reset` | -| 4 | `WWDGRSTF` | 窗口看门狗(本项目不用) | `wwdg reset` | -| 5 | `SFTRSTF` | 软件复位(`NVIC_SystemReset`:栈溢出钩子 / 断言钩子) | `soft reset` | -| 6 | `LPWRRSTF` | 低功耗管理复位 | `lpwr reset` | -| 7 | 全 0 | 未识别 | `unknown reset` | - -> **注意 §5.3 的既有"上电/重启"时间窗分类(10 min)与本节的复位原因分类是 -> 互补的**:前者靠 `run.log` 时间差推断(无硬件依据),后者靠硬件标志(权威)。 -> **建议:硬件标志优先,时间窗分类降级为兜底**(当 `RCC_CSR` 读到全 0 时使用)。 - -### 4.9 决策九:模拟器如何兼容 - -模拟器(`simulator/`)在 PC 上跑 UI 与配置/日志自检。IWDG 是 MCU 专属外设: - -| 文件 | 处理 | +| 通道 | 现状 | |---|---| -| `simulator/sim/sim_port.c` | 增加 `iwdg_*` 桩函数(空实现 + 计数器,供自检断言) | -| 心跳位图本身 | **可在模拟器正常工作**——它是纯 RAM 逻辑。建议模拟器也跑位图, -便于用 `--watchdogtest` 自检模式回归"缺失检测"逻辑 | +| 串口 | 每次开机第一行区打 `[APP] reset cause: <类型> (CSR=0x…)`(`@test` 已把它当启动锚点并校验次数与期望值) | +| `selftest.log` | **看门狗/窗口看门狗复位 → `syslog_post(SYSLOG_SELFTEST, "watchdog reset")`**(只记故障,符合既有约定) | +| `run.log` | 仍是"上电 / 重启"二分类(靠末条 epoch 时间窗推断),**未接入硬件复位原因**——细粒度原因不上 `run.log`/UI(避免中文字符集重生成),见 §9 O2 | +| `/log/wdev.log` | 停喂现场证据(`sys_log_evidence()`,>8 KB 先 unlink 重建,防长跑无界增长) | -**模拟器无法验证的部分**:真实复位行为、LSI 漂移、`RCC_CSR` 标志—— -这些必须真机验证(见 §7)。 +### 4.7 模拟器与主机侧 + +- **模拟器不编译本模块**(`simulator/` 无 `task_watchdog.c`,也无 `--watchdogtest`)——原计划的 S1 已降级,判定逻辑改由 **L0.5 主机侧单测**覆盖(39 断言,见 §7)。 +- 不可建模的部分:真实复位、LSI 漂移、`RCC_CSR` 标志——必须真机(§7)。 --- -## 5. 详细设计 +## 5. 实现现状 -### 5.1 文件与接口 +### 5.1 文件与职责 -| 文件 | 类型 | 职责 | -|---|---|---| -| `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)` | -| `App/log/sys_log_mcu.c` | 修改 | `run.log` 写入复位原因描述串 | -| `Inc/FreeRTOSConfig.h` | 修改 | `configASSERT` 宏改写(§4.6) | +| 文件 | 职责 | +|---|---| +| `App/util/task_watchdog.{c,h}` | 心跳位图、判定状态机、IWDG 配置与启动、FTL/FTP 心跳路由、取证旁路 | +| `Src/iwdg.c` | CubeMX 生成;`USER CODE BEGIN 0` 宏改名 → 只填参数不启动(§2.4) | +| `Src/main.c` | `HAL_Init()` 后锁存复位原因 + 清 `RMVF`;`HAL_Init()` **前** `task_wd_tick_snapshot()`;TIM7 回调内 `task_wd_tick_probe()` | +| `App/app/app_main.c` | `reset cause:` 上报 + `selftest.log` 记录 + `task_wd_seg_report()` | +| `App/task/default_task.c` | 唯一喂狗者:`task_wd_start()` + 每轮 `task_wd_poll()` / `task_wd_beat()` / `task_wd_seg()` | +| `App/task/{net,ui}_task.c`、`App/sync/time_sync.c`、`Src/freertos.c`(ftpTask) | 心跳埋点 + `task_wd_bind()` 登记 | +| `Drivers/BSP/NET/lftpd/*` | 弱钩子 `lftpd_task_wd_beat()` 的节拍点(事件循环、传输分片、连接等待、卷锁等待) | +| `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.{c,h}` | 弱钩子 `nand_ftl_wd_beat()` / `nand_ftl_wd_feed()` 的调用点(FTL 入口与 `wait_busy` 轮询) | +| `App/log/sys_log_mcu.c` | `sys_log_evidence()` → `/log/wdev.log` | +| `Inc/FreeRTOSConfig.h` | `configASSERT` 改造(L2) | +| `test/host/watchdog/` | 判定状态机主机侧单测(`#include` 生产源码 + 强符号桩) | +| `MDK-ARM/STM32F407-Demo.uvprojx` | 挂载 `task_watchdog.c` 与 `stm32f4xx_hal_iwdg.c` | +| `MDK-ARM/STM32F407-Demo.sct` | `UNINIT_CCM`(0x1000FF00,256 B)承载 `.noinit` 取证槽 | -**不需要改动的**: - -- ❌ `Inc/stm32f4xx_hal_conf.h` —— **不启用** `HAL_IWDG_MODULE_ENABLED`(会 include 不存在的头文件,§2.2) -- ❌ `MDK-ARM/*.uvprojx` —— 无需手改 XML 添加源文件 -- ❌ `STM32F407-Demo.ioc` —— 无需重新生成 CubeMX - -> ⚠️ **`App/util/task_watchdog.c` 需登记进 MDK 工程**:新建 `.c` 文件后必须在 -> `MDK-ARM/*.uvprojx` 的对应 Group 中挂载,否则不参与编译。 -> 这是唯一一处工程文件改动(新增一个 `` 节点),与"删改既有节点"的低风险操作不同。 -> **替代做法**:若不便改工程文件,也可把实现直接放进 `App/task/default_task.c` -> 内的 `USER CODE` 区(.c 已在工程中)——**代价是模块边界不清,不推荐**。 - -> 关于 `Src/freertos.c` 的埋点:该文件是 CubeMX 生成文件,`vApplicationAssertFailed()` -> 必须写在 `/* USER CODE BEGIN Application */` 区内(现有两个钩子就在该区内,位置正确); -> **心跳埋点则不在该文件**——各任务循环分别在 `App/task/*.c` / `App/sync/*.c` 中。 - -### 5.2 接口设计 +### 5.2 对外接口 ```c -/* App/util/task_watchdog.h */ +void task_wd_beat(task_wd_id_t id); /* 各任务循环内置位(1~2 指令,任意上下文) */ +void task_wd_bind(task_wd_id_t id); /* 任务入口一次:登记句柄供 FTL 心跳路由 */ +void task_wd_start(void); /* 仅 defaultTask,bring-up 后一次:配置并启动 IWDG */ +void task_wd_poll(void); /* 仅 defaultTask 主循环:抽样 + 判定 + 喂狗(内部 100ms 节流) */ +uint32_t task_wd_last_mask(void); /* 观测:上次抽样位图(0 位 = 缺席任务) */ +uint32_t task_wd_miss_count(void); /* 观测:累计未达标轮数 */ +uint32_t task_wd_is_armed(void); /* 观测:1 = IWDG 已启动 */ -/* 任务 ID(位序号 = 位图上的位置,与掩码一一对应) */ -typedef enum { - TASK_WD_DEFAULT = 0, - TASK_WD_NET = 1, - TASK_WD_FTP = 2, - TASK_WD_TEST = 3, - TASK_WD_UI = 4, - TASK_WD_TIMESYNC = 5, - TASK_WD_ID_MAX -} task_wd_id_t; - -/* 各任务在自己的循环里调用:置位自己的心跳位(1~2 条指令,可放中断外任意位置) */ -void task_wd_beat(task_wd_id_t id); - -/* defaultTask 主循环调用:抽样 + 判定 + 必要时喂狗;内部节流到 100ms 一次 */ -void task_wd_poll(void); - -/* defaultTask 启动段末尾调用:启动 IWDG 并初始化位图 */ -void task_wd_start(void); - -/* 观测接口(供自检/串口报告) */ -uint32_t task_wd_miss_count(void); /* 累计"未达标"轮数 */ -uint32_t task_wd_last_mask(void); /* 上次抽样到的位图(定位卡死任务) */ +/* 取证(仅 MCU 构建):进段打点与时间源存活性,见 §5.6 */ +void task_wd_seg(uint32_t seg); +void task_wd_seg_report(void); +void task_wd_tick_snapshot(void); /* 必须在 HAL_Init() 之前 */ +void task_wd_tick_probe(void); /* TIM7 ISR 内,每 1ms */ ``` -### 5.3 实现要点 +任务 ID(位序号即掩码位,**顺序不可插空**):`DEFAULT=0 / NET=1 / FTP=2 / TEST=3 / UI=4 / TIMESYNC=5`。 -```c -/* App/util/task_watchdog.c 核心逻辑(示意) */ +本模块**不创建任何 FreeRTOS/CMSIS-RTOS 对象**,可安全用于内核启动前后的上下文(唯一例外:`task_wd_bind()`/`nand_ftl_wd_beat()` 需内核句柄,内核前为空操作)。 -/* ---- IWDG 寄存器级驱动(§2.2 路径 C,不依赖 HAL IWDG 驱动) ---- */ -#include "stm32f407xx.h" /* 提供 IWDG 实例指针与位域宏(CMSIS 设备头已含) */ +### 5.3 判定状态机要点 -#define IWDG_KEY_RELOAD 0x0000AAAAu /* 喂狗:重装计数器 */ -#define IWDG_KEY_ENABLE 0x0000CCCCu /* 启动(此后不可停) */ -#define IWDG_KEY_WRITE_ENABLE 0x00005555u /* 解锁 PR/RLR 写保护 */ -#define IWDG_KEY_WRITE_DISABLE 0x00000000u /* 锁定 PR/RLR */ - -/* 超时配置:PR=4 → 4×2^4 = 64 分频;RLR=1249 - * 标称 64×1249/32000 ≈ 2498 ms;最坏(LSI=47k) ≈ 1701 ms */ -#define WD_IWDG_PRESCALER 4u -#define WD_IWDG_RELOAD 1249u - -static void wd_iwdg_init(void) -{ - IWDG->KR = IWDG_KEY_WRITE_ENABLE; /* 解锁 PR/RLR */ - IWDG->PR = WD_IWDG_PRESCALER; /* 写入即开始同步 */ - IWDG->RLR = WD_IWDG_RELOAD; - while (IWDG->SR != 0u) { /* 等 PVU/RVU 清零:配置已同步到 LSI 域 */ } - IWDG->KR = IWDG_KEY_RELOAD; /* 先装初值 */ - IWDG->KR = IWDG_KEY_ENABLE; /* 启动:写 0xCCCC */ -} - -static inline void wd_iwdg_feed(void) -{ - IWDG->KR = IWDG_KEY_RELOAD; /* 写 0xAAAA */ -} - -/* ---- 心跳位图与判定 ---- */ -static volatile uint32_t s_hb_bits; /* 心跳位图:各任务只置位,校验者取走清零 */ -static uint32_t s_miss_streak; /* 连续未达标轮数 */ -static uint32_t s_miss_total; /* 累计未达标轮数(观测用) */ -static uint32_t s_last_mask; /* 上次抽样结果(观测用) */ - -#define WD_POLL_PERIOD_MS 100u /* 抽样周期 */ -#define WD_MISS_TOLERANCE 8u /* 连续未达标容忍次数(8×100ms = 800ms) */ - -/* 必需位掩码:编译期按构建配置生成(§4.1 / §4.2) */ -static uint32_t wd_required_mask(void) -{ - uint32_t m = (1u << TASK_WD_DEFAULT) | (1u << TASK_WD_NET) - | (1u << TASK_WD_FTP) | (1u << TASK_WD_UI) - | (1u << TASK_WD_TIMESYNC); -#if defined(TEST_SUITE_STORAGE) || defined(TEST_SUITE_NET) /* 按实际套件宏调整 */ - m |= (1u << TASK_WD_TEST); -#endif -#if defined(ENABLE_HW_LAYER_TESTS) - m &= ~(1u << TASK_WD_NET); /* netTask 自挂起,不要求心跳 */ -#endif - return m; -} - -void task_wd_beat(task_wd_id_t id) -{ - s_hb_bits |= (1u << (uint32_t)id); /* 单字写,天然原子 */ -} - -void task_wd_poll(void) -{ - static uint32_t s_last_tick; - uint32_t now = HAL_GetTick(); - uint32_t snapshot; - - if ((now - s_last_tick) < WD_POLL_PERIOD_MS) { - return; /* 节流:非 100ms 边界直接返回 */ - } - s_last_tick = now; - - snapshot = s_hb_bits; /* 读 */ - s_hb_bits = 0u; /* 清:语义 = "自上次抽样以来谁动过" */ - s_last_mask = snapshot; - - if (snapshot == wd_required_mask()) { - s_miss_streak = 0u; - wd_iwdg_feed(); /* ✅ 全员健康:喂狗 */ - } else { - s_miss_total++; - if (s_miss_streak < WD_MISS_TOLERANCE) { - s_miss_streak++; - } - if (s_miss_streak >= WD_MISS_TOLERANCE) { - /* 停喂 → 约 2.5s 后 IWDG 复位。 - * 复位前尽力留证据:裸 UART(不依赖内核、不缺界) */ - dbg_console_write_raw("[ERR] WATCHDOG: task stall mask=...\r\n", ...); - /* 不喂狗,让 IWDG 自己复位(真复位原因=IWDGRSTF,可被 §4.7 识别) */ - } - } -} +``` +未启动 → 直接返回 +未到 100ms 边界 → 直接返回(节流) +取走并清零位图 → 记入近 8 次环形历史(取证) +snapshot == required → miss_streak=0、证据门闩复位、喂狗 +否则 → miss_total++、miss_streak++(饱和在容忍值) + 首轮 → 裸 UART `MISS start …` + miss_streak >= 8 → DBG_ERROR task stall + (本 episode 首次)落盘证据 + 不喂狗 ``` -> **为何"停喂"而不是直接 `NVIC_SystemReset()`**: -> 直接软复位会让 `RCC_CSR` 置 `SFTRSTF` 而非 `IWDGRSTF`, -> 丢失"是看门狗抓到的"这一关键信息。**让 IWDG 自然溢出**, -> 复位原因才是权威的 `watchdog reset`。代价是额外的 ≤2.5 s 延迟(可接受)。 +⚠ **证据门闩 `s_stall_evidenced`**:落盘走 NAND → FTL 路径会 `nand_ftl_wd_feed()` **直接喂狗**。若每 100 ms 都写,等于在"停喂"窗口里持续喂狗 → IWDG 永不溢出(2026-10-03 V2 首轮实测:stall 已声明、`miss` 累至 1157≈115 s 仍不复位)。每 episode 只落一次,恢复健康后复位门闩。注意 `s_miss_streak` 达上限后会**饱和**,不能用它判"首次"。 -### 5.4 各任务的埋点位置(**2026-09-29 已全部落地**) +### 5.4 平台后端(可测性) -| 任务 | 实际埋点位置 | 备注 | +硬件访问收敛到三个 `__weak` 钩子(声明在 `task_watchdog.h` 末段,**非对外接口**): + +| 钩子 | MCU 默认实现 | 主机侧 | |---|---|---| -| `defaultTask` | `App/task/default_task.c`:`for(;;)` 首行 `task_wd_poll()` + `task_wd_beat(TASK_WD_DEFAULT)` | 50 ms 节拍,天然满足;同时是唯一喂狗者 | -| `netTask` | `App/task/net_task.c`:① 主循环 `for(;;)` 首行;② `net_stack_bring_up` 的 PHY 等待循环内 `osDelay(50)` 前;③ `ENABLE_HW_LAYER_TESTS` 自挂起分支(掩码已剔除,埋点无副作用) | ② 为 §4.4 白名单项:PHY 最长等待 3 s(60×50 ms),必须在其中补心跳 | -| `ftpTask` | ⚠️ **首期不埋点**——由 `wd_required_mask()` **剔除**(见 §4.3) | 已知缺口:`Lib/lftpd` 事件循环改动面大,二期再补 | -| `uiTask` | `App/task/ui_task.c`:`ui_port_step(10)` / `ui_net_sink_poll()` 之后 | 10 ms 节拍;等待 NET_READY 期间不喂属**预期**(该段卡死即应复位) | -| `timeSyncTask` | `App/sync/time_sync.c`:① `time_sync_task` 主循环首行;② 监听重试的 500 ms / 2 s 延时前;③ accept 轮询 200 ms 延时前;④ 收尾 50 ms 延时前;⑤ **`time_sync_handle_conn` 内的等待循环**(关键:把 30 s 总预算切成 `TIME_SYNC_WAIT_SLICE_MS`=200 ms 的片,每片先喂心跳) | ⑤ 是本案最要害的一处:对端连上但不发时间包是完全正常的场景,若不切片,30 s 等待必然被判卡死 | -| `testTask` | 不埋点——由套件掩码决定,按 §4.1 排除 | 套件运行时可长阻塞 | +| `wd_platform_feed()` | `IWDG->KR = 0xAAAA` + 记录喂狗时刻/最大间隔 | 强符号桩:记录"何时被喂" | +| `wd_platform_start()` | 解锁 → 写 PR/RLR(等 PVU/RVU)→ `0xCCCC` | 强符号桩:不碰寄存器 | +| `wd_platform_tick_ms()` | `HAL_GetTick()`(时基 = TIM7,非 SysTick) | 强符号桩:驱动时间推进 | -> ⚠️ **实施时的新发现(补充 §4.4 白名单)**:`timeSyncTask` 的风险**不止** -> 主循环里那两处 `vTaskDelay(2000)`——`time_sync_handle_conn` 内部的 -> `net_recv_to(..., rem)` 单次可睡满 **30 s 总预算**。初稿只注意到 `vTaskDelay` -> 的两处,会漏掉这处。处理方式取"**切片**"而非"放宽窗口": -> 新增 `TIME_SYNC_WAIT_SLICE_MS = 200U`,把单次等待的 `rem` 夹到 ≤200 ms, -> 每片先 `task_wd_beat()`。总预算语义(≤30 s)完全不变,只是把一次长睡眠 -> 拆成多次短睡眠——同时提升了"对端提前到达"的响应粒度。 +与 `App/cfg/cfg_file_store.h` 的"共享核心 + 平台后端"同款:判定状态机成为纯逻辑,主机侧 `#include "task_watchdog.c"` 原样纳入生产源码并以强符号覆盖 → **测的是生产代码**,MCU 构建行为零变化。 + +### 5.5 心跳埋点位置(实测) + +| 任务 | 位置 | +|---|---| +| defaultTask | 主循环首段 `task_wd_poll()` + `task_wd_beat(TASK_WD_DEFAULT)`;入口 `task_wd_bind()` | +| netTask | 主循环;`net_stack_bring_up` 的 PHY 等待循环内;就绪屏障等待分片;入口 `task_wd_bind()` | +| ftpTask | 主循环 = lftpd 事件循环:`lftpd.c` 事件循环每轮、RETR/STOR 每分片;`lftpd_inet.c` 连接/数据等待每轮每片;`lftpd_io.c` 卷锁等待每片;入口 `task_wd_bind()` | +| uiTask | 主循环;就绪屏障等待分片;入口 `task_wd_bind()` | +| timeSyncTask | 主循环;监听重试(500 ms/2 s)前;accept 轮询;`time_sync_handle_conn` 的 **200 ms 切片**;屏障等待分片;入口 `task_wd_bind()` | +| testTask | **不埋点、不登记、不入掩码** | +| NAND FTL | `gd5f2gq5ue.c`:长操作入口 `nand_ftl_wd_beat()` + `nand_ftl_wd_feed()`;`gd5f_wait_busy` 每 100 ms 双管(路由到调用者任务) | + +> 弱钩子在驱动侧声明(`lftpd.h` / `gd5f2gq5ue.h`),本模块提供**强符号**路由到心跳位图 → 驱动层不向上依赖应用层(分层铁律)。 + +### 5.6 取证机制(复位后仍可读) + +误复位后 RAM 态丢失、串口又可能来不及打,故有三层"复位不丢"的取证(全部只取证,不参与判定): + +| 层 | 载体 | 开机读出并打印 | +|---|---|---| +| round 7 | RAM(近 8 次抽样位图环、最大喂狗间隔、console tx_fault 计数) | 停喂时 `STALL snap=… req=… h=… mg=… cf=… seg=…`(裸 UART + `/log/wdev.log` 各一份) | +| round 8 | `.noinit` 槽 `s_wd_seg_trail[4]`(magic/段号/时刻/循环序号) | `PREVRUN seg=N seq=N t=ms`——直接指向 defaultTask 主循环最后进段 | +| round 8b | `.noinit` 槽 `s_wd_tick_cur/prev[4]`(uwTick + TIM7 中断计数) | `TICKRUN uw=… n=… duw=…`——判定 HAL 时基(TIM7)是否与停摆同刻冻结 | + +要点: +- `.noinit` 必须 `section(".noinit")` **与** `zero_init` 同时给,并配 scatter 的 `UNINIT` 执行区;只给 section 时 armcc 会当"带初值 RW"→ 启动 scatterload 抹掉保留内容。 +- `task_wd_tick_snapshot()` **必须在 `HAL_Init()` 之前**调用:TIM7 在 `HAL_Init` 里就开始 1 ms 中断,而开机上报要到 `app_main_init`(≈117 ms)才跑,不先搬走上一轮终值就被覆盖。 +- `task_wd_tick_probe()` 在 TIM7 ISR 内**严禁调用任何 FreeRTOS/CMSIS-RTOS API**:TIM7 远早于 `xPortStartScheduler()`,此时 ISR 版内核 API 触发 `port.c` 优先级断言并把系统卡死在"USART1 尚未初始化"的断言打印路径上(2026-10-02 实测)。 --- -## 6. 可观测性 +## 6. 可观测性与判读 -### 6.1 串口标记(供 `@test` 采集) - -**实际实现**(2026-09-29 落地,`App/app/app_main.c` + `App/util/task_watchdog.c` + -`Src/freertos.c`): +### 6.1 串口标记 | 标记 | 时机 | 含义 | |---|---|---| -| `[APP] reset cause: <类型> (CSR=0x%08X)` | `app_main_init` 早期 | 复位原因,类型为 `IWDG (independent watchdog)` / `WWDG (window watchdog)` / `software (NVIC_SystemReset)` / `power-on (POR)` / `external pin (NRST)` / `brown-out (BOR)` / `unknown`;同时打印 `RCC_CSR` 原值便于事后核对 | +| `[APP] reset cause: <类型> (CSR=0x%08X)` | `app_main_init` 早期 | 复位原因 + `RCC_CSR` 原值(`@test` 启动锚点,出现 0 次或 >1 次判 FAIL) | +| `PREVRUN seg=… seq=… t=…` | 开机(裸 UART) | 上一轮 defaultTask 最后进段(magic 命中才打,一次复位只报一次) | +| `TICKRUN uw=… n=… duw=…` | 紧随其后 | 上一轮 HAL 时基终值;`duw≈0` → TIM7 与主循环同刻停摆;`duw≈2000+` → 另有阻塞点 | | `[WD] armed: timeout ~2498ms (worst ~1701ms), miss_tol=800ms` | `task_wd_start()` | IWDG 已启动(真机自检必查标记) | -| `[WD] task stall: mask=0x%08X req=0x%08X miss=%lu` | 连续 8 窗未达标时(每窗一次,共打印至复位) | 即将停喂 → 约 2.5 s 后 IWDG 复位。`mask` 的 0 位 = 缺席任务 | -| `[ERR] ASSERT FAILED : (resetting)` | `vApplicationAssertFailed()` | FreeRTOS 断言失败(`Src/freertos.c`),打印后主动复位 | -| `[ERR] HARDFAULT PC=.. LR=.. xPSR=.. R0-R3=.. CFSR=.. HFSR=.. [BFAR=..] [MMFAR=..]` | `HardFault_Handler` → `fault_entry_hard` | 硬故障现场(`Src/stm32f4xx_it.c`) | -| `[ERR] MEMFAULT PC=.. …` | `MemManage_Handler` → `fault_entry_mem` | MPU/特权级违规,含 `MMFAR` 出错地址(`CFSR.MMARVALID` 置位时) | -| `[ERR] BUSFAULT PC=.. …` | `BusFault_Handler` → `fault_entry_bus` | 非法总线访问/取指,含 `BFAR` 出错地址(`CFSR.BFARVALID` 置位时) | -| `[ERR] USAGEFAULT PC=.. …` | `UsageFault_Handler` → `fault_entry_usage` | 未定义指令/非法状态 | -| `[ERR] STACK OVERFLOW task=` | `vApplicationStackOverflowHook` | 既有钩子(栈溢出) | -| `[ERR] MALLOC FAILED (heap exhausted)` | `vApplicationMallocFailedHook` | 既有钩子(堆耗尽,只告警不复位) | +| `MISS start snap=… req=… t=…` | 缺席 episode 首轮(裸 UART) | poller 仍在抽样,给出首次缺席位图 | +| `[WD] task stall: mask=… req=… miss=…` | 连续 8 轮缺席(每轮一次) | 已停喂 → 最坏 1.7 s 后 IWDG 复位;`mask` 的 0 位 = 缺席任务 | +| `STALL snap=… req=… h=… mg=… cf=… seg=…` | 停喂 episode 首次 | 盘上+串口双份证据;`mg` 最大喂狗间隔、`cf` 控制台发送故障计数 | +| `[ERR] ASSERT FAILED : (resetting)` | `configASSERT` | 断言失败,打印后软复位 | +| `[ERR] HARDFAULT / MEMFAULT / BUSFAULT / USAGEFAULT PC=… CFSR=… [BFAR=] [MMFAR=]` | 4 个 Fault | 崩溃现场 + 故障名归因 | +| `[ERR] STACK OVERFLOW task=` / `[ERR] MALLOC FAILED …` | 内核钩子 | 栈溢出(复位)/ 堆耗尽(仅告警) | -> 四个 Fault 报告都由 `hard_fault_report(tag, frame)` 输出,格式统一为 -> `[ERR] PC=… LR=… xPSR=… R0..R3=… CFSR=… HFSR=…`,后接 -> `BFAR` / `MMFAR`(仅当 `CFSR` 对应 valid 位置 1)。**输出后 `HAL_Delay(200)` -> 排空 UART 再 `NVIC_SystemReset()`**,因此 `@test` 采集到的日志会以 -> 一次干净的复位收尾(复位原因应为 `software`,而非 `IWDG`)。 -> -> ⚠️ **与 `@test` 禁项的关系**:`run_boot_test.py` 的 -> `FORBIDDEN_DEFAULT = "FAIL;ERROR;HardFault;[ERR]"` **已含 `[ERR]`**, -> 因此**任何一条崩溃日志都会让 `@test` 判 FAIL** —— 这是刻意的: -> 出现 Fault 即代表本次运行不算通过。「4 个 Fault 打各自的名字」的意义在于 -> **归因**:日志里直接看到 `MEMFAULT` / `BUSFAULT` / `USAGEFAULT` 就知道是 -> 哪类故障,而不必再从笼统的 `HARDFAULT` 里反推(后者还需人工读 `CFSR` 子域)。 +> **与 `@test` 禁项的关系**:`run_boot_test.py` 的 Forbidden 含 `[ERR]` → **任何崩溃日志都会让本次运行判 FAIL**(刻意)。四个 Fault 打各自名字的意义在于**归因**,不必再从笼统 `HARDFAULT` 反推 `CFSR` 子域。 +> 日志标签:`[APP]` = `app_main.c` 的 `DBG_TAG`;`[WD]` = `task_watchdog.c` 的 `DBG_TAG`。全部纯 ASCII(规避 ARMCC CP936 字符串陷阱)。 -> 日志标签说明:`[APP]` 是 `App/app/app_main.c` 的 `DBG_TAG`;`[WD]` 是 -> `App/util/task_watchdog.c` 的 `DBG_TAG`(定义在两个 include 之间, -> 是 `dbg_log.h` 的既有取标签方式)。全部为纯 ASCII,规避 `AGENTS.md` 记载的 -> ARMCC CP936 字符串字面量陷阱。 +### 6.2 判读顺序(现场"设备反复重启") -### 6.2 与 `run.log` 的集成 - -`sys_log_mcu.c` 的 `syslog_mcu_mounted()` 当前只写 `power on` / `reboot` -(靠 `run.log` 末条 epoch 时间差推断)。**现状:未改**——本次实施选择 -"先只在串口上报 `reset cause`",理由: - -1. 串口标记已经能支撑 `@test` 判定与人工归因,`run.log` 的价值在于**长期累计**; -2. 写 `run.log` 涉及 UI 字符集("看门狗"三字是否在位)与既有 600 s 时间窗分类逻辑, - 改动面比串口上报大,**留作二期**(本模块后续项,暂未登记)。 - -**二期计划**(复位原因映射表,优先级见 §4.8): - -``` -power-on (POR) → "power on" (既有) -IWDG / WWDG → "watchdog" (新增,UI 字符集需补"看门狗"三字) -software → "soft reset" (新增) -external pin → "pin reset" (新增) -其余 → 回退既有 600s 时间窗分类 -``` - -> ⚠️ **UI 字符集影响**:`docs/子系统设计/运行与自检记录设计说明.md` §6.1 记录了 -> UI 中文字符集需按 `font-regen` 流程重生成。新增描述串会引入新汉字 -> (如"看门狗",或改用"装置重启记录"复用既有字),**实施时须跑一次字符集 diff**。 -> **简化方案**:直接复用既有"装置重启记录",把细粒度复位原因只写 `run.log` 与串口, -> 不上 UI——**推荐**(零字符集改动)。 +1. 看 `reset cause:` —— `IWDG` = 看门狗抓到;`software` = 崩溃钩子/断言主动复位;`power-on` = 真的断电重启。 +2. `IWDG` 时看 `PREVRUN seg=` —— 指向 defaultTask 主循环阻塞点;再看 `TICKRUN duw=` 判断 HAL 时基是否同时冻结。 +3. 看 `MISS start` / `task stall` 的 `mask` 缺席位 —— 定位是哪个任务先停摆。 +4. 串口被僵尸通道吞掉时读 `/log/wdev.log`(`STALL` 行的盘上副本)。 --- -## 7. 验证计划 +## 7. 验证记录 -### 7.1 真机验证(`@test` 闭环) - -| # | 场景 | 注入方式 | 期望 | +| # | 场景 | 结果 | 证据 / 备注 | |---|---|---|---| -| V1 | 正常启动 | 常规烧录 | 串口 `[WD] watchdog armed`;运行 10 min 不复位 | -| V2 | **任务饿死** | 临时在 `netTask` 循环插 `while(1);` | 800 ms 后串口 `[ERR] WATCHDOG: task stall mask=0x...`;≤2.5 s 复位;重启后 `[WD] reset cause=watchdog reset` | -| V3 | **断言失败** | 临时在某处插 `configASSERT(0)` | 串口 `[ERR] ASSERT ...`;立即软复位;重启后 `cause=soft reset` | -| V4 | **关中断死循环** | 临时插 `taskDISABLE_INTERRUPTS(); while(1);` | ≤2.5 s 复位(检验 IWDG 独立于中断);`cause=watchdog reset` | -| V5 | **长期不误杀** | 跑 FTP 大文件传输 + UI 菜单遍历 + 配置保存 + 网络重配 ≥ 30 min | 零复位(重点验证 §4.4 白名单是否完备) | -| V6 | **存储套件共存** | `TEST_SUITE_STORAGE` 构建下跑套件 | 套件运行期间不复位(验证掩码剔除生效) | -| V7 | **上电复位辨识** | 断电再上电 | `cause=power on`(非 watchdog) | -| V8 | **引脚复位辨识** | 按复位键(或 `reset.bat` 无参复位) | `cause=pin reset` | +| V1 | 正常启动、长期不复位 | ✅ 常态成立 | 每次 `@test` 开机日志均含 `[WD] armed:`;2026-10-10 复测开机 524 ms 内完成、无 stall | +| V2 | **任务饿死**(netTask 插 `while(1)`) | ✅ **2026-10-03 闭环** | 800 ms 声明 `task stall: mask=0x00000001 req=0x00000037` → ≤2.5 s IWDG 复位(`reset cause: IWDG`,`RCC_CSR=0x24000000`)。首轮曾不复位,根因是证据落盘自身喂狗 → 加门闩(§5.3) | +| V3 | 断言失败 | 🟡 未单独注入 | 宏与钩子已在用(`configASSERT` 全内核可达);行为经编译与代码路径核对,未做定向注入用例 | +| V4 | **关中断死循环** | ✅ **2026-10-03 闭环** | `__disable_irq()` + `while(1)` → 每 ~4.9 s 稳定复位(20+ 周期,`IWDG`)→ 证实 IWDG 独立于中断 | +| V5 | **长期不误杀**(真实负载 ≥30 min) | ✅ **2026-10-03 闭环** | FTP 512 KB 上/下传 + `e02_probe` 探针 30 min → **120/120 次传输全 PASS、零 IWDG 复位**。遗留观察 44 次 stall(探针场景专属)→ 登记 **N-17** | +| V6 | 测试套件共存 | 🟡 部分 | 2026-10-02 曾出现测试构建 IWDG 复位循环,已按 §3.5 掩码规则修复;未跑完整套件用例矩阵 | +| V7 | 上电复位辨识 | 🟡 未单独注入 | `reset cause:` 每次开机都在打(POR 串已实现),未做专门断电对照 | +| V8 | 引脚复位辨识 | ⏳ 未做 | 未做定向注入(`external pin (NRST)` 分支已实现) | +| U1 | 判定状态机主机侧单测 | ✅ 39/39 | `test/host/watchdog/run_unit_test.py`(L0.5,pre-commit `watchdog-unit-test`) | +| U2 | 门禁接入 | ✅ | 改 `App/util/task_watchdog*.{c,h}` 或 `test/host/watchdog/**` 触发 | +| S1 | 模拟器 `--watchdogtest` | ⏳ **已降级不做** | 位图与判定逻辑已由 U1 覆盖;模拟器不编译本模块 | -> **V5 是本次实施的核心验收项**:误杀比不杀更有害。必须在真实业务负载下 -> 长时间无复位,才算通过。 - -### 7.2 模拟器验证 - -| # | 场景 | 命令 | 期望 | -|---|---|---|---| -| S1 | 心跳位图逻辑 | 新增 `lvgl_sim.exe --watchdogtest` | 模拟某任务停摆 → 断言 `miss_streak` 递增、`required_mask` 判定正确 | - -> 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 保留意义仅剩"在模拟器全链路中联调",优先级可降。 +用例细节见 [`../测试说明/看门狗单元测试说明.md`](../测试说明/看门狗单元测试说明.md);故障注入流程见 [`../测试说明/实机测试实施指南.md`](../测试说明/实机测试实施指南.md) §4。 --- -## 8. 内存与资源预算 +## 8. 资源预算(`.map` 实测,2026-10-10) | 项 | 大小 | 位置 | |---|---|---| -| 心跳位图 + 状态(`s_hb_bits` / 计数 / `s_last_mask` / 节流 tick) | ≈ 20 B | `.bss` | -| `task_watchdog.c` 代码(含 IWDG 寄存器操作,约 20 行) | ≈ 400~600 B | Flash(.text) | -| `vApplicationAssertFailed()` | ≈ 100 B | Flash | -| 复位原因锁存(`s_reset_cause`) | ≈ 4 B | `.bss` | -| **合计** | **≈ 24 B RAM / ≈ 0.7 KB Flash** | — | +| `task_watchdog.o` 代码 | **1098 B**(含内联数据 294 B)+ RO 137 B | Flash | +| `task_watchdog.o` RW + ZI | **44 B + 104 B** | `.data` / `.bss`(主 SRAM) | +| `.noinit` 取证槽(3 × 4 字) | **48 B**(`UNINIT_CCM` 上限 256 B,占 18.8%) | CCM 尾部,复位不清、启动不碰 | +| `iwdg.o` | **28 B**(只剩填 `hiwdg` 结构体) | Flash;`HAL_IWDG_Init` 94 B、`HAL_IWDG_Refresh` 12 B 均被链接器移除 | +| 复位原因锁存 `g_rcc_csr_latched` | 4 B | `.data` | -对比现状 RAM 占用(`RW_IRAM1 ≈ 87 KB / 66%`,见 `docs/内存管理/`), -增量可忽略。**无需重新做 RAM 预算评估**。 +全局水位(`docs/内存管理/RAM使用分析与优化方案.md` §2.1 受管快照):RW_IRAM1 **89,168 / 131,072 B(68.0%)**、RW_CCM 49,184 / 65,280 B(75.3%)、ROM 268,276 B(25.6%)。看门狗模块自身增量(≈1.2 KB Flash / ≈150 B RAM + 48 B CCM)在其中可忽略。 -> ✅ **注意**:因走寄存器路径(§2.2),**不引入** `stm32f4xx_hal_iwdg.c` -> (HAL 封装约 0.3 KB Flash),上表已据此计。相比"补 HAL 驱动"路径省下的这部分 -> Flash 恰与本模块自身代码量相当——**两条路径的 Flash 净增量接近,但寄存器路径 -> 零工程文件改动**,这是选它的实质理由。 +> 与"走裸寄存器不引入 HAL 驱动"的旧估算不同:现走 CubeMX 路径,`stm32f4xx_hal_iwdg.c` **已编译进工程**,但真正被链接的只有 `MX_IWDG_Init` 填结构体那 28 B(§2.4 的宏改名把 94 B 的 `HAL_IWDG_Init` 移除)。 --- -## 9. 风险与开放问题 +## 9. 边界与未办项 -| # | 风险 | 评估 | 处置 | -|---|---|---|---| -| R1 | **启动段(`app_main_init` + bring-up)无保护** | 已知边界;该段死机表现为"无输出 + LED 不闪",可人工辨识 | §4.5 决策 C 的显式代价;后续如需覆盖另立方案 | -| R2 | **LSI 精度差导致超时漂移** | 47 kHz 时超时打七折(2500→1705 ms) | §4.3 已按最坏值校验,并留 2× 裕度 | -| R3 | **`ftpTask` 卡死检测不到** | 第一步剔除掩码 = FTP 死锁不可自恢复 | 记录为已知缺口;第二步改 `Lib/lftpd` 埋点(需评估对第三方库的侵入度) | -| R4 | **`timeSyncTask` 2 s delay 与窗口冲突** | 必然误杀 | §5.4 推荐拆分为 10×200 ms | -| R5 | **NAND 擦写偶发超 800 ms** | 表现为偶发误复位;若发生会极难定位(复位后现场已丢) | 先按 800 ms 实施,台架长跑(V5)观测;若出现则增大容忍次数或把存储操作改为分段置位 | -| R6 | **`RCC_CSR` 标志清不完导致误判** | 忘清 `RMVF` 会使下次软复位读回上次标志 | §4.7 明确"读后即清";V7/V8 验证 | -| R7 | **断言钩子自身崩溃**(内核态已损坏) | 打印可能二次 HardFault | 复用 `hook_ptr_ok()` 的指针校验模式;即使失败仍由 IWDG 兜底 | -| R8 | **`configCHECK_FOR_STACK_OVERFLOW` 钩子复位归为 `soft reset`** | 栈溢出与断言在复位原因上不可区分 | 可接受;`run.log` 的 `selftest.log` 侧已有告警记录 | +### 9.1 已知边界(刻意接受) -### 开放问题 - -| # | 问题 | 待定 | +| # | 边界 | 现状与处置 | |---|---|---| -| O1 | 是否把 `ftpTask` 纳入监视 | 取决于是否愿意改 `Lib/lftpd`(第三方库侵入度评估) | -| O2 | 是否上 UI 显示细粒度复位原因 | 涉及字符集重生成成本,倾向"只写 log/串口" | -| O3 | 启动段(R1)是否需要覆盖 | 待现场故障统计决定;倾向不做(复杂度收益比不划算) | -| O4 | 是否需要复位计数与"连续复位"抑制 | 若某故障导致复位风暴(复位→立刻再死),可考虑"连续 N 次看门狗复位后进入安全模式"。**当前不做**,记录为后续可选增强 | +| R1 | **启动段无保护**(复位 → `task_wd_start()` 约数秒) | 接受;表现为"无输出 + LED 不闪",人工辨识。要覆盖需"短超时 + 手动多喂",复杂度收益比不划算 | +| R2 | **LSI 精度差** | 已按 47 kHz 最坏值校验,停喂到复位仍留 ~900 ms | +| R3 | **喂狗节奏饥饿** | 控制台 drain 曾把主循环拉长导致误复位(2026-10-02 gen-288)。现缓解 = drain 单次 250 ms 预算 + 发送故障 300 ms 冷却;**结构根治(USART1 TX-only)未做** → 登记 **S-3** | +| R4 | **NAND 真实卡死时 FTL 直接喂狗** | 设备存活但存储功能冻结(与 ORE 自愈同类边界)。若要求"NAND 死也必须复位",需给 `nand_ftl_wd_feed()` 加次数/时限上限 | +| R5 | **栈溢出与断言在复位原因上不可区分**(都是 `SFTRSTF`) | 可接受;串口分别打 `[ERR] STACK OVERFLOW` / `[ERR] ASSERT FAILED`,`selftest.log` 侧另有记录 | +| R6 | **`RCC_CSR` 读后即清** | 已在 `main.c` 统一处理;漏清会让下次软复位读回上次标志 | + +### 9.2 未办项(登记簿同步) + +| 编号 | 内容 | 与本案关系 | +|---|---|---| +| **N-17** | `time_sync` 30 s 挂起客户端掉线 + 监听重建期间 NET/FTP/UI 心跳缺口 800 ms~1 s(声明 stall 但未复位),**余量仅 ~0.7 s** | 待定位监听重建为何阻塞其它任务;必要时缩短重试 | +| **S-3** | USART1 TX-only 结构根治(灭 ORE 僵尸机制) | 是 R3 节奏饥饿的机制根源 | +| **N-4 / C1** | 网络 pending 二期时"看门狗接 `selftest` 留痕" | 二期一并做 | +| O1 | 是否需要复位计数与"连续 N 次看门狗复位后进入安全模式" | 当前不做;若现场出现复位风暴再评估 | +| O2 | 细粒度复位原因是否上 `run.log`/UI | 倾向不上(避免中文字符集重生成),只保留串口 + `selftest.log` | --- -## 10. 实施步骤(按序执行,每步端到端验证) +## 10. CubeMX 重生成下的定制存活(方法论) -| 步 | 内容 | 验证 | 状态 | +看门狗是本项目**第一次**把 CubeMX 生成流程引入可靠性关键路径,由此沉淀出与看门狗本身无关的通用方法论。`AGENTS.md`「平台约束」指向本节,改动 `.ioc`/`Src/`/`Inc/` 前请先读。 + +### 10.1 覆盖史(三次重生成) + +| 次 | 触发 | 被覆盖的定制 | 结论 | |---|---|---|---| -| **0** | **(新增)** CubeMX 勾选 IWDG(Prescaler=64 / Reload=1249)→ 重生成 → **恢复被覆盖的定制** | `@build` 0/0 + 启动日志齐全 | ✅ **2026-09-29 完成**,覆盖 4 处见下 | -| **1** | 新建 `App/util/task_watchdog.c/h`;实现心跳位图 + 抽样判定 + IWDG 延后启动;**登记进 MDK 工程**(`App` 组,`sys_clock.c` 之后) | `@build` 0/0 | ✅ 完成 | -| **2** | `Src/main.c` 复位原因锁存 + 清标志;`App/app/app_main.c` 打印描述串 | `[APP] reset cause:` 出现 | ✅ 完成(`run.log` 写入留二期) | -| **3** | `Src/iwdg.c` 阻止 `HAL_IWDG_Init()` 自动启动(P0) | 开机不死循环 | ✅ 完成 | -| **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` | ✅ **2026-10-03 台架**(V2/V4 均触发 IWDG 复位) | -| **8** | 模拟器 `--watchdogtest` + stub | S1 | ⏳ 待做(位图逻辑已由 U1 覆盖,优先级降低) | -| **9** | 长跑(V5,≥30 min,真实负载) | **零复位** | ✅ **2026-10-03 台架**(30 min / 120×FTP 零复位) | -| **10** | 本文档改写为"现状设计说明";`待处理内容.md` N-03 销项;`README.md` 登记 | 门禁 | ✅ N-03 已销项(第 7~9 步结论已入本文变更记录) | +| 1(2026-09-29) | 勾选 IWDG | 4 处:`Src/iwdg.c` 开机死循环(P0)、`HardFault_Handler` 采集被打回空(P1)、`FreeRTOSConfig.h` 两个加固宏整块删除(P1)、`freertos.c` 栈注释(P2) | 覆盖**有限且可枚举**,"重生成 → 逐项恢复 → 编译验证"可固化 | +| 2(2026-09-29) | 再次重生成 | 审计脚本检出 5 处;**关键发现**:CubeMX 换了一代中断函数模板(旧自造区 `HardFault_Handler`/`HardFault_Report` → 函数体内 `HardFault_IRQn 0`/`W1_HardFault_IRQn 0`),**生成区之外的自造标记区连同内容一起消失** | 🔴 **只信 CubeMX 当前模板真实生成的区名**,别凭记忆;优先用 `USER CODE BEGIN 0` 这类从未变过的标准位 | +| 3(2026-10-09) | 启用 ADC1 | 同一批定制再次全被抹;`FREE_RTOS_CONFIG_HARDENING` 自造区**第二次**消失;`freertos.c` 三处栈注释第四次丢失 | 加固宏改走 **L1**(`.ioc` 的 `FREERTOS.configCHECK_FOR_STACK_OVERFLOW=2` / `configUSE_MALLOC_FAILED_HOOK=1`,GUI 在 FreeRTOS → **Config parameters** → **Hook function related definitions**);启用后 CubeMX 生成 `USER CODE BEGIN 4/5` **空桩** → 钩子实现必须整体搬进 4/5 区(否则同名两处定义 = 编译错),helper `hook_ptr_ok()` 放 `PD` 区 | -### 10.1 第 0 步实测:CubeMX 重生成覆盖了哪 4 处 +> ⚠ **别凭印象断言"界面里没有该项"**——先翻标签页或查 `db/mcu/IP/*_Modes.xml`:那两个加固宏一直躺在 "Config parameters" 里,因此白丢了两次。 -`.ioc` 的 `ProjectManager.DeletePrevious=true` + `KeepUserCode=true` 决定了 -**只有 `USER CODE BEGIN/END` 标记区内的代码受保护**。实测被覆盖的定制: - -| 级别 | 文件 | 被覆盖内容 | 恢复方式 | -|---|---|---|---| -| **P0** | `Src/iwdg.c`(新文件) | CubeMX 生成的 `MX_IWDG_Init()` 直接调 `HAL_IWDG_Init()` → 开机死循环 | 改为只填参数(**此改动在生成区,无 USER CODE 位可用**),插 `IWDG-DEFER-START-ANCHOR` 锚点注释供审计检出 | -| **P1** | `Src/stm32f4xx_it.c` | `HardFault_Handler` 崩溃现场采集(PC/LR/xPSR/R0-R3/CFSR/HFSR/BFAR/MMFAR)被打回空 `while(1)` | 整函数替换为 `__asm` 版;生成实体用 `#define` 改名保留(链接器自动移除);**报告实现迁入 `USER CODE BEGIN 0`** | -| **P1** | `Inc/FreeRTOSConfig.h` | `configCHECK_FOR_STACK_OVERFLOW 2` + `configUSE_MALLOC_FAILED_HOOK 1` **整个块被删除** | 插回 `USER CODE BEGIN FREE_RTOS_CONFIG_HARDENING` 块 | -| **P2** | `Src/freertos.c` | 两处中文注释丢失(任务栈大小的实测记录) | 恢复注释(栈**数值**未丢,`.ioc` 同步到位) | - -> ⚠️ **P1 两处的教训**:它们都在 `USER CODE` 标记区**之外**(`stm32f4xx_it.c` 的 -> `HardFault_Handler` 由 CubeMX 直接生成;`FreeRTOSConfig.h` 的加固宏原本写在 -> 非 `USER CODE` 区)。**P1 的丢失不会立刻暴露**——栈溢出检测与堆失败钩子只在 -> 真发生溢出/耗尽时才起作用,且 `HardFault_Handler` 被打回空死循环后 -> **崩溃时连现场都打不出来**(只剩 IWDG 兜底复位,但归因信息全丢)。 -> 故本次已把这三个宏搬进 `USER CODE BEGIN FREE_RTOS_CONFIG_HARDENING` 区。 -> -> **2026-10-09 第三次重生成(启用 ADC1):同一批定制再次全被抹,并暴露两个新事实** -> -> 1. **"自造区不可信"再次成立**:`FREE_RTOS_CONFIG_HARDENING` 连标记带内容**第二次**消失。已按 §10.2 改走 **L1**——`.ioc` 的 `FREERTOS.configCHECK_FOR_STACK_OVERFLOW=2` / `FREERTOS.configUSE_MALLOC_FAILED_HOOK=1`(GUI 里就能设,见上),自造区删除,审计两项也从"校验自造区"升级为"校验 `.ioc`"。 -> 2. **新坑**:在 GUI 里启用这两个钩子后,CubeMX 会在 `Src/freertos.c` 生成 `USER CODE BEGIN 4 / 5` 区(默认内容是**空桩**),与 `Application` 区的真实实现**重复定义 → 编译错**(这是本项目唯一一次重生成后直接编译不过)。**最终落点:整个钩子函数写进 4 / 5 区**(见 §10.2),`Application` 区的旧实现搬走;配套 helper `hook_ptr_ok()` 须在钩子之前 → 放 `USER CODE BEGIN PD` 区。 -> 3. `Src/iwdg.c` 与 4 个 Fault Handler(L3)**第三次**被抹,锚点全部检出并按 HEAD 复原;`Src/freertos.c` 三处栈注释再次丢失(栈**值**未丢)→ 第四次重生成又丢一次,已**集中成一段"任务栈核算依据"放进 `USER CODE BEGIN Variables` 区**(L2,重生成不丢);各 `.stack_size` 旁不再留注释——那里在生成区之外,补了也留不住。 -> 4. 审计清单 18 → **21 项**(新增 `adc1-hal-enabled` 与钩子落点三项 `stack-overflow-hook-impl`(区 4) / `malloc-failed-hook-impl`(区 5) / `stack-overflow-hook-helper`(区 PD),两项加固检查升级为 `.ioc` 校验),现 21 通过 / 1 警告 / 0 失败。 - -> **每步改完必须 `@build` 0 错误 0 警告**(`AGENTS.md` 铁律)。 - -#### ⚠️ 第 0 步复现(2026-09-29 第二次重生成):审计脚本有效,但机制假设需修正 - -第一次重生成后我们建立了审计脚本并"加固"了恢复方案,随后**用户又重生成了一次**, -据此验证机制: - -- ✅ **审计脚本准确检出全部 5 处覆盖**(16 项清单中 5 项 FAIL)——L3 兜底手段有效 -- ✅ **`Src/main.c` 的复位原因锁存完好**(在 `USER CODE BEGIN Init` 区)——L2 有效性的**正向证据** -- ✅ `MDK-ARM/*.uvprojx` 中 `task_watchdog.c` 挂载保住 - -但暴露出 **L2 的一个致命前提错误**: - -> **🔴 关键发现:CubeMX 会"换一代"中断函数模板 —— -> "生成区之外的自造标记区"同样不被保留。** -> -> - **旧模板**:在 `HardFault_Handler` 函数**前后**生成自造区 -> `/* USER CODE BEGIN HardFault_Handler */` 与 `/* USER CODE BEGIN HardFault_Report */` -> → 上一轮我们把手写实现放进这两个区,**当时确实生效了**。 -> - **新模板**:改成函数体**内部**的 -> `/* USER CODE BEGIN HardFault_IRQn 0 */` 与 `/* USER CODE BEGIN W1_HardFault_IRQn 0 */` -> → 旧的 `HardFault_Handler` / `HardFault_Report` 区**整段消失**,放在里面的代码一并丢失。 -> -> **教训**:`USER CODE` 区有效的前提是"**该区由 CubeMX 生成且名字仍被当前模板使用**"。 -> CubeMX 换模板(或改区名)时,**依赖旧区名的内容会连同区一起消失**, -> 而它看起来仍像"我们遵守了规则"。**不要假设自造区名的稳定性——只信 CubeMX 当前 -> 模板真实生成的区名**(用一次重生成后的文件核对,而非记忆)。 - -据此对 `stm32f4xx_it.c` 的恢复方案做了**第二次升级**: - -| 组件 | 上一轮落点 | **本轮最终落点** | 稳定性 | -|---|---|---|---| -| 崩溃报告实现(`hard_fault_report` 等) | 自造区 `HardFault_Report` | **`USER CODE BEGIN 0`**(文件头部,CubeMX 标准保护位,名字从未变过) | ⭐⭐ L2 真有效 | -| `HardFault_Handler` 实体 | 自造区 `HardFault_Handler` | 2026-10-09 起:`__asm` 实现放 **`USER CODE BEGIN 0` 保护区**(必须在 4 个 `#define` **之前**);生成实体经 `#define HardFault_Handler HardFault_Handler_Gen` 改名让位,链接器自动移除(`.map` 实测 `Removing ... (2 bytes)`),生成区保持 CubeMX 原样 | ⭐ **L2**(重生成不丢) | - -> **为什么 Handler 只能放生成区**:CubeMX 本代模板把整个函数体(含 `HardFault_IRQn 0` / -> `W1_HardFault_IRQn 0`)连同函数一起重写;函数体之外的自造区不保留。查询 `.map` -> 确认向量表指向我们的 `__asm` 版(`0x080001a1`,`.emb_text`,18 字节), -> 生成版被链接器丢弃 —— **零代码开销,且未删除生成区任何一行**。 - -> **ARMCC 语法坑(本轮踩过)**:`__asm volatile ("MRS %0, lr" : "=r" (x))` 是 GCC 内联 -> 汇编语法,ARMCC(MDK)报 `#18: expected a ")"`;`__asm { MRS x, lr }` 块内联会把 -> `lr` 当 C 标识符报 `#20: identifier "lr" is undefined`。**唯一可行写法**是 -> 整个函数用 `__asm void f(void) { ... }`(旧代码即此写法,已验证可行)。 - -### 10.2 让定制"重生成不丢"的三层手段(**通用方法论**) - -第 0 步的 4 处覆盖暴露了一个**与看门狗无关的通用问题**:CubeMX 重生成会覆盖 -标记区之外的一切手工改动。解决办法按可靠性分三层,优先级从高到低: +### 10.2 三层手段(优先级从高到低) | 层 | 手段 | 适用 | 可靠性 | |---|---|---|---| -| **L1 写进 `.ioc`** | 让 CubeMX **自己生成**这份定制 | 凡是 CubeMX 界面能表达的配置 | ⭐⭐⭐ **根治**——配置成了事实源,重生成只会照着写 | -| **L2 搬进 `USER CODE` 区** | 把手工代码移到 `/* USER CODE BEGIN X */` 与 `/* USER CODE END X */` 之间 | 有现成标记区的文件(`Src/freertos.c` / `mpu_wrappers` 等),或 CubeMX 为该函数生成了标记区 | ⭐⭐ **有效**——`KeepUserCode=true` 时 CubeMX 逐字保留 | -| **L3 审计 + 基线** | 无法 L1/L2 的存量,用脚本定期核对 | 标记区外的存量(如 4 个 Fault Handler 实体) | ⭐ **兜底**——不防丢失,但**丢了一定知道** | +| **L1 写进 `.ioc`** | 让 CubeMX 自己生成 | 界面能表达的配置 | ⭐⭐⭐ 根治——重生成只会照写 | +| **L2 搬进 `USER CODE` 区** | 移到 `USER CODE BEGIN/END` 之间 | 有真实标记区(**当前模板生成的**)的文件 | ⭐⭐ 有效(`KeepUserCode=true` 时逐字保留) | +| **L3 审计 + 基线** | 脚本定期核对 | 标记区外的存量 | ⭐ 兜底——不防丢,但丢了一定知道 | -#### L1:哪些定制能写进 `.ioc` +**本项目已落 L1 的项**:任务栈/优先级/名称(`FREERTOS.Tasks01`,**改栈请改 `.ioc`,不要改 `freertos.c`**)、队列 `netMsgQueue`、事件组 `sys_evt`、堆 `configTOTAL_HEAP_SIZE`、FPU、两个加固宏、**IWDG 分频/重载**、时钟树(含 LSI)、`KeepUserCode=true`(必须为 true,否则 L2 整体失效)。 -本项目 `.ioc` 已持有(重生成不会丢): +**生成区内那句"必须去掉"的调用 → 用宏改名升级为 L2**(2026-10-09 定,本案两处): -| 定制 | `.ioc` 键 | 说明 | -|---|---|---| -| **任务栈大小** | `FREERTOS.Tasks01=...defaultTask,24,1024,...` | ✅ **最值得强调**:栈值写在 `.ioc` 里,CubeMX 直接生成到 `Src/freertos.c` 的 `.stack_size`。曾担心"重生成会覆盖栈大小",实测**保住了**(`.ioc` 已同步)。**改栈请改 `.ioc`,不要改 `freertos.c`** | -| 任务优先级 / 名称 / 入口函数 | 同上 | ✅ 同一条目 | -| 消息队列 `netMsgQueue` | `FREERTOS.Queues01` | ✅ | -| 事件组 `sys_evt` | `FREERTOS.Events01` | ✅ | -| FreeRTOS 堆大小 | `FREERTOS.configTOTAL_HEAP_SIZE=49152` | ✅ | -| FPU 使能 | `FREERTOS.configENABLE_FPU=1` | ✅ | -| **栈溢出检测(模式 2)** | `FREERTOS.configCHECK_FOR_STACK_OVERFLOW=2` | ✅ **2026-10-09 迁入 L1**(原先写在 `FreeRTOSConfig.h` 的自造区里,**两次重生成两次被抹**)。GUI 位置:FreeRTOS 页 → **Config parameters** 标签 → **Hook function related definitions** 组 | -| **堆分配失败钩子** | `FREERTOS.configUSE_MALLOC_FAILED_HOOK=1` | ✅ 同上(同一组,可选 0/1) | -| IWDG 分频 / 重载 | `IWDG.IPParameters` + `IWDG.Prescaler` / `IWDG.Reload` | ✅ 本次新增,见 §2.2 | -| 时钟树(含 LSI) | `RCC.*` | ✅ | -| `KeepUserCode=true` | `ProjectManager.KeepUserCode` | ✅ **这个必须为 true**,否则 L2 整体失效 | +- `Src/iwdg.c`:`USER CODE BEGIN 0` 放空实现 + `#define HAL_IWDG_Init(a_h) iwdg_deferred_start(a_h)` → 生成区原样、**重生成零 diff**(§2.4)。 +- `Src/stm32f4xx_it.c`:4 个 `__asm void X_Handler` 放 `USER CODE BEGIN 0`,**必须写在 4 个 `#define X X_Gen` 之前**(写在之后会被自己的宏改名);生成实体经宏改名让位,链接器自动移除(`.map` 各 `Removing … (2 bytes)`),向量表仍指向我们的 `__asm` 版。 -**能写进 `.ioc` 但本项目尚未写的**(可作为后续加固项): +> **L3 清单已清空**:所有定制要么在 `.ioc`(L1)、要么在 `USER CODE` 区(L2)——**重生成应当零 diff**。剩余 1 项 WARN 是 `Src/freertos.c` 的 `timeSyncTask` 手工定义(属性块在 `USER CODE BEGIN Variables` 区,本身受保护,WARN 只提示"它不在 `.ioc` 里")。 -| 定制 | 现状 | 建议 | -|---|---|---| -| `configASSERT` 定义 | 在 `USER CODE BEGIN 1` 区(L2) | CubeMX 无对应界面项,保持 L2 + 审计 | -| 4 个 Fault Handler 实体 | `stm32f4xx_it.c` 标记区**外**(L3) | ⚠️ CubeMX 为这些函数**不生成** `USER CODE` 区,无法 L2。见下方"存量清单" | -| `fault_handler_enable()`(SHCSR 使能三位) | `Src/stm32f4xx_it.c` 的 `USER CODE BEGIN 1` 区(L2) | ❌ **CubeMX 无此配置项**(已查模板源码证实,见下) | +**为什么 `fault_handler_enable()`(SHCSR 使能三位)写不进 `.ioc`**(2026-10-01 查模板源码三重证据):① `tpl_main_c.ftl` 的 `MX_NVIC_Init()` 只在 `systemHandler=="false"` 时生成 `HAL_NVIC_EnableIRQ`,而三个 Fault 在 `NVIC-STM32F417_Modes.xml` 都标 `Y`;② 全部模板中 `SHCSR` 只出现 1 次且是 MPU 清理的 `&= ~MEMFAULTENA`(禁用),无任何 `|=`;③ `.ioc` 无 MPU,且 `HAL_MPU_Enable` 只置 `MEMFAULTENA` 一位。⚠ `.ioc` 的 `NVIC.MemoryManagement_IRQn=true` **不代表已使能**(只是向量可见)。现走 L2(`USER CODE BEGIN 1`)+ 审计项兜底。 -> ✅ **2026-10-09 更正**:本节原先写"CubeMX 的 FreeRTOS 界面**没有** `configCHECK_FOR_STACK_OVERFLOW` / `configUSE_MALLOC_FAILED_HOOK` 这一项的勾选框"——**判断有误,该项一直在 GUI 里**:FreeRTOS 页 → **Config parameters** 标签 → **Hook function related definitions** 组(DB 证据:`db/mcu/IP/FREERTOS-*-v*_Cube_Modes.xml` 里两个 `RefParameter`,可选值 0/1/2 与 0/1)。现已迁入 `.ioc`(见上一表),`Inc/FreeRTOSConfig.h` 里自造的 `FREE_RTOS_CONFIG_HARDENING` 区**随之删除**——它正是"自造区名不可信"的又一例(两次重生成两次被抹,2026-09-29 与 2026-10-09 各一次)。 -> -> ⚠️ **连带坑与最终落点(2026-10-09 实测)**:一旦在 GUI 里启用这两个钩子,CubeMX 会在 `Src/freertos.c` 生成 `USER CODE BEGIN 4 / 5` 区,默认内容是**空桩**——若实现仍留在 `USER CODE Application` 区,就出现**同名函数两处定义 → 编译错**。 -> **正确做法:整个钩子函数(含签名)写进 4 / 5 区**,`Application` 区里的旧实现删掉。这两个区名由 CubeMX 当前模板生成,且**其存在由 `.ioc` 启用钩子保证**(L1 兜住 L2),比"每次重生成后删桩"更省事也更不易漏。签名与生成原型保持一致(`xTaskHandle` → `TaskHandle_t`,即 `void *`)。配套 helper `hook_ptr_ok()` 必须在钩子之前定义 → 放 `USER CODE BEGIN PD` 区。审计项 `stack-overflow-hook-impl`(区 4)/ `malloc-failed-hook-impl`(区 5)/ `stack-overflow-hook-helper`(区 PD)分别校验。 - -##### 为什么 `fault_handler_enable()` 写不进 `.ioc`(2026-10-01 查模板源码证实) - -用户问"能否通过 CubeMX 配置",逐条查证 CubeMX 安装目录 -`db/templates/*.ftl` 与 `db/mcu/IP/NVIC-STM32F417_Modes.xml` -(F407 复用 F417 定义),**三条可能路径全部否定**: - -| 路径 | 查证结果 | -|---|---| -| ① NVIC 面板勾选三个 Fault 的 `Enabled` | `tpl_main_c.ftl` 的 `MX_NVIC_Init()`:`[#if vector.systemHandler=="false"]` **才**生成 `HAL_NVIC_EnableIRQ(...)`。而 `NVIC-STM32F417_Modes.xml` 中 `MemoryManagement/BusFault/UsageFault_IRQn` 都标 `Y`(= `systemHandler`)→ **永不生成使能代码**(`HardFault_IRQn` 标 `N` 是个例外,但它恒使能、无需使能) | -| ② 任何模板里找 `SHCSR` 置位 | 全部模板中 `SHCSR` **只出现 1 次**(`tpl_main_c.ftl:2269`),且是 MPU 清理时的 `SCB->SHCSR &= ~SCB_SHCSR_MEMFAULTENA_Msk`(**禁用**)。**无任何 `\|=` 置位** | -| ③ 启用 MPU(`HAL_MPU_Enable` 内部会置 `MEMFAULTENA`) | `.ioc` 的 `Mcu.IP0..13` 列表**无 MPU**;且该函数**只置 `MEMFAULTENA` 一位**,`BUSFAULTENA`/`USGFAULTENA` 仍拿不到 | - -**实测印证**:`Src/stm32f4xx_hal_msp.c` 的 `HAL_MspInit()` 里只有 -`HAL_NVIC_SetPriority(PendSV_IRQn, 15, 0)`(因它优先级 ≠ 0 才生成), -三个 Fault **一行代码都没生成**——与模板逻辑完全吻合。 - -> **结论**:`fault_handler_enable()` **只能手工写**。但它是 **L2**(放在 -> `USER CODE BEGIN 1` 区)——**重生成不会丢**,所以这条不是风险,只是 -> "无法享受 L1 的'CubeMX 自己生成'待遇"。**已挂审计项 `fault-handler-enable` 兜底。** - -> ⚠️ 顺带提醒:`.ioc` 里那三行 `NVIC.MemoryManagement_IRQn=true\:0\:0\:false\:...` -> **不代表"已使能"**——第 1 段 `true` 只是"该向量在 NVIC 表里可见/已配置优先级", -> 与 `SHCSR` 使能位无关。**不要误以为勾了 CubeMX 的 Enabled 就等于使能了故障。** - -> **判断口诀**:能在外设/中间件的图形界面里找到对应输入框的 → 一定写 `.ioc`; -> 找不到(如 `configASSERT` 定义、故障异常处理函数体、`SHCSR` 使能位)→ 只能 L2/L3。 -> ⚠️ 但**别凭印象断言"界面里没有"**——先翻一遍各标签页或查 `db/mcu/IP/*_Modes.xml`: -> FreeRTOS 那两个加固宏就一直躺在 "Config parameters" 标签页里,因此白丢了两次。 - -#### L2:`USER CODE` 区的可用位置(本项目实测) - -| 文件 | 可用标记区 | 已用于 | -|---|---|---| -| `Inc/FreeRTOSConfig.h` | `Header` / `Includes` / `MESSAGE_BUFFER_LENGTH_TYPE` / `1` / `Defines` | `configASSERT`(`1`)。加固宏已迁 L1(写 `.ioc`、由模板生成),不再占标记区 | -| `Src/freertos.c` | 共 25 个,含 `Includes` / `PD` / `PTD` / `Variables` / `4` / `5` / `Application` / … | 栈溢出钩子(`4`)、malloc 失败钩子(`5`)、helper `hook_ptr_ok`(`PD`)、**任务栈核算依据 + `timeSyncTask` 属性定义(`Variables`)**、断言钩子及其余应用代码(`Application`) | -| `Src/stm32f4xx_it.c` | 共 52 个,含文件头 `0`(标准保护位)+ 各中断函数**内部**的 `_IRQn 0` / `W1__IRQn 0` | 崩溃报告实现 + **4 个 `__asm` Handler** + 4 个 `#define X X_Gen`(均在 `0`,**Handler 必须写在 `#define` 之前**)、`fault_handler_enable()`(`1`) | -| `Src/main.c` | 共 19 个,含 `Init` / `SysInit` / `2` / `PV` / `PFP` | 复位原因锁存(`Init`) | -| `Src/iwdg.c` | 共 7 个,含 `IWDG_Init 0/1/2` | ❌ **本次改动用不上**(见下方特例) | -| `Inc/main.h` | 共 7 个 | 引脚宏等 | - -> 🔴 **两条硬教训(2026-09-29 实测,务必先读)** -> -> **① 不要依赖"自造标记区"**。上一轮我们按"CubeMX 保留任何形态的 `USER CODE` 注释对" -> 的结论,在 `stm32f4xx_it.c` 里自造了 `HardFault_Handler` / `HardFault_Report` 区, -> 当时**确实生效**。但第二次重生成时 CubeMX **换了中断函数模板**,区名从 -> `HardFault_Handler` 变成函数体内的 `HardFault_IRQn 0` / `W1_HardFault_IRQn 0`, -> **旧区连同内容一起消失**。 -> → **只信 CubeMX 当前模板真实生成的区名**,用重生成后的文件核对,别凭记忆。 -> → **宁可用文件头 `USER CODE BEGIN 0` 这类"区名从未变过"的标准位**。 -> -> **② 生成区里那句"必须去掉"的调用,可用宏改名把它升级成 L2(2026-10-09)**。 -> `Src/iwdg.c` 要处理的是 `hiwdg.Init.*` 赋值与 `HAL_IWDG_Init()` 调用之间那一句,而 -> CubeMX 只在调用**之前**(`IWDG_Init 0/1`)和**之后**(`IWDG_Init 2`)生成标记区—— -> **正中间没有**;塞进 `IWDG_Init 1` 区也阻止不了区外那行被覆盖(HAL 调用在区外)。 -> 此前只能"每次重生成后手工删那一句"(L3)。现在的做法:在文件头 `USER CODE BEGIN 0` -> (标准保护位)放一个空实现 + `#define HAL_IWDG_Init(a_h) iwdg_deferred_start(a_h)`, -> 把**本编译单元里**那句生成调用改名到空实现 → 生成区保持原样、重生成**零 diff**, -> 不必再手工删。`.map` 证实 `Removing stm32f4xx_hal_iwdg.o(i.HAL_IWDG_Init), (94 bytes)` -> ——真 HAL 初始化根本没被链接进来,`MX_IWDG_Init` 只剩填结构体那几条指令。 -> ⚠ **不能走"反初始化"**:IWDG 一旦启动就**无法停止**(唯一手段是复位),所以只能在 -> 启动**之前**拦截,不能在 `IWDG_Init 2` 区里"撤销"。 -> 同一技巧也用于 4 个 Fault Handler(`#define HardFault_Handler HardFault_Handler_Gen` -> 把**生成实体**改名,我们的实现放 `USER CODE BEGIN 0`)。区别:Handler 是"定义"而非 -> "调用",所以我们的实现必须写在 `#define` **之前**,否则会被自己的宏改写。 - -#### L3:存量清单(标记区外,只能靠人记 + 脚本盯) - -| 文件 | 内容 | 为什么无法 L1/L2 | -|---|---|---| -| `Src/freertos.c` | `.stack_size` 行内的中文注释 | 该行由 CubeMX 按 `.ioc` 生成,"值"受保护、"注释"不受。**注释是锦上添花**,丢失不影响功能(值在 `.ioc` 里);核算依据已集中到 `USER CODE BEGIN Variables` 区(L2) | - -> **结论**:**L3 清单已清空(0 项)**——`Src/iwdg.c`(宏改名让生成调用变空操作)与 -> 4 个 Fault Handler 实体(`__asm` 实现搬进 `USER CODE BEGIN 0`)已于 2026-10-09 -> 先后升级为 L2(见上方硬教训②)。所有定制现在要么在 `.ioc`(L1),要么在 -> `USER CODE` 区(L2)——**CubeMX 重生成应当零 diff**(只剩 `.ioc` 里没表达、 -> 且落在生成区的注释类内容,见上表)。 -> 剩余 1 项 WARN 是 `Src/freertos.c` 的 `timeSyncTask` 手工定义(属性块在 -> `USER CODE BEGIN Variables` 区,本身受保护;WARN 只是提示"它不在 `.ioc` 里")。 - -#### 四个 Fault 异常的采集(2026-09-29 完成) - -`HardFault` 之外,`MemManage` / `BusFault` / `UsageFault` 原先都是**空 `while(1)`** -(死机静默)。现已全部升级为崩溃采集: - -| Fault | 触发条件 | 命中时的地址寄存器 | 使能位 | -|---|---|---|---| -| `HardFault` | 其他 Fault 未使能时的兜底 / 升级 | — | 恒使能 | -| `MemManage` | MPU 禁止区访问、非法特权级访问 | `MMFAR`(`CFSR.MMARVALID` bit7) | `SHCSR.MEMFAULTENA` bit16 | -| `BusFault` | 非法总线访问/取指(时钟未开、野指针、DMA 越界) | `BFAR`(`CFSR.BFARVALID` bit15) | `SHCSR.BUSFAULTENA` bit17 | -| `UsageFault` | 未定义指令、非法状态(本项目不启用非对齐/除零陷入) | —(看 `CFSR` 子域) | `SHCSR.USGFAULTENA` bit18 | - -**关键**:复位后 `SHCSR` **只开了 `HardFault`**,后三者默认禁用——不使能的话, -它们触发时会被**升级成 `HardFault`**,日志只显示笼统的 `HARDFAULT`、看不出根因。 -故新增 `fault_handler_enable()`(`Src/stm32f4xx_it.c` 的 `USER CODE BEGIN 1` 区, -声明在 `Inc/stm32f4xx_it.h` 的 `USER CODE BEGIN EFP` 区),在 -`app_main_init()` **第一行**调用(纯寄存器写,不依赖日志/时钟)。 - -> ⚠️ **这三位没法在 CubeMX 里配**(2026-10-01 查模板源码证实,详见 §10.2 -> 「为什么 `fault_handler_enable()` 写不进 `.ioc`」)。**不要以为在 NVIC 面板 -> 勾了 `Enabled` 就使能了故障**——CubeMX 对 System Exception 只生成优先级 -> (且仅当优先级 ≠ 0),**从不生成使能代码**。好在函数体在 `USER CODE BEGIN 1` -> 区,重生成不会丢(L2),另有审计项 `fault-handler-enable` 兜底。 - -**刻意不打开的两项**(都会把普通 bug 升级成死机,对本项目弊大于利): - -- `CCR.UNALIGN_TRP`:非对齐访问陷入。Cortex-M4 硬件本可处理非对齐访问, - 打开后协议解析里常见的非对齐取字段会直接死机 -- `CCR.DIV_0_TRP`:除零陷入。除零应由业务代码自己判,不该靠硬件兜 - -**实现要点(ARMCC 汇编限制)**:`__asm` 块**取不到 C 的 static 数据符号** -(实测 `ADR r1, xxx_tag` 报 `A1516E: Bad symbol`,去掉 `static` 也无效), -且 `__asm` 里无法给 C 函数传字符串参数。故采用: - -``` -__asm 版 Handler(.emb_text) → 只取异常帧指针,B 到 per-fault 入口 -fault_entry_hard/mem/bus/usage(C) → 各自带上故障名,调 hard_fault_report(tag, frame) -hard_fault_report(tag, frame)(C) → 统一打印现场后 NVIC_SystemReset() -``` - -日志形如 `[ERR] MEMFAULT PC=... CFSR=... MMFAR=...`(故障名 + 全量现场)。 - -#### L3 落地:审计脚本 + 基线 +### 10.3 审计脚本 ```bash python scripts/check_cubemx_survival.py # 审计报告(人读) -python scripts/check_cubemx_survival.py --check # 门禁模式(pre-commit 已挂) -python scripts/check_cubemx_survival.py --write # 重建基线(确认改动无误后) +python scripts/check_cubemx_survival.py --write # 确认改动无误后重建基线 ``` -- **清单**:21 项覆盖 P0/P1 全部防线(iwdg 延后启动、4 个 Fault Handler 采集、 - 故障使能、两个加固宏、断言改造 ×2、看门狗模块与工程挂载、复位原因锁存、 - HAL 驱动与使能宏、`.ioc` 关键项) -- **已实测有效**:2026-09-29 第二次重生成时,脚本**准确检出全部 5 项被覆盖**(5 FAIL) -- **漂移检测**:把各项内容哈希存 `.cubemx_survival_baseline.json`,`--check` 比对。 - ⚠️ **`MDK-ARM/*.uvprojx` 排除在漂移检测之外**(17 项入基线,18 项参与断言)—— - 该文件每次 `@build` 后被 Keil UV4 自动改写(XML 格式归一化 + `PackID` 降级到 - 本机 DFP 版本),内容每次构建都不同;若参与检测则**每次构建后提交都会被拦**。 - 脚本内以 `no_baseline=True` 标记,只保留"源文件仍挂载"断言 -- **已挂 pre-commit**(`cubemx-survival` 钩子):改动 `.ioc` / `Src/` / `Inc/` / - `task_watchdog` / `.uvprojx` 时自动触发 -- **注释过滤**:断言前先剥掉 C 注释,避免"注释里提到 `HAL_IWDG_Init`"被误判 - (本项目多处注释刻意引用历史代码,不做过滤会恒定误报)。 - ⚠ 由此推论(2026-10-09 定):**`protected` 项的 `must_contain` 只能断言代码符号**—— - 锚点注释会被剥掉,断言它等于没断言。故 `iwdg-defer-start` / `hardfault-handler` / - `fault-handlers-3` 三项已全部改为 `protected`(区 `0`)+ 代码符号断言 - (如 `iwdg_deferred_start`、`__asm void HardFault_Handler`、`#define X X_Gen`)。 - 脚本内以 `probe`(正项)/ `probe_neg`(禁项)分离处理 +- **清单 21 项**覆盖 P0/P1 全部防线(iwdg 延后启动、4 个 Fault Handler 采集、故障使能、两个加固宏、断言改造 ×2、看门狗模块与工程挂载、复位原因锁存、HAL 驱动与使能宏、`.ioc` 关键项、钩子落点三项)。 +- **漂移检测**:各项内容哈希存 `.cubemx_survival_baseline.json`;有意改动确认后 `--write` 重建。 +- ⚠ `MDK-ARM/*.uvprojx` **排除在漂移检测之外**(`no_baseline=True`)——该文件每次 `@build` 后被 UV4 改写(XML 归一化 + `PackID` 降级),参与检测会每次构建后都被拦;只保留"源文件仍挂载"断言。 +- **注释过滤**:断言前剥掉 C 注释,故 `protected` 项的 `must_contain` **只能断言代码符号**(锚点注释会被剥掉,断言它等于没断言)。 +- 已挂 pre-commit `cubemx-survival`(改 `.ioc`/`Src/`/`Inc/`/`task_watchdog`/`.uvprojx` 触发)。 -> **推荐工作流**:**每次 CubeMX 重生成后,立刻跑一次 -> `python scripts/check_cubemx_survival.py`**——它会把"被覆盖了哪几处" -> 直接列出来(含修复提示),比事后靠故障现象反推快得多。 -> 本项目的两次重生成都是靠它把损失锁定在"5 处、含修复提示"的范围内。 +> **工作流**:每次 CubeMX 重生成后**立刻**跑一次该脚本——它直接列出"被覆盖了哪几处 + 修复提示",比事后靠故障现象反推快得多。 --- -## 11. 结论 - -| 议题 | 结论 | -|---|---| -| **选型** | **IWDG**(独立 LSI),非 WWDG(依赖 APB1 + 中断,在关中断/HardFault 场景失效) | -| **实施路径** | **CubeMX 勾选 IWDG 生成**(`.ioc` = 配置单一事实源,与其他外设一致);唯一手工改动 = `Src/iwdg.c` 中阻止 `HAL_IWDG_Init()` 立即启动(§2.2 / §4.5) | -| **防丢失** | **L1 写 `.ioc` > L2 搬进 `USER CODE` 区 > L3 审计兜底**(§10.2);⚠️ **自造区名不可依赖**(CubeMX 换模板会连区带内容一起删,2026-09-29 实测);已落 `scripts/check_cubemx_survival.py` + pre-commit 钩子 | -| **架构** | **任务心跳位图(各自置位,无锁单字)+ defaultTask 单一喂狗者**——避免"有人跑就喂狗"的假保护 | -| **超时** | 2498 ms(PR=4 / RLR=1249);最坏(LSI 47 kHz)≈1701 ms;中间留 800 ms 停喂判定裕度 | -| **启动时机** | **defaultTask bring-up 完成后**——绕开初始化深链超时陷阱,代价是启动段无保护 | -| **断言** | `configASSERT` 改"先记录后复位",与 IWDG 形成双保险 | -| **诊断** | ① 读 `RCC_CSR` 硬件标志判复位原因(权威),替代"靠时间差猜上电/重启";② **4 个 Fault(Hard/MM/Bus/Usage)全部采集现场**并打故障名(`fault_handler_enable()` 使能后三者,否则被升级成 HardFault) | -| **已知代价** | ① 启动段无保护 ② `ftpTask` 首期不监视 ③ LSI 精度差需按最坏值设计 | -| **成本** | ≈ 24 B RAM / ≈ 0.7 KB Flash,可忽略 | - ---- - -## 附录 A:调研依据 +## 附录 A:参考依据(当前有效) | 来源 | 引用点 | |---|---| -| `docs/芯片手册/STM32F4xx参考手册_V18英文版_1-1000.md` §21(IWDG) | 12 位递减计数器、LSI 时钟源、启动后不可停、超时公式 | -| 同上 §6.3.21(`RCC_CSR`) | `IWDGRSTF`(bit29) / `WWDGRSTF`(bit30) / `SFTRSTF`(bit28) / `PORRSTF`(bit27) / `PINRSTF` / `LPWRRSTF` / `RMVF`(bit24);"except reset flags by power reset only" | -| 同上 §(LSI 小节) | LSI ≈ 32 kHz、可用于 IWDG、制造偏差大、可测校 | -| `STM32F407-Demo.ioc` | `RCC.LSI_VALUE=32000`;无 IWDG 配置项 | -| `Inc/stm32f4xx_hal_conf.h:58` | `HAL_IWDG_MODULE_ENABLED` 处于注释态 | -| `Inc/stm32f4xx_hal_conf.h:375` | `#include "stm32f4xx_hal_iwdg.h"` 位于 `#ifdef HAL_IWDG_MODULE_ENABLED` 内 | -| `find Drivers -iname "*iwdg*"` | **零命中**——HAL IWDG 驱动源码不在仓库 | -| `MDK-ARM/*.uvprojx` | `stm32f4xx_hal_*.c` 列表无 `stm32f4xx_hal_iwdg.c` | -| `Drivers/CMSIS/.../stm32f407xx.h:573-579` | `IWDG_TypeDef`(KR/PR/RLR/SR) | -| 同上 `:944` / `:1068` | `IWDG_BASE` / `IWDG` 实例指针 | -| 同上 `:9330-9353` | `IWDG_KR_KEY_Msk` / `IWDG_PR_PR_Msk` / `IWDG_RLR_RL_Msk` / `IWDG_SR_PVU` / `IWDG_SR_RVU` | -| 同上 `:10317-10336` | `RCC_CSR_RMVF`(24) / `BORRSTF`(25) / `PINRSTF`(26) / `PORRSTF`(27) / `SFTRSTF`(28) / `IWDGRSTF`(29) / `WWDGRSTF`(30) | -| `Inc/FreeRTOSConfig.h:163` | `configASSERT` = 关中断死循环 | -| `Src/freertos.c:323-360` | 既有 `vApplicationStackOverflowHook`(复位)/ `vApplicationMallocFailedHook`(不复位) | -| `App/task/net_task.c:160-215` | `ENABLE_HW_LAYER_TESTS` 自挂起、主循环 `osDelay(1)`、PHY 等待 60×50 ms | -| `App/task/default_task.c:95-204` | bring-up 深链、主循环 50 ms 基准 | -| `App/sync/time_sync.c:68/75` | `vTaskDelay(2000)` 与监视窗口冲突 | -| `Src/freertos.c:242-250` | `ftpTask` 在屏障上 `osWaitForever` 阻塞 | -| `docs/问题记录/项目审查与优化建议报告.md` §4.1 | N-03a/b/c 缺陷原始登记与建议 | +| `docs/芯片手册/STM32F4xx参考手册_V18英文版_1-1000.md` §21 | 12 位递减计数器、LSI 时钟源、启动后不可停、超时公式、`SR.PVU/RVU` 同步 | +| 同上 §6.3.22(`RCC_CSR`) | `PORRSTF/BORRSTF/PINRSTF/SFTRSTF/IWDGRSTF/WWDGRSTF/RMVF`;"复位标志仅由电源复位清除" | +| `Drivers/CMSIS/.../stm32f407xx.h` | `IWDG_TypeDef`(KR/PR/RLR/SR)、`IWDG_BASE`、`IWDG_SR_PVU/RVU`、`RCC_CSR_*` 位定义 | +| `STM32F407-Demo.ioc` | `Mcu.IP4=IWDG`、`IWDG.Prescaler/Reload`、`RCC.LSI_VALUE=32000`、`FREERTOS.Tasks01`、两个加固宏 | +| `Inc/stm32f4xx_hal_conf.h:58` | `HAL_IWDG_MODULE_ENABLED`(已启用) | +| `MDK-ARM/STM32F407-Demo.uvprojx` | 挂载 `task_watchdog.c`、`stm32f4xx_hal_iwdg.c` | +| `MDK-ARM/STM32F407-Demo.sct` | `UNINIT_CCM 0x1000FF00 … 0x100` + `*(.noinit)` | +| `MDK-ARM/STM32F407-Demo.map` | `Removing … HAL_IWDG_Init (94 bytes)`、`task_watchdog.o` 尺寸、`.noinit` 48 B | +| `Inc/FreeRTOSConfig.h:165` | `configASSERT` → `vApplicationAssertFailed` | +| `App/task/net_task.c` / `default_task.c` / `sync/time_sync.c` / `NET/lftpd/*` / `GD5F2GQ5UE/gd5f2gq5ue.c` | 心跳与喂狗埋点(见 §5.5) | + +> 立项时(2026-09-29)的"仓库里根本没有 IWDG 驱动"一类取证已随实施失效,不再列出;当时的原始登记见 `../问题记录/项目审查与优化建议报告.md` §4.1(N-03,已销项)。 ## 附录 B:备选方案取舍记录 | 备选 | 否决理由 | |---|---| -| 每任务各自喂狗 | 只要有任一任务存活就喂狗,无法检测单任务卡死(§3.1) | -| 用 FreeRTOS 软件定时器喂狗 | 定时器任务独立于业务任务,其存活不代表业务健康;且其栈小、优先级高,不适合承载判定逻辑 | -| 用 `osEventFlags` 做心跳 | 需 7 位,`osEventFlagsSet` 可能触发调度,任务循环内代价过高 | -| WWDG(窗口看门狗) | 依赖 APB1 时钟与中断,在关中断/HardFault/时钟异常场景失效(§2) | -| 独立看门狗任务 | 需新建任务(TCB+栈),RAM 预算紧张;且"谁来监视监视者"问题无解 | -| 直接 `NVIC_SystemReset()` 而非让 IWDG 溢出 | 复位原因变 `SFTRSTF`,丢失"看门狗抓到"的权威信息(§5.3) | -| 动态改 IWDG 超时 | IWDG 分频/重装值在启动后不可改(需先复位再配),项目无需此能力 | -| **借助 CubeMX 重新生成以取得 IWDG 驱动** | 重生成会重写 `Src/`/`Inc/` 生成区,与既有定制冲突;且历史上有行尾空白抖动问题(§2.2 路径 A) | -| **手工补入 HAL IWDG 驱动并改 `.uvprojx`** | 需手改 XML 工程文件(结构脆弱),且引入约 0.3 KB Flash 封装,而 CMSIS 头已够用(§2.2 路径 B) | -| 每任务用独立递减计数器而非位图 | 计数器回绕判定繁琐,且需临界区保护;位图"取走清零"单字原子更优(§3.3) | +| 每任务各自喂狗 | 任一任务存活就喂狗,检测不到单任务卡死(§3.1) | +| FreeRTOS 软件定时器喂狗 | 定时器任务存活不代表业务健康;栈小优先级高,不适合承载判定 | +| `osEventFlags` 做心跳 | 需 6 位,`osEventFlagsSet` 可能触发调度,循环内代价过高 | +| WWDG | 依赖 APB1 与中断,在关中断/Fault/时钟异常场景失效(§2.1,V4 已反证 IWDG 独立性) | +| 独立看门狗任务 | 新建任务(TCB+栈)与 RAM 预算冲突;"谁来监视监视者"无解 | +| 直接 `NVIC_SystemReset()` 而非让 IWDG 溢出 | 复位原因变 `SFTRSTF`,丢失权威归因(§4.2) | +| 动态改 IWDG 超时 | 启动后 `PR/RLR` 不可改(须先复位再配),无需此能力 | +| 手工补 HAL IWDG 驱动 + 手改 `.uvprojx` | 工程 XML 结构脆弱、`.ioc` 与代码脱节 → 改走 CubeMX(§2.4) | +| 每任务独立计数器而非位图 | 回绕判定繁琐且需临界区;位图"取走清零"单字原子更优(§3.3) | +| 在 `IWDG_Init 2` 区"反初始化"撤销启动 | IWDG 启动后**无法停止**(唯一手段是复位)→ 只能在启动前拦截(§2.4) | +| 放宽监视窗口以容纳 30 s 网络等待 | 窗口一放宽,真卡死的检测就迟钝;改取"**切片喂位**"(§4.3) | --- @@ -1142,3 +569,4 @@ python scripts/check_cubemx_survival.py --write # 重建基线(确认改动 | 2026-10-03 | **V2/V4/V5 台架验收闭环(N-03 销项)** | 三用例全部通过。**V2 任务饿死**:`netTask` 循环插 `while(1)` → 800ms 声明 `[WD] task stall: mask=0x00000001 req=0x00000037`,≤2.5s 触发 IWDG 复位(`reset cause: IWDG`,`RCC_CSR=0x24000000`)。⚠️ 首轮实测**不复位**(stall 已声明、`miss` 累至 1157≈115s):根因是**停摆证据落盘自身喂狗**——`wd_stall_evidence()`→`sys_log_evidence()` 在停喂窗口里每 100ms 轮都写 NAND(`/log/wdev.log` + `SYSLOG_RUN` flush 诱导),经 `nand_ftl_wd_feed()` **直接重装 IWDG**,"停喂"语义被证据机制抵消。修复:加 `s_stall_evidenced` **每 episode 门闩**(每轮停摆只落盘一次,此后不再触碰 NAND)→ 复测复位正常。**V4 关中断死循环**:`__disable_irq()` 后插 `while(1)` → 设备每 ~4.9s 稳定复位(20+ 周期,`reset cause: IWDG`),证实 IWDG **独立于中断**(PRIMASK 屏蔽 TIM7/SysTick/PendSV 仍能复位)。**V5 长期不误杀**:FTP 512KB 上/下传 + `e02_probe` 网络探针持续 **30 min** → **120/120 次 FTP 传输全 PASS(MATCH)、零 IWDG 复位**(唯一复位为起始烧录的 `software (NVIC_SystemReset)`)。Step 4 堆告警全程未触发(`min_ever=4728 B` > 3072 B)。**遗留观察(已登记 `待处理内容.md`)**:V5 期间 44 次 `task stall`(`mask=0x00000001` 持续 ≥800ms)**100% 与 `e02_probe` 的 30s 挂起客户端→`time_sync` 掉线+监听重建(`listen FAIL`×2,约 4s)重合**;隔离实验证实「仅 FTP(无探针)120s = 0 次 stall」→ 该停摆为**探针场景专属**,非一般负载误杀。因缺口(~1s)< IWDG 最坏超时(1701ms)而未复位,与 §4.3 包含关系一致 | | 2026-10-09 | **IWDG 延后启动由 L3 升级为 L2:不再需要每次重生成后手工删那一句** | 起因是提问"能否在初始化函数下方的自定义区里先反初始化"。查证:**反初始化不可行**——IWDG 一旦启动就**无法停止**(参考手册:唯一停止手段是复位),故不能在 `IWDG_Init 2` 区撤销。改用等价且免手工的做法:`Src/iwdg.c` 的 `USER CODE BEGIN 0`(标准保护位)放空实现 `iwdg_deferred_start()` + `#define HAL_IWDG_Init(a_h) iwdg_deferred_start(a_h)`,把**本编译单元里**那句生成调用 `HAL_IWDG_Init(&hiwdg)` 改名到空实现 → 生成区保持 CubeMX 原样、**重生成零 diff**。`.map` 证实 `Removing stm32f4xx_hal_iwdg.o(i.HAL_IWDG_Init), (94 bytes)`、`iwdg.o(i.MX_IWDG_Init)` 只引用 `.bss`(空实现被编译器消掉)。审计项 `iwdg-defer-start` 相应由 `wholefile`+锚点注释改为 `protected`(区 `0`)+ 代码符号断言(`iwdg_deferred_start` / `#define HAL_IWDG_Init`——`protected` 比对前会剥掉注释,锚点注释不能当断言)。§10.2 硬教训②与 L3 存量清单同步:L3 只剩 4 个 Fault Handler 实体(同一招可根治,尚未做)。`@build` 0 错误 0 警告;审计 21 通过 / 1 警告 / 0 失败 | | 2026-10-09 | **4 个 Fault Handler 由 L3 升级为 L2:重生成完全零 diff** | 用与 iwdg 同一招。`Src/stm32f4xx_it.c` 的 4 个 `__asm void X_Handler` 从**生成区**搬进 `USER CODE BEGIN 0`,且必须写在文件头那 4 个 `#define X X_Gen` **之前**(写在之后会被自己的宏改名);配套的 `#undef X` 随之删除(不再需要)。生成区恢复 CubeMX 原样(`void HardFault_Handler(void)` + `HardFault_IRQn 0` / `W1_HardFault_IRQn 0` 标记区),4 个锚点注释(`HARD-FAULT-ANCHOR` / `MEMFAULT-ANCHOR` / `BUSFAULT-ANCHOR` / `USAGEFAULT-ANCHOR`)全部退役。`.map` 复核:`startup_stm32f407xx.o(RESET) refers to stm32f4xx_it.o(.emb_text) for HardFault_Handler`(向量表仍指向我们的 `__asm` 版)、`.emb_text → i.fault_entry_* → i.hard_fault_report`、4 个 `*_Gen` 各 `Removing ... (2 bytes)`。审计项 `hardfault-handler` / `fault-handlers-3` 由 `wholefile`+锚点改为 `protected`(区 `0`)+ 代码符号断言。**L3 清单清空**,重生成预期零 diff。`@build` 0 错误 0 警告;审计 21 通过 / 1 警告 / 0 失败 | +| 2026-10-10 | **正文按当前代码重写(清理立项前的描述)** | 正文原样保留了大量**立项前(2026-09-29)**的状态取证与已废弃的"路径 C 考古",与现状矛盾,故重写为现状设计说明(1144 → 571 行;**历史与逐次决策全部保留在下方变更记录,未删**)。主要更正:① §1.1"现状:无任何硬件看门狗"改为**历史背景**(IWDG 已在 `.ioc`/`hal_conf`/工程三处就位);② **ftpTask 早已纳入监视**——经 `lftpd_task_wd_beat()` 弱钩子(事件循环 / RETR·STOR 分片 / 连接与卷锁等待),旧正文"首期不埋点、由掩码剔除"及 R3/O1 已作废;③ 补齐正文缺失的三块现状实现:**FTL 心跳路由**(`task_wd_bind()` + `nand_ftl_wd_beat()`)、**FTL 直接喂狗**(`nand_ftl_wd_feed()`,含"设备存活但功能冻结"边界)、**round 7/8/8b 取证机制**(`/log/wdev.log`、`.noinit` 槽 + `UNINIT_CCM`、`PREVRUN`/`TICKRUN`/`MISS start`/`STALL`);④ 掩码改为按代码实况列表(产品 `0x37` / `ENABLE_HW_LAYER_TESTS` `0x35` / **testTask 任何构建都不入掩码**)并写明"精确相等"语义;⑤ 复位原因优先级按 `reset_cause_str()` 实际顺序改写(旧表把 POR 排第一,代码是 **IWDG 最优先**);⑥ §4.6 更正为**已集成**:看门狗复位写 `selftest.log`(旧正文"未改");⑦ §8 预算改用 `.map` 实测(`task_watchdog.o` 1098 B 代码 / 44+104 B RAM / `.noinit` 48 B;`iwdg.o` 仅 28 B,`HAL_IWDG_Init` 94 B 被移除);⑧ 附录 B 自相矛盾修正(旧表把"借助 CubeMX"列为否决,实际已选它);⑨ §7 由"验证计划"改为**验证记录**(V2/V4/V5 ✅、U1 39 断言 ✅,V3/V6/V7 标为未单独注入、V8 未做)。连带同步:`Src/iwdg.c`、`Src/freertos.c`、`App/task/default_task.c` 三处注释的章节引用(旧 §4.5/§4.6 → 新 §4.1/§4.4),`scripts/check_cubemx_survival.py` 提示语(§10.1 → §10.2/§10.3),`实机测试实施指南.md`(§10 → §7 ×3)、`看门狗单元测试说明.md`(§10 → §7)、`模拟器差距清单.md` H-1(§4.9 → §4.7,并更正"模拟器不编译本模块、无 IWDG 桩")、`docs/README.md`("实施中/待验证" → 已闭环)。⚠️ **下方历史行里的 §x.y 引用指当时编号**,不再回溯改写 | diff --git a/docs/测试说明/实机测试实施指南.md b/docs/测试说明/实机测试实施指南.md index ed55549a..7fc10de5 100644 --- a/docs/测试说明/实机测试实施指南.md +++ b/docs/测试说明/实机测试实施指南.md @@ -7,7 +7,7 @@ > 无实机时**只读本文,不执行任何烧录**。 > **上游**:`错误码传播审计.md`(E-xx)· `待处理内容.md`(统一登记簿:P-x / N·S·T-x / > S1x·A-x / 阶段排序 / 诊断基线)· -> `子系统设计/看门狗设计说明.md`(§10 台架步骤)。 +> `子系统设计/看门狗设计说明.md`(§7 验证记录)。 > **约定**:本文事项完成后**先销上游登记**,再在文末变更记录留一行; > 结论与证据沉淀进对应模块文档/问题记录(2026-10-02 台架窗口的完整取证链—— > IWDG PR/RLR 静默错配 / dhara resume 回滚 / ORE 僵尸通道——见 `看门狗设计说明.md` 2026-10-02 变更记录、 @@ -103,7 +103,7 @@ > V2/V4/V5 三用例全部通过——V2(任务饿死)声明 stall 后 ≤2.5s `reset cause: IWDG`;V4(关中断死循环)每 ~4.9s 稳定 IWDG 复位(IWDG 独立于中断);V5(30 min 真实负载)**120/120 次 FTP 传输全 PASS、零 IWDG 复位**。 > **遗留观察**:V5 期间 44 次 stall 声明 100% 与 `e02_probe` 探针的 time_sync 30s 掉线+监听重建重合;隔离实验(仅 FTP 120s)**0 次 stall** → 探针场景专属,非一般负载误杀(已登记 `待处理内容.md`)。 -> 来源:`子系统设计/看门狗设计说明.md` §10 步骤 7~10。 +> 来源:`子系统设计/看门狗设计说明.md` §7 验证记录(原实施步骤已并入该文档文末变更记录)。 > 判定状态机与 IWDG 配置(PR/RLR 按设计值写入)已落地并经真机验证(2026-10-02,`930215f`, > 见设计说明 2026-10-02 变更记录);故障注入 + 长跑验收结论见上文 2026-10-03 段落。 @@ -433,7 +433,7 @@ CH395F / STORAGE / GD5F,未含 RTC;怀疑 RTC 构建下 `testTask` 不被创 | 本文项 | 上游文档 | 下游落地文档 | |---|---|---| | S7 自检判定 | —(原复审项,已闭环 2026-10-02) | `测试说明/自动化测试流程.md`(参数已落地,`4a4cd29`) | -| V2/V4/V5 | `子系统设计/看门狗设计说明.md` §10 | `子系统设计/看门狗设计说明.md` 变更记录(2026-10-03) | +| V2/V4/V5 | `子系统设计/看门狗设计说明.md` §7 验证记录 | `子系统设计/看门狗设计说明.md` 变更记录(2026-10-03) | | S11 堆告警 | —(原复审项,已闭环 2026-10-03) | `内存管理/RAM使用分析与优化方案.md`(基线更新) | | S9 GD5F | `问题记录/错误码传播审计.md` 第二档 #1 | `问题记录/GD5F2GQ5UE_Trap_Records.md`、`测试说明/GD5F2GQ5UE_Test_Guide.md` | | N-2 日志静默 | `待处理内容.md` N-2 | `问题记录/错误码传播审计.md` E-02 | diff --git a/docs/测试说明/模拟器差距清单.md b/docs/测试说明/模拟器差距清单.md index 3d860fb3..696a9541 100644 --- a/docs/测试说明/模拟器差距清单.md +++ b/docs/测试说明/模拟器差距清单.md @@ -60,7 +60,7 @@ | # | 差距 | 模拟器行为 | 真机行为 | 影响 | 回归补测 | |---|---|---|---|---|---| -| H-1 | **IWDG 硬件行为** | 无 IWDG;`sim_port.c` 桩(空实现 + 计数器) | 独立看门狗,LSI 时钟驱动,超时真复位 | **真实复位行为、LSI 漂移、`RCC_CSR` 标志均不可建模**;位图/判定逻辑可在模拟器跑(纯 RAM) | 真机 `@test`(复位原因日志)+ 看门狗单测(L0.5 已覆盖位图逻辑)
`看门狗设计说明.md` §4.9 | +| H-1 | **IWDG 硬件行为** | **不编译看门狗模块**(`simulator/` 无 `task_watchdog.c`,也无 IWDG 桩) | 独立看门狗,LSI 时钟驱动,超时真复位 | **真实复位行为、LSI 漂移、`RCC_CSR` 标志均不可建模**;位图/判定逻辑也不在模拟器内跑——改由主机侧单测覆盖 | 真机 `@test`(复位原因日志 + 故障注入 V2/V4)+ 看门狗主机侧单测(L0.5,39 断言)
`看门狗设计说明.md` §4.7 / §7 | | H-2 | **CH395F 芯片** | 无(Winsock 代其 BSD API) | SPI 寄存器/命令/中断、PHY 协商、真实以太网帧 | 寄存器时序、命令回复、中断风暴、PHY 100M 全双工强制 | 板端 CH395F 套件(52 断言点)+ `@test` | | H-3 | **GD5F NAND** | 无 | SPI NAND:BBT 扫描、ECC、坏块管理、dhara FTL | 坏块/ECC/写入失败等路径完全不可测 | 板端 GD5F 套件(76 断言点)+ `@test` | | H-4 | **SD2506 RTC** | 无 | I2C RTC:时间读写、闹钟、温度、电池电压 | 时间源相关(`sys_clock_init` 降级路径)在模拟器上不可触发 | 板端 RTC 套件(18 断言点)+ `sys_clock` L0.5(已覆盖算术面) | diff --git a/docs/测试说明/看门狗单元测试说明.md b/docs/测试说明/看门狗单元测试说明.md index c10e42fe..1111137e 100644 --- a/docs/测试说明/看门狗单元测试说明.md +++ b/docs/测试说明/看门狗单元测试说明.md @@ -93,7 +93,7 @@ test/host/watchdog/watchdog_test.c | **门禁层级** | **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,见 `子系统设计/看门狗设计说明.md` §10)保证**真机复位链路**正确 | +| **与板端测试的关系** | 互补:本单测保证**判定逻辑**正确;板端 `@test` 故障注入(V2/V4,见 `子系统设计/看门狗设计说明.md` §7 验证记录)保证**真机复位链路**正确 | --- diff --git a/scripts/check_cubemx_survival.py b/scripts/check_cubemx_survival.py index 9dfeea35..e9a8dd7b 100644 --- a/scripts/check_cubemx_survival.py +++ b/scripts/check_cubemx_survival.py @@ -30,7 +30,7 @@ CubeMX 重生成存活审计(check_cubemx_survival.py) 退出码:0 = 通过;1 = 有 FAIL 项。 -设计依据:docs/子系统设计/看门狗设计说明.md §10.1;AGENTS.md「平台约束」。 +设计依据:docs/子系统设计/看门狗设计说明.md §10.2/§10.3;AGENTS.md「平台约束」。 注意 ---- @@ -423,7 +423,7 @@ def evaluate(): "WARN", "legacy:" + path, "标记区外手工内容(重生成会覆盖):" + "、".join(hits), - "这些内容 CubeMX 不保护。核对清单见 docs/子系统设计/看门狗设计说明.md §10.1。", + "这些内容 CubeMX 不保护。核对清单见 docs/子系统设计/看门狗设计说明.md §10.3。", )) return results, snap