新增看门狗设计说明:IWDG 选型与任务心跳位图方案
This commit is contained in:
@@ -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 请求架构、五路完成语义、设计边界与性能特征 |
|
||||
|
||||
@@ -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 中挂载,否则不参与编译。
|
||||
> 这是唯一一处工程文件改动(新增一个 `<File>` 节点),与"删改既有节点"的低风险操作不同。
|
||||
> **替代做法**:若不便改工程文件,也可把实现直接放进 `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=<n> 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` 寄存器定义的实测行号 |
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user