ARM Cortex-M3权威指南-中断和异常(2)

中断和异常

它支持16-4-1=11 种系统异常(同步)(保留了 4+1 个档位),外加 240 个外部中断输入(异步)。在 CM3 中取消了 FIQ 的概念(v7 前的 ARM 都有这个 FIQ,快中断请求),这是因为有了更新更好的机制——中断优先级管理以及嵌套中断支持,它们被纳入 CM3 的中断管理逻辑中。因此,支持嵌套中断的系统就更容易实现 FIQ。虽然 CM3 是支持 240 个外中断的,但具体使用了多少个是由芯片生产商决定。 CM3 还有一个NMI(不可屏蔽中断)输入脚。当它被置为有效(assert)时, NMI 服务例程会无条件地执行。 NMI 究竟被拿去做什么,还要视处理器的设计而定。在多数情况下, NMI 会被连接到一个看门狗定时器,有时也会是电压监视功能块,以便在电压掉至危险级别后警告处理器。 

  • 向量表

当 CM3 内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR 的入口地址, CM3 使用了“向量表查表机制”。这里使用一张向量表。向量表其实是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 处必须包含一张向量表,用于初始时的异常分配。 举个例子,如果发生了异常 11(SVC),则 NVIC 会计算出偏移移量是 11x4=0x2C,然后从那里取出服务例程的入口地址并跳入。要注意的是这里有个另类: 0 号类型并不是什么入口地址,而是给出了复位后 MSP 的初值。

CM3允许向量表重定位即从其它地址处开始执行各异常向量。这些地址对应的区域可以是代码区,但也可以是 RAM 区。在 RAM区就可以修改向量的入口地址了。为了实现这个功能, NVIC中有一个寄存器,称为“向量表偏移量寄存器”(在地址 0xE000_ED08 处),通过修改它的值就能定位向量表。

  • 异常堆栈操作

内核还会在异常处理的始末自动地执行 PUSH 与 POP 操作。注意:在寄存器列表中,不管寄存器的序号是以什么顺序给出的,汇编器都将把它们升序排序。然后先 push 序号大的寄存器,所以也就先 pop 序号小的寄存器。如果不按升序写寄存器,也许有些汇编器会给出语法错误。在进入 ESR 时, CM3 会自动把一些寄存器压栈,这里使用的是发生本异常的瞬间正在使用的 SP指针(MSP 或者是 PSP)。离开 ESR 后,只要 ESR 没有更改过 CONTROL[1],就依然使用发生本次异常的瞬间正在使用的 SP 指针来执行出栈操作。

  • 异常复位流程

复位流程:1)从地址 0x0000,0000 处取出 MSP 的初始值。2)从地址 0x0000,0004 处取出 PC 的初始值——这个值是复位向量, LSB 必须是 1。 然后从这个值所对应的地址处取指。

请注意,这与传统的 ARM 架构不同——其实也和绝大多数的其它单片机不同。传统的 ARM 架构总是从 0 地址开始执行第一条指令。它们的 0 地址处总是一条跳转指令。 在 CM3 中,在 0 地址处提供 MSP 的初始值,然后紧跟着就是向量表。 向量表中的数值是 32 位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令。

向量表跟随在 MSP 的初始值之后——也就是第 2 个表目。要注意因为 CM3 是在 Thumb 态下执行,所以向量表中的每个数值都必须把 LSB 置 1(也就是奇数)。正是因为这个原因,图 3.18 中使用0x101 来表达地址 0x100。当 0x100 处的指令得到执行后,就正式开始了程序的执行。在此之前初始化 MSP 是必需的,因为可能第 1 条指令还没来得及执行,就发生了 NMI 或是其它 fault。 MSP 初始化好后就已经为它们的服务例程准备好了堆栈

中断与异常的区别
中断对 CM3 核来说都是“意外突发事件” ——也就是说,该请求信号来自 CM3 内核的外面,来自各种片上外设和外扩的外设,
对 CM3 来说是“异步”的;而异常则是因 CM3 内核的活动产生的——在执行指令或访问存储器时产生, 因此对 CM3 来说是“同步”的。

