【STM32】hardfault定位方法

目录

1. 案例1 

1.1 STM32出现硬件错误可能有以下原因:

1.2 出现问题时排查的方法:

1.3 STM32出现HardFault_Handler故障的原因主要有两个方面:

2. 案例2

2.1 方法1   如何精确定位出问题代码的所在位置:

2.2 方法2:最简单,最明显

2.3 方法3:此方法和方法一大致相同



1. 案例1 

    在用Keil对STM32的程序进行仿真时程序有时会跑飞,停止仿真程序会停在HardFault_Handler函数里的死循环while(1)中。这说明STM32出现了硬件错误。

/**
  * @brief  This function handles Hard Fault exception.
  * @param  None
  * @retval None
  */
void HardFault_Handler(void)
{
  /* Go to infinite loop when Hard Fault exception occurs */
  while (1)
  {
  }
}

1.1 STM32出现硬件错误可能有以下原因:

  • 数组越界操作;
  • 内存溢出,访问越界;
  • 堆栈溢出,程序跑飞;
  • 中断处理错误;
  • 内存溢出或者访问越界。这个需要自己写程序的时候规范代码,遇到了需要慢慢排查。
  • 堆栈溢出。增加堆栈的大小。
  •  

1.2 出现问题时排查的方法:

1、发生异常之后可首先查看LR寄存器中的值,确定当前使用堆栈为MSP或PSP,然后找到相应堆栈的指针,并在内存中查看相应堆栈里的内容。由于异常发生时,内核将R0~R3、R12、Return address、PSR、LR寄存器依次入栈,其中Return address即为发生异常前PC将要执行的下一条指令地址,因此在堆栈中反数第三个字即为出错位置。

2、默认的HardFault_Handler处理方法是B .将它改成BX LR直接返回的形式。然后在这条语句打个断点,一旦在断点中停下来,说明出错了,然后再返回,就可以返回到出错的位置的下一条语句那儿。
这个有时候可能需要在反汇编模式下调试,因为可以是程序跑飞一会儿才出现HardFault_Handler。

3、还是将中断函数修改,打印中断时的一些信息:

//HardFault_Hander()定义如下:
void HardFault_Handler(void)
{
  uint32_t r_sp ;

  r_sp = __get_PSP(); //获取SP的值
  PERROR(ERROR,Memory Access Error!);
  Panic(r_sp);
  while (1);

}

CPSR   当前程序状态寄存器 (Current Program State Register)

SPSR    保存的程序状态寄存器 (Saved Program State Register), 有6个,主要是在处理异常的时候使用.

每一种处理器模式下都有一个专用的物理寄存器作为备份的程序状态寄存器SPSR , 当特定的异常发生时,这个物理寄存器负责保存CPSR当前程序状态寄存器的内容, 当异常处理程序返回时,再将内容恢复到当前程序状态器中,继续向下执行原来程序.

PC    程序计数器,是用来计数的,指示指令在存储器的存放位置,也就是个地址信息.

 

1.3 STM32出现HardFault_Handler故障的原因主要有两个方面:

出现问题时排查的方法:

      发生异常之后可首先查看LR寄存器中的值,确定当前使用堆栈为MSP或PSP,然后找到相应堆栈的指针,并在内存中查看相应堆栈里的内容。由于异常发生时,内核将R0~R3、R12、LR、PC、XPRS 寄存器依次入栈,其中LR即为发生异常前PC将要执行的下一条指令地址。

注意:寄存器均是32位,且STM32是小端模式。(参考Cortex-M3权威)
【STM32】hardfault定位方法

SP值为0x20008560,查看堆栈里面的值依次为R0~R3、R12、LR、PC、XPRS, 例如R0(10 27 00 00),  显然堆栈后第21个字节到24字节即为LR,该地址0x08001FFD即为异常前PC将要执行的下一条指令地址(即StackFlow()后面的语句处 RCC->CR &= (uint32_t)0xFFFBFFFF)

