H a r d F a u l t

28 篇文章 0 订阅

1 Cortex-M3/4的Fault简介

        Cortex-M3/4的Fault异常是由于非法的存储器访问(比如访问0地址、写只读存储位置等)和非法的程序行为(比如除以0等)等造成的。常见的4种异常及产生异常的情况如下:

Bus Fault:在fetch指令、数据读写、fetch中断向量或中断时存储恢复寄存器栈情况下,检测到内存访问错误则产生Bus Fault。

Memory Management Fault:访问了内存管理单元(MPU)定义的不合法的内存区域,比如向只读区域写入数据。

Usage Fault:检测到未定义指令或在存取内存时有未对齐。还可以通过软件配置是否检测到除0和其它未对齐内存访问也产生该异常,默认关闭,需要在工程初始化时配置:

  1. SCB->CCR |= 0x18; // enable div-by-0 and unaligned fault  

Hard Fault:在调试程序过程中,这种异常最常见。上面三种异常发生任何一种异常都会引起Hard Fault,在上面的三种异常未使能的情况下,默认发生异常时进入Hard Fault中断服务程序。使能前三种异常也要在初始化时配置:

  1. SCB->SHCSR |= 0x00007000;   // enable Usage Fault, Bus Fault, and MMU Fault  
  2. 在默认复位初始化时,Hard Fault使能,其它三者不使能,因此当程序中出现不合法内存访问(一般是指针错误引起)或非法的程序行为(一般就是数学里面常见的除0)时都将产生Hard Fault中断


    2 Hard Fault调试方法

    假设IDE环境为Keil,芯片为STM32F103。

    在stm32f10x_it.c中,添加软件断点,一旦调试时出现Hard Fault则会在停在__breakpoint(0)处。

    1. /** 
    2.   * @brief  This function handles Hard Fault exception. 
    3.   * @param  None 
    4.   * @retval None 
    5.   */  
    6. void HardFault_Handler(void)  
    7. {  
    8.   /* Go to infinite loop when Hard Fault exception occurs */  
    9.   if (CoreDebug->DHCSR & 1) {  //check C_DEBUGEN == 1 -> Debugger Connected  
    10.       __breakpoint(0);  // halt program execution here         
    11.   }  
    12.   while (1)  
    13.   {  
    14.   }  
    15. }  


    当进入Hard Fault断点后,菜单栏Peripherals >Core Peripherals >Fault Reports打开异常发生的报告,查看发生异常的原因。

      


    上面的报告发生了BUS FAULT,并将Fault的中断服务转向Hard Fault。


    相对于检测发生了什么异常,定位异常发生位置显得更重要。
    (1)打开Call Stack窗口(如下图左侧,断点停在Hard Fault服务程序中)


    (2)在Call Stack的HardFault_Handler上右键Show Caller Code(有的Keil版本也可以直接双击)

    (2)在Call Stack的HardFault_Handler上右键Show Caller Code(有的Keil版本也可以直接双击)


    这时将跳转到发生异常的源代码位置(如上图),异常发生在p->hour=0这一行。这里错误很明显:指针p尚未为成员变量分配
    内存空间,直接访问未分配的内粗空间肯定出错。


    再说明2点:
    [1] 在复杂的情况下,即使定位了异常发生位置也很难容易的改正错误,要学会使用Watch窗口对发生错误的指针变量进行跟踪;
    [2] 在问题不明晰的情况下,尝试分析反汇编代码,就自己遇到的,部分情况下的异常发生在BL等跳转指令处,BL跳转到了不合法的内存地址产生异常


    Refrences:

    [1] Application Note 209. Using Cortex-M3 and Cortex-M4 Fault Exceptions. 

    [2] Cortex-M3权威指南

    、、、、---------------------------------------next article

    hard fault

    在调试RTC过程中,程序在主循环中执行两次后就进入hard fault的while(1)中断,keil显示调试窗口显示imprecise data bus error。完善RTC配置的时序也无济于事。网上查到一些hard fault的资料:

    <STM32F10xxx Cortex-M3 programming manual>2.3.2对hard fault, bus fault等有具体的解释。keil的网站上http://www.keil.com/appnotes/files/apnt209.pdf也有概括性的解释:hard fault由bus fault, memory management fault或usage fault引起,前者有固定的仅次于NMI的高优先级;调试过程中出现的bus error属于bus fault,是取指或取值时的内存错误。ST论坛上对于hard fault的讨论,大牛们说:

    是由于读写了一个非法位置,

    “100% of the hard faults I've had are caused by variables accessing out of bounds. ”,

    "The Cortex-M3 pushes fault context on to the stack (some 8 dwords as I recall), I think Joseph Yiu has some example of instrumenting this. This could should permit you to determine the faulting PC. With this and the register info, and a map file you should be able to zero in on what is going on.

                    MRS     R0, PSP         ; Read PSP
                    LDR     R1, [R0, #24]    ; Read Saved PC from Stack" 能看到出错的PC值倒是一个很方便的事情,不过还没试过。

    还有若干链接,未及一一详看,先备着:https://my.st.com/public/STe2ecommunities/mcu/Lists/ARM%20CortexM3%20STM32/Flat.aspx?RootFolder=https%3a%2f%2fmy%2est%2ecom%2fpublic%2fSTe2ecommunities%2fmcu%2fLists%2fARM%20CortexM3%20STM32%2fHard%20Fault%20error&FolderCTID=0x01200200770978C69A1141439FE559EB459D758000626BE2B829C32145B9EB5739142DC17E&currentviews=1147http://forums.arm.com/lofiversion/index.php?t13632.htmlhttps://my.st.com/public/STe2ecommunities/mcu/Lists/ARM%20CortexM3%20STM32/Flat.aspx?RootFolder=%2Fpublic%2FSTe2ecommunities%2Fmcu%2FLists%2FARM%20CortexM3%20STM32%2FHardFault%20Exception%20Why%20oh%20why%21http://www.google.com/search?hl=en&q=Joseph+Yiu+hard+fault+exception+handler

    论坛里大家都说到了Joseph Yiu,哪天要好好拜读一下他的CortexM3权威指南。

     

    回头看自己的程序,从最简逻辑开始烧写运行,发现当增加到在Time_Display()时进入了hard fault。检查代码,函数中定义了一个char类型数组,用于存放需要显示到LCD上的时间字符串,但数组长度小于字符串长度。增大长度,就解决了问题。果然如大牛们所说,问题存在于数组越界。

     

    之前我也犯过类似错误,可当时的现象是,串口实际发出的数据和数组中的数据相比,后半部分时对时错。当时的变量为全局变量,此处变量为局部变量。查到如下说明:“一个由c/C++编译的程序占用的内存分为以下几个部分: 1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放. 4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放. 5、程序代码区—存放函数体的二进制代码。”全局变量储存在全局区,它的越界将影响其他变量的值,对程序运行不会有致命影响。局部变量在栈中,同时入栈的还有函数的回退地址,函数参数等。本次出现问题的代码段为:

    [cpp]  view plain copy
    1. void Time_Show(void)  
    2. {  
    3.     while (1)  
    4.     {  
    5.         /* If 1s has been elapased */  
    6.         if (TimeDisplay == 1)  
    7.         {  
    8.             uint32_t Counter = 0;  
    9.             Counter = RTC_GetCounter();  
    10.             Time_Display(Counter);  
    11.             TimeDisplay = 0;  
    12.         }  
    13.     }  
    14. }  
    15.   
    16. void Time_Display(uint32_t TimeVar)  
    17. {  
    18.     uint32_t THH = 0, TMM = 0, TSS = 0;  
    19.     char buf[10];  
    20.     /* Reset RTC Counter when Time is 23:59:59 */  
    21.     if (TimeVar == 0x0001517F)  
    22.     {  
    23.         RTC_WaitForLastTask();  
    24.         RTC_SetCounter(0x0);  
    25.         /* Wait until last write operation on RTC registers has finished */  
    26.         RTC_WaitForLastTask();  
    27.     }  
    28.       
    29.     /* Compute  hours */  
    30.     THH = TimeVar / 3600;  
    31.     /* Compute minutes */  
    32.     TMM = (TimeVar % 3600) / 60;  
    33.     /* Compute seconds */  
    34.     TSS = (TimeVar % 3600) % 60;  
    35.   
    36.     /*  sprintf(buf, "0x%08x", buf);     
    37.     sprintf(buf, "0x%08x", &TimeVar);    
    38.     sprintf(buf, "0x%08x", &THH);    
    39.     sprintf(buf, "0x%08x", &TMM);    
    40.     sprintf(buf, "0x%08x", &TSS); 
    41.     */    
    42.     sprintf(buf, "%0.2d:%0.2d:%0.2d", THH, TMM, TSS);  
    43.     LCD_DisplayStringLine(LCD_LINE_1, buf);  
    44. }  

    在Time_Display()中通过用sprintf将地址赋值给变量(即代码中注视掉的sprintf语句),并在LCD上显示的办法观察到,栈内的变量分布情况为:

    可以看出,栈从内存地址高位向低位生长,参数在栈底,变量按照定义的顺序依次往上摞。系统给buf多留了两字节的空间,其余变量(包括函数参数timevar和局部变量TXX)在内存中依次紧密排列,没有出现windows中将函数回退地址的入栈时间放于参数之后,使参数和变量之间有四字节空隙的情况。这说明函数的回退地址和一些寄存器的入栈保存另有其他时机。同时注意到,代码中有用sprintf取得变量地址的语句时,工作正常,不会进入hardfault。因此有必要比较两段代码对内存空间造成的影响。

    1. 进入hard fault是在Time_Show()函数一个循环执行完毕时。因此有必要看一下汇编,了解具体对寄存器和内存的数据读写操作:

    [cpp]  view plain copy
    1.    208:      if (TimeDisplay == 1)   
    2.    209:     {   
    3. 0x08000E72 4C05      LDR      r4,[pc,#20]  ; @0x08000E88  
    4.    210:           uint32_t Counter = 0;   
    5. 0x08000E74 2500      MOVS     r5,#0x00  
    6. 0x08000E76 6820      LDR      r0,[r4,#0x00]  
    7. 0x08000E78 2801      CMP      r0,#0x01  
    8. 0x08000E7A D1FC      BNE      0x08000E76  
    9.    211:           Counter = RTC_GetCounter();   
    10. 0x08000E7C F7FFFE2C  BL.W     RTC_GetCounter (0x08000AD8)  
    11.    212:       Time_Display(Counter);   
    12. 0x08000E80 F7FFFF98  BL.W     Time_Display (0x08000DB4)  
    13.    213:       TimeDisplay = 0;   
    14. 0x08000E84 6025      STR      r5,[r4,#0x00]  
    15.    214:     }   
    16. 0x08000E86 E7F6      B        0x08000E76  

    在这一段中,R4存放变量TimeDisplay的地址,R0为TimeDisplay的值。循环的最后一步,寄存器R4中的地址加0作为新地址,R5从内存中的该新地址取值存入。如果R4指向的地址非法,则读取该地址很有可能产生hard fault。

    2.查看Time_Display()的汇编

    (1)添加了显示变量地址的代码,而无hard fault的情况。

    主循环的起始部分汇编代码如下,每次进入循环只需将Time_Display()时入栈的回退地址弹出作为PC。

    [cpp]  view plain copy
    1.    200: void Time_Show(void)   
    2. 0x08000E44 B009      ADD      sp,sp,#0x24  
    3. 0x08000E46 BD00      POP      {pc}  
    4. 0x08000E48 517F      STR      r7,[r7,r5]  

    刚进入Time_Display()时的汇编代码如下,进入时将R0和LR寄存器压入栈中。

    [cpp]  view plain copy
    1.    165: void Time_Display(uint32_t TimeVar)   
    2. 0x08000DAC E8BD4010  POP      {r4,lr}  
    3. 0x08000DB0 F7FFBEEA  B.W      RTC_WaitForLastTask (0x08000B88)  
    4.    166: {   
    5. 0x08000DB4 B501      PUSH     {r0,lr}  
    6. 0x08000DB6 B088      SUB      sp,sp,#0x20  
    7.    167:         uint32_t THH = 0, TMM = 0, TSS = 0;   
    8.    168:         char buf[10];   

    此时STM32芯片寄存器和内存的情况如下图所示。

     

    根绝汇编中把R0和LR压入栈中的指令,对应LR和R0的值,在局部变量所在内存空间寻找,可以发现LR最先入栈,接着是函数参数和其余变量,这和最开始打印出的各变量地址也是吻合的。因此,如果buf越界不是太多,只是改写了其余局部变量的数据,不影响回退地址。另外,查看函数所有汇编代码,没有对R4的操作。至函数执行完成并返回,R4的值始终为0x20000000。综上,函数可以继续执行而不会出错。

     

    (2)产生hard fault的情况。

    主循环的起始部分汇编代码如下,需要在Time_Display()后的寄存器值和回退地址都弹出。

    [cpp]  view plain copy
    1. <span style="font-size:13px;">   196: void Time_Show(void)   
    2. 0x08000D90 BD1F      POP      {r0-r4,pc}  
    3. 0x08000D94 517F      STR      r7,[r7,r5]</span>  

    刚进入Time_Display()时的汇编代码如下,将R0-R4,及LR都压入栈中。

    [cpp]  view plain copy
    1.    165: void Time_Display(uint32_t TimeVar)   
    2. 0x08000D40 E8BD4010  POP      {r4,lr}  
    3. 0x08000D44 F7FFBEEA  B.W      RTC_WaitForLastTask (0x08000B1C)  
    4.    166: {   
    5.    167:         uint32_t THH = 0, TMM = 0, TSS = 0;   
    6.    168:         char buf[10];   
    7.    169:         /* Reset RTC Counter when Time is 23:59:59 */   
    8. 0x08000D48 B51F      PUSH     {r0-r4,lr}  
    9. 0x08000D4A 4604      MOV      r4,r0  

    此时STM32芯片寄存器和内存的情况如下图所示。

    此时buf的地址为0x200003ec,即R1的起始位置。变量和寄存器值的覆盖关系,或许是编译器检测到R1~R3的值在出栈后将不会被使用,而对内存进行的优化。此时内存中没有其他局部变量的位置,是因为在改动了代码的情况下,编译器判断为,只需在寄存器里就可以完成计算操作,因此改变了函数的汇编代码,没有占用内存空间。buf的赋值是按从低地址到高地址的顺序进行的。从内存的分配图中可以看出,如果buf越界,数组元素超过12个,就将影响到R4的内容。而如1中所述,R4的内容是Time_Display()退出后,需要读取的内存地址。如果经sprintf()后,buf内有15个字符,加上0x00,共16个字符,正好完全覆盖R4,且R4的最高位为0x00,显然是一个非法的内存空间,因此将进入hard fault。如果buf内的字符数落在(12,16)区间内,R4的地址合法(仍为0x20开头),不会进入hard fault,但地址已被修改,错误的内存空间中数值未知,程序跑飞。这些分析与实际测试结果是一致的。

     

    问题得到了解释,也不知花了一天时间分析这些值不值。出错与否,除了程序本身的正确以外,编译器将C翻译成汇编的发挥程度也是很大的决定因素。想避免这些头疼的问题,结论就一句话:数组不要越界


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值