四个 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
@@ -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 |