文章目录
- 六、异常
- 6.1 异常类型
- 6.2 优先级定义
- 6.3 向量表
- 6.4 中断输入和挂起行为
- 6.5 错误异常
- 6.5.1 总线错误
- 6.5.2 存储器管理错误
- 6.5.3 使用错误
- 6.5.4 硬件错误
- 6.5.5 处理错误
- 6.6 请求管理调用和可挂起的服务调用
六、异常
6.1 异常类型
Cortex-M3内置的异常架构支持多个系统异常和外部中断,编号1-15的异常为系统异常,16以上的则为外部中断输人。多数异常具有可编程的优先级,有些则具有固定优先级。
Cortex-M3芯片的中断输入数量可能会不同(1~240),优先级的数量可能也会有差异,这是由于为了满足不同需要,芯片设计者可以配置Cortex-M3设计的源代码。
如表7.1所示,异常类型1-15为系统异常(异常类型0不存在)。类型16及以上的异常为外部中断输入(见表7.2)。
当前正在运行的异常编号可以通过这两个寄存器体现出来:中断程序状态寄存器(IPSR)以及嵌套向量中断控制器(NVIC)中的中断控制状态寄存器(VECTACTIVE域)。
应该注意的是,这里的中断编号(如中断#0)代表Cortex-M3 NVIC的输入。在实际的微控制器产品或片上系统(SoC)中,外部中断引脚的数量可能与NVIC中断输入的数量不同。例如,编号靠前的几个中断输入可能会被用作内部外设,而外部中断引脚则可能会使用后面的一些中断输入。因此,你需要查看芯片生产商的数据手册,以确定中断的数量。
当已使能的中断产生但不能立即执行时(例如,如果当前正在执行更高优先级的中断或者中断屏蔽寄存器置位),它就会被挂起(有些错误异常例外)。这样在异常执行前就会有一个寄存器用于保存异常请求,这点和传统的ARM处理器不同。之前的处理是,产生中断的设备,如中断请求(IRQ)/快速中断请求(FIQ),需要在它们得到处理前一直保持中断请求。现在,借助NVIC中的挂起寄存器,即便请求源撤销了请求信号,已产生的中断还是会被处理的。
6.2 优先级定义
在Cortex-M3中,异常是否执行以及何时执行都是会受到异常优先级制约的。高优先级(优先级编号较小)异常可以抢占低优先级(优先级编号较大)异常,这是嵌套异常/中断时发生的情况。有些异常(复位、NMI和硬件错误)具有固定的优先级,它们的优先级数值为负,这样就比其他异常的优先级要高,其他的异常具有可编程的优先级。
Cortex-M3支持3个最高的固定优先等级以及多达256个可编程的优先级(最多128个抢占等级)。不过,大多数Cortex-M3芯片支持的优先级数较少,比如8、16和32等。在 Cortex-M3芯片或SoC的设计阶段,设计人员可以选取合适的优先级。通过去除优先级配置寄存器的最低位,可以将优先级数量减小。
例如,如果设计中只实现了3位的优先等级,优先级配置寄存器的情况可以参考图7.1。
由于第0位到第4位没有使用,它们读出始终为0,而对这些位的写操作则会被忽略。按照这种设置,可用的优先级数值为0x00(高优先级)、0x20、0x40、0x60、0x80、0xA0、0xC0、和0xE0(最低)。
类似地,如果设计中使用了4位优先级,优先级配置寄存器的情况如图7.2所示。
如果使用了更多位,可用的优先等级也就更多(见图7.3)。不过,位数的增加也加大了门数量,功耗也会因此变大。对于Cortex-M3,优先级寄存器可以使用的最小数量为3位(8个等级)。
为了简化Cortex-M3设备间的软件移植,寄存器被移出的为最低位而不是最高位。这样,在具有4位优先级配置寄存器的设备上写的程序,就可能运行在3位优先级配置寄存器的设备上。如果移出不是最低位而是最高位,在Cortex-M3设备间移植软件时,我们所得到的优先级就可能同预想的相反。例如,如果应用程序使用的IRQ#0的优先级为0x05、 IRQ#1的优先级为0x03,IRQ#1的优先级应该更高。而当最高两位移出后,IRQ#0的优先级就会变成0x01,这样它的优先级就比IRQ#1的高了。
具有3位、5位和8位异常优先级寄存器的设备上可用的优先级如表7.3所示。
有些读者可能会想知道为什么优先级配置寄存器有8位宽,却只有128个抢占等级?这是因为8位寄存器被进一步分为了两部分:抢占优先级和子优先级。
NVIC中有一个配置寄存器被称作优先级组(NVIC中应用中断和复位控制寄存器的一部分,见表7.4),通过这个寄存器,每个具有可编程优先级的异常的优先级配置寄存器可以分为两个部分,高半部分(左边的位)为抢占优先级,而低半部分(右边的位)则为子优先级(见表7.5)。
抢占优先级决定了处理器已经在运行另外一个中断处理时能否再次产生中断。子优先级数值只用于两个具有相同抢占优先级的异常同时发生时的情况,在这种情况下,具有较高子优先级(数值较小)的异常将会先处理。
由于优先级的分组,抢占等级的最大宽度为7位,这样就有了128个等级。当寄存器组为7时,所有具有可编程优先级的异常的等级相同,并且这些异常间不会发生抢占。而硬件错误、NMI和复位还可以抢占这些异常,因为它们的优先级数值为-1、-2和-3。
在确定有效的抢占优先级和子优先级时,你需要考虑以下因素:
- 已使用的优先级配置寄存器;
- 优先级组设置。
例如,如果配置寄存器的宽度为3(位7到位5可用)并且优先级组被设置为5,这样就有4个抢占优先等级(位7到6位),而且每个抢占等级包含两个子优先级(位5)。
按照图7.4所示的设置,可用的优先等级列在图7.5中。使用相同的设计,如果优先级组被设置为1,这样只能有8个抢占等级而且每个抢占等级中不再包含子优先级(抢占优先级的bit[1:0]始终为0)。优先级配置寄存器的定义如图7.6所示,可用的优先级则可以参考图7.7。
若一个Cortex-M3设备使用了优先级配置寄存器的所有8位,将优先级组设置为0后,抢占等级的最大值仅为128。优先级域的地址如图7.8所示。
若两个中断具有相同的抢占优先级和子优先级,当它们同时被处理器确认时,异常编号较小的中断的优先级更高(IRQ#0的优先级比IRQ#1的高)。
要避免中断优先级被意外修改,在写应用中断和复位控制寄存器(地址0xE000ED0C)时应非常小心。多数情况下,配置完优先级组后,除了产生复位外,就不要使用这个寄存器了(见图7.4)。
6.3 向量表
当异常产生且要被Cortex-M3处理时,处理器需要定位异常处理的起始地址,这个信息存储在存储器中的向量表中。向量表默认位于存储器地址0处,向量地址为向量编号乘4(见表7.6)。
由于地址0处应该为启动代码,且该位置通常为Flash存储器或者ROM设备,因此其数值在运行时不能改变。不过,向量表可以重定位到代码或RAM区域中的其他位置,这样我们可以在运行时修改异常处理。设置NVIC中的向量表偏移寄存器(地址0xE000ED08)可以实现这个目的,地址偏移应该同向量表大小对齐,并且需要扩大为下一个2的整数次方。例如,如果IRQ输入的数量为32,异常总数为32+16(系统异常)=48,将其扩大为2的整数次方64,与4相乘(每个向量4字节)得到256字节(0x100)。因此,向量表偏移应该设置为0x0、0x100和0x200等。向量表偏移寄存器中的内容如表7.7所示。
若应用程序支持异常处理的动态修改,启动映像的开始部分应该包含以下内容(最少):
- 主栈指针的初始值;
- 复位向量;
- NMI向量;
- 硬件错误向量。
由于NMI和硬件错误可能会在启动过程中产生,所以以上内容是必需的。其他的异常则是使能后才会产生。
启动过程完成后,可以将新向量表定义到静态随机访问存储器,并且将向量表重定位到新的位置。
6.4 中断输入和挂起行为
本节描述IRQ输入和挂起行为,这些内容同样适用于NMI输入,只是NMI在多数情沉况会立即执行,除非内核已经在执行NMI处理、被调试器暂停或者由于某些严重的系统错误而处于锁定状态。
当中断输入被确认后,它将会被挂起,这也就意味着它将被置于等待处理器处理请求的状态。即使中断源取消了中断,在优先级允许时,挂起中断状态仍会引起中断处理执行。如图7.9所示,中断处理开始后,挂起状态就会自动清除。
不过,如果在处理器对挂起中断作出回应前,挂起状态已经被清除了(例如,由于 PRIMASK/FAULTMASK被置为1,挂起状态被软件写NVIC中断控制寄存器清除了,中断无法立即执行),该中断就会被取消(见图7.10)。中断挂起状态可以在NVIC中访问,并且是可写的,因此,你可以清除挂起中断或者通过软件设置挂起寄存器。
当处理器开始执行程序时,中断处于活动状态并且挂起位会被自动清除(见图7.11)。当中断处于活动状态时,在中断程序完成及中断返回前(也被称作异常退出,参见第9章),用户无法再次处理相同的中断。之后活动状态会被清除,如果挂起状态为1,中断处理可以再次执行。在中断服务程序结束前可以再次将中断挂起。
若中断源将中断请求信号持续置为有效,在中断服务程序结束时该中断会再次被挂起(见图7.12),这点和传统的ARM7TDMI类似。
如果在处理器开始处理前,中断产生了多个信号脉冲,如图7.13所示,它们会被当做一次中断请求。如果中断被取消后,在中断服务程序处理时又产生了一个脉冲,它会被再次挂起(见图7.14)。
即使中断被禁止了,中断挂起仍然可以发生,挂起的中断在使能后可以触发中断流程。因此,在使能中断前,检查下是否有挂起寄存器置位是很有必要的,因为中断源可能之前就已经被激活从而设置了挂起状态。如果有必要的话,用户在使能中断前可以清除挂起状态。
6.5 错误异常
多个系统异常可以用于错误处理,错误可以分为以下类型:
- 总线错误;
- 存储器管理错误;
- 使用错误;
- 硬件错误。
6.5.1 总线错误
在AHB接口的传输过程中,错误的响应会导致总线错误,它可以发生在以下阶段:
- 取指,通常也被称作指令预取终止;
- 读/写数据,通常也被称为数据终止。
在Cortex-M3中,以下情况会产生总线错误:
- 中断处理开始时栈的PUSH操作,也叫压栈错误;
- 中断处理结束时栈的POP操作,也叫出栈错误;
- 当处理器开始中断流程时读取中断向量地址(取向量)(硬件错误的一种特殊情况)。
当以上这些总线错误产生时(除了取向量),若总线错误异常已使能并且当前没有同优先级或更高优先级的异常在执行,总线错误处理就会被执行。如果总线错误处理使能而同时处理器接收到另外一个具有更高优先级的异常,总线错误异常就会被挂起。最后,如果总线错误处理未使能或者总线错误发生时,处理器正在执行更高或相同优先级的异常处理,这时硬件错误异常就会执行。如果在硬件错误处理执行期间产生了另外一个总线错误,内核会进入锁定状态。
要使能总线错误异常,需要设置NVIC中系统处理控制和状态寄存器的BUSFAULTENA位。设置之前,若向量表已经被重定位在RAM中,应该确认向量表中已经设置了总线错误处理的起始地址。
那么,在处理器进入总线错误处理后怎么才能发现哪里出了问题?NVIC具有多个错误状态寄存器(FSR),其中有一个为总线错误状态寄存器(BFSR)。通过这个寄存器,总线错误处理可以确定引发错误的原因,如数据/指令访问或者中断压栈、出栈操作。
对于确定的总线错误,可以通过压栈的程序计数器找到错误的指令。如果BFSR的 BFARVALID位为1,也可能确定引起错误的存储器位置,该操作可以通过读取另外一个 NVIC寄存器总线错误地址寄存器(BFAR)完成。不过,总线错误可能无法获取到类似的信息,因为在收到错误时,处理器可能已经执行了多条其他指令。
BFSR的编程模型为:8位宽,可以通过对地址0xE000ED29的字节传输访问,或者通过地址0xE000ED28的字传输访问,BFSR在第二个字节(见表7.8)。写1时会清除错误指示位。
6.5.2 存储器管理错误
存储器管理错误可由以下问题引起:存储器访问同MPU的设置冲突,或者即使没有 MPU,某些非法访问也可能会引发错误(例如,试图在不可执行区域执行代码)。
下面为一些常见的MPU错误:
- 访问的存储器区域在MPU设置中未定义;
- 写只读区域;
- 用户态访问只允许特权访问的区域。
当发生存储器管理错误时,如果存储器管理处理已经使能,那么存储器管理错误处理就会执行。如果发生错误的同时产生了另外一个更高优先级的异常,另一个异常就会首先处理而存储器管理错误则会被挂起。若处理器正在运行的异常处理的优先级相同或更高,或者存储器管理错误处理未使能,硬件错误处理就会执行。如果存储器管理错误发生在硬件错误或MI处理执行过程中,处理器内核会进人锁定状态。
和总线错误处理类似,存储器管理错误处理需要被使能。可以通过NVIC的系统处理控制和状态寄存器中的MEMFAULTENA位来进行使能操作。若向量表已经被重定位在 RAM中,应该确认向量表中已经设置了存储器管理错误处理的起始地址。
NVIC的存储器管理错误状态寄存器(MFSR)用以指示引发存储器管理错误的原因,若状态寄存器表明错误为数据访问冲突(DACCVIOL位)或者指令访问冲突(IACCVIOL位),引发错误的代码可以通过压栈的程序计数器定位到。若MFSR中的MMARVALID位置1,也可以通过NVIC中的存储器管理地址寄存器(MMAR)确定引发错误的存储器地址。
MFSR的编程模型如表7.9所示,它是8位宽的,可以通过地址0xE000ED28的字节传输或字传输进行访问,MFSR在最低字节。和其他的FSR相同,错误状态位可以通过写1清除。
6.5.3 使用错误
使用错误可由以下情况引发:
- 未定义的指令;
- 协处理器指令(Cortex-M3不支持协处理器,不过可以通过错误异常机制模拟协处理器,运行为其他Cortex处理器编译的软件);
- 试图切换至ARM状态(软件可以利用该错误机制测试处理器是否支持ARM代码,由于Cortex-M3不支持ARM状态,在试图切换时会产生使用错误);
- 非法的中断返回(链接寄存器内有非法/错误值);
- 使用多加载或存储指令的非对齐访问。
通过设置NVIC中的特定位,也可以在以下情况下产生错误:
- 被零除;
- 任意非对齐存储器访问。
当发生使用错误时,如果使用处理已经使能,那么使用错误处理通常会执行。不过,如果发生错误的同时产生了另外一个更高优先级的异常,使用错误则会被挂起。若处理器正在运行的异常处理的优先级相同或更高,或者使用错误处理未使能,硬件错误处理就会执行。如果使用错误发生在硬件错误或MI处理执行过程中,处理器内核会进入锁定状态。
可以通过NVIC的系统处理控制和状态寄存器中的USGFAULTENA位使能使用错误处理。若向量表已经被重定位在RAM中,则向量表中应该已经首先设置了使用错误处理的起始地址。
NVIC的使用错误状态寄存器(UFSR)表示使用错误的原因,在错误处理内部,引发错误的程序代码也可以通过压栈的程序计数器数值定位。
UFSR如图7.9所示,它占据两个字节,可以通过地址0xE000ED2A的半字传输访问,或者通过地址0xE000ED28的字传输访问,MFSR在高半字。和其他的FSR相同,错误状态位可以通过写1清除。
6.5.4 硬件错误
硬件错误处理可以由使用错误、总线错误以及存储器管理错误引发,前提是这些错误无法执行。另外,它可由取向量过程中的总线错误引发(在异常处理过程中读取向量表)。通过NVIC中的硬件错误状态寄存器可以确定错误是否由取向量引起,若不是,硬件错误处理需要检查其他的FSR以确定硬件错误的原因。
硬件错误状态寄存器(HFSR)的细节如表7.11所示。和其他的FSR相同,错误状态位可以通过写1清除。
6.5.5 处理错误
在软件开发过程中,我们可以使用FSR来确定程序中引起错误的原因并进行修正。各种错误的一般原因在本书附录E疑难解答中进行了总结。在实际运行的系统中,情况是不一样的,在确定了错误的原因后,软件需要决定下一步的处理。在运行OS的系统中,引发错误的任务或应用程序可以被终止,而有些情况下,系统可能需要复位,错误恢复措施取决于目标应用。合理处理错误可以使得产品更加健壮,不过首先要做的还是要防止错误的产生。下面为一些错误处理的方法:
- 复位:可以使用应用中断和复位控制寄存器中的SYSRESETREQ进行复位,除了调试逻辑,系统中的大部分都会复位。根据应用程序的不同,若不想复位整个系统,你可以通过VECTRESET只复位处理器。
- 恢复:有些情况下,引起错误异常的问题是可以解决的,例如,对于协处理器指令的情况,使用协处理器模拟软件可以将问题解决。
- 任务终止:对于运行OS的系统,引发错误的任务可能会被终止,如果有必要的话还可以重新启动。
在手动清除之前,FSR会保持它们之前的状态。错误处理应该处理它们相应的错误状态位,要不然,下次另外一个错误产生时,错误处理会再次被触发而且可能会误认为之前的错误仍然存在并试图再次对其进行处理。FSR采用一种写后清的机制(对需要清除的位写1)。
芯片生产商也可以在芯片中增加其他的FSR,以指示其他的错误情形。AFSR的使用取决于每个芯片设计的需求。
6.6 请求管理调用和可挂起的服务调用
请求管理调用(SVC)和可挂起的服务调用(PendSV)为两个面向软件和操作系统的异常。SVC用于产生系统功能调用,例如,操作系统可以通过SVC提供硬件访问的入口,而不是让用户程序直接访问硬件。当用户程序要使用特定硬件时,它就会使用SVC指令产生 SVC异常,然后操作系统中的异常处理就会执行并且提供用户应用程序请求的服务。按照这种方式,OS可以控制对硬件的访问,而且防止用户应用程序直接操作硬件可以提高系统的健壮性。
由于用户程序无须知道硬件的编程细节,SVC也可以提高软件的可移植性。用户程序只需知道应用程序编程接口(API)函数编号和参数,实际的硬件级的编程由设备驱动处理(见图7.15)。
SVC异常可以通过SVC指令产生,该指令需要一个立即数,这也是一种参数传递方法。之后SVC异常处理可以提取出这个参数并且确定要执行的动作。例如,
对于C语言开发,svc函数(对于ARM RealView C编译器或KEIL ARM微控制器开发套件)可用于产生SVC指令,或者使用其他C编译器中的内联汇编也可以。
当SVC处理执行时,你可以读取压栈的程序计数器数值,然后从这个地址处将指令读出来并且屏蔽掉不需要的位,进而确定SVC指令中的立即数。如果系统在用户应用程序中使用进程栈指针,你可能还需要确定首先使用的是哪个栈。这种情况可以通过进人异常处理时的链接寄存器数值加以确定(第8章会对这个问题进行更加深入的探讨)。
由于Cortex-M3的中断优先级模型,用户无法在SVC处理中使用SVC(由于优先级和当前的相同)。这么做的话会导致使用错误,由于同样的原因,NMI处理或硬件错误处理中也无法使用SVC。
PendSV(可挂起的服务调用)在OS中同SVC一同使用,尽管SVC(通过SVC指令)无法被挂起(调用SVC的应用程序希望所需的任务可以立即执行),PendSV却可以被挂起, OS可以用它将异常挂起,以便其他重要的任务完成后才会执行某个动作。PendSV由对 NVIC中断控制状态寄存器的PENDSVSET位写1产生。
PendSV的典型应用为上下文切换(任务间切换),例如,系统可能具有两个活动任务,上下文切换可以通过以下方式触发:
- 调用SVC函数;
- 系统定时器(SYSTICK)。
下面来看一个简单的例子:系统中有两个任务,并且上下文切换由SYSTICK异常触发(见图7.16)。
如果中断请求发生在SYSTICK异常之前,SYSTICK异常将会抢占IRQ处理。在这种情况下,OS不应再执行上下文切换,否则IRQ处理就会延后,而对于Cortex-M3来说,如果OS在中断活跃时试图切换至线程模式则会引发使用错误(见图7.17)。
要想避免IRQ处理的延迟,有些OS在检测到没有IRQ处理正在执行时会直接进行上下文切换。不过,这样会给任务切换带来一个很长的延迟,特别是中断源的频率同 SYSTICK异常的相近时。
PendSV异常解决了这个问题,它在所有其他IRQ处理完成前不会执行上下文切换。要实现这个目的,PendSV被设置为了最低优先级。若OS检测到某个IRQ处于活动状态(IRQ处理运行并被SYSTICK抢占),它会通过将PendSV异常挂起来延迟上下文切换。图7.18中的上下文切换实例的流程如下:
- 任务调用SVC用于任务切换(如等待某个任务完成);
- OS收到请求,准备上下位切换,并且挂起PendSV异常;
- 当CPU退出SVC时,会立即进入PendSV并执行上下文切换;
- 当PendSV完成后就返回到线程等级,并执行任务B;
- 中断产生并进入中断处理;
- 在运行中断服务程序时,SYSTICK异常产生(用于OS节拍);
- OS执行重要的操作,然后挂起PendSV异常并准备进行上下文切换;
- 当SYSTICK异常退出后,CPU返回中断服务程序;
- 当中断服务程序完成后,PendSV开始并执行实际的上下文切换操作;
- 当PendSV完成后,程序返回到线程模式,此时会返回任务A并进行处理。