四个 Fault 全部采集现场:MemManage/BusFault/UsageFault 由空死循环升级

原状:HardFault 已有崩溃采集,但 MemManage / BusFault / UsageFault 仍是空
while(1) —— 这三类故障发生时日志完全静默,且因 SHCSR 默认未使能,实际会
被升级成 HardFault,报告出笼统的 "HARDFAULT"、看不出根因。

改动:

- Src/stm32f4xx_it.c:三个 Handler 升级为手工 __asm 版(取 MSP/PSP 帧指针)
  -> B 到 per-fault C 入口 fault_entry_{mem,bus,usage} -> hard_fault_report()。
  hard_fault_report 增加 tag 参数,日志统一为
  "[ERR] <TAG> PC=.. LR=.. xPSR=.. R0-R3=.. CFSR=.. HFSR=..[/BFAR][/MMFAR]"。
  生成实体经 #define X X_Gen 改名保留,链接器各移除 2 字节(.map 已验)。
  三个各带独立锚点 MEMFAULT/BUSFAULT/USAGEFAULT-ANCHOR。
- 新增 fault_handler_enable()(USER CODE BEGIN 1 区):使能 SHCSR 的
  MEMFAULTENA/BUSFAULTENA/USGFAULTENA。声明入 Inc/stm32f4xx_it.h,调用在
  app_main_init() 第一行(纯寄存器写,不依赖日志/时钟)。
- 刻意不启用 CCR.UNALIGN_TRP / CCR.DIV_0_TRP:会把普通 bug 升级成死机,
  对本项目弊大于利(说明写进函数注释)。

实现约束(本轮踩坑):ARMCC 的 __asm 块取不到 C 的数据符号(ADR r1, xxx_tag
报 A1516E: Bad symbol,去 static 也无效),也无法给 C 函数传字符串参数,
故故障名必须由 C 侧 per-fault 入口带上。

配套:

- check_cubemx_survival.py:新增 fault-handlers-3 / fault-handler-enable 两项,
  UNPROTECTED_LEGACY 清掉三个 Fault(WARN 2->1),清单 16 -> 18 项。
- AGENTS.md:新增"4 个 Fault 全部采集"条目;ARMCC 汇编条目补 A1516E 第 3 坑;
  校验标记补 [ERR](四个 Fault 日志都以它开头 -> 一律判 FAIL,刻意)。
- 看门狗设计说明.md:§6.1 补 4 条 Fault 标记与 @test 禁项关系;
  §10.2 L3 清单与四个 Fault 采集小节;§11 结论表与变更记录。
- 待处理内容.md:新增 N-03d(已完成),N-03c 补第二次重生成结论。

