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\)"), ], ), ]