應對STM32 Cortex-M3 Hard Fault異常


STM32 Cortex-M3 Hard Fault

Hard fault (硬錯誤,也有譯為硬件錯誤的)是在STM32(如無特別說明,這里的STM32指的是Cortex-M3的核)上編寫程序中所產生的錯誤,造成Hard Fault錯誤的原因也是最為紛繁復雜的。由於能導致該錯誤的原因很多,所以一但出現,比較難找到其原因。網上有很多類似的這種方法,現在我將其稍加整理,並結合我曾經遇到過的問題,詳細說明。

硬fault 是總線fault、存儲器管理fault 以及用法fault 上訪的結果。如果這些fault 的服務例程無法執行,它們就會成為“硬傷”——上訪(escalation)成硬fault。另外,在取向量(異常處理是對異常向量表的讀取)時產生的總線fault,也按硬fault 處理。在NVIC 中有一個硬fault 狀態寄存器(HFSR),它指出產生硬fault 的原因。如果不是由於取向量造成的,則硬fault 服務例程必須檢查其它的fault 狀態寄存器,以最終決定是誰上訪的。

1 寄存器描述

首先查看硬故障寄存器,判別原因。

 

 

對於調試故障,有個調試故障寄存器,在0xE000ED30處,有詳細介紹,不做探討;

對於取中斷發生的,有兩類原因,一是在取向量過程中發生總線 fault,二是向量表偏移量設置有誤。

本文重點介紹位30所示的,上訪類錯誤。

這樣Fault類異常有了三類,用法錯誤,存儲管理錯誤,總線錯誤。

 

對於這些寄存器詳盡的描述,見權威指南。

2 確定發生錯誤的地方  

2.1 查找出錯原因

Cortex-M3有雙堆棧功能,在帶有操作系統時,一般都會使用。在Keil軟件使用JTAG調試為例,系統的啟動文件中,將斷點打在下面4個地方。

HardFaultException

        B       HardFaultException

MemManageException

        B       MemManageException

BusFaultException

        B       BusFaultException

UsageFaultException

        B       UsageFaultException

程序跑飛以后,就會停在上面的4個斷點的一個地方。可以通過兩種方式查找原因。

第一種,在KEIL軟件下,利用軟件提供的功能查找故障原因。

 

在點出的窗口中,可以大體確定是哪個寄存器、什么原因造成了Hard Fault。

第二種,通過在內存觀察窗口,直接輸入上面那些寄存器的值來確定,通過觀看寄存器那個位被置1了,確定出錯原因。

2.2 確定出錯地方

然后查看左側寄存器欄中Banked確定現在使用的是那個堆棧,MSP或者是PSP,確定以后,在內存查看窗口,輸入堆棧的地址,以這個地址開始的8個32位數值,應該依次是R0,R1,R2,R3,R12,R14,R15,XPSR的數值,據此判定你的堆棧地址是不是對的(有時需要考慮堆棧的增長方向)。R14,R15的地址就是我們出錯的代碼所在的地址,需要在這個地址基礎上,首先偶數對齊,然后向上減去8個字節。

需要考慮的是,在使用MSP的時候,有出錯的地方並不一定在R14,R15處,而是在XPSR往后的第二個地址處,在這個附近查找,排除故障。

3 兩個例子

       下面就我之前碰到過的,舉例說明,這兩個例子分析出結果后,會覺得很簡單,但是查找原因的過程有點費勁。

