Cortex-M3架构学习:异常

异常类型

Cortex-M3 在内核水平上搭载了一个异常响应系统,支持为数众多的系统异常和外部中断。其 中,编号为 1-15 的对应系统异常,大于等于 16 的则全是外部中断。
Cortex-M3支持的中断源数目为 240 个,做成芯片后,厂商可按需选择个数,并且优先级的位数也由芯片厂商最终决定。
类型编号为 1-15 的系统异常如下所示,

只看上述表的描述很难知道每个系统异常的作用,本文在后面会做出解释。
外部中断列表如下,

优先级

在 CM3 中,优先级对于异常来说很关键的,它会决定一个异常是否能被掩蔽,以及在未掩蔽的
情况下何时可以响应。
优先级的数值越小,则优先级越高。CM3 支持中断嵌套,使得高优先级异常会抢占(preempt)低优先级异常。
有 3 个系统异常:复位,NMI 以及硬 fault,它们有固定的优先 级,并且它们的优先级号是负数,从而高于所有其它异常。
所有其它异常的优先级则都是可编程的,但是无法设置成负数。
CM3支持3个固定优先级和256个可编程优先级,且支持128级抢占式优先级。
Cortex-M3处理器支持灵活的优先级配置,可以通过实现的位数来划分优先级。3位表示优先级意味着系统总共支持2³ = 8个不同的优先级级别。

在这个优先级寄存器中,通常低5位未实现(未使用),这意味着这些位在硬件层面被固定为零。因此,即使整个寄存器有8位,实际用于描述优先级的位数可能是较少的(例如3位)。当只使用3位来表示优先级时,这些位的值总是对齐到寄存器的高位,表示优先级的编码。
具体的编码如下,

  • 0x00:最高优先级(所有位为0)
  • 0x20:次高优先级(高3位为001)
  • 0x40:优先级略低(高3位为010)
  • 0x60:优先级略低(高3位为011)
  • 0x80:中等优先级(高3位为100)
  • 0xA0:优先级较低(高3位为101)
  • 0xC0:更低优先级(高3位为110)
  • 0xE0:最低优先级(高3位为111)
    备注:CM3的优先级寄存器的MSB(高位)对齐的

抢占优先级和子优先级

Cortex-M3处理器将完整的256个优先级按位划分为抢占优先级子优先级

这种划分方式由NVIC(嵌套向量中断控制器)中的"应用程序中断及复位控制寄存器"(AIRCR寄存器)中的"优先级组"位段决定。

  • 抢占优先级(Preempt Priority):高位部分,控制中断的抢占关系。具有更高抢占优先级的中断可以抢占低优先级中断。
  • 子优先级(Sub Priority):低位部分,当多个中断有相同的抢占优先级时,子优先级决定哪个中断首先被响应。

抢占优先级和子优先级的优先级组的配置如下,

举个例子解释上表,比如当我们选择分组位置为2时,那么八位的优先级寄存器的第7位到第2位表示的是抢占优先级,0位和1位表示子优先级;并且,从上表可以看到,子优先级至少占了1位,所以抢占优先级最多占7位,所以最多为128级。

向量表

Cortex-M3的向量表是一个存储指针(函数入口地址)的数组,位于内存的某个特定地址。向量表中的每个条目是一个32位的地址指针,指向该中断或异常的处理程序函数。表的最开始部分包含两个特殊的条目:

  • 初始堆栈指针(Initial Stack Pointer) :向量表的第一个条目(地址0x00000000)保存处理器复位时的初始堆栈指针值。处理器复位后会从这里读取堆栈指针,并将其加载到主堆栈指针(MSP)寄存器中。
  • 复位向量(Reset Vector) :向量表的第二个条目(地址0x00000004)保存复位异常(Reset)的处理程序地址。当系统复位时,处理器会从这里跳转到启动代码。

在这两个特殊条目之后,向量表的其余部分包含各种异常和中断的处理程序入口地址,如下,

向量表通常位于内存地址0x00000000处。