验证:@build 0 错误 0 警告(Flash +156 B,RAM 无变化);pre-commit 13 钩子
全通过;审计 18 通过 / 1 警告 / 0 失败。
This commit is contained in:
2026-09-29 09:16:19 +08:00
parent 67d26ac37a
commit 8a18e388c7
9 changed files with 254 additions and 50 deletions
+4 -2
View File
@@ -1,8 +1,10 @@
{
"snapshot": {
"iwdg-defer-start": "f5c340730afbb4b7",
"hardfault-handler": "2afdbb1dcdc9a954",
"hardfault-report-impl": "7db7627c59f9f004",
"hardfault-handler": "1dfa875a1b75bea4",
"fault-handlers-3": "1dfa875a1b75bea4",
"fault-handler-enable": "be87fcd3a7926138",
"hardfault-report-impl": "7a4d1c2b4a4b6fee",
"stack-overflow-check": "4548c9c6ef1b2409",
"malloc-failed-hook": "4548c9c6ef1b2409",
"assert-not-deadloop": "d4a00718ab2bd071",
+5 -3
View File
@@ -16,7 +16,7 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
- `@build` = `& "MDK-ARM\build.bat"`(仓库根目录用**相对路径**);退出码 0 = 0 错误 0 警告,1 = 有警告或错误(查 `MDK-ARM/build_log.txt`);`build.bat` 硬编码本机 Keil 路径 `C:\Keil_v5\UV4\UV4.exe`(换机需改)
- `@test` / `test/host/run_boot_test.py` 一键闭环(2026-09-20 起为纯 Python 标准库实现,替代 PowerShell 版;`& '.venv\Scripts\python.exe' test/host/run_boot_test.py`):开串口+清日志 → 烧录(`MDK-ARM/download.bat`,下载后 Reset&Run)→ wait-idle(等 RX 静默 8s,超时 30s)→ `test/host/verify_log.py` 标记校验;退出码 0=PASS / 1=FAIL / 2=采集或烧录错误 / 3=用法/预检错误
- 默认校验标记:Required `App init OK` / `Network Init` / `net init: OK`;Forbidden `FAIL` / `ERROR` / `HardFault`;按模块定制标记(网络/存储/RTC/ADC/RS485/UI)见 `docs/测试说明/自动化测试流程.md`
- 默认校验标记:Required `App init OK` / `Network Init` / `net init: OK`;Forbidden `FAIL` / `ERROR` / `HardFault` / `[ERR]`(**四个 Fault 的崩溃日志都以 `[ERR]` 开头 → 一律判 FAIL**,这是刻意的:出现 Fault 即本次运行不通过);按模块定制标记(网络/存储/RTC/ADC/RS485/UI)见 `docs/测试说明/自动化测试流程.md`
- ⚠️ 旧绝对路径 `D:\Code\DTU 程序\...` 出现即失效(仓库已搬迁),一律改用仓库相对路径
- **下载/烧录铁律(2026-09-20 实测)**:走 `& "MDK-ARM\reset.bat" flash [文件]`(= `ST-LINK_CLI -c SWD -P <文件> -Rst -Run`),烧后 `-Rst -Run` **不可省略**——裸 `-P` 会报 `Programming Complete`,但目标核处于死循环/异常状态时写入并不落地(芯片内容不变;这就是 09/19~09/20 "反复重下载、芯片一直是旧镜像、二分法失效" 的根因);`.hex`/`.bin` 均可直烧(1MB ≈ 6.5s,快于 UV4 路径);只重启不烧录用 `reset.bat` 无参(= `-c SWD -Rst -Run`,本机 3 线接线唯一复位手段:nRST 未接、`-HardRst`/`RM=` 不可用)
- **烧录后必须回读校验**:`ST-LINK_CLI -c SWD -CmpFile <文件> 0x08000000`(全片逐字节比对,`No difference found` 才算真正写入);CLI = `C:\Program Files (x86)\STMicroelectronics\STM32 ST-LINK Utility\ST-LINK Utility\ST-LINK_CLI.exe`(v3.5.0.0,探针 SN `0B250D12A218303030303032`)→ 完整用法/复位语义/故障排查 `docs/测试说明/ST-LINK_CLI_复位使用说明.md`
@@ -38,8 +38,10 @@ 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~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)`),**零代码开销且不删生成区任何一行**
- **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` 触发)——清单 18 项 + 哈希基线漂移检测,直接列出"被覆盖了哪几处"。⚠️ **不要依赖"自造标记区"**:2026-09-29 实测 CubeMX **换了一代中断函数模板**(旧 `HardFault_Handler`/`HardFault_Report` 自造区 → 新函数体内 `HardFault_IRQn 0`/`W1_HardFault_IRQn 0`),旧区**连同内容一起消失**(看起来仍像"遵守了规则")。**生成区内无标记位可用的改动只能 L3 兜底**(如 `Src/iwdg.c` 的 `HAL_IWDG_Init()` 调用、4 个 Fault Handler 本体——本代模板整函数重写),一律加锚点注释(`IWDG-DEFER-START-ANCHOR` / `HARD-FAULT-ANCHOR` / `MEMFAULT-ANCHOR` / `BUSFAULT-ANCHOR` / `USAGEFAULT-ANCHOR`)供审计检出。方法论详见 `docs/子系统设计/看门狗设计说明.md` §10.1/§10.2
- **4 个 Cortex-M4 Fault 全部采集现场**(2026-09-29):`HardFault` / `MemManage` / `BusFault` / `UsageFault` 均为手工 `__asm` Handler(取 MSP/PSP 帧指针)→ `B` 到 per-fault C 入口 `fault_entry_{hard,mem,bus,usage}`(各自带故障名)→ `hard_fault_report(tag, frame)` 打 `[ERR] <TAG> PC/LR/xPSR/R0-R3/CFSR/HFSR[/BFAR][/MMFAR]` 后 `NVIC_SystemReset()`。**后三者默认禁用**(复位后 `SHCSR` 只开 HardFault),不使能就会被**升级成 HardFault**、日志看不出根因 → `fault_handler_enable()`(`Src/stm32f4xx_it.c` 的 `USER CODE BEGIN 1` 区)在 `app_main_init()` **第一行**调用。**刻意不启用** `CCR.UNALIGN_TRP` / `CCR.DIV_0_TRP`(会把普通 bug 升级成死机)。⚠️ **ARMCC `__asm` 取不到 C 的数据符号**(`ADR r1, xxx_tag` 报 `A1516E: Bad symbol`,去 static 也无效)且无法传字符串参数 → 故障名必须由 C 侧 per-fault 入口带上,不要在 `__asm` 里取 C 变量
- **`MDK-ARM/*.uvprojx` 每次 `@build` 后被 Keil UV4 自动改写,改动一律不入库**(2026-09-29 实测):UV4 构建时会把工程 XML 归一化成它本机风格的旧式写法(`<Tag />` → `<Tag></Tag>`、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 文件名` 计数变化)才提交
- **ARMCC 内联汇编语法(2026-09-29 踩坑,共 3 坑)**:① `__asm volatile ("MRS %0, lr" : "=r" (x))` 是 **GCC 语法**,ARMCC 报 `#18: expected a ")"`;② `__asm { MRS x, lr }` 块内联会把寄存器名当 C 标识符报 `#20: identifier "lr" is undefined`;③ `__asm` 块里**取不到 C 的数据符号**,`ADR r1, xxx_tag` 报 `A1516E: Bad symbol 'xxx_tag'`(去掉 `static` 也无效——汇编器只认自己作用域内的符号)。**结论**:整个函数用 `__asm void f(void) { ... }`;要传"字符串/表格"这类 C 侧数据,就让 `__asm` 只取寄存器参数然后 `B` 到一个普通 C 函数,由 C 侧补上。若要覆盖 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 约束,勿"优化")
+8
View File
@@ -15,6 +15,7 @@
#include "app_cfg.h"
#include "main.h" /* ST_DIR4_GPIO_Port / ST_DIR4_Pin 等 CubeMX 宏 */
#include "stm32f4xx_it.h" /* fault_handler_enable() */
#include "usart.h" /* huart5 */
#include "rs485.h"
#include "sd2506.h"
@@ -90,6 +91,13 @@ static void reset_cause_str(uint32_t csr, char *buf, unsigned int len) {
* 6) 触发首次 ADC 转换
*/
int app_main_init(void) {
/* 0) 使能 MemManage / BusFault / UsageFault(复位后默认只有 HardFault 使能)。
* 必须尽早——放在所有业务初始化**之前**,这样从第一行业务代码起,
* 任何内存/总线/非法指令类故障都会被独立捕获并打出故障名,
* 而不是被升级成 HardFault 后只看到笼统的 "HARDFAULT"。
* 纯寄存器写(SCB->SHCSR),不依赖日志/时钟,故放在最前无副作用。 */
fault_handler_enable();
/* 日志引擎初始化(创建待发队列;须先于任何 syslog_post) */
(void)syslog_mcu_init();
+4
View File
@@ -69,6 +69,10 @@ void DMA2_Stream3_IRQHandler(void);
void DMA2_Stream7_IRQHandler(void);
/* USER CODE BEGIN EFP */
/* 使能 MemManage / BusFault / UsageFault 三个异常(默认只有 HardFault 使能)。
* 见 Src/stm32f4xx_it.c 的实现注释;须在 app_main_init() 早期调用。 */
void fault_handler_enable(void);
/* USER CODE END EFP */
#ifdef __cplusplus
+130 -22
View File
@@ -92,9 +92,25 @@ static void hard_fault_puthex(uint32_t v)
hard_fault_write(out, 8u);
}
void hard_fault_report(uint32_t *frame)
/* 崩溃现场报告核心(四个 Fault 共用)。
* 入口参数:tag - 故障名(ASCII 字面量,如 "HARDFAULT" / "MEMFAULT")
* frame - 异常栈帧 {R0,R1,R2,R3,R12,LR,PC,xPSR}
* 说明:CFSR 一次性打出,其子域已含 MMARVALID(bit7)/BFARVALID(bit15),
* 故 MMFAR/BFAR 的"是否有意义"由 CFSR 判定,四个 Fault 都能看全。
*
* ⚠ 本函数**不直接**被异常向量调用——异常入口是下方的 `__asm` 版 Handler,
* 它们只负责取异常帧指针,然后 `B` 到下面 4 个 per-fault 入口之一
* (每个入口自带 tag 字面量后再调本函数)。
* 这样做的原因:ARMCC 的 `__asm` 块**取不到 C 的 static 数据符号**
* (实测:`ADR r1, xxx_tag` 报 `A1516E: Bad symbol`,去掉 static 也无效),
* 而 `__asm` 里也无法给 C 函数传结构体/字符串参数。故把"带 tag"这一步
* 放在普通 C 函数里做。
*/
void hard_fault_report(const char *tag, uint32_t *frame)
{
HF_PUTS("\r\n[ERR] HARDFAULT PC=");
HF_PUTS("\r\n[ERR] ");
hard_fault_write(tag, 16u);
HF_PUTS(" PC=");
hard_fault_puthex(frame[6]);
HF_PUTS(" LR=");
hard_fault_puthex(frame[5]);
@@ -127,10 +143,38 @@ void hard_fault_report(uint32_t *frame)
NVIC_SystemReset();
}
/* 四个 per-fault 入口:__asm Handler 取到帧指针后 `B` 到这里,由 C 补上故障名。
* 名字用 `fault_entry_*` 前缀,与向量符号(`*_Handler`)区分开。 */
void fault_entry_hard(uint32_t *frame)
{
hard_fault_report("HARDFAULT", frame);
}
void fault_entry_mem(uint32_t *frame)
{
hard_fault_report("MEMFAULT", frame);
}
void fault_entry_bus(uint32_t *frame)
{
hard_fault_report("BUSFAULT", frame);
}
void fault_entry_usage(uint32_t *frame)
{
hard_fault_report("USAGEFAULT", frame);
}
/* 为生成函数改名,避免与下方手工 `__asm` 实现符号冲突。
* 生成实体改名为 HardFault_Handler_Gen(永不执行,仅为占用名字不冲突,
* 保持生成区代码零删除)。 */
* 生成实体改名为 <原名>_Gen(永不执行,仅为占名字不冲突,
* 保持生成区代码零删除;未被引用 → 链接器自动移除)。
*
* 覆盖的 4 个 Cortex-M4 异常:HardFault / MemManage / BusFault / UsageFault。
* 后三者须在 SHCSR 中使能对应位才会真正触发(见 fault_handler_enable)。 */
#define HardFault_Handler HardFault_Handler_Gen
#define MemManage_Handler MemManage_Handler_Gen
#define BusFault_Handler BusFault_Handler_Gen
#define UsageFault_Handler UsageFault_Handler_Gen
/* USER CODE END 0 */
/* External variables --------------------------------------------------------*/
@@ -184,16 +228,20 @@ void NMI_Handler(void)
* 命名:`#undef HardFault_Handler` 后重定义为 __asm 版本,与向量表符号一致;
* 为避免与下方生成实体"重复定义"(ARMCC 报 #247,其"已定义"检查
* 早于 C 作用域),已在文件头用 `#define HardFault_Handler
* HardFault_Handler_Gen` 把生成实体改名为不冲突的符号。 */
* HardFault_Handler_Gen` 把生成实体改名为不冲突的符号。
*
* ⚠ ARMCC 语法:内联汇编**只能**用整函数 `__asm void f(void) {...}` 形态。
* `__asm volatile("MRS %0, lr" : "=r"(x))`(GCC 语法)报 #18;
* `__asm { MRS x, lr }` 块内联会把寄存器名当 C 标识符报 #20(2026-09-29 实测)。 */
#undef HardFault_Handler
__asm void HardFault_Handler(void)
{
extern hard_fault_report
extern fault_entry_hard
TST lr, #4
ITE EQ
MRSEQ r0, MSP
MRSNE r0, PSP
B hard_fault_report
B fault_entry_hard
}
void HardFault_Handler_Gen(void)
@@ -206,13 +254,24 @@ void HardFault_Handler_Gen(void)
}
/**
* @brief This function handles Memory management fault.
*/
void MemManage_Handler(void)
* @brief This function handles Memory management fault.
*/
/* MEMFAULT-ANCHOR — 见 HARD-FAULT-ANCHOR(同一套采集方案,勿删本锚点)
* 触发条件:访问 MPU 禁止区 / 非法特权级访问。须 SHCSR 的 MEMFAULTENA(bit16) 使能。
* 命中时 CFSR 的 MMARVALID(bit7) 置 1,MMFAR 记录出错地址(报告函数按此判定)。 */
#undef MemManage_Handler
__asm void MemManage_Handler(void)
{
/* USER CODE BEGIN MemoryManagement_IRQn 0 */
extern fault_entry_mem
TST lr, #4
ITE EQ
MRSEQ r0, MSP
MRSNE r0, PSP
B fault_entry_mem
}
/* USER CODE END MemoryManagement_IRQn 0 */
void MemManage_Handler_Gen(void)
{
while (1)
{
/* USER CODE BEGIN W1_MemoryManagement_IRQn 0 */
@@ -221,13 +280,24 @@ void MemManage_Handler(void)
}
/**
* @brief This function handles Pre-fetch fault, memory access fault.
*/
void BusFault_Handler(void)
* @brief This function handles Pre-fetch fault, memory access fault.
*/
/* BUSFAULT-ANCHOR — 见 HARD-FAULT-ANCHOR(同一套采集方案,勿删本锚点)
* 触发条件:非法总线访问 / 取指(外设时钟未开、错误指针、DMA 越界等)。
* 须 SHCSR 的 BUSFAULTENA(bit17) 使能。命中时 CFSR 的 BFARVALID(bit15) 置 1。 */
#undef BusFault_Handler
__asm void BusFault_Handler(void)
{
/* USER CODE BEGIN BusFault_IRQn 0 */
extern fault_entry_bus
TST lr, #4
ITE EQ
MRSEQ r0, MSP
MRSNE r0, PSP
B fault_entry_bus
}
/* USER CODE END BusFault_IRQn 0 */
void BusFault_Handler_Gen(void)
{
while (1)
{
/* USER CODE BEGIN W1_BusFault_IRQn 0 */
@@ -236,13 +306,25 @@ void BusFault_Handler(void)
}
/**
* @brief This function handles Undefined instruction or illegal state.
*/
void UsageFault_Handler(void)
* @brief This function handles Undefined instruction or illegal state.
*/
/* USAGEFAULT-ANCHOR — 见 HARD-FAULT-ANCHOR(同一套采集方案,勿删本锚点)
* 触发条件:未定义指令 / 非对齐访问 / 除零(须 DIV_0_TRP)/ 非法状态。
* 须 SHCSR 的 USGFAULTENA(bit18) 使能。
* ⚠ 本项目**刻意不使用硬件除零陷入**(C 代码里除零应自己判,避免把普通 bug 升级为死机)。 */
#undef UsageFault_Handler
__asm void UsageFault_Handler(void)
{
/* USER CODE BEGIN UsageFault_IRQn 0 */
extern fault_entry_usage
TST lr, #4
ITE EQ
MRSEQ r0, MSP
MRSNE r0, PSP
B fault_entry_usage
}
/* USER CODE END UsageFault_IRQn 0 */
void UsageFault_Handler_Gen(void)
{
while (1)
{
/* USER CODE BEGIN W1_UsageFault_IRQn 0 */
@@ -250,6 +332,32 @@ void UsageFault_Handler(void)
}
}
/* USER CODE BEGIN 1 */
/* 使能 MemManage / BusFault / UsageFault 三个异常。
*
* 为什么必须做:复位后 SCB->SHCSR 只开了 HardFault,后三者处于**禁用**状态——
* 它们触发时会被升级成 HardFault,`hard_fault_report` 打出来的是 "HARDFAULT"
* 且看不出根因。这里逐个置使能位,日志里才能直接看到 MEMFAULT / BUSFAULT /
* USAGEFAULT 字样。
*
* ⚠ 刻意**不**打开以下两项(都会把普通 bug 升级成死机,对本项目弊大于利):
* - CCR.UNALIGN_TRP :非对齐访问陷入。Cortex-M4 硬件本可处理非对齐访问,
* 打开后协议解析里常见的非对齐取字段会直接死机。
* - CCR.DIV_0_TRP :除零陷入。除零应由业务代码自己判,不要靠硬件兜。
* 打开它们只会让"本该只是算错个值"变成"整机复位"。
*
* 调用时机:app_main_init() 早期(日志可用前即可,越早越好)。
*/
void fault_handler_enable(void)
{
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk |
SCB_SHCSR_BUSFAULTENA_Msk |
SCB_SHCSR_USGFAULTENA_Msk;
}
/* USER CODE END 1 */
/**
* @brief This function handles Debug monitor.
*/
@@ -36,10 +36,10 @@
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 264,860 B (258.65 KB) | 1,048,576 B (1024 KB) | 25.3% |
| ER_IROM1 | 265,016 B (258.80 KB) | 1,048,576 B (1024 KB) | 25.3% |
| RW_IRAM1 | 87,080 B (85.04 KB) | 131,072 B (128 KB) | 66.4% |
| RW_CCM | 49,184 B (48.03 KB) | 65,536 B (64 KB) | 75.0% |
| **Flash (ROM 合计)** | **265,616 B (259.4 KB)** | 1,048,576 B (1024 KB) | **25.3%** |
| **Flash (ROM 合计)** | **265,772 B (259.5 KB)** | 1,048,576 B (1024 KB) | **25.3%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
+65 -9
View File
@@ -628,9 +628,26 @@ void task_wd_poll(void)
| `[WD] armed: timeout ~2498ms (worst ~1701ms), miss_tol=800ms` | `task_wd_start()` | IWDG 已启动(真机自检必查标记) |
| `[WD] task stall: mask=0x%08X req=0x%08X miss=%lu` | 连续 8 窗未达标时(每窗一次,共打印至复位) | 即将停喂 → 约 2.5 s 后 IWDG 复位。`mask` 的 0 位 = 缺席任务 |
| `[ERR] ASSERT FAILED <file>:<line> (resetting)` | `vApplicationAssertFailed()` | FreeRTOS 断言失败(`Src/freertos.c`),打印后主动复位 |
| `[ERR] HARDFAULT PC=.. LR=.. xPSR=.. R0-R3=.. CFSR=.. HFSR=.. [BFAR=..] [MMFAR=..]` | `HardFault_Handler` → `fault_entry_hard` | 硬故障现场(`Src/stm32f4xx_it.c`) |
| `[ERR] MEMFAULT PC=.. …` | `MemManage_Handler` → `fault_entry_mem` | MPU/特权级违规,含 `MMFAR` 出错地址(`CFSR.MMARVALID` 置位时) |
| `[ERR] BUSFAULT PC=.. …` | `BusFault_Handler` → `fault_entry_bus` | 非法总线访问/取指,含 `BFAR` 出错地址(`CFSR.BFARVALID` 置位时) |
| `[ERR] USAGEFAULT PC=.. …` | `UsageFault_Handler` → `fault_entry_usage` | 未定义指令/非法状态 |
| `[ERR] STACK OVERFLOW task=<name>` | `vApplicationStackOverflowHook` | 既有钩子(栈溢出) |
| `[ERR] MALLOC FAILED (heap exhausted)` | `vApplicationMallocFailedHook` | 既有钩子(堆耗尽,只告警不复位) |
> 四个 Fault 报告都由 `hard_fault_report(tag, frame)` 输出,格式统一为
> `[ERR] <TAG> PC=… LR=… xPSR=… R0..R3=… CFSR=… HFSR=…`,后接
> `BFAR` / `MMFAR`(仅当 `CFSR` 对应 valid 位置 1)。**输出后 `HAL_Delay(200)`
> 排空 UART 再 `NVIC_SystemReset()`**,因此 `@test` 采集到的日志会以
> 一次干净的复位收尾(复位原因应为 `software`,而非 `IWDG`)。
>
> ⚠️ **与 `@test` 禁项的关系**:`run_boot_test.py` 的
> `FORBIDDEN_DEFAULT = "FAIL;ERROR;HardFault;[ERR]"` **已含 `[ERR]`**,
> 因此**任何一条崩溃日志都会让 `@test` 判 FAIL** —— 这是刻意的:
> 出现 Fault 即代表本次运行不算通过。「4 个 Fault 打各自的名字」的意义在于
> **归因**:日志里直接看到 `MEMFAULT` / `BUSFAULT` / `USAGEFAULT` 就知道是
> 哪类故障,而不必再从笼统的 `HARDFAULT` 里反推(后者还需人工读 `CFSR` 子域)。
> 日志标签说明:`[APP]` 是 `App/app/app_main.c` 的 `DBG_TAG`;`[WD]` 是
> `App/util/task_watchdog.c` 的 `DBG_TAG`(定义在两个 include 之间,
> 是 `dbg_log.h` 的既有取标签方式)。全部为纯 ASCII,规避 `AGENTS.md` 记载的
@@ -888,13 +905,50 @@ external pin → "pin reset" (新增)
| 文件 | 内容 | 为什么无法 L1/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/stm32f4xx_it.c` | 4 个 Fault Handler 实体(`HardFault` / `MemManage` / `BusFault` / `UsageFault`) | 本代模板整函数重写、函数体外无区。各带独立锚点(`HARD-FAULT-ANCHOR` / `MEMFAULT-ANCHOR` / `BUSFAULT-ANCHOR` / `USAGEFAULT-ANCHOR`)+ 审计 FAIL 项 |
| `Src/freertos.c` | `.stack_size` 行内的中文注释 | 该行由 CubeMX 按 `.ioc` 生成,"值"受保护、"注释"不受。**注释是锦上添花**,丢失不影响功能(值在 `.ioc` 里) |
> **结论**:L3 清单已从"3 个 Fault 处理函数"扩展为"3 项生成区改动 + 3 个 Fault 函数"。
> 前 3 项已挂审计 FAIL(丢了一定知道),后 3 项建议后续一并升级为崩溃采集 + 审计。
> **L3 的定位是兜底而非根治**——只要改动落在生成区,就不可能 L1/L2。
> **结论**:L3 清单只剩 2 项(iwdg 生成区改动、4 个 Fault Handler 实体)——**均为
> 生成区内无标记位可用的客观限制**,靠锚点注释 + 审计 FAIL 兜底(丢了一定知道)。
> 其余要么已在 `.ioc`(L1),要么已进 `USER CODE` 区(L2)。
> 剩余 1 项 WARN 是 `Src/freertos.c` 的 `timeSyncTask` 手工定义(属性块在
> `USER CODE BEGIN Variables` 区,本身受保护;WARN 只是提示"它不在 `.ioc` 里")。
#### 四个 Fault 异常的采集(2026-09-29 完成)
`HardFault` 之外,`MemManage` / `BusFault` / `UsageFault` 原先都是**空 `while(1)`**
(死机静默)。现已全部升级为崩溃采集:
| Fault | 触发条件 | 命中时的地址寄存器 | 使能位 |
|---|---|---|---|
| `HardFault` | 其他 Fault 未使能时的兜底 / 升级 | — | 恒使能 |
| `MemManage` | MPU 禁止区访问、非法特权级访问 | `MMFAR`(`CFSR.MMARVALID` bit7) | `SHCSR.MEMFAULTENA` bit16 |
| `BusFault` | 非法总线访问/取指(时钟未开、野指针、DMA 越界) | `BFAR`(`CFSR.BFARVALID` bit15) | `SHCSR.BUSFAULTENA` bit17 |
| `UsageFault` | 未定义指令、非法状态(本项目不启用非对齐/除零陷入) | —(看 `CFSR` 子域) | `SHCSR.USGFAULTENA` bit18 |
**关键**:复位后 `SHCSR` **只开了 `HardFault`**,后三者默认禁用——不使能的话,
它们触发时会被**升级成 `HardFault`**,日志只显示笼统的 `HARDFAULT`、看不出根因。
故新增 `fault_handler_enable()`(`Src/stm32f4xx_it.c` 的 `USER CODE BEGIN 1` 区,
声明在 `Inc/stm32f4xx_it.h` 的 `USER CODE BEGIN EFP` 区),在
`app_main_init()` **第一行**调用(纯寄存器写,不依赖日志/时钟)。
**刻意不打开的两项**(都会把普通 bug 升级成死机,对本项目弊大于利):
- `CCR.UNALIGN_TRP`:非对齐访问陷入。Cortex-M4 硬件本可处理非对齐访问,
打开后协议解析里常见的非对齐取字段会直接死机
- `CCR.DIV_0_TRP`:除零陷入。除零应由业务代码自己判,不该靠硬件兜
**实现要点(ARMCC 汇编限制)**:`__asm` 块**取不到 C 的 static 数据符号**
(实测 `ADR r1, xxx_tag` 报 `A1516E: Bad symbol`,去掉 `static` 也无效),
且 `__asm` 里无法给 C 函数传字符串参数。故采用:
```
__asm 版 Handler(.emb_text) → 只取异常帧指针,B 到 per-fault 入口
fault_entry_hard/mem/bus/usage(C) → 各自带上故障名,调 hard_fault_report(tag, frame)
hard_fault_report(tag, frame)(C) → 统一打印现场后 NVIC_SystemReset()
```
日志形如 `[ERR] MEMFAULT PC=... CFSR=... MMFAR=...`(故障名 + 全量现场)。
#### L3 落地:审计脚本 + 基线
@@ -904,11 +958,12 @@ python scripts/check_cubemx_survival.py --check # 门禁模式(pre-commit
python scripts/check_cubemx_survival.py --write # 重建基线(确认改动无误后)
```
- **清单**:16 项覆盖 P0/P1 全部防线(iwdg 延后启动、崩溃采集 ×2、两个加固宏、
断言改造 ×2、看门狗模块与工程挂载、复位原因锁存、HAL 驱动与使能宏、`.ioc` 关键项)
- **清单**:18 项覆盖 P0/P1 全部防线(iwdg 延后启动、4 个 Fault Handler 采集、
故障使能、两个加固宏、断言改造 ×2、看门狗模块与工程挂载、复位原因锁存、
HAL 驱动与使能宏、`.ioc` 关键项)
- **已实测有效**:2026-09-29 第二次重生成时,脚本**准确检出全部 5 项被覆盖**(5 FAIL)
- **漂移检测**:把各项内容哈希存 `.cubemx_survival_baseline.json`,`--check` 比对。
⚠️ **`MDK-ARM/*.uvprojx` 排除在漂移检测之外**(15 项入基线,16 项参与断言)——
⚠️ **`MDK-ARM/*.uvprojx` 排除在漂移检测之外**(17 项入基线,18 项参与断言)——
该文件每次 `@build` 后被 Keil UV4 自动改写(XML 格式归一化 + `PackID` 降级到
本机 DFP 版本),内容每次构建都不同;若参与检测则**每次构建后提交都会被拦**。
脚本内以 `no_baseline=True` 标记,只保留"源文件仍挂载"断言
@@ -938,7 +993,7 @@ python scripts/check_cubemx_survival.py --write # 重建基线(确认改动
| **超时** | 2498 ms(PR=4 / RLR=1249);最坏(LSI 47 kHz)≈1701 ms;中间留 800 ms 停喂判定裕度 |
| **启动时机** | **defaultTask bring-up 完成后**——绕开初始化深链超时陷阱,代价是启动段无保护 |
| **断言** | `configASSERT` 改"先记录后复位",与 IWDG 形成双保险 |
| **诊断** | 读 `RCC_CSR` 硬件标志判复位原因(权威),替代"靠时间差猜上电/重启" |
| **诊断** | ① 读 `RCC_CSR` 硬件标志判复位原因(权威),替代"靠时间差猜上电/重启";② **4 个 Fault(Hard/MM/Bus/Usage)全部采集现场**并打故障名(`fault_handler_enable()` 使能后三者,否则被升级成 HardFault) |
| **已知代价** | ① 启动段无保护 ② `ftpTask` 首期不监视 ③ LSI 精度差需按最坏值设计 |
| **成本** | ≈ 24 B RAM / ≈ 0.7 KB Flash,可忽略 |
@@ -994,3 +1049,4 @@ python scripts/check_cubemx_survival.py --write # 重建基线(确认改动
| 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 警告 |
| 2026-09-29 | **3 个 Fault 升级为崩溃采集** | `MemManage` / `BusFault` / `UsageFault` 由空 `while(1)`(死机静默)升级为与 `HardFault` 同一套采集:生成区整函数替换(各带独立锚点 `MEMFAULT-ANCHOR` / `BUSFAULT-ANCHOR` / `USAGEFAULT-ANCHOR`)+ `#define X X_Gen` 改名保留生成实体(链接器各移除 2 字节)。新增 `fault_handler_enable()` 使能 `SHCSR` 后三者(默认只开 HardFault,不开则故障被升级、看不出根因),声明入 `Inc/stm32f4xx_it.h`、调用在 `app_main_init()` 第一行。因 ARMCC `__asm` 取不到 C 数据符号,改为 per-fault C 入口 `fault_entry_{hard,mem,bus,usage}` 带故障名调 `hard_fault_report(tag, frame)`。**刻意不启用** `UNALIGN_TRP` / `DIV_0_TRP`(避免把普通 bug 升级成死机)。审计脚本新增 `fault-handlers-3` / `fault-handler-enable` 两项、`UNPROTECTED_LEGACY` 清掉 3 个 Fault(WARN 2→1)、清单 16→18 项。`@build` 0 错误 0 警告;审计 18 通过 / 1 警告 / 0 失败 |
+2 -1
View File
@@ -33,4 +33,5 @@
| N-03 | **引入独立看门狗(IWDG)+ 任务心跳位图**——现状全库无看门狗(HAL 驱动源码也不在仓库),任何形态死机不可自恢复;另 `configASSERT` 为关中断死循环(连日志都不打)。**代码已落地并编译通过(2026-09-29)**:CubeMX 生成 IWDG(Prescaler=64/Reload=1249)+ `App/util/task_watchdog.{c,h}` 心跳位图 + 6 任务埋点 + `RCC_CSR` 复位原因锁存 + `configASSERT` 改造 | **剩台架验证**(故障注入 V2/V4、长跑 V5 ≥30 min);文档 §10 第 7~10 步待办 | **见 [`子系统设计/看门狗设计说明.md`](子系统设计/看门狗设计说明.md)**(§10 有实施状态表) | `审查记录/项目审查与优化建议报告.md` N-03a/b/c |
| N-03a | `configASSERT(x)` = `taskDISABLE_INTERRUPTS(); for(;;);`(`Inc/FreeRTOSConfig.h`)→ 断言时无任何输出 | ✅ **已修(2026-09-29)**:改为 `vApplicationAssertFailed(__FILE__, __LINE__)`,打印文件名+行号后主动复位(`Src/freertos.c`);宏已迁入 `USER CODE BEGIN 1` 区防重生成丢失 | 同上 N-03b |
| N-03b | 启动段(`app_main_init` + `defaultTask` bring-up)在新方案下**无看门狗保护** | 显式代价(未变):`看门狗设计说明.md` §4.5 决策 C + §9 R1;该段死机表现为"无输出 + LED 不闪" | 同上 N-03c |
| N-03c | **CubeMX 重生成会覆盖 `USER CODE` 标记区之外的定制** | ⚠️ **2026-09-29 实测确认**:重生成覆盖 4 处(P0 `Src/iwdg.c` 自动启动、P1 `HardFault_Handler` 打回空死循环、P1 `FreeRTOSConfig.h` 两个加固宏被删、P2 `freertos.c` 中文注释);已全部恢复并加"重生成会删除"警示注释;**今后每次 CubeMX 重生成后须重跑本文档 §10.1 的核对清单** | 本次实施记录 |
| N-03c | **CubeMX 重生成会覆盖 `USER CODE` 标记区之外的定制** | ⚠️ **2026-09-29 两次重生成实测确认**:首次覆盖 4 处、第二次覆盖 5 处(P0 `Src/iwdg.c` 自动启动、P1 `HardFault_Handler` 打回空死循环、P1 `FreeRTOSConfig.h` 两个加固宏被删、P2 `freertos.c` 中文注释)。✅ **已建立自动检出**:`scripts/check_cubemx_survival.py`(18 项清单 + 哈希基线,已挂 pre-commit)——第二次实测**准确检出全部 5 处**。⚠️ 关键发现:CubeMX **换了一代中断模板**,"生成区外的自造标记区"同样不被保留(详见设计文档 §10.1)。**今后每次重生成后跑一次该脚本即可** | 本次实施记录 |
| N-03d | 3 个次要 Fault(`MemManage`/`BusFault`/`UsageFault`)为空 `while(1)`,死机静默 | ✅ **已修(2026-09-29)**:升级为与 `HardFault` 同一套崩溃采集(手工 `__asm` Handler → per-fault C 入口 → `hard_fault_report(tag, frame)`,日志打 `[ERR] MEMFAULT/BUSFAULT/USAGEFAULT + 全量现场`);新增 `fault_handler_enable()` 使能 `SHCSR` 后三者(默认只开 HardFault,否则故障被升级、看不出根因),在 `app_main_init()` 第一行调用。**刻意不启用** `UNALIGN_TRP`/`DIV_0_TRP`(避免把普通 bug 升级成死机)。`@build` 0/0;审计 18 通过 / 1 警告 / 0 失败 | 见 [`子系统设计/看门狗设计说明.md`](子系统设计/看门狗设计说明.md) §10.2 |
+34 -11
View File
@@ -96,20 +96,51 @@ CHECKS = [
must_contain=[
"HARD-FAULT-ANCHOR", # 手工锚点注释,重生成后会消失
"__asm void HardFault_Handler", # 手工汇编 Handler
"hard_fault_report", # 跳转目标
"fault_entry_hard", # 跳转目标(C 侧带故障名)
],
must_not_contain=["HardFault_Handler(void)\r\n{\r\n while (1)"],
hint="HardFault_Handler 须为手工汇编版(取 MSP/PSP 交 hard_fault_report),"
hint="HardFault_Handler 须为手工汇编版(取 MSP/PSP 交 fault_entry_hard),"
"生成实体改名 HardFault_Handler_Gen。被打回空 while(1) 后死机将完全静默。"
"恢复后补回 `HARD-FAULT-ANCHOR` 锚点注释。",
),
# ⚠ 2026-09-29:MemManage / BusFault / UsageFault 三个也升级为崩溃采集。
# 模板形态与 HardFault 相同(整函数重写、函数体外无标记区)→ 同一套方案:
# 生成区整函数替换 + 独立锚点 + `#define X X_Gen` 改名保留生成实体。
# 由一项统一检查(同文件、同 kind),避免 4 条重复项。
dict(
id="fault-handlers-3",
kind="wholefile",
path="Src/stm32f4xx_it.c",
must_contain=[
"MEMFAULT-ANCHOR",
"BUSFAULT-ANCHOR",
"USAGEFAULT-ANCHOR",
"__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。"
),
dict(
id="hardfault-report-impl",
kind="protected",
path="Src/stm32f4xx_it.c",
region="0",
must_contain=["hard_fault_report", "CFSR"],
hint="HardFault 现场报告实现(打印 PC/LR/xPSR/CFSR/HFSR/BFAR/MMFAR)"
hint="崩溃现场报告实现(打印 PC/LR/xPSR/CFSR/HFSR/BFAR/MMFAR)"
"须在 USER CODE BEGIN 0 区内(生成区内的标准保护位)。",
),
# ---------------- P1:FreeRTOS 加固 ----------------
@@ -368,14 +399,6 @@ UNPROTECTED_LEGACY = [
("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\)"),
],
),
]