3.1 memcpy內存拷貝函數引發

       總線故障寄存器中IMPERCISERR位,標示不精確的數據總線訪問錯誤,權威指南中對此有詳盡的說明,“或者傳送的數據單位尺寸不能為設備所接受,此時有可能是LDM/STM指令造成的”。

       Memcpy函數的原因是這樣的void *memcpy(void *dest, const void *src, size_t n),其中src是源地址,dest是目的地址,n是要拷貝的字節長度。KEIL自帶的函數中並不檢查這三個參數是否有效,我所開發的程序中,源地址和目的地址都在外存(外部擴展的內存,本次大小是4M)中,假設size的大小是0xFFFF FFFF,這樣的數值非常的大,單純的拷貝都需要10多秒。程序中定義了很多的變量都在外存,這個拷貝函數所在的任務優先級比較低,可能被中斷或者其它的任務打斷。

       我調試程序的時候,首先是發生在了中斷的地方,外存數組地址到了0x21FF 2200,原來定義在6802 1000,加起來立刻超出了外存大小。修改中斷,最終確定是傳入的參數n太大了,直接是0xFFFF FFFF,這樣memcpy函數會在這里陷入死循環,一直到外存耗盡,地址再增加,找不到外存地址了,然后觸發Hard Fault。

3.2 濫用臨界區

       程序中的一些關鍵代碼,有時候需要在臨界區中執行,但是臨界區若使用不當,則也會造成錯誤。

OS_ENTER_CRITICAL();   

       。。。。。。。。。。。。。。。。。。

       。。。。。。。。。。。。。。。。。。

       OS_EXIT_CRITICAL();      

 

#define  OS_ENTER_CRITICAL()  {cpu_sr = OS_CPU_SR_Save();} 

OS_CPU_SR_Save

MRS     R0, PRIMASK     ;保存全局中斷標志  ;   

CPSID   I                 ;關中斷

    BX      LR

將全局中斷標志保存到R0中,此時R0是0,CPSID   I則執行關中斷命令,此時PRIMASK是1。

 

#define  OS_EXIT_CRITICAL()   {OS_CPU_SR_Restore(cpu_sr);}

OS_CPU_SR_Restore

    MSR     PRIMASK, R0         ;恢復全局中斷標志

    BX      LR

將R0放入全部中斷寄存器中,則允許所有中斷了。

 

程序中如何保護R0的,細看匯編發現,實際上在執行關中斷后,將R0保存到了sp+8處,開中斷時再取出來,這樣才保證了不會被修改。

