diff --git a/.cubemx_survival_baseline.json b/.cubemx_survival_baseline.json index 1b4cbe66..f8eabee1 100644 --- a/.cubemx_survival_baseline.json +++ b/.cubemx_survival_baseline.json @@ -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", diff --git a/AGENTS.md b/AGENTS.md index 30e1dbd5..5075cf29 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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] 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 归一化成它本机风格的旧式写法(`` → ``、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 约束,勿"优化") diff --git a/App/app/app_main.c b/App/app/app_main.c index dbbf67f2..d220529c 100644 --- a/App/app/app_main.c +++ b/App/app/app_main.c @@ -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(); diff --git a/Inc/stm32f4xx_it.h b/Inc/stm32f4xx_it.h index d86a5de8..9dd4e938 100644 --- a/Inc/stm32f4xx_it.h +++ b/Inc/stm32f4xx_it.h @@ -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 diff --git a/Src/stm32f4xx_it.c b/Src/stm32f4xx_it.c index f900c8b6..c55e4faa 100644 --- a/Src/stm32f4xx_it.c +++ b/Src/stm32f4xx_it.c @@ -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. */ diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index ff45192b..b2a0f9ff 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| 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%** | 各区的容量与常量来源: diff --git a/docs/子系统设计/看门狗设计说明.md b/docs/子系统设计/看门狗设计说明.md index 39e67bf4..905f530b 100644 --- a/docs/子系统设计/看门狗设计说明.md +++ b/docs/子系统设计/看门狗设计说明.md @@ -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 : (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=` | `vApplicationStackOverflowHook` | 既有钩子(栈溢出) | | `[ERR] MALLOC FAILED (heap exhausted)` | `vApplicationMallocFailedHook` | 既有钩子(堆耗尽,只告警不复位) | +> 四个 Fault 报告都由 `hard_fault_report(tag, frame)` 输出,格式统一为 +> `[ERR] 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 失败 | diff --git a/docs/待处理内容.md b/docs/待处理内容.md index 6c413f38..187a808f 100644 --- a/docs/待处理内容.md +++ b/docs/待处理内容.md @@ -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 | diff --git a/scripts/check_cubemx_survival.py b/scripts/check_cubemx_survival.py index 03852c5b..889f4780 100644 --- a/scripts/check_cubemx_survival.py +++ b/scripts/check_cubemx_survival.py @@ -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\)"), - ], - ), ]