【理解ARM架构】异常处理

🐱作者:一只大喵咪1201
🐱专栏:《理解ARM架构》
🔥格言:你只管努力,剩下的交给时间!
图

⚡ARM系统中异常与中断处理流程

图
如上图所示arm系统中异常与中断的硬件框图,左侧的按键,定时器其他等等被叫做中断源,它们发出的中断汇聚到中断控制器,也就是NVIC,再由中断控制器将中断发信号给CPU,告诉它发生了那些紧急情况,CPU会中断当前正在执行的代码去处理中断。

除了中断,异常也可以打断CPU的运行,如上图所示右侧框中:

  • 指令不对
  • 数据访问有问题
  • reset信号

等等情况,这些都可以打断CPU运行,这些都属于异常

  • 中断属于一种异常。

ARM系统中处理异常与中断的重点在于保存现场以及恢复现场,中断的使用过程如下:

  • 初始化

    • 设置中断源,让它可以产生中断
    • 设置中断控制器(可以屏蔽某个中断,优先级)
    • 设置CPU总开关,使能中断
  • 执行其他程序:正常程序

  • 产生中断,举例:按下按键—>中断控制器—>CPU

  • cpu每执行完一条指令都会检查有无中断/异常产生

  • 发现有中断/异常产生,开始处理:

    • 保存现场
    • 分辨异常/中断,调用对于异常/中断的处理函数
    • 恢复现场

🍢向量表

不同的芯片,不同的架构,在这方面的处理稍有差别。先来认识一下向量表。向量,在数学定义里是有方向的量,在程序里可以认为向量就是一个数组,里面有多个项,在ARM架构里,对于异常/中断,它们的处理入口函数会整齐地排放在向量表中。

tu
如上图所示,我们在使用CubeMX或者固件库创建好的工程中,在start.s中存在一个向量表__Vectors,其中上面的蓝色框中是处理异常的入口地址,下面的蓝色框中是处理中断的入口地址。

板子上电以后,从__Vectors处的第一个DCD处执行,这里是设置栈顶的,__initial_sp就是栈顶的地址。然后再执行第二个DCD处的Reset_Handler,我们的main函数等就放在这里。

cortex M3/M4:

M3/M4的向量表中,放置的是具体异常/中断的处理函数的地址。比如发生Reset异常时,CPU就会从向量表里找到第1项,得到Reset_Handler函数的地址,跳转去执行。

比如发生EXTI Line 0中断时,CPU就会从向量表里找到第22项,得到EXTI0_IRQHandler函数的地址,跳转去执行。

cortex A7:

tu

如上图所示A7的向量表中,放置的是某类异常的跳转指令。比如发生Reset异常时,CPU就会从向量表里找到第0项,得到b reset指令,执行后就跳转到reset函数。

比如发生任何的中断时,CPU就会从向量表里找到第6项,得到ldr pc, _irq指令,执行后就跳转到_irq函数。

🍢保存现场

在跳转到向量表执行入口函数之前,先要保存现场,也就是将CPU中寄存器中的值先保存下来。

为什么要保存现场?

tu
如上图所示代码示意图,任何程序,最终都会转换为机器码,上述C代码可以转换为右边的汇编指令。

对于这4条指令,它们可能随时被异常打断,怎么保证异常处理完后,被打断的程序还能正确运行?

  • 这4条指令涉及R0、R1寄存器,程序被打断、恢复运行时,R0、R1要保持不变。
  • 执行完第3条指令时,比较结果保存在程序状态寄存器里,程序被打断、恢复运行时,程序状态寄存器要保持不变。
  • 这4条指令,读取a、b内存,程序被打断、恢复运行时,a、b内存要保持不变

内存保持不变,这很容易实现,程序不越界就可以。所以,关键在于R0、R1、程序状态寄存器要保持不变(当然不止这些寄存器),因为这些寄存器在中断中也有可能用到,此时就会改变原本的值。