pending悬起的功能

如果一个发生的异常不能被即刻响应,就称它被“悬起” (pending)。不过,少数 fault 异常是不允许被悬起的。一个异常被悬起的原因,可能是系统当前正在执行一个更高优先级异常的服务例程,或者因相关掩蔽位的设置导致该异常被除能。对于每个异常源,在被悬起的情况下,都会有一个对应的“悬起状态寄存器”保存其异常请求。待到该异常能够响应时,执行其服务例程,这与传统的 ARM 是完全不同的。在以前,是由产生中断的设备保持住请求信号;CM3 则由 NVIC 的悬起状态寄存器来解决这个问题。于是,哪怕设备在后来已经释放了请求信号,曾经的中断请求也不会错失。

中断优先级

CM3支持中断抢占。优先级的数值越小,则优先级越高有3个系统异常:复位, NMI 以及硬fault,它们有固定的优先级,并且它们的优先级号是负数,从而高于所有其它异常。所有其它异常的优先级则都是可编程的,但不能被编程为负数。
如果使用更多的位来表达优先级,则可以使用的值也更多,同时需要的门也更多——带来更多的成本和功耗。 CM3 允许的最少使用位数为 3 个位,亦即至少要支持 8 级优先级。

抢占优先级决定了抢占行为:当系统正在响应某异常 L 时,如果来了抢占优先级更高的异常 H,则 H 可以抢占 L。亚优先级则处理“内务”:当抢占优先级相同的异常有不止一个悬起时,就优先响应亚优先级最高的异常。在计算抢占优先级和亚优先级的有效位数时,芯片实际使用了多少位来表达优先级;优先级组是如何划分的。举个例子,如果只使用 3 个位来表达优先级([7:5]),并且优先级组的值是 5(从比特 5处分组),则你得到 4 级抢占优先级,且在每个抢占优先级的内部有 2 个亚优先级。如果优先级完全相同的多个异常同时悬起,则先响应异常编号最小的那一个

中断处理时序图

当中断输入脚被 assert后,该中断就被悬起。当某中断的服务例程开始执行时,就称此中断进入了“活跃”状态,并且其悬起位会被硬件自动清除,在一个中断活跃后,直到其服务例程执行完毕,并且返回了,才能对该中断的新请求予以响应。当然,新请求的响应亦是由硬件自动清零悬起标志位。

如果中断源咬住请求信号不放,该中断就会在其上次服务例程返回后再次被置为悬起状态。

异常类型

总线faults异常

当 AHB 接口上正在传送数据时,如果回复了一个错误信号(error response),则会产生总线faults,产生的场合可以是,1)取指,通常被称作“预取流产”(prefetch abort);2)数据读/写,通常被称作“数据流产”(data abort)

哪些因素会导致 AHB 回复一个错误信号?
AHB 回复的错误信号会触发总线 fault,诱因可以是:

  1.  企图访问无效的存储器 region。常见于访问的地址没有相对应的存储器。
  2. 设备还没有作好传送数据的准备。比如,在尚未初始化 SDRAM 控制器的时候试图访问 SDRAM。
  3.  在企图启动一次数据传送时,传送的尺寸不能为目标设备所支持。例如,某设备只接受字型数据,却试图送给它字节型数据。
  4.  因为某些原因,设备不能接受数据传送。例如,某些设备只有在特权级下才允许访问,可当前却是用户级。

使能总线fault需要在NVIC中配置BUSFAULTENA位,并且配置好中断向量表。发生总线fault后,NVIC提供了事故原因的状态寄存器,总线fault状态寄存器(BFSR):是在数据访问时,在取指时,还是在中断的堆栈操作时。总线 fault 地址寄存器(BFAR):存储异常指令地址。

数据访问产生的总线 fault可分为精确的总线fault不精确的总线fault:例如缓冲区写入就是不精确总线fault,启动缓冲区写入时指令早已执行,中途触发了总线fault。

存储器管理fault

