# 看门狗设计说明 > **模块名称**:独立看门狗(IWDG)+ 任务心跳位图(`App/util/task_watchdog.{c,h}`) > **文档定位**:**现状设计说明**——选型依据、当前实现、可观测性与判读方法、验证结论。 > **状态**:✅ 已落地并台架闭环(2026-10-03 V2/V4/V5);未办项集中在 §9。 > **日期**:2026-09-29 创建;**2026-10-10 按当前代码重写正文**(旧正文含大量立项前的 > 描述,已清理;实施史与逐次决策见 §10 与文末变更记录,未删)。 > **相关文档**: > [`运行与自检记录设计说明.md`](运行与自检记录设计说明.md)(复位记录与 `selftest.log`)、 > [`../测试说明/单元测试/看门狗单元测试说明.md`](../测试说明/单元测试/看门狗单元测试说明.md)(主机侧纯逻辑单测,判定状态机)、 > [`../测试说明/板级测试/实机测试实施指南.md`](../测试说明/板级测试/实机测试实施指南.md)(故障注入台架流程)、 > [`../架构设计/项目启动顺序管理.md`](../架构设计/项目启动顺序管理.md)(启动时序与就绪屏障)、 > [`../测试说明/测试工具/ST-LINK_CLI_复位使用说明.md`](../测试说明/测试工具/ST-LINK_CLI_复位使用说明.md)(复位语义)、 > 《项目审查与优化建议报告》(本模块缺口的立项出处;该条已修复并按「修复即销项」从报告删除,**完整记录即本文**;报告本身 2026-10-25 已删除) --- ## 0. 一页速览 | 项 | 当前值 / 落点 | 事实来源 | |---|---|---| | 看门狗外设 | **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`;v1.5 起按位分账,多任务交替缺席各自清零、不再串成复合停喂) | 同上 | | 必需掩码 | 产品构建 **`0x1F7`**(default/net/ftp/ui/timesync/adc/iec104/protect);`ENABLE_HW_LAYER_TESTS` 下 **`0x1F5`**(剔 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 断言(主机侧纯逻辑单测,pre-commit `watchdog-unit-test`) | `test/host/watchdog/` | | 重生成审计 | 21 项 / 1 警告 / 0 失败(含 iwdg 延后启动、看门狗挂载、复位锁存) | `scripts/check_cubemx_survival.py` | --- ## 1. 问题域与目标 ### 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` 无看门狗条目、无复位原因读取——缺陷原登记于《项目审查与优化建议报告》(已修复并按销项删除正文)。**该节只作立项背景**:现状是 IWDG 已启用(`.ioc` + `HAL_IWDG_MODULE_ENABLED` + 驱动已挂载),复位原因每次开机都在上报。 --- ## 2. 选型与参数 ### 2.1 为什么是 IWDG 而不是 WWDG | 维度 | **IWDG**(独立) | **WWDG**(窗口) | |---|---|---| | 时钟源 | **LSI ≈ 32 kHz**(独立 RC,启动后强制开启且不可关) | APB1(42 MHz,依赖 PLL) | | 依赖 CPU 时钟 | ❌ | ✅(PLL 失锁/时钟切换期失效) | | 依赖中断 | ❌ | ✅(早期唤醒中断 EWI) | | 复位标志 | `IWDGRSTF` | `WWDGRSTF` | | 能否停 | **启动后只能靠复位停** | 可关 | | 喂狗窗口 | 只有下限 | 有上下限(过早喂也复位) | **结论:IWDG。** 本项目的故障谱(死锁、饿死、Fault、断言挂死)可能伴随时钟异常或中断被关闭,WWDG 恰在这些场景失效;IWDG 走独立 LSI,是全场景兜底。台架 V4(§7)实测:`__disable_irq()` 后死循环仍每 ~4.9 s 稳定复位,证实独立于中断。 **代价**:LSI 精度差(可在 17–47 kHz 漂移,随温度/电压变化),`.ioc` 按 `RCC.LSI_VALUE=32000` 建模,**必须按最坏情况校验**(§2.2)。 ### 2.2 超时计算 IWDG 为 12 位递减计数器(RM0090 §21.3): ``` T_IWDG = (4 × 2^PR) × RLR / F_LSI ``` | PR | 分频 | RLR | 标称(32 kHz) | **最坏(47 kHz)** | |---|---|---|---|---| | **4** | **64** | **1249** | **2498 ms** | **≈1701 ms** | | 4 | 64 | 2499 | 4998 ms | ≈3403 ms | | 5 | 128 | 1249 | 4996 ms | ≈3402 ms | **LSI 偏快时超时变短**,故按 47 kHz 校验。三个时长必须严格包含: ``` T_任务循环 < 监视窗口(100ms) < 停喂判定(同一任务连续缺席 800ms) < T_IWDG最坏(1701ms) ``` 即:停喂后仍有约 **900 ms** 余量才到复位;实测 V2 从声明 stall 到复位 ≤2.5 s。 ⚠ v1.5(2026-10-25)起本链**字面成立**:旧实现的停喂从第一个缺席窗就开始(全掩码 精确匹配是喂狗前置),800ms 只是申报时点;现在停喂时点 = 同一位第 8 连击窗,此前 1..7 窗照常喂狗。代价 = 真停摆从挂死到复位总时长 +约 0.7 s(最坏 ≈2.5 s / 标称 ≈3.3 s,V2 判据「声明后 ≤2.5 s」仍成立);收益 = 「本窗缺 A、下窗缺 B」式互不相干 抖动不再累计成停喂(2026-10-25 大窗 16:24/16:28 两起误升级实证,判据改写动因)。 ### 2.3 配置落点 | 层 | 落点 | 内容 | |---|---|---| | **写进 `.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` 检查无收益 | IWDG 是"写键值"式外设(KR 只写,读回恒 0): | 键值 | 用途 | |---|---| | `0x00005555` | 解锁 `PR`/`RLR` 写保护 | | `0x0000CCCC` | 启动(此后不可停,只能靠复位) | | `0x0000AAAA` | 重装计数器(喂狗) | ⚠ `PR`/`RLR` 每只写完后须等 `SR.PVU`/`SR.RVU` 清零(同步到 LSI 域,≤5 个 LSI 周期),**否则后写的那只会被静默丢弃**(2026-10-02 SWD 实测)。 ### 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 实测)。 **现做法(搬进 USER CODE 区,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()` 拆开/延后",必须逐一核对被拆掉的函数还有哪些副作用没搬到新位置。** --- ## 3. 架构:心跳位图 + 单一喂狗者 ### 3.1 为什么不能"每个任务各自喂狗" `HAL_IWDG_Refresh()` 塞进每个任务循环 → **只要还有任何一个任务在跑,狗就被喂**,单任务卡死永远检测不到,退化成"只检测全部任务同时挂死"(现实中几乎不发生)。 ### 3.2 采用方案:任务置位 → 校验者判定 → 校验者喂狗 ![看门狗心跳模型:任务置位 → 校验者判定 → 校验者喂狗](../images/看门狗心跳模型.svg) > 图源:`tools/arch_diagram/gen_watchdog_svg.py`(改图改脚本重跑,见 > [`../架构设计/图形绘制流程.md`](../架构设计/图形绘制流程.md))。 **语义**:喂狗 = "所有被监视的任务都在窗口内动过"。 | 阶段 | 代码(`App/util/task_watchdog.c`) | |---|---| | ① 任务置位 | `task_wd_beat(id)`:`s_hb_bits \|= (1u << id)`(仅置位) | | ② 校验者取走清零 | `snapshot = s_hb_bits; s_hb_bits = 0u;`(单字原子) | | ③ 按位分账(v1.5) | 对掩码内每一位:出现→该位连击清零;缺席→+1(饱和封顶)。掩码外的位不入账(监视范围随构建变化时账本自动跟随) | | ④ 全中窗 | `snapshot == wd_required_mask()`(**精确相等**,§3.5)→ `s_miss_streak=0; s_stall_evidenced=0;` 喂狗 | | ⑤ 缺席窗(v1.5) | `s_miss_total++`;复合边沿(上一窗全中)打一条 `MISS start`(**纯诊断**,不再代表停喂);**任一必需位连击 ≥8** → `[WD] task stall` + **停喂**(每 episode 只落一次盘上证据);所有位连击 <8 → **照常喂狗** | ### 3.3 为什么用"取走并清零" 单字读-改-写在 Cortex-M4 上天然原子(无锁)。对比: | 方案 | 问题 | |---|---| | 每任务独立计数器,比较"是否增长" | 需临界区,32 位回绕判定繁琐 | | `osEventFlags` 做心跳 | 需 6 位;`osEventFlagsSet` 可能触发调度,任务循环内代价过高 | | **单字位图 + 取走清零** | ✅ 零临界区、单指令、一个静态字 | 语义优势:校验者看到的位图 = "自上次抽样以来谁动过" → 缺席可连续累计 → 可判"连续 N 轮未达标"。 ### 3.4 为什么喂狗者在 defaultTask | 候选 | 评估 | |---|---| | **defaultTask** | ✅ 已是"系统管家"(配置引擎属主、syslog 唯一写者、控制台唯一发送者);50 ms 循环基准天然适配 100 ms 抽样 | | netTask | ❌ 节拍最紧(`osDelay(1)`)且是最可能卡死的任务之一——让被监视者喂狗语义错乱 | | 独立看门狗任务 | ❌ 新建任务(栈 + TCB)与 RAM 预算冲突;且"谁来监视监视者"无解 | | 中断 / FreeRTOS timer | ❌ 其存活不代表业务健康 | > ⚠ **对抗性风险即期望行为**:defaultTask 自身卡死 → 狗永不喂 → 必然复位(fail-safe)。 > 但"喂狗点被长 I/O 阻塞"这一形态需要单独处置,见 §4.3。 ### 3.5 掩码与"精确匹配"语义 抽样判定是 **`snapshot == required`**,不是 `(snapshot & required) == required`。因此**掩码必须恰好等于"会置位的任务集合"**——多一个位同样判失败。 | 构建 | 掩码 | 会置位的任务 | |---|---|---| | 产品 | `0x77` = bit0,1,2,4,5,6 | default / net / **ftp** / ui / timesync / **adc** | | `ENABLE_HW_LAYER_TESTS`(CH395F 硬件层测试,netTask 入口自挂起) | `0x75` = bit0,2,4,5,6 | default / ftp / ui / timesync / **adc**(屏障等待按 100 ms 分片喂位;adcTask 不依赖任何屏障,任何构建都置位) | | 测试套件(`TEST_SUITE_*`,创建 testTask) | 同上 `0x77` | **testTask 任何构建都不入掩码、也不置位**(它可长阻塞;置位反而引入多余位误杀) | > 2026-10-02 台架:旧版"仅剔 NET 位 + 屏障等待者不置位"会让测试构建陷入 IWDG 复位循环——**掩码与会置位集合必须成对维护**,改任一处都要同步。 --- ## 4. 关键设计决策(现状) ### 4.1 启动时刻:defaultTask bring-up 完成后 `task_wd_start()` 位于 `default_task.c:175`——存储深链(`f_mount`/`f_mkfs`/卷探针/syslog 挂载/配置加载)之后,即全系统最长初始化链完成、系统进入稳态之时。启动前先喂一次,使首个抽样窗口从启动时刻起算。 **显式代价**:从复位到此处约数秒的窗口**无看门狗保护**;该段死机表现为"无任何输出 + LED 不闪",可人工辨识(§9 R1)。 ### 4.2 停喂,而不是直接 `NVIC_SystemReset()` 软复位会把 `RCC_CSR` 置 `SFTRSTF`,丢失"是看门狗抓到的"这一权威归因。让 IWDG 自然溢出 → `IWDGRSTF`。代价是额外 ≤2.5 s 延迟(可接受)。 ### 4.3 长耗时窗口的三类处置 统一原则:**启动段不启动狗;主循环段用"分片心跳 / 心跳路由 / 直接喂狗"三类手段覆盖**; 另有一类**根本不等锁**的场景(卷锁 try 失败)不属于"等待怎么活下来",它只需要 **可见**,用表中 ④「只计数」那一条(道G task-17,它不喂位也不需要喂位)。 | 场景 | 位置 | 时长 | 处置手段 | |---|---|---|---| | 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()` | | 卷锁 try 失败(defaultTask / uiTask **从不等锁**,取不到就跳过本轮) | `App/fs/vol_lock.c` 观测计数 | 不定 | **④ 只计数、不在失败分支留日志**:锁层记 5 态(`acquired` / `try_timeout` / `blk_timeout` / `failopen`,16 B 静态),读口 `vol_lock_get_stats()` **不取锁**、任意上下文可调。为什么不许在 try 失败处补一条日志——唯一能把事写下来的通道(`/log/wdev.log`、`run.log`)自己也要取这把锁,而卷锁**非递归**,被卷忙挡住时既写不出还会自取死锁;一次性留痕交给 syslog 唯一写者按差值 latch 补投(道G task-17,接口口径见 `App/fs/vol_lock.h`) | | 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` 刻意不登记(不在掩码内)。 **③ 直接喂狗**的语义边界(显式声明):NAND 真实卡死时该路径持续喂狗 = **设备存活但存储功能冻结**(与控制台 ORE 自愈同类边界,见 §9 R5)。 ### 4.4 断言与崩溃钩子 `Inc/FreeRTOSConfig.h:165`(搬进 `USER CODE BEGIN 1` 区): ```c #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 后强制复位**(双保险)。 **2026-10-10 栈整改**:钩子里的 `name[49]`/`num[16]` 改**函数级 static**(终点路径、打印完即复位、无并发),行号转换改原地反转,去掉 `HAL_Delay(200)`(`con_tx_bounded` 已等 TC)。原因:`configASSERT` 在 FreeRTOS 内部可达 → 本钩子挂在**每个任务**的最坏调用链尾,放栈上的 81 B 会出现在所有任务的静态最坏链上。 4 个 Fault(Hard/Mem/Bus/Usage)的采集与使能(`fault_handler_enable()` 使能后三者,否则被升级成 HardFault 看不出根因)见 §10.2 与 `AGENTS.md`「平台约束」。 **`Error_Handler()` 统一降级(2026-10-12)**:CubeMX 弱定义的这个入口旧形态是 `__disable_irq(); while(1){}`——**单处外设初始化失败即整机挂死**,只剩 IWDG 兜底且不留任何自检日志。现改为打 1 条自检 + `NVIC_SystemReset()`,与 4 个 Fault 处理同款自恢复(IWDG 退回"最后兜底"而非唯一出路;复位归因 `SFTRSTF` → 开机日志 `software (NVIC_SystemReset)`,§4.5)。三条实现约束: | 约束 | 原因 | |---|---| | **不得 `__disable_irq()`** | 日志必须能走串口,关中断 = 现场彻底消失 | | **`__builtin_return_address(0)` 不可用** | AC5 前端接受但编成库函数调用,MicroLIB 无该符号 → `L6218E`(2026-10-12 台架实证,勿再尝试);ARMCC `__asm` 只有整函数形态且取不到 C 符号(§10.4)。→ **失败点靠"最后一条成功日志"与 `main()` 的 `MX_*_Init` 顺序之间定位**(GPIO→DMA→USART1→SPI2→SPI1→I2C1→UART4→UART5→USART2→USART3→IWDG→ADC1→TIM3→`app_main_init`) | | **日志路径必须内核前安全** | 调用点全在 `osKernelStart` 之前:控制台写者未注册 → `con_append` 走 `dbg_console_write_raw`(DWT/计数有界轮询,`huart1` 未就绪即返回),`sys_clock_get_str` 未初始化打占位串——全程无内核 API(§4.7 与 `AGENTS.md`「平台约束」同源) | 成本 = 仅这 1 条日志(≈90 B Flash,见 `AGENTS.md` 日志成本)。重生成存活:函数体在 `USER CODE Error_Handler_Debug` 区(`KeepUserCode` 逐字保留)+ `check_cubemx_survival.py` 审计兜底。 ### 4.5 复位原因锁存与优先级 `RCC_CSR` 的复位标志只由 POR 清除、读后不自动清 → 必须读并锁存后 `SET_BIT(RCC->CSR, RCC_CSR_RMVF)`,否则下次软复位会读回上次残留。 现状:`Src/main.c` 在 `HAL_Init()` 之后(`USER CODE BEGIN Init` 区)锁存到 `g_rcc_csr_latched` 并清标志;`app_main_init()` 早期翻译并打印。 **代码实际优先级**(`app_main.c reset_cause_str()`,按"最可信归因"而非因果强度): | 顺序 | 标志 | 描述串 | |---|---|---| | 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` | 看门狗排在软件复位**之前**:两者同时置位时(崩溃钩子软复位 + 期间也超时),报告看门狗更有价值。 ### 4.6 与 `run.log` / `selftest.log` 的集成 | 通道 | 现状 | |---|---| | 串口 | 每次开机第一行区打 `[APP] reset cause: <类型> (CSR=0x…)`(`/serial-boot-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 重建,防长跑无界增长) | ### 4.7 模拟器与主机侧 - **模拟器不编译本模块**(`simulator/` 无 `task_watchdog.c`,也无 `--watchdogtest`)——原计划的模拟器侧看门狗自检已降级,判定逻辑改由**主机侧纯逻辑单测**覆盖(39 断言,见 §7)。 - 不可建模的部分:真实复位、LSI 漂移、`RCC_CSR` 标志——必须真机(§7)。 --- ## 5. 实现现状 ### 5.1 文件与职责 | 文件 | 职责 | |---|---| | `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` 改造(USER CODE 区) | | `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` 取证槽 | ### 5.2 对外接口 ```c 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 已启动 */ /* 取证(仅 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 */ ``` 任务 ID(位序号即掩码位,**顺序不可插空**):`DEFAULT=0 / NET=1 / FTP=2 / TEST=3 / UI=4 / TIMESYNC=5`。 本模块**不创建任何 FreeRTOS/CMSIS-RTOS 对象**,可安全用于内核启动前后的上下文(唯一例外:`task_wd_bind()`/`nand_ftl_wd_beat()` 需内核句柄,内核前为空操作)。 ### 5.3 判定状态机要点 ``` 未启动 → 直接返回 未到 100ms 边界 → 直接返回(节流) 取走并清零位图 → 记入近 8 次环形历史(取证) 按位分账:掩码内每位 出现→连击清零 / 缺席→+1(饱和);worst = 各位最大连击 snapshot == required → miss_streak=0、证据门闩复位、喂狗 否则 → miss_total++;复合边沿(miss_streak==0)→ 裸 UART `MISS start …`(诊断行) worst >= 8 → DBG_ERROR task stall +(本 episode 首次)落盘证据 + 不喂狗 worst < 8 → **照常喂狗**(单任务亚容忍缺席不牵连全局停喂) ``` ⚠ 2026-10-25 大窗判读前,后两行按复合 `miss_streak >= 8` 判定:互不相干的缺席 串成停喂链(变更记录同日行);按位分账后 `s_miss_streak` 退化为纯诊断边沿计数。 ⚠ **证据门闩 `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 平台后端(可测性) 硬件访问收敛到三个 `__weak` 钩子(声明在 `task_watchdog.h` 末段,**非对外接口**): | 钩子 | MCU 默认实现 | 主机侧 | |---|---|---| | `wd_platform_feed()` | `IWDG->KR = 0xAAAA` + 记录喂狗时刻/最大间隔 | 强符号桩:记录"何时被喂" | | `wd_platform_start()` | 解锁 → 写 PR/RLR(等 PVU/RVU)→ `0xCCCC` | 强符号桩:不碰寄存器 | | `wd_platform_tick_ms()` | `HAL_GetTick()`(时基 = TIM7,非 SysTick) | 强符号桩:驱动时间推进 | 与 `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()` | | adcTask | 主循环每 1 ms 节拍 `task_wd_beat(TASK_WD_ADC)`;**不做 NAND I/O → 不登记 FTL 心跳路由**(无需 `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 态丢失、串口又可能来不及打,故有三层"复位不丢"的取证(全部只取证,不参与判定): | 层 | 载体 | 开机读出并打印 | |---|---|---| | 第 7 轮 | RAM(近 8 次抽样位图环、最大喂狗间隔、console tx_fault 计数) | 停喂时 `STALL snap=… req=… h=… mg=… cf=… seg=…`(裸 UART + `/log/wdev.log` 各一份) | | 第 8 轮 | `.noinit` 槽 `s_wd_seg_trail[4]`(magic/段号/时刻/循环序号) | `PREVRUN seg=N seq=N t=ms`——直接指向 defaultTask 主循环最后进段 | | 第 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.1 串口标记 | 标记 | 时机 | 含义 | |---|---|---| | `[APP] reset cause: <类型> (CSR=0x%08X)` | `app_main_init` 早期 | 复位原因 + `RCC_CSR` 原值(`/serial-boot-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 已启动(真机自检必查标记) | | `MISS start snap=… req=… t=…` | **复合**缺席 episode 首轮(裸 UART) | poller 仍在抽样,给出首次缺席位图。⚠ v1.5 起为**纯诊断行**:出现不代表停喂(停喂只看同一必需位第 8 连击窗) | | `[WD] task stall: mask=… req=… miss=…` | **同一必需位**连续 8 窗缺席起,每窗一条(v1.5 按位分账;旧复合判据见变更记录) | 该位对应任务 800 ms 没跑过 → 停喂 → 最坏 1.7 s 后 IWDG 复位;`mask` 的 0 位 = 缺席任务(多个 0 位且**跨窗换位** = 抖动串联嫌疑,v1.5 后不再触发本行) | | `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 …` | 内核钩子 | 栈溢出(复位)/ 堆耗尽(仅告警) | > **与 `/serial-boot-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 字符串陷阱)。 ### 6.2 判读顺序(现场"设备反复重启") 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. 验证记录 | # | 场景 | 结果 | 证据 / 备注 | |---|---|---|---| | V1 | 正常启动、长期不复位 | ✅ 常态成立 | 每次 `/serial-boot-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(探针场景专属)→ 登记「探针窗口心跳缺口」 | | V6 | 测试套件共存 | 🟡 部分 | 2026-10-02 曾出现测试构建 IWDG 复位循环,已按 §3.5 掩码规则修复;未跑完整套件用例矩阵 | | V7 | 上电复位辨识 | 🟡 未单独注入 | `reset cause:` 每次开机都在打(POR 串已实现),未做专门断电对照 | | V8 | 引脚复位辨识 | ⏳ 未做 | 未做定向注入(`external pin (NRST)` 分支已实现) | | 主机单测 | 判定状态机主机侧单测 | ✅ 39/39 | `test/host/watchdog/run_unit_test.py`(主机侧纯逻辑单测,pre-commit `watchdog-unit-test`) | | 门禁触发 | 门禁接入 | ✅ | 改 `App/util/task_watchdog*.{c,h}` 或 `test/host/watchdog/**` 触发 | | 模拟器 | `--watchdogtest` | ⏳ **已降级不做** | 位图与判定逻辑已由主机单测覆盖;模拟器不编译本模块 | 用例细节见 [`../测试说明/单元测试/看门狗单元测试说明.md`](../测试说明/单元测试/看门狗单元测试说明.md);故障注入流程见 [`../测试说明/板级测试/实机测试实施指南.md`](../测试说明/板级测试/实机测试实施指南.md) §4。 --- ## 8. 资源预算(`.map` 实测,2026-10-10) | 项 | 大小 | 位置 | |---|---|---| | `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` | 全局水位(`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)在其中可忽略。 > 与"走裸寄存器不引入 HAL 驱动"的旧估算不同:现走 CubeMX 路径,`stm32f4xx_hal_iwdg.c` **已编译进工程**,但真正被链接的只有 `MX_IWDG_Init` 填结构体那 28 B(§2.4 的宏改名把 94 B 的 `HAL_IWDG_Init` 移除)。 --- ## 9. 边界与未办项 ### 9.1 已知边界(刻意接受) | # | 边界 | 现状与处置 | |---|---|---| | R1 | **启动段无保护**(复位 → `task_wd_start()` 约数秒) | 接受;表现为"无输出 + LED 不闪",人工辨识。要覆盖需"短超时 + 手动多喂",复杂度收益比不划算 | | R2 | **LSI 精度差** | 已按 47 kHz 最坏值校验,停喂到复位仍留 ~900 ms | | R3 | **喂狗节奏饥饿** | 控制台 drain 曾把主循环拉长导致误复位(2026-10-02 台架日志代次 288)。现缓解 = drain 单次 250 ms 预算 + 发送故障 300 ms 冷却;**结构根治(USART1 改只发不收)2026-10-26 已裁定不做 → 现行缓解即长期方案**(机制详述 `控制台单写者日志设计说明.md` §12) | | R4 | **NAND 真实卡死时 FTL 直接喂狗** | 设备存活但存储功能冻结(与 ORE 自愈同类边界)。若要求"NAND 死也必须复位",需给 `nand_ftl_wd_feed()` 加次数/时限上限 | | R5 | **栈溢出与断言在复位原因上不可区分**(都是 `SFTRSTF`) | 可接受;串口分别打 `[ERR] STACK OVERFLOW` / `[ERR] ASSERT FAILED`,`selftest.log` 侧另有记录 | | R6 | **`RCC_CSR` 读后即清** | 已在 `main.c` 统一处理;漏清会让下次软复位读回上次标志 | | R7 | **UI 推流的网络往返卡死不占心跳位**(产品掩码仍 `0x77`) | 刻意:往返卡死的根因在 netTask,netTask 已在掩码内、由它暴露;为"远端显示卡了"新增一位只会把观感问题升级成整机复位。UI 心跳已由自身喂位自产、不受推流影响;推流耗时看 `[PROF]` 的 `uis`/`uisn`。成因与判读见本文 **§11.3 第 2 条**;门控方案与残留暴露见 `../UI设计/RemoDispBus协议与接收设计说明.md` §3 | ### 9.2 未办项(登记簿同步) | 条目 | 内容 | 与本案关系 | |---|---|---| | 探针窗口心跳缺口 | `time_sync` 30 s 挂起客户端掉线 + 监听重建期间 NET/FTP/UI 心跳缺口 800 ms~1 s(声明 stall 但未复位),**余量仅 ~0.7 s** | 待定位监听重建为何阻塞其它任务;必要时缩短重试 | | O1 | 是否需要复位计数与"连续 N 次看门狗复位后进入安全模式" | 当前不做;若现场出现复位风暴再评估 | | O2 | 细粒度复位原因是否上 `run.log`/UI | 倾向不上(避免中文字符集重生成),只保留串口 + `selftest.log` | --- ## 10. CubeMX 重生成下的定制存活(方法论) 看门狗是本项目**第一次**把 CubeMX 生成流程引入可靠性关键路径,由此沉淀出与看门狗本身无关的通用方法论。`AGENTS.md`「平台约束」指向本节,改动 `.ioc`/`Src/`/`Inc/` 前请先读。 > **2026-10-26(道G task-26)一句话登记,机制不在本文复述**:四个 Fault 与栈溢出 / 断言钩子的**崩溃上报发送口已归一**进控制台单写者(`dbg_console_write_raw` + 一次整行独占,不再自行调用 `HAL_UART_Transmit`),机制正文与残余见 `docs/子系统设计/控制台单写者日志设计说明.md` §4.4.1。本节只关心一件事:这些实现仍须整体待在 `Src/freertos.c` 的 `USER CODE BEGIN 4/5` 区与 `Src/stm32f4xx_it.c` 的 `USER CODE BEGIN 0` 区(§10.2 的「搬进 USER CODE 区」条),改完跑 `scripts/check_cubemx_survival.py`(§10.3)。同族另一笔只登记指针:断言钩子与临界区在控制台路径上互达会构成**幻影环**,armlink 的 `In Cycle` 记账随环形状波动(同一 port.o 三棵树实测每链差 0~80 B,休眠≠解耦),三树取证、量化表与「configASSERT 裁空宏 ≠ 优化收益」的钉死句见 `docs/内存管理/RAM使用分析与优化方案.md` §4.0.7(道G task-38)。 ### 10.1 覆盖史(三次重生成) | 次 | 触发 | 被覆盖的定制 | 结论 | |---|---|---|---| | 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` 三处栈注释第四次丢失 | 加固宏改走 **写进 `.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` 区 | > ⚠ **别凭印象断言"界面里没有该项"**——先翻标签页或查 `db/mcu/IP/*_Modes.xml`:那两个加固宏一直躺在 "Config parameters" 里,因此白丢了两次。 ### 10.2 三层手段(优先级从高到低) | 层 | 手段 | 适用 | 可靠性 | |---|---|---|---| | **写进 `.ioc`** | 让 CubeMX 自己生成 | 界面能表达的配置 | ⭐⭐⭐ 根治——重生成只会照写 | | **搬进 `USER CODE` 区** | 移到 `USER CODE BEGIN/END` 之间 | 有真实标记区(**当前模板生成的**)的文件 | ⭐⭐ 有效(`KeepUserCode=true` 时逐字保留) | | **审计 + 基线** | 脚本定期核对 | 标记区外的存量 | ⭐ 兜底——不防丢,但丢了一定知道 | **本项目已写进 `.ioc` 的项**:任务栈/优先级/名称(`FREERTOS.Tasks01`,**改栈请改 `.ioc`,不要改 `freertos.c`**)、队列 `netMsgQueue`、事件组 `sys_evt`、堆 `configTOTAL_HEAP_SIZE`、FPU、两个加固宏、**IWDG 分频/重载**、时钟树(含 LSI)、`KeepUserCode=true`(必须为 true,否则 USER CODE 区这一层整体失效)。 **生成区内那句"必须去掉"的调用 → 用宏改名升级为「搬进 USER CODE 区」**(2026-10-09 定,本案两处): - `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`、要么搬进 `USER CODE` 区——**重生成应当零 diff**。剩余 1 项 WARN 是 `Src/freertos.c` 的 `timeSyncTask` 手工定义(属性块在 `USER CODE BEGIN Variables` 区,本身受保护,WARN 只提示"它不在 `.ioc` 里")。**`adcTask`(B1,2026-10-10)同为手工定义**——属性块在同区、`osThreadNew` 在 `USER CODE BEGIN RTOS_THREADS` 区,同样受保护、同样不在 `.ioc` 里(B1 原计划走「写进 `.ioc`」,实际按 timeSyncTask 先例落「搬进 USER CODE 区」:避免为单任务触发全工程重生成)。 **为什么 `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` **不代表已使能**(只是向量可见)。现走 USER CODE 区(`USER CODE BEGIN 1`)+ 审计项兜底。 ### 10.3 审计脚本 ```bash python scripts/check_cubemx_survival.py # 审计报告(人读) python scripts/check_cubemx_survival.py --write # 确认改动无误后重建基线 ``` - **清单 21 项**覆盖 P0/P1 全部防线(iwdg 延后启动、4 个 Fault Handler 采集、故障使能、两个加固宏、断言改造 ×2、看门狗模块与工程挂载、复位原因锁存、HAL 驱动与使能宏、`.ioc` 关键项、钩子落点三项)。 - **漂移检测**:各项内容哈希存 `.cubemx_survival_baseline.json`;有意改动确认后 `--write` 重建。 - ⚠ `MDK-ARM/*.uvprojx` **排除在漂移检测之外**(`no_baseline=True`)——该文件每次 `/keil-build` 后被 UV4 改写(XML 归一化 + `PackID` 降级),参与检测会每次构建后都被拦;只保留"源文件仍挂载"断言。 - **注释过滤**:断言前剥掉 C 注释,故 `protected` 项的 `must_contain` **只能断言代码符号**(锚点注释会被剥掉,断言它等于没断言)。 - 已挂 pre-commit `cubemx-survival`(改 `.ioc`/`Src/`/`Inc/`/`task_watchdog`/`.uvprojx` 触发)。 > **工作流**:每次 CubeMX 重生成后**立刻**跑一次该脚本——它直接列出"被覆盖了哪几处 + 修复提示",比事后靠故障现象反推快得多。 ### 10.4 ARMCC `__asm` 的三条语法约束(2026-09-29 踩坑) > **出处**:自 `AGENTS.md`「ARMCC 内联汇编语法」条目迁入(AGENTS.md 瘦身,不再当事实存放地)。4 个 Fault Handler 是手工 `__asm`(§10.2 的「搬进 USER CODE 区」条);ARMCC 的内联汇编只有一种合法形态,以下三种写法全部实测踩坑: | # | 尝试写法 | ARMCC 实际报错 | 根因 | |---|---|---|---| | ① | `__asm volatile ("MRS %0, lr" : "=r" (x))` | `#18: expected a ")"` | 这是 **GCC 内联汇编语法**,ARMCC 不支持 | | ② | `__asm { MRS x, lr }`(块内联) | `#20: identifier "lr" is undefined` | 块内联把寄存器名当 **C 标识符**解析 | | ③ | `__asm` 块内 `ADR r1, xxx_tag` 取 C 侧字符串/表格 | `A1516E: Bad symbol 'xxx_tag'` | `__asm` 块里**取不到 C 的数据符号**;去掉 `static` 也无效——汇编器只认自己作用域内的符号 | **结论 / 可用写法**:整个函数写成 `__asm void f(void) { ... }`;要传"字符串/表格"这类 C 侧数据,就让 `__asm` 只取**寄存器参数**然后 `B` 到一个普通 C 函数、由 C 侧补上——所以 4 个 Fault 的故障名必须由 C 侧 per-fault 入口 `fault_entry_{hard,mem,bus,usage}` 带上,`__asm` 里不取 C 变量。 **覆盖 CubeMX 生成的同名函数**(对 §10.2 那条的补充):用 `#define f f_Gen`(**写在生成实体之前**)把生成实体改名,链接器会把未被引用的 `f_Gen` 自动移除(`.map` 可见 `Removing ... (N bytes)`)——**零代码开销且不删生成区任何一行**。 --- ## 11. 任务级饥饿的诊断方法与实测基线(2026-10-10 结案沉淀) > **来源**:`[ERR][WD] task stall: mask=0x00000067 req=0x00000077`(缺 bit4 = `TASK_WD_UI`) > 的完整归因与处置。故障本身已结案——**累计 38 次上板启动 0 复现**(主机侧回归的早期基线 3/10), > 原始分析文档按登记簿"完成即销项"规则删除。本节只留**可复用的判读口径、诊断方法、 > 实测基线与教训**;ADC 侧的处置细节在 [`ADC采样节拍设计说明.md`](ADC采样节拍设计说明.md) §6 与 §9, > UI 推流门控在 `../UI设计/RemoDispBus协议与接收设计说明.md` §3,就绪门控 API 在 > `../网络/BSD_Socket_API_使用指南.md` §10.1。 ### 11.1 现场日志的三条判读口径(都被误读过) | 现象 | 正确读法 | |---|---| | 启动期出现 `MISS start snap=0x67 req=0x77` | **良性 episode**:`[WD] armed` 在 bring-up 完成后武装,而 `required_mask` 静态含 UI 位 → UI 首跳之前必现一次,随后自行恢复。**不是故障判据**,只作"UI 尚未起跳"的旁证 | | `miss=11` / `miss=12` | `miss=` 是**累计**缺席次数(`s_miss_total`),**不是连续次数**;连续次数自 v1.5 起按位分账(`s_miss_bits[]`,各饱和在 `WD_MISS_TOLERANCE=8`;旧全局 `s_miss_streak` 退化为复合边沿计数)。缺口要用 `t=`(毫秒时基)差算,本例恰为 800 ms | | `reset cause: software (NVIC_SystemReset)` | **不能**用来区分"IWDG 溢出"与"台架拉 NRST / 烧录工具软件复位"——本台架**健康**启动同样报这一串。停摆的权威证据是 `[ERR][WD] task stall` 行本身 | ### 11.2 诊断四步:先量化,再归因 1. **看掩码缺哪几位。** 只缺**一位** = 定点打击(该任务独有的依赖或优先级处境); **多位同时缺** = 全局饥饿 / CPU 饱和。本例 `0x67` 只缺 UI,其余五个 Normal 任务心跳 全正常 → **当场否掉"计算量把 CPU 吃满"这个直觉**(用户假设方向对、结论不成立)。 2. **把嫌疑任务的单拍成本逐项算出来**(不要估)。当时算式:ADC1 一轮 9 通道 ≈67 µs、 TPAFE 快照 + 9 路取数 ≈10~20 µs、压环 + 5 实例傅氏 ≈10~15 µs → 单拍 0.10~0.13 ms = **占空比 10%~13%**,距饱和很远。 3. **用计数速率反证"有没有空转"。** `ring` 在两次心跳间严格 +10000/10 s = **恰好 1000 次/秒**; 若该任务失去阻塞(空转),按 0.11 ms 单拍应为 ≈9000 次/秒 → 反证它每拍都睡满。 4. **用 `[PROF]` 指认最大非让出占用**(§11.4)。实测最大是 `defaultTask` 控制台排空 ≈46 ms,**没有任何任务接近 800 ms** → 800 ms 属**异常长尾**,只能靠复现现场的快照抓, 不能靠健康剖面指认;⚠ 别把"健康剖面里没有"当成"不存在"。 ### 11.3 三条结构性脆弱点(当时全部从代码证实) | # | 脆弱点 | 现状 | |---|---|---| | 1 | **uiTask 是 `required_mask` 里唯一的 `osPriorityBelowNormal`**(其余五个全 `Normal`)。FreeRTOS 数值越大优先级越高 → 任一 Normal 任务持续就绪,uiTask 就**完全拿不到 CPU**;`configUSE_TIME_SLICING=1` 只保护**同优先级**公平 | **至今未变**(刻意不提优先级,理由见 §11.6)。风险由"心跳自产 + 往返门控 + 采集零忙等"三面压住 | | 2 | **UI 心跳曾依赖 netTask 往返**:`ui_net_sink_poll()` → `net_accept()` → `osMessageQueuePut(osWaitForever)` + `xTaskNotifyWait(portMAX_DELAY)`,**之后**才 `task_wd_beat(TASK_WD_UI)` → netTask 慢一拍、UI 心跳晚一拍。这正是"**只有 UI 位缺失**"的根因(别的任务没这条依赖) | ✅ 已断两处:心跳提到 poll **之前**(`App/task/ui_task.c`);无客户端时门控让往返 = **0 次**(`uis` 实测 1 µs) | | 3 | **自定截止期一旦错过就不可恢复**:`next += 1; osDelayUntil(next)` 且**丢弃返回值** → 单拍 >1 ms 后 `next` 相对当前时刻永久落后 → 该任务从此不再阻塞、以 Normal 优先级空转(**连续**慢拍才锁死,单次慢拍会自己追平)。返回值本是现成的重同步钩子 | ✅ 已消:adcTask 改**事件驱动唤醒**(`osThreadFlagsWait`),"落后"这个概念不存在了(`ADC采样节拍设计说明.md` §6) | ⚠ 顺带揪出的两个**独立正确性**缺陷(与停摆无关,但同源发现,别只当性能问题): - **无新帧仍压环**:`r == 0`(TPAFE 无新帧)时也压帧 → 上一拍的重复值进 20 点 DFT 窗, 等效采样间隔被拉长。当时 `idle` 占 3.2% → 窗长从 20.0 ms 漂到 ≈20.6 ms ≈ **3% 频率失配** → 幅值/相位**系统性误差**(不是随机噪声)。现仅 `r == 1` 压帧,上板印证 `ring == frames`。 - **失败拍静默沿用上一拍值**:失败支路不清 `s_stm32_valid` → 照样压环、`skip` 抓不到。 现失败即清标志走整帧跳过(`ADC采样节拍设计说明.md` §4.4;单测 TC-ADCIN-070..078 钉死)。 ### 11.4 `[PROF]` / `[PROF2]` 字段语义与当前基线 探针本体见 `App/util/task_prof.h`(DWT `CYCCNT`,168 周期 = 1 µs;每 ID 记 max/count/last, enter/exit 段还能读出"此刻仍在段内已多久"——**永久阻塞的段永不结算,只有这个字段能证明**)。 | 字段 | 探针 | 含义 | 基线(2026-10-10 v2.8,无客户端,开机 29 s) | |---|---|---|---| | `uip` / `uil` | UI_PERIOD | uiTask 循环周期最坏 / 最近值 | **61.3 ms** / 10.0 ms——⚠ 地板是 **LVGL 整帧渲染**(独立实测帧周期 61.6 ms),**与网络无关** | | `uiw` | UI_WORK | 本轮起点→心跳前(含 LVGL) | **11.4 ms**(改前 ≈50 ms 且 ≈`uis` → 当时单轮几乎全是推流) | | `uis` / `uisn` | UI_SINK | 推流往返最坏 / 此刻仍在段内 | **1 µs** / 0(改前 ≈50 ms);**`uisn` 持续非 0 = "客户端在连且 netTask 拥塞"这一格真发生的铁证** | | `adcm` | ADC_BEAT | adcTask 单拍(含心跳日志) | 2.0~2.25 ms——⚠ 峰值来自**每 10 s 一次的心跳日志**,不是采集,勿混读 | | `adcp` | ADC_POLL | `adc_input_poll()` 单次 | **8 µs**(DMA 前 122 µs;超时拍曾 2.2 ms) | | `defd` / `defs` | DEF_DRAIN / DEF_SYSLOG | 控制台排空 / syslog 落盘 | **46 ms** / 7 µs——**当前最大的单一非让出占用** | | `netp` / `netn` | NET_POLL | `net_poll()` 最坏 / 在段内 | **0 µs** / 0——⚠ **0 是真实值**:空闲时 `net_poll()` 开头读 INT# GPIO 为高即 `break`,整条就是一次 GPIO 读,别判成"探针坏了" | | `gl` | glitch | DWT 丢样 / 计数器重置计数 | 0(非 0 时以上数据不可信) | **判读口诀**:`uip` 大而 `uiw`/`uis` 小 → **被调度饿死**;`uis` 大 → **阻塞在网络往返**; `uiw` 大而 `uis` 小 → **自身渲染拖长**。三种机理在日志上必须能分开,否则归因只能靠猜。 ### 11.5 三条教训(同类问题会复发) 1. **门控类优化必须保留低频兜底。** `readable` 只由 RECV_OK 中断置位,而 `net_reconcile_tick()` 的三类对账**不含**"芯片 RX 非空但 `readable=0`" → 中断一旦丢失,纯门控会把已到达的数据 **永久藏住**。现每 32 轮无条件 recv 一次。**拿"自愈能力"换性能是隐性负债。** 2. **门控会顺手消灭暴露路径。** 加了 accept 门控后,监听控制块消失时 `net_can_accept()` 恒返 0 → 改前"每轮 accept 撞 BADF"这条**暴露**随之消失 → 端口永久不再接受新客户端而无声。 现补 `net_get_sock(g_listen) == NULL` 判定。**加"跳过"逻辑时要问:原来哪条错误是靠这次调用暴露出来的?** 3. **把"定时轮询"换成"事件驱动"时,必须单独核对节拍源启动时刻 vs 消费者创建时刻** (本仓 TIM7 分支 2026-10-02 的实测坑同款)——否则第一次上电就踩"ISR 对还不存在的消费者动手"。 ### 11.6 明确不做的四条(理由要留着,别被"顺手优化"推翻) | 备选 | 为什么不做 | |---|---| | **提高 uiTask 优先级** | 会**掩盖**"心跳依赖他任务"与"忙等"两个真实缺陷;而且它根本不解决"UI 在等 netTask"。缺陷已修完后,提优先级才成为可讨论项 | | 给 `net_*` 加"有界等待"版 | 回执通道是"每任务一个 32 位通知值",超时后**迟到的回执会被下一次调用的零等待拾取当成新结果**(`net_wait_result` 注释里 L7 挂死/整段错位的同款根因)。要做对必须引入请求序号,代价高于搬家 | | 把推流搬出 uiTask(独立 `uiSinkTask`) | 实测 `uis`=1 µs 而 `uip` 仍 60.9 ms = **LVGL 帧周期地板** → 搬走推流**不会让刷新变快**;收益只剩极端并发下的观感隔离,成本含**必改 `.ioc`** 的重生成与全套回归。若日后成问题,正确方向是先治 netTask 排队公平性(见 `../网络/Net层阻塞模型架构设计.md`),不是搬 UI | | 把推流塞进 netTask | 零往返零新增 RAM,但共享源码要走 backend 端口(双端同改)、netTask 这个唯一执行者更胖、一个 UI 侧 bug 直接变成整机复位 | > **2026-10-25 大窗补记(判读实锤,勿与上表混淆)**:条目「提高 uiTask 优先级」与 > 「有界等待」的否决理由**依旧成立**——大窗里 UI 的 ~1.2 s 单串停摆(9 条 stall 中唯一 > 的真单任务链,bit4 全程缺席到 mg≈900+)发生在**监听重建窗**内,正是上表所说 > 「UI 在等 netTask」的实况;修复方向仍按 v1.3 既注走**事件驱动 accept / 等待面搬家**, > 不是提优先级、也不是给 net_* 裸加超时(迟到回执拾取风险原样在册)。同期暴露的 > **复合停喂判据**问题(本窗缺 UI、下窗缺 IEC104/FTP 串成 16:24/16:28 两起误申报, > `mg=877/918` 均为复合链而非单任务链)已由 **v1.5 按位分账**根治:判据见 §2.2/§3.2/ > §5.3,回归钉 = 主机单测 TC-WATCHDOG-017(交替缺席零申报、持续喂狗)/ 018(同位 8 > 连击必报 + 恢复后账本重启)。原始日志判读材料:`docs/测试说明/测试数据/boot_log_data/` > (megawin 同日摘录入库,gz)。 --- ## 附录 A:参考依据(当前有效) | 来源 | 引用点 | |---|---| | `docs/芯片手册/STM32F4xx参考手册_V18英文版_1-1000.md` §21 | 12 位递减计数器、LSI 时钟源、启动后不可停、超时公式、`SR.PVU/RVU` 同步 | | `docs/芯片手册/STM32F4xx参考手册_V18英文版_1-1000.md` §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 驱动"一类取证已随实施失效,不再列出;当时的原始登记在《项目审查与优化建议报告》§4.1(该报告 2026-10-25 已删除),已随「修复即销项」删除——**取证与实施记录以本文为准**。 ## 附录 B:备选方案取舍记录 | 备选 | 否决理由 | |---|---| | 每任务各自喂狗 | 任一任务存活就喂狗,检测不到单任务卡死(§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) | --- ## 变更记录 | 日期 | 变更 | 说明 | |---|---|---| | 2026-09-29 | 创建 | 调研 + 设计方案:IWDG + 任务心跳位图;含选型、九项设计决策、验证计划与风险清单 | | 2026-09-29 | 修正实施路径 | 实测发现 HAL IWDG 驱动源码不在仓库且未编入 MDK 工程 → 初稿改为**直接寄存器操作**(§2.2 路径 C);超时数值按公式核算修正;补 `RCC_CSR`/`IWDG` 寄存器定义的实测行号 | | 2026-09-29 | **路径再变更 + 实施落地** | 决策改走 **CubeMX 图形配置**(§2.2 路径 A):`HAL_IWDG_MODULE_ENABLED` 改为经 CubeMX 生成(避免"取消注释即编译失败")。同时落地 §5 骨架到 `App/util/task_watchdog.{c,h}`,各任务埋点(default/net/ui/time_sync),`RCC_CSR` 复位原因锁存(`Src/main.c` + `App/app/app_main.c`),`configASSERT` 改为 `vApplicationAssertFailed()`。实测 CubeMX 重生成覆盖 4 处既有定制(P0×1 + P1×2 + P2×1),已全部恢复,`@build` 0 错误 0 警告 | | 2026-09-29 | **新增 §10.2 三层手段 + 审计脚本** | 沉淀通用方法论(写进 `.ioc` / 搬进 `USER CODE` 区 / 审计+基线);新建 `scripts/check_cubemx_survival.py`(16 项清单 + 漂移检测)+ `.cubemx_survival_baseline.json` + pre-commit `cubemx-survival` 钩子;`AGENTS.md` 与 `docs/代码规范/pre-commit使用说明.md` 同步 | | 2026-09-29 | **第二次重生成验证 + 机制修正** | 审计脚本**实测有效**(准确检出 5 处覆盖)。⚠️ **关键发现**:CubeMX 换了一代中断函数模板(旧 `HardFault_Handler`/`HardFault_Report` 自造区 → 新函数体内 `HardFault_IRQn 0`/`W1_HardFault_IRQn 0`),**"生成区外的自造标记区"同样不被保留**——上一轮假设被证伪。据此升级恢复方案:报告实现迁入 `USER CODE BEGIN 0`(标准保护位);Handler 本体改为生成区整函数替换(`#define` 改名保留生成实体,链接器自动移除,`.map` 已验证)+ `HARD-FAULT-ANCHOR` 锚点。同步修正审计脚本(`hardfault-handler` 改 `wholefile` + 锚点)、`UNPROTECTED_LEGACY` 清单、§10.1/§10.2 全文。`@build` 0 错误 0 警告 | | 2026-09-29 | **3 个 Fault 升级为崩溃采集** | `MemManage` / `BusFault` / `UsageFault` 由空 `while(1)`(死机静默)升级为与 `HardFault` 同一套采集:生成区整函数替换(各带独立锚点 `MEMFAULT-ANCHOR` / `BUSFAULT-ANCHOR` / `USAGEFAULT-ANCHOR`)+ `#define X X_Gen` 改名保留生成实体(链接器各移除 2 字节)。新增 `fault_handler_enable()` 使能 `SHCSR` 后三者(默认只开 HardFault,不开则故障被升级、看不出根因),声明入 `Inc/stm32f4xx_it.h`、调用在 `app_main_init()` 第一行。因 ARMCC `__asm` 取不到 C 数据符号,改为 per-fault C 入口 `fault_entry_{hard,mem,bus,usage}` 带故障名调 `hard_fault_report(tag, frame)`。**刻意不启用** `UNALIGN_TRP` / `DIV_0_TRP`(避免把普通 bug 升级成死机)。审计脚本新增 `fault-handlers-3` / `fault-handler-enable` 两项、`UNPROTECTED_LEGACY` 清掉 3 个 Fault(WARN 2→1)、清单 16→18 项。`@build` 0 错误 0 警告;审计 18 通过 / 1 警告 / 0 失败 | | 2026-10-01 | **查证 `fault_handler_enable()` 的 CubeMX 可行性** | 用户问"能否通过 CubeMX 配置"→ **否**。查 CubeMX 安装目录模板源码得三重证据:① `tpl_main_c.ftl` 的 `MX_NVIC_Init()` 只在 `systemHandler=="false"` 时生成 `HAL_NVIC_EnableIRQ`,而 `NVIC-STM32F417_Modes.xml` 中三个 Fault 都标 `Y`(=systemHandler)→ 永不生成使能代码;② **全部模板中 `SHCSR` 只出现 1 次**且是 MPU 清理的 `&= ~MEMFAULTENA`(禁用),**无任何 `\|=` 置位**;③ `.ioc` 无 MPU(`Mcu.IP` 列表),且 `HAL_MPU_Enable` 只置 `MEMFAULTENA` 一位。实测印证:`HAL_MspInit()` 里三个 Fault 一行代码都没生成。结论:只能手工写,但属 **USER CODE 区**(`USER CODE BEGIN 1` 区)→ 重生成不丢,有审计项兜底。另新增警示:`.ioc` 的 `NVIC.MemoryManagement_IRQn=true` **不代表已使能**(第 1 段只是"向量可见")。写入 §10.2 与四个 Fault 采集小节 | | 2026-10-01 | **新增 §7.3 主机侧单元测试** | 硬件访问收敛到薄平台层(`App/util/task_watchdog.h` 末段声明 3 个 `__weak` 钩子:喂狗/启动/取 tick),MCU 端行为零变化(`@build` 0/0)。新建 `test/host/watchdog/`(`watchdog_test.c` 39 用例 + `run_unit_test.py` MSVC 运行器 + `stub/` 三桩头),`#include` 生产源码以保证测的是真代码。挂 pre-commit `watchdog-unit-test` 钩子。新增 `docs/测试说明/单元测试/看门狗单元测试说明.md`;`App/util/README.md`、`test/host/README.md`、`测试开发标准流程.md`(新增主机侧纯逻辑单测层)同步。顺带改善:`s_last_tick` 由函数局部 static 提为文件 static(可测性 + 模块状态归一)。§5.1 文件表与 §10 步骤表登记;§7.2 S1 优先级降低(位图逻辑已由 U1 覆盖) | | 2026-10-02 | **P0 根因修复:IWDG 超时被静默配成复位默认值(比设计短 5 倍)** | round 3~8 反复出现的"空 STOR(0 字节截断)→ IWDG 误复位"**根因不是"poller 自身停摆"**。真因:`Src/iwdg.c` 为实现**延后启动**(§4.5 决策 C)删掉了 `HAL_IWDG_Init()`(`IWDG-DEFER-START-ANCHOR`),而 `HAL_IWDG_Init()` 是**唯一**会把 `hiwdg.Init.Prescaler/Reload` 写进 IWDG 寄存器的路径——删掉后 `MX_IWDG_Init()` 只填结构体,`WD_IWDG_PRESCALER`/`WD_IWDG_RELOAD` 沦为**死宏**,寄存器停在**复位默认值** `PR=0`(÷4)/`RLR=4095`。实测(SWD 回读)实际超时 = 4×4096/LSI ≈ **349~512 ms**,而设计是 64×1250/LSI ≈ **1701~2498 ms**。两个后果:① `WD_MISS_TOLERANCE`(8×100ms=800ms)**永远等不到**就被 IWDG 复位 → **停喂判定分支成死代码**(这正是 §7"STALL 从未执行"的真正原因,当时据此误判为"poller 停摆");② 任何 >400ms 的心跳缺口都直接误复位(空 STOR 的 FTL flush 使 NET/FTP/UI 心跳缺 ~400ms)。**修复**:`wd_platform_start()`(`__weak`,**不在 CubeMX 生成区**,重生成不丢)补上按设计值配置:解锁(`KR=0x5555`) → `PR` → 等 `SR.PVU` 清 → `RLR` → 等 `SR.RVU` 清 → `KR=0xCCCC`(不等同步会静默丢弃后写的那只,SWD 实测)。`.map` 证实 `Removing stm32f4xx_hal_iwdg.o(i.HAL_IWDG_Init), (94 bytes)`,故这是全固件**唯一**写 PR/RLR 的点。**验证**:SWD 现场改回设计值后,同一空 STOR 由 **5/5 复位 → 100% 成功**;固件烧录后 3 次开机回读均为 `PR=4/RLR=1249`。`@build` 0/0;本地 CI 静态门禁+主机侧纯逻辑单测全绿(278/278 断言) | | 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 延后启动由审计兜底升级为 USER CODE 区:不再需要每次重生成后手工删那一句** | 起因是提问"能否在初始化函数下方的自定义区里先反初始化"。查证:**反初始化不可行**——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 硬教训②与审计兜底存量清单同步:审计兜底只剩 4 个 Fault Handler 实体(同一招可根治,尚未做)。`@build` 0 错误 0 警告;审计 21 通过 / 1 警告 / 0 失败 | | 2026-10-09 | **4 个 Fault Handler 由审计兜底升级为 USER CODE 区:重生成完全零 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`)+ 代码符号断言。**审计兜底清单清空**,重生成预期零 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 引用指当时编号**,不再回溯改写 | | 2026-10-10 | **adcTask 纳入监视(B1):必需掩码 `0x37`→`0x77`** | B1(ADC 采集链路)落地时新增 `adcTask`(`App/task/adc_task.c`:1 ms 采样节拍触发 CONVST + 消费 `tpafe5160_snapshot()`)并纳入 IWDG 监视:`task_wd_id_t` 新增 `TASK_WD_ADC = 6`;`wd_required_mask()` 产品 `0x37`→`0x77`、`ENABLE_HW_LAYER_TESTS` `0x35`→`0x75`(adcTask **不依赖任何就绪屏障** → 任何构建都置位)。adcTask 主循环每 1 ms `task_wd_beat(TASK_WD_ADC)`;**不做 NAND I/O → 不登记 FTL 心跳路由**(无需 `task_wd_bind()`)。主机侧单测 `beat_all()` 与掩码断言(`tc_003`/`tc_005`/`tc_011` 的 `0x37`→`0x77`)同步,39 断言全绿。任务由 `Src/freertos.c` 的 `USER CODE` 区手工创建(同 timeSyncTask,不在 `.ioc` 里),栈 512 words、优先级 Normal,`stack_report.py` 实测最坏链 504 B(余 1544 B / 75%)。见 `docs/算法/继电保护功能复现规划.md` §7 | | 2026-10-10 | **B1 `@test` 实机闭环:adcTask 掩码真机验证** | 台架闭环(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`)确认 `adcTask` 纳入掩码后**无 adc 相关误复位**:`[STACK] adc peak_used=496/2048 free_min=1552 (75%)`、`frames/seq` 每 10 s 严格 +10000(=1000 Hz)、`idle=0`/`frst_err=0`、全片回读 `No difference found`。启动早期两次 `MISS start snap=0x67 req=0x77` 仅缺 bit4(`timeSync` 未启动,属正常就绪窗;adc 的 bit6 自始置位),掩码随后收敛、全程无复位。同轮另修 `dbg_console_writer_ready()` 门控(adcTask 周期日志在写者注册前抑制,见 `控制台单写者日志设计说明.md`) | | 2026-10-10 | **新增 §11:任务级饥饿的诊断方法与实测基线** | 承接 Task #32(`task stall: mask=0x67 req=0x77`,缺 `TASK_WD_UI`)结案后的沉淀——原分析文档 `docs/问题记录/UI任务看门狗停摆分析.md` 按"完成即销项"删除,可复用部分并入本文:**§11.1** 三条易误读的判读口径(启动期 `MISS start 0x67` 良性、`miss=` 是累计不是连续、`reset cause` 不能区分 IWDG 与台架);**§11.2** 诊断四步(先看掩码缺几位 → 量化占空比 → 用计数速率反证空转 → 用 `[PROF]` 指认占用者;本例否掉"CPU 饱和",实测 10%~13%);**§11.3** 三条结构性脆弱点(uiTask 唯一 BelowNormal/心跳依赖 netTask 往返/自定截止期不可恢复)与两个顺带修掉的**正确性**缺陷(无新帧仍压环 ≈3% 频率失配、失败拍静默沿用旧值);**§11.4** `[PROF]`/`[PROF2]` 全字段语义 + v2.8 基线(含 `netp=0` 是真实值、`adcm` 峰值来自心跳日志、`uip` 地板 = LVGL 帧周期)与三机理判读口诀;**§11.5** 三条教训(门控须留低频兜底、门控会消灭暴露路径、事件驱动要核对节拍源与消费者创建时刻);**§11.6** 明确不做的四条及理由。§9.1 的 R7 行改指本文 §11.3 | | 2026-10-11 | **新增 §10.4:ARMCC `__asm` 的三条语法约束** | AGENTS.md 瘦身:「ARMCC 内联汇编语法」条目的完整细节(GCC 语法报 `#18`、块内联把寄存器名当 C 标识符报 `#20`、`__asm` 取不到 C 数据符号 `A1516E`、可用写法 = 整函数 `__asm void f(void)` + `B` 到 C 入口、`#define f f_Gen` 让位机理)迁入本文,AGENTS.md 只留一行禁令与指针 | | 2026-10-12 | **iec104Task / protectTask 纳入监视(B4/B5):必需掩码 `0x77`→`0xF7`→`0x1F7`** | 两行合并记录(B4 落地时漏补本文,B5 一并纠偏):**B4** 新增 `TASK_WD_IEC104 = 7`(iec104Task 100 ms 服务循环喂位,NET_READY 屏障等待按 100 ms 分片喂位),产品 `0x77`→`0xF7`、HW 层 `0x75`→`0xF5`;**B5** 新增 `TASK_WD_PROTECT = 8`(protectTask 20 ms 判据节拍喂位,见 `docs/算法/继电保护功能复现规划.md` §7),产品 `0xF7`→`0x1F7`、HW 层 `0xF5`→`0x1F5`。两任务均**不依赖存储/网络之外的屏障**(protectTask 无任何屏障)→ 任何构建都置位;均不做 NAND I/O → 不登记 FTL 心跳路由。主机侧单测 `beat_all()` 与 `tc_003`/`tc_005`/`tc_011` 掩码断言同步至 `0x1F7`,39 断言全绿。⚠ 注意 `0x1F7` 不含 bit3(testTask 任何构建不入掩码,口径不变) | | 2026-10-25 | **停喂判定按位分账(v1.5,大窗判读 F2)**:stall/停喂唯一判据从「复合连续缺席窗数」改为「**任一必需位的连续缺席窗数**」——同一任务连续 8 窗(800 ms)没跑过才申报停摆并停喂;多任务交替缺席(本窗缺 A、下窗缺 B)各自清零、且缺席窗在容忍线下**照常喂狗**(旧实现自第一个缺席窗起就不喂,800ms 只是申报点)。动因 = 2026-10-25 台架大窗:54 个探针重建窗里 9 条 stall 行中 16:24/16:28 两起为复合误升级(mg=877/918,无任何单位到 800ms 单串),16:39 为真 UI 单串(~1.2 s,受害者面修复另立 F3 票);`MISS start` 降级为复合边沿**诊断行**(不再代表停喂,判读口径 §6.1/§11.1 同步);真停摆到复位的总时长最坏 +约 0.7 s(§2.2 链注)。§0/§2.2/§3.2/§5.3/§6.1/§11.1/§11.6 同步;主机单测 `test/host/watchdog/watchdog_test.c` 断言按新判据改写 + 新增 **TC-WATCHDOG-017**(20 窗交替缺席:零 stall、20 窗持续喂)/ **TC-WATCHDOG-018**(同位 8/9/10 连击必报停 + 停喂;恢复窗喂、账本重启,第二段亚容忍串不误报),47 断言全绿 | | 2026-10-26 | **卷锁 try 失败纳入 ④「只计数」口径 + 修一处会骗人的契约(道G task-17)** | §4.3 场景表新增一行:卷锁 try 失败(defaultTask / uiTask 从不等锁,取不到就跳过本轮)既不需要分片也不需要心跳,缺的是**可观测**。落地形态:`App/fs/vol_lock.c` 记 5 态计数(`acquired` / `try_timeout` / `blk_timeout` / `failopen`,共 16 B 静态、一次自增),`vol_lock_get_stats()` **不取锁**、不关中断、任意上下文可调、NULL 无害。为什么不许在 try 失败处补一条日志:能把事写下来的通道(`/log/wdev.log`、`run.log`)自己也要取这把锁,而卷锁**非递归**,被卷忙挡住时既写不出东西还会自取死锁——所以留痕只能由 syslog 唯一写者按差值 latch 补投(另立 task-40 接在 `syslog_drain()` 的 `s_quiet_drop` 差值块之后)。同时修契约:`App/fs/vol_lock.h` 原写「0 - 超时未获取」、`vol_lock.c` 原 :38 把 fail-open 归给 0,而代码(:46)与 `TC-VOLLOCK-008` 都是**返 1**;现在两处都按代码写实并显式警告「返 1 也可能是零互斥」,`failopen` 计数就是它的证据。**观测时刻刻意不在锁层**:记时刻要读 tick,等于给 defaultTask 取锁链添 callee 与本帧(本档 §11 一贯按字节省的就是这条链),改由消费方观测并自盖 `obs_first_ms` / `obs_last_ms`——那是**被观测到的时刻**,不等于发生时刻,命名与文案都不许写成"首次发生"。机器证据:`test/host/vol_lock` 新增 TC-VOLLOCK-015~019(45 断言全绿:T2/T3 互不冒领、fail-open 计数可累加、读取不消耗证据);另把「喂狗点取锁一律 timeout=0」从注释纪律升级成会 FAIL 的断言——4 套假件加 `s_last_wait_to`,断言落在 TC-SYSLOG-010/012、TC-SOESTORE-016/018、TC-CFGFST-003/016/032/033、TC-LFTPIO-034,并做过红-绿反证(把 `sys_log_mcu.c:318` 的 `0u` 改 `50u` → 135/137 双红;把 `lftpd_io.c:48` 的 `100u` 改 `0u` → 32/33 红;随后按原串改回、SHA-256 逐字节相同) |