From e18e9f2cfd3dcf86ff26f9a2118f83ff30ae3488 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 29 Sep 2026 06:46:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=9C=8B=E9=97=A8=E7=8B=97?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E8=AF=B4=E6=98=8E=EF=BC=9AIWDG=20=E9=80=89?= =?UTF-8?q?=E5=9E=8B=E4=B8=8E=E4=BB=BB=E5=8A=A1=E5=BF=83=E8=B7=B3=E4=BD=8D?= =?UTF-8?q?=E5=9B=BE=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 1 + docs/子系统设计/看门狗设计说明.md | 771 ++++++++++++++++++++++++++++++ docs/待处理内容.md | 8 + 3 files changed, 780 insertions(+) create mode 100644 docs/子系统设计/看门狗设计说明.md diff --git a/docs/README.md b/docs/README.md index 032dafbb..f791f264 100644 --- a/docs/README.md +++ b/docs/README.md @@ -76,6 +76,7 @@ docs/ | [`系统墙钟时间维护说明.md`](子系统设计/系统墙钟时间维护说明.md) | `sys_clock`:RTC 基准、tick 回绕防护、30 天惰性再基准化 | | [`控制台单写者日志设计说明.md`](子系统设计/控制台单写者日志设计说明.md) | 控制台日志单写者模型(避免多任务交错输出) | | [`运行与自检记录设计说明.md`](子系统设计/运行与自检记录设计说明.md) | `/log/run.log`·`selftest.log` 定长记录与待机显示 | +| [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | **调研+方案(待实施)**:IWDG 选型、任务心跳位图、复位原因分类、验证计划 | | [`网络/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 new file mode 100644 index 00000000..7280d637 --- /dev/null +++ b/docs/子系统设计/看门狗设计说明.md @@ -0,0 +1,771 @@ +# 看门狗设计说明 + +> **模块名称**:独立看门狗(IWDG)+ 任务心跳位图 +> **文档定位**:**调研 + 设计方案**——记录选型依据、在既有平台上落地的约束、 +> 推荐架构与分步实施路径。**尚未实施**,实施后本文件应改写为"现状设计说明"。 +> **状态**:📋 设计完成,待实施(登记于 [`../待处理内容.md`](../待处理内容.md) N-03) +> **日期**:2026-09-29 +> **相关文档**: +> [`../审查记录/项目审查与优化建议报告.md`](../审查记录/项目审查与优化建议报告.md)(N-03 缺陷原始登记)、 +> [`../审查记录/下一步任务规划.md`](../审查记录/下一步任务规划.md)(排期)、 +> [`运行与自检记录设计说明.md`](运行与自检记录设计说明.md)(§5.3 上电/重启分类)、 +> [`../架构设计/项目启动顺序管理.md`](../架构设计/项目启动顺序管理.md)(就此绪屏障)、 +> [`../测试说明/ST-LINK_CLI_复位使用说明.md`](../测试说明/ST-LINK_CLI_复位使用说明.md)(复位语义) + +--- + +## 1. 问题域 + +### 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 一个**不存在的头文件**,直接编译失败。 +> 本方案因此**不走 HAL 驱动,改用直接寄存器操作**(见 §2.2)——这反而是更优路径, +> 因为 CMSIS 设备头**已提供完整的 IWDG 寄存器定义与实例指针**,零新增依赖。 + +### 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` 节拍与"串口静默窗口"观测习惯。 + +--- + +## 2. 选型:为什么是 IWDG 而不是 WWDG + +STM32F407 有两个看门狗,本项目只能用其中一个。 + +| 维度 | **IWDG**(独立) | **WWDG**(窗口) | +|---|---|---| +| 时钟源 | **LSI ≈ 32 kHz**(独立 RC,`IWDG` 一启动即强制 LSI 开启且不可关闭) | APB1 时钟(168/4 = 42 MHz,依赖 PLL) | +| 依赖 CPU 时钟 | ❌ 不依赖 | ✅ 依赖;PLL 失锁/时钟切换期失效 | +| 依赖中断 | ❌ 不依赖 | ✅ 依赖(早期唤醒中断 EWI) | +| 复位类型 | `IWDGRSTF`(VDD 域) | `WWDGRSTF` | +| 能否停 | **启动后只能靠复位停** | 可关 | +| 喂狗窗口 | 只有**下限**(太晚不喂就复位) | 有**上下限**(过早喂也复位) | + +**结论:选 IWDG。** 核心理由是本项目的故障谱——死锁、饿死、HardFault、断言挂死。 +这类故障**可能伴随时钟异常或中断被关闭**(`configASSERT` 就是关中断), +而 WWDG 依赖 APB1 与中断,恰恰在这些场景下失效。IWDG 走独立 LSI,是全场景兜底。 + +**代价**:LSI 精度差(datasheet 典型 32 kHz,实际可在 17–47 kHz 区间漂移,且随 +温度/电压变化)。项目 `.ioc` 已按 `RCC.LSI_VALUE=32000` 建模,超时需按**最坏情况** +留裕度(见 §4.3)。 + +### 2.1 IWDG 超时计算 + +STM32F4 的 IWDG 为 **12 位递减计数器**,超时公式(参考手册 §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)** | +|---|---|---|---|---| +| 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 | + +(表中数值经脚本按上述公式核算,非目测估计。) + +> ⚠️ **关键**:LSI 偏快时超时**变短**。设计必须按"最坏情况(LSI=47 kHz,即超时打约七折)" +> 校验,而不是按标称 32 kHz。上表"最坏超时"列即按 47 kHz 计算。 + +**本方案选定(见 §4.3)**:`PR=4`(64 分频)/ `RLR=1249` → 标称 **2498 ms**、 +最坏 **≈1701 ms**。 + +### 2.2 实施路径:直接寄存器操作,不引入 HAL IWDG 驱动 + +**约束事实**(§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 后生成 | ❌ **高风险**:CubeMX 重生成会重写 `Src/`/`Inc/` 生成区,项目有大量既存定制(见 `AGENTS.md`「平台约束」),且会带回行尾空白抖动(2026-09-20 已记录过该问题) | +| B. 手工补入 `stm32f4xx_hal_iwdg.c/h` | 从 STM32CubeF4 包拷贝两文件 + 注册进 MDK 工程 | ⚠️ 可行但需**手改 `.uvprojx`**(XML 结构脆弱),且引入约 0.3 KB Flash 的 HAL 封装 | +| **C. 直接寄存器操作**(**本方案选定**) | 用 CMSIS 已提供的 `IWDG` 实例指针 + 寄存器位域宏 | ✅ **零新增依赖、零工程文件改动、代码量约 20 行** | + +**选 C 的依据**:CMSIS 设备头 `Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f407xx.h` +**已完整提供**所需的一切(已实测确认): + +| 已有定义 | 位置 | 内容 | +|---|---|---| +| `IWDG_TypeDef` | `: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` 定义,本方案需自行定义): + +| 用途 | 值 | +|---|---| +| 喂狗(重装计数) | `0x0000AAAA` | +| 启动看门狗 | `0x0000CCCC` | +| 解锁 PR/RLR 写保护 | `0x00005555` | +| 锁定 PR/RLR(保护配置) | `0x00000000` | + +IWDG 是**"写键值"式外设**:写 `0x5555` 解锁 → 配 `PR`/`RLR` → 写 `0xCCCC` 启动 +(启动后**不可停**,只能复位)→ 此后周期性写 `0xAAAA` 喂狗。 + +> **为何这会被称为"更优路径"**:HAL 的 `HAL_IWDG_Init()` 内部做的事就是上面这几步, +> 外加一个 `IWDG_HandleTypeDef` 状态机。本项目不需要那层封装——寄存器操作总共 +> 约 20 行,且完全落在 `App/util/task_watchdog.c` 内,**不触碰任何 CubeMX 生成文件**。 + +--- + +## 3. 架构:心跳位图 + 单一喂狗者 + +### 3.1 为什么不能"每个任务各自喂狗" + +最朴素的方案——`HAL_IWDG_Refresh()` 塞进每个任务循环——有致命缺陷: + +**只要还有任何一个任务在跑,狗就会被喂。** 那么真正的故障场景(`netTask` 死锁、 +`uiTask` 饿死)**永远检测不到**,因为别的任务还在正常喂。 + +它退化成"只检测全部任务同时挂死",而那种情况现实中几乎不发生。 + +### 3.2 采用方案:任务置位 → 校验者判定 → 校验者喂狗 + +``` + ┌──────────────── 各自任务循环 ────────────────┐ + │ defaultTask / netTask / ftpTask / uiTask │ + │ / timeSyncTask / testTask │ + │ 每轮循环:s_hb_bits |= (1u << MY_ID) │ ← 无锁,单字原子写 + └───────────────────────┬──────────────────────┘ + │ (位图,仅置位,不清零) + ▼ + ┌──────────── defaultTask(唯一喂狗者)─────────┐ + │ 每 100 ms 抽样: │ + │ snapshot = s_hb_bits; s_hb_bits = 0; │ ← 取走并清零 + │ if (snapshot != ALL_REQUIRED_MASK) │ + │ → 有任务在 100 ms 内没动过 │ + │ → 累计未达标次数 / 记录缺失任务名 │ + │ if (连续 N 次未达标) HAL_IWDG_Refresh(); │ ← 只在全员健康时喂 + └───────────────────────────────────────────────┘ + │ + ▼ + IWDG(超时 2.5 s,PR=4 / RLR=1249) +``` + +**语义**:喂狗 = "**所有被监视的任务都在窗口内动过**"。 +任何单个任务卡死 → 位图长期残缺 → 喂狗停止 → 2.5 s 后 IWDG 复位。 + +### 3.3 为什么心跳位图用"取走并清零" + +``` +snapshot = s_hb_bits; /* 读 */ +s_hb_bits = 0; /* 清 */ +``` + +这是**单字读-改-写**,在 Cortex-M4 上天然原子(无锁)。相比之下: + +| 方案 | 问题 | +|---|---| +| 每任务用独立 `volatile uint32_t` 计数器,校验者比较"是否增长" | 需额外临界区,且 32 位计数器回绕判定繁琐 | +| 用 `osEventFlags` 做心跳 | 需要 7 个事件位;`osEventFlagsSet` **可能阻塞/触发调度**,任务循环里代价过高 | +| **单字位图 + 取走清零** | ✅ 零临界区、单指令原子、一个静态字 | + +**取走清零的语义优势**:校验者看到的位图 = "**自上次抽样以来**哪些任务动过"。 +若某任务卡死,它的位在每轮抽样里恒为 0 → 连续缺失可累计 → 可判"连续 N 轮未达标"。 + +### 3.4 为什么喂狗者在 defaultTask + +| 候选 | 评估 | +|---|---| +| **defaultTask** | ✅ 已具"单写者/属主"文化(配置引擎属主、syslog 唯一写者、控制台唯一发送者);循环基准 50 ms,天然适合 100 ms 抽样;栈已 4096 words,加少量逻辑无压力 | +| netTask | ❌ 主循环 `osDelay(1)`,节拍最紧,但它是**最可能卡死的任务之一**(SPI 事务等待)——让被监视者喂狗语义错乱 | +| 独立看门狗任务 | ❌ 需新建任务(栈 + TCB),RAM 预算紧张;且它比 defaultTask 更"旁路",出问题时无人知道 | +| 中断/timer | ❌ FreeRTOS timer 任务优先级高、栈小,且它的存活本身不能代表业务健康 | + +**defaultTask 是既有的"系统管家"角色**,喂狗职责与之一致。 + +> ⚠️ **对抗性风险**:defaultTask 自身卡死时,狗**永远不会被喂** → 必然复位。 +> 这正是期望行为(fail-safe),无需额外设计。 + +--- + +## 4. 关键设计决策 + +### 4.1 决策一:监视谁 —— 是否包含 testTask + +`testTask` 仅在启用测试套件时创建(`configCHECK_FOR_STACK_OVERFLOW` 与 +`TEST_SUITE_*` 决定),且套件运行时**独占存储/CH395F**,其循环可能长时间 +阻塞(如 nand_ftl 掉电模拟)。 + +| 方案 | 说明 | +|---|---| +| **A. 位图掩码按构建配置编译期生成**(推荐) | `#if defined(TEST_SUITE_*)` 决定 required mask 是否含 testTask 位 | +| B. 运行时判定(句柄非 NULL 才要求) | 更灵活,但要求校验者接触 TCB,代价高 | + +选 **A**:编译期掩码是最简单且零运行时开销的做法,与项目"按套件条件编译"的既有风格一致。 + +### 4.2 决策二:`ENABLE_HW_LAYER_TESTS`(netTask 自挂起)的处理 + +`net_task_func` 在 `ENABLE_HW_LAYER_TESTS` 下入口即 `for(;;) osDelay(1000);` +(`App/task/net_task.c:166-171`)——**该任务在该构建下本就不干活**。 + +若照常要求它置心跳位,则**该构建永不喂狗**(要么把 `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 ✅ | + +**统一原则**:**启动段不喂狗,主循环段才喂**。 + +### 4.5 决策五:IWDG 的启动时刻 + +关键约束来自项目铁律「`osKernelStart` 前禁止任何 FreeRTOS/CMSIS-RTOS API 调用」, +以及 `app_main_init` 内的深链耗时(NAND 初始化、RTC 读取、ADC 校准,合计可能 > 1 s)。 + +| 方案 | 评估 | +|---|---| +| **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` 完成后启动一个"短超时 + 手动多喂" +> 的临时监视(复杂度显著上升,暂不采用)。 + +### 4.6 决策六:`configASSERT` 改为"先记录后复位" + +现状(`Inc/FreeRTOSConfig.h:163`): + +```c +#define configASSERT( x ) if ((x) == 0) {taskDISABLE_INTERRUPTS(); for( ;; );} +``` + +改为: + +```c +/* 断言失败:记录任务名后软复位(由 IWDG 兜底真死循环场景) */ +#define configASSERT( x ) \ + if ((x) == 0) { vApplicationAssertFailed( ( const char * )__FILE__, __LINE__ ); } +``` + +在 `Src/freertos.c` 实现 `vApplicationAssertFailed()`,复用现有 +`hook_ptr_ok()` + `dbg_console_write_raw()` 的**裸 UART 有界发送**模式 +(与 `vApplicationStackOverflowHook` 同构),随后 `NVIC_SystemReset()`。 + +**收益**:断言从"无日志永久砖"变为"有日志 + 自动复位"。 +即使复位逻辑本身因内核态损坏而失效,**IWDG 仍会在 2.5 s 后强制复位**(双保险)。 + +### 4.7 决策七:与既有钩子的复位原因区分 + +`NVIC_SystemReset()` 置 `SFTRSTF`,IWDG 超时置 `IWDGRSTF`。 +两者在 `RCC_CSR` 中**不同位**,可区分。 + +但要注意 **`RCC_CSR` 的复位标志只由 POR(上电)清除**, +且读后**不会自动清**——必须在读完并记录后手工 `SET_BIT(RCC->CSR, RCC_CSR_RMVF)` +清标志,否则下次软复位会看到"上一次的残留标志"。 + +**统一方案**:在 `Src/main.c` 的 `HAL_Init()` 之后、`app_main_init()` 之前, +一次性读取并锁存复位原因到静态变量,随后清标志: + +``` +读 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.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` 自检模式回归"缺失检测"逻辑 | + +**模拟器无法验证的部分**:真实复位行为、LSI 漂移、`RCC_CSR` 标志—— +这些必须真机验证(见 §7)。 + +--- + +## 5. 详细设计 + +### 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()` 接口 | +| `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) | + +**不需要改动的**: + +- ❌ `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 接口设计 + +```c +/* App/util/task_watchdog.h */ + +/* 任务 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); /* 上次抽样到的位图(定位卡死任务) */ +``` + +### 5.3 实现要点 + +```c +/* App/util/task_watchdog.c 核心逻辑(示意) */ + +/* ---- IWDG 寄存器级驱动(§2.2 路径 C,不依赖 HAL IWDG 驱动) ---- */ +#include "stm32f407xx.h" /* 提供 IWDG 实例指针与位域宏(CMSIS 设备头已含) */ + +#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 识别) */ + } + } +} +``` + +> **为何"停喂"而不是直接 `NVIC_SystemReset()`**: +> 直接软复位会让 `RCC_CSR` 置 `SFTRSTF` 而非 `IWDGRSTF`, +> 丢失"是看门狗抓到的"这一关键信息。**让 IWDG 自然溢出**, +> 复位原因才是权威的 `watchdog reset`。代价是额外的 ≤2.5 s 延迟(可接受)。 + +### 5.4 各任务的埋点位置 + +| 任务 | 埋点位置 | 备注 | +|---|---|---| +| `defaultTask` | `default_task_func` 的 `for(;;)` 循环首行 | 50 ms 节拍,天然满足 | +| `netTask` | 主循环 `for(;;)` 内(`net_poll` 之后) | 1 ms 节拍;另需在 `net_stack_bring_up` 的 PHY 等待循环内补埋点(§4.4) | +| `ftpTask` | ⚠️ 见 §4.3 —— 第一步**剔除掩码**;后续改 `Lib/lftpd` 时再补 | 已知缺口 | +| `uiTask` | `ui_task_func` 主循环(`osDelay(10)` 处) | 10 ms 节拍 | +| `timeSyncTask` | `time_sync_task` 主循环 | 最长 `vTaskDelay(2000)`——⚠️ **2 s > 800 ms 容忍窗口**,需拆分:在 `vTaskDelay` 前多次分段置位,或把该 timeout 缩短 | +| `testTask` | 由套件掩码决定 | 套件运行时可长阻塞,按 §4.1 排除 | + +> ⚠️ **`timeSyncTask` 是关键风险点**:其主循环有两处 `vTaskDelay(pdMS_TO_TICKS(2000))` +> (`App/sync/time_sync.c:68/75`)。若心跳位只在循环首行,则 2 s 内位图恒缺席, +> **必然触发停喂**。处理三选一:① 每 200 ms 分段置位;② 缩短该 delay; +> ③ 该任务单独用"宽松窗口"(增大容忍次数到 25 次 = 2.5 s,但会逼近 IWDG 超时,不推荐)。 +> **推荐 ①**:把 2 s 拆成 10 × 200 ms 循环,每次置位并检查退出条件——语义不变,且提升响应性。 + +--- + +## 6. 可观测性 + +### 6.1 串口标记(供 `@test` 采集) + +| 标记 | 时机 | 含义 | +|---|---|---| +| `[WD] reset cause=<类型>` | `app_main_init` 早期 | 复位原因(`power on` / `watchdog reset` / `soft reset` / `pin reset`) | +| `[WD] watchdog armed (timeout=2500ms)` | `task_wd_start()` | IWDG 已启动(真机自检必查标记) | +| `[WD] miss streak= mask=0x%08X` | 每次未达标(限流打印) | 谁没动(`mask` 中 0 位 = 缺失任务) | +| `[ERR] WATCHDOG: task stall` | 停喂时 | 即将复位,最后遗言 | +| `[WD] beat ok` | 可选,调试期 | 心跳正常(默认关闭,避免刷屏) | + +### 6.2 与 `run.log` 的集成 + +`sys_log_mcu.c` 的 `syslog_mcu_mounted()` 当前只写 `power on` / `reboot` +(靠 `run.log` 末条 epoch 时间差推断)。**改为优先用硬件标志**: + +``` +复位原因(§4.8 优先级)→ 描述串映射: + power on → "power on" (既有) + watchdog reset → "watchdog" (新增,UI 字符集需补"看门狗"三字) + soft reset → "soft reset" (新增) + pin reset → "pin reset" (新增) + 其余 → 回退既有 600s 时间窗分类 +``` + +> ⚠️ **UI 字符集影响**:`docs/子系统设计/运行与自检记录设计说明.md` §6.1 记录了 +> UI 中文字符集需按 `font-regen` 流程重生成。新增描述串会引入新汉字 +> (如"看门狗",或改用"装置重启记录"复用既有字),**实施时须跑一次字符集 diff**。 +> **简化方案**:直接复用既有"装置重启记录",把细粒度复位原因只写 `run.log` 与串口, +> 不上 UI——**推荐**(零字符集改动)。 + +--- + +## 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` | + +> **V5 是本次实施的核心验收项**:误杀比不杀更有害。必须在真实业务负载下 +> 长时间无复位,才算通过。 + +### 7.2 模拟器验证 + +| # | 场景 | 命令 | 期望 | +|---|---|---|---| +| S1 | 心跳位图逻辑 | 新增 `lvgl_sim.exe --watchdogtest` | 模拟某任务停摆 → 断言 `miss_streak` 递增、`required_mask` 判定正确 | + +> IWDG 硬件行为在模拟器不可建模,S1 只验证**位图与判定逻辑**(纯 RAM 部分)。 + +--- + +## 8. 内存与资源预算 + +| 项 | 大小 | 位置 | +|---|---|---| +| 心跳位图 + 状态(`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** | — | + +对比现状 RAM 占用(`RW_IRAM1 ≈ 87 KB / 66%`,见 `docs/内存管理/`), +增量可忽略。**无需重新做 RAM 预算评估**。 + +> ✅ **注意**:因走寄存器路径(§2.2),**不引入** `stm32f4xx_hal_iwdg.c` +> (HAL 封装约 0.3 KB Flash),上表已据此计。相比"补 HAL 驱动"路径省下的这部分 +> Flash 恰与本模块自身代码量相当——**两条路径的 Flash 净增量接近,但寄存器路径 +> 零工程文件改动**,这是选它的实质理由。 + +--- + +## 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` 侧已有告警记录 | + +### 开放问题 + +| # | 问题 | 待定 | +|---|---|---| +| O1 | 是否把 `ftpTask` 纳入监视 | 取决于是否愿意改 `Lib/lftpd`(第三方库侵入度评估) | +| O2 | 是否上 UI 显示细粒度复位原因 | 涉及字符集重生成成本,倾向"只写 log/串口" | +| O3 | 启动段(R1)是否需要覆盖 | 待现场故障统计决定;倾向不做(复杂度收益比不划算) | +| O4 | 是否需要复位计数与"连续复位"抑制 | 若某故障导致复位风暴(复位→立刻再死),可考虑"连续 N 次看门狗复位后进入安全模式"。**当前不做**,记录为后续可选增强 | + +--- + +## 10. 实施步骤(按序执行,每步端到端验证) + +| 步 | 内容 | 验证 | +|---|---|---| +| **1** | 新建 `App/util/task_watchdog.c/h`;实现 IWDG 寄存器级 `wd_iwdg_init/feed` + 心跳位图;**登记进 MDK 工程** | `@build` 0/0 | +| **2** | `Src/main.c` 复位原因锁存 + 清标志;`syslog_mcu.c` 写描述串 | V7/V8(`cause` 正确) | +| **3** | 实现抽样判定 + 串口标记(**暂不启动 IWDG**) | V1 观察 `[WD] beat ok`(调试期) | +| **4** | 各任务埋点(含 `timeSyncTask` 拆分) | V5 短跑(5 min)无 `miss` 打印 | +| **5** | `task_wd_start()` 启动 IWDG(写 `0xCCCC`) | V1 + V5 | +| **6** | `configASSERT` 改造 + `vApplicationAssertFailed()` | V3 | +| **7** | 故障注入(V2/V4) | 复位 + `cause` 正确 | +| **8** | 模拟器 `--watchdogtest` + stub | S1 | +| **9** | 长跑(V5,≥30 min,真实负载) | **零复位** | +| **10** | 本文档改写为"现状设计说明";`待处理内容.md` N-03 销项;`README.md` 登记 | 门禁 | + +> **每步改完必须 `@build` 0 错误 0 警告**(`AGENTS.md` 铁律)。 +> +> **第 1 步的风险点**:`App/util/task_watchdog.c` 需在 `MDK-ARM/*.uvprojx` 中挂载。 +> 若挂载不顺,可临时把实现放进 `App/task/default_task.c`(已在工程内)验证逻辑, +> 待工程文件可改时再抽成独立模块。**两种形态的逻辑完全一致**。 + +--- + +## 11. 结论 + +| 议题 | 结论 | +|---|---| +| **选型** | **IWDG**(独立 LSI),非 WWDG(依赖 APB1 + 中断,在关中断/HardFault 场景失效) | +| **实施路径** | **直接寄存器操作**(不引入 HAL IWDG 驱动)——驱动源码不在仓库、也未经由 CubeMX 生成,CMSIS 设备头已提供全部寄存器定义(§2.2) | +| **架构** | **任务心跳位图(各自置位,无锁单字)+ defaultTask 单一喂狗者**——避免"有人跑就喂狗"的假保护 | +| **超时** | 2498 ms(PR=4 / RLR=1249);最坏(LSI 47 kHz)≈1701 ms;中间留 800 ms 停喂判定裕度 | +| **启动时机** | **defaultTask bring-up 完成后**——绕开初始化深链超时陷阱,代价是启动段无保护 | +| **断言** | `configASSERT` 改"先记录后复位",与 IWDG 形成双保险 | +| **诊断** | 读 `RCC_CSR` 硬件标志判复位原因(权威),替代"靠时间差猜上电/重启" | +| **已知代价** | ① 启动段无保护 ② `ftpTask` 首期不监视 ③ LSI 精度差需按最坏值设计 | +| **成本** | ≈ 24 B RAM / ≈ 0.7 KB Flash,可忽略 | + +--- + +## 附录 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 缺陷原始登记与建议 | + +## 附录 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) | + +--- + +## 变更记录 + +| 日期 | 变更 | 说明 | +|---|---|---| +| 2026-09-29 | 创建 | 调研 + 设计方案:IWDG + 任务心跳位图;含选型、九项设计决策、验证计划与风险清单 | +| 2026-09-29 | 修正实施路径 | 实测发现 HAL IWDG 驱动源码不在仓库且未编入 MDK 工程 → 改为**直接寄存器操作**(§2.2 路径 C);超时数值按公式核算修正;补 `RCC_CSR`/`IWDG` 寄存器定义的实测行号 | diff --git a/docs/待处理内容.md b/docs/待处理内容.md index c0d1b6a8..8fce18e2 100644 --- a/docs/待处理内容.md +++ b/docs/待处理内容.md @@ -25,3 +25,11 @@ | # | 事项 | 说明 | 出处 | |---|---|---|---| | N-4 | **pending 机制二期:ACCEPT + SEND 阻塞路径 pending 化**——E-06(accept 轮询计时失真)、E-20(lftpd 控制通道 errno 分类/501)、E-21(timeSync errno 分级)剩余分量共用该机制扩展;届时 L3/L5 基线再重锁、探针族扩充(close/reconfig/EOF/disconnect-during-pending)、看门狗接 selftest 留痕、模拟器差距清单 +1 | 一期(RECV)已落地并入主线:`a45c0cb`(+文档闭环 `7ff2958`);架构现状见 `Net层阻塞模型架构设计.md`,二期设计基线与门禁见 `下一步任务规划.md` §3.1 | 同左 + `错误码传播审计.md` 第二档 #2 | + +## 可靠性与自恢复 + +| # | 事项 | 说明 | 出处 | +|---|---|---|---| +| N-03 | **引入独立看门狗(IWDG)+ 任务心跳位图**——现状全库无看门狗(HAL 驱动源码也不在仓库),任何形态死机不可自恢复;另 `configASSERT` 为关中断死循环(连日志都不打)。**设计方案已完成**(含选型、九项决策、验证计划、风险清单),待实施 | **设计见 [`子系统设计/看门狗设计说明.md`](子系统设计/看门狗设计说明.md)**;实施路径为直接寄存器操作(不引入 HAL IWDG 驱动);实施后本文档改写为现状设计说明并销项 | `审查记录/项目审查与优化建议报告.md` N-03a/b/c | +| N-03a | `configASSERT(x)` = `taskDISABLE_INTERRUPTS(); for(;;);`(`Inc/FreeRTOSConfig.h:163`)→ 断言时无任何输出 | 修复方案见 `看门狗设计说明.md` §4.6(改为"先记录后复位");N-03 实施时一并处理 | 同上 N-03b | +| N-03b | 启动段(`app_main_init` + `defaultTask` bring-up)在新方案下**无看门狗保护** | 显式代价:`看门狗设计说明.md` §4.5 决策 C + §9 R1;该段死机表现为"无输出 + LED 不闪" | 同上 N-03c |