图
如上图所示,在ARM处理器中有这些寄存器,而且在ARM中有个ATPCS规则(ARM-THUMB procedure call standard(ARM-Thumb过程调用标准)约定R0-R15寄存器的用途。

图
如上图所示,R0-R3用在调用者和被调用者之间传参数,R4~R11在被调用者(函数)内使用,R12~R15是特殊用途的寄存器,还有一个程序状态寄存器,对于M3/M4它被称为XPSR

  • 保存现场就是在保存R0~R15以及XPSR寄存器。

在发生异常/中断后,在处理异常/中断前,需要保存现场,难道需要保存所有这些寄存器吗?不是的。

  • 这些这些寄存器被拆分成2部分:调用者保存的寄存器(R0-R3,R12,LR,PSR)被调用者保存的寄存器(R4-R11)

怎么理解呢?(R0-R3,R12,LR,PSR)这些寄存器是用来传参或者保存返回地址的,调用者主动将这些寄存器给被调用者直接使用,站在被调用者的角度,它认为它得到了允许,既然是你让我用的,那我就随便用了。

站在调用者的角度,就有责任来保证自己不受影响,所以在给被调用者使用之前,需要将这些寄存器的值保存起来,调用结束以后方便将值恢复到这些寄存器中。

(R4-R11)这些寄存器被调用者在使用的时候,并没有得到调用者的允许,所以它在使用之前有责任将这些寄存器原本的值保存起来,在使用完毕后再将值恢复到寄存器中,以防影响到调用者。

  • 所以在处理中断/异常之前,要将R0~R3,R12,LR,XPSR寄存器中的值保存。

保存现场时寄存器中的值保存到哪里呢?

tu
如上图所示,在保存现场时,将调用者要保存的寄存器挨个压栈,高编号寄存器值放在高地址。

  • 在M3/M4中,现场保存是由硬件完成的,我们写程序的不用关心。

  • 异常/中断类型的分辨也是由硬件完成的。

在保存完现场以后,就直接跳转到向量表中对于的处理入口执行对应的处理函数。


🍢恢复现场

图
如上图所示现场保护时栈的情况,在处理函数执行完毕后,它返回LR所指示的位置(普通调用是这样),难道把LR设置为被中断时程序的地址就行了吗?

如果只是返回LR所指示的地方,也就是执行MOV PC, LR,此时程序直接就返回到产生中断/异常的位置开始执行代码了,硬件帮我们保存在栈里的寄存器,怎么恢复?

所以M3/M4在调用异常处理函数前,把LR设置为一个特殊的值,该特殊的值被称为EXC_RETURN

图
如上图所示,该特殊值是一个32位的地址,它具有特别的意义,以后会具体讲解它的意义。

当处理函数执行完毕以后,会执行MOV PC, LR,当PC寄存器的值等于EXC_RETURN时,会触发异常返回机制,简单地说:会从栈里恢复R0-R3,R12,LR,PC,PSR等寄存器。

然后再把栈中红色框中的返回地址赋值给PC寄存器,让程序从产生中断/异常位置继续执行。

  • 恢复现场是由软件触发,硬件恢复的。

所谓软件触发就是我们在处理函数中执行return函数,此时就会触发异常/中断返回机制,由硬件将栈中保存的值恢复到寄存器中。

⚡异常处理

在了解了异常/中断的处理流程以后,来写代码感受一下。继续使用前面的代码。

图
如上图,修改散列文件,让代码段的加载地址和链接地址相等,不再需要代码段重定位,让代码在Flash上运行。

🍢未定义指令异常

所谓未定义指令就是写一条CPU不认识的指令,此时就会出异常,硬件就会让程序跳转到向量表中对应的处理入口,去执行处理函数。

tu

如上图,在向量表中只保留HardFault_HandlerUsageFault_Handler两个异常处理入口,并且声明这两个函数。

tu
如上图,定义HardFault_HandlerUsageFault_Handler两个异常处理函数,在函数里打印一句话,然后陷入死循环。

tu
如上图所示,声明串口初始化函数,然后在执行未定义指令之前初始化串口,否则就无法看到打印的东西了,因为串口还没有初始化就发生了异常。

然后会执行DCD 0XFFFFFFFF未定义指令,此时就会产生异常,这属于一个使用异常,所以应该会去UsageFault_Handler处执行处理函数。

tu
如上图,但是此时从串口助手上看到的是HardFault_Handler,说明执行的是HardFault_Handler处理函数,而不是UsageFault_Handler函数,这是为什么呢?

tu
如上图所示,未定义指令属于"处理器操作相关的错误",如果没有使能Usage Fault",发就会触发Hard Fault,所以上面执行的就是HardFault_Handler处理函数。

为了执行HardFault_Handler处理函数,需要将Usage Fault使能,在M3/M4内核中,有一个用于异常和中断控制的SCB寄存器:

tu
如上图所示SCB寄存器部分位,详细内容在ARM Cortex-M3与Cortex-M4权威指南这本书中有详细接收,该寄存器的基地址是0xE000ED00

TU
如上图,为了访问SCB寄存器方便,将该寄存器使用结构体描述出来。
图
如上图,定义一个函数UsageFaultInit,在里面将SCB寄存器的第18位,也就是SHCSR位置一,在执行未定义指令之前调用该函数,此时就使能了UsageFault

图
如上图,在用法错误异常处理函数UsageFault_Handler中,只打印异常名,不陷入死循环。

图
如上图,此时就会疯狂打印UsageFault_Handler,说明不停的在执行UsageFault_Handler处理函数。为什么会不停执行呢?执行一遍不就可以了吗?

  • 用法错误异常仍然存在,虽然执行了UsageFault_Handler处理函数,但是没有将该异常清除。

tu
如上图,在UsageFault_Handler函数中,先打印出保护现场时,调用者保护的R0~R3,R12,LR,返回地址,XPSR,这七项,它们存在栈中。

图
如上图所示,由于要在UsageFault_Handler函数中打印栈中存放的寄存器值,所以在调用该函数的时候要进行传参,而向量表中存放的入口处理函数指针是没有形参的。

所以重新定义一个入口处理函数UsageFault_Handler_asm,如上图红色框,将该函数放入到向量表中,当发生UsageFault的时候,就会跳转去执行该函数。

在该函数中,通过R0寄存器传参栈顶指针SP,然后再调用我们之前实现的UsageFault_Handler

  • 调用UsageFault_Handler函数的时候不能使用BL指令,因为这是异常处理函数,不能直接返回到LR中的地址处,需要触发恢复现场机制。
  • 所以只能使用B来调用UsageFault_Handler,现场恢复机制不在这里触发。

tu

如上图所示,此时串口仍然疯狂输出,我们截取打印内容中栈里的值,发现在调用UsageFault_Handler处理函数之前的现场保存时,存放到栈中的返回地址是0x08000068,程序执行完处理函数后会返回到这个地址继续执行。

tu
如上图,打开反汇编文件,查看0x08000068地址处的内容,发现该地址处就是那条未定义指令。

也就是说,未定义指令引起异常后调用处理函数,处理完毕以后又回到了异常指令这里,再次执行,再次引发异常,如此反复导致疯狂输出。

tu
如上图所示,在UsageFault_Handler函数中,设置栈中的返回地址,让其指向下一条指令,也就是在调用异常处理函数结束以后,硬件进行现场恢复完成,然后让PC指向未定义指令的下一条指令。

tu
如上图所示,此时程序就能正常执行了。

🍢SVC异常

在ARM指令中,有一条指令:

SVC #VAL

其中,VAL是个立即数,代表着一个编号,当SVC异常产生时,会调用对应编号的处理函数,默认情况下我们只有一个处理函数,所以该值一般填1。

当CPU执行了SVC指令后,会触发一个异常,在操作系统中,比如各类RTOS或者Linux,都会使用SVC指令故意触发异常,从而导致内核的异常处理函数被调用,进而去使用内核的服务(系统调用)。

比如Linux中,各类文件操作的函数openreadwrite,它的实质都是SVC指令。本喵这里不讲解这些,只是看一下SVC异常发生后的现象。

tu

如上图,定义一个SVC_Handler函数来处理SVC异常。

tu
如上图,在启动文件中,将SVC_Handler处理函数放入向量表并且声明,然后在Reset_Handler中执行SVC #1指令产生异常。

tu
如上图所示,此时可以看到,SVC_Handler处理函数被调用了,所以说,产生SVC异常时,会去执行对应的处理函数。

图
如上图所示,先给R0~R3,R12,LR赋值,然后在产生SVC异常后进入处理函数时停下来,查看此时栈中的内容,可以看到,我们原本赋给寄存器中的值此时保存在栈中。

  • 在调用异常处理函数之前,硬件进行了现场保存,将调用者保存的寄存器中的值放到了栈中。

🍢SysTick异常

Cortex-M处理器内部集成了一个小型的、名为SysTick的定时器,也叫做滴答定时器。可以使用它来为操作系统提供系统时钟,也可以把它当做一般的定时器。

它是一个24位的定时器,向下计数,在时钟源的驱动下,计数值到达0时,可以触发SysTick异常。

图
如上图所示SysTick定时器框图,每到了一次时钟信号,VAL计数器就会减一,当减到0以后会产生一次SysTick异常。

然后再自动从LOAD重装载寄存器中读取计数值到VAL中,如此反复产生多次异常。

控制SysTick定时器的寄存器基地址为0xE000E010


tu
如上图所示STCK_CTRL控制寄存器,通过BIT2来选择时钟源,该位是1时选择处理器时钟,也就是晶振直接作为时钟,STM32F103ZET6的晶振频率是8MHZ。

通过BIT1来使能SysTick异常,将该位设置为1,通过BIT0来使能SysTick定时器,将该位设置成1。

图
如上图所示计数器STK_VAL寄存器,其bit0~bit23存放的是计数值,要给它设置一个初始值。

图
如上图所示STK_LOAD重装载寄存器,VAL减为0以后会从这里重新拿值,所以该寄存器的值要设置成和VAL中的值一样。

图
如上图所示SCB_ICSR寄存器,SysTick异常发生以后,需要在处理函数中清除异常,将该寄存器的BIT25设置为1。


图
如上图,为了使用方便,同样将SysTick定时器用到的寄存器用结构体描述出来。

tu
如上图所示,定义一个SysTickInit函数来初始化滴答定时器,将VALLOAD寄存器的值都设置为8000,定时时间为1s,因为晶振时钟频率是8000。

再设置CTRL控制寄存器中的bit0~bi2,全部设置为1,表示选择晶振作为时钟源,使能SysTick异常,使能SysTick定时器。

图
如上图,定义异常处理函数SysTick_Handler,在里面清除SysTick异常,并且打印异常名字。

tu
如上图所示,声明异常处理函数SysTick_Handler,并将其放到向量表中。再声明定时器初始化函数SysTickInit,并在调用mymain之前调用,完成滴答定时器初始化。

tu
如上图,此时每隔一秒钟会产生一次SysTick中断,会调用一次SysTick_Handler异常处理函数。

⚡总结

要清楚异常发生的流程,包括现场保存,分辨异常源且执行相应的处理函数,通过软件触发现场恢复机制。其中现场保存和现场恢复是由硬件完成的,包括异常源的分辨也是。

  • 异常并不会经过中断控制器NVIC。
  • 45
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 25
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只大喵咪1201

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值