加 CubeMX 重生成存活审计:防标记区外定制被静默覆盖

背景:.ioc 的 KeepUserCode 只保护 USER CODE 标记区,标记之外的手工改动
重生成即被覆盖,且多数不报编译错(2026-09-29 实测一次覆盖 4 处,含
HardFault_Handler 崩溃采集被打回空 while(1)、两个 FreeRTOS 加固宏被删)。

新增 scripts/check_cubemx_survival.py:
- 16 项易失清单(IWDG 延后启动 / 崩溃采集 / 栈溢出检测 / 堆失败钩子 /
  断言改造 / 看门狗模块与工程挂载 / 复位原因锁存 / HAL 驱动与使能 /
  .ioc 关键项),断言前剥掉 C 注释防误报
- 哈希基线漂移检测(.cubemx_survival_baseline.json),--write 重建
- 标记区外存量以 WARN 提示(3 个 Fault 处理函数、栈注释)

已挂 pre-commit cubemx-survival 钩子(改动 .ioc / Src / Inc /
task_watchdog / .uvprojx 时触发)。

文档:看门狗设计说明 §10.2 新增"重生成不丢的三层手段"方法论
(L1 写 .ioc > L2 搬进 USER CODE 区(可自造标记区)> L3 审计兜底)
及 .ioc / USER CODE 区可用位置 / 存量清单三张对照表;AGENTS.md 平台约束
新增该规则;pre-commit 使用说明 §2.12 新增钩子详解。

