【理解ARM架构】异常处理

news/2025/2/12 8:30:56/

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

目录

  • ⚡ARM系统中异常与中断处理流程
    • 🍢向量表
    • 🍢保存现场
    • 🍢恢复现场
  • ⚡异常处理
    • 🍢未定义指令异常
    • 🍢SVC异常
    • 🍢SysTick异常
  • ⚡总结

⚡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。

http://www.ppmy.cn/news/1248752.html

相关文章

python 爬虫之 爬取网站信息并保存到文件

文章目录 前期准备探索该网页的HTML码的特点开始编写代码存入文件总的程序文件存储效果 前期准备 随便找个网站进行爬取,这里我选择的是(一个卖书的网站) https://www.bookschina.com/24hour/62700000/ 我的目的是爬取这个网站的这个页面的书籍的名称以…

Redis之秒杀系统

目录 Redis 秒杀 Mysql数据库设计 Mysql秒杀实现 MysqlRedis秒杀实现 秒杀是一种高并发场景,通常指的是在短时间内(秒级别)有大量用户同时访问某个商品或服务,争相抢购的情景。在这种情况下,系统需要处理大量并发请…

滴滴也崩了!

昨日,据国内多家媒体报道,从11月27日晚上开始,滴滴因系统故障导致App服务异常,不显示定位且无法打车。 这次瘫痪事件持续了12个小时,根据滴滴公布的2023年第三季度财报,滴滴出行日均单量达到3130万单&#…

matlab频谱合成音乐《追光者》

选择你喜欢的一首钢琴曲,下载并分析曲谱,用matlab工具用频谱合成方法完成这首曲子的音乐合成。 前言:此文章为个人使用Matlab合成一首《追光者》音乐,且带混响和声效果 文章目录 一.题目二.要求三.课程设计目的四.概要设计五.详细…

Kubernetes - Pod 拉取镜像报错 ImagePullBackOff

问题描述 Pod 拉取镜像报错 ImagePullSecrets 原因分析 若对方容器库也网络正常,但是拉取不下来,一般这种情况是因为没配置授权 Secrets 解决方案 apiVersion: v1 kind: Secret metadata:name: my-docker-reg-secretnamespace: prod data:.dockerconf…

VMware系列:Vmware vSphere常见问题及解决办法

Vmware vSphere常见问题及解决办法 1. 虚拟机文件被锁,无法正常 power on故障状态:祸根:解决方法:2. 忽视掉ESXi/vCenter Server提示SSH事件的方法3. 尝试迁移一台带USB设备的VM失败故障状态:故障分析:解决方案:4. Convert Linux系统的Troublshooting过程5. vCenter Serv…

牛客 算法题 golang语言实现

题目 HJ101 输入整型数组和排序标识,对其元素按照升序或降序进行排序 描述 输入整型数组和排序标识,对其元素按照升序或降序进行排序数据范围: 1 ≤ � ≤ 10001≤n≤1000 ,元素大小满足 0 ≤ � &#…

数组元素积的符号

数组元素积的符号 描述 : 已知函数 signFunc(x) 将会根据 x 的正负返回特定值: 如果 x 是正数,返回 1 。如果 x 是负数,返回 -1 。如果 x 是等于 0 ,返回 0 。 给你一个整数数组 nums 。令 product 为数组 nums 中所有元素值的…