0f7b5a87fb
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号) 一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、 采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。 门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏 undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8 reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF; id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。 test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字, 是本批唯一改变构建产物的文件集合。 为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged 部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、 不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。 实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01% (基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 / 告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。 基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处 引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使 识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。 ⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二 根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有), 今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。 固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0; App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889 断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿; check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带 BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。 .cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法: Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。 同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%, RW_CCM 57,376 B / 87.9%,阈值 92%。
614 lines
27 KiB
Python
614 lines
27 KiB
Python
#!/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.2/§10.3;AGENTS.md「平台约束」。
|
||
|
||
注意
|
||
----
|
||
`MDK-ARM/*.uvprojx` **不参与漂移检测**(`no_baseline=True`):该文件每次
|
||
`@build` 后被 Keil UV4 自动改写(XML 格式归一化 + `PackID` 降级到本机 DFP
|
||
版本),内容每次构建都不同。若参与检测,则**每次构建后提交都会被拦**。
|
||
本项只保留 `must_contain` 断言("源文件仍挂载")。
|
||
"""
|
||
|
||
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:开机死循环防线 ----------------
|
||
# ✅ 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="protected",
|
||
path="Src/iwdg.c",
|
||
region="0",
|
||
must_contain=["iwdg_deferred_start", "#define HAL_IWDG_Init"],
|
||
severity="FAIL",
|
||
hint="MX_IWDG_Init() 不得真正启动 IWDG(HAL_IWDG_Init 内部立即启动,而它跑在 "
|
||
"app_main_init 之前 → 开机必复位)。现由 `USER CODE BEGIN 0` 的空实现 + 宏改名"
|
||
"拦截,启动移交 task_wd_start();丢了宏 = 启动段看门狗就跑起来 = 开机死循环。",
|
||
),
|
||
# ---------------- P1:崩溃可观测性 ----------------
|
||
# ⚠ 2026-09-29 重要变更:CubeMX 把本文件的中断函数模板**换了一代**——
|
||
# 旧模板在函数前后给自造区(HardFault_Handler / HardFault_Report),
|
||
# 新模板改成函数体**内部**的 `HardFault_IRQn 0` / `W1_HardFault_IRQn 0`。
|
||
# 实测"生成区之外的自造标记区"**不会被保留**。
|
||
# ✅ 2026-10-09 落点升级为 L2(与 iwdg 同一招):4 个 `__asm` Handler 与报告实现
|
||
# 全部放 `USER CODE BEGIN 0`(标准保护位),生成区恢复 CubeMX 原样;
|
||
# 生成实体靠 `#define X X_Gen` 改名让位(未被引用 → 链接器自动移除)。
|
||
# ⚠ 顺序硬约束:我们的定义必须在 4 个 `#define` **之前**,否则被自己的宏改名。
|
||
# 判据用区内的代码符号(protected 项比对前会剥掉注释,锚点注释不能当断言)。
|
||
dict(
|
||
id="hardfault-handler",
|
||
kind="protected",
|
||
path="Src/stm32f4xx_it.c",
|
||
region="0",
|
||
must_contain=[
|
||
"__asm void HardFault_Handler", # 手工汇编 Handler(取 MSP/PSP)
|
||
"fault_entry_hard", # 跳转目标(C 侧带故障名)
|
||
"#define HardFault_Handler HardFault_Handler_Gen", # 给生成实体让位
|
||
],
|
||
hint="HardFault_Handler 须为手工汇编版(取 MSP/PSP 交 fault_entry_hard),"
|
||
"生成实体改名 HardFault_Handler_Gen。被打回空 while(1) 后死机将完全静默。"
|
||
"恢复位置 = `USER CODE BEGIN 0`,且必须在 4 个 #define 之前。",
|
||
),
|
||
# MemManage / BusFault / UsageFault 三个同样升级为崩溃采集(同文件、同方案,
|
||
# 由一项统一检查,避免 3 条重复项)。
|
||
dict(
|
||
id="fault-handlers-3",
|
||
kind="protected",
|
||
path="Src/stm32f4xx_it.c",
|
||
region="0",
|
||
must_contain=[
|
||
"__asm void MemManage_Handler",
|
||
"__asm void BusFault_Handler",
|
||
"__asm void UsageFault_Handler",
|
||
"fault_entry_mem",
|
||
"fault_entry_bus",
|
||
"fault_entry_usage",
|
||
],
|
||
hint="MemManage/BusFault/UsageFault 须为手工汇编版(各跳 fault_entry_* 打故障名)。"
|
||
"被打回空 while(1) 后这三类故障会被升级成 HardFault 且看不出根因。",
|
||
),
|
||
dict(
|
||
id="fault-handler-enable",
|
||
kind="protected",
|
||
path="Src/stm32f4xx_it.c",
|
||
region="1",
|
||
must_contain=["fault_handler_enable", "SCB_SHCSR_MEMFAULTENA_Msk"],
|
||
hint="MemManage/BusFault/UsageFault 默认**禁用**,须在 SHCSR 置使能位才会独立触发;"
|
||
"否则它们被升级成 HardFault、日志只显示 HARDFAULT。"
|
||
"⚠ 这三位**无法在 CubeMX 配置**(2026-10-01 查模板源码证实:System Exception "
|
||
"只生成优先级、从不生成使能代码;.ioc 里的 NVIC.xxx=true 只是'向量可见',"
|
||
"不代表已使能)→ 只能手工写,故必须在本审计清单里兜底。"
|
||
),
|
||
dict(
|
||
id="hardfault-report-impl",
|
||
kind="protected",
|
||
path="Src/stm32f4xx_it.c",
|
||
region="0",
|
||
must_contain=["hard_fault_report", "CFSR"],
|
||
hint="崩溃现场报告实现(打印 PC/LR/xPSR/CFSR/HFSR/BFAR/MMFAR)"
|
||
"须在 USER CODE BEGIN 0 区内(生成区内的标准保护位)。",
|
||
),
|
||
# Error_Handler 统一降级(2026-10-12;详述见看门狗设计说明.md §4.4)。函数体本身在 CubeMX 的
|
||
# `USER CODE BEGIN/END Error_Handler_Debug` 区内(L2 受保护),重生成不丢;
|
||
# 本项是 L3 兜底:被打回模板默认(__disable_irq+while(1) 静默挂死)**不报编译错**,
|
||
# 只在真出外设初始化故障时暴露——整机只剩 IWDG 兜底且无自检日志。
|
||
dict(
|
||
id="error-handler-degrade",
|
||
kind="protected",
|
||
path="Src/main.c",
|
||
region="Error_Handler_Debug",
|
||
must_contain=["DBG_ERROR", "NVIC_SystemReset"],
|
||
must_not_contain=["__disable_irq", "__builtin_return_address"],
|
||
hint="Error_Handler 必须打 1 条自检日志后 NVIC_SystemReset(),不得回到 "
|
||
"__disable_irq()+while(1) 静默挂死(无日志、只剩 IWDG 兜底)。"
|
||
"⚠ 勿用 __builtin_return_address:AC5 编成库调用,MicroLIB 无符号 = L6218E(2026-10-12 实证)。"
|
||
"恢复位置 = `USER CODE BEGIN Error_Handler_Debug` 区。",
|
||
),
|
||
# ---------------- P1:FreeRTOS 加固 ----------------
|
||
# ⚠ 2026-10-09 升级:这两项原先靠 FreeRTOSConfig.h 里**自造**的
|
||
# `USER CODE BEGIN FREE_RTOS_CONFIG_HARDENING` 区——模板不认这个区名,
|
||
# 重生成连标记带内容一起消失(实测两次)。现已改为 **L1 写进 .ioc**:
|
||
# GUI = FreeRTOS 页 → "Config parameters" 标签 → "Hook function related
|
||
# definitions" 组 → CHECK_FOR_STACK_OVERFLOW=2 / USE_MALLOC_FAILED_HOOK=1,
|
||
# 由 CubeMX 模板自己生成到 FreeRTOSConfig.h,永不被打回。
|
||
dict(
|
||
id="stack-overflow-check",
|
||
kind="ioc",
|
||
key="FREERTOS.configCHECK_FOR_STACK_OVERFLOW",
|
||
want="2",
|
||
hint="栈溢出机械检测(模式 2)必须在 .ioc 里(GUI: FreeRTOS → Config parameters → "
|
||
"Hook function related definitions)。关闭后溢出静默腐蚀堆相邻块。",
|
||
),
|
||
dict(
|
||
id="malloc-failed-hook",
|
||
kind="ioc",
|
||
key="FREERTOS.configUSE_MALLOC_FAILED_HOOK",
|
||
want="1",
|
||
hint="堆分配失败钩子必须在 .ioc 里(同上 GUI 位置)。",
|
||
),
|
||
# ⚠ 2026-10-09 连带坑与最终落点:在 GUI 里启用这两个钩子后,CubeMX 会在 freertos.c
|
||
# 生成 `USER CODE BEGIN 4 / 5` 区(默认内容是空桩)。若把我们的实现留在
|
||
# `Application` 区、又留着生成桩 → **同名函数两处定义 = 编译错**。
|
||
# 最终落点(比"每次删桩"更稳):**整个钩子函数写进 4 / 5 区**——区名由 CubeMX
|
||
# 生成、且其存在由 `.ioc` 启用钩子保证,重生成时 CubeMX 只保留区内容、不再补桩。
|
||
# 配套 helper `hook_ptr_ok()` 必须在钩子之前定义,故放 `USER CODE BEGIN PD` 区。
|
||
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="4",
|
||
must_contain=["vApplicationStackOverflowHook", "STACK OVERFLOW", "NVIC_SystemReset"],
|
||
hint="栈溢出钩子实现(裸 UART 打任务名后复位)须**整体**写在 `USER CODE BEGIN 4` 区内。",
|
||
),
|
||
dict(
|
||
id="malloc-failed-hook-impl",
|
||
kind="protected",
|
||
path="Src/freertos.c",
|
||
region="5",
|
||
must_contain=["vApplicationMallocFailedHook", "MALLOC FAILED"],
|
||
hint="堆分配失败钩子实现(只告警不复位)须**整体**写在 `USER CODE BEGIN 5` 区内。",
|
||
),
|
||
dict(
|
||
id="stack-overflow-hook-helper",
|
||
kind="protected",
|
||
path="Src/freertos.c",
|
||
region="PD",
|
||
must_contain=["hook_ptr_ok"],
|
||
hint="任务名指针范围校验 helper 须在钩子之前定义(现放 `USER CODE BEGIN PD` 区);"
|
||
"缺了它,钩子会因不可信 TCB 指针二次崩溃(2026-09-24 实测 HardFault)。",
|
||
),
|
||
# ---------------- 看门狗模块 ----------------
|
||
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"],
|
||
no_baseline=True, # 见下方说明:本文件每次 @build 后被 Keil 改写
|
||
hint="task_watchdog.c 须挂在 MDK 工程 App 组;CubeMX 重生成可能重建 .uvprojx。",
|
||
),
|
||
dict(
|
||
id="net-msgq-adopt",
|
||
kind="protected",
|
||
path="Src/freertos.c",
|
||
region="RTOS_QUEUES",
|
||
must_contain=["net_msg_queue_adopt"],
|
||
hint="消息队列句柄的唯一使用者是 net_socket,创建后必须在 USER CODE BEGIN RTOS_QUEUES 区"
|
||
"绑定给它(替代此前 BSP 对该全局变量的裸 extern)。丢了这一行,net_socket 的句柄"
|
||
"恒为 NULL → 所有 net_* 请求被判空丢弃,网络全线静默不通(无崩溃、无报错日志)。",
|
||
),
|
||
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,是最彻底的防丢失手段。",
|
||
),
|
||
# ---------------- ADC1(2026-10-09 首次启用) ----------------
|
||
dict(
|
||
id="adc1-hal-enabled",
|
||
kind="generated",
|
||
path="Inc/stm32f4xx_hal_conf.h",
|
||
must_contain=["#define HAL_ADC_MODULE_ENABLED"],
|
||
hint="HAL_ADC_MODULE_ENABLED 须为**生效**状态(非注释),否则 adc.c 不参与编译。",
|
||
),
|
||
# 注:ADC 时钟分频**不设防护**(2026-10-09 决定)。CubeMX 面板里 DIV2 已被约束禁用
|
||
# (FadcConstraint Max=36000000;APB2=84 MHz → DIV2=42 MHz 超限,选不到),
|
||
# .ioc 虽不保存 ADC1.ClockPrescaler 键,重生成也只会回落到约束允许的值。
|
||
# ADC1 上电自校准(2026-10-10 补):RM0090 §13.3.6 的**必需**步骤,CubeMX **不生成**,
|
||
# 且本仓库裁剪版 HAL **没有** HAL_ADCEx_Calibration_Start → 只能手写(L2),
|
||
# 故必须在此兜底。丢了它不报编译错,只让 ADC 带失调误差(难从日志看出)。
|
||
dict(
|
||
id="adc1-calibration-call",
|
||
kind="protected",
|
||
path="Src/adc.c",
|
||
region="ADC1_Init 2",
|
||
must_contain=["adc1_calibrate("],
|
||
hint="MX_ADC1_Init() 末尾必须调用 adc1_calibrate()(F4 ADC 上电自校准,CubeMX 不生成)。"
|
||
"丢了它 = ADC 未校准 → 存在失调误差,且不会报任何编译/运行错误。",
|
||
),
|
||
dict(
|
||
id="adc1-calibration-impl",
|
||
kind="protected",
|
||
path="Src/adc.c",
|
||
region="1",
|
||
must_contain=["int adc1_calibrate(void)", "ADC1_CAL_BIT", "ADC_CR2_ADON"],
|
||
hint="自校准实现(清 ADON→等生效→置 ADON→置 CAL→等 CAL 由硬件清零)须在 "
|
||
"`USER CODE BEGIN 1` 区。本仓库 HAL 无 HAL_ADCEx_Calibration_Start、"
|
||
"CMSIS 头无 ADC_CR2_CAL,两者都是裁剪造成的,勿以为『标准 HAL 里有』。",
|
||
),
|
||
# ---------------- ADC 采样节拍 + DMA(TIM3 + DMA2_Stream4) ----------------
|
||
# 节拍源 TIM3:同一个更新事件 ① ISR 脉冲 TPAFE CONVST ② 经 TRGO 触发 ADC1
|
||
# ③ 置位 ADC_WAKE_FLAG。丢了 ② 不报编译错,只是两片转换器不再同源(相位差自由漂移)。
|
||
dict(
|
||
id="adc-tick-tim3-branch",
|
||
kind="protected",
|
||
path="Src/main.c",
|
||
region="Callback 1",
|
||
must_contain=["TIM3", "tpafe5160_start_conv_irq()", "adc_tick_irq_notify()"],
|
||
hint="HAL_TIM_PeriodElapsedCallback 的 TIM3 分支必须同时做三件事:判 TIM3、脉冲 CONVST、"
|
||
"通知消费者。⚠ 节拍源**不能**换回 TIM6/TIM7——F4 上 ADC1 的外部触发清单里没有 "
|
||
"TIM6/TIM7 TRGO(只接 DAC),TIM3 的 TRGO 才是 ADC1 能收到的那个。",
|
||
),
|
||
dict(
|
||
id="ioc-adc-trig-t3trgo",
|
||
kind="ioc",
|
||
key="ADC1.ExternalTrigConv",
|
||
want="ADC_EXTERNALTRIGCONV_T3_TRGO",
|
||
hint="ADC1 外部触发必须是 T3_TRGO(与节拍源 TIM3 配对)。改回软件触发 = 每拍由任务启动"
|
||
"转换,DMA 与 TPAFE 不再同源对齐,且重新引入忙等。",
|
||
severity="FAIL",
|
||
),
|
||
dict(
|
||
id="ioc-adc-dma-continuous",
|
||
kind="ioc",
|
||
key="ADC1.DMAContinuousRequests",
|
||
want="ENABLE",
|
||
hint="DMAContinuousRequests(DDS 位)必须 ENABLE:关掉后一轮 9 通道只发**一次** DMA 请求"
|
||
"→ 静默丢 8 路,不报任何编译/运行错误。",
|
||
severity="FAIL",
|
||
),
|
||
dict(
|
||
id="adc1-dma-link",
|
||
kind="generated",
|
||
path="Src/adc.c",
|
||
must_contain=["hdma_adc1", "__HAL_LINKDMA", "DMA2_Stream4", "DMA_CIRCULAR"],
|
||
hint="ADC1 的 DMA 挂载(Stream4 + 循环模式 + __HAL_LINKDMA)须在位。"
|
||
"ADC1 在 DMA2 上只挂 Stream0 或 Stream4;选 Stream0 会连锁搬走 SPI1_RX/USART1_RX。",
|
||
),
|
||
dict(
|
||
id="adc1-dma-irq-priority",
|
||
kind="generated",
|
||
path="Src/dma.c",
|
||
must_contain=["HAL_NVIC_SetPriority(DMA2_Stream4_IRQn, 6, 0)"],
|
||
hint="⚠ CubeMX 在**换 DMA 流**时会把该流的 NVIC 优先级静默重置为 **0**(实测两轮)。"
|
||
"优先级 0 高于 configMAX_SYSCALL_INTERRUPT_PRIORITY(5) → 会抢占 FreeRTOS 临界区,"
|
||
"违反本仓『外设中断 ≥5』。每次改 DMA 流后必须回 NVIC 页核对。",
|
||
severity="FAIL",
|
||
),
|
||
]
|
||
|
||
|
||
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 <name> */` 与 `/* USER CODE END <name> */` 之间的内容。
|
||
|
||
实测本项目生成文件的标记形态是**单行注释**:
|
||
/* 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
|
||
|
||
# ---- 取待检内容 ----
|
||
# `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:
|
||
results.append(
|
||
(level, cid, "%s 的 USER CODE BEGIN/END %s 区不存在" % (path, c["region"]), c["hint"])
|
||
)
|
||
snap[cid] = None
|
||
continue
|
||
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
|
||
|
||
# ---- 基线快照 ----
|
||
# `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_neg]
|
||
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.3。",
|
||
))
|
||
|
||
return results, snap
|
||
|
||
|
||
# 已知的"标记区外"手工内容(存量,只能人工盯)
|
||
UNPROTECTED_LEGACY = [
|
||
(
|
||
"Src/freertos.c",
|
||
[
|
||
("timeSyncTask 手工定义(不在 .ioc FREERTOS.Tasks01 里)", r"timeSyncTask_attributes"),
|
||
],
|
||
),
|
||
]
|
||
|
||
|
||
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", newline="\n") 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())
|