void StackFlow(void)
{
    uint32_t a[3], i;
    for (i = 0; i < 10000; i++)
    {
        a[i] = 1;
    }
}

/**
  * @brief  Demo state machine.
  * @param  None
  * @retval None
  */
void Menu_Init(void)
{
    uint8_t *pdata;
    StackFlow();

    USBH_UsrLog("Starting MSC Demo");

    /* Create Menu Semaphore */
    osSemaphoreDef(osSem);

    MenuEvent = osSemaphoreCreate(osSemaphore(osSem), 1);
    //*pdata = 10;


    /* Force menu to show Item 0 by default */
    osSemaphoreRelease(MenuEvent);

    /* Menu task */
    osThreadDef(Menu_Thread, MSC_MenuThread, osPriorityHigh, 0,
                8 * configMINIMAL_STACK_SIZE);
    osThreadCreate(osThread(Menu_Thread), NULL);
}

SP值为0x20001198,查看堆栈里面的值依次为R0~R3、R12、LR、PC、XPRS, 例如R0(10 27 00 00),  显然堆栈后第21个字节到24字节即为LR,该地址0x08003B61即为异常前PC将要执行的下一条指令地址(即StackFlow()后面的语句处 RCC->CR &= (uint32_t)0xFFFBFFFF)
 

【STM32】hardfault定位方法【STM32】hardfault定位方法

【STM32】hardfault定位方法

 

2. 案例2

发生异常后我们可以首先查看LR寄存器的值,确认当前使用的堆栈是MSP还是PSP,然后找到相对应的堆栈指针,并在内存中查看相对应堆栈的内容,内核将R0~R3,R12,LR,PC,XPRS寄存器依次入栈,其中LR即为发生异常前PC将要执行的下一条指令地址。

那么Cortex-M3 内核HardFault错误调试定位方法有:

2.1 方法1   如何精确定位出问题代码的所在位置:


以访问越界为例:(对STM32F103C8T6内部flash模拟EEPROM)

#define STM32_FLASH_SIZE 64                                         

#define STM32_FLASH_WREN 1           

#define FLASH_SAVE_ADDR  0X08078000            

#define FLASH_HIS_ADDR  0X08078002      

...

       FLASH_SAVE_ADDR是开始存储的基地址,STM32F103C8T6内部flash大小是64K,在STM32的内部闪存(FLASH)地址起始于0x08000000,一般情况下,程序就从此地址开始写入。因此STM32F103C8T6的结束地址应该是64*1024转换成16进制后加上单片机flash的基地址得到的结果就是0x08010000,那么以上代码设置了FLASH_SAVE_ADDR为0X08078000已经超出了该单片机的范围,因此如果在用此单片机操作flash是如果对这个地址进行写和读的会发生错误。现在假设你在不知情的状况下对这个地址进行了操作,然后程序运行时进入

HardFault_Handler中断中。那么要找出错误代码在哪个地方,可以使用以下方法(调试软件MDK):

1:进入调试调试界面在HardFault_Handler的while(1)处打上断点。

3:等待代码运行到此,这时查看LR寄存器的

如果是正常运行那么显示的寄存器类似下图所示:
【STM32】hardfault定位方法

     发生异常之后可首先查看LR寄存器中的值,确定当前使用堆栈为MSP或PSP,然后找到相应堆栈的指针,并在内存中查看相应堆栈里的内容。在Cortex_M3权威指南中可以看到如下图所示:

 【STM32】hardfault定位方法

     由这张图可见,这个按位或的作用是把R14寄存器的低4位设为D,在这个异常返回后进入线程模式,使用线程堆栈PSP,因为任务运行时要确保使用的是线程模式,只有发生中断或异常时,才让系统进入Handle模式并使用MSP。

     在xPortPendSVHandler里之所以没有这一行,是因为在进入这个异常前,系统正在跑任务,使用的就是线程模式和PSP,进入异常后变成Handle模式和MSP,但在异常返回时会自动回到这个异常发生前的模式也就是线程模式与PSP。

