×
嵌入式 > 嵌入式开发 > 详情

stm32 hard fault及堆栈探究

发布时间:2020-08-11 发布时间:
|

在调试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的网站上也有概括性的解释:hard fault由bus fault, memory management fault或usage fault引起,前者有固定的仅次于NMI的高优先级;调试过程中出现的bus error属于bus fault,是取指或取值时的内存错误。ST论坛上对于hard fault的讨论,大牛们说:

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

“100% of the hard faults Ive 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值倒是一个很方便的事情,不过还没试过。

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

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

  1. voidTime_Show(void)
  2. {
  3. while(1)
  4. {
  5. /*If1shasbeenelapased*/
  6. if(TimeDisplay==1)
  7. {
  8. uint32_tCounter=0;
  9. Counter=RTC_GetCounter();
  10. Time_Display(Counter);
  11. TimeDisplay=0;
  12. }
  13. }
  14. }
  15. voidTime_Display(uint32_tTimeVar)
  16. {
  17. uint32_tTHH=0,TMM=0,TSS=0;
  18. charbuf[10];
  19. /*ResetRTCCounterwhenTimeis23:59:59*/
  20. if(TimeVar==0x0001517F)
  21. {
  22. RTC_WaitForLastTask();
  23. RTC_SetCounter(0x0);
  24. /*WaituntillastwriteoperationonRTCregistershasfinished*/
  25. RTC_WaitForLastTask();
  26. }
  27. /*Computehours*/
  28. THH=TimeVar/3600;
  29. /*Computeminutes*/
  30. TMM=(TimeVar%3600)/60;
  31. /*Computeseconds*/
  32. TSS=(TimeVar%3600)%60;
  33. /*sprintf(buf,"0x%08x",buf);
  34. sprintf(buf,"0x%08x",&TimeVar);
  35. sprintf(buf,"0x%08x",&THH);
  36. sprintf(buf,"0x%08x",&TMM);
  37. sprintf(buf,"0x%08x",&TSS);
  38. */
  39. sprintf(buf,"%0.2d:%0.2d:%0.2d",THH,TMM,TSS);
  40. LCD_DisplayStringLine(LCD_LINE_1,buf);
  41. }

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

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

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

  1. 208:if(TimeDisplay==1)
  2. 209:{
  3. 0x08000E724C05LDRr4,[pc,#20];@0x08000E88
  4. 210:uint32_tCounter=0;
  5. 0x08000E742500MOVSr5,#0x00
  6. 0x08000E766820LDRr0,[r4,#0x00]
  7. 0x08000E782801CMPr0,#0x01
  8. 0x08000E7AD1FCBNE0x08000E76
  9. 211:Counter=RTC_GetCounter();
  10. 0x08000E7CF7FFFE2CBL.WRTC_GetCounter(0x08000AD8)
  11. 212:Time_Display(Counter);
  12. 0x08000E80F7FFFF98BL.WTime_Display(0x08000DB4)
  13. 213:TimeDisplay=0;
  14. 0x08000E846025STRr5,[r4,#0x00]
  15. 214:}
  16. 0x08000E86E7F6B0x08000E76

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

2.查看Time_Display()的汇编

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

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

  1. 200:voidTime_Show(void)
  2. 0x08000E44B009ADDsp,sp,#0x24
  3. 0x08000E46BD00POP{pc}
  4. 0x08000E48517FSTRr7,[r7,r5]

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

  1. 165:voidTime_Display(uint32_tTimeVar)
  2. 0x08000DACE8BD4010POP{r4,lr}
  3. 0x08000DB0F7FFBEEAB.WRTC_WaitForLastTask(0x08000B88)
  4. 166:{
  5. 0x08000DB4B501PUSH{r0,lr}
  6. 0x08000DB6B088SUBsp,sp,#0x20
  7. 167:uint32_tTHH=0,TMM=0,TSS=0;
  8. 168:charbuf[10];

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

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

(2)产生hard fault的情况。

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

  1. 196:voidTime_Show(void)
  2. 0x08000D90BD1FPOP{r0-r4,pc}
  3. 0x08000D94517FSTRr7,[r7,r5]

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

  1. 165:voidTime_Display(uint32_tTimeVar)
  2. 0x08000D40E8BD4010POP{r4,lr}
  3. 0x08000D44F7FFBEEAB.WRTC_WaitForLastTask(0x08000B1C)
  4. 166:{
  5. 167:uint32_tTHH=0,TMM=0,TSS=0;
  6. 168:charbuf[10];
  7. 169:/*ResetRTCCounterwhenTimeis23:59:59*/
  8. 0x08000D48B51FPUSH{r0-r4,lr}
  9. 0x08000D4A4604MOVr4,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翻译成汇编的发挥程度也是很大的决定因素。想避免这些头疼的问题,结论就一句话:数组不要越界。



『本文转载自网络,版权归原作者所有,如有侵权请联系删除』

热门文章 更多
短期NAND Flash价格微幅下调.明年市况具涨势?