stm32 hard fault及堆栈探究

发布者:幸福家园最新更新时间:2016-07-28 来源: eefocus关键字:stm32  hard  fault  堆栈 手机看文章 扫描二维码
随时随地手机看文章

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

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 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值倒是一个很方便的事情,不过还没试过。

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

 

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

  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()函数一个循环执行完毕时。因此有必要看一下汇编,了解具体对寄存器和内存的数据读写操作:

  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。

 
  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寄存器压入栈中。

 
  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()后的寄存器值和回退地址都弹出。

 
  1.    196: void Time_Show(void)   
  2. 0x08000D90 BD1F      POP      {r0-r4,pc}  
  3. 0x08000D94 517F      STR      r7,[r7,r5]  

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

 
  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翻译成汇编的发挥程度也是很大的决定因素。想避免这些头疼的问题,结论就一句话:数组不要越界。

关键字:stm32  hard  fault  堆栈 引用地址:stm32 hard fault及堆栈探究

上一篇:STM32 RTC小结
下一篇:STM32 USART中断小程序

推荐阅读最新更新时间:2024-03-16 15:02

STM32自己总结如何正确创建一个工程
1、建立好文件夹,文件夹中有相应的库函数和头文件等等 以上通过我的G盘里面的建立工程模板解压即可得到 2、通过keil创建test_creat.uvproj到以目录 选择完芯片以后,弹出窗口一定要选择“否” 3、添加文件,只用添加C文件进去 添加完成以后会看到: 4、头文件路径的添加 stm32f10x_it.h ,stm32f10x.h, stm32f10x_conf.h ,core_cm3.h, system_stm32f10x.h 主要添加以上的头文件位置还有各种ppp外设头文件的位置也就是src文件夹 5.编译会出现的问题及解决方法 1、去掉STM32F10
[单片机]
<font color='red'>STM32</font>自己总结如何正确创建一个工程
stm32控制舵机DS3115转动
一、舵机DS3115 一般来讲,舵机主要由以下几个部分组成, 舵盘、减速齿轮组、位置反馈电位计5k、直流电机、控制电路板等。 工作原理:控制电路板接受来自信号线的控制信号(具体信号待会再讲),控制电机转动,电机带动一系列齿轮组,减速后传动至输出舵盘。舵机的输出轴和位置反馈电位计是相连的,舵盘转动的同时,带动位置反馈电位计,电位计将输出一个电压信号到控制电路板,进行反馈,然后控制电路板根据所在位置决定电机的转动方向和速度,从而达到目标停止。 舵机的基本结构是这样,但实现起来有很多种。例如电机就有有刷和无刷之分,齿轮有塑料和金属之分,输出轴有滑动和滚动之分,壳体有塑料和铝合金之分,速度有快速和慢速之分,体积有大中小三种之分等等,组合不
[单片机]
stm32定时器实现60秒定时秒表
#include led.h #include delay.h #include key.h #include sys.h #include lcd.h #include usart.h #include timer.h #include beep.h extern u32 sec; int main(void) { delay_init(); //延时函数初始化 NVIC_Configuration(); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级 uart_init(9600); //串口初始化为9600 LED_Ini
[单片机]
STM32在IAR中如何使用printf函数
STM32使用printf函数给串口打印信息的执行步骤为: 1.重定向printf函数 给uart.c文件中增加如下函数: int fputc(int ch, FILE *f) { USART_SendData(USART2, (unsigned char) ch);// USART1 可以换成 USART2 等 while (!(USART2- SR & USART_FLAG_TXE)); return (ch); } 2.增加头文件stdio #include stdio.h 3.添加宏 在IAR中使用printf应在Options- C/C++Compler- Preprocess
[单片机]
<font color='red'>STM32</font>在IAR中如何使用printf函数
STM32】外部中断不可以同PIN
问题背景: 在做低功耗外部中断唤醒的时候,发现PD2配置成外部中断唤醒之后,之前配置的PB2不可以正常唤醒了,注释掉PD2的外部中断GPIO配置之后,又可以正常唤醒。 问题原因: 查阅资料发现STM32的外部中断即使是不同PORT,但是只要是同PIN也是不可以同时配置为外部中断使用的。我们看STM32CubeMX发现配置的时候也是配置不了的,是互斥的。 STM32外部中断不可以共用PIN 这也验证了HAL库中外部中断回调函数只有一个形参PIN的判断,不区分PORT。 /** * @brief EXTI line detection callback. * @param GPIO_Pin Specifies th
[单片机]
STM32硬件错误的调试技巧
在用Keil对STM32的程序进行仿真时程序有时会跑飞,停止仿真程序会停在HardFault_Handler函数里的死循环while(1)中。 这说明STM32出现了硬件错误。 硬件错误中断 STM32出现硬件错误可能有以下原因: 数组越界操作; 内存溢出,访问越界; 堆栈溢出,程序跑飞; 中断处理错误; 遇到这种情况,可以通过以下2种方式来定位到出错代码段。 方法1: 在硬件中断函数HardFault_Handler里的while(1)处打调试断点,程序执行到断点处时点击STOP停止仿真。 示例 1.2 在Keil菜单栏点击View——Registers Window,在寄存器查看窗口查找R14(LR)的值。 如果R
[单片机]
<font color='red'>STM32</font>硬件错误的调试技巧
STM32速成笔记(3)—按键检测
一、按键 检测 原理 按键检测原理比较简单,按键按下和不按下,其连接引脚的电平是不一样的,按键检测正是通过检测按键引脚的电平变化来实现的。比如按键未按下时引脚电平为高电平,按键按下后为低电平。我们在检测按键时只需要检测按键引脚是否变为低电平来确定按键是否按下。 二、 硬件 连接 按键的硬件连接决定了我们在配置按键IO时IO的状态。以我们使用的普中核心板为例,上面有三个按键 普中核心板按键硬件电路图 其中K1一端接VCC,另一端接单片机。K2和K3一端接地,另一端接单片机。硬件电路不同,导致他们在进行按键检测时IO的配置不同。 针对K1这种按键电路,按键按下时, 单片机 的引脚接到VCC,因此在未按下的情况下该引脚的默认电平
[单片机]
<font color='red'>STM32</font>速成笔记(3)—按键检测
小广播
添点儿料...
无论热点新闻、行业分析、技术干货……
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved