From 67d26ac37a75da6a4f56d9bbfda27e0b1198073f Mon Sep 17 00:00:00 2001
From: Wanderingss <1624155937@qq.com>
Date: Tue, 29 Sep 2026 07:52:39 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20CubeMX=20=E9=87=8D?=
=?UTF-8?q?=E7=94=9F=E6=88=90=E9=98=B2=E4=B8=A2=E5=A4=B1=E6=9C=BA=E5=88=B6?=
=?UTF-8?q?=EF=BC=9A=E5=AE=A1=E8=AE=A1=E8=84=9A=E6=9C=AC=E5=AE=9E=E6=B5=8B?=
=?UTF-8?q?=E6=9C=89=E6=95=88=EF=BC=8C=E6=81=A2=E5=A4=8D=205=20=E5=A4=84?=
=?UTF-8?q?=E8=A2=AB=E8=A6=86=E7=9B=96=E5=AE=9A=E5=88=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
第二次重生成验证了审计机制,并证伪了上一轮的一处假设:
- 审计脚本 check_cubemx_survival.py 准确检出全部 5 处覆盖(16 项中 5 FAIL),
机制有效;Src/main.c 复位原因锁存(USER CODE BEGIN Init 区)完好,
是"USER CODE 区有效"的正向证据。
- 关键发现:CubeMX 本次换了一代中断函数模板(旧 HardFault_Handler /
HardFault_Report 自造区 → 新函数体内 HardFault_IRQn 0 / W1_HardFault_IRQn 0),
"生成区之外的自造标记区"同样不被保留,旧假设被证伪。
恢复方案(第二次升级):
- stm32f4xx_it.c:报告实现迁入 USER CODE BEGIN 0(区名从未变过的标准保护位);
Handler 本体改为生成区整函数替换,用 #define HardFault_Handler
HardFault_Handler_Gen 把生成实体改名保留,链接器自动移除(.map 实证
Removing ... (2 bytes)),向量表指向手工 __asm 版(0x080001a1,18 字节)。
- iwdg.c:去掉 HAL_IWDG_Init() 调用,补 IWDG-DEFER-START-ANCHOR 锚点注释。
- FreeRTOSConfig.h:恢复 FREE_RTOS_CONFIG_HARDENING 块(两个加固宏)。
- freertos.c:补回两处任务栈中文注释(栈值未丢,.ioc 已同步)。
配套:
- check_cubemx_survival.py:hardfault-handler 改 wholefile + 锚点检查;
UNPROTECTED_LEGACY 清单更新。
- AGENTS.md:新增"CubeMX 换模板"与"ARMCC 内联汇编语法"两条铁律,
以及"uvprojx 被 Keil 自动改写不入库"条目。
- 看门狗设计说明.md:§10.1 补第二次重生成验证结论与模板换代发现,
§10.2 L2/L3 全面改写(自造区名不可依赖)。
验证:@build 0 错误 0 警告;pre-commit 13 钩子全通过;审计 16 通过 / 2 警告 / 0 失败。
---
.cubemx_survival_baseline.json | 7 +-
AGENTS.md | 4 +-
Src/freertos.c | 13 ++-
Src/iwdg.c | 23 +++--
Src/stm32f4xx_it.c | 152 +++++++++++++++++-------------
docs/子系统设计/看门狗设计说明.md | 114 +++++++++++++++++-----
scripts/check_cubemx_survival.py | 87 +++++++++++++----
7 files changed, 271 insertions(+), 129 deletions(-)
diff --git a/.cubemx_survival_baseline.json b/.cubemx_survival_baseline.json
index 4f4274de..1b4cbe66 100644
--- a/.cubemx_survival_baseline.json
+++ b/.cubemx_survival_baseline.json
@@ -1,15 +1,14 @@
{
"snapshot": {
- "iwdg-defer-start": "0aad7da77d2ed59c",
- "hardfault-report": "83aa6eee48e67318",
- "hardfault-report-impl": "20bee193982cd39b",
+ "iwdg-defer-start": "f5c340730afbb4b7",
+ "hardfault-handler": "2afdbb1dcdc9a954",
+ "hardfault-report-impl": "7db7627c59f9f004",
"stack-overflow-check": "4548c9c6ef1b2409",
"malloc-failed-hook": "4548c9c6ef1b2409",
"assert-not-deadloop": "d4a00718ab2bd071",
"assert-impl": "b7fd1fadb4522edf",
"stack-overflow-hook-impl": "b7fd1fadb4522edf",
"watchdog-module": "3c70ea43ff03523e",
- "watchdog-in-project": "328ce92129bdc449",
"reset-cause-latch": "52fc71acbb0f1aed",
"iwdg-hal-driver": "4b902875a84554e9",
"hal-iwdg-enabled": "c8c9b916b05464b9",
diff --git a/AGENTS.md b/AGENTS.md
index cebe253f..30e1dbd5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -38,7 +38,8 @@ 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 处定制,其中 P1 两处**不报编译错**、只在真出故障时暴露)。防止定制丢失按三层优先级:**L1 写进 `.ioc`**(凡 CubeMX 界面能表达的配置,如任务栈/优先级/队列/事件组/堆大小/IWDG 参数——权限最高,重生成只会照写)→ **L2 搬进 `USER CODE` 区**(可**自造**标记区:CubeMX 会保留任何 `/* USER CODE BEGIN 名 */`…`/* USER CODE END 名 */` 对,即使区名非预定义)→ **L3 审计兜底**。**每次重生成后立刻跑 `python scripts/check_cubemx_survival.py`**(已挂 pre-commit `cubemx-survival` 钩子;改动 `.ioc`/`Src/`/`Inc/`/`task_watchdog`/`.uvprojx` 触发)——清单 16 项 + 哈希基线漂移检测,直接列出"被覆盖了哪几处"。方法论详见 `docs/子系统设计/看门狗设计说明.md` §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` 触发)——清单 16 项 + 哈希基线漂移检测,直接列出"被覆盖了哪几处"。⚠️ **不要依赖"自造标记区"**:2026-09-29 实测 CubeMX **换了一代中断函数模板**(旧 `HardFault_Handler`/`HardFault_Report` 自造区 → 新函数体内 `HardFault_IRQn 0`/`W1_HardFault_IRQn 0`),旧区**连同内容一起消失**(看起来仍像"遵守了规则")。**生成区内无标记位可用的改动只能 L3 兜底**(如 `Src/iwdg.c` 的 `HAL_IWDG_Init()` 调用、`HardFault_Handler` 本体——本代模板整函数重写),一律加锚点注释(`IWDG-DEFER-START-ANCHOR` / `HARD-FAULT-ANCHOR`)供审计检出。方法论详见 `docs/子系统设计/看门狗设计说明.md` §10.1/§10.2
+- **ARMCC 内联汇编语法(2026-09-29 踩坑)**:`__asm volatile ("MRS %0, lr" : "=r" (x))` 是 **GCC 语法**,ARMCC 报 `#18: expected a ")"`;`__asm { MRS x, lr }` 块内联会把寄存器名当 C 标识符报 `#20: identifier "lr" is undefined`。**唯一可行写法**:整个函数用 `__asm void f(void) { ... }`。若要覆盖 CubeMX 生成的同名函数,用 `#define f f_Gen`(在生成实体之前)把生成实体改名——链接器会把未被引用的 `f_Gen` 自动移除(`.map` 可见 `Removing ... (N bytes)`),**零代码开销且不删生成区任何一行**
- FreeRTOS V10.3.1(CMSIS-RTOS V2);HAL 时基 = TIM7(非 SysTick),tick = 1ms;NVIC 4 位优先级分组,外设中断优先级 ≥ 5
- **内核启动(`osKernelStart`)前禁止任何 FreeRTOS/CMSIS-RTOS API 调用**(2026-09-20 启动失败实证根因,修复 `82411cf`):`osMessageQueueNew`/`osMessageQueuePut`/`osMutexNew`/`osSemaphoreNew` 等在内核启动前调用会杀死启动(09/19~09/20 "每次必死" 的代码层根因 = `syslog_mcu_init` 在内核启动前调用 `osMessageQueueNew`)。内核启动前代码的标准模式:纯寄存器/HAL 轮询操作 + **无锁静态缓冲**(如 `s_preboot[8]` 暂存内核前日志)+ **内核启动后惰性创建真队列**(任务上下文首次使用时,见 `syslog_drain`);凡在内核启动前执行的初始化函数里不得创建任何 CMSIS 对象
- CH395F SPI:每次事务须 `ch395f_spi_begin()/end()` 包裹;分频系数**必须为 8**(PCB 约束,勿"优化")
@@ -90,6 +91,7 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
- **单连接 socket 的断开语义**:监听 Socket 在 CONNECT 后自身即数据通道(`g_client == g_listen`),EOF 时 net 层已 `auto_relisten`;应用层**不得**在 EOF 时 `net_close` 该 fd(会连监听一起毁掉 → 端口"连接被拒"),仅当数据 fd ≠ 监听 fd(模拟器多连接后端)时才关
- 网口配置:`/config.ini` [network] 段(工厂默认 192.168.1.100/24、gw .1;MAC 全零 = 芯片内置;UI 保存立即生效;旧档 `/cfg/netcfg.bin` 首启自动迁移)→ `docs/子系统设计/网络配置说明.md`
- **新 .c 文件必须手动注册到两处**才能双端编译:`MDK-ARM/STM32F407-Demo.uvprojx` + `simulator/CMakeLists.txt`(用 `new-source-file` skill)
+- **`MDK-ARM/*.uvprojx` 每次 `@build` 后被 Keil UV4 自动改写,改动一律不入库**(2026-09-29 实测):UV4 构建时会把工程 XML 归一化成它本机风格的旧式写法(`` → ``、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 文件名` 计数变化)才提交
- 测试构建:`test/board/test_config.h` 的 `TEST_SUITE_CH395F/STORAGE/GD5F/RTC` **互斥**(一次只能启用一个,`#error`);全部注释 = 产品构建;**破坏性用例默认关闭**:同文件 `ENABLE_DESTRUCTIVE_TESTS`(默认注释)控制写/擦 NAND、格式化、坏块注入与 FatFS 写类用例——关闭时套件为**只读模式**(开机横幅 `mode: READ-ONLY`,可随时在在用设备上跑、不动卷内容与容量),只在**产品首次上电/产线首启**或明确需要时打开(`mode: DESTRUCTIVE`);2026-09-21 实测一轮破坏性 GD5F 运行即让 dhara 多记一个坏块(容量 95248 → 95200;该计数持久化在 dhara journal checkpoint 头里,普通恢复模式清不掉,需全片擦除——见 `docs/测试说明/存储维护与容量恢复.md`);CH395F 套件 `test/board/ch395f_test.h`:硬件层 Phase 1~5 与 NET 层 Phase 6~11 互斥;CH395F 硬件层模式下 netTask(基础线程:无条件创建、全构建唯一实例)入口自挂起(不运行),testTask 独占 CH395F
- **改驱动前先读对应 `docs/问题记录/*_Trap_Records.md`**(CH395F / GD5F2GQ5UE / SD2506);各模块测试指南见 `docs/测试说明/`;**存储维护/容量恢复**(全片擦除恢复 dhara 坏块记账)见 `docs/测试说明/存储维护与容量恢复.md`
- **LVGL I1 绘制缓冲含 palette 头(2026-09-21 实证)**:索引色格式 draw buffer 布局 = `[palette 8B][pixel data stride×h]`(`lv_draw_buf_create` 加 8B、`lv_draw_buf_goto_xy`/`lv_refr get_max_row` 都按此跳/扣),但 `lv_display_set_buffers`/`lv_draw_buf_init` 的容量校验**只看 `stride×h`**——只给 3200B 会被静默接受:末 8 个像素字节写到缓冲外(曾打脏相邻 `s_frame_box[0]` 指针 → HardFault),且 flush 收到的首地址含 palette(整帧错位 8 字节)。`ui_port.c` 按 `stride×h + palette` 供给并在 flush 时 `+palette` 取像素;详见 `docs/UI设计/UI_设计架构.md` §1.4 第 8 条
diff --git a/Src/freertos.c b/Src/freertos.c
index 08920b6e..e005ef89 100644
--- a/Src/freertos.c
+++ b/Src/freertos.c
@@ -68,10 +68,11 @@ const osThreadAttr_t timeSyncTask_attributes = {
osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes = {
.name = "defaultTask",
- .stack_size = 1024 * 4, /* 512→1024: 该任务扛 mount+mkfs+FAT探针+cfg加载+syslog写+
- * 配置引擎(全深层 FatFS I/O),2KB 实测峰值溢出——开启
- * configCHECK_FOR_STACK_OVERFLOW=2 后被钩子抓到(2026-09-21)。
- * 此前静默溢出腐蚀堆相邻块。改 .ioc 前须同步此值防 CubeMX 覆盖 */
+ /* 512→1024: 该任务扛 mount+mkfs+FAT探针+cfg加载+syslog写+配置引擎
+ * (全深层 FatFS I/O),2KB 实测峰值溢出——开启
+ * configCHECK_FOR_STACK_OVERFLOW=2 后被钩子抓到(2026-09-21)。
+ * ⚠ 注释位于 .ioc 生成区之外,CubeMX 重生成会丢(值本身在 FREERTOS.Tasks01 里,不丢)。 */
+ .stack_size = 1024 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* Definitions for netTask */
@@ -92,7 +93,9 @@ const osThreadAttr_t ftpTask_attributes = {
osThreadId_t testTaskHandle;
const osThreadAttr_t testTask_attributes = {
.name = "testTask",
- .stack_size = 2048 * 4, /* 1024→2048: 存储套件 deep call chain + 局部缓冲,4KB 实测溢出(2026-09-20) */
+ /* 1024→2048: 存储套件 deep call chain + 局部缓冲,4KB 实测溢出(2026-09-20)。
+ * ⚠ 注释位于 .ioc 生成区之外,CubeMX 重生成会丢(值本身在 FREERTOS.Tasks01 里,不丢)。 */
+ .stack_size = 2048 * 4,
.priority = (osPriority_t) osPriorityBelowNormal,
};
/* Definitions for uiTask */
diff --git a/Src/iwdg.c b/Src/iwdg.c
index aeea85df..26e90206 100644
--- a/Src/iwdg.c
+++ b/Src/iwdg.c
@@ -40,18 +40,23 @@ void MX_IWDG_Init(void)
hiwdg.Instance = IWDG;
hiwdg.Init.Prescaler = IWDG_PRESCALER_64;
hiwdg.Init.Reload = 1249;
- /* USER CODE BEGIN IWDG_DEFER_START */
- /* ⚠ 生成区改动(唯一一处):原为 `HAL_IWDG_Init(&hiwdg)` 直呼。
+ /* 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)。
- * 若 CubeMX 重生成覆盖此处,表现为"开机即复位",按本注释恢复即可。 */
- /* USER CODE END IWDG_DEFER_START */
+ * 按 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 调用在区外,仍会被覆盖。故此改动只能在生成区。 */
/* USER CODE BEGIN IWDG_Init 2 */
/* USER CODE END IWDG_Init 2 */
diff --git a/Src/stm32f4xx_it.c b/Src/stm32f4xx_it.c
index 4a8e52cc..f900c8b6 100644
--- a/Src/stm32f4xx_it.c
+++ b/Src/stm32f4xx_it.c
@@ -53,70 +53,15 @@
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
-/* USER CODE END 0 */
-
-/* External variables --------------------------------------------------------*/
-extern DMA_HandleTypeDef hdma_spi1_rx;
-extern DMA_HandleTypeDef hdma_spi1_tx;
-extern DMA_HandleTypeDef hdma_spi2_rx;
-extern DMA_HandleTypeDef hdma_spi2_tx;
-extern SPI_HandleTypeDef hspi1;
-extern SPI_HandleTypeDef hspi2;
-extern DMA_HandleTypeDef hdma_usart1_tx;
-extern DMA_HandleTypeDef hdma_usart1_rx;
-extern UART_HandleTypeDef huart4;
-extern UART_HandleTypeDef huart5;
-extern UART_HandleTypeDef huart1;
-extern UART_HandleTypeDef huart2;
-extern UART_HandleTypeDef huart3;
-extern TIM_HandleTypeDef htim7;
-
-/* USER CODE BEGIN EV */
-
-/* USER CODE END EV */
-
-/******************************************************************************/
-/* Cortex-M4 Processor Interruption and Exception Handlers */
-/******************************************************************************/
-/**
- * @brief This function handles Non maskable interrupt.
- */
-void NMI_Handler(void)
-{
- /* USER CODE BEGIN NonMaskableInt_IRQn 0 */
-
- /* USER CODE END NonMaskableInt_IRQn 0 */
- /* USER CODE BEGIN NonMaskableInt_IRQn 1 */
- while (1)
- {
- }
- /* USER CODE END NonMaskableInt_IRQn 1 */
-}
-
-/**
- * @brief This function handles Hard fault interrupt.
- */
-/* USER CODE BEGIN HardFault_Handler */
-/* 崩溃现场采集(本函数为手工改写,CubeMX 重生成会覆盖!):
- * EXC_RETURN bit2 判断异常帧在 MSP 还是 PSP,取帧指针交给 C 报告函数。
- * 空 while(1) 版会让死机在日志上完全静默(2026-09-20 存储套件死机排查实证) */
-__asm void HardFault_Handler(void)
-{
- extern hard_fault_report
- TST lr, #4
- ITE EQ
- MRSEQ r0, MSP
- MRSNE r0, PSP
- B hard_fault_report
-}
-/* USER CODE END HardFault_Handler */
-
-/* USER CODE BEGIN HardFault_Report */
-/*
- * 函数功能:HardFault 现场报告(裸机上下文,不经内核/printf)
- * 入口参数:frame - 异常栈帧 {R0,R1,R2,R3,R12,LR,PC,xPSR}
- * 函数说明:直接 HAL_UART_Transmit 轮询输出 PC/LR/xPSR 与 CFSR/HFSR 故障
- * 寄存器,供开机日志定位崩溃点(对照 .map 找符号);输出后复位。
+/* HardFault 现场报告实现(2026-09-29 迁移至此,**保护区**内):
+ * 原放在紧随 HardFault_Handler 之后的自造区 `HardFault_Report` 里——
+ * 实测 CubeMX 重生成不会保留"生成区之外的自造标记区"(它整段按新模板重写),
+ * 故迁入本文件本来就有的 `USER CODE BEGIN 0`(生成区内的标准保护位),
+ * 从此重生成不会丢失。
+ *
+ * 说明:本函数由 __asm void HardFault_Handler 用 B 直接跳入(见下方),
+ * 不经内核、不 printf、不遍历指针,只用 HAL_UART_Transmit 轮询 + SCB 寄存器读取,
+ * 因此在内核状态已不可信时仍可用。输出后 HAL_Delay(200) 排空 UART 再复位。
*/
static void hard_fault_write(const char *s, uint16_t len)
{
@@ -181,7 +126,84 @@ void hard_fault_report(uint32_t *frame)
HAL_Delay(200);
NVIC_SystemReset();
}
-/* USER CODE END HardFault_Report */
+
+/* 为生成函数改名,避免与下方手工 `__asm` 实现符号冲突。
+ * 生成实体改名为 HardFault_Handler_Gen(永不执行,仅为占用名字不冲突,
+ * 保持生成区代码零删除)。 */
+#define HardFault_Handler HardFault_Handler_Gen
+/* USER CODE END 0 */
+
+/* External variables --------------------------------------------------------*/
+extern DMA_HandleTypeDef hdma_spi1_rx;
+extern DMA_HandleTypeDef hdma_spi1_tx;
+extern DMA_HandleTypeDef hdma_spi2_rx;
+extern DMA_HandleTypeDef hdma_spi2_tx;
+extern SPI_HandleTypeDef hspi1;
+extern SPI_HandleTypeDef hspi2;
+extern DMA_HandleTypeDef hdma_usart1_tx;
+extern DMA_HandleTypeDef hdma_usart1_rx;
+extern UART_HandleTypeDef huart4;
+extern UART_HandleTypeDef huart5;
+extern UART_HandleTypeDef huart1;
+extern UART_HandleTypeDef huart2;
+extern UART_HandleTypeDef huart3;
+extern TIM_HandleTypeDef htim7;
+
+/* USER CODE BEGIN EV */
+
+/* USER CODE END EV */
+
+/******************************************************************************/
+/* Cortex-M4 Processor Interruption and Exception Handlers */
+/******************************************************************************/
+/**
+ * @brief This function handles Non maskable interrupt.
+ */
+void NMI_Handler(void)
+{
+ /* USER CODE BEGIN NonMaskableInt_IRQn 0 */
+
+ /* USER CODE END NonMaskableInt_IRQn 0 */
+ /* USER CODE BEGIN NonMaskableInt_IRQn 1 */
+ while (1)
+ {
+ }
+ /* USER CODE END NonMaskableInt_IRQn 1 */
+}
+
+/**
+ * @brief This function handles Hard fault interrupt.
+ */
+/* HARD-FAULT-ANCHOR — 本锚点与下方改动一体,重生成会一并消失,
+ * 供 scripts/check_cubemx_survival.py 检出(勿删):
+ *
+ * 崩溃现场采集(手工改写)。EXC_RETURN bit2 判断异常帧在 MSP 还是 PSP,
+ * 取帧指针交给 C 报告函数 hard_fault_report()(实现在文件头 USER CODE BEGIN 0
+ * 保护区内)。空 while(1) 版会让死机在日志上完全静默(2026-09-20 实证)。
+ *
+ * 命名:`#undef HardFault_Handler` 后重定义为 __asm 版本,与向量表符号一致;
+ * 为避免与下方生成实体"重复定义"(ARMCC 报 #247,其"已定义"检查
+ * 早于 C 作用域),已在文件头用 `#define HardFault_Handler
+ * HardFault_Handler_Gen` 把生成实体改名为不冲突的符号。 */
+#undef HardFault_Handler
+__asm void HardFault_Handler(void)
+{
+ extern hard_fault_report
+ TST lr, #4
+ ITE EQ
+ MRSEQ r0, MSP
+ MRSNE r0, PSP
+ B hard_fault_report
+}
+
+void HardFault_Handler_Gen(void)
+{
+ while (1)
+ {
+ /* USER CODE BEGIN W1_HardFault_IRQn 0 */
+ /* USER CODE END W1_HardFault_IRQn 0 */
+ }
+}
/**
* @brief This function handles Memory management fault.
diff --git a/docs/子系统设计/看门狗设计说明.md b/docs/子系统设计/看门狗设计说明.md
index 7fdd7cab..39e67bf4 100644
--- a/docs/子系统设计/看门狗设计说明.md
+++ b/docs/子系统设计/看门狗设计说明.md
@@ -759,21 +759,62 @@ external pin → "pin reset" (新增)
| 级别 | 文件 | 被覆盖内容 | 恢复方式 |
|---|---|---|---|
-| **P0** | `Src/iwdg.c`(新文件) | CubeMX 生成的 `MX_IWDG_Init()` 直接调 `HAL_IWDG_Init()` → 开机死循环 | 改为只填参数;插 `USER CODE BEGIN IWDG_DEFER_START` 注释块说明(**唯一必需的手工改动**) |
-| **P1** | `Src/stm32f4xx_it.c` | `HardFault_Handler` 崩溃现场采集(91 行:PC/LR/xPSR/R0-R3/CFSR/HFSR/BFAR/MMFAR)被打回空 `while(1)` | 整块换回手工版(`__asm void` 汇编取 MSP/PSP + `hard_fault_report`) |
+| **P0** | `Src/iwdg.c`(新文件) | CubeMX 生成的 `MX_IWDG_Init()` 直接调 `HAL_IWDG_Init()` → 开机死循环 | 改为只填参数(**此改动在生成区,无 USER CODE 位可用**),插 `IWDG-DEFER-START-ANCHOR` 锚点注释供审计检出 |
+| **P1** | `Src/stm32f4xx_it.c` | `HardFault_Handler` 崩溃现场采集(PC/LR/xPSR/R0-R3/CFSR/HFSR/BFAR/MMFAR)被打回空 `while(1)` | 整函数替换为 `__asm` 版;生成实体用 `#define` 改名保留(链接器自动移除);**报告实现迁入 `USER CODE BEGIN 0`** |
| **P1** | `Inc/FreeRTOSConfig.h` | `configCHECK_FOR_STACK_OVERFLOW 2` + `configUSE_MALLOC_FAILED_HOOK 1` **整个块被删除** | 插回 `USER CODE BEGIN FREE_RTOS_CONFIG_HARDENING` 块 |
-| **P2** | `Src/freertos.c` | 三处中文注释丢失(任务栈大小的实测记录) | 恢复注释(栈**数值**未丢,`.ioc` 同步到位) |
+| **P2** | `Src/freertos.c` | 两处中文注释丢失(任务栈大小的实测记录) | 恢复注释(栈**数值**未丢,`.ioc` 同步到位) |
> ⚠️ **P1 两处的教训**:它们都在 `USER CODE` 标记区**之外**(`stm32f4xx_it.c` 的
> `HardFault_Handler` 由 CubeMX 直接生成;`FreeRTOSConfig.h` 的加固宏原本写在
> 非 `USER CODE` 区)。**P1 的丢失不会立刻暴露**——栈溢出检测与堆失败钩子只在
> 真发生溢出/耗尽时才起作用,且 `HardFault_Handler` 被打回空死循环后
> **崩溃时连现场都打不出来**(只剩 IWDG 兜底复位,但归因信息全丢)。
-> 故本次已把这三个宏搬进 `USER CODE BEGIN 1` 区,并加注释
-> "⚠ 本段为手工添加,CubeMX 重生成会删除(2026-09-29 实测),改完须核对本块存在"。
+> 故本次已把这三个宏搬进 `USER CODE BEGIN FREE_RTOS_CONFIG_HARDENING` 区。
-> **每步改完必须 `@build` 0 错误 0 警告**(`AGENTS.md` 铁律)。第 0~6 步已一次性
-> 验证通过:`BUILD_OK: 0 errors, 0 warnings`。
+> **每步改完必须 `@build` 0 错误 0 警告**(`AGENTS.md` 铁律)。
+
+#### ⚠️ 第 0 步复现(2026-09-29 第二次重生成):审计脚本有效,但机制假设需修正
+
+第一次重生成后我们建立了审计脚本并"加固"了恢复方案,随后**用户又重生成了一次**,
+据此验证机制:
+
+- ✅ **审计脚本准确检出全部 5 处覆盖**(16 项清单中 5 项 FAIL)——L3 兜底手段有效
+- ✅ **`Src/main.c` 的复位原因锁存完好**(在 `USER CODE BEGIN Init` 区)——L2 有效性的**正向证据**
+- ✅ `MDK-ARM/*.uvprojx` 中 `task_watchdog.c` 挂载保住
+
+但暴露出 **L2 的一个致命前提错误**:
+
+> **🔴 关键发现:CubeMX 会"换一代"中断函数模板 ——
+> "生成区之外的自造标记区"同样不被保留。**
+>
+> - **旧模板**:在 `HardFault_Handler` 函数**前后**生成自造区
+> `/* USER CODE BEGIN HardFault_Handler */` 与 `/* USER CODE BEGIN HardFault_Report */`
+> → 上一轮我们把手写实现放进这两个区,**当时确实生效了**。
+> - **新模板**:改成函数体**内部**的
+> `/* USER CODE BEGIN HardFault_IRQn 0 */` 与 `/* USER CODE BEGIN W1_HardFault_IRQn 0 */`
+> → 旧的 `HardFault_Handler` / `HardFault_Report` 区**整段消失**,放在里面的代码一并丢失。
+>
+> **教训**:`USER CODE` 区有效的前提是"**该区由 CubeMX 生成且名字仍被当前模板使用**"。
+> CubeMX 换模板(或改区名)时,**依赖旧区名的内容会连同区一起消失**,
+> 而它看起来仍像"我们遵守了规则"。**不要假设自造区名的稳定性——只信 CubeMX 当前
+> 模板真实生成的区名**(用一次重生成后的文件核对,而非记忆)。
+
+据此对 `stm32f4xx_it.c` 的恢复方案做了**第二次升级**:
+
+| 组件 | 上一轮落点 | **本轮最终落点** | 稳定性 |
+|---|---|---|---|
+| 崩溃报告实现(`hard_fault_report` 等) | 自造区 `HardFault_Report` | **`USER CODE BEGIN 0`**(文件头部,CubeMX 标准保护位,名字从未变过) | ⭐⭐ L2 真有效 |
+| `HardFault_Handler` 实体 | 自造区 `HardFault_Handler` | **生成区内整函数替换** + `HARD-FAULT-ANCHOR` 锚点;生成实体经 `#define HardFault_Handler HardFault_Handler_Gen` 改名保留,链接器自动移除(`.map` 实测 `Removing ... (2 bytes)`) | ⭐ L3 审计兜底 |
+
+> **为什么 Handler 只能放生成区**:CubeMX 本代模板把整个函数体(含 `HardFault_IRQn 0` /
+> `W1_HardFault_IRQn 0`)连同函数一起重写;函数体之外的自造区不保留。查询 `.map`
+> 确认向量表指向我们的 `__asm` 版(`0x080001a1`,`.emb_text`,18 字节),
+> 生成版被链接器丢弃 —— **零代码开销,且未删除生成区任何一行**。
+
+> **ARMCC 语法坑(本轮踩过)**:`__asm volatile ("MRS %0, lr" : "=r" (x))` 是 GCC 内联
+> 汇编语法,ARMCC(MDK)报 `#18: expected a ")"`;`__asm { MRS x, lr }` 块内联会把
+> `lr` 当 C 标识符报 `#20: identifier "lr" is undefined`。**唯一可行写法**是
+> 整个函数用 `__asm void f(void) { ... }`(旧代码即此写法,已验证可行)。
### 10.2 让定制"重生成不丢"的三层手段(**通用方法论**)
@@ -819,29 +860,41 @@ external pin → "pin reset" (新增)
| 文件 | 可用标记区 | 已用于 |
|---|---|---|
| `Inc/FreeRTOSConfig.h` | `Header` / `Includes` / `FREE_RTOS_CONFIG_HARDENING` / `MESSAGE_BUFFER_LENGTH_TYPE` / `1` / `Defines` | 加固宏(HARDENING)、`configASSERT`(1) |
-| `Src/freertos.c` | 共 25 个,含 `Includes` / `Application` / `Variables` / `PD` / `PTD` / … | 钩子实现(`Application`) |
-| `Src/stm32f4xx_it.c` | 共 52 个,**每个中断函数都有专属区**(`HardFault_Handler` / `DMA1_Stream3_IRQn` / …) | 崩溃采集(`HardFault_Handler` + `HardFault_Report`) |
+| `Src/freertos.c` | 共 25 个,含 `Includes` / `Application` / `Variables` / `PD` / `PTD` / … | 钩子实现(`Application`)、`timeSyncTask` 属性定义(`Variables`) |
+| `Src/stm32f4xx_it.c` | 共 52 个,含文件头 `0`(标准保护位)+ 各中断函数**内部**的 `_IRQn 0` / `W1__IRQn 0` | 崩溃报告实现(`0`) |
| `Src/main.c` | 共 19 个,含 `Init` / `SysInit` / `2` / `PV` / `PFP` | 复位原因锁存(`Init`) |
-| `Src/iwdg.c` | 共 7 个,含 `IWDG_Init 0/1/2` / `IWDG_DEFER_START`(我方新增) | 阻止自动启动(`IWDG_DEFER_START`) |
+| `Src/iwdg.c` | 共 7 个,含 `IWDG_Init 0/1/2` | ❌ **本次改动用不上**(见下方特例) |
| `Inc/main.h` | 共 7 个 | 引脚宏等 |
-> ⚠️ **`Src/iwdg.c` 的特例**:本次需要的"不自动启动"改动**必须落在生成区**
-> (`hiwdg.Init.*` 赋值语句与 `HAL_IWDG_Init()` 调用之间的位置),
-> 而 CubeMX **不为这两句之间生成标记区**。于是采取"**在生成区里插入一个
-> 自定义命名的 USER CODE 块**"——实测 CubeMX 会保留**任何**形态的
-> `USER CODE BEGIN/END` 注释对,即使区名不是它预定义的。这是 L2 的
-> 一个有用变体:**可以自己造标记区**。
+> 🔴 **两条硬教训(2026-09-29 实测,务必先读)**
+>
+> **① 不要依赖"自造标记区"**。上一轮我们按"CubeMX 保留任何形态的 `USER CODE` 注释对"
+> 的结论,在 `stm32f4xx_it.c` 里自造了 `HardFault_Handler` / `HardFault_Report` 区,
+> 当时**确实生效**。但第二次重生成时 CubeMX **换了中断函数模板**,区名从
+> `HardFault_Handler` 变成函数体内的 `HardFault_IRQn 0` / `W1_HardFault_IRQn 0`,
+> **旧区连同内容一起消失**。
+> → **只信 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` 本体(本代模板整函数重写,函数体外无区)。
#### L3:存量清单(标记区外,只能靠人记 + 脚本盯)
| 文件 | 内容 | 为什么无法 L1/L2 |
|---|---|---|
-| `Src/stm32f4xx_it.c` | `MemManage_Handler` / `BusFault_Handler` / `UsageFault_Handler` 的空 `while(1)`(**建议后续加崩溃采集**,目前 `HardFault_Handler` 已加) | CubeMX 不为这三个函数生成 `USER CODE` 区(与 `HardFault_Handler` 不同,后者有);可仿照 `HardFault_Handler` **自造标记区**(见 L2 变体) → **可升级为 L2** |
+| `Src/iwdg.c` | 不调 `HAL_IWDG_Init()`(**P0:开机死循环防线**) | 生成区内无标记位可用(见上方硬教训②)。锚点 `IWDG-DEFER-START-ANCHOR` + 审计 FAIL 项 |
+| `Src/stm32f4xx_it.c` | `HardFault_Handler` 实体 | 本代模板整函数重写、函数体外无区。锚点 `HARD-FAULT-ANCHOR` + 审计 FAIL 项 |
+| `Src/stm32f4xx_it.c` | `MemManage_Handler` / `BusFault_Handler` / `UsageFault_Handler` 的空 `while(1)`(**建议后续加崩溃采集**) | CubeMX 为这三者生成的是函数体**内部**的区,函数体被整段重写 → 只能整函数替换 + 审计。目前仅 WARN |
| `Src/freertos.c` | `.stack_size` 行内的中文注释 | 该行由 CubeMX 按 `.ioc` 生成,"值"受保护、"注释"不受。**注释是锦上添花**,丢失不影响功能(值在 `.ioc` 里) |
-> **结论**:真正必须靠 L3 盯的只有 3 个 Fault 处理函数;其余要么已在 `.ioc`(L1),
-> 要么已进 `USER CODE` 区(L2)。**建议后续把 3 个 Fault 处理函数也改成自造标记区版**,
-> 那样 L3 清单可清空。
+> **结论**:L3 清单已从"3 个 Fault 处理函数"扩展为"3 项生成区改动 + 3 个 Fault 函数"。
+> 前 3 项已挂审计 FAIL(丢了一定知道),后 3 项建议后续一并升级为崩溃采集 + 审计。
+> **L3 的定位是兜底而非根治**——只要改动落在生成区,就不可能 L1/L2。
#### L3 落地:审计脚本 + 基线
@@ -851,17 +904,26 @@ python scripts/check_cubemx_survival.py --check # 门禁模式(pre-commit
python scripts/check_cubemx_survival.py --write # 重建基线(确认改动无误后)
```
-- **清单**:16 项覆盖 P0/P1 全部防线(iwdg 延后启动、崩溃采集、两个加固宏、
- 断言改造、看门狗模块与工程挂载、复位原因锁存、HAL 驱动与使能宏、`.ioc` 关键项)
-- **漂移检测**:把各项内容哈希存 `.cubemx_survival_baseline.json`,`--check` 比对
+- **清单**:16 项覆盖 P0/P1 全部防线(iwdg 延后启动、崩溃采集 ×2、两个加固宏、
+ 断言改造 ×2、看门狗模块与工程挂载、复位原因锁存、HAL 驱动与使能宏、`.ioc` 关键项)
+- **已实测有效**:2026-09-29 第二次重生成时,脚本**准确检出全部 5 项被覆盖**(5 FAIL)
+- **漂移检测**:把各项内容哈希存 `.cubemx_survival_baseline.json`,`--check` 比对。
+ ⚠️ **`MDK-ARM/*.uvprojx` 排除在漂移检测之外**(15 项入基线,16 项参与断言)——
+ 该文件每次 `@build` 后被 Keil UV4 自动改写(XML 格式归一化 + `PackID` 降级到
+ 本机 DFP 版本),内容每次构建都不同;若参与检测则**每次构建后提交都会被拦**。
+ 脚本内以 `no_baseline=True` 标记,只保留"源文件仍挂载"断言
- **已挂 pre-commit**(`cubemx-survival` 钩子):改动 `.ioc` / `Src/` / `Inc/` /
`task_watchdog` / `.uvprojx` 时自动触发
- **注释过滤**:断言前先剥掉 C 注释,避免"注释里提到 `HAL_IWDG_Init`"被误判
- (本项目多处注释刻意引用历史代码,不做过滤会恒定误报)
+ (本项目多处注释刻意引用历史代码,不做过滤会恒定误报);
+ 但**正项检查(`must_contain`)在 `wholefile` 模式下不剥注释**,
+ 因为锚点本身就是注释(`IWDG-DEFER-START-ANCHOR` / `HARD-FAULT-ANCHOR`)。
+ 脚本内以 `probe`(正项)/ `probe_neg`(禁项)分离处理
> **推荐工作流**:**每次 CubeMX 重生成后,立刻跑一次
> `python scripts/check_cubemx_survival.py`**——它会把"被覆盖了哪几处"
> 直接列出来(含修复提示),比事后靠故障现象反推快得多。
+> 本项目的两次重生成都是靠它把损失锁定在"5 处、含修复提示"的范围内。
---
@@ -871,7 +933,7 @@ python scripts/check_cubemx_survival.py --write # 重建基线(确认改动
|---|---|
| **选型** | **IWDG**(独立 LSI),非 WWDG(依赖 APB1 + 中断,在关中断/HardFault 场景失效) |
| **实施路径** | **CubeMX 勾选 IWDG 生成**(`.ioc` = 配置单一事实源,与其他外设一致);唯一手工改动 = `Src/iwdg.c` 中阻止 `HAL_IWDG_Init()` 立即启动(§2.2 / §4.5) |
-| **防丢失** | **L1 写 `.ioc` > L2 搬进 `USER CODE` 区 > L3 审计兜底**(§10.2);已落 `scripts/check_cubemx_survival.py` + pre-commit 钩子 |
+| **防丢失** | **L1 写 `.ioc` > L2 搬进 `USER CODE` 区 > L3 审计兜底**(§10.2);⚠️ **自造区名不可依赖**(CubeMX 换模板会连区带内容一起删,2026-09-29 实测);已落 `scripts/check_cubemx_survival.py` + pre-commit 钩子 |
| **架构** | **任务心跳位图(各自置位,无锁单字)+ defaultTask 单一喂狗者**——避免"有人跑就喂狗"的假保护 |
| **超时** | 2498 ms(PR=4 / RLR=1249);最坏(LSI 47 kHz)≈1701 ms;中间留 800 ms 停喂判定裕度 |
| **启动时机** | **defaultTask bring-up 完成后**——绕开初始化深链超时陷阱,代价是启动段无保护 |
@@ -930,3 +992,5 @@ python scripts/check_cubemx_survival.py --write # 重建基线(确认改动
| 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 三层手段 + 审计脚本** | 沉淀通用方法论(L1 `.ioc` / L2 `USER CODE` 区 / L3 审计+基线);新建 `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 警告 |
diff --git a/scripts/check_cubemx_survival.py b/scripts/check_cubemx_survival.py
index 540863c1..03852c5b 100644
--- a/scripts/check_cubemx_survival.py
+++ b/scripts/check_cubemx_survival.py
@@ -31,6 +31,13 @@ CubeMX 重生成存活审计(check_cubemx_survival.py)
退出码:0 = 通过;1 = 有 FAIL 项。
设计依据:docs/子系统设计/看门狗设计说明.md §10.1;AGENTS.md「平台约束」。
+
+注意
+----
+`MDK-ARM/*.uvprojx` **不参与漂移检测**(`no_baseline=True`):该文件每次
+`@build` 后被 Keil UV4 自动改写(XML 格式归一化 + `PackID` 降级到本机 DFP
+版本),内容每次构建都不同。若参与检测,则**每次构建后提交都会被拦**。
+本项只保留 `must_contain` 断言("源文件仍挂载")。
"""
import argparse
@@ -55,32 +62,55 @@ 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` 区的注释)。
+ # 故本项按"文件整体 + 特征串"检查,并靠一个稳定锚点注释定位。
dict(
id="iwdg-defer-start",
- kind="protected",
+ kind="wholefile",
path="Src/iwdg.c",
- region="IWDG_DEFER_START",
- must_not_contain=["HAL_IWDG_Init(&hiwdg)"],
+ must_contain=["IWDG-DEFER-START-ANCHOR"], # 手工锚点注释,重生成后会消失
+ must_not_contain=["HAL_IWDG_Init(&hiwdg)"], # 真实调用不得存在
+ severity="FAIL",
hint="MX_IWDG_Init() 不得调 HAL_IWDG_Init()(内部立即启动,"
- "而它跑在 app_main_init 之前 → 开机必复位)。启动移交 task_wd_start()。",
+ "而它跑在 app_main_init 之前 → 开机必复位)。启动移交 task_wd_start()。"
+ "恢复后补回 `IWDG-DEFER-START-ANCHOR` 锚点注释。",
),
# ---------------- P1:崩溃可观测性 ----------------
+ # ⚠ 2026-09-29 重要变更:CubeMX 把本文件的中断函数模板**换了一代**——
+ # 旧模板在函数前后给自造区(HardFault_Handler / HardFault_Report),
+ # 新模板改成函数体**内部**的 `HardFault_IRQn 0` / `W1_HardFault_IRQn 0`。
+ # 实测"生成区之外的自造标记区"**不会被保留**。故现在的落点是:
+ # Handler 实体 → 生成区内整函数替换(无 USER CODE 位可用),
+ # 靠 `HARD-FAULT-ANCHOR` 锚点注释检出丢失;
+ # 生成实体用 `#define HardFault_Handler HardFault_Handler_Gen`
+ # 改名保留,链接器自动移除(map 已验证 2 bytes removed)
+ # 报告实现 → `USER CODE BEGIN 0`(生成区内的标准保护位,重生成不丢)
dict(
- id="hardfault-report",
- kind="protected",
+ id="hardfault-handler",
+ kind="wholefile",
path="Src/stm32f4xx_it.c",
- region="HardFault_Handler",
- must_contain=["hard_fault_report"],
- hint="HardFault_Handler 须为手工汇编版(取 MSP/PSP 交 hard_fault_report)。"
- "被打回空 while(1) 后死机将完全静默。",
+ must_contain=[
+ "HARD-FAULT-ANCHOR", # 手工锚点注释,重生成后会消失
+ "__asm void HardFault_Handler", # 手工汇编 Handler
+ "hard_fault_report", # 跳转目标
+ ],
+ must_not_contain=["HardFault_Handler(void)\r\n{\r\n while (1)"],
+ hint="HardFault_Handler 须为手工汇编版(取 MSP/PSP 交 hard_fault_report),"
+ "生成实体改名 HardFault_Handler_Gen。被打回空 while(1) 后死机将完全静默。"
+ "恢复后补回 `HARD-FAULT-ANCHOR` 锚点注释。",
),
dict(
id="hardfault-report-impl",
kind="protected",
path="Src/stm32f4xx_it.c",
- region="HardFault_Report",
- must_contain=["CFSR"],
- hint="HardFault 现场报告实现(打印 PC/LR/xPSR/CFSR/HFSR/BFAR/MMFAR)。",
+ region="0",
+ must_contain=["hard_fault_report", "CFSR"],
+ hint="HardFault 现场报告实现(打印 PC/LR/xPSR/CFSR/HFSR/BFAR/MMFAR)"
+ "须在 USER CODE BEGIN 0 区内(生成区内的标准保护位)。",
),
# ---------------- P1:FreeRTOS 加固 ----------------
dict(
@@ -138,6 +168,7 @@ CHECKS = [
kind="generated",
path="MDK-ARM/STM32F407-Demo.uvprojx",
must_contain=["task_watchdog.c"],
+ no_baseline=True, # 见下方说明:本文件每次 @build 后被 Keil 改写
hint="task_watchdog.c 须挂在 MDK 工程 App 组;CubeMX 重生成可能重建 .uvprojx。",
),
dict(
@@ -268,6 +299,10 @@ def evaluate():
continue
# ---- 取待检内容 ----
+ # `probe_neg`:用于 must_not_contain(禁项)——**总是剥注释**,
+ # 避免"注释里提到历史代码"被误判为真实调用。
+ # `probe` :用于 must_contain(正项)——`wholefile` 模式下不剥注释,
+ # 因为锚点本身可能就是注释(如 IWDG-DEFER-START-ANCHOR)。
if c["kind"] == "protected":
body = region_of(text, c["region"])
if body is None:
@@ -276,16 +311,28 @@ def evaluate():
)
snap[cid] = None
continue
- probe = strip_comments(body) # 只对保护区做注释过滤
+ probe = strip_comments(body)
+ probe_neg = probe
+ elif c["kind"] == "wholefile":
+ body = text
+ probe = text # 正项:整文件原文(锚点可能是注释)
+ probe_neg = strip_comments(text) # 禁项:仍剥注释
else:
body = text
probe = text
+ probe_neg = text
- snap[cid] = sha(probe)
+ # ---- 基线快照 ----
+ # `no_baseline=True` 的项不参与漂移检测。理由:`MDK-ARM/*.uvprojx`
+ # 每次 `@build` 后被 Keil UV4 自动改写(XML 格式归一化 + PackID 降级到
+ # 本机 DFP 版本),内容每次构建都不同 → 若参与漂移检测,则**每次构建后
+ # 提交都会被拦**。本项只保留 must_contain 断言("源文件仍挂载")。
+ if not c.get("no_baseline"):
+ snap[cid] = sha(probe)
# ---- 断言 ----
bad = [n for n in c.get("must_contain", []) if n not in probe]
- bad += [n for n in c.get("must_not_contain", []) if n in probe]
+ bad += [n for n in c.get("must_not_contain", []) if n in probe_neg]
if bad:
results.append(
(level, cid, "%s:%s" % (c["region"] if c["kind"] == "protected" else path,
@@ -318,15 +365,15 @@ UNPROTECTED_LEGACY = [
(
"Src/freertos.c",
[
- ("任务栈注释(.stack_size 行内中文说明)", r"\.stack_size\s*=\s*\d+\s*\*\s*4"),
+ ("timeSyncTask 手工定义(不在 .ioc FREERTOS.Tasks01 里)", r"timeSyncTask_attributes"),
],
),
(
"Src/stm32f4xx_it.c",
[
- ("MemManage_Handler 空 while(1)", r"void MemManage_Handler\(void\)"),
- ("BusFault_Handler 空 while(1)", r"void BusFault_Handler\(void\)"),
- ("UsageFault_Handler 空 while(1)", r"void UsageFault_Handler\(void\)"),
+ ("MemManage_Handler 仍为空 while(1),死机静默", r"void MemManage_Handler\(void\)"),
+ ("BusFault_Handler 仍为空 while(1),死机静默", r"void BusFault_Handler\(void\)"),
+ ("UsageFault_Handler 仍为空 while(1),死机静默", r"void UsageFault_Handler\(void\)"),
],
),
]