然而,在Cortex-M3中,可以通过设置NVIC中的向量表偏移寄存器(来改变向量表的基地址。这使得在不同的内存地址区域(如RAM)中存储向量表成为可能。

中断输入及悬起

中断输入

中断输入是由外部设备或系统内部事件引发的一种信号,用来通知处理器发生了某个事件,要求进行处理。Cortex-M3支持多种类型的中断源,如外部GPIO中断、定时器中断、UART中断等。

中断输入信号被触发后,Cortex-M3的嵌套向量中断控制器(NVIC)会根据中断的优先级来决定如何处理。

NVIC通过中断向量表查找中断服务程序(ISR)的入口地址,并执行相应的处理程序。

流程如下,

  • 中断触发:外部信号或内部事件触发中断。
  • 中断悬起:中断控制器将该中断标记为"悬起状态"。
  • 中断处理:当处理器空闲或优先级合适时,NVIC将转移到对应的中断服务程序(ISR)。
  • 中断结束:执行完中断服务程序后,NVIC清除中断悬起标志,并恢复正常程序执行。

中断悬起

中断悬起是指在中断信号输入后,该中断进入等待处理的状态。Cortex-M3的中断系统允许多个中断同时处于悬起状态,并根据优先级进行调度。

  • 多个中断的悬起:如果多个中断同时触发,NVIC会根据中断的优先级来决定哪个中断优先处理。优先级更高的中断将优先被服务,而优先级较低的中断将继续处于悬起状态,直到处理器有空处理它们。
  • 中断嵌套:如果一个中断正在处理过程中,又有更高优先级的中断触发且悬起,处理器会暂停当前的中断处理,转而执行优先级更高的中断。这就是所谓的中断嵌套。
  • 悬起中断的自动清除:通常,NVIC在执行完中断服务程序后,会自动清除该中断的悬起状态。然而,某些中断可能需要手动清除悬起标志,具体取决于外设和应用程序的设计。

Fault异常

总线Fault

总线Fault是指当处理器尝试访问总线上的设备或内存时发生的错误。这类错误通常与外设、内存或总线控制器等硬件组件相关。
AHB 回复的错误信号会触发总线 fault的原因有:

  • 企图访问无效的存储器 region。常见于访问的地址没有相对应的存储器。
  • 访问某个外设时,外设无法正常响应(如无法执行读写操作)。
  • 在企图启动一次数据传送时,传送的尺寸不能为目标设备所支持。例如, 某设备只接受字型数据,却试图送给它字节型数据。
  • 因为某些原因,设备不能接受数据传送。例如,某些设备只有在特权级下 才允许访问,可当前却是用户级。
  • 多主设备同时访问同一总线,导致仲裁冲突。

在发送Fault后,处理方式如下,

  • 处理器进入总线Fault异常,并执行总线Fault处理程序。
  • NVIC中的Bus Fault Status Register(BFSR)会记录具体的Fault原因,帮助开发者调试和定位问题。

总线Fault也分为精确Fault和不精确Fault,如下,

存储器Fault

存储器管理 faults 多与 MPU 有关,其诱因常常是某次访问触犯了 MPU 设置的保护规范。

触发存储器Fault的原因如下,

  • 访问了所有 MPU regions 覆盖范围之外的地址
  • 访问了没有存储器与之对应的空地址
  • 往只读 region 写数据,当代码尝试访问被保护的区域,如只读存储器、不可执行的存储器区域,或特定特权模式下才能访问的区域时
  • 用户级下访问了只允许在特权级下访问的地址
  • Cortex-M3有专门的硬件机制来检测栈溢出。若栈指针超出有效的栈区域(由MPU配置),则会引发存储器访问故障。

和总线 fault 一样,MemManage fault 必须被使能才能正常响应。

存储器管理 fault 状态寄存器(MFSR),地址为0xE000_ED28,内容如下,

备注:在 MemManage fault 发生后,如果其服务例程是使能的,则执行服务例程。如果同时还发生了其它高优先级异常,则优先处理这些高优先级的异常,MemManage 异常被悬起。

用法Fault

用法Fault常见触发原因,

  • 未定义指令: 如果程序执行了无效或未定义的指令(通常是由于代码损坏或编译错误),会触发Usage Fault。

  • 除以零错误: 当程序试图执行整数除以零的操作时,Cortex-M3会产生Usage Fault(除非除零检测被禁用)。

  • 对无效地址的访问: 访问无效地址或者超出合法范围的地址(例如NULL指针)可能导致Usage Fault。

  • 对特权指令的非法访问: 在用户模式下执行特权指令,如修改特权寄存器等,可能会引发Usage Fault。

  • 对协处理器的非法访问: 如果程序试图访问未实现的协处理器指令(Cortex-M3没有FPU,因此对浮点运算单元的非法访问也可能引发此Fault)。

  • 未对齐的内存访问: Cortex-M3内核对某些数据访问要求内存地址对齐。例如,32位访问要求地址是4字节对齐,64位访问需要8字节对齐。如果访问未对齐的地址,可能会导致Usage Fault。

  • 尝试进入 ARM 状态:因为 CM3 不支持 ARM 状态,所以用法 fault 会在切换时产生。

那我们如何捕获用法Fault,可以使用SCB寄存器捕获错误。

在发生Usage Fault时,SCB的CFSR(Configurable Fault Status Register)寄存器会提供详细的错误信息。

UFSR是CFSR(Configurable Fault Status Register)的一部分,位于该寄存器的高16位,负责记录各种与Usage Fault相关的错误。

硬Fault

硬fault 是总线 fault、存储器管理 fault 以及用法 fault 上访的结果。
如果这些 fault 的服务例程无法执行,它们就会成为"硬伤"------上访(escalation)成硬 fault。
在 NVIC 中有一个硬 fault 状态寄存器(HFSR),它指出产生硬 fault 的原因。

SVC和PendSV

在Cortex-M3内核中,SVC(Supervisor Call)PendSV(Pending Supervisor Call)是两个与操作系统任务管理密切相关的异常(异常号分别为11和14),通常用于实现系统调用和任务切换等功能。

SVC

SVC指令是一种特权指令,允许程序从用户模式切换到特权模式以执行系统调用。

它通常用于操作系统内核提供的服务,比如任务管理、内存管理或硬件资源的访问。

在使用操作系统时,应用程序(通常在用户模式下运行)不能直接访问硬件资源,而是通过操作系统提供的接口间接控制硬件。SVC就是这种接口机制的一部分。

SVC工作机制:

  • 用户程序执行一条SVC指令(SVC指令后跟一个立即数,比如0x3,作为系统服务调用的索引)。
  • SVC指令被执行时,处理器自动产生一个SVC异常,这时处理器会切换到特权模式,保存当前上下文,并跳转到操作系统定义的SVC_Handler中断服务例程。
  • SVC_Handler根据传递的立即数,决定要执行的具体系统服务。
  • 处理完用户的系统服务请求后,操作系统返回控制权给用户程序,继续执行剩余的代码。

SVC和操作系统之间的示意图如下,

备注:我们不能在 SVC 服务例程中嵌套使用 SVC 指令(事实上这样做 也没意义),因为同优先级的异常不能抢占自身。这种作法会产生一个用法 fault。同理,在 NMI 服务例程中也不得使用 SVC,否则将触发硬 fault。

PendSV

PendSV是Cortex-M3中的另一种异常,主要用于实现任务切换,通常用于延迟或悬挂执行低优先级的任务。

PendSV不会像SVC一样通过指令触发,而是通过设置相关寄存器触发。例如,操作系统在需要任务切换时,会手动设置PendSV中断悬起位(SCB->ICSR中的PENDSVSET位),来触发PendSV异常。

当需要切换任务时,操作系统会触发PendSV异常,以便保存当前任务状态,并切换到下一个任务。

PendSV的典型应用场景:

  • 任务调度:在RTOS中,PendSV用于触发任务调度器。每次任务切换时,操作系统会触发PendSV,保存当前任务状态并恢复下一个任务的状态。
  • 延迟任务处理:由于PendSV的优先级最低,它通常被用于在中断结束后,处理低优先级的任务或操作系统调度操作。

PendSV异常处理流程:

  1. 操作系统检测到需要任务切换时,设置PendSV。
  2. 当没有其他高优先级中断需要处理时,处理器进入PendSV异常。
  3. 操作系统保存当前任务的上下文任务的寄存器、堆栈指针 ),选择下一个任务,并恢复它的上下文。