存储器管理 faults 多与 MPU 有关,其诱因常常是某次访问触犯了 MPU 设置的保护规范。另外,某些非法访问,例如,在不可执行的存储器区域试图取指,也会触发一个 MemManage fault,而且在这种场合下,即使没有 MPU 也会触发 MemMange fault。
MemManage faults 的常见诱因如下所示:
 访问了所有 MPU regions 覆盖范围之外的地址
 访问了没有存储器与之对应的空地址
 往只读 region 写数据
 用户级下访问了只允许在特权级下访问的地址
在 MemManage fault 发生后,如果其服务例程是使能的,则执行服务例程。如果同时还发生了其它高优先级异常,则优先处理这些高优先级的异常, MemManage 异常被悬起。如果 MemMange fault是被同级或高优先级异常的服务例程引发的,或者MemManage fault被除能,则和总线fault一样:上访成硬 fault,最终执行的是硬 fault 的服务例程。如果硬 fault 服务例程或 NMI 服务例程的执行也导致了 MemManage fault,那就不可救要了——内核将被锁定。

为了调查 MemManage fault 的案发现场, NVIC 中有一个“存储器管理 fault 状态寄存器(MFSR)”,它指出导致 MemManage fault 的原因。如果是因为一个数据访问违例(DACCVIOL 位)或是一个取指访问违例(IACCVIOL 位),则违例指令的地址已经被压入栈中。如果还有 MMARVALID位被置位,则还能进一步查出引发此 fault 时访问的地址——读取 NVIC“存储器管理地址寄存器(MMAR)”的值。

用法fault

用法 faults 发生的场合可以是:

  1. 执行了协处理器指令。 Cortex-M3 本身并不支持协处理器,但是通过 fault 异常机制,可以建立一套“软件模拟”的机制,来执行一段程序模拟协处理器的功能,从而可以方便地在其它 Cortex 处理器间移植。
  2. 执行了未定义的指令。同上一点的道理,亦可以软件模拟未定义指令的功能。
  3. 尝试进入 ARM 状态。因为 CM3 不支持 ARM 状态,所以用法 fault 会在切换时产生。软件可以利用此机制来测试某处理器是否支持 ARM 状态。
  4. 无效的中断返回(LR 中包含了无效/错误的值)
  5. 使用多重加载/存储指令时,地址没有对齐。

硬fault

硬 fault 是上文讨论的总线 fault、存储器管理 fault 以及用法 fault 上访的结果。在取向量(异常处理时对异常向量表的读取)时产生的总线 fault 也按硬 fault 处理。在 NVIC中有一个硬 fault 状态寄存器(HFSR),它指出产生硬 fault 的原因。如果不是由于取向量造成的,则硬 fault 服务例程必须检查其它的 fault 状态寄存器,以最终决定是谁上访的。硬 fault 状态寄存器(地址: 0xE000_ED2C):

 应对fault策略

在软件开发过程中,我们可以根据各种 fault 状态寄存器的值来判定程序错误,并且改正它们。
在一个RTOS实时系统中,发生了 Faults 后,如果不加以处理常会危及系统的运行。因此在找出了导致 fault 的原因后,软件必须决定下一步该怎么办。下面就给出一些应付 fault 的常用方法。

  1. 复位:这通常是最后一招。通过设置 NVIC“应用程序中断及复位控制寄存器”中的 VECTRESET位,将只复位处理器内核而不复位其它片上设施。取决于芯片的复位设计,有些 CM3 芯片可以使用该寄存器的 SYSRESETREQ 位来复位。这种只限于内核中的复位不会殃及其它的系统部件。
  2. 恢复:在一些场合下,还是有希望解决产生 fault 的问题的。例如,如果程序尝试访问了协处理器,可以通过一个协处理器的软件模拟器来解决此问题——当然是以牺牲性能为代价的,要不然还要硬件加速干啥。
  3. 中止相关任务:如果系统运行了一个 RTOS,则相关的任务可以被终结或者重新开始。各个 fault 状态寄存器(FSRs)都保持住它们的状态,直到手工清除。 Fault 服务例程在处理了相应的 fault 后不要忘记清除这些状态,否则如果下次又有新的 fault 发生,服务例程在检视fault 源时,又将看到早先已经处理的 fault 遗留下来的状态标志。此时,将无法判断哪个 fault是新发生的。 FSRs 采用一个写时清除机制(写 1 时清除)。芯片厂商也可以再添加自己的 FSR,以表示其它 fault 情况。