在vPortSVCHandler这个函数被调用之前,系统一直是处于Handle模式并使用MSP的。(因为复位后就是Handle模式,因此大部分没用上系统的STM32的工程,都是让STM32处于Handle这个最高模式下运行的。)

      看到LR寄存器中的值是0xFFFFFFFD,因此我应该去看PSP的地址,找到该地址的地址然后如下图所示打开内存,输入上面找到的寄存器地址,右键选择以long型查看地址如下所示:

【STM32】hardfault定位方法

然后查看这个地址向下数六个long地址,为什么是6个long地址呢,因为由于异常发生时,内核将R0~R3、R12、Returnaddress、PSR、LR寄存器依次入栈,其中Return address即为发生异常前PC将要执行的下一条指令地址;

大概是0x08xxxxxx这样开始的即为出错的代码位置,然后可以反汇编查看,如下图所示:
【STM32】hardfault定位方法

【STM32】hardfault定位方法

可以看到是对应的C语言程序是在读FLASH函数中发生了错误,因此可以判断为访问越界的问题。

 

【STM32】hardfault定位方法

 

2.2 方法2:最简单,最明显

在调试状态下,当进入HardFault断点后,菜单栏Peripherals >Core Peripherals >FaultReports打开异常发生的报告,查看发生异常的原因。

【STM32】hardfault定位方法

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

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

【STM32】hardfault定位方法

【STM32】hardfault定位方法

 【STM32】hardfault定位方法

 

2.3 方法3:此方法和方法一大致相同

下面介绍怎么找出程序中的异常。

【STM32】hardfault定位方法

接下来在keil_MDK工程中,编译代码,并debug,之后全速运行,可以看到如下图所示程序进入HardFault异常。

【STM32】hardfault定位方法

如下所示我们找到SP寄存器,0x200045B8即为栈地址,栈里面的值依次为R0~R3、R12、PC(Return address)、xPSR(CPSR或SPSR)、LR。如图我们看到划红线的地方,注意从右往左看。分别为0x0800427D和0x08004BFA。

【STM32】hardfault定位方法

在show code at address中输入0x08004BFA,点击go to即找到出现异常的代码段附近下面要执行的程序

【STM32】hardfault定位方法

我们用同样的方法在show code at address中输入0x0800427D,找到如下代码段

【STM32】hardfault定位方法

可以发现异常代码就在uart_send_noackdata这个函数里,这个函数里我们定义了一个指针,没有给他分配空间便开始使用了。由此我们掌握了第一种查找异常的方法。只要记录栈里面第21~24以及25到28字节的内容即可方便的找到异常代码。下面介绍使用.map文件查找异常。.map文件在keil工程里面随着程序的编译会自动生成。
 

在.map文件里我们查找0x08004BFA,找到了0x08004bd8指示是uart_send_noackdata函数,到此我们找到了异常代码所在的位置。

【STM32】hardfault定位方法

 由此我们知道我们只要找到栈里面PC(Return address)、xPSR(CPSR或SPSR)寄存器里的内存地址便可以找到异常代码。

【STM32】hardfault定位方法

CPSR

当前程序状态寄存器 (Current Program State Register)

SPSR

保存的程序状态寄存器 (Saved Program State Register), 有6个,主要是在处理异常的时候使用.

每一种处理器模式下都有一个专用的物理寄存器作为备份的程序状态寄存器SPSR , 当特定的异常发生时,这个物理寄存器负责保存CPSR当前程序状态寄存器的内容, 当异常处理程序返回时,再将内容恢复到当前程序状态器中,继续向下执行原来程序.

PC

程序计数器,是用来计数的,指示指令在存储器的存放位置,也就是个地址信息.