门禁:build 0 错误 0 警告;pre-commit 全通过。
This commit is contained in:
2026-09-29 07:30:00 +08:00
parent 7feeea6c56
commit 7a18d2ce28
6 changed files with 578 additions and 3 deletions
+47 -3
View File
@@ -20,7 +20,8 @@ git commit
├─ coding-standard C 代码规范检查(规范V1.0 可机械校验项)
├─ doc-links 文档链接存在性(D-3/D-7,.md)
├─ task-table FreeRTOS 任务表一致性(D-2/D-7)
└─ mem-numbers 内存/Flash 数字一致性(D-7 体积档)
├─ mem-numbers 内存/Flash 数字一致性(D-7 体积档)
└─ cubemx-survival CubeMX 重生成存活审计(易失定制清单 + 基线漂移)
└─ commit-msg(.git/hooks/commit-msg)
└─ gitlint 校验 commit message 格式
```
@@ -38,6 +39,7 @@ git commit
| scripts/check_doc_links.py | 随仓库提交 | 文档链接存在性检查(纯 Python,无依赖) |
| scripts/gen_task_table.py | 随仓库提交 | FreeRTOS 任务表生成/校验(纯 Python,无依赖) |
| scripts/check_mem_numbers.py | 随仓库提交 | 内存/Flash 数字一致性(纯 Python,无依赖) |
| scripts/check_cubemx_survival.py | 随仓库提交 | CubeMX 重生成存活审计(纯 Python,无依赖) |
> **PATH 要求**:cppcheck 安装后需在系统 PATH 中(`C:\Program Files\Cppcheck`)。winget 自动写入 User PATH,但当前终端不会立即生效,需**重开终端**。
@@ -225,7 +227,40 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
> 峰值 `1184` 都是合法值)。改为"只报历史快照里出现的、当前已变的区域值"后 **0 误报**。
> 宁可窄而准——吵的门禁会被绕过。
### 2.12 gitlint — Commit Message 格式
### 2.12 cubemx-survival — CubeMX 重生成存活审计
用 `scripts/check_cubemx_survival.py` 核对"重生成易失清单",防止 CubeMX 重新生成时
**静默覆盖**手工定制。`.ioc` 的 `ProjectManager.KeepUserCode=true` **只**保护
`/* USER CODE BEGIN/END */` 标记区内的内容——标记之外的一切都会被模板重写,
且多数**不报编译错误**,只在真出故障时才暴露(2026-09-29 实测一次重生成覆盖 4 处,
含 `HardFault_Handler` 崩溃采集被打回空 `while(1)`、两个 FreeRTOS 加固宏被整块删除)。
| 项 | 说明 |
|---|---|
| **触发条件** | 改动 `STM32F407-Demo.ioc` / `Src/*.{c,h}` / `Inc/*.h` / `App/util/task_watchdog.*` / `MDK-ARM/*.uvprojx` / 本脚本 |
| **处理方式** | **阻止提交**;清单项缺失或基线漂移即 FAIL |
| **检查内容** | 16 项:IWDG 延后启动、崩溃采集×2、栈溢出检测、堆失败钩子、断言改造×2、看门狗模块与工程挂载、复位原因锁存、HAL 驱动与使能宏、`.ioc` 关键项×3 |
| **基线文件** | `.cubemx_survival_baseline.json`(各项内容哈希;`--write` 重建) |
| **防误报** | 断言前**剥掉 C 注释**——本项目多处注释刻意引用历史代码(如"原为 `HAL_IWDG_Init(&hiwdg)` 直呼"),不过滤会恒定误报 |
```powershell
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py # 审计报告(人读,含修复提示)
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py --check # 门禁模式(钩子用)
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py --write # 重建基线(确认改动无误后)
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py --report # 只打印报告,不比对基线
```
> **推荐工作流**:**每次 CubeMX 重生成后立刻跑一次**(不带参数)——它会把
> "被覆盖了哪几处"直接列出并附修复提示,比事后靠故障现象反推快得多。
>
> **标记区外的存量**以 WARN 提示(`Src/stm32f4xx_it.c` 的 3 个 Fault 处理函数、
> `Src/freertos.c` 的栈注释)——这些 CubeMX 不保护,只能人工盯;建议后续
> 仿照 `HardFault_Handler` **自造 `USER CODE` 标记区**把它们升级为受保护内容。
>
> 方法论(L1 写 `.ioc` > L2 搬进 `USER CODE` 区 > L3 审计兜底)详见
> [`../子系统设计/看门狗设计说明.md`](../子系统设计/看门狗设计说明.md) §10.2。
### 2.13 gitlint — Commit Message 格式
| 项 | 说明 |
|---|---|
@@ -348,13 +383,15 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
| 文件 | 位置 | 说明 |
|---|---|---|
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(12 个钩子 + 1 个预留) |
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(13 个钩子 + 1 个预留) |
| `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) |
| `.cubemx_survival_baseline.json` | 仓库根 | CubeMX 易失定制基线(哈希;`--write` 重建) |
| `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 |
| `scripts/check_coding_standard.py` | scripts/ | 代码规范检查脚本(规范 V1.0 可机械校验项) |
| `scripts/check_doc_links.py` | scripts/ | 文档链接存在性检查(D-3/D-7) |
| `scripts/gen_task_table.py` | scripts/ | FreeRTOS 任务表生成/校验(D-2/D-7) |
| `scripts/check_mem_numbers.py` | scripts/ | 内存/Flash 数字一致性(D-7 体积档) |
| `scripts/check_cubemx_survival.py` | scripts/ | CubeMX 重生成存活审计 |
| `docs/代码规范/嵌入式C语言代码规范_V1.0.md` | docs/代码规范/ | 代码规范原文(S1~S19 规则来源) |
| `docs/代码规范/cppcheck使用说明.md` | docs/代码规范/ | cppcheck 钩子详解(规则/配置/抑制) |
| `docs/文档规范.md` | docs/ | 文档体系规范(链接形态/命名/门禁要求) |
@@ -374,3 +411,10 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
卸载钩子: pre-commit uninstall
更新钩子版本: pre-commit autoupdate
```
**CubeMX 重生成后立刻执行**(检查定制是否被覆盖):
```
python scripts/check_cubemx_survival.py # 列"被覆盖了哪几处"+修复提示
python scripts/check_cubemx_survival.py --write # 确认无误后重建基线
```
@@ -775,6 +775,94 @@ external pin → "pin reset" (新增)
> **每步改完必须 `@build` 0 错误 0 警告**(`AGENTS.md` 铁律)。第 0~6 步已一次性
> 验证通过:`BUILD_OK: 0 errors, 0 warnings`。
### 10.2 让定制"重生成不丢"的三层手段(**通用方法论**)
第 0 步的 4 处覆盖暴露了一个**与看门狗无关的通用问题**:CubeMX 重生成会覆盖
标记区之外的一切手工改动。解决办法按可靠性分三层,优先级从高到低:
| 层 | 手段 | 适用 | 可靠性 |
|---|---|---|---|
| **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` 这类"生成区必须改动"的特例 | ⭐ **兜底**——不防丢失,但**丢了一定知道** |
#### L1:哪些定制能写进 `.ioc`
本项目 `.ioc` 已持有(重生成不会丢):
| 定制 | `.ioc` 键 | 说明 |
|---|---|---|
| **任务栈大小** | `FREERTOS.Tasks01=...defaultTask,24,1024,...` | ✅ **最值得强调**:栈值写在 `.ioc` 里,CubeMX 直接生成到 `Src/freertos.c` 的 `.stack_size`。曾担心"重生成会覆盖栈大小",实测**保住了**(`.ioc` 已同步)。**改栈请改 `.ioc`,不要改 `freertos.c`** |
| 任务优先级 / 名称 / 入口函数 | 同上 | ✅ 同一条目 |
| 消息队列 `netMsgQueue` | `FREERTOS.Queues01` | ✅ |
| 事件组 `sys_evt` | `FREERTOS.Events01` | ✅ |
| FreeRTOS 堆大小 | `FREERTOS.configTOTAL_HEAP_SIZE=49152` | ✅ |
| FPU 使能 | `FREERTOS.configENABLE_FPU=1` | ✅ |
| IWDG 分频 / 重载 | `IWDG.IPParameters` + `IWDG.Prescaler` / `IWDG.Reload` | ✅ 本次新增,见 §2.2 |
| 时钟树(含 LSI) | `RCC.*` | ✅ |
| `KeepUserCode=true` | `ProjectManager.KeepUserCode` | ✅ **这个必须为 true**,否则 L2 整体失效 |
**能写进 `.ioc` 但本项目尚未写的**(可作为后续加固项):
| 定制 | 现状 | 建议 |
|---|---|---|
| `configCHECK_FOR_STACK_OVERFLOW` | 在 `USER CODE BEGIN FREE_RTOS_CONFIG_HARDENING` 区(L2) | ⚠️ CubeMX 的 FreeRTOS 界面**没有**这一项的勾选框,只能靠 L2 + 审计。**保持现状** |
| `configUSE_MALLOC_FAILED_HOOK` | 同上 | 同上 |
| `configASSERT` 定义 | 在 `USER CODE BEGIN 1` 区(L2) | 同上。CubeMX 无对应界面项 |
| `MemManage/BusFault/UsageFault_Handler` | `stm32f4xx_it.c` 标记区**外**(L3) | ⚠️ CubeMX 为这些函数**不生成** `USER CODE` 区,无法 L2。见下方"存量清单" |
> **判断口诀**:能在外设/中间件的图形界面里找到对应输入框的 → 一定写 `.ioc`;
> 找不到(如 FreeRTOS 的两个加固宏、断言定义、故障异常处理函数体)→ 只能 L2/L3。
#### L2:`USER CODE` 区的可用位置(本项目实测)
| 文件 | 可用标记区 | 已用于 |
|---|---|---|
| `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/main.c` | 共 19 个,含 `Init` / `SysInit` / `2` / `PV` / `PFP` | 复位原因锁存(`Init`) |
| `Src/iwdg.c` | 共 7 个,含 `IWDG_Init 0/1/2` / `IWDG_DEFER_START`(我方新增) | 阻止自动启动(`IWDG_DEFER_START`) |
| `Inc/main.h` | 共 7 个 | 引脚宏等 |
> ⚠️ **`Src/iwdg.c` 的特例**:本次需要的"不自动启动"改动**必须落在生成区**
> (`hiwdg.Init.*` 赋值语句与 `HAL_IWDG_Init()` 调用之间的位置),
> 而 CubeMX **不为这两句之间生成标记区**。于是采取"**在生成区里插入一个
> 自定义命名的 USER CODE 块**"——实测 CubeMX 会保留**任何**形态的
> `USER CODE BEGIN/END` 注释对,即使区名不是它预定义的。这是 L2 的
> 一个有用变体:**可以自己造标记区**。
#### 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/freertos.c` | `.stack_size` 行内的中文注释 | 该行由 CubeMX 按 `.ioc` 生成,"值"受保护、"注释"不受。**注释是锦上添花**,丢失不影响功能(值在 `.ioc` 里) |
> **结论**:真正必须靠 L3 盯的只有 3 个 Fault 处理函数;其余要么已在 `.ioc`(L1),
> 要么已进 `USER CODE` 区(L2)。**建议后续把 3 个 Fault 处理函数也改成自造标记区版**,
> 那样 L3 清单可清空。
#### L3 落地:审计脚本 + 基线
```bash
python scripts/check_cubemx_survival.py # 审计报告(人读)
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` 比对
- **已挂 pre-commit**(`cubemx-survival` 钩子):改动 `.ioc` / `Src/` / `Inc/` /
`task_watchdog` / `.uvprojx` 时自动触发
- **注释过滤**:断言前先剥掉 C 注释,避免"注释里提到 `HAL_IWDG_Init`"被误判
(本项目多处注释刻意引用历史代码,不做过滤会恒定误报)
> **推荐工作流**:**每次 CubeMX 重生成后,立刻跑一次
> `python scripts/check_cubemx_survival.py`**——它会把"被覆盖了哪几处"
> 直接列出来(含修复提示),比事后靠故障现象反推快得多。
---
## 11. 结论
@@ -783,6 +871,7 @@ external pin → "pin reset" (新增)
|---|---|
| **选型** | **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 钩子 |
| **架构** | **任务心跳位图(各自置位,无锁单字)+ defaultTask 单一喂狗者**——避免"有人跑就喂狗"的假保护 |
| **超时** | 2498 ms(PR=4 / RLR=1249);最坏(LSI 47 kHz)≈1701 ms;中间留 800 ms 停喂判定裕度 |
| **启动时机** | **defaultTask bring-up 完成后**——绕开初始化深链超时陷阱,代价是启动段无保护 |