▼點擊下方名片,關注公眾號,獲取更多精彩內容▼
這里是《賀老師講嵌入式AI》,我是《嵌入式AI:讓單片機學會思考》課程主理人,專注AI在MCU上的落地實踐。
關注公眾號后,回復【嵌入式AI】,可以免費獲取更多賀老師準備的專業資料。
模型在 PC 上驗證正常,轉換也沒有報錯;燒進 STM32,程序卻在第一次調用 ai_network_run() 或 Invoke() 后直接鉆進 HardFault_Handler。這時最常見的處理是把激活區再加幾十 KB,重新編譯,繼續碰運氣。
HardFault 不是一種故障原因,而是多類異常最終匯合的入口。數組越界、任務棧耗盡、無效地址、錯誤內存區域、非對齊訪問、FPU 配置不一致,都可能落到同一個死循環里。只看“程序停在 HardFault”幾乎沒有診斷價值。
Cortex-M3、M4、M7、M33 等內核的 System Control Block 會記錄故障狀態。最重要的是 CFSR、HFSR、BFAR 和 MMFAR。異常發生時,處理器還會把 R0、R1、R2、R3、R12、LR、PC 和 xPSR 壓入當前棧;其中 PC 指向觸發異常附近的指令。
| 1. 保存異常棧 PC / LR / xPSR |
| 2. 讀取故障寄存器 CFSR / HFSR |
| 3. 定位故障地址 BFAR / MMFAR |
| 4. 回查代碼與內存區域 源代碼 / Map 文件 / RAM 邊界 |
圖 1:HardFault 定位需要同時保留“哪條指令”和“訪問了哪里”
下面這段代碼適用于 GCC / Clang 工具鏈。它先根據異常返回值判斷使用 MSP 還是 PSP,再把壓棧現場和故障寄存器保存到全局變量。進入斷點后,直接在調試器的 Expressions 窗口查看 g_fault。
typedef struct
{
uint32_t r0, r1, r2, r3, r12;
uint32_t lr, pc, xpsr;
uint32_t cfsr, hfsr, mmfar, bfar;
} fault_snapshot_t;
volatile fault_snapshot_t g_fault;
__attribute__((naked)) void HardFault_Handler(void)
{
__asm volatile(
"tst lr, #4 \n"
"ite eq \n"
"mrseq r0, msp \n"
"mrsne r0, psp \n"
"b hardfault_c \n");
}
void hardfault_c(uint32_t *sp)
{
g_fault.r0 = sp[0];
g_fault.r1 = sp[1];
g_fault.r2 = sp[2];
g_fault.r3 = sp[3];
g_fault.r12 = sp[4];
g_fault.lr = sp[5];
g_fault.pc = sp[6];
g_fault.xpsr = sp[7];
g_fault.cfsr = SCB->CFSR;
g_fault.hfsr = SCB->HFSR;
g_fault.mmfar = SCB->MMFAR;
g_fault.bfar = SCB->BFAR;
__BKPT(0);
while (1) {}
}
CFSR 是 MemManage、BusFault 和 UsageFault 三組狀態位的組合。不要只看十六進制值,把置位項逐個解碼,才能判斷下一步應該查地址、查棧還是查編譯選項。
例如,調試器里看到 PRECISERR=1、BFARVALID=1,同時 BFAR=0x24080020。如果目標芯片的 AXI SRAM 結束于 0x2407FFFF,這個地址已經越界 32 Bytes。此時繼續改模型結構沒有意義,應檢查是誰把寫指針推進了 RAM 末端。
隨后用 g_fault.pc 回到源代碼。STM32CubeIDE 可以在 Disassembly 或 Memory 視圖跳轉到該地址;也可以用 ELF 文件執行:
arm-none-eabi-addr2line -e Debug/project.elf -f -C 0x0801234A如果 PC 落在卷積內核或內存復制函數中,不代表庫本身有問題。底層算子通常只是第一個使用了壞指針的地方;真正把指針寫壞的操作,可能發生在更早的輸入填充、量化轉換或緩沖區初始化階段。
模型文件能放進 Flash,不等于推理時的 RAM 足夠。權重通常是只讀常量,主要占用 Flash;輸入、輸出、中間特征圖和算子臨時緩沖在運行時占用 RAM。TFLite Micro 把這些對象規劃進 Tensor Arena,STM32Cube.AI 則生成網絡激活緩沖區。兩者都可能比模型文件本身更接近 RAM 上限。
全局變量 | 中間張量 | 傳感器輸入 | 任務運行空間 |
圖 2:真正需要核對的是鏈接后的 RAM 布局,不是數據手冊首頁的 SRAM 總容量
在 STM32CubeIDE 的鏈接器選項中啟用 Map 輸出,重新構建后搜索以下內容:
tensor_arenaai_activations:確認起始地址、長度和所在 SRAM Bank。.bss.data:確認大型幀緩沖、音頻環形緩沖是否已經吃掉同一塊 RAM。_Min_Stack_Size激活區不要定義成推理函數里的局部大數組。局部數組進入當前線程棧,編譯能通過,運行時卻可能覆蓋任務控制塊或異常棧。建議使用靜態存儲,并按推理庫要求對齊:
#define ARENA_BYTES (96U * 1024U)
__attribute__((aligned(32)))
static uint8_t tensor_arena[ARENA_BYTES];
STM32H7、F7 一類具有多塊 SRAM 和 D-Cache 的器件還要檢查總線可達性。CPU 可以訪問某塊 RAM,不等于采集數據的 DMA 也可以訪問;具體 DMA、MDMA、DTCM、AXI SRAM 和 D2 SRAM 的連接關系隨型號變化,必須查對應參考手冊。DMA 把數據寫入可緩存 SRAM 后,緩存維護錯誤通常表現為“模型讀到舊數據、結果漂移”,不一定觸發 HardFault。不要把數據一致性問題和非法地址訪問混為一類。
修復后只跑通一次還不夠。數組越界可能沒有立即撞上非法地址,而是先改壞旁邊的數據,過幾十次推理才崩。給激活區前后加固定護欄,每次推理后檢查,可以把“偶發 HardFault”提前變成明確的越界報警。
#define GUARD_BYTES 32U
#define GUARD_VALUE 0xA5U
typedef struct
{
uint8_t guard_before[GUARD_BYTES];
uint8_t arena[ARENA_BYTES];
uint8_t guard_after[GUARD_BYTES];
} arena_guarded_t;
__attribute__((aligned(32)))
static arena_guarded_t g_ai_mem;
static bool guard_ok(const uint8_t *p)
{
for (uint32_t i = 0; i < GUARD_BYTES; ++i) {
if (p[i] != GUARD_VALUE) return false;
}
return true;
}
初始化時把兩側護欄填成 0xA5,把 g_ai_mem.arena 交給推理框架。每次推理后檢查兩側。如果后護欄先變化,優先檢查寫越界;如果護欄完好但 CFSR 報棧錯誤,轉查任務棧;如果地址與邊界都正常而輸出偶爾變化,再轉向 DMA 和 Cache 一致性。
| 最后記住這條順序 |
參考資料:Arm CMSIS-Core Register Mapping
Arm CMSIS-View Fault Storage
ST AN4839:STM32F7/H7 L1 Cache
TensorFlow:Optimizing TFLite Runtime Memory

END