网页建站需要多少钱,熊猫代理ip破解版,wordpress启动广告,网站建设调研报告#x1f431;作者#xff1a;一只大喵咪1201 #x1f431;专栏#xff1a;《理解ARM架构》 #x1f525;格言#xff1a;你只管努力#xff0c;剩下的交给时间#xff01; 目录 ⚡ARM系统中异常与中断处理流程#x1f362;向量表#x1f362;保存现场#x1f362;恢… 作者一只大喵咪1201 专栏《理解ARM架构》 格言你只管努力剩下的交给时间 目录 ⚡ARM系统中异常与中断处理流程向量表保存现场恢复现场 ⚡异常处理未定义指令异常SVC异常SysTick异常 ⚡总结 ⚡ARM系统中异常与中断处理流程 如上图所示arm系统中异常与中断的硬件框图左侧的按键定时器其他等等被叫做中断源它们发出的中断汇聚到中断控制器也就是NVIC再由中断控制器将中断发信号给CPU告诉它发生了那些紧急情况CPU会中断当前正在执行的代码去处理中断。
除了中断异常也可以打断CPU的运行如上图所示右侧框中
指令不对数据访问有问题reset信号
等等情况这些都可以打断CPU运行这些都属于异常。 中断属于一种异常。 ARM系统中处理异常与中断的重点在于保存现场以及恢复现场中断的使用过程如下 初始化 设置中断源让它可以产生中断设置中断控制器(可以屏蔽某个中断优先级)设置CPU总开关使能中断 执行其他程序正常程序 产生中断举例按下按键—中断控制器—CPU cpu每执行完一条指令都会检查有无中断/异常产生 发现有中断/异常产生开始处理 保存现场分辨异常/中断调用对于异常/中断的处理函数恢复现场 向量表
不同的芯片不同的架构在这方面的处理稍有差别。先来认识一下向量表。向量在数学定义里是有方向的量在程序里可以认为向量就是一个数组里面有多个项在ARM架构里对于异常/中断它们的处理入口函数会整齐地排放在向量表中。 如上图所示我们在使用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 如上图所示A7的向量表中放置的是某类异常的跳转指令。比如发生Reset异常时CPU就会从向量表里找到第0项得到b reset指令执行后就跳转到reset函数。
比如发生任何的中断时CPU就会从向量表里找到第6项得到ldr pc, _irq指令执行后就跳转到_irq函数。
保存现场
在跳转到向量表执行入口函数之前先要保存现场也就是将CPU中寄存器中的值先保存下来。
为什么要保存现场 如上图所示代码示意图任何程序最终都会转换为机器码上述C代码可以转换为右边的汇编指令。
对于这4条指令它们可能随时被异常打断怎么保证异常处理完后被打断的程序还能正确运行
这4条指令涉及R0、R1寄存器程序被打断、恢复运行时R0、R1要保持不变。执行完第3条指令时比较结果保存在程序状态寄存器里程序被打断、恢复运行时程序状态寄存器要保持不变。这4条指令读取a、b内存程序被打断、恢复运行时a、b内存要保持不变
内存保持不变这很容易实现程序不越界就可以。所以关键在于R0、R1、程序状态寄存器要保持不变(当然不止这些寄存器)因为这些寄存器在中断中也有可能用到此时就会改变原本的值。 如上图所示在ARM处理器中有这些寄存器而且在ARM中有个ATPCS规则(ARM-THUMB procedure call standardARM-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寄存器中的值保存。 保存现场时寄存器中的值保存到哪里呢 如上图所示在保存现场时将调用者要保存的寄存器挨个压栈高编号寄存器值放在高地址。 在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不认识的指令此时就会出异常硬件就会让程序跳转到向量表中对应的处理入口去执行处理函数。 如上图在向量表中只保留HardFault_Handler和UsageFault_Handler两个异常处理入口并且声明这两个函数。 如上图定义HardFault_Handler和UsageFault_Handler两个异常处理函数在函数里打印一句话然后陷入死循环。 如上图所示声明串口初始化函数然后在执行未定义指令之前初始化串口否则就无法看到打印的东西了因为串口还没有初始化就发生了异常。
然后会执行DCD 0XFFFFFFFF未定义指令此时就会产生异常这属于一个使用异常所以应该会去UsageFault_Handler处执行处理函数。 如上图但是此时从串口助手上看到的是HardFault_Handler说明执行的是HardFault_Handler处理函数而不是UsageFault_Handler函数这是为什么呢 如上图所示未定义指令属于处理器操作相关的错误如果没有使能Usage Fault发就会触发Hard Fault所以上面执行的就是HardFault_Handler处理函数。
为了执行HardFault_Handler处理函数需要将Usage Fault使能在M3/M4内核中有一个用于异常和中断控制的SCB寄存器 如上图所示SCB寄存器部分位详细内容在ARM Cortex-M3与Cortex-M4权威指南这本书中有详细接收该寄存器的基地址是0xE000ED00。 如上图为了访问SCB寄存器方便将该寄存器使用结构体描述出来。 如上图定义一个函数UsageFaultInit在里面将SCB寄存器的第18位也就是SHCSR位置一在执行未定义指令之前调用该函数此时就使能了UsageFault。 如上图在用法错误异常处理函数UsageFault_Handler中只打印异常名不陷入死循环。 如上图此时就会疯狂打印UsageFault_Handler说明不停的在执行UsageFault_Handler处理函数。为什么会不停执行呢执行一遍不就可以了吗
用法错误异常仍然存在虽然执行了UsageFault_Handler处理函数但是没有将该异常清除。 如上图在UsageFault_Handler函数中先打印出保护现场时调用者保护的R0~R3,R12,LR,返回地址,XPSR这七项它们存在栈中。 如上图所示由于要在UsageFault_Handler函数中打印栈中存放的寄存器值所以在调用该函数的时候要进行传参而向量表中存放的入口处理函数指针是没有形参的。
所以重新定义一个入口处理函数UsageFault_Handler_asm如上图红色框将该函数放入到向量表中当发生UsageFault的时候就会跳转去执行该函数。
在该函数中通过R0寄存器传参栈顶指针SP然后再调用我们之前实现的UsageFault_Handler。 调用UsageFault_Handler函数的时候不能使用BL指令因为这是异常处理函数不能直接返回到LR中的地址处需要触发恢复现场机制。所以只能使用B来调用UsageFault_Handler现场恢复机制不在这里触发。 如上图所示此时串口仍然疯狂输出我们截取打印内容中栈里的值发现在调用UsageFault_Handler处理函数之前的现场保存时存放到栈中的返回地址是0x08000068程序执行完处理函数后会返回到这个地址继续执行。 如上图打开反汇编文件查看0x08000068地址处的内容发现该地址处就是那条未定义指令。
也就是说未定义指令引起异常后调用处理函数处理完毕以后又回到了异常指令这里再次执行再次引发异常如此反复导致疯狂输出。 如上图所示在UsageFault_Handler函数中设置栈中的返回地址让其指向下一条指令也就是在调用异常处理函数结束以后硬件进行现场恢复完成然后让PC指向未定义指令的下一条指令。 如上图所示此时程序就能正常执行了。
SVC异常
在ARM指令中有一条指令
SVC #VAL其中VAL是个立即数代表着一个编号当SVC异常产生时会调用对应编号的处理函数默认情况下我们只有一个处理函数所以该值一般填1。
当CPU执行了SVC指令后会触发一个异常在操作系统中比如各类RTOS或者Linux都会使用SVC指令故意触发异常从而导致内核的异常处理函数被调用进而去使用内核的服务(系统调用)。
比如Linux中各类文件操作的函数open、read、write它的实质都是SVC指令。本喵这里不讲解这些只是看一下SVC异常发生后的现象。 如上图定义一个SVC_Handler函数来处理SVC异常。 如上图在启动文件中将SVC_Handler处理函数放入向量表并且声明然后在Reset_Handler中执行SVC #1指令产生异常。 如上图所示此时可以看到SVC_Handler处理函数被调用了所以说产生SVC异常时会去执行对应的处理函数。 如上图所示先给R0~R3,R12,LR赋值然后在产生SVC异常后进入处理函数时停下来查看此时栈中的内容可以看到我们原本赋给寄存器中的值此时保存在栈中。 在调用异常处理函数之前硬件进行了现场保存将调用者保存的寄存器中的值放到了栈中。 SysTick异常
Cortex-M处理器内部集成了一个小型的、名为SysTick的定时器也叫做滴答定时器。可以使用它来为操作系统提供系统时钟也可以把它当做一般的定时器。
它是一个24位的定时器向下计数在时钟源的驱动下计数值到达0时可以触发SysTick异常。 如上图所示SysTick定时器框图每到了一次时钟信号VAL计数器就会减一当减到0以后会产生一次SysTick异常。
然后再自动从LOAD重装载寄存器中读取计数值到VAL中如此反复产生多次异常。
控制SysTick定时器的寄存器基地址为0xE000E010。 如上图所示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定时器用到的寄存器用结构体描述出来。 如上图所示定义一个SysTickInit函数来初始化滴答定时器将VAL和LOAD寄存器的值都设置为8000定时时间为1s因为晶振时钟频率是8000。
再设置CTRL控制寄存器中的bit0~bi2全部设置为1表示选择晶振作为时钟源使能SysTick异常使能SysTick定时器。 如上图定义异常处理函数SysTick_Handler在里面清除SysTick异常并且打印异常名字。 如上图所示声明异常处理函数SysTick_Handler并将其放到向量表中。再声明定时器初始化函数SysTickInit并在调用mymain之前调用完成滴答定时器初始化。 如上图此时每隔一秒钟会产生一次SysTick中断会调用一次SysTick_Handler异常处理函数。
⚡总结
要清楚异常发生的流程包括现场保存分辨异常源且执行相应的处理函数通过软件触发现场恢复机制。其中现场保存和现场恢复是由硬件完成的包括异常源的分辨也是。 异常并不会经过中断控制器NVIC。