From 7a18d2ce287c64b32aa3e54d99b07d0faa92af00 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 29 Sep 2026 07:30:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=20CubeMX=20=E9=87=8D=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=AD=98=E6=B4=BB=E5=AE=A1=E8=AE=A1=EF=BC=9A=E9=98=B2?= =?UTF-8?q?=E6=A0=87=E8=AE=B0=E5=8C=BA=E5=A4=96=E5=AE=9A=E5=88=B6=E8=A2=AB?= =?UTF-8?q?=E9=9D=99=E9=BB=98=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景:.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 全通过。 --- .cubemx_survival_baseline.json | 21 ++ .pre-commit-config.yaml | 12 + AGENTS.md | 1 + docs/代码规范/pre-commit使用说明.md | 50 +++- docs/子系统设计/看门狗设计说明.md | 89 ++++++ scripts/check_cubemx_survival.py | 408 ++++++++++++++++++++++++++++ 6 files changed, 578 insertions(+), 3 deletions(-) create mode 100644 .cubemx_survival_baseline.json create mode 100644 scripts/check_cubemx_survival.py diff --git a/.cubemx_survival_baseline.json b/.cubemx_survival_baseline.json new file mode 100644 index 00000000..4f4274de --- /dev/null +++ b/.cubemx_survival_baseline.json @@ -0,0 +1,21 @@ +{ + "snapshot": { + "iwdg-defer-start": "0aad7da77d2ed59c", + "hardfault-report": "83aa6eee48e67318", + "hardfault-report-impl": "20bee193982cd39b", + "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", + "ioc-keepusercode": "b5bea41b6c623f7c", + "ioc-iwdg-present": "d3a6e667048c95db", + "ioc-task-stacks": "72969c5a8569f4d2" + }, + "_note": "CubeMX 存活审计基线;改动用 --write 重建" +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f7b3be14..f4209956 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -136,3 +136,15 @@ repos: language: system files: ^(docs/内存管理/.*\.md|scripts/check_mem_numbers\.py|App/.*\.c|App/.*\.h|Src/.*\.c|Inc/.*\.h)$ pass_filenames: false + + # 6) CubeMX 重生成存活审计(2026-09-29 加) + # 背景:.ioc 的 KeepUserCode 只保护 USER CODE 标记区;标记之外的手工改动 + # 重生成即被覆盖,且多数不报编译错(崩溃采集被打回空 while(1)、加固宏被删)。 + # 本钩子验证"重生成易失清单"各项是否仍在预期位置,并对关键区做哈希漂移检测。 + # 范围:仅在这些易失文件变动时触发(改动 .ioc 或生成文件本身就值得复核)。 + - id: cubemx-survival + name: CubeMX 重生成存活审计 + entry: python scripts/check_cubemx_survival.py --check + language: system + files: ^(STM32F407-Demo\.ioc|Src/.*\.(c|h)|Inc/.*\.h|App/util/task_watchdog\.(c|h)|MDK-ARM/.*\.uvprojx|scripts/check_cubemx_survival\.py)$ + pass_filenames: false diff --git a/AGENTS.md b/AGENTS.md index c7389767..cebe253f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,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 处定制,其中 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 - 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 约束,勿"优化") diff --git a/docs/代码规范/pre-commit使用说明.md b/docs/代码规范/pre-commit使用说明.md index 3297610a..9eb560be 100644 --- a/docs/代码规范/pre-commit使用说明.md +++ b/docs/代码规范/pre-commit使用说明.md @@ -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 # 确认无误后重建基线 +``` diff --git a/docs/子系统设计/看门狗设计说明.md b/docs/子系统设计/看门狗设计说明.md index 67a3f6b2..7fdd7cab 100644 --- a/docs/子系统设计/看门狗设计说明.md +++ b/docs/子系统设计/看门狗设计说明.md @@ -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 完成后**——绕开初始化深链超时陷阱,代价是启动段无保护 | diff --git a/scripts/check_cubemx_survival.py b/scripts/check_cubemx_survival.py new file mode 100644 index 00000000..540863c1 --- /dev/null +++ b/scripts/check_cubemx_survival.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +CubeMX 重生成存活审计(check_cubemx_survival.py) + +模块名称:CubeMX 重生成存活审计 +作者:王建锋 +创建日期:2026-09-29 + +背景 +---- +`.ioc` 的 `ProjectManager.KeepUserCode=true` **只保护** `USER CODE BEGIN/END` +标记之间的内容。标记之外的任何手工改动,在 CubeMX 重新生成时会被模板覆盖, +且**多数不会立刻报编译错误**(典型:崩溃采集函数被打回空 while(1)、 +加固宏被整块删除)——只在真出故障时才暴露,排查代价极高。 +2026-09-29 实测:一次重生成覆盖了 4 处定制(P0×1 + P1×2 + P2×1)。 + +本脚本做两件事 +-------------- +1. **存活审计**:检查"重生成易失清单"里的每一项是否仍在预期位置, + 对标记区外的存量给出显式警告(存量无法消除,只能靠人记)。 +2. **漂移检测**:把关键位置的哈希写入 `.cubemx_survival_baseline.json`, + `--check` 比对,任一项变化即 FAIL——用于"重生成后立刻发现被改了什么"。 + +用法 +---- + python scripts/check_cubemx_survival.py # 审计(人读报告) + python scripts/check_cubemx_survival.py --check # 门禁模式(默认,供 pre-commit) + python scripts/check_cubemx_survival.py --write # 重建基线(确认改动无误后) + +退出码:0 = 通过;1 = 有 FAIL 项。 + +设计依据:docs/子系统设计/看门狗设计说明.md §10.1;AGENTS.md「平台约束」。 +""" + +import argparse +import hashlib +import json +import os +import re +import sys + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASELINE = os.path.join(REPO, ".cubemx_survival_baseline.json") + +# --------------------------------------------------------------------------- +# 重生成易失清单 +# +# kind: +# "protected" — 位于 USER CODE 标记区内,CubeMX 会保留;审计只验"区还在" +# "unprotected" — 位于标记区外,CubeMX **会**覆盖;审计给 WARN(存量风险) +# "generated" — 必须由 CubeMX 生成;审计验"没被删" +# "ioc" — 必须写在 .ioc 里;否则重生成会丢失(这是根治手段) +# --------------------------------------------------------------------------- + +CHECKS = [ + # ---------------- P0:开机死循环防线 ---------------- + dict( + id="iwdg-defer-start", + kind="protected", + path="Src/iwdg.c", + region="IWDG_DEFER_START", + must_not_contain=["HAL_IWDG_Init(&hiwdg)"], + hint="MX_IWDG_Init() 不得调 HAL_IWDG_Init()(内部立即启动," + "而它跑在 app_main_init 之前 → 开机必复位)。启动移交 task_wd_start()。", + ), + # ---------------- P1:崩溃可观测性 ---------------- + dict( + id="hardfault-report", + kind="protected", + path="Src/stm32f4xx_it.c", + region="HardFault_Handler", + must_contain=["hard_fault_report"], + hint="HardFault_Handler 须为手工汇编版(取 MSP/PSP 交 hard_fault_report)。" + "被打回空 while(1) 后死机将完全静默。", + ), + 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)。", + ), + # ---------------- P1:FreeRTOS 加固 ---------------- + dict( + id="stack-overflow-check", + kind="protected", + path="Inc/FreeRTOSConfig.h", + region="FREE_RTOS_CONFIG_HARDENING", + must_contain=["configCHECK_FOR_STACK_OVERFLOW"], + hint="栈溢出机械检测必须开启(模式 2)。关闭后溢出静默腐蚀堆相邻块。", + ), + dict( + id="malloc-failed-hook", + kind="protected", + path="Inc/FreeRTOSConfig.h", + region="FREE_RTOS_CONFIG_HARDENING", + must_contain=["configUSE_MALLOC_FAILED_HOOK"], + hint="堆分配失败钩子必须开启(1)。", + ), + dict( + id="assert-not-deadloop", + kind="protected", + path="Inc/FreeRTOSConfig.h", + region="1", + must_contain=["vApplicationAssertFailed"], + must_not_contain=["taskDISABLE_INTERRUPTS(); for( ;; );}"], + hint="configASSERT 须走 vApplicationAssertFailed(打印后复位)," + "不得回到关中断死循环。", + ), + dict( + id="assert-impl", + kind="protected", + path="Src/freertos.c", + region="Application", + must_contain=["vApplicationAssertFailed"], + hint="vApplicationAssertFailed() 实现须在 Src/freertos.c 的 USER CODE Application 区。", + ), + dict( + id="stack-overflow-hook-impl", + kind="protected", + path="Src/freertos.c", + region="Application", + must_contain=["vApplicationStackOverflowHook"], + hint="栈溢出钩子实现(打印任务名后复位)。", + ), + # ---------------- 看门狗模块 ---------------- + dict( + id="watchdog-module", + kind="generated", + path="App/util/task_watchdog.c", + must_contain=["task_wd_poll", "IWDG->KR"], + hint="看门狗模块(App/util/),非 CubeMX 管辖,但须确认未被误删。", + ), + dict( + id="watchdog-in-project", + kind="generated", + path="MDK-ARM/STM32F407-Demo.uvprojx", + must_contain=["task_watchdog.c"], + hint="task_watchdog.c 须挂在 MDK 工程 App 组;CubeMX 重生成可能重建 .uvprojx。", + ), + dict( + id="reset-cause-latch", + kind="protected", + path="Src/main.c", + region="Init", + must_contain=["g_rcc_csr_latched", "RCC_CSR_RMVF"], + hint="复位原因锁存:HAL_Init() 后立即读 RCC->CSR 并写 RMVF 清除。", + ), + # ---------------- 必须由 CubeMX 生成(缺 = 配错或误删) ---------------- + dict( + id="iwdg-hal-driver", + kind="generated", + path="Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_iwdg.c", + hint="HAL IWDG 驱动须由 CubeMX 勾选 IWDG 后生成(手工补入会被重生成删掉)。", + ), + dict( + id="hal-iwdg-enabled", + kind="generated", + path="Inc/stm32f4xx_hal_conf.h", + must_contain=["#define HAL_IWDG_MODULE_ENABLED"], + hint="HAL_IWDG_MODULE_ENABLED 须为**生效**状态(非注释)。", + ), + # ---------------- .ioc 必须持有(根治:写进 .ioc 才不怕重生成) ---------------- + dict( + id="ioc-keepusercode", + kind="ioc", + key="ProjectManager.KeepUserCode", + want="true", + hint="KeepUserCode 必须为 true,否则 USER CODE 区不再受保护。", + severity="FAIL", + ), + dict( + id="ioc-iwdg-present", + kind="ioc", + key="IWDG.IPParameters", + hint="IWDG 须在 .ioc 中激活(否则重生成会删掉 iwdg.c 与 HAL 驱动)。", + ), + dict( + id="ioc-task-stacks", + kind="ioc", + key="FREERTOS.Tasks01", + must_contain=["1024", "2048", "4096"], + hint="任务栈大小必须写在 .ioc(FREERTOS.Tasks01)里——写入 .ioc 的栈值" + "会被 CubeMX 直接生成到 freertos.c,是最彻底的防丢失手段。", + ), +] + + +def read(p): + try: + with open(os.path.join(REPO, p), encoding="utf-8", errors="replace") as f: + return f.read() + except OSError: + return None + + +def region_of(text, name): + """取 `/* USER CODE BEGIN */` 与 `/* USER CODE END */` 之间的内容。 + + 实测本项目生成文件的标记形态是**单行注释**: + /* USER CODE BEGIN HardFault_Handler */ + ... 内容 ... + /* USER CODE END HardFault_Handler */ + (不是 `/* USER CODE BEGIN x` 换行再 `*/`)。故按整行匹配。 + """ + m = re.search( + r"/\* USER CODE BEGIN %s \*/\n(.*?)\n[^\n]*/\* USER CODE END %s \*/" + % (re.escape(name), re.escape(name)), + text, + re.S, + ) + return m.group(1) if m else None + + +def sha(s): + return hashlib.sha256(s.encode("utf-8")).hexdigest()[:16] + + +def strip_comments(code): + """去掉 C 注释,避免把注释里提到的禁用符号误判为真实调用。 + + 本项目多处注释**刻意**引用"原为 XX 直呼"之类的历史信息(如 Src/iwdg.c + 的 IWDG_DEFER_START 区),若不过滤注释,`must_not_contain` 会恒定误报。 + """ + code = re.sub(r"/\*.*?\*/", " ", code, flags=re.S) + code = re.sub(r"//[^\n]*", " ", code) + return code + + +def evaluate(): + """返回 (results, snapshot)。results = [(level, id, msg, hint)]""" + results = [] + snap = {} + ioc = read("STM32F407-Demo.ioc") or "" + + for c in CHECKS: + cid = c["id"] + path = c.get("path") + level = c.get("severity", "FAIL") + + # ---- .ioc 类 ---- + if c["kind"] == "ioc": + key = c["key"] + m = re.search(r"^%s=(.*)$" % re.escape(key), ioc, re.M) + if not m: + results.append((level, cid, ".ioc 缺少 %s" % key, c["hint"])) + snap[cid] = None + continue + val = m.group(1) + snap[cid] = sha(val) + if "want" in c and val.strip().lower() != c["want"].lower(): + results.append((level, cid, ".ioc %s = %r(期望 %r)" % (key, val, c["want"]), c["hint"])) + continue + for needle in c.get("must_contain", []): + if needle not in val: + results.append(("WARN", cid, ".ioc %s 中未见 %r" % (key, needle), c["hint"])) + break + else: + results.append(("OK", cid, ".ioc %s 就位" % key, "")) + continue + + text = read(path) + if text is None: + results.append((level, cid, "文件不存在:%s" % path, c["hint"])) + snap[cid] = None + continue + + # ---- 取待检内容 ---- + if c["kind"] == "protected": + body = region_of(text, c["region"]) + if body is None: + results.append( + (level, cid, "%s 的 USER CODE BEGIN/END %s 区不存在" % (path, c["region"]), c["hint"]) + ) + snap[cid] = None + continue + probe = strip_comments(body) # 只对保护区做注释过滤 + else: + body = text + probe = text + + 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] + if bad: + results.append( + (level, cid, "%s:%s" % (c["region"] if c["kind"] == "protected" else path, + "缺失/命中禁用项 " + ", ".join(repr(b) for b in bad)), + c["hint"]) + ) + else: + results.append(("OK", cid, "%s 就位" % (c["region"] if c["kind"] == "protected" else path), "")) + + # ---- 标记区外的存量风险(无法自动修复,只提示) ---- + for path, needles in UNPROTECTED_LEGACY: + text = read(path) or "" + hits = [] + for name, pat in needles: + if re.search(pat, text): + hits.append(name) + if hits: + results.append(( + "WARN", + "legacy:" + path, + "标记区外手工内容(重生成会覆盖):" + "、".join(hits), + "这些内容 CubeMX 不保护。核对清单见 docs/子系统设计/看门狗设计说明.md §10.1。", + )) + + return results, snap + + +# 已知的"标记区外"手工内容(存量,只能人工盯) +UNPROTECTED_LEGACY = [ + ( + "Src/freertos.c", + [ + ("任务栈注释(.stack_size 行内中文说明)", r"\.stack_size\s*=\s*\d+\s*\*\s*4"), + ], + ), + ( + "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\)"), + ], + ), +] + + +def main(): + ap = argparse.ArgumentParser(description="CubeMX 重生成存活审计") + g = ap.add_mutually_exclusive_group() + g.add_argument("--check", action="store_true", help="门禁模式:与基线比对,任一项漂移即失败") + g.add_argument("--write", action="store_true", help="重建基线(确认改动无误后)") + g.add_argument("--report", action="store_true", help="只打印审计报告,不比对基线") + args = ap.parse_args() + + results, snap = evaluate() + + fails = [r for r in results if r[0] == "FAIL"] + warns = [r for r in results if r[0] == "WARN"] + oks = [r for r in results if r[0] == "OK"] + + # ---------- 漂移检测 ---------- + drift = [] + if not args.write and not args.report: + if os.path.exists(BASELINE): + try: + with open(BASELINE, encoding="utf-8") as f: + base = json.load(f).get("snapshot", {}) + except (OSError, ValueError): + base = {} + for k, v in snap.items(): + if k in base and base[k] != v and base[k] is not None and v is not None: + drift.append(k) + # 基线缺失不算失败(换机/首次),但提示 + + # ---------- 输出 ---------- + print("CubeMX 重生成存活审计") + print(" 事实源:.ioc (KeepUserCode) + 各生成文件的 USER CODE 标记区") + print(" 通过 %d / 警告 %d / 失败 %d" % (len(oks), len(warns), len(fails))) + print() + + if fails: + print("[FAIL] 以下内容丢失或配置错误(重生成后必须立刻修):") + for _, cid, msg, hint in fails: + print(" - %-26s %s" % (cid, msg)) + if hint: + print(" → %s" % hint) + print() + + if warns: + print("[WARN] 提示(多为标记区外存量,无法自动消除):") + for _, cid, msg, hint in warns: + print(" - %-26s %s" % (cid, msg)) + print() + + if drift: + print("[FAIL] 基线漂移(这些位置的内容与上次确认的不同):") + for k in drift: + print(" - %s" % k) + print(" → 若是本次有意改动,确认无误后跑 `--write` 重建基线;") + + if args.write: + with open(BASELINE, "w", encoding="utf-8") as f: + json.dump( + {"snapshot": snap, "_note": "CubeMX 存活审计基线;改动用 --write 重建"}, + f, + ensure_ascii=False, + indent=2, + ) + f.write("\n") + print("已重建基线:%s" % os.path.relpath(BASELINE, REPO)) + return 0 + + if fails or drift: + return 1 + print("通过:重生成易失清单全部就位。") + return 0 + + +if __name__ == "__main__": + sys.exit(main())