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中断来保存当前任务的上下文,并恢复下一个任务的上下文。

相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习
im_AMBER5 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J5 天前
从“Hello World“ 开始 C++
c语言·c++·学习