备注:PendSV中断的优先级为最小,当然也可以通过NVIC进行修改。

PendSV 的典型使用场合是在上下文切换时(在不同任务之间切换)。

下面我们来看看为什么需要PendSV悬起,来延迟执行上下文切换。
假设有这么一个系统,里面有两个就绪的任务,并且通过 SysTick 异常启动上下文切换。
正常情况如下,

也就是当滴答定时器的时间到达以后,正常进行任务的上下文切换,但是有这样一种情况,就是现在SysTick中断触发的那一刻,处理器正在执行其他中断服务程序(ISR) ,由于滴答定时器中断的优先级一般比外部中断优先级高,那么滴答定时器中断就会抢占外部中断ISR,导致外部中断只能等滴答定时器中断执行完上下文切换在继续进行,但是这是会产生冲突的,因为一般的中断的优先级都比任务切换的优先级高,就会发生一些不好的事(具体的大家可以自行搜索),图解如下,

为了处理这个问题,PendSV中断就可以完美解决这个问题。

当出现上述情况时,滴答定时器的服务函数只会调用PendSV中断,将PendSV悬起,由于PendSV的优先级设置为最低优先级,这时候虽然滴答定时器触发仍然会抢占之前的外部中断(ISR),但是并不会在这期间进行任务切换,因为任务切换选择在PendSV中进行,此时ISR仍然继续执行,确切的说当所有的高优先级中断都执行完后,此时会触发之前悬起的PendSV,并在其中断服务函数中进行上下文切换,很好的避免上下文切换和中断一起进行这个问题。

