layout: post
title: "异常"
date: 2024-1-16 15:39:08 +0800
tags: Cotex-M3 Cotex-M3权威指南
异常
编号为1-15的对应系统异常,大于等于16的则全是外部中断。除了个别异常的优先级被定死外,其它异常的优先级都是可编程的。
所有能打断正常执行流的事件都称为异常。
在NVIC的中断控制及状态寄存器中,有一个VECTACTIVE位段;另外,还有一个特殊功能寄存器IPSR。在它们二者的里面,都记录了当前正服务的异常,给出了它的编号。
如果一个发生的异常不能被即刻响应,就称它被"悬起"(pending)。CM3则由NVIC的悬起状态寄存器来解决这个问题。于是,哪怕设备在后来已经释放了请求信号,曾经的中断请求也不会错失。
优先级定义
优先级的数值越小,则优先级越高。CM3支持中断嵌套,使得高优先级异常会抢占(preempt)低优先级异常
原则上,CM3支持3个固定的高优先级和多达256级的可编程优先级,并且支持128级抢 占(128的来历请见下文分解------译注)。但是,绝大多数CM3芯片都会精简设计,以致实际上支持的优先级数会更少,如8级,16级,32级等。
- 使用MSB的原因
通过让优先级以MSB对齐,可以简化程序的跨器件移植。比如,如果一个程序早先在支持4位优先级的器件上运行,在移植到只支持3位优先级的器件后,其功能不受影响。但若是对齐到LSB,则会使MSB丢失,导致数值大于7的低优先级一下子升高了,甚至会发生"优先级反转":使它高于小于等于7的优先级。如,8号优先级因为损失了MSB,现在反而变成0号了;而15号优先级则变成7号优先级,它则不会影响0-6号优先级,使得这个问题更隐蔽。
子优先级和抢占优先级
MSB所在的位段(左边的)对应抢占优先级,而LSB所在的位段(右边的)对应子优先级
NVIC中有一个寄存器是"应用程序中断及复位控制寄存器"
0xE000_ED0C
子优先级至少是1个位。因此抢占优先级最多是7个位,这就造成了最多只有128级抢占的现象
CM3允许从比特7处分组,此时所有的位都表达子优先级,没有任何位表达抢占优先级,因而所有优先级可编程的异常之间就不会发生抢占------相当于在它们之中除能了CM3的中断嵌套机制。当然还有凌驾于法律之上的三位老大:复位,NMI和硬fault。它们无论何时出现,都立即无条件抢占所有优先级可编程的"平民异常"。
虽然[4:0]未使用,却允许从它们中分组。例如,如果优先级组为1,则所有可用的8个优先级都是抢占优先级
向量表
CM3需要定位其服务例程的入口地址。这些入口地址存储在所谓的"(异常)向量表"中。缺省情况下,CM3认为该表位于零地址处,且各向量占用4字节。
NVIC中有一个寄存器,称为"向量表偏移量寄存器"(在地址0xE000_ED08处),通过修改它的值就能重定位向量表。
向量表的起始地址是有要求的:必须先求出系统中共有多少个向量,再把这个数字向上"圆整"到2的整次幂,而起始地址必须对齐到后者的边界上。
中断悬起
当中断输入脚被置为有效(asser)t后,该中断就被悬起。即使后来中断源撤消了中断请求,已经被标记成悬起的中断也被记录下来。到了系统中它的优先级最高的时候,就会得到响应。
但是,如果在某个中断得到响应之前,其悬起状态被清除了(例如,在PRIMASK或FAULTMASK置位的时候软件清除了悬起状态标志),则中断被取消
在一个中断活跃后,直到其服务例程执行完毕,并且返回(亦称为中断退出)后,才能对该中断的新请求予以响应(单实例)。
新请求在得到响应时,亦是由硬件自动清零其悬起标志位。
Fault类型的异常
- 总线Fault
- 存储器Fault
- 用法Fault
- 硬Fault
总线Fault
AHB接口传输数据的时候如果回复了一个错误信号, 会产生一个Fault
欲使能总线fault服务例程,需要在NVIC的"系统Handler控制及状态寄存器"中置位BUSFAULTENA位。要注意的是:在使能之前,总线fault服务例程的入口地址必须已经在向量表中配置好,否则就成了作法自毙------程序可能跑飞。
如果总线fault被除能,或者总线fault是被某同级或更高优先级异常的服务例程引发的,则总线fault被迫成为"硬伤"------上访成硬fault,使得最后执行的是硬fault的服务例程
对于精确的总线fault(见下框说明),肇事指令的地址被压在堆栈中。如果BFSR中的BFARVALID位为1,还可以找出是在访问哪块存储器时产生该总线fault的------该存储器的地址被放到"总线fault地址寄存器(BFAR)"中
在不精确的总线faults中,导致此fault的指令早已完成了。例如,缓冲区写入。启动缓冲区写入的指令不知何时已经执行了,但是写到中途时触发了总线fault。此时,肇事指令早已"逃逸"------在若干个时钟周期就执行过了,而且不能确定是具体几个周期之前,CM3也不会记录这期间的程序跳转动作。因此无法确认"肇事者",故而该fault是不精确的。精确的总线fault则不同,它是被最后一个完成的操作触发的。例如,一个存储器读取导致的fault总是精确的,因为该指令必须等全部读完时才算执行完成。这样,任何在读取过程中发生的fault总能落在该指令的头上。
BFSR寄存器的程序员模型如下所示:它是一个8位的寄存器,并且可以使用字传送和字节传送来读取它。如果以字方式访问,地址是0xE000_ED28,并且第2个字节有效;如果以字节方式访问,则地址直接就是0xE000_ED29,
存储器管理Fault
其诱因常常是某次访问触犯了MPU设置的保护规范。在不可执行的存储器区域试图取指,也会触发一个MemManage fault
如果MemMange fault是被同级或高优先级异常的服务例程引发的,或者MemManage fault被除能,则和总线fault一样:上访成硬fault,最终执行的是硬fault的服务例程
如果硬fault服务例程或NMI服务例程的执行也导致了MemManage fault,那就不可救要了------内核将被锁定。
在NVIC"系统handler控制及状态寄存器"中的使能位是MEMFAULTENA
用法Fault
如果需要严格要求程序的质量,还可以让CM3在遇到除数为零的时候,以及遇到未对齐访问的时候也产生用法fault。在NVIC中有两个控制位分别与它们对应。通过设置这两个控制位,就可以激活它们。
没有使能的时候会导致结果和上面一样
在NVIC"系统handler控制及状态寄存器"中的使能位是USGFAULTENA。
NVIC中有一个"用法fault状态寄存器(UFSR)
硬Fault
是上文讨论的总线fault、存储器管理fault以及用法fault上访的结果
另外,在取向量(异常处理时对异常向量表的读取)时产生的总线fault也按硬fault处理。
fault状态寄存器(HFSR),它指出产生硬fault的原因。
应对Fault
复位。这也是最后一招。通过设置NVIC"应用程序中断及复位控制寄存器"中的VECTRESET位,将只复位处理器内核而不复位其它片上设施。取决于芯片的复位设计,有些CM3芯片可以使用该寄存器的SYSRESETREQ位来复位。这种只限于内核中的复位不会殃及其它的系统部件。
恢复:在一些场合下,还是有希望解决产生fault的问题的。例如,如果程序尝试访问了协处理器,可以通过一个协处理器的软件模拟器来解决此问题------当然是以牺牲性能为代价的,要不然还要硬件加速干啥。
中止相关任务:如果系统运行了一个RTOS,则相关的任务可以被终结或者重新开始。
各个fault状态寄存器(FSRs)都保持住它们的状态,直到手工清除。Fault服务例程在处理了相应的fault后不要忘记清除这些状态,否则如果下次又有新的fault发生,服务例程在检视fault源时,又将看到早先已经处理的fault遗留下来的状态标志。此时,将无法判断哪个fault是新发生的。FSRs采用一个写时清除机制(写1时清除)。芯片厂商也可以再添加自己的FSR,以表示其它fault情况。
SVC和PendSV
SVC: 系统服务中断
PendSV: 可悬起系统调用
SVC函数
SVC用于产生系统函数的调用请求。例如,操作系统通常不让用户程序直接访问硬件,而是通过提供一些系统服务函数,让用户程序使用SVC发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。
使用户程序从控制硬件的繁文缛节中解脱出来,而是由OS负责控制具体的硬件
OS的代码可以经过充分的测试,从而能使系统更加健壮和可靠
它使用户程序无需在特权级下执行,用户程序无需承担因误操作而瘫痪整个系统的风险
通过SVC的机制,还让用户程序变得与硬件无关,因此在开发应用程序时无需了解硬件的操作细节,从而简化了开发的难度和繁琐度,并且使应用程序跨硬件平台移植成为可能
SVC异常通过执行"SVC "指令来产生。该指令需要一个立即数,充当系统调用代号。SVC异常服务例程稍后会提取出此代号,从而获知本次调用的具体要求,再调用相应的服务函数
assembly
SVC 0x3 ; 调用3号系统服务
上次执行的SVC指令地址可以根据自动入栈的返回地址计算出。找到了SVC指令后,就可以读取该SVC指令的机器码,从机器码中萃取出立即数,就获知了请求执行的功能代号。
如果用户程序使用的是PSP,服务例程还需要先执行MRS Rn, PSP指令来获取应用程序的堆栈指针。通过分析LR的值,可以获知在SVC指令执行时,正在使用哪个堆栈
我们不能在SVC服务例程中嵌套使用SVC指令, 这种作法会产生一个用法fault。同理,在NMI服务例程中也不得使用SVC,否则将触发硬fault。
对于SVC异常来说,若因优先级不比当前正处理的高,或是其它原因使之无法立即响应,将上访成硬fault
PendSV函数
PendSV则不同,它是可以像普通的中断一样被悬起的(不像SVC那样会上访)。
OS可以利用它"缓期执行"一个异常------直到其它重要的任务完成后才执行动作。悬起PendSV 的方法是:手工往NVIC的PendSV悬起寄存器中写1。悬起后,如果优先级不够高,则将缓期等待执行。
如果OS在某中断活跃时尝试切入线程模式,将触犯用法fault异常。
期的OS大多会检测当前是否有中断在活跃中,只有在无任何中断需要响应时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它可以把任务切换动作拖延很久(因为如果抢占了IRQ,则本次SysTick在执行后不得作上下文切换,只能等待下一次SysTick异常),尤其是当某中断源的频率和SysTick异常的频率比较接近时,会发生"共振",使上下文切换迟迟不能进行
PendSV异常会自动延迟上下文切换的请求,直到其它的ISR都完成了处理后才放行。为实现这个机制,需要把PendSV编程为最低优先级的异常。