最近變得好懶散,什麼也不想做,回家也是對着電腦發呆,我好像失去了什麼,但是我好像本來什麼也沒有啊。沒有擁有,又何談失去呢,看來是庸人自擾,也許發呆只是潛意識裏逃避進取的藉口罷了。非要說失去了什麼,可能就是光陰了,不管你是在發呆還是在拼命,時間從來不會停下前進的步伐。以此說來,在過去的歲月裏,不管你做了什麼,失去了什麼,獲得了什麼,都應無怨無悔,這本來就是你自己的選擇。發呆久了,人也變得遲鈍,敲幾行字清醒一下,不能秀逗了嘛。畢竟當你什麼也沒有的時候,還要在程序裏討得一碗殘羹冷炙,飢寒交迫中求得一線希望,黑暗中等待黎明。
在項目中遇到MCU出現HardFault錯誤,查了好久,結合網上找到的零散的解決問題的方法,不斷驗證後解決了問題。在這裏寫成博客,當作學習筆記,也給遇到類似問題的程序猿提供解決問題的思路。
一、問題描述
項目中使用LPC1857單片機,這是一款NXP出的Cortex-M3內核MCU,我們做了一個bootload代碼,用於IAP升級。在bootloader代碼中我們使用了官方移植好的ucosIII系統,當檢測到APP應用程序格式正確時,執行跳轉功能,直接從IAP代碼跳到APP代碼中運行,但是跳轉後在運行APP代碼時MCU出現HardFault錯誤。
二、解決思路
1、HardFault錯誤原因
既然MCU出現了HardFault錯誤,我們首先要搞清楚什麼原因引起的。在《CORTEX-M3權威指南》裏,對引起HardFault錯誤的可能原因進行了詳細的描述,這裏我們也轉述相關基礎知識點,便於大家理解。
Cortex-M3內核提供四種異常:總線fault、存儲器管理fault、用法異常和硬fault。當前三種異常沒有使能時,它們的異常狀態會上報到硬fault,所以引起HardFault錯誤的原因有以上四種情況。每種異常中又有多個標誌位用於指示引起異常的具體原因,下面對每個標誌位做簡單的介紹。
(1)總線Fault:在取址、數據讀/寫、取中斷向量、進入/退出中斷時寄存器堆棧操作(入棧/出棧)時檢測到內存訪問錯誤。
位 |
可能的原因 |
STKERR |
(自動)入棧期間出錯 1. 堆棧指針的值被破壞 2. 堆棧用量太大,到達了未定義存儲器的區域 3. PSP未經初始化就使用 |
UNSTKERR |
(自動)出棧期間出錯。如果沒有發生過 STKERR,則最可能的就是在異常 處理期間把 SP的值破壞了 |
IMPRECISERR |
與設備之間傳送數據的過程中發生總線錯誤。可能是因爲設備未經初始化而 引起;或者在用戶級訪問了特權級的設備,或者傳送的數據單位尺寸不能爲 設備所接受。此時,有可能是 LDM/STM指令造成了非精確總線 fault。 |
PRECISERR |
在數據訪問期間的總線錯誤。通過 BFAR 可以獲取具體的地址。發生 fault 的原因同上。 |
IBUSERR |
同 MemManage fault中的 IACCVIOL |
位 |
可能的原因 |
MSTKERR |
入棧時發生錯誤(異常響應序列開始時) 1)堆棧指針的值被破壞 2)堆棧容易過大,已經超出 MPU允許的 region範圍 |
MUNSTKERR |
出棧時發生錯誤(異常響應序列終止時)。入棧時沒有發生錯誤,出棧時卻出 錯,總令人有些匪夷所思,可能的原因是 1. 異常服務例程破壞了堆棧指針 2. 異常服務例程更改了 MPU配置 |
DACCVIOL |
內存訪問保護違例。這是 MPU發揮作用的體現。常常是用戶應用程序企圖訪 問特權級 region所致 |
IACCVIOL |
1. 內存訪問保護違例。常常是用戶應用程序企圖訪問特權級 region。在這種情 況下,入棧的 PC給出的地址,就是產生問題的代碼之所在 2. 跳轉到不可執行指令的 regions 3. 異常返回時,使用了無效的 EXC_RETURN值 4. 向量表中有無效的向量。例如,異常在向量建立之前就發生了,或者加載的 是用於傳統 ARM內核的可執行映像 5. 在異常處理期間,入棧的 PC值被破壞了 |
位 |
可能的原因 |
DIVBYZERO |
當 DIV_0_TRP置位時則發生了除數爲零的情況。引發此 fault的指令可以從入棧 的 PC讀取 |
UNALIGNED |
當 UNALIGN_TRP 置位時發生未對齊訪問。引發此 fault 的指令可以從入棧的 PC讀取 |
|
|
NOCP |
企圖執行一個協處理器指令。引發此 fault的指令可以從入棧的 PC讀取 |
INVPC |
1. 異常返回時使用了無效的 EXC_RETURN,例如 1) 當 EXC_RETURN=0xFFFF_FFF1時卻要返回線程模式 2) 當 EXC_RETURN=0xFFFF_FFF9時卻要返回 handler模式 2. 無效的異常活動狀態,例如 1) 當前異常的活動狀態已經清除了,卻在此時執行異常返回。往往是因爲濫 用 VECTCLRACTIVE或清除了 SHCSR中活動狀態所致 2) 在尚有異常的活動位置位時,卻要返回線程模式 3. 由於堆棧指針錯誤導致了 IPSR 的值不正確。對於 INVPC fault,入棧的 PC 指出了該 fault服務例程在何處搶佔了其它代碼。這個問題往往是由比較隱晦 的程序錯誤造成的,欲詳細調查該問題的原因,最好使用 ITM的跟蹤功能。 4. ICI/IT位對當前指令無效。當 LDM/STM指令被異常打斷後,在異常服務例程 中又更改了入棧的 PC。結果在中斷返回時,非零的 ICI 位段作用到了不使用 ICI位段的指令上。如果是其它原因破壞了 PSR的值,也可能導致此 fault。 |
INVSTATE |
1. 加載到 PC 中的跳轉地址值是偶數(LSB=0)。通過檢查入棧 PC 的值,一下子 就可以查出該問題。 2. 向量地址的 LSB=0,診斷方法同上。 3. 入棧的 PSR在異常處理過程中被破壞,使得在返回時內核嘗試進入 ARM狀態。 |
UNDEFINSTR |
1. 使用了 CM3不支持的指令 2. 代碼段中的數據被破壞 3. 連接時加載了 ARM目標碼。請檢查編譯階段的設置 4. 指令對齊的問題。例如,在使用GNU工具鏈時,忘記了在.ascii後使用.align, 就有可能導致下一條指令沒有對齊 |
位 |
可能的原因 |
DEBUGEVF |
因調試事件導致的 fault 1. 斷點/觀察點事件 2. 在硬 fault服務例程的執行過程中,沒有使能監視器異常(MON_EN=0)也 沒有使能停機調試(C_DEBUGEN=0),卻執行了 BKPT指令。缺省時,有些 C編譯器可能會在半主機代碼中使用 BKPT指令。 |
FORCED |
這是 fault“上訪”的情況 1. 試圖在 SVC/監視器服務例程中執行 SVC/BKPT,或者在其它擁有相同或更 高優先級的服務例程中執行 SVC/BKPT。 2. 發生了 fault,但是它的服務例程被除能 3. 發生了 fault,但是當前處理器在響應同級或更高優先級的異常 4. 發生了 fault,但是它被掩蔽了 |
VECTBL |
取向量失敗, 1. 在取向量過程中發生總線 fault 2. 向量表偏移量設置有誤 |
(1)在MDK中使用JTAG在線調試,出現HardFault異常後,可以查看引起錯誤的原因,定位是哪種錯誤引起的單片機異常,具體方法如下圖所示:
在該項目中,我們查看的結果是總線fault中的STKERR引起的,也就是棧指針溢出引起的,至於程序中哪段代碼引起的,要通過下面的方法定位。
(2)定位出錯的代碼行
在線調試模式下,查看左側寄存器欄中Banked確定現在使用的是哪個堆棧,MSP或者是PSP,確定以後,在內存查看窗口,輸入堆棧的地址,以這個地址開始的8個32位數值,依次是R0,R1,R2,R3,R12,R14,R15,XPSR的數值,據此判定你的堆棧地址是不是對的(有時需要考慮堆棧的增長方向)。R14,R15的地址就是我們出錯的代碼所在的地址,需要在這個地址基礎上,首先偶數對齊,然後向上減去8個字節。需要考慮的是,在使用MSP的時候,有出錯的地方並不一定在R14,R15處,而是在XPSR往後的第二個地址處,在這個附近查找,排除故障。
該項目中,最後定位出錯的代碼行是(MOVS R0,#0;MSR PSP,R0;),這兩句彙編語句是ucosiii啓動任務時調用的,該代碼在os_cpu_a.s文件中,意思使MCU的從棧指針PSP指向地址0。
3、ucosIII對棧指針MSP和PSP的使用
既然是ucosIII中使用PSP棧指針引起的HardFault,我們就要搞清楚ucosIII中對PSP的使用方法。這個要查看ucosIII的啓動代碼了,由於啓動代碼是用匯編寫的,逐行逐行的分析太累也太難理解,有興趣的可以自己網上找找資料。如果你想在底層開發有所突破,還是要認真看看這段彙編代碼的,大家都不會的你會了,你就是專家。這裏我們只看重點的地方。
啓動文件是os_cpu_a.s,ucosIII啓動任務時,使用了(MOVS R0,#0;MSR PSP,R0;)使PSP指向地址0,目的是把PSP作爲首次進行任務切換的標誌位,如果PSP等於0表示第一次進行任務切換,這時不需要把R4-R11壓棧,因爲在這之前還沒有任務運行。如果不爲0,就要先執行R4-R11壓棧操作。除了這裏,還有一段代碼很重要(MSR PSP, R0;ORR LR, LR, #0x04),這兩句彙編,前一句使PSP指向當前任務的棧,後一句的意思是在PENDSV中斷中執行完任務切換後,恢復使用PSP作爲普通任務的棧指針,因爲CORTEX-M3在中斷中使用的是MSP棧指針。對於MSP和PSP的用法,請自己查找資料學習,這裏不再介紹。
4、產生HardFault的原因
從上面的分析,我們可以知道ucosIII啓動時先把PSP棧指針指向地址0,在PendSV中斷中使用MSP棧指針執行完任務切換後,恢復使用PSP棧指針作爲普通任務的棧指針。在bootloader代碼中ucosIII啓動前,MCU默認使用MSP棧指針作爲任務棧指針,這時使PSP指向地址0沒有任何問題,因爲MCU這時根本沒有使用PSP。ucosIII啓動後,任務的棧指針就被ucosIII切換成了PSP,這時我們在普通任務裏跳轉到APP代碼運行,那麼APP代碼運行時使用的棧指針還是PSP。APP代碼中也要運行ucosIII的啓動代碼,這樣PSP正在被MCU調用的時候突然被ucosIII的啓動代碼把值修改成了0,由於地址0超出了MCU的內存地址空間,所以MCU內核就會報STKERR異常,即棧指針溢出,進而產生HardFault異常。
5、解決方案
bootloader代碼中,在執行跳轉到APP之前,先把棧指針修改成MSP,這樣跳轉到APP之後,MCU使用的是MSP棧指針,那麼ucosIII就可以隨意修改PSP的值了。把MCU使用的棧指針PSP修改成MSP的代碼是__set_CONTROL(0)。