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.(博主在文章里一步步讲述,非常清晰,让人一看就明白)

应对STM32 Cortex-M3 Hard Fault异常的更多相关文章

  1. STM32 F4xx Fault 异常错误定位指南

    STM32 F407 采用 Cortex-M4 的内核,该内核的 Fault 异常可以捕获非法的内存访问和非法的编程行为.Fault异常能够检测到以下几类非法行为: 总线 Fault: 在取址.数据读 ...

  2. ARM 架构、ARM7、ARM9、STM32、Cortex M3 M4 、51、AVR 之间有什么区别和联系?(转载自知乎)

    ARM架构:  由英国ARM公司设计的一系列32位的RISC微处理器架构总称,现有ARMv1~ARMv8种类. ARM7:       一类采用ARMv3或ARMv4架构的,使用冯诺依曼结构的内核. ...

  3. STM32学习之路入门篇之指令集及cortex——m3的存储系统

    STM32学习之路入门篇之指令集及cortex——m3的存储系统 一.汇编语言基础 一).汇编语言:基本语法 1.汇编指令最典型的书写模式: 标号 操作码        操作数1, 操作数2,... ...

  4. Cortex-M3和Cortex-M4 Fault异常应用之一 ----- 基础知识

    1. 摘要 Cortex-M内核实现了一个高效异常处理模块,可以捕获非法内存访问和数个程序错误条件.本应用笔记从程序员角度描述Cortex-M Fault异常,并且讲述在软件开发周期中的Fault用法 ...

  5. ARM Cortex M3(V7-M架构)硬件启动程序 一

    Cortex-m3启动代码分析笔记 启动代码文件名是STM32F10X.S,它的作用先总结下,然后再分析. 启动代码作用一般是: 1)堆和栈的初始化: 2)中断向量表定义: 3)地址重映射及中断向量表 ...

  6. Implementation of Serial Wire JTAG flash programming in ARM Cortex M3 Processors

    Implementation of Serial Wire JTAG flash programming in ARM Cortex M3 Processors The goal of the pro ...

  7. 【freertos】002-posix模拟器设计与cortex m3异常处理

    目录 前言 posix 标准接口层设计 模拟器的系统心跳 模拟器的task底层实质 模拟器的任务切换原理 cortex M3/M4异常处理 双堆栈指针 双操作模式 栈帧 EXC_RETURN 前言 如 ...

  8. ARM Cortex M3系列GPIO口介绍(工作方式探讨)

    一.Cortex M3的GPIO口特性    在介绍GPIO口功能前,有必要先说明一下M3的结构框图,这样能够更好理解总线结构和GPIO所处的位置. Cortex M3结构框图     从图中可以看出 ...

  9. Cortex-M3和Cortex-M4 Fault异常应用之二 ----- Fault处理函数的实现

    在项目处于调试期间,Fault处理程序可能只是一个断点指令,调试器遇到这个指令后停止程序的运行.默认情况下,由于非硬Fault被禁能,所有发生的非Fault都会上访成硬Fault,因此只要在硬Faul ...

  10. segment fault异常及常见定位手段

    问题背景 最近boot中遇到个用户态程序的segment fault异常,除了一句"Segment fault"打印外无其他任何打印.该问题复现概率较低,定位起来比较棘手.我们的b ...

随机推荐

  1. oauth2.0授权码模式详解

    授权码模式原理 授权码模式(authorization code)是功能最完整.流程最严密的授权模式.它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动. 它 ...

  2. 第11章 Media Queries 与Responsive 设计

    Media Queries--媒体类型(一) 随着科学技术不断的向前发展,网页的浏览终端越来越多样化,用户可以通过:宽屏电视.台式电脑.笔记本电脑.平板电脑和智能手机来访问你的网站.尽管你无法保证一个 ...

  3. 模块—— 序列化模块、random模块、os模块 、 sys模块、hashlib模块、collections模块

    今天我们来说说Python中的模块: 第三方模块 可以下载/安装/使用 第一步:将pip.exe 所在的目录添加到环境变量中第二步:输入pip第三步:pip install 要安装的模块名称  #pi ...

  4. hive配置参数的说明:

    hive.ddl.output.format:hive的ddl语句的输出格式,默认是text,纯文本,还有json格式,这个是0.90以后才出的新配置: hive.exec.script.wrappe ...

  5. Repeat Number(数论)

    Repeat Number 题目描述: Definition: a+b = c, if all the digits of c are same ( c is more than ten), then ...

  6. SQLite入门(二)读写二进制数据

    //读二进制数据的函数 BOOL OpenBinDataFile(BYTE **pBUf,UINT &len) {     if (pBUf == NULL)     {         re ...

  7. java面试题之----jdbc中使用的设计模式(桥接模式)

    1.JDBC(JavaDatabase Connectivity) JDBC是以统一方式访问数据库的API. 它提供了独立于平台的数据库访问,也就是说,有了JDBC API,我们就不必为访问Oracl ...

  8. 微软宣布在Azure上支持更多的开放技术和选择

    微软和我都热爱Linux,并且就在情人节过去几天之后,我非常高兴能用几个激动人心的消息来表达这种对Linux的热爱,您将会看到在Azure上的云部署将具有更加开放的选择性和灵活性. 这些激动人心的消息 ...

  9. devexpress chart 散点图加载并分组显示(可以自定义颜色 同组中的点颜色相同)

    this.dChart.Diagram.Series.Clear();//清空图的内容 var groups = result.GroupBy(itm => itm["flag&quo ...

  10. skype for business server2015部署向导启动服务失败

    命令行执行start-cspool失败 解决: 1.cmd执行servers.msc打开服务列表,将所有skype服务启动,默认是延迟启动 2.用管理员权限打开cmd,而不是普通权限 重新执行启动服务 ...