- 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 一致。
80 KiB
看门狗设计说明
模块名称:独立看门狗(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 设计目标
- 可恢复:任务级死锁 / 饿死 / 断言挂死 → 硬件复位,无需人工到场。
- 可诊断:复位后能区分"上电 / 看门狗 / 软件 / 引脚 / 欠压",并留下证据。
- 不误杀:正常业务的长耗时窗口(NAND 擦写与 GC、FAT 挂载、PHY 链路等待、FTP 大文件传输、LVGL 重绘)不得触发复位。
- 低代价: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",是全固件唯一写这两只寄存器的点。 通用教训:凡"把某个 HALInit()拆开/延后",必须逐一核对被拆掉的函数还有哪些副作用没搬到新位置。
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 判读顺序(现场"设备反复重启")
- 看
reset cause:——IWDG= 看门狗抓到;software= 崩溃钩子/断言主动复位;power-on= 真的断电重启。 IWDG时看PREVRUN seg=—— 指向 defaultTask 主循环阻塞点;再看TICKRUN duw=判断 HAL 时基是否同时冻结。- 看
MISS start/task stall的mask缺席位 —— 定位是哪个任务先停摆。 - 串口被僵尸通道吞掉时读
/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 诊断四步:先量化,再归因
- 看掩码缺哪几位。 只缺一位 = 定点打击(该任务独有的依赖或优先级处境);
多位同时缺 = 全局饥饿 / CPU 饱和。本例
0x67只缺 UI,其余五个 Normal 任务心跳 全正常 → 当场否掉"计算量把 CPU 吃满"这个直觉(用户假设方向对、结论不成立)。 - 把嫌疑任务的单拍成本逐项算出来(不要估)。当时算式:ADC1 一轮 9 通道 ≈67 µs、
TPAFE 快照 + 9 路取数 ≈10
20 µs、压环 + 5 实例傅氏 ≈1015 µs → 单拍 0.10~0.13 ms = 占空比 10%~13%,距饱和很远。 - 用计数速率反证"有没有空转"。
ring在两次心跳间严格 +10000/10 s = 恰好 1000 次/秒; 若该任务失去阻塞(空转),按 0.11 ms 单拍应为 ≈9000 次/秒 → 反证它每拍都睡满。 - 用
[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 三条教训(同类问题会复发)
- 门控类优化必须保留低频兜底。
readable只由 RECV_OK 中断置位,而net_reconcile_tick()的三类对账不含"芯片 RX 非空但readable=0" → 中断一旦丢失,纯门控会把已到达的数据 永久藏住。现每 32 轮无条件 recv 一次。拿"自愈能力"换性能是隐性负债。 - 门控会顺手消灭暴露路径。 加了 accept 门控后,监听控制块消失时
net_can_accept()恒返 0 → 改前"每轮 accept 撞 BADF"这条暴露随之消失 → 端口永久不再接受新客户端而无声。 现补net_get_sock(g_listen) == NULL判定。加"跳过"逻辑时要问:原来哪条错误是靠这次调用暴露出来的? - 把"定时轮询"换成"事件驱动"时,必须单独核对节拍源启动时刻 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 3Src/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 ≈ **349WD_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 逐字节相同) |