STR      r0,[sp,#0x08]tPendTimes = 0;

同時,開中斷, LDR      r0,[sp,#0x08],則從sp+8處取出來,保存到R0中。

 

臨界區中的代碼完成如下內容:

netconn_write(tradeconn,g_u8TcpSendBuf,l_u32CodeSendLen,NETCONN_COPY);

調用TCPIP_APIMSG(&msg);,

sys_mbox_post(mbox, &msg);

OSQPost(mbox->hMBox, msg)發送消息,OS_EventTaskRdy函數修改線程的狀態,使OSTCBStatPend變為等待完畢;

此時若協議棧線程優先級高於當前任務,則會觸發任務調度,懸起OSPendSV,但是由於關閉了中斷,即使在調用OS_ENTER_CRITICAL()后,也無法打開中斷,故不能執行中斷,任務無法切換。

同理,調用sys_arch_sem_wait(apimsg->msg.conn->op_completed, 0);,也無法阻塞自身,執行任務調度,程序在臨界區里面變成了單線程在跑。

一直等待代碼執行完畢開中斷后,懸起的軟中才能執行,本來應該在發送消息和等待消息處執行任務切換的,現在只能等待臨界區執行完畢后,才能執行任務切換中斷。此刻的PSP是0x2000DFAC,臨界區的那段代碼我們也有壓棧操作,即是0x2000 DFAC后面的內容也是我們需要的,如下圖所示。

 

原來的內容是這樣的,如下圖所示:

 

此時在OSPendSV中,執行如下語句

MRS     R0, PSP                                             ; PSP is process stack pointer

CBZ     R0, OSPendSV_nosave           ;   

SUBS    R0, R0, #0x20                  ; save remaining regs r4-11 on process stack

    STM     R0, {R4-R11}

從PSP-32個字節處開始,保存R4到R11這8個寄存器32個字節,則原來的內容都被覆蓋了,而這些內容正好是我們需要的。被修改后的截圖如所示,原來的內容被改成R4到R11這幾個寄存器的值。

 

其中從0801556D變成了68130000,協議棧線程如下執行。

msg->msg.apimsg->function(&(msg->msg.apimsg->msg));

函數的地址變成了6813 0000,而6813 0000,是我們的外存,

在這里執行代碼0x68130006 F63A07E1  DCD      0xF63A07E1   ; ? Undefined

最終是這句話,觸發了Hard fault。

3.3 運行中記錄出錯位置

以3.2為例子,進行簡單的反推。啟動文件中的Hard中斷處理一般如下所示,即讓程序陷入這個死循環。

HardFaultException

;        B       HardFaultException

現在我們要在記錄重要數據,即此刻系統的運行情況,主要包括:此刻堆棧情況、以及R0等8個寄存器的值、相關Hard硬件寄存器的值,若是任務引發的,還要記錄任務的ID號,因此修改這個異常處理函數。

HardFaultException

              TST LR, #4                         ;將LR的值與4按位相與

              ITE EQ                              //若為0則是MSP,否則是PSP

              MRSEQ R0, MSP

              MRSNE R0, PSP

              B hard_fault_handler_c                //這個是C語言編寫的函數

void hard_fault_handler_c(unsigned int * hardfault_args)

{

       unsigned int stacked_r0,stacked_r1,stacked_r2,stacked_r3;

       unsigned int stacked_r12,stacked_lr, stacked_pc, stacked_psr;

       stacked_r0 = ((unsigned long) hardfault_args[0]);

       stacked_r1 = ((unsigned long) hardfault_args[1]);

       stacked_r2 = ((unsigned long) hardfault_args[2]);

       stacked_r3 = ((unsigned long) hardfault_args[3]);

       stacked_r12 = ((unsigned long) hardfault_args[4]);

       stacked_lr = ((unsigned long) hardfault_args[5]);

       stacked_pc = ((unsigned long) hardfault_args[6]);

       stacked_psr = ((unsigned long) hardfault_args[7]);

       sprintf((char*)g_cDataBuf,"[Hard fault handler]\n");

       Usart232SendStr(g_cDataBuf);

       sprintf((char*)g_cDataBuf,"The task pri id = 0x%0.8x\n", OSPrioCur);   //任務ID號

       Usart232SendStr(g_cDataBuf);

       sprintf((char*)g_cDataBuf,"SP = 0x%0.8x\n", hardfault_args);               //堆棧地址

       Usart232SendStr(g_cDataBuf);

       sprintf((char*)g_cDataBuf,"R0 = 0x%0.8x\n", stacked_r0);

       Usart232SendStr(g_cDataBuf);

       sprintf((char*)g_cDataBuf,"R1 = 0x%0.8x\n", stacked_r1);

       Usart232SendStr(g_cDataBuf);

       sprintf((char*)g_cDataBuf,"R2 = 0x%0.8x\n", stacked_r2);

       Usart232SendStr(g_cDataBuf);

       sprintf((char*)g_cDataBuf,"R3 = 0x%0.8x\n", stacked_r3);

       Usart232SendStr(g_cDataBuf);

       sprintf((char*)g_cDataBuf,"R12 = 0x%0.8x\n", stacked_r12);

       Usart232SendStr(g_cDataBuf);

       sprintf((char*)g_cDataBuf,"LR = 0x%0.8x\n", stacked_lr);

       Usart232SendStr(g_cDataBuf);

       sprintf((char*)g_cDataBuf,"PC = 0x%0.8x\n", stacked_pc);

       Usart232SendStr(g_cDataBuf);

       sprintf((char*)g_cDataBuf,"PSR = 0x%0.8x\n", stacked_psr);

       Usart232SendStr(g_cDataBuf);

       exit(0); // terminate

       return;

}

以3.2為例,發生異常后,串口的輸出入下所示:

[Hard fault handler]

The task pri id = 0x00000014                     //任務優先級是20

SP = 0x200077d8                               //當前任務的堆棧地址是0x2000 77D8

R0 = 0x2000dfa0

R1 = 0x68130000

R2 = 0x2000df9c

R3 = 0x20002100

R12 = 0x00000001

LR = 0x0801c7fb                              //分析得出,這個地址就是出錯的地方

PC = 0x68130000

PSR = 0x00000000

此時需要借助map文件分析,map文件中得出對應的代碼和數據位置。

    tcpip_thread                             0x0801c7ad   Thumb Code   190  tcpip.o(i.tcpip_thread)

i.tcpsvr_accept_20                       0x0801c874   Section       64  ftpmanage.o(i.tcpsvr_accept_20)

0x0801 c7fb應該在tcpip文件中的tciip_thread函數里。

T_LWIP_THREAD_STK                        0x20007000   Data        2048  sys_arch.o(.bss)

    rsuPib                                   0x20007800   Data          32  para.o(.bss)

堆棧空間是0x2000 77D8,是在T_LWIP_THREAD_STK這個棧空間里,這也是協議棧任務的堆棧空間,證明判斷的任務優先級為20是正確的。

從0x0801 C7AD處開始的16進制文件如下圖所示,再將匯編文件列出(需要KeiL生成)。

 

tcpip_thread PROC

;;;232    static void

;;;233    tcpip_thread(void *arg)

000000  b508              PUSH     {r3,lr}                  //開始

;;;234    {

;;;235      struct tcpip_msg *msg;

;;;236      LWIP_UNUSED_ARG(arg);

;;;237   

;;;238    #if IP_REASSEMBLY

;;;239      sys_timeout(IP_TMR_INTERVAL, ip_reass_timer, NULL);

;;;240    #endif /* IP_REASSEMBLY */

;;;241    #if LWIP_ARP

;;;242      sys_timeout(ARP_TMR_INTERVAL, arp_timer, NULL);

000002  2200              MOVS     r2,#0

000004  492e              LDR      r1,|L11.192|

000006  f2413088          MOV      r0,#0x1388

00000a  f7fffffe          BL       sys_timeout

;;;243    #endif /* LWIP_ARP */

;;;244    #if LWIP_DHCP

;;;245      sys_timeout(DHCP_COARSE_TIMER_MSECS, dhcp_timer_coarse, NULL);

;;;246      sys_timeout(DHCP_FINE_TIMER_MSECS, dhcp_timer_fine, NULL);

;;;247    #endif /* LWIP_DHCP */

;;;248    #if LWIP_AUTOIP

;;;249      sys_timeout(AUTOIP_TMR_INTERVAL, autoip_timer, NULL);

;;;250    #endif /* LWIP_AUTOIP */

;;;251    #if LWIP_IGMP

;;;252      sys_timeout(IGMP_TMR_INTERVAL, igmp_timer, NULL);

;;;253    #endif /* LWIP_IGMP */

;;;254    #if LWIP_DNS

;;;255      sys_timeout(DNS_TMR_INTERVAL, dns_timer, NULL);

;;;256    #endif /* LWIP_DNS */

;;;257   

;;;258      if (tcpip_init_done != NULL) {

00000e  482d              LDR      r0,|L11.196|

000010  6800              LDR      r0,[r0,#0]  ; tcpip_init_done

000012  b128              CBZ      r0,|L11.32|

;;;259        tcpip_init_done(tcpip_init_done_arg);

000014  482b              LDR      r0,|L11.196|

000016  1d00              ADDS     r0,r0,#4

000018  6800              LDR      r0,[r0,#0]  ; tcpip_init_done_arg

00001a  492a              LDR      r1,|L11.196|

00001c  6809              LDR      r1,[r1,#0]  ; tcpip_init_done

00001e  4788              BLX      r1

                  |L11.32|

;;;260      }

;;;261    

;;;262      LOCK_TCPIP_CORE();

;;;263      while (1) {                          /* MAIN Loop */

000020  e04c              B        |L11.188|

                  |L11.34|

;;;264        sys_mbox_fetch(mbox, (void *)&msg);

000022  4669              MOV      r1,sp

000024  4827              LDR      r0,|L11.196|

000026  1f00              SUBS     r0,r0,#4

000028  6800              LDR      r0,[r0,#0]  ; mbox

00002a  f7fffffe          BL       sys_mbox_fetch

;;;265        switch (msg->type) {

00002e  9800              LDR      r0,[sp,#0]

000030  7800              LDRB     r0,[r0,#0]

000032  2805              CMP      r0,#5

000034  d240              BCS      |L11.184|

000036  e8dff000          TBB      [pc,r0]

00003a  030b              DCB      0x03,0x0b

00003c  222b3500          DCB      0x22,0x2b,0x35,0x00

;;;266    #if LWIP_NETCONN

;;;267        case TCPIP_MSG_API:

;;;268            //if(msg->msg.apimsg->msg.conn == NULL)

;;;269            //  break;

;;;270          LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: API message %p\n", (void *)msg));

;;;271          msg->msg.apimsg->function(&(msg->msg.apimsg->msg));

000040  9a00              LDR      r2,[sp,#0]

000042  6892              LDR      r2,[r2,#8]

000044  1d10              ADDS     r0,r2,#4

000046  9a00              LDR      r2,[sp,#0]

000048  6892              LDR      r2,[r2,#8]

00004a  6811              LDR      r1,[r2,#0]

00004c  4788              BLX      r1

;;;272          break;

00004e  e034              B        |L11.186|          //0x0801 c7fb對應的代碼

;;;273    #endif /* LWIP_NETCONN */

;;;274   

;;;275        case TCPIP_MSG_INPKT:

;;;276          LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: PACKET %p\n", (void *)msg));

;;;277    #if LWIP_ARP

;;;278          if (msg->msg.inp.netif->flags & NETIF_FLAG_ETHARP) {

000050  9800              LDR      r0,[sp,#0]

 

從代碼看地址對應是00004e  e034              B        |L11.186|      ,即switch分支的break語句,但是實際應該是上面的那句,BLX      r1,而此時R1的值是

R1 = 0x68130000,即跳轉到6813 0000處執行,與在3.2的分析是一樣的。

這也只能判斷出出錯的位置,原因還是需要仿真調試,才能找到。

3.4 總結

發生Hard Fault以后,意味着程序跑飛了,有的原因是很簡單的,但是有的需要仔細分析,以上兩個例子應該都算比較簡單的。以第一個例子來說,若從首次定位來看是在中斷里,中斷里變量的數值太大,超出了外存的大小,但是這個值為什么會這么大?正常中斷接收數據不會變成有0x21FF 2200這么多字節的,此時就需要考慮是別的地方踩到了此處的內存,導致取出來的數據一下子變成了這么大。然后逐步定位,才能找出真正的原因。

4 參考文獻

網上有幾篇非常不錯的文章,可以看看,加深理解。

[1] Cortex-M3 權威指南,Joseph Yiu 著,宋岩 譯。(在書的附錄E中對Fault類異常有非常詳盡的介紹)

[2] Cortex-M3技術參考手冊,周立功。(在書的P89到96頁對以上所介紹的寄存器有很詳細的描述)

[3] Application Note 209,Using Cortex-M3 and Cortex-M4 Fault Exceptions. KEIL Tools by ARM.(這個是KEIL軟件下使用的說明,介紹的例子可以一看)

[4] 教你如何找到導致程序跑飛的指令,,http://blog.sina.com.cn/ifreecoding.(博主在文章里一步步講述,非常清晰,讓人一看就明白)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM