嵌入式开发中断全解(2)- Hard Fault的诊断方法详解

承接上次的文章,讲几个大家应该都看过下面的几个中断,有的是在启动文件中或者是.c文件中。

注意:上述是ST公司的Stm32芯片


这里的GD32芯片是国产芯片,和stm32类似的操作,代码可以兼容。

1、void NMI_Handler(void)

不可屏蔽中断。主要是两方面触发,一个是外设触发,一个软件设置触发。首先要意识到,所有中断在某些情况下都是可屏蔽的。例如,如果中断控制器完全关闭,则不会向 CPU 传递任何中断。术语不可屏蔽中断实际上涵盖了一类中断,即使“正常”中断被屏蔽,仍可以将其传递给 CPU。NMI 仍然可以被屏蔽,但是通过标准内核代码难以访问的单独控制状态。在某些情况下屏蔽所有中断的能力也存在于其他具有悠久 NMI 历史的架构上。

操作系统软件需要仅在特定情况下被屏蔽的单独中断类别有两个主要原因。第一个原因很简单,中断可能会意外地被禁用,从而使系统处于无响应状态。乍一看,似乎可以轻松编写软件以在禁用中断后始终启用中断,但事实并非如此。现代操作系统内核通常跨越数百万行可以直接操作中断标志的代码。中断处理例程可能会导致同步异常,从而导致非线性代码路径,并可能导致禁用中断的死锁。还要考虑以完全内核权限运行并能够直接操纵 CPU 中断标志​​的第三方驱动程序。应该清楚的是,简单的代码检查或静态分析不足以防止中断被禁用。在一些场景中,能够快速传递中断至关重要,例如调试、跨 PE 同步和热补丁。原文克里斯托弗·达尔。
NMI属于内部中断,并且默认是使能的。NMI对外有一个引脚与之相关联,该引脚的默认功能就是NMI。我们可以知道,当我们使用该引脚用作其它功能的时候,如果把NMI引脚在电路上接地,程序在启动的时候就会触发NMI中断,从而进入到NMI_Handler函数中去,那程序一直卡在这了,后期再说说怎么屏蔽。(主要大家要看看芯片用户手册)。void NMI_Handler(void)函数原型如下。

/*!
    \brief      this function handles NMI exception 
    \param[in]  none
    \param[out] none
    \retval     none
*/
void NMI_Handler(void)
{
}

2、void SVC_Handler(void)
SVC(Supervisor Call)指令用于产生一个SVC异常。它是用户模式代码中的主进程,用于创造对特权操作系统代码的调用。SVC是用于呼叫操作系统所提供API的正道。用户程序只需知道传递给操作系统的参数,而不必知道各API函数的地址。

/*!
    \brief      this function handles SVC exception
    \param[in]  none
    \param[out] none
    \retval     none
*/
void SVC_Handler(void)
{
}

3、void DebugMon_Handler(void)

调试监视器(断点, 数据观察点, 或外部调试请求),大部分debug的时候就是调试的时候遇到.

/*!
    \brief      this function handles DebugMon exception
    \param[in]  none
    \param[out] none
    \retval     none
*/
void DebugMon_Handler(void)
{
}

4、void PendSV_Handler(void)
PendSV是为系统级服务提供的中断驱动。在一个操作系统环境中,当没有其他异常正在执行时,可以使用PendSV来进行上下文的切换。
在进入PendSV处理函数时:
(1)xPSR、PC、LR、R12、R0~R3已经在处理栈中被保存。
(2)处理模式切换到线程模式。
(3)栈是主堆栈。
由于PendSV在系统中被设置为最低优先级,因此只有当没有其他异常或者中断在执行时才会被执行。

/*!
    \brief      this function handles PendSV exception
    \param[in]  none
    \param[out] none
    \retval     none
*/
void PendSV_Handler(void)
{
}

5、void SysTick_Handler(void)
看到这个大家肯定会想到滴答定时器,就是系统定时器 , 以后再分享个系统定时器的内容,在参考手册中, 有这样说法, 当systic为计数为0 则成1外部参考时钟源 21mhz bit[2]为0 内核时钟168MHZ bit[2] 为1 ,使能定时器中断为1,当计数产生为0产生中断,或者将bit[1]0使能位(0关闭,1打开, 系统定时器中断 一般用在操作系统的延时

/*!
    \brief      this function handles SysTick exception
    \param[in]  none
    \param[out] none
    \retval     none
*/
void SysTick_Handler(void)
{
    delay_decrement();
}

以下四种常见的错误异常。

6、void HardFault_Handler(void)
以下异常处理被关闭,而又发生了异常,则触发
• 执行异常处理时,发生了异常,则触发
• 复位时默认使能
传说中的段错误。主要原因两方面:(1)中断问题,例如中断嵌套太多,中断优先级问题,中断标志位没清楚等;(2)堆栈不够或者溢出,例如:当你需要大数组去存取数据,数组就会设置的太大,就会跑飞进入这个中断,这时候你可以把栈堆分配多一点。如何分配堆栈空间?可以先去了解内存布局。

/*!
    \brief      this function handles HardFault exception
    \param[in]  none
    \param[out] none
    \retval     none
*/

void HardFault_Handler(void)
{
    /* if Hard Fault exception occurs, go to infinite loop */
    while(1){
    }
}

硬错误异常是应对系统严重错误而设计的,其优先级为-1,仅次于NMI异常。触发硬错误异常的原因有:
• 调试出错
• 后面三个MemManage_Handler\BusFault_Handler\UsageFault_Handler常规错误异常不能及时响应
• 为响应中断,取中断向量时发生错误
• 仍可通过PRIMASK关闭
注意:

当总线错误异常、存储器管理错误异常、用法错误异常无法及时得到响应时或未被使能时,系统将产生硬错异常。当硬错误异常服务例程又引发新的硬错误异常时,系统将进入死锁状态,只能由复位使其退出.

7、void MemManage_Handler(void)
• 违反MPU设定的存储器访问规则
• 复位时默认未使能
在内核的MCU上写程序时,稍不留神,就可能出现内存溢出的情况。即,数组、指针变量溢出,导致MCU访问内部禁止访问的地址上。这样MCU就会跳转到 错误或者硬件错误中断上去,造成设备死机。也可以在这两个中断中加入强制CPU复位重启,但不能完全解决问题,其实发生存储管理错误中断(MemManage_Handler)后,都发现程序是跳到硬件错误中断(HardFault_Handler)里,说白了又是内存布局问题。

/*!
    \brief      this function handles MemManage exception
    \param[in]  none
    \param[out] none
    \retval     none
*/
void MemManage_Handler(void)
{
    /* if Memory Manage exception occurs, go to infinite loop */
    while(1){
    }
}

存储器管理错误异常是由于违规访问存储器空间或由某些非法访问引发的;通常包括以下类型:
• 访问存储区域时违反了MPU的设置规则
• 越权访问,访问了没有权限访问的地址;
• 访问了没有存储器的地址
• 试图从不可执行区域(XN)执行代码
• 对只读区域进行写操作

使能控制
• SCB->SHCSR. MEMFAULTENA @0xE000ED24

存储器管理错误异常,通常是与MPU相关联的。其诱因往往是违反了MPU设置的访问规则。某些非法访问,如在不可执行的存储器区域取指,也会会引发该异常,既使系统中没有MPU 或MPU被关掉了

发生Memory Management Fault时,可以查看存储器管理错误状态寄存器了解异常的类型
• SCB->CFSR. Memory Manage Fault(MFSR) @0xE000ED28

8、void BusFault_Handler(void)
• 总线错误(Bus Fault)
• 取指令、数据读写、堆栈操作
• 复位时默认未使能

/*!
    \brief      this function handles BusFault exception
    \param[in]  none
    \param[out] none
    \retval     none
*/

void BusFault_Handler(void)
{
    /* if Bus Fault exception occurs, go to infinite loop */
    while(1){
    }
}

当内核通过AHB接口传送数据时收到了一个错误应答信号,则认总线传输错误,进而发起总线错误异常请求;通常异常来自:
• 取指令时发生错误,通常称为“预取指流产”
• 数据读、写时发生错误,也叫“数据流产”
• 响应中断时,出、入栈发生错误
使能控制
• SCB->SHCSR. BUSFAULTENA @0xE000ED24
AHB总线收到”错误”应答,通常有以下诱因:

企图访问无效的存储器区域,常见于企图访问的地址没有存储器
设备未准备就绪,不能进行总线访问
企图发起的传输宽度不被目标设备所支持
在用户权限下程序企图访问特权模式下的设备

发生Bus Fault时,可以查看总线错误状态寄存器了解异常
的大致类型,供异常处理程序分析
• SCB->CFSR.Bus Fault(BFSR) @0xE000ED2

9、void UsageFault_Handler(void)
• 用法错误(Usage Fault)
• 执行未定义指令、非对齐操作、除零
• 复位时默认未使能

/*!
    \brief      this function handles UsageFault exception
    \param[in]  none
    \param[out] none
    \retval     none
*/
void UsageFault_Handler(void)
{
    /* if Usage Fault exception occurs, go to infinite loop */
    while(1){
    }
}

发生用法错误时,可通过查看寄存器了解出错的原因
• SCB->CFSR. Usage Fault(UFSR) @0xE000ED2A

解决方法:
通常一直循环进入某个中断,代码底层没错的前提就打断点,看看是在哪出错,检查内存使用,有没有访问越界,堆栈够不够。或者把可能出错的程序全部屏蔽。看是否再出现内存溢出,如果不再出现了,那就一点点打开屏蔽的代码。这样就可以轻松定位出错的位置上。然后再去分析为什会出现内存溢出。以下就是我这个小白,老是出错的问题:

第一,没有给指针变量赋初始值就开始使用。
第二,在给数组变量copy时,长度超限。如果这个数组是局部变量就会改变了栈中的其它变量和压栈的cpu寄存器,如果数组是,物理上相邻的变量就会被改变。
第三,中断与应用程序对同一指针变量的修改不同步。

(c语言基础一定要牢固啊!!!!以后再分享些c语言学习)

错误异常发生时:
• 上下文(Stack Frame)被入栈保存
• R0~R3, R12, LR,PC,xPSR
• 上下文保存在哪个堆栈中?由此时的LR决定
• LR = EXC_RETURN
• LR.2 = 1,保存在Main Stack,由MSP指向
• LR.2 = 0,保存在Process Stack,由PSP指向

记录发生异常时的现场数据 :
采集现场有助于分析出错原因可以通过串口来看,如下面这段代码.

HardFault_Handler: 
TST LR, #4 
ITE EQ 
MRSEQ R0, MSP 
MRSNE R0, PSP 
B hard_fault_handler_c
void hard_fault_handler_c (unsigned int * hardfault_args) 
{ 
unsigned int stacked_r0; 
unsigned int stacked_r1; 
unsigned int stacked_r2; 
unsigned int stacked_r3; 
unsigned int stacked_r12; 
unsigned int stacked_lr; 
unsigned int stacked_pc; 
unsigned int 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_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]);
printf ("\n\n[Hard fault handler - all numbers in hex]\n"); 
printf ("R0 = %x\n", stacked_r0); 
printf ("R1 = %x\n", stacked_r1); 
printf ("R2 = %x\n", stacked_r2); 
printf ("R3 = %x\n", stacked_r3); 
printf ("R12 = %x\n", stacked_r12); 
printf ("LR [R14] = %x subroutine call return address\n", stacked_lr); 
printf ("PC [R15] = %x program counter\n", stacked_pc); 
printf ("PSR = %x\n", stacked_psr); 
printf ("BFAR = %x\n", (*((volatile unsigned long *)(0xE000ED38)))); 
printf ("CFSR = %x\n", (*((volatile unsigned long *)(0xE000ED28)))); 
printf ("HFSR = %x\n", (*((volatile unsigned long *)(0xE000ED2C)))); 
printf ("DFSR = %x\n", (*((volatile unsigned long *)(0xE000ED30)))); 
printf ("AFSR = %x\n", (*((volatile unsigned long *)(0xE000ED3C)))); 
printf ("SCB_SHCSR = %x\n", SCB->SHCSR); while (1); 
}

俗话说:复习是进步他爹!!总结是进步他舅!!
单片机中断分为内部中断和外部中断两大类,外部中断由单片机外部设备产生,中断产生后通过单片机的外部管脚传递给单片机,传递这个中断信号最简单的方法就是 规定单片机的管脚在什么状态下有外部中断产生,这样单片机通常是有一个或多个IO口,当在输入状态时可以用来检测外部中断信号。嵌入式开发中断全解(1)
文章很长,能看到这也希望都能学到些东西,努力成为"栈溢出"工程师 ! ! ! 加油 !

物联沃分享整理
物联沃-IOTWORD物联网 » 嵌入式开发中断全解(2)- Hard Fault的诊断方法详解

发表评论