
直接說結(jié)論這活兒我調(diào)了兩天最后發(fā)現(xiàn)不是芯片壞了不是代碼寫錯了是時鐘沒配好就急著訪問TCM導致的。最近項目里把主控換成了STM32N6本來想用DTCM做音頻緩沖結(jié)果一上電跑沒多久就進Hard Fault調(diào)試器一看PC指針直接飛到0xFFFFFFFE典型的取指異常。后來把整個啟動流程翻了個底朝天總算把問題根因挖出來了順手把TCM訪問的各種坑都踩了一遍寫出來給后面用這顆芯片的人避雷。先說下環(huán)境開發(fā)板是N6官方的NUCLEO-N6IDE用的STM32CubeIDE 1.16HAL庫版本是1.4.0RTOS用的ThreadX代碼里通過MPU配置了應(yīng)用安全區(qū)和應(yīng)用非安全區(qū)功能TCM這塊走的是DTCM地址0x20000000。這套組合是目前N6開發(fā)比較典型的搭配如果你是剛拿到N6的芯片手冊準備開搞這篇應(yīng)該能幫你省下不少時間。1. 問題現(xiàn)象Hard Fault來得毫無征兆現(xiàn)象是這樣的系統(tǒng)啟動后RTOS調(diào)度器正常運行幾個任務(wù)跑起來都正常但一旦某個任務(wù)里對DTCM地址進行寫操作比如往0x20000100寫一段音頻數(shù)據(jù)系統(tǒng)馬上跳進Hard Fault。更詭異的是有時候不是馬上掛而是寫了幾十次之后才掛看起來像是隨機性的。第一次遇到這種情況我第一反應(yīng)是懷疑MPU配置錯了因為N6這顆芯片的MPU配置比以前的M4/M7復(fù)雜不少而且我確實用了安全區(qū)/非安全區(qū)的功能。于是我把MPU的Region配置全部打出來對照手冊一個字段一個字段檢查結(jié)果沒發(fā)現(xiàn)問題。又懷疑是DMA沖突把DMA關(guān)掉測試問題依舊。后來實在沒辦法把Hard Fault的現(xiàn)場完整抓下來分析通過調(diào)試器讀取HFSR、CFSR、BFAR、MMFAR這幾個關(guān)鍵的Fault狀態(tài)寄存器發(fā)現(xiàn)CFSR里的IBUSERR位被置1這說明問題出在指令總線錯誤上也就是CPU去取指令的時候訪問了一個不允許訪問的地址。再一看BFAR指向的地址是0x20000000附近——這不就是DTCM的地址嗎CPU取指令跑到DTCM去了這本身就很有問題但更重要的是為什么取指令會訪問DTCM這里插一句Debug下連接ST-LINK的SWD接口其實能直接把N6的Fault狀態(tài)寄存器全讀出來這個信息非常關(guān)鍵。如果你連芯片的調(diào)試接口都不用光靠串口打印Fault信息那基本是在盲人摸象。后面我會詳細講怎么看這幾個寄存器。2. 問題定位從Fault狀態(tài)寄存器倒推根因2.1 先看懂Hard Fault的“案發(fā)現(xiàn)場”ARM Cortex-M系列的Fault機制是分級的Hard Fault是最高級別的錯誤但它只是個“匯總”真正的原因要看下面這幾個寄存器HFSRHardFault Status RegisterHard Fault的狀態(tài)看FORCED位是否置1如果置1說明是由其他Fault升級過來的。CFSRConfigurable Fault Status Register這個寄存器其實是三個寄存器的合體包括MMFSRMemManage Fault Status Register、BFSRBus Fault Status Register、UFSRUsage Fault Status Register分別對應(yīng)內(nèi)存管理錯誤、總線錯誤、用法錯誤。BFARBus Fault Address Register記錄總線錯誤發(fā)生的地址。MMFARMemManage Fault Address Register記錄內(nèi)存管理錯誤發(fā)生的地址。調(diào)試器連接后我在Fault處理函數(shù)里加了斷點程序一進Hard Fault就停下來然后手工讀這幾個寄存器。讀出來的結(jié)果是這樣的HFSR 0x40000000 // FORCED位置1說明有次級Fault CFSR 0x00008200 // IBUSERR位置1指令總線錯誤 BFAR 0x20000000 // 出錯地址指向DTCM看到這個結(jié)果思路就清晰了CPU在訪問DTCM地址的時候指令總線層面報錯了。換句話說CPU試圖從DTCM取指令但芯片不允許因為DTCM這個區(qū)域根本沒有被配置成可執(zhí)行屬性。但這里有個問題代碼里并沒有任何函數(shù)指針指向DTCM為什么CPU會去DTCM取指令這就得回到ARM的異常處理機制和N6的啟動流程上找答案了。2.2 疑點追蹤為什么CPU會去DTCM取指令我先懷疑的是中斷向量表出了問題。因為在Cortex-M系列里中斷向量表是放在Flash起始地址的中斷發(fā)生時會從向量表取中斷服務(wù)函數(shù)的地址。如果向量表被錯誤地修改或者啟動代碼里對VTOR向量表偏移寄存器的設(shè)置不對CPU就可能跑到奇怪的地方去取指令。但是我又想向量表在Flash里不在TCM里這個路徑解釋不通。再往下追我注意到一個細節(jié)N6這顆芯片用的是雙核架構(gòu)一個Cortex-M55主核加一個神經(jīng)處理單元NPU而且M55支持TrustZone所以有安全狀態(tài)和非安全狀態(tài)之分。在TrustZone架構(gòu)下中斷控制器NVIC的處理方式還帶上了安全屬性中斷向量表也可以配置成從非安全區(qū)取向量或者從安全區(qū)取向量。問題恰恰出在這里。我啟用了應(yīng)用安全區(qū)和應(yīng)用非安全區(qū)功能但中斷向量表的配置沒有跟上。具體來說中斷發(fā)生時NVIC根據(jù)當前的執(zhí)行狀態(tài)要去對應(yīng)的向量表取中斷服務(wù)函數(shù)的地址。如果這個向量表地址配置不對或者對應(yīng)區(qū)域的執(zhí)行權(quán)限沒有設(shè)置好CPU取指令的時候就會觸發(fā)總線錯誤進而升級為Hard Fault。這就能解釋為什么問題看起來是“隨機”的因為只有在中斷觸發(fā)的時候才會走到這一步而我的測試代碼里SysTick中斷是最頻繁的所以看起來像是跑一段時間就掛實際上是每次SysTick觸發(fā)都有概率出錯只不過有時候碰巧取到的地址還能執(zhí)行有時候就直接掛了。3. 核心原因分析N6時鐘配置與TCM訪問權(quán)限的聯(lián)動關(guān)系3.1 時鐘配置是這一切的起點先說結(jié)論STM32N6這顆芯片和以前的STM32系列最大的不同是它的TCM接口并不是直接掛在系統(tǒng)總線上的而是通過AXI總線矩陣和時鐘域交叉連接。在默認狀態(tài)下TCM區(qū)域的時鐘可能沒有完全使能或者時鐘頻率配置不對導致CPU訪問TCM時總線矩陣返回了一個錯誤響應(yīng)。我當時用的時鐘配置是外部25MHz晶振PLL倍頻到800MHz主頻這是N6的正常工作頻率。但我漏了一個細節(jié)N6的DTCM和ITCM在默認狀態(tài)下是由一個獨立的時鐘域控制的叫TCM時鐘域。這個時鐘域需要你在時鐘樹里單獨使能并且要和CPU主頻配置成同步關(guān)系。如果TCM時鐘域沒有使能或者使能了但頻率配置不對那么CPU一旦去訪問TCM地址總線矩陣因為時鐘域不一致會返回一個錯誤響應(yīng)。這個錯誤響應(yīng)在指令總線上就表現(xiàn)為IBUSERR最終升級成Hard Fault。我后來在STM32CubeMX里仔細檢查時鐘樹發(fā)現(xiàn)TCM時鐘默認是關(guān)閉的。CubeMX里有個選項叫“Enable TCM Clock”默認是灰色的需要先在Advanced Settings里勾選然后重新生成代碼。勾選之后系統(tǒng)會生成一段初始化代碼專門用來使能TCM時鐘域。我把這段代碼加上之后Hard Fault問題就消失了。3.2 再深挖一層MPU配置與執(zhí)行權(quán)限時鐘問題解決了之后我又深入測了一輪發(fā)現(xiàn)還有一個隱藏的問題如果把DTCM配置成可執(zhí)行的倒是不會再觸發(fā)IBUSERR但這樣會引入安全隱患因為攻擊者可以利用緩沖區(qū)溢出把惡意代碼寫到DTCM然后跳過去執(zhí)行繞過Flash區(qū)的執(zhí)行權(quán)限保護。這是我在配置MPU時特別注意的。N6的MPU配置里每個Region都有Execute NeverXN位如果你把DTCM所在的Region配置成XN那么即使DTCM地址被訪問了只要是指令取指就會被拒絕。這樣做的代價是如果你的確需要從DTCM執(zhí)行代碼那就得把這個Region的XN位清掉。但一般情況下TCM是用來放數(shù)據(jù)的不應(yīng)該設(shè)成可執(zhí)行所以XN位必須置1。我遇到的情況是我把MPU的Region配置寫好了XN位也置1了但仍然出現(xiàn)Hard Fault。后來排查發(fā)現(xiàn)是Region的優(yōu)先級配置問題。N6的MPU支持16個Region每個Region有一個優(yōu)先級號優(yōu)先級號小的Region優(yōu)先級更高。我把DTCM的Region優(yōu)先級配置成了8但另一個無關(guān)的Region優(yōu)先級是8兩個Region優(yōu)先級相同覆蓋范圍又有重疊結(jié)果MPU的行為就變得不確定了。ARM的MPU設(shè)計里如果兩個Region的優(yōu)先級相同且地址范圍重疊實際生效的是編號較大的Region還是編號較小的Region這在不同的芯片上表現(xiàn)可能不一樣。N6的參考手冊里明確寫了默認情況下編號較大的Region優(yōu)先級更高或者相反取決于具體實現(xiàn)我在配置的時候沒有留意這個細節(jié)導致DTCM的MPU配置被另一個Region覆蓋了XN位失效CPU就能去TCM取指了。所以我在MPU配置上踩的坑其實是兩個一個是時鐘沒使能另一個是MPU Region優(yōu)先級重疊導致XN位失效。這兩個問題疊加在一起構(gòu)成了Hard Fault的完整根因。4. 完整解決方法與實操步驟4.1 第一步確認TCM時鐘已使能在STM32CubeMX里打開Clock Configuration找到TCM相關(guān)的時鐘選項。如果你的芯片封裝和CubeMX版本不同這個選項的位置可能略有差異但關(guān)鍵詞是“TCM”或“DTCM/ITCM”。把TCM時鐘使能并把分頻系數(shù)設(shè)置成和CPU主頻同步。生成代碼之后在SystemClock_Config函數(shù)里你會看到類似這樣的初始化代碼static void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; __HAL_RCC_TCM_CLK_ENABLE(); // 使能TCM時鐘 RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; ... }關(guān)鍵就是這行__HAL_RCC_TCM_CLK_ENABLE()沒有它TCM就是一坨不可訪問的地址。如果你是手動寫寄存器對應(yīng)的操作是操作RCC的時鐘使能寄存器具體位要看參考手冊的RCC章節(jié)。這里有個操作心得要分享不要完全信任CubeMX的圖形界面生成代碼之后一定要搜一下整個工程里有沒有TCM_CLK_ENABLE或類似的關(guān)鍵字確認它真的在SystemClock_Config里被調(diào)用了。我遇到過一次CubeMX里勾選上了但生成的代碼因為某種原因沒包含這行初始化導致問題反復(fù)出現(xiàn)。4.2 第二步修正MPU配置避免Region優(yōu)先級沖突MPU配置這塊我建議直接在代碼里手動寫不要完全依賴CubeMX的MPU配置界面因為N6的MPU支持TrustZone界面配置的靈活度有限而且很容易出現(xiàn)我遇到的Region優(yōu)先級問題。核心配置邏輯如下void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct {0}; HAL_MPU_Disable(); // 配置DTCM區(qū)域地址0x20000000大小512KB具體大小看芯片型號 MPU_InitStruct.Enable MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress 0x20000000; MPU_InitStruct.Size MPU_REGION_SIZE_512KB; MPU_InitStruct.AccessPermission MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable MPU_REGION_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable MPU_REGION_CACHEABLE; MPU_InitStruct.IsShareable MPU_REGION_NOT_SHAREABLE; MPU_InitStruct.Number MPU_REGION_NUMBER0; MPU_InitStruct.TypeExtField MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable 0x00; MPU_InitStruct.DisableExec MPU_INSTRUCTION_ACCESS_DISABLE; // XN位置1 MPU_InitStruct.IsSecure MPU_REGION_SECURE; // 安全區(qū)屬性 HAL_MPU_ConfigRegion(MPU_InitStruct); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); }重點看兩個參數(shù)MPU_REGION_NUMBER0和MPU_INSTRUCTION_ACCESS_DISABLE。MPU_REGION_NUMBER0意味著這個Region的優(yōu)先級最高不會再被其他Region覆蓋MPU_INSTRUCTION_ACCESS_DISABLE就是XN位置1禁止從DTCM取指令。至于其他Region比如Flash、SRAM、外設(shè)區(qū)域建議按實際需求配置但優(yōu)先級要錯開不要出現(xiàn)兩個Region優(yōu)先級相同且地址范圍重疊的情況。這個坑特別隱蔽因為MPU的查找邏輯是逐項匹配的優(yōu)先級相同的情況下最終生效哪個Region和芯片內(nèi)部的硬件實現(xiàn)有關(guān)不同型號可能不一樣最穩(wěn)妥的方法就是不要制造這種歧義。4.3 第三步確認中斷向量表的配置在啟用了TrustZone安全/非安全功能之后N6的中斷向量表有兩種處理方式一種是使用默認的向量表放在Flash起始地址SCB-VTOR保持默認值。另一種是使用非安全向量表通過SCB-VTOR設(shè)置到非安全區(qū)域。我的需求是讓所有的中斷都在非安全狀態(tài)下處理所以我把非安全中斷向量表放在了一個固定的RAM地址并在安全狀態(tài)下完成了初始化然后把控制權(quán)交給非安全狀態(tài)。這一點如果你不用TrustZone可能根本遇不到但只要啟用了就一定要在啟動代碼里檢查。N6的參考手冊里有個專門的章節(jié)講“Exception entry behavior in Security state”簡單說就是如果當前是安全狀態(tài)中斷向量從安全向量表取如果是非安全狀態(tài)中斷向量從非安全向量表取。這兩個向量表分別有獨立的VTOR設(shè)置SCB-VTOR和NS_Disaster-VTOR其中NS_Disaster是非安全別名寄存器區(qū)域。我最終的做法是保持安全向量表在Flash起始地址同時把非安全向量表也復(fù)制到了非安全RAM區(qū)域并設(shè)置了對應(yīng)的VTOR。因為我的RTOS和所有應(yīng)用任務(wù)都在非安全區(qū)運行這樣每個中斷都能正確地從非安全向量表里取到服務(wù)函數(shù)地址。這里有個提醒復(fù)制中斷向量表不是簡單地把Flash里的數(shù)組拷到RAM里就行因為N6的向量表里不僅有函數(shù)地址還包含了一些啟動配置比如堆棧指針的初始值。如果你在運行中重新設(shè)置VTOR一定要確保新的向量表里第一項仍然是正確的堆棧指針值否則第一次壓棧就會把棧指針搞壞那又是另一個Hard Fault。4.4 第四步利用調(diào)試器驗證修復(fù)效果修復(fù)完成后不要急著跑應(yīng)用邏輯先做一輪針對性的驗證。我的驗證方法是在Hard Fault處理函數(shù)里保留調(diào)試斷點如果再有Fault發(fā)生能讓調(diào)試器停下來。寫一個簡單的壓力測試任務(wù)死循環(huán)往DTCM地址寫數(shù)據(jù)同時在另一個任務(wù)里頻繁觸發(fā)SysTick和外部中斷。跑24小時觀察是否有Fault發(fā)生。如果一切正常再用調(diào)試器讀一次HFSR和CFSR寄存器確認它們是0說明Fault狀態(tài)已經(jīng)被清除了。另外N6的調(diào)試器支持cortex_m_debug插件如果你用OpenOCD的話可以在GDB里直接查看TCM區(qū)域的內(nèi)容這對我確認DTCM的數(shù)據(jù)寫入是否真正生效很有幫助。我在壓力測試時確實通過GDB看到了DTCM地址上的數(shù)據(jù)在持續(xù)變化說明TCM訪問恢復(fù)正常了。5. 常見問題與排查技巧實錄5.1 Hard Fault的相關(guān)狀態(tài)寄存器速查表很多新手遇到Hard Fault第一反應(yīng)是加打印信息但Fault發(fā)生的時候CPU已經(jīng)異常了打印信息往往不可靠。正確的做法是用調(diào)試器讀取狀態(tài)寄存器。我把常用的寄存器整理成了一張速查表方便大家對照寄存器關(guān)鍵位含義排查方向HFSRFORCED (bit30)是否有其他Fault升級為Hard Fault如果置1去看CFSRCFSRIBUSERR (bit8)指令總線錯誤檢查取指地址是否可執(zhí)行CFSRPRECISERR (bit1)精確數(shù)據(jù)總線錯誤檢查數(shù)據(jù)訪問地址是否合法CFSRIACCVIOL (bit0)指令訪問違例檢查MPU執(zhí)行權(quán)限配置BFAR-總線錯誤發(fā)生時的地址看這個地址屬于哪個區(qū)域MMFAR-內(nèi)存管理錯誤發(fā)生時的地址看這個地址是否在MPU允許范圍內(nèi)這個表看起來簡單但實際排查的時候非常有用。我建議你Fault處理函數(shù)的第一行就寫一個死循環(huán)等待調(diào)試器不要急著恢復(fù)執(zhí)行否則現(xiàn)場很容易被后續(xù)的中斷覆蓋。5.2 我踩過的三個典型坑第一個坑是CubeMX生成代碼不完整。N6在CubeMX里的支持還比較新某些配置選項勾選后不會立即在代碼里體現(xiàn)尤其是TCM時鐘這種“高級選項”。解決方法就是我在前面說的生成代碼后全工程搜索關(guān)鍵寄存器操作確認真的有初始化代碼。第二個坑是MPU Region優(yōu)先級沖突。這個坑最隱蔽因為即便你配置錯了系統(tǒng)大多數(shù)時候還是正常的只有在特定條件下才出問題表現(xiàn)出來就是“偶發(fā)性Hard Fault”。我建議你在啟用MPU后把所有Region的配置打印出來對照檢查優(yōu)先級和地址范圍確保沒有重疊且優(yōu)先級不沖突。第三個坑是結(jié)構(gòu)體對齊。這個坑和TCM沒有直接關(guān)系但我在做音頻數(shù)據(jù)處理時遇到了一并分享給大家。如果你在TCM里定義一個結(jié)構(gòu)體數(shù)組然后又通過指針強制轉(zhuǎn)換去訪問很容易觸發(fā)對齊錯誤。在Cortex-M55這種支持非對齊訪問的核上對齊問題不一定觸發(fā)Fault但會降低訪問效率有時候還會因為編譯器的優(yōu)化行為導致意外。解決方法是聲明結(jié)構(gòu)體時加上__attribute__((aligned(4)))或__attribute__((packed))根據(jù)你的實際需求選擇。5.3 給新手的調(diào)試建議如果你剛接觸STM32N6我建議你先不要直接上完整系統(tǒng)而是按下面這個順序來先跑一個裸機點燈程序確認芯片和調(diào)試環(huán)境正常。再跑一個GPIO中斷程序確認中斷向量表正常。然后加上MPU配置確認非安全區(qū)/安全區(qū)的功能正常。最后再上RTOS和應(yīng)用邏輯。每一步都用調(diào)試器驗證不要跳躍。因為N6的復(fù)雜度和F103完全不是一個量級跳過中間步驟直接上大系統(tǒng)出了問題還真不好定位。我調(diào)TCM這個問題時就是因為直接跳到了最后一步導致很難判斷是時鐘問題、MPU問題、還是中斷向量表問題走了不少彎路。另一個建議是把所有Fault相關(guān)的中斷都指向同一個函數(shù)在這個函數(shù)里讀寄存器并保存到全局變量這樣即使不上調(diào)試器你也可以通過串口把寄存器值發(fā)給上位機分析。雖然調(diào)試器更方便但不排除有些場景就是連不上調(diào)試器這時候這個備用方案就很重要了。我當時是在串口中斷里加了個簡單的輸出把HFSR、CFSR、BFAR的值發(fā)出來才真正確認了問題指向。6. 后續(xù)還能怎么擴展這個問題解決之后我又在N6上試了一些別的TCM用法順便分享一下。N6的ITCM和DTCM都是緊耦合內(nèi)存訪問延遲極低放關(guān)鍵代碼和熱數(shù)據(jù)非常合適。但如果你的代碼量比較大ITCM空間不夠可以考慮只把最頻繁調(diào)用的那幾個中斷服務(wù)函數(shù)放到ITCM里其他代碼留在Flash中運行。這樣既能享受ITCM的低延遲又不會因為空間不足而遷就。放代碼到ITCM的操作很簡單在GCC環(huán)境下用__attribute__((section(.itcm)))修飾即可。但要注意鏈接腳本里必須有對應(yīng)的段定義否則鏈接會報錯。我是直接在鏈接腳本里新增了一個.itcm段然后把中斷服務(wù)函數(shù)放進去實測中斷響應(yīng)時間確實短了一些但幅度沒有想象中大因為這顆芯片的Flash加速器做得很不錯除非你的中斷頻率特別高并且對延遲敏感否則沒有必要大動干戈。如果你是在做音頻信號處理、圖像處理這些數(shù)據(jù)密集型應(yīng)用把緩沖區(qū)放到DTCM是非常合理的用法因為它的功耗比訪問外部SDRAM低延遲也更穩(wěn)定。但一定要記住做完MPU配置后要確認緩沖區(qū)所在的地址空間沒有被錯誤地設(shè)置成Cacheable且不可緩沖否則DMA和CPU之間的數(shù)據(jù)一致性可能會出問題。我當時還試過把RTOS的任務(wù)棧放到DTCM效果還不錯。ThreadX在N6上跑的時候任務(wù)切換的上下文保存和恢復(fù)會頻繁訪問棧放在DTCM里能明顯減少總線競爭。不過N6的DTCM容量有限如果任務(wù)數(shù)量多棧就分不過來這需要你根據(jù)實際工程做取舍。我當時的做法是給高優(yōu)先級任務(wù)分配DTCM棧普通任務(wù)繼續(xù)用SRAM這樣資源和性能都能兼顧。最后再說一個細節(jié)N6的TCM訪問問題和D-Cache、I-Cache的狀態(tài)也有關(guān)系。如果你在啟動階段使能了Cache然后又對TCM地址做了一些特殊操作可能因為Cache預(yù)取導致意外的總線訪問。我在調(diào)試過程中曾經(jīng)瞬間懷疑過自己的Cache配置后來核對下來發(fā)現(xiàn)不是Cache的問題。但如果你也遇到類似情況建議先關(guān)Cache跑一遍復(fù)現(xiàn)測試再開Cache對比這樣能快速排除一個干擾項。問題定位和解決的經(jīng)驗就是這些了。TCM這個坑說大不大但確實隱蔽如果對N6的時鐘域和MPU配置邏輯不熟很容易繞圈子。希望這篇能幫你少踩幾個坑。