IWDG 延后启动改走宏改名(L3→L2):生成调用变空操作,重生成后不再需要手工删

This commit is contained in:
2026-10-10 01:30:47 +08:00
parent 63ee090fd1
commit e2e1d0fd42
6 changed files with 71 additions and 50 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{
"snapshot": {
"iwdg-defer-start": "f5c340730afbb4b7",
"iwdg-defer-start": "ca839ffc1cda439f",
"hardfault-handler": "1dfa875a1b75bea4",
"fault-handlers-3": "1dfa875a1b75bea4",
"fault-handler-enable": "be87fcd3a7926138",
@@ -12,7 +12,7 @@
"stack-overflow-hook-impl": "a006edbd30d2c4d7",
"malloc-failed-hook-impl": "fcd7250e5cd6403b",
"stack-overflow-hook-helper": "cacd9fd028be9b5b",
"watchdog-module": "58f95a6cb88e6925",
"watchdog-module": "c2823569e3c42317",
"reset-cause-latch": "52fc71acbb0f1aed",
"iwdg-hal-driver": "4b902875a84554e9",
"hal-iwdg-enabled": "1a8ed6cff34f7ba6",
+1 -1
View File
@@ -40,7 +40,7 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
## 平台约束
- `Inc/`/`Src/` 中 CubeMX 生成文件带 `USER CODE BEGIN/END` 标记:自定义代码**只写标记区之间**;`Drivers/BSP/` 为纯手工代码
- **CubeMX 重生成会覆盖标记区之外的一切**(`.ioc` 的 `KeepUserCode=true` **只**保护 `USER CODE` 区;2026-09-29 两次重生成各覆盖 4~5 处定制,其中 P1 两处**不报编译错**、只在真出故障时暴露)。防止定制丢失按三层优先级:**L1 写进 `.ioc`**(凡 CubeMX 界面能表达的配置,如任务栈/优先级/队列/事件组/堆大小/IWDG 参数——权限最高,重生成只会照写)→ **L2 搬进 `USER CODE` 区**(**只信 CubeMX 当前模板真实生成的区名**,优先文件头 `USER CODE BEGIN 0` 这类从未变过的标准位)→ **L3 审计兜底**。**每次重生成后立刻跑 `python scripts/check_cubemx_survival.py`**(已挂 pre-commit `cubemx-survival` 钩子;改动 `.ioc`/`Src/`/`Inc/`/`task_watchdog`/`.uvprojx` 触发)——清单 21 项 + 哈希基线漂移检测,直接列出"被覆盖了哪几处"。**2026-10-09 第三次重生成(启用 ADC1)补充**:`Inc/FreeRTOSConfig.h` 的自造 `FREE_RTOS_CONFIG_HARDENING` 区**第二次**被抹 → 已改走 **L1**:`configCHECK_FOR_STACK_OVERFLOW=2` / `configUSE_MALLOC_FAILED_HOOK=1` 在 GUI 的 **FreeRTOS → Config parameters → Hook function related definitions** 里设(写进 `.ioc` 由模板生成,永不再丢;教训:**别凭印象断言"界面里没有该项"**,先翻标签页或查 `db/mcu/IP/*_Modes.xml`)。⚠️ 启用后 CubeMX 会在 `Src/freertos.c` 生成 `USER CODE BEGIN 4 / 5` 区(默认内容是**空桩**)——**钩子实现必须整体写进这两个区**(原先在 `USER CODE Application` 区的实现要搬走,否则同名函数两处定义 = **编译错**;配套 helper `hook_ptr_ok()` 须在钩子之前 → 放 `USER CODE BEGIN PD` 区)。审计项 `stack-overflow-hook-impl` / `malloc-failed-hook-impl` / `stack-overflow-hook-helper` 分别校验 4 / 5 / PD 三个区。⚠️ **不要依赖"自造标记区"**:2026-09-29 实测 CubeMX **换了一代中断函数模板**(旧 `HardFault_Handler`/`HardFault_Report` 自造区 → 新函数体内 `HardFault_IRQn 0`/`W1_HardFault_IRQn 0`),旧区**连同内容一起消失**(看起来仍像"遵守了规则")。**生成区内无标记位可用的改动只能 L3 兜底**(如 `Src/iwdg.c` 的 `HAL_IWDG_Init()` 调用、4 个 Fault Handler 本体——本代模板整函数重写),一律加锚点注释(`IWDG-DEFER-START-ANCHOR` / `HARD-FAULT-ANCHOR` / `MEMFAULT-ANCHOR` / `BUSFAULT-ANCHOR` / `USAGEFAULT-ANCHOR`)供审计检出。方法论详见 `docs/子系统设计/看门狗设计说明.md` §10.1/§10.2
- **CubeMX 重生成会覆盖标记区之外的一切**(`.ioc` 的 `KeepUserCode=true` **只**保护 `USER CODE` 区;2026-09-29 两次重生成各覆盖 4~5 处定制,其中 P1 两处**不报编译错**、只在真出故障时暴露)。防止定制丢失按三层优先级:**L1 写进 `.ioc`**(凡 CubeMX 界面能表达的配置,如任务栈/优先级/队列/事件组/堆大小/IWDG 参数——权限最高,重生成只会照写)→ **L2 搬进 `USER CODE` 区**(**只信 CubeMX 当前模板真实生成的区名**,优先文件头 `USER CODE BEGIN 0` 这类从未变过的标准位)→ **L3 审计兜底**。**每次重生成后立刻跑 `python scripts/check_cubemx_survival.py`**(已挂 pre-commit `cubemx-survival` 钩子;改动 `.ioc`/`Src/`/`Inc/`/`task_watchdog`/`.uvprojx` 触发)——清单 21 项 + 哈希基线漂移检测,直接列出"被覆盖了哪几处"。**2026-10-09 第三次重生成(启用 ADC1)补充**:`Inc/FreeRTOSConfig.h` 的自造 `FREE_RTOS_CONFIG_HARDENING` 区**第二次**被抹 → 已改走 **L1**:`configCHECK_FOR_STACK_OVERFLOW=2` / `configUSE_MALLOC_FAILED_HOOK=1` 在 GUI 的 **FreeRTOS → Config parameters → Hook function related definitions** 里设(写进 `.ioc` 由模板生成,永不再丢;教训:**别凭印象断言"界面里没有该项"**,先翻标签页或查 `db/mcu/IP/*_Modes.xml`)。⚠️ 启用后 CubeMX 会在 `Src/freertos.c` 生成 `USER CODE BEGIN 4 / 5` 区(默认内容是**空桩**)——**钩子实现必须整体写进这两个区**(原先在 `USER CODE Application` 区的实现要搬走,否则同名函数两处定义 = **编译错**;配套 helper `hook_ptr_ok()` 须在钩子之前 → 放 `USER CODE BEGIN PD` 区)。审计项 `stack-overflow-hook-impl` / `malloc-failed-hook-impl` / `stack-overflow-hook-helper` 分别校验 4 / 5 / PD 三个区。⚠️ **不要依赖"自造标记区"**:2026-09-29 实测 CubeMX **换了一代中断函数模板**(旧 `HardFault_Handler`/`HardFault_Report` 自造区 → 新函数体内 `HardFault_IRQn 0`/`W1_HardFault_IRQn 0`),旧区**连同内容一起消失**(看起来仍像"遵守了规则")。**生成区内无标记位可用的改动**分两种处理(2026-10-09 更新):① 要**去掉/替换生成区里的一句调用** → 用宏改名升级为 **L2**:例 `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)` 自动变空操作(`.map` 证实 `Removing ... HAL_IWDG_Init, 94 bytes`),生成区保持原样、**重生成零 diff**,不必再手工删。⚠ **IWDG 启动后无法停止**(唯一手段是复位),所以不能"反初始化",只能在启动前拦截。② 要**替换生成实体的定义**且该位置无区可用(4 个 Fault Handler 本体——本代模板整函数重写)→ 仍 L3 兜底,加锚点注释(`HARD-FAULT-ANCHOR` / `MEMFAULT-ANCHOR` / `BUSFAULT-ANCHOR` / `USAGEFAULT-ANCHOR`)供审计检出;我们的定义必须写在 `#define X X_Gen` **之前**,否则被自己的宏改写(此招也可升级为 L2,尚未做)。方法论详见 `docs/子系统设计/看门狗设计说明.md` §10.1/§10.2
- **4 个 Cortex-M4 Fault 全部采集现场**(2026-09-29):`HardFault` / `MemManage` / `BusFault` / `UsageFault` 均为手工 `__asm` Handler(取 MSP/PSP 帧指针)→ `B` 到 per-fault C 入口 `fault_entry_{hard,mem,bus,usage}`(各自带故障名)→ `hard_fault_report(tag, frame)` 打 `[ERR] <TAG> PC/LR/xPSR/R0-R3/CFSR/HFSR[/BFAR][/MMFAR]` 后 `NVIC_SystemReset()`。**后三者默认禁用**(复位后 `SHCSR` 只开 HardFault),不使能就会被**升级成 HardFault**、日志看不出根因 → `fault_handler_enable()`(`Src/stm32f4xx_it.c` 的 `USER CODE BEGIN 1` 区)在 `app_main_init()` **第一行**调用。**刻意不启用** `CCR.UNALIGN_TRP` / `CCR.DIV_0_TRP`(会把普通 bug 升级成死机)。⚠️ **ARMCC `__asm` 取不到 C 的数据符号**(`ADR r1, xxx_tag` 报 `A1516E: Bad symbol`,去 static 也无效)且无法传字符串参数 → 故障名必须由 C 侧 per-fault 入口带上,不要在 `__asm` 里取 C 变量
- **`SHCSR` 的 Fault 使能位无法在 CubeMX 配置**(2026-10-01 查模板源码证实):CubeMX 对 System Exception **只生成优先级(且仅当优先级 ≠ 0),从不生成使能代码**——`tpl_main_c.ftl` 的 `MX_NVIC_Init()` 用 `[#if vector.systemHandler=="false"]` 挡住 `HAL_NVIC_EnableIRQ`,而 `NVIC-STM32F417_Modes.xml` 里 `MemoryManagement/BusFault/UsageFault_IRQn` 都标 `Y`(=systemHandler);**全部模板中 `SHCSR` 只出现 1 次**且是 MPU 清理的 `&= ~MEMFAULTENA`(禁用),无任何 `|=` 置位;MPU 路径也堵死(`.ioc` 无 MPU,且 `HAL_MPU_Enable` 只置 `MEMFAULTENA` 一位)。⚠️ **`.ioc` 的 `NVIC.MemoryManagement_IRQn=true` 不代表已使能**(第 1 段只是"向量在 NVIC 表里可见")——别误以为勾了 Enabled 就等于开了故障。结论:只能手工写(现放 `USER CODE BEGIN 1` 区,属 L2,重生成不丢)+ 审计项 `fault-handler-enable` 兜底
- **`MDK-ARM/*.uvprojx` 每次 `@build` 后被 Keil UV4 自动改写,改动一律不入库**(2026-09-29 实测):UV4 构建时会把工程 XML 归一化成它本机风格的旧式写法(`<Tag />` → `<Tag></Tag>`、XML 声明加 `standalone="no"`),并把 `PackID` 降到**本机装的 DFP 版本**(本机实测 `3.1.1` → `3.0.0`,属**降级**)。这些差异是环境相关的,提交后会把别人的 Pack 版本也一起改掉。**判断方法**:`git diff MDK-ARM/STM32F407-Demo.uvprojx` 里若只见 XML 格式/`PackID` 差异而**无源文件挂载增删** → `git checkout -- MDK-ARM/STM32F407-Demo.uvprojx` 丢弃。只有真的新增/删除源文件(`grep 文件名` 计数变化)才提交
+6 -5
View File
@@ -400,8 +400,8 @@ void task_wd_beat(task_wd_id_t id) {
* IWDG-DEFER-START-ANCHOR 注释)。此处才真正启动——使"启动段不喂狗"
* 成立(§4.5)。IWDG 一旦启动不可停,只能靠复位终止。
* ⚠ PR/RLR 的写入由 wd_platform_start() 一并完成(v1.4 修复):
* 删掉 HAL_IWDG_Init() 后它是唯一写这两只寄存器的路径,否则寄存器
* 停在复位默认值、超时只有 ~400ms(详见该函数说明)。
* 生成的 HAL_IWDG_Init() 被改名成空实现后它是唯一写这两只寄存器的路径,
* 否则寄存器停在复位默认值、超时只有 ~400ms(详见该函数说明)。
*/
void task_wd_start(void) {
if (s_started != 0u) {
@@ -553,9 +553,10 @@ __weak void wd_platform_feed(void) {
}
/* ==== 为什么 IWDG 参数必须在这里写(2026-10-02 台架实测,P0 根因)====
* `Src/iwdg.c` 为实现"延后启动"删掉了 `HAL_IWDG_Init()`(见该文件
* IWDG-DEFER-START-ANCHOR),而 `HAL_IWDG_Init()` 是**唯一**会把
* `hiwdg.Init.Prescaler/Reload` 写进寄存器的路径——删掉后 `MX_IWDG_Init()`
* `Src/iwdg.c` 为实现"延后启动"把生成的 `HAL_IWDG_Init(&hiwdg)` 改名成空实现
* (见该文件 `USER CODE BEGIN 0` 里的 IWDG-DEFER-START-ANCHOR;2026-10-09 起
* 不再靠"每次重生成后删掉那一句"),而 `HAL_IWDG_Init()` 是 HAL 里**唯一**会把
* `hiwdg.Init.Prescaler/Reload` 写进寄存器的路径——改名后 `MX_IWDG_Init()`
* 只填结构体,PR/RLR 保持**复位默认值**(PR=0 → /4、RLR=4095)。
* 实测读数:`IWDG->PR = 0`、`IWDG->RLR = 4095` → 实际超时 4×4096/LSI
* ≈ 349~512ms,比设计的 1701~2498ms **短约 5 倍**。后果有二:
+29 -17
View File
@@ -21,7 +21,32 @@
#include "iwdg.h"
/* USER CODE BEGIN 0 */
/*
* IWDG-DEFER-START-ANCHOR — 看门狗"延后启动"改动(勿删本块)
*
* CubeMX 每次都会在 MX_IWDG_Init() 的生成区里写
* if (HAL_IWDG_Init(&hiwdg) != HAL_OK) { Error_Handler(); }
* 而 HAL_IWDG_Init() 内部第一句就是 __HAL_IWDG_START()(写 KR=0xCCCC,**立即启动**)。
* 本函数被 CubeMX 插在 main() 的 MX_*_Init 序列里,早于 app_main_init()——那段含
* NAND 初始化 / RTC / ADC,耗时可达数秒且无喂狗点,按 2.5s 超时的最坏值(≈1.7s)
* 必然复位 → **开机死循环**(2026-09-29 实测确认)。
*
* ⚠ "反初始化"这条路走不通:IWDG 一旦启动就**无法停止**(参考手册:唯一停止手段是
* 复位),所以不能在 `IWDG_Init 2` 区里把它"撤销"掉。
* ✅ 免手工的做法(2026-10-09 改,替代此前"每次重生成后删掉那一句"):把本编译单元里
* 的 HAL_IWDG_Init **改名到下面的空实现**。宏只作用于其后、本文件内的调用点
* (含生成区那一句),生成代码一行都不用动 → 重生成后自动失效,不必再手工删。
* 真正的启动移交 task_wd_start() → wd_platform_start()(App/util/task_watchdog.c),
* 由它解锁并写 PR/RLR、再写 KR=0xCCCC 启动(见 docs/子系统设计/看门狗设计说明.md §4.5 决策 C)。
* ⚠ 本宏只影响本编译单元;task_watchdog.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)
/* USER CODE END 0 */
IWDG_HandleTypeDef hiwdg;
@@ -40,23 +65,10 @@ void MX_IWDG_Init(void)
hiwdg.Instance = IWDG;
hiwdg.Init.Prescaler = IWDG_PRESCALER_64;
hiwdg.Init.Reload = 1249;
/* IWDG-DEFER-START-ANCHOR — 本锚点与下方改动一体,重生成会一并消失,
* 供 scripts/check_cubemx_survival.py 检出(勿删):
*
* ⚠ 本行为生成区改动(生成区里**唯一**被手工改的一句,2026-09-29 起):
* 原为 `if (HAL_IWDG_Init(&hiwdg) != HAL_OK) { Error_Handler(); }`。
* 原因:HAL_IWDG_Init() 内部第一句即 __HAL_IWDG_START()(立即启动看门狗,
* 之后不可停、只能复位)。而本函数被 CubeMX 插在 main() 的 MX_*_Init 序列里,
* 早于 app_main_init()——那段含 NAND 初始化/RTC/ADC,耗时可达数秒且无喂狗点,
* 按 2.5s 超时最坏值(≈1.7s)必然复位 → **开机死循环**(2026-09-29 实测确认)。
* 故只填参数不启动;启动移交 task_wd_start()(App/util/task_watchdog.c),
* 由 defaultTask 在存储 bring-up 完成后调用——即"启动段不喂狗、主循环段才喂"
* (见 docs/子系统设计/看门狗设计说明.md §4.5 决策 C)。
*
* ⚠ 若 CubeMX 重生成覆盖此处,症状 = **开机即复位**(LED 不闪、日志停在早期)。
* 恢复方法:删掉 HAL_IWDG_Init 调用,改为本注释形态(含锚点)。
* 注:不可把下面两句赋值与这里的替换一起塞进 IWDG_Init 1 区——该区位于
* 两句赋值**之前**,HAL 调用在区外,仍会被覆盖。故此改动只能在生成区。 */
if (HAL_IWDG_Init(&hiwdg) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN IWDG_Init 2 */
/* USER CODE END IWDG_Init 2 */
+21 -13
View File
@@ -880,7 +880,7 @@ external pin → "pin reset" (新增)
|---|---|---|---|
| **L1 写进 `.ioc`** | 让 CubeMX **自己生成**这份定制 | 凡是 CubeMX 界面能表达的配置 | ⭐⭐⭐ **根治**——配置成了事实源,重生成只会照着写 |
| **L2 搬进 `USER CODE` 区** | 把手工代码移到 `/* USER CODE BEGIN X */` 与 `/* USER CODE END X */` 之间 | 有现成标记区的文件(`Src/freertos.c` / `mpu_wrappers` 等),或 CubeMX 为该函数生成了标记区 | ⭐⭐ **有效**——`KeepUserCode=true` 时 CubeMX 逐字保留 |
| **L3 审计 + 基线** | 无法 L1/L2 的存量,用脚本定期核对 | 标记区外的存量、`Src/iwdg.c` 这类"生成区必须改动"的特例 | ⭐ **兜底**——不防丢失,但**丢了一定知道** |
| **L3 审计 + 基线** | 无法 L1/L2 的存量,用脚本定期核对 | 标记区外的存量(如 4 个 Fault Handler 实体) | ⭐ **兜底**——不防丢失,但**丢了一定知道** |
#### L1:哪些定制能写进 `.ioc`
@@ -963,24 +963,31 @@ external pin → "pin reset" (新增)
> → **只信 CubeMX 当前模板真实生成的区名**,用重生成后的文件核对,别凭记忆。
> → **宁可用文件头 `USER CODE BEGIN 0` 这类"区名从未变过"的标准位**。
>
> **② 生成区内无标记位可用的改动,只能 L3 兜底**。`Src/iwdg.c` 需要改的是
> `hiwdg.Init.*` 赋值与 `HAL_IWDG_Init()` 调用之间那一句,而 CubeMX 只在调用**之前**
> (`IWDG_Init 0/1`)和**之后**(`IWDG_Init 2`)生成标记区——**正中间没有**。
> 塞进 `IWDG_Init 1` 区也不能阻止区外那行被覆盖(HAL 调用在区外)。
> → 该改动**只能在生成区**,靠 `IWDG-DEFER-START-ANCHOR` 锚点注释 + 审计脚本检出。
> 同理 `HardFault_Handler` 本体(本代模板整函数重写,函数体外无区)。
> **② 生成区里那句"必须去掉"的调用,可用宏改名把它升级成 L2(2026-10-09)**。
> `Src/iwdg.c` 要处理的是 `hiwdg.Init.*` 赋值与 `HAL_IWDG_Init()` 调用之间那一句,而
> CubeMX 只在调用**之前**(`IWDG_Init 0/1`)和**之后**(`IWDG_Init 2`)生成标记区——
> **正中间没有**;塞进 `IWDG_Init 1` 区也阻止不了区外那行被覆盖(HAL 调用在区外)。
> 此前只能"每次重生成后手工删那一句"(L3)。现在的做法:在文件头 `USER CODE BEGIN 0`
> (标准保护位)放一个空实现 + `#define HAL_IWDG_Init(a_h) iwdg_deferred_start(a_h)`,
> 把**本编译单元里**那句生成调用改名到空实现 → 生成区保持原样、重生成**零 diff**,
> 不必再手工删。`.map` 证实 `Removing stm32f4xx_hal_iwdg.o(i.HAL_IWDG_Init), (94 bytes)`
> ——真 HAL 初始化根本没被链接进来,`MX_IWDG_Init` 只剩填结构体那几条指令。
> ⚠ **不能走"反初始化"**:IWDG 一旦启动就**无法停止**(唯一手段是复位),所以只能在
> 启动**之前**拦截,不能在 `IWDG_Init 2` 区里"撤销"。
> 同一技巧也用于 4 个 Fault Handler(`#define HardFault_Handler HardFault_Handler_Gen`
> 把**生成实体**改名,我们的实现放 `USER CODE BEGIN 0`)。区别:Handler 是"定义"而非
> "调用",所以我们的实现必须写在 `#define` **之前**,否则会被自己的宏改写。
#### L3:存量清单(标记区外,只能靠人记 + 脚本盯)
| 文件 | 内容 | 为什么无法 L1/L2 |
|---|---|---|
| `Src/iwdg.c` | 不调 `HAL_IWDG_Init()`(**P0:开机死循环防线**) | 生成区内无标记位可用(见上方硬教训②)。锚点 `IWDG-DEFER-START-ANCHOR` + 审计 FAIL 项 |
| `Src/stm32f4xx_it.c` | 4 个 Fault Handler 实体(`HardFault` / `MemManage` / `BusFault` / `UsageFault`) | 本代模板整函数重写、函数体外无区。各带独立锚点(`HARD-FAULT-ANCHOR` / `MEMFAULT-ANCHOR` / `BUSFAULT-ANCHOR` / `USAGEFAULT-ANCHOR`)+ 审计 FAIL 项 |
| `Src/freertos.c` | `.stack_size` 行内的中文注释 | 该行由 CubeMX 按 `.ioc` 生成,"值"受保护、"注释"不受。**注释是锦上添花**,丢失不影响功能(值在 `.ioc` 里) |
| `Src/stm32f4xx_it.c` | 4 个 Fault Handler 实体(`HardFault` / `MemManage` / `BusFault` / `UsageFault`) | 本代模板整函数重写、函数体外无区。各带独立锚点(`HARD-FAULT-ANCHOR` / `MEMFAULT-ANCHOR` / `BUSFAULT-ANCHOR` / `USAGEFAULT-ANCHOR`)+ 审计 FAIL 项。⚠ 可用与 iwdg 同一招根治(把实现搬进 `USER CODE BEGIN 0`、写在 4 个 `#define` **之前**),尚未做 |
| `Src/freertos.c` | `.stack_size` 行内的中文注释 | 该行由 CubeMX 按 `.ioc` 生成,"值"受保护、"注释"不受。**注释是锦上添花**,丢失不影响功能(值在 `.ioc` 里);核算依据已集中到 `USER CODE BEGIN Variables` 区(L2) |
> **结论**:L3 清单只剩 2 项(iwdg 生成区改动、4 个 Fault Handler 实体)——**均为
> 生成区内无标记位可用的客观限制**,靠锚点注释 + 审计 FAIL 兜底(丢了一定知道)。
> 其余要么已在 `.ioc`(L1),要么已进 `USER CODE` 区(L2)。
> **结论**:L3 清单只剩 **1 项**(4 个 Fault Handler 实体)——`Src/iwdg.c` 已于
> 2026-10-09 用宏改名升级为 L2(见上方硬教训②)。其余要么已在 `.ioc`(L1),
> 要么已进 `USER CODE` 区(L2)。
> 剩余 1 项 WARN 是 `Src/freertos.c` 的 `timeSyncTask` 手工定义(属性块在
> `USER CODE BEGIN Variables` 区,本身受保护;WARN 只是提示"它不在 `.ioc` 里")。
@@ -1130,3 +1137,4 @@ python scripts/check_cubemx_survival.py --write # 重建基线(确认改动
| 2026-10-01 | **新增 §7.3 主机侧单元测试(L0.5)** | 硬件访问收敛到薄平台层(`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`(新增 L0.5 层)同步。顺带改善:`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 L0+L0.5 全绿(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 延后启动由 L3 升级为 L2:不再需要每次重生成后手工删那一句** | 起因是提问"能否在初始化函数下方的自定义区里先反初始化"。查证:**反初始化不可行**——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 硬教训②与 L3 存量清单同步:L3 只剩 4 个 Fault Handler 实体(同一招可根治,尚未做)。`@build` 0 错误 0 警告;审计 21 通过 / 1 警告 / 0 失败 |
+12 -12
View File
@@ -62,22 +62,22 @@ BASELINE = os.path.join(REPO, ".cubemx_survival_baseline.json")
CHECKS = [
# ---------------- P0:开机死循环防线 ----------------
# ⚠ 这一项是**生成区内的手工改动**(无 USER CODE 区可用):
# 需要改的是 `hiwdg.Init.*` 赋值与 `HAL_IWDG_Init()` 调用之间的位置,
# 而 CubeMX 只在该调用**之前**(IWDG_Init 0/1)和**之后**(IWDG_Init 2)
# 生成标记区——正中间没有。塞进 IWDG_Init 1 区也不能阻止区外那行被覆盖
# (另见 `usr init 1` 区的注释)。
# 故本项按"文件整体 + 特征串"检查,并靠一个稳定锚点注释定位。
# ✅ 2026-10-09 由 L3 升级为 L2:不再删生成区那句 `HAL_IWDG_Init(&hiwdg)`,
# 而是在 `USER CODE BEGIN 0` 里放一个空实现 + `#define HAL_IWDG_Init(a_h)
# iwdg_deferred_start(a_h)`,把本编译单元里那句生成调用改名到空实现。
# → 生成区保持原样、重生成**零 diff**,不必再手工删那一句。
# ⚠ "反初始化"不可行:IWDG 启动后无法停止(唯一手段是复位),故只能拦截启动本身。
# 判据用区内的代码符号(protected 项会剥掉注释再比对,锚点注释不能当断言)。
dict(
id="iwdg-defer-start",
kind="wholefile",
kind="protected",
path="Src/iwdg.c",
must_contain=["IWDG-DEFER-START-ANCHOR"], # 手工锚点注释,重生成后会消失
must_not_contain=["HAL_IWDG_Init(&hiwdg)"], # 真实调用不得存在
region="0",
must_contain=["iwdg_deferred_start", "#define HAL_IWDG_Init"],
severity="FAIL",
hint="MX_IWDG_Init() 不得调 HAL_IWDG_Init()(内部立即启动,"
"而它跑在 app_main_init 之前 → 开机必复位)。启动移交 task_wd_start()。"
"恢复后补回 `IWDG-DEFER-START-ANCHOR` 锚点注释。",
hint="MX_IWDG_Init() 不得真正启动 IWDG(HAL_IWDG_Init 内部立即启动,而它跑在 "
"app_main_init 之前 → 开机必复位)。现由 `USER CODE BEGIN 0` 的空实现 + 宏改名"
"拦截,启动移交 task_wd_start();丢了宏 = 启动段看门狗就跑起来 = 开机死循环。",
),
# ---------------- P1:崩溃可观测性 ----------------
# ⚠ 2026-09-29 重要变更:CubeMX 把本文件的中断函数模板**换了一代**——