Files
STM32F4-Base/docs/子系统设计/看门狗设计说明.md
T
wandering1 8f8d97b3d8
CI / 静态门禁 (pre-commit) (push) Failing after 8m4s
CI / 主机侧单测 (MSVC) (push) Successful in 27s
docs(编号): 批次六余账文档面——CubeMX 层号、双写疤、采集链旧层名共 61 站点
- CubeMX 防丢失三层优先级 L1/L2/L3(批次 3c 漏网 30 处):看门狗设计说明 25、
  门禁说明/cubemx-survival 3(含「优先 L1/L2」→「优先写进 .ioc 与搬进 USER CODE
  区」)、ADC通道映射 2。
- 改名遗留的中文重叠双写 14 处:菜单页面设计「一级菜单菜单」×6、「二级菜单菜单」
  ×2;README「语义取数与有效性」重复;存储架构「3字节字节地址」;链路延迟
  「队列往返队列往返」;继电保护「出口出口与记录层」;产品需求「全周傅氏相量」重复。
- 采集链旧层名 5 处:「L2 相量层」→ 全周傅氏相量、「L0 采样驱动」→ 采样驱动层、
  「L2-x 前缀」→ 相量输出标签前缀、「L1 层 v2.3」→ 语义取数与有效性层 v2.3。
- UI 菜单层级 2 处:产品需求「4 级(L0~L3)」→「4 级(主界面 / 一级 / 二级 /
  三级菜单)」、「主界面 (L0)」→「主界面」。
- 登记簿条目改名「L5 队列往返基线待重验」→「队列往返基线待重验」+ 引用侧 5 处
  同步(登记簿 §1/§7、实机测试实施指南 §7.3 与步骤表、编号规范 §1 好例子清单——
  那个混在短语里的 L5 恰恰是个坏例子)。归档里的历史 note 原文不动。
- 归档 §10.3:四族收口记录 + 边界与裁定(peer JSON 键属冻结证据 schema 不改;
  REQ2FIRST / FULLFRAME 不并入 FRAME_RT;L1-3/L1-4 属不可还原死号,就地删号留
  事实;「原 L2 基线 3/10」按上下文读作主机侧回归的早期基线)。
- 判族保留(永久):硬件位号与封装尺寸 27 处(网表 L1~L3 磁珠电感、
  SOT-23_L2.9-W1.3、CRYSTAL-L3.2)、芯片手册原文 3 处双写;伪命中 4 条没删
  (「遥测量 测量值」「已登记登记簿」「谁来监视监视者」×2)——那些是正常词组。
- 门禁:check_doc_links --all --sections-strict 通过(195 篇、章节引用 3292 处);
  git diff --check 干净;逐文件 BOM / U+FFFD / CRLF 与 HEAD 一致。
2026-10-28 18:57:03 +08:00

80 KiB
Raw Blame History

看门狗设计说明

模块名称:独立看门狗(IWDG)+ 任务心跳位图(App/util/task_watchdog.{c,h}) 文档定位:现状设计说明——选型依据、当前实现、可观测性与判读方法、验证结论。 状态:✅ 已落地并台架闭环(2026-10-03 V2/V4/V5);未办项集中在 §9。 日期:2026-09-29 创建;2026-10-10 按当前代码重写正文(旧正文含大量立项前的 描述,已清理;实施史与逐次决策见 §10 与文末变更记录,未删)。 相关文档: 运行与自检记录设计说明.md(复位记录与 selftest.log)、 ../测试说明/看门狗单元测试说明.md(主机侧纯逻辑单测,判定状态机)、 ../测试说明/实机测试实施指南.md(故障注入台架流程)、 ../架构设计/项目启动顺序管理.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,不必再手工删那一句:

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 采用方案:任务置位 → 校验者判定 → 校验者喂狗

看门狗心跳模型:任务置位 → 校验者判定 → 校验者喂狗

图源:tools/arch_diagram/gen_watchdog_svg.py(改图改脚本重跑,见 ../架构设计/图形绘制流程.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 区):

#define configASSERT( x ) if ((x) == 0) { vApplicationAssertFailed(__FILE__, __LINE__); }

vApplicationAssertFailed()(Src/freertos.c 的 USER CODE Application 区)经 hook_ptr_ok() 校验文件指针、取路径末段,裸 UART 打 [ERR] ASSERT FAILED <file>:<line> (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…)(@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 对外接口

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 原值(@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 <file>:<line> (resetting) configASSERT 断言失败,打印后软复位
[ERR] HARDFAULT / MEMFAULT / BUSFAULT / USAGEFAULT PC=… CFSR=… [BFAR=] [MMFAR=] 4 个 Fault 崩溃现场 + 故障名归因
[ERR] STACK OVERFLOW task=<name> / [ERR] MALLOC FAILED … 内核钩子 栈溢出(复位)/ 堆耗尽(仅告警)

与 @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 正常启动、长期不复位 ✅ 常态成立 每次 @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 §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 审计脚本

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)——该文件每次 @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 §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 路取数 ≈1020 µs、压环 + 5 实例傅氏 ≈1015 µ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 38 反复出现的"空 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 ≈ **349512 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 逐字节相同)