CM3 中的 fault 状态寄存器组

SVC和PendSV

SVC用于产生系统函数的调用,目的是不让用户程序直接访问硬件,而是通过一些系统服务函数,让用户程序使用SVC发出对系统服务函数的呼叫请求。好处是:

  1. 解耦用户程序与底层驱动,由os控制具体的硬件,用户程序移植更方便。
  2. 使用特权等级优势,由os操作硬件,提高了系统的健壮性和可靠性。

SVC指令需要一个立即数来充当系统调用代号,从而获取响应的服务函数。在 SVC 服务例程中不能嵌套使用 SVC 指令,因为同优先级的异常不能抢占自身。这种作法会产生一个用法 fault。同理,在 NMI服务例程中也不得使用 SVC,否则将触发硬 fault。

PendSV是让请求缓期执行,当其他中断完毕后再执行,因此PendSV优先级最低。

NVIC控制器与中断控制

NVIC 的寄存器以存储器映射的方式来访问,除了包含控制寄存器和中断处理的控制逻辑之外, NVIC 还包含了 MPU、 SysTick 定时器以及调试控制相关的寄存器。NVIC 的访问地址是 0xE000_E000。

中断配置基础

每个外部中断都在 NVIC 的下列寄存器中“挂号”:

  • 使能与除能寄存器
  • 悬起与“解悬”寄存器
  • 优先级寄存器
  • 活动状态寄存器

另外,下列寄存器也对中断处理有重大影响

  • 异常掩蔽寄存器(PRIMASK, FAULTMASK 以及 BASEPRI)
  • 向量表偏移量寄存器
  • 软件触发中断寄存器
  • 优先级分组位段

中断使能与除能

CM3 中可以有 240 对使能位/除能位,每个中断拥有一对。这 240 个对子分布在 8 对 32 位寄存器中(最后一对没有用完)。欲使能一个中断,你需要写 1 到对应 SETENA 的位中;欲除能一个中断,你需要写 1 到对应的 CLRENA 位中。

地址:SETENAs: xE000_E100 – 0xE000_E11C ; CLRENAs:0xE000E180 - 0xE000_E19C

中断的悬起与解悬

如果中断发生时,正在处理同级或高优先级异常,或者被掩蔽,则中断不能立即得到响应。此时中断被悬起。中断的悬起状态可以通过“中断设置悬起寄存器(SETPEND)”和“中断悬起清除寄存器(CLRPEND)”来读取,还可以写它们来手工悬起中断。

地址:SETPENDs:0xE000_E200 – 0xE000_E21C ; CLRPENDs:0xE000E280 - 0xE000_E29C

优先级

每个外部中断都有一个对应的优先级寄存器,每个寄存器占用 8 bit,但是允许最少只使用最高 3 bit。 4 个相临的优先级寄存器拼成一个 32 位寄存器。如前所述,根据优先级组设置,优先级可以被分为高低两个位段,分别是抢占优先级和亚优先级。优先级寄存器都可以按字节访问,当然也可以按半字/字来访问。

地址:中断优先级寄存器阵列 0xE000_E400 – 0xE000_E4EF    系统异常优先级寄存器阵列 0xE000_ED18 - 0xE000_ED23

活动状态

每个外部中断都有一个活动状态位。在处理器执行了其 ISR 的第一条指令后,它的活动位就被置 1,并且直到 ISR 返回时才硬件清零。

地址 ACTIVE 寄存器族 0xE000_E300_0xE000_E31C

PRIMASK 与 FAULTMASK 特殊功能寄存器