图解如下,

备注:由于一般中断服务函数中所执行的程序都非常短,所以不必担心在处理PendSV时前面的高优先级中断处理很长时间,任务之间切换不会被显著延迟。

总结

SVC与PendSV的对比

  • SVC(Supervisor Call,系统服务调用) :用于执行系统服务调用,例如创建任务、删除任务、启动调度等。它通过SVC指令触发,可以从用户模式切换到特权模式,执行内核级的操作。

  • PendSV(Pending Supervisor Call,可悬起系统调用):主要用于任务切换。在需要任务切换时,通过触发PendSV中断来保存当前任务的上下文,并恢复下一个任务的上下文。

相关推荐
优思学院27 分钟前
优思学院|如何从零开始自己学习六西格玛?
大数据·运维·服务器·学习·六西格玛黑带·cssbb
LN花开富贵43 分钟前
stm32g431rbt6芯片中VREF+是什么?在电路中怎么设计?
笔记·stm32·单片机·嵌入式硬件·学习
怀九日43 分钟前
C++(学习)2024.9.18
开发语言·c++·学习·面向对象·引用·
一道秘制的小菜44 分钟前
C++第七节课 运算符重载
服务器·开发语言·c++·学习·算法
极术社区1 小时前
ResNeXt学习
开发语言·学习·计算机视觉·php
徳一3 小时前
【pytorch学习笔记,利用Anaconda安装pytorch和paddle深度学习环境+pycharm安装---免额外安装CUDA和cudnn】
pytorch·深度学习·学习
酒饮微醉-3 小时前
论文阅读--Planning-oriented Autonomous Driving(一)
论文阅读·笔记·学习·自动驾驶
GoppViper3 小时前
golang学习笔记28——golang中实现多态与面向对象
笔记·后端·学习·golang·多态·面向对象
南宫理的日知录3 小时前
73、Python之函数式编程:“一行流”大全,人生苦短,我用Python
开发语言·python·学习·编程学习
GISer_Jing3 小时前
CSS学习路线
前端·css·学习