PRIMASK 用于除能在 NMI 和硬 fault 之外的所有异常,它有效地把当前优先级改为 0;FAULTMASK更绝,它把当前优先级改为‐1,这么一来,连硬fault都被掩蔽了。NMI不会被掩蔽。

系统异常中断配置寄存器

各种fault异常的使能是通过系统handler控制及状态寄存器(SHCSR)来实现的。

软件中断

软件中断,能以多种方式产生。最简单的就是使用相应的SETPEND寄存器;而更专业更快捷的作法,则是通过使用软件触发中断寄存器STIR,如表8.8所示。

systick定时器

SysTick定时器被捆绑在NVIC中,用于产生SYSTICK异常(异常号:15)。大多操作系统需要一个硬件定时器来产生操作系统需要的滴答中断,作为整个系统的时基,提供定时器、任务切换等作用。CM3芯片内部都带有一个定时器,软件在不同CM3器件间移植工作得以简化。该定时器的时钟源可以是内部时钟(FCLK,CM3的自由时钟)、或者外部时钟(CM3处理器的STCLK信号)。

校准值寄存器提供了这样一个解决方案:TENMS值为芯片产生10ms中断的默认值。最简单的作法就是:直接把TENMS的值写入重装载寄存器,这样一来,就能做到每10ms来一次 SysTick异常。如果需要其它的SysTick异常周期,则可以根据TENMS的值加以比例计算。

异常的具体行为

异常的响应序列

入栈->取向量->选择堆栈指针MSP/PSP

  • 入栈

响应异常的第一个动作就是要保护现场,CM3自动的将寄存器放到正确的位置。先把PC与xPSR值保存起来就可以更好的启动服务例程指令的预取和执行。根据ARM函数调用约定,保护异常时的中间结果R0-R3以及R12。

Cortex-M3 在进入异常服务例程时,自动压栈了 R0-R3, R12, LR, PSR 和 PC,并且在返回时自动弹出它们,这多清爽!既加速了中断的响应,也再不需要汇编语言代码了

  •  取向量

在数据总线忙着入栈操作时,指令总线在同时从向量表中找出正确的异常向量。

  • 更新寄存器

在入栈和取向量完毕之后执行服务程序之前。会更新SP,PSR、PC、LR等寄存器。

  • 异常返回

异常服务程序执行完成之后,需要一个正式的返回指令,目的是改变PC值。出栈、更新NVIC寄存器。

 嵌套的中断

CM3内核和NVIC全力支持了中断嵌套的功能,。表现在1)自动编排异常优先级,相同或者低优先级的异常无法抢占,只能阻塞,因此对于SVC里面再次SVC会发生fault错误。2)自动入栈和出栈。

风险:如果嵌套过深而且此时住堆栈所剩无几使,很容易发生堆栈被踩的风险,从而导致系统紊乱跑飞。

  • 咬尾中断

当处理器在响应某异常时,如果又发生其它异常,但它们优先级不够高,则被阻塞——这个我们已经知道。那么在当前的异常执行返回后,系统处理悬起的异常时,倘若还是先POP然后又把POP出来的内容PUSH回去,这不是白白浪费CPU时间吗。为了改进这种铺张浪费行为,引入了咬尾中断机制。

晚到的高优先级异常

CM3的中断处理还有另一个机制,它强调了优先级的作用,这就是“晚到的异常处理”。当CM3对某异常的响应序列还处在早期:入栈的阶段,尚未执行其服务例程时,如果此时收到了高优先级异常的请求,则本次入栈就成了为高优先级中断所做的入栈。可见,它虽然来晚了,却还是因优先级高而受到偏袒,低优先级的异常为它“火中取栗”。

异常的返回值

在进入异常服务程序后, LR的值被自动更新为特殊的EXC_RETURN,这是一个高28位全为1的值,只有[3:0]的值有特殊含义,如表9.3所示。当异常服务例程把这个值送往PC时,就会启动处理器的中断返回序列。因为LR的值是由CM3自动设置的,所以只要没有特殊需求,就不要改动它。

中断延迟

在设计实时系统时,必须对中断延迟进行严肃和仔细地估算。在这里,中断延迟的定义是:从检测到某中断请求,到执行了其服务例程的第一条指令时,已经流逝了的时间。在CM3中,若存储器系统够快,且总线系统允许入栈与取指同时进行,同时该中断可以立即响应,则中断延迟是雷打不动的12周期(满足硬实时所要求的确定性)。在与时间赛跑的这12个周期里,处理器内部一直开足马力,进行了入栈、取向量、更新寄存器以及服务例程取指的一系列操作。但若存储器太慢以至于引入等待周期,或者还有其它因素,则会引入额外的延时。反正CM3内核是决不会拖后腿的。

几个特殊情况:

  • 当处理咬尾中断时,省去了堆栈操作,因此切入新异常服务例程的耗时可以短至6周期。
  • 如果在总线接口上还有未完成的(outstanding)数据传送,例如有一个带缓冲的写操作未完成,处理器也只能等待此传送完成。这是迫不得以的——只有这样,才能保证在发生了总线fault时,其服务例程能够安全地抢占其它程序。
  • 当多个中断同时请求时,也会发生中断延迟,这表现在只有优先级最高的得到立即响应。
  • 如果中断被掩蔽(也就是俗称的关中,在多任务系统下满地都有),则在掩蔽期间也会附加中断延迟。

异常响应期间的faults

中断响应的每步骤都可以触发faults。

入栈错误:

  1. 如果在入栈期间引起了总线fault,则本次入栈操作将被强行中止,并且把总线异常悬起或者在允许时立即响应。若除能了总线fault,则此次故障将成为“硬伤”——上访至硬fault。
  2. 在总线fault被使能的情况下,如果它的优先级比正在响应的异常高,则抢占之;否则将悬起直到引起fault的异常执行完毕。如果入栈操作引起MPU访问违例,则产生存储管理fault,并且必须立即执行MemFault服务例程,否则将无条件上访成硬fault。
  3. 入栈是自动完成的,因此不可能产生用法fault。

出栈错误:

如果在中断返回时的出栈期间引起了总线fault,则本次出栈操作将被强行中止,并且把总线异常悬起或立即响应。若除能了总线fault,则此次故障将成为“硬伤”——上访至硬fault。其它情况下,只要总线fault的优先级比当前的高(也包括比当前最深嵌套的优先级高),则可以立即响应。类似地,如果是因MPU访问违例造成的MemManage fault,且MemManage ault的服务例程必须能立即执行,否则无条件硬fault。

取向量期间:

取向量期间若是在取向量期间发生总线fault,则比较罕见,这也是最严重的,因此直接上硬fault。

无效返回时:

如果LR中的EXC_RETURN不是合法的值,则引起用法fault。如果用法fault被除能,也上访成硬fault。

处理器锁定(lockup)

在下列场合会导致锁定

  1. 在硬fault服务程序中产生fault(双重fault)
  2. 在NMI服务程序中产生fault
  3. 在复位序列中产生总线fault

在锁定状态下寄存器和存储器都被“冻结”, PC的值被强制为0xFFFF_FFFx,并且原地打转地定死在那里一直取指。与此同时, CM3的另一条名叫“LOCKUP”的输出信号线将被置为有效,芯片厂商可以检测此信号,并且在系统复位发生器上触发一个复位。因在双重fault下的优先级为-1,NMI优先级为-2,NMI还能响应(再次证明了它的第一优先地位)。然而在NMI服务例程退出后,又回到锁定状态。

如何避免锁定状态

避免不必要的堆栈访问,对于NMI来说,进入NMI常常是在危急关头,比如掉电、短路等硬件故障,这时存储系统可能已经失效,而对于硬fault来说,有可能SP指针已经出错了(干扰,堆栈溢出),再次操作fault会重蹈覆辙。在设计硬fault、总线fault,存储管理fault的服务例程时,要检查下SP的值是否在合理范围,再做后续工作。

posted @ 2021-08-27 11:41  zephyr~  阅读(4908)  评论(0编辑  收藏  举报