Loading

嵌入式笔记4.3 Cortex-M3与Cortex-M4异常和中断详解

内容来自于《ARM Cortex-M3与Cortex-M4权威指南》一书中的第七章,加上我自己的一些浅薄理解

一、异常和中断的简介

1. 外设中断处理流程

  • 当外设或硬件需要处理器的服务时(产生外设中断),一般会出现下面的流程(中断处理流程):
    1. 外设向处理器发出中断请求。
    2. 处理器挂起当前正在执行的任务。
    3. 处理器执行外设的中断服务例程(ISP)为外围设备提供服务,必要时可选择通过软件清除中断请求。
    4. 处理器恢复以前挂起的任务。
  • 对于 Cortex-M4 处理器 ,当异常被接受后 ,有些寄存 器会被自动保存到栈中,而且也会在返回流程中自动恢复。利用这种机制 ,可以将异常处理写作普通的 C 函数 ,同时也不会带来额外的软件开销。

2. 中断处理与异常处理

​ 所有的 Cortex-M 处理器都会提供一个用于中断处理的嵌套向量中断控制器(NVIC),除了中断请求,还有其他需要服务的事件 ,将其称为“异常”。按照 ARM 的说法,中断也是一种异常。Cortex-M 处理器中的其他异常包括错误异常和其他用于 OS 支持的系统异常(如SVC 指令)。处理异常的程序代码一般被称作异常处理,它们属于已编译程序映像的一部分。

3. 支持的异常和中断数量

​ Cortex-M3 和 Cortex-M4 的 NVIC 支持最多 240 个 IRQ(中断请求)、1 个不可屏蔽中断(NMI)、1个 SysTick(系统节拍)定时中断及多个系统异常。多数 IRQ 由定时器、I/O 端口和通信接口(如 UART 和 PC)等外设产生。NMI 通 常由看门狗定时器或掉电检测器等外设产生 ,其余的异常则是来自处理器内核 ,中断还可以利用软件生成。

二、异常类型

​ Cortex-M 处理器的异常架构具有多种特性,支持多个系统异常和外部中断。编号 1~15 的为系统异常,16 及以上的则为中断输入(处理器的输入,不必从封装上的 I/O 引脚上访问)。 包括所有中断在内的多数异常 ,都具有可编程的优先级,一些系统异常则具有固定的优先级。

​ 不同的 Cortex-M3 或 Cortex-M4 微控制器的中断源的编号(1~240)可能会不同,优先级也可能会有所差异。这是因为为了满足不同的应用需求,芯片设计者可能会对 Cortex-M3 或 Cortex-M4 设计进行相应的配置。

  • 系统异常列表(中断号)(硬件层面)
异常编号 异常类型 优先级 描述
1 复位 -3(最高) 复位
2 NMI -2 不可屏蔽中断(外部 NMI 输入)。
3 硬件错误 -1 所有的错误都可能会引发,前提是相应的错误处理未使能。
4 MemManage 错误 可编程 存储器管理错误,存储器管理单元冲突或访问非法位置。
5 总线错误 可编程 总线错误;通常发生在 AHB 接口接收到来自总线从的错误响应时(如果是指令提取,则也称为预取中止;如果是数据访问,则称为数据中止)。也可能是由其他非法访问引起的。
6 使用错误 可编程 由于程序错误或试图访问协处理器而出现异常(Cortex-M3和Cortex-M4处理器不支持协处理器)。
7~10 保留 NA -
11 SVC 可编程 SuperVisor调用;通常在操作系统环境中使用,以允许应用程序任务访问系统服务。
12 调试监控 可编程 调试监视器;使用基于软件的调试解决方案时,断点、观察点等调试事件的异常。
13 保留 NA -
14 PendSV 可编程 可挂起的服务调用;操作系统通常在上下文切换等过程中使用的异常。
15 SYSTICK 可编程 系统节拍定时器。当其在处理器中存案时,由定时器外设产生。可用于 OS 或简单的定时器外设。
16 外部中断 #0 可编程 可由片上外设或外设中断源产
17 外部中断 #1 可编程 可由片上外设或外设中断源产
... ... ... ...
255 外部中断 #239 可编程 可由片上外设或外设中断源产

对于实际的微控制器产品或片上系统外部中断输入引脚编号同 NVIC 的中断输入编号可能会不一致。


对于使用 CMSIS-Core 的普通编程,中断标识由中断枚举实现,从数值 0 开始(代表中断#0),系统异常的编号为负数。CMSIS-Core 还定义了系统异常处理的名称。

  • CMSIS-Core 异常定义(中断类型(枚举))(软件层面)
异常编号 异常类型 CMSIS-Core 枚举 CMSIS-Core 枚举值 异常处理名
1 复位 - - Reset_Handler
2 NMI NonMaskableInt_IRQn -14 NMI_Handler
3 硬件错误 HardFault_IRQn -13 HardFault_Handler
4 MemManage 错误 MemoryManagement_IRQn -12 MemManage_Handler
5 总线错误 BusFault_IRQn -11 BusFault_Handler
6 使用错误 BusFault_IRQn -10 UsageFault_Handler
11 SVC SVCall_IRQn -5 SVC_Handler
12 调试监控 DebugMonitor_IRQn -4 DebugMon_Handler
14 PendSV PendSV_IRQn -2 PendSV_Handler
15 SYSTICK SysTick_IRQn -1 SysTick_Handler
16 中断 #0 (device-specific) 0 (device-specific)
17 中断 #1 - #239 (device-specific) 1 to 239 (device-specific)

​ CMSIS-Core 访问函数之所以使用另外一种编号系统,是因为这样可以稍微提高部分 API 函数的效率。中断的编号和枚举定义是同设备相关的,它们位于微控制器供应商提供的头文件中,在一个名为 IRQn 的 typedef 段中。CMSIS-Core 中的多个 NVIC 访问函数都会使用这种枚举定义。

CMSIS-Core 枚举指的是 Cortex Microcontroller Software Interface Standard(CMSIS)核心模块中定义的枚举类型。CMSIS 是由 ARM 公司提供的一组软件接口标准,旨在提供一致的、可移植的编程接口,使开发人员能够更容易地编写和移植嵌入式软件。

CMSIS-Core 是 CMSIS 标准的核心模块,包含了对 Cortex-M 处理器核心的通用接口定义。其中包括了处理器的寄存器定义、中断向量表、中断控制函数等。CMSIS-Core 提供了一种通用的方式来访问和控制处理器的功能,使得开发人员可以更加轻松地编写移植性更强的嵌入式软件。

在 CMSIS-Core 中,枚举类型被广泛用于定义各种状态、错误码、中断号等。这些枚举类型使得代码更加清晰和可读,同时提供了一种标准的方式来表示特定的状态或类型。开发人员可以直接使用这些枚举类型,而无需关心具体的实现细节,从而提高了代码的可维护性和可移植性。

三、中断管理简介

​ Cortex-M 处理器具有多个用于中断和异常管理的可编程寄存器,这些寄存器多数位于 NVIC 和系统控制块(SCB)中。实际上,SCB 是作为 NVIC 的一部分实现的,不过 CMSISCore 将其寄存器定义在了单独的结构体中。处理器内核中还有用于中断屏蔽的寄存器(如 PRIMASK、FAULTMASK 和 BASEPRD)。为了简化中断和异常管理,CMSIS-Core 提供了多个访问函数。

SCB 代表系统控制器(System Control Block),它是ARM Cortex-M 处理器中的一个特殊寄存器,用于配置和控制处理器的各种系统级功能。SCB 包含了处理器的重要控制和状态寄存器,例如中断控制器、系统控制和状态寄存器(如特权级别、系统状态标志等)、系统时钟配置等。在嵌入式系统中,程序可以使用 SCB 来配置处理器的行为,例如设置中断优先级、配置异常处理、配置时钟等。

NVIC 和 SCB 位于系统控制空间(SCS),地址从 0xE000E000 开始,大小为 4KB。SCS 中还有 SysTick 定时器 、存储器保护单元(MPU)以及用于调试的寄存器等。该地址区域中基本上所有的寄存器都只能由运行在特权访问等级的代码访问。唯一的例外为软件触发中断寄存器(STIR),它可被设置为非特权模式访问。

  • 常用的基本中断控制 CMSIS-Core 函数
函数 用法
void NVIC_EnableIRQ(IRQn_Type IRQn) 使能外部中断
void NVIC_DisableIRQ(IRQn_Type IRQn) 禁止外部中断
void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority) 设置中断的优先级
void __enable_irq(void) 清除 PRIMASK 使能中断
void __disable_irq(void) 设置 PRIMASK 禁止所有中断
void NVIC_SetPriorityGrouping(uint32_t PriorityGroup) 设置优先级分组配置

如果有必要,还可以直接访问 NVIC 或 SCB 中的寄存器。不过,在将代码从 Cortex-M 处理器移植到另外一个不同的处理器时,这样会限制软件的可移植性。


复位后,所有中断都处于禁止状态 ,且默认的优先级为 0 。在使用任何一个中断之前,需要:

  • 设置所需中断的优先级(该步是可选的)。

  • 使能外设中的可以触发中断的中断产生控制。

  • 使能 NVIC 中的中断。


​ 当触发中断时 ,对应的中断服务程序(ISR)会执行(可能需要在处理中清除外设产生的中断请求)。 可以在启动代码(startup_stm32xxxxx.s)中的向量表内找到 ISR 的名称(函数名) ,启动代码也是由微控制器供应商提供的。ISR 的名称需要同向量表使用的名称一致,这样链接器才能将 ISR 的起始地址放到向量表的正确位置中。

四、优先级定义

​ 对于 Cortex-M 处理器异常是否能被处理器接受以及何时被处理器接受并执行异常处理 ,是由异常的优先级处理器当前的优先级决定的。更高优先级的异常(优先级编号更小)可以抢占低优先级的异常(优先级编号更大),这就是异常/中断嵌套的情形。有些异常(复位、NMI 和 HardFault)具有固定的优先级,其优先级由负数表示,这样,它们的优先级就会比其他的异常高。其他异常则具有可编程的优先级,范围为 0~255。

​ Cortex-M3 和 Cortex-M4 处理器在设计上具有 3 个固定的最高优先级以及 256 个可编程优先级(具有最多 128 个抢占等级(7位)),可编程优先级的实际数量由芯片设计商决定。多数 Cortex-M3 或 Cortex-M4 芯片支持的优先级较少,如 8、16、32 等。这是因为大量的优先级会增加 NVIC 的复杂度,而且会增加功耗降低速度。多数情况下 ,应用程序只需少量的编程优先级。因此,芯片设计人员需要基于目标应用的优先级数量定制处理器设计。优先级的减少是通过去除优先级配置寄存器的最低位(LSB)实现的。 中断优先级由优先级寄存器控制,宽度为 3~8 位。

​ 实际使用的位数越多,可用的优先级就越多。不过,优先级位数多了以后门数也会增加,因此也会加大芯片的功耗。对于 ARMv7-M 架构,宽度最少为 3 位(8 个等级)。 对于 CortexM3 和 Cortex-M4 处理器,所有的优先级寄存器的复位值都为 0

​ 之所以移除优先级寄存器的最低位(LSB)而不是最高位(MSB),因为这样处理的话,在 Cortex-M4 设备间移植软件时会更加容易。按照这种方式,在具有 4 位优先级配置寄存器的设备上写的程序,就可能会在具有 3 位优先级配置寄存器的设备上运行。若移除的是 MSB 而不是 LSB,则在 Cortex-M4 芯片间移植应用程序时优先级的配置可能会相反。

  • 具有 3 位、5 位和 8 位优先级寄存器的设备可能的异常优先级

8 位寄存器会被进一步分为两个部分:分组优先级和子优先级。(备注:早期的技术参考手册和本书之前的版本也会将分组优先级称为抢占优先级)利用系统控制块(SCB)中一个名为优先级分组的配置寄存器(属于 SCB 中的应用中断和复位控制寄存器),每个具有可编程优先级的优先级配置寄存器可被分为两部分。上半部分(左边的位)为分组(抢占)优先级,而下半部分(右边的位)则为子优先级。

  • 不同优先级分组下优先级寄存器中的抢占优先级域和子优先级域定义

在处理器已经在运行一个中断处理时能否产生另外一个中断(抢占),是由该中断的抢占优先级决定的。子优先级只会用在具有两个相同分组优先级的异常同时产生的情形,此时,具有更高子优先级(数值更小)的异常会被首先处理。若两个中断同时被确认,且它们具有相同的分组/抢占优先级和子优先级,则异常编号更小的中断的优先级更高(IRQ #0 的优先级高于 IRQ #1 的)。

由于优先级分组的存在,分组(抢占)优先级的最大宽度为 7,因此也就有了 128 个等级。

若优先级分组被设置为 7,所有具有可编程优先级的异常则会处于相同的等级,这些异常间也不会产生抢占,硬件错误、NMI 和复位则是例外,因为它们的优先级分别为 -1、-2 和 -3,它们可以抢占这些异常。

​ 可以通过 CMSIS-Core 函数来访问优先级分组设置以处理优先级信息。

  • 优先级分组管理的 CMSIS-Core 函数
函数 用法
void NVIC_SetPriorityGrouping(uint32_t PriorityGroup) 设置优先级分组数值
uint32_t NVIC_GetPriorityGrouping(uint32_t PriorityGroup) 获取优先级分组数值
uint32_t NVIC_EncodePriority(uint32_t PriorityGroup, uint32_t PreemptPriority, uint32_t Sub priority) 基于优先级分组、组优先级和子优先级生成优先级数值
void NVIC_DecodePriority (uint32_t Priority, uint32_t PriorityGroup, uint32_t * pPreemptPriority, uint32_t * pSub priority) 提取组优先级和子优先级(结果由修改指针指向的数值后得到)

在确定实际的分组优先级和子优先级时,必须要考虑以下因素:

  • 实际的优先级配置寄存器
  • 优先级分组设置

若配置寄存器的宽度为 3(第 7~5 位可用)且优先级分组为 5,则会有 4 个分组/抢占优先级(第 7~ 6 位),而且每个分组/抢占优先级具有两个子优先级。

对于相同的设计,若优先级分组为 0x01(第 7~2 位为抢占优先级),则只会有 8 个分组优先级且每个抢占等级中没有进一步的子优先级(优先级寄存器的 bit[l : 0] 总是 0)。

​ 为了避免中断优先级被意外修改,在写入应用中断和复位控制寄存器(地址为 0xE000ED0C)时要非常小心。多数情况下,在配置了优先级分组后,若不是要产生一次复位,就不要再使用这个寄存器了。

五、向量表和向量表重定位

​ 当 Cortex-M 处理器接受了某异常请求后,处理器需要确定该异常处理(若为中断则是 ISR)的起始地址。该信息位于存储器内的向量表中,向量表默认从地址 0 开始,向量地址则为异常编号乘 4。向量表一般被定义在微控制器供应商提供的启动代码中。

其实在具体实现中中断向量地址并不是异常编号乘 4,因为软件实现用的是 CMSIS-Core 枚举值,而 CMSIS-Core 枚举值并不是从零开始的,前面系统异常的枚举值是负数,因此计算中断向量地址是使用 CMSIS-Core 枚举值加上系统异常占的位数(#define NVIC_USER_IRQ_OFFSET 16 在 core_cm4.h 中定义)再乘 4。

启动代码中使用的向量表还包含主栈指针(MSP)的初始值,这种设计是很有必要的,因为 NMI 等异常可能会紧接着复位产生,而且此时还没有进行任何初始化操作。

需要注意的是,Cortex-M 处理器中的向量表和 ARM7TDMI 等传统的 ARM 处理器的向量表不同。对于传统的 ARM 处理器,向量表中存在跳转到相应处理的指令,而 Cortex-M 处理器的向量表则为异常处理的起始地址

​ 一般来说,起始地址(0x00000000)处应为启动存储器 ,它可以为 Flash 存储器或 ROM 设备,而且在运行时不能对它们进行修改。不过,有些应用可能需要在运行时修改或定义向量表。为了进行这种处理,Cortex-M3 和 Cortex-M4 处理器实现了一种名为向量表重定位的特性。

​ 向量表重定位特性提供了一个名为向量表偏移寄存器(VTOR)的可编程寄存器。该寄存器将正在使用的存储器的起始地址定义为向量表。注意,该寄存器在 CortexM3 的版本 r2Po 和 r2Pl 间稍微有些不同。对于 Cortex-M3 r2Po 或之前版本,向量表只能位于 CODE 和 SRAM 区域,而这个限制在 Cortex-M3 r2pl 和 Cortex-M4 中已经不存在了。VTOR 的复位值为 0,若使用符合 CMSIS 的设备驱动库进行应用编程,可以通过 SCB-> VTOR 访问该寄存器。要将向量表重定位到 SRAM 区域的开头处,可以使用下面的代码:

  • 复制向量表到 SRAM 开头处(0x20000000)的代码实例
//注意,下面的存储器屏障指令的使用是基于架构建议的,
//Cortex - M3 和 Cortex- M4 并非强制要求
//字访问的宏定义
#define HW32_REG(ADDRESS)	(*((volatile unsigned long *)(ADDRESS)))
#define VTOR_NEW_ADDR	0x20000000
int i;	//循环变量
//在设置 VTOR 前首先将向量表复制到 SRAM
for (i=0; i<48; i++){			//假定异常的最大数量为 48
	//将每个向量表入口从 Flash 复制到 SRAM
	HW32_REG((VTOR_NEW_ADDR + (i<<2))) = HW32_REG((i<<2);)
}
_DMB();	//数据存储器屏障,确保到存储器的写操作结束
SCB->VTOR = VTOR_NEW_ADDR;	//将 VTOR 设置为新的向量 表位置
_DSB();	//数据同步屏除,确保接下来的所有指令都使用新配置

VTOR 是 Cortex-M 系列处理器中的一个寄存器,全称为 Vector Table Offset Register,中文称为向量表偏移寄存器。这个寄存器用于指定中断向量表的起始地址,决定了处理器在发生中断时应该跳转到哪个中断处理函数。
在 ARM Cortex-M 处理器中,中断向量表通常位于内存的起始地址处。通过修改 VTOR 寄存器,可以将中断向量表的起始地址从默认的 Flash 存储器中移动到 RAM 等其他位置,以实现中断处理函数的动态加载或跳转。这在某些应用场景中非常有用,比如在运行时动态更新中断处理程序。
__NVIC_SetVector 函数中,SCB->VTOR 被用来获取中断向量表的起始地址,然后通过计算中断号和偏移量,将给定中断的处理函数地址写入到中断向量表的相应位置。

/**
  \brief   Set Interrupt Vector
  \details Sets an interrupt vector in SRAM based interrupt vector table.
           The interrupt number can be positive to specify a device specific interrupt,
           or negative to specify a processor exception.
           VTOR must been relocated to SRAM before.
  \param [in]   IRQn      Interrupt number
  \param [in]   vector    Address of interrupt handler function
 */
__STATIC_INLINE void __NVIC_SetVector(IRQn_Type IRQn, uint32_t vector)
{
  uint32_t vectors = (uint32_t )SCB->VTOR;
  (* (int *) (vectors + ((int32_t)IRQn + NVIC_USER_IRQ_OFFSET) * 4)) = vector;
  /* ARM Application Note 321 states that the M4 does not require the architectural barrier */
}

在使用 VTOR 时,需要将向量表大小扩展为下一个 2 的整数次方,且新向量表的基地址必须要对齐到这个数值。

在 Cortex-M 处理器中,中断向量表的基地址必须对齐到一个特定的值,这个值通常是向量表大小的一个整数倍。这是因为处理器在响应中断时,会根据中断向量表的基地址和中断号来计算中断处理函数的地址。如果基地址没有正确对齐,会导致计算出的地址错误,从而无法正确执行中断处理。

将向量表大小扩展为下一个 2 的整数次方,是为了确保向量表的大小满足对齐要求,并且能够容纳所有可能的中断向量。通过这种方式,可以简化处理器在计算中断处理函数地址时的逻辑,并确保能够正确地处理所有中断。

例 1:微控制器有 32 个中断源向量表大小为(32(用于中断)+ 16(用于 系统异常空间))* 4(每个向量的字节数)= 192(0xC0)。将其扩大为下一个 2 的整数次方就得到 256 字节,因此,向量表的地址可被设置为 0x00000000、0x00000100 以及 0x00000200 等。

例 2:微控制器有 75 个中断源向量表大小为(75(用于中断)+ 16(用于 系统异常空间))* 4(每个向量的字节数)= 364(0x16C)。将其扩大为下一个 2 的整数次方就得到 512 字节,因此,向量表的地址可被设置为 0x00000000、0x00000200 以及 0x00000400 等。

由于中断的最小数量为 1,最小的向量表对齐为 128 字节,因此,VTOR 的最低 7 位保留,且被强制置为 0。

向量表重定位特性可用于多种情形:

  1. 具有 Boot loader 的设备

有些微控制器具有多个程序存储器:启动 ROM 和用户 Flash 存储器。微控制器生产商一般会将 Boot loader 预先写到启动 ROM 中,这样在微控制器启动时,启动 ROM 中的 Boot loader 就会首先执行,而且在跳转到用户 Flash 的应用程序前,VTOR 会被设置为指向用户Flash 存储器的开始处,因此会使用用户 Flash 中的向量表。

  1. 应用程序加载到 RAM

有些情况下,应用程序可能会被从外部设备加载到 RAM 中执行,它可能会位于 SD 卡中,或者甚至需要通过网络传输。在这种情况下,存储在片上存储器中用于启动的程序需要初始化一些硬件、复制位于外部设备中的应用程序到 RAM、更新 VTOR 后执行存储在外部的程序。

  1. 动态修改向量表

有些情况下,ROM 中可能会有一个中断的多个处理实例,可能需要在应用的不同阶段在它们之间进行切换。在这种情况下,可以将向量表从程序存储器复制到 SRAM,并且设置 VTOR 指向 SRAM 中的向量表。由于 SRAM 中的内容可在任意时间修改,因此可以轻易地在应用的不同阶段修改中断向量。

向量表最少也要提供 MSP 的初始值以及用于系统启动的复位向量。另外,对于一些应用,若设备在启动时有触发 NMI 的可能,也许还需 要加入 NMI 向量和用于错误处理的 HardFault 向量。

六、中断输入和挂起行为

每个中断都有多个属性:

  • 每个中断都可被禁止(默认)或使能。
  • 每个中断都可被挂起(等待服务的请求)或解除挂起。
  • 每个中断都可处于活跃(正在处理)或非活跃状态。

为了支持这些属性,NVIC 中包含了多个可编程寄存器,它们可用于中断使能控制、挂起状态和只读的活跃状态位。

​ 这些状态属性具有多种可能的组合。例如,在处理中断时(活跃中断),可以将其禁止,若在中断退出前产生了同一个中断的新请求,由于该活跃中断被禁止了,它就会处于挂起状态。若满足以下条件,中断请求可被处理器接受:挂起状态置位 ,中断使能,且中断的优先级比当前等级高(包括中断屏蔽寄存器配置)。

​ NVIC 在设计上既支持产生脉冲中断请求的外设 ,也支持产生高电 平中断请求的外设。无须配置任何一个 NVIC 寄存器以选择其中一种中断类型。对于脉冲中断请求,脉冲宽度至少要为一个时钟周期;而对于电平触发的请求,在 ISR 中的操作清除请求之前,请求服务的外设要一直保持信号电平(如写入寄存器以清除中断请求)。 尽管外部中断请求在 I/O 引脚上的电平可能是低有效的,NVIC 收到的请求信号为高有效。

​ 中断的挂起状态被存储在 NVIC 的可编程寄存器中,当 NVIC 的中断输入被确认后,它就会引发该中断的挂起状态。即便中断请求被取消 ,挂起状态仍会为高。这样,NVIC 可以处理脉 冲中断请求。

​ 挂起状态的意思是,中断被置于一种等待处理器处理的状态。有些情况下,处理器在中断挂起时就会进行处理。不过 ,若处理器已经在处理另外一个更高或同等优先级的中断,或者中断被某个中断屏蔽寄存器给屏蔽掉了,那么在其他的中断处理结束前或中断屏蔽被清除前 ,挂起请求会一直保持。

​ 这一点和传统的 ARM 处理器不同。按照之前的方式,若设备产生了中断,如中断请求(IRQ)/快速中断请求(FIQ),那么在它们得到处理前需要一直保持请求信号。目前,由于NVIC 中的挂起请求寄存器保存中断请求 ,即使请求中断的源设备取消了请求信号,已产生的中断仍会被处理。

当处理器开始处理中断请求时,中断的请求信号会被自动清除。

当中断正被处理时,它就会处于活跃状态。注意在中断入口处,多个寄存器会被自动压入栈中,这也被称作压栈。同时,ISR 的起始地址会被从向量表中取出。

​ 对于许多微控制器设计,外设会产生电平触发的中断,因此 ISR 必须要手动清除中断请求,如写入外设中的某个寄存器。在中断服务完成后,处理器会执行异常返回。 之前自动压栈的寄存器会被恢复出来,而且被中断的程序也会继续执行。中断的活跃状态会被自动清除。

​ 当中断处于活跃状态时,处理器无法在中断完成和异常返回(有时也被称作异常退出)前再次接受同一个中断请求。

​ 中断的挂起状态位于中断挂起状态寄存器中,软件代码可以访问这些寄存器,因此,可以手动清除或设置中断的挂起状态。

  • 若中断请求产生时处理器正在处理另一个具有更高优先级的中断,而在处理器对该中断请求做出响应之前,挂起状态被清除掉了,该请求就会被取消且不会再得到处理。
  • 若外设持续保持某个中断请求,那么即使软件尝试着清除该挂起状态,挂起状态还是会再次置位的。
  • 若在得到处理后,中断源仍在继续保持中断请求,那么这个中断就会再次进入挂起状态且再次得到处理器的服务。
  • 对于脉冲中断请求,若在处理器开始处理前,中断请求信号产生了多次,它们会被当作一次中断请求。

​ 中断的挂起状态可以在其正被处理时再次置位。

  • 在之前的中断请求正被处理时产生了新的请求,这样会引发新的挂起状态,因此,处理器在前一个 ISR 结束后需要再次处理这个中断。

​ 即使中断被禁止了,它的挂起状态仍可置位。在这种情况下,若中断稍后被使能了,它仍可以被触发并得到服务。有些时候,这种情况并不是我们所希望的,因此需要在使能 NVIC 中的中断前手动清除挂起状态

​ 一般来说,NMI 的请求方式和中断类似。若当前没有在运行 NMI 处理,或者处理器被暂停或处于锁定状态,由于 NMI 具有最高优先级且不能被禁止,因此它几乎会立即执行。

七、异常流程简介

1. 接受异常请求

若满足下面的条件,处理器会接受请求:

  • 处理器正在运行(未被暂停或处于复位状态)。

  • 异常处于使能状态(NMI 和 HardFault 为特殊情况,它们总是使能的)。

  • 异常的优先级高于当前等级。

  • 异常未被异常屏蔽寄存器(如 PRIMASK)屏蔽。

注意,对于 SVC 异常,若 SVC 指令被意外用在某异常处理中,且该异常处理的优先级不小于 SVC,它就会引起 HardFault 异常处理的执行。

2. 异常进人流程

异常进入流程包括以下操作:

  1. 多个寄存器和返回地址被压入当前使用的栈。这样就可以将异常处理用普通 c 函数实现。若处理器处于线程模式且正使用进程栈指针(PSP),则 PSP 指向的栈区域就会用于该压栈过程 ,否则就会使用主栈指针(MSP)指向的栈区域。
  2. 取出异常向量(异常处理/ISR 的起始地址)。 为了减少等待时间,这一步可能会和压栈操作并行执行。
  3. 取出待执行异常处理的指令。在确定了异常处理的起始地址后,指令就会被取出。
  4. 更新多个 NVIC 寄存器和内核寄存器,其中包括挂起状态和异常的活跃状态,处理器内核中的寄存器包括程序状态寄存器(PSR)、链接寄存器(LR)、程序计数器(PC)以及栈指针(SP)。

根据压栈时实际使用的栈,在异常处理开始前,MSP 或 PSP 的数值会相应地被自动调整。PC 也会被更新为异常处理的起始地址,而链接寄存器(LR)则会被更新为名为 EXC_ RETURN 的特殊值。该数值为 32 位,且高 27 位为 1。低 5 位中有些部分用于保存异常流程的状态信息(如压栈时使用的哪个栈)。该数值用于异常返回。

3. 执行异常处理

在异常处理内部,可以执行外设所需的服务。在执行异常处理时,处理器就会处于处理模式。此时:

  • 栈操作使用主栈指针(MSP)

  • 处理器运行在特权访问等级

​ 若更高优先级的异常在这个阶段产生,处理器会接受新的中断,而当前正在执行的处理会被挂起且被更高优先级的处理抢占,这种情况名为异常嵌套

​ 若另一个在这个阶段产生的异常具有相同或更低的优先级,新到的异常就会处于挂起状态,且等当前异常处理完成后才会得到处理。在异常处理的结尾,程序代码执行的返回会引起 EXC_RETURN 数值被加载到程序计数器中(PC),并触发异常返回机制。

EXC_RETURN 是一个特殊的值,它包含了有关异常发生前的处理状态的信息,比如异常发生时的堆栈状态、处理模式等。当异常处理程序执行完毕,通过加载 EXC_RETURN 到程序计数器,处理器能够恢复到异常发生时的上下文,并继续执行中断或异常之后的指令。

异常返回机制是 Cortex-M 处理器中非常重要的一部分,它确保了异常处理程序的正确执行和异常处理后的流程恢复。

4. 异常返回

​ 对于某些处理器架构 ,异常返回会使用一个特殊的指令。不过,这也就意味着异常处理无法像普通 C 代码那样编写和编译。对于 ARM Cortex-M 处理器,异常返回机制由一个特殊的地址 EXC_RETURN 触发,该数值在异常入口处产生且被存储在链接寄存器(LR)中。当该数值由某个允许的异常返回指令写入 PC 时,它就会触发异常返回流程。

​ 异常返回可由指令产生。当触发了异常返回机制后,处理器会访问栈空间里在进入异常期间被压入栈中的寄存器数值,且将它们恢复到寄存器组中,这个过程被称作出栈。另外,多个 NVIC 寄存器(如活跃状态)和处理器内核中的寄存器(如 PSR、SP 和 CONTROL)都会更新。

  • 可用于触发异常返回的指令
返回指令 描述
BX<reg> 若 EXC_RETURN 数值仍在 LR 中,则在异常处理结束时可以使用 BX LR 指令执行中断返回
POP {PC}POP {..., PC} 在进入异常处理后,LR 的值通常会 被压入栈中,可以使用操作一个寄存器或包括 PC 在内的多个寄存器的 POP 指令 ,将 EXC_RETURN 放到程序计数器中,这样处理器会执行中断返回
加载(LDR)或多加载(LDM) 可以利用 PC 为目的寄存器的 LDR 或 LDM 指令产生中断返回

在压栈操作的同时,处理器会取出之前被中断的程序的指令,并使得程序尽快继续执行。

由于使用了 EXC_RETURN 数值触发异常返回,异常处理(包括中断服务程序)就可以和普通的 C 函数/子例程一样实现。在生成代码时,C 编译器将 LR 中的 EXC_RETURN 数值作为普通返回地址处理。由于 EXC_RETURN 机制,函数一般不会返回到地址 0xF0000000~0xFFFFFFFF。不过,根据架构定义,这段地址区域不能用于程序代码 [具有永不执行(XN)存储器属性],因此这样也不会有什么问题。

八、中断控制用的 NVIC 寄存器细节

NVIC 用于配置外设中断信息。

1. 简介

NVIC 中有多个用于中断控制的寄存器(异常类型 16~255),这些寄存器位于系统控制空间(SCS)地址区域(SCS 的地址是 0xE000E000)。

  • 用于中断控制的 NVIC 寄存器列表
地址 寄存器 CMSIS-Core 符号 功能
0xE000E100~0xE000E11C 中断设置使能寄存器 NVIC->ISER[0]~
NVIC->ISER[7]
写 1 设置使能
0xE000E180~0xE000E19C 中断清除使能寄存器 NVIC->ICER[0]~
NVIC->ICER[7]
写 1 清除使能
0xE000E200~0xE000E21C 中断设置挂起寄存器 NVIC->ISPR[0]~
NVIC->ISPR[7]
写 1 设置挂起状态
0xE000E280~0xE000E29C 中断清除挂起寄存器 NVIC->ICPR[0]~
NVIC->ICPR[7]
写 1 清除挂起状态
0xE000E300~0xE000E31C 中断活跃位寄存器 NVIC->IABR[0]~
NVIC->IABR[7]
活跃状态位,只读
0xE000E400~0xE000E4EF 中断优先级寄存器 NVIC->IP[0]~
NVIC->IR[239]
每个中断的中断优先级(8 位宽)
0xE000EF00 软件触发中断寄存器 NVIC->STIR 写中断编号设置相应中断的挂起状

除了软件触发中断寄存器(STIR)外,所有这些寄存器都只能在特权等级访问。STIR 默认只能在特权等级访问,不过可以配置为非特权等级访问。

根据默认设置,系统复位后:

  • 所有中断被禁止(使能位 = 0)
  • 所有中断的优先级为 0(最高的可编程优先级)
  • 所有中断的挂起状态清零

2. 中断使能寄存器

​ 中断使能寄存器可由两个地址进行配置。要设置使能位,需要写入 NVIC->ISER[n] 寄存器地址;要清除使能位,需要写入 NVIC->ICER[n] 寄存器地址。这样,使能或禁止一个中断时就不会影响其他的中断使能状态,ISER/ICER 寄存器都是 32 位宽,每个位代表一个中断输入。

​ 由于 Cortex-M3 或 Cortex-M4 处理器中可能存在 32 个以上的外部中断,因此 ISER 和 ICER 寄存器也会不止一个,如 NVIC->ISER[0] 和 NVIC->ISER[1] 等。只有存在的中断的使能位才会被实现,因此,若只有 32 个中断输入,则寄存器只会有 ISER 和 ICER。尽管 CMSIS-Core 头文件将 ISER 和 ICER 定义成了字(32 位),这些寄存器可以按照字、半字或字节的方式访问。由于前 16 个异常类型为系统异常,外部中断 #0 的异常编号为16。

  • 中断使能设置和清除寄存器(0xE000E100~0xE000E11C,0xE000E180~0xE000E19C)
地址 名称 类型 复位值 描述
0xE000E100 NVIC->ISER[0] R/W 0 设置中断 0~31 的使能
Bit[0] 用于中断 #0(异常 #16)
Bit[1] 用于中断 #2(异常 #17)
...
Bit[31] 用于中断 #31(异常 #47)
写 1 将位置 1,写 0 无作用
读出值表示当前使能状态
0xE000E104 NVIC->ISER[1] R/W 0 设置中断 31~63 的使能
写 1 将位置 1,写 0 无作用
读出值表示当前使能状态
0xE000E108 NVIC->ISER[2] R/W 0 设置中断 64~95 的使能
写 1 将位置 1,写 0 无作用
读出值表示当前使能状态
... ... ... ... ...
0xE000E180 NVIC->ICER[0] R/W 0 清除中断 0~31 的使能
Bit[0] 用于中断 #0(异常 #16)
Bit[1] 用于中断 #2(异常 #17)
...
Bit[31] 用于中断 #31(异常 #47)
写 1 将位置 0,写 0 无作用
读出值表示当前使能状态
0xE000E184 NVIC->ICER[1] R/W 0 清除中断 32~63 的使能
写 1 将位置 0,写 0 无作用
读出值表示当前使能状态
0xE000E188 NVIC->ICER[2] R/W 0 清除中断 64~95 的使能
写 1 将位置 0,写 0 无作用
读出值表示当前使能状态
... ... ... ... ...

CMSIS-Core 提供了下面用于访问中断使能寄存器的函数:

void NVIC_EnableIRQ(IRQn_Type IRQn);	//使能中断
void NVIC_DisableIRQ(IRQn_Type IRQn);	//禁止中断

3. 设置中断挂起和清除中断挂起

​ 若中断产生但没有立即执行(例如,若正在执行另一个更高优先级的中断处理),它就会被挂起。

​ 中断挂起状态可以通过中断设置挂起(NVIC->ISPR[n])和中断清除挂起(NVIC->ICPR[n])寄存器访问。与使能寄存器类似,若存在 32 个以上的外部中断输入,则挂起状态控制寄存器可能会不止一个。

​ 挂起状态寄存器的数值可由软件修改,因此可以通过 NVIC->ICPR[n] 取消一个当前被挂起的异常,或通过 NVIC->ISPR[n] 产生软件中断。

  • 中断挂起设置和清除寄存器(0xE000E200~0xE000E21C,0xE000E280~0xE000E29C)
地址 名称 类型 复位值 描述
0xE000E200 NVIC->ISPR[0] R/W 0 设置外部中断 0~31 的挂起
Bit[0] 用于中断 #0(异常 #16)
Bit[1] 用于中断 #2(异常 #17)
...
Bit[31] 用于中断 #31(异常 #47)
写 1 将位置 1,写 0 无作用
读出值表示当前挂起状态
0xE000E204 NVIC->ISPR[1] R/W 0 设置外部中断 31~63 的挂起
写 1 将位置 1,写 0 无作用
读出值表示当前挂起状态
0xE000E208 NVIC->ISPR[2] R/W 0 设置外部中断 64~95 的挂起
写 1 将位置 1,写 0 无作用
读出值表示当前挂起状态
... ... ... ... ...
0xE000E280 NVIC->ICPR[0] R/W 0 清除外部中断 0~31 的挂起
Bit[0] 用于中断 #0(异常 #16)
Bit[1] 用于中断 #2(异常 #17)
...
Bit[31] 用于中断 #31(异常 #47)
写 1 将位置 0,写 0 无作用
读出值表示当前挂起状态
0xE000E284 NVIC->ICPR[1] R/W 0 清除外部中断 32~63 的挂起
写 1 将位置 0,写 0 无作用
读出值表示当前挂起状态
0xE000E288 NVIC->ICPR[2] R/W 0 清除外部中断 64~95 的挂起
写 1 将位置 0,写 0 无作用
读出值表示当前挂起状态
... ... ... ... ...

CMSIS-Core 提供了下面用于访问中断挂起寄存器的函数:

void NVIC_SetPendingIRQ(IRQn_Type IRQn);	//设置中断的挂起状态
void NVIC_ClearPendingIRQ(IRQn_Type IRQn);	//清除中断的挂起状态
uint32_t NVIC_GetPendingIRQ(IRQn_Type IRQn);//查询中断的挂起状态

4. 活跃状态

​ 每个外部中断都有一个活跃状态位,当处理器开始执行中断处理时,该位会被置 1,而在执行中断返回时会被清零。不过,在中断服务程序执行期间,更高优先级的中断可能会产生且抢占。在此期间,尽管处理器在执行另一个中断处理,之前的中断仍会被定义为活跃的。尽管 IPSR 表示当前正在执行的异常服务,它无法告诉你当有嵌套异常时某个异常是否为活跃的。中断活跃状态寄存器为 32 位宽,不过还能通过半字或字节传输访问。若外部中断的数量超过 32,则活跃状态寄存器会不止一个。外部中断的活跃状态寄存器为只读的

  • 中断活跃状态寄存器(0xE000E300~0xE000E31C)
地址 名称 类型 复位值 描述
0xE000E300 NVIC->IABR[0] R 0 外部中断 #0~31 的活跃状态
Bit[0] 用于中断 #0
Bit[1] 用于中断 #1
...
Bit[2] 用于中断 #2
0xE000E304 NVIC->IABR[1] R 0 外部中断 #32~63 的活跃状态
- - - - -

CMSIS-Core 提供了下面用于访问中断活跃状态寄存器的函数:

uint32_t NVIC_GetActive(IRQn_Type IRQn);	//获取中断的活跃状态

5. 优先级

​ 每个中断都有对应的优先级寄存器,其最大宽度为 8 位,每个寄存器可以根据优先级分组设置被进一步划分为分组优先级和子优先级。优先级寄存器可以通过字节 、半字或字访问。优先级寄存器的数量取决于芯片中实际存在的外部中断数。

  • 中断优先级寄存器(0xE000E400~0xE000E4EF)
地址 名称 类型 复位器 描述
0xE000E400 NVIC->IP[0] R/W 0(8位) 外部中断 #0 的优先级
0xE000E401 NVIC->IP[1] R/W 0(8位) 外部中断 #1 的优先级
- - - - -
0xE000E41F NVIC->IP[31] R/W 0(8位) 外部中断 #31 的优先级
- - - - -

CMSIS-Core 提供了下面用于访问中断优先级寄存器的函数:

void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority);	//设置中断或异常的优先级
uint32_t NVIC_GetPriority(IRQn_Type IRQn);	//读取中断或异常的优先级

​ 若需要确定 NVIC 中可用的优先级数量,可以使用微控制器供应商提供的 CMSIS-Core 头文件中的“_NVIC_PRIO_BITS”伪指令。另外,还可以将 0xFF 写人其中一个中断优先级寄存器,并在将其读回后查看多少位为 1。若设备实际实现了 8 个 优先级(3 位),读回值则为 0xE0。

6. 软件触发中断寄存器

​ 除了 NVIC->ISPR[n] 寄存器外,还可以通过软件触发中断寄存器(NVIC->STIR)利用软件来触发中断。

  • 软件触发中断寄存器(0xE000EF00)
名称 类型 复位值 描述
8 : 0 NVIC->STIR W - 写中断编号可以设置中断的挂起位,如写 0 挂起外部中断 #0

例如,利用下面的 C 代码可以产生中断 #3:

NVIC-> STIR = 3;

其功能和下面利用 NVIC->ISPR[n] 的 CMSIS-Core 函数调用相同:

NVIC_SetPendingIRQ(TimerO_IRQn);	//假定 TimerO_IRQn 等于 3
//TimerO_IRQn 为定义在设备相关头文件中的枚举

​ 与只能在特权等级访问的 NVIC->ISPR[n] 不同,要让非特权程序代码触发一次软件中断,可以设置配置控制寄存器(SCB->CCR,地址 0XE000ED14)中的第 1 位(USERSETMPEND)。USERSETMPEND 默认为清零状态,这就意味着只有特权代码才能使用 NVIC->STIR。

​ 与 NVIC->ISPR[n] 类似,NVIC->STIR 无法触发 NMI 以及 SysTick 等系统异常。系统控制块(SCB)中的其他寄存器可用于系统异常管理。

7. 中断控制器类型寄存器

​ NVIC 在 0xE000E004 地址处还有一个中断控制器类型寄存器,它是一个只读寄存器 ,给出了 NVIC 支持的中断输入的数量,单位为 32。

  • 中断控制器类型寄存器(SCnSCB->ICTR,0xE000E004)
名称 类型 复位值 描述
4 : 0 INTLINESNUM R - 以 32 为单位的中断输入数量
0=1~32
1=33~64
...

​ 利用 CMSIS 的设备驱动库,可以使用 SCnSCB->ICTR 来访问这个只读寄存器。(SCnSCB 表示“未在 SCB 中的系统控制寄存器”)。 与中断控制器类型寄存器只能给出可用中断的大致数量不同,可以在 PRIMASK 置位的情况下(禁止中断产生),通过下面的方法能得到可用中断的确切数量:写入中断使能/挂起寄存器等中断控制寄存器,读回后查看中断使能/挂起寄存器中实际实现的位数。

九、用于异常和中断控制的 SCB 寄存器细节

SCB 用于配置系统中断(异常)。

1. SCB 寄存器简介

​ 除了 CMSIS-Core 中的 NVIC 数据结构,系统控制块(SCB)数据结构中还包含了一些常用于中断控制的寄存器,这些寄存器中只有一部分与中断或异常控制有关。

  • SCB 中的寄存器一览
地址 寄存器 CMSIS-Core 符号 功能
0xE000ED00 CPU ID SCB->CPUID 可用于识别处理器类型和版本的 ID 代码
0xE000ED04 中断控制和状态 SCB->ICSR 系统异常的控制和状态
0xE000ED08 向量表偏移寄存器 SCB->VTOR 使能向量表重定位到其他的地址
0xE000ED0C 应用中断/复位控制寄存器 SCB->AIRCR 优先级分组配置和自复位控制
0xE000ED10 系统控制寄存器 SCB->SCR 休眠模式和低功耗特性的配置
0xE000ED14 配置控制寄存器 SCB->CCR 高级特性的配置
0xE000ED18~0xE000ED23 系统处理优先级寄存 SCB->SHP[0]~SCB->SHP[11] 系统异常的优先级设置
0xE000ED24 系统处理控制和状态寄存器 SCB->SHCSR 使能错误异常和系统异常状态的控制
0xE000ED28 可配置错误状态寄存器 SCB->CFSR 引起错误异常的提示信息
0xE000ED2C 硬件错误状态寄存器 SCB->HFSR 引起硬件错误异常的提示信息
0xE000ED30 调试错误状态寄存器 SCB->DFSR 引起调试事件的提示信息
0xE000ED34 存储器管理错误寄存器 SCB->MMFAB 存储器管理错误的地址值
0xE000ED38 总线错误寄存器 SCB->BFAR 总线错误的地址值
0xE000ED3C 辅助错误状态寄存器 SCB->AFSR 设备相关错误状态的信息
0xE000ED40~0xE000ED44 处理器特性寄存器 SCB->PFR[0]~SCB->PFR[1] 可用处理器特性的只读信息
0xE000ED48 调试特性寄存器 SCB->DFR 可用调试特性的只读信息
0xE000ED4C 辅助特性寄存器 SCB->AFR 可用辅助特性的只读信息
0xE000ED50~0xE000ED5C 存储器模块特性寄存 SCB->MMFR[0]~SCB->MMFR[3] 可用存储器模块特性的只读信息
0xE000ED60~0xE000ED70 指令集属性寄存器 SCB->ISAR[0]~SCB->ISAR[4] 指令集特性的只读信息
0xE000ED88 协处理器访问控制寄存器 SCB->CPACR 使能浮点特性的寄存器,只存在于具有浮点单元的 Cortex-M4

2. 中断控制和状态寄存器(ICSR)

ICSR 寄存器可在应用程序中用于:

  • 设置和清除系统异常的挂起状态,其中包括 SysTick、PendSV 和 NMI。
  • 通过读取 VECTACTIVE 可以确定当前执行的异常/中断编号。

另外,调试器还可利用该寄存器确定中断状态。VECTACTIVE 域和 IPSR 相同。

  • 中断控制和状态寄存器(SCB->ICSR,0xE000ED04)
名称 类型 复位值 描述
31 NMIPENDSET R/W 0 写 1 挂起 NMI
读出值表示 NMI 挂起状态
28 PENDSVSET R/W 0 写 1 挂起系统调用
读出值表示挂起状态
27 PENDSVCLR W 0 写 1 清除 PendSV 挂起状态
26 PENDSTSET R/W 0 写 1 挂起 PendSV 异常
读出值表示挂起状态
25 PENDSTCLR W 0 写 1 清除 SYSTICK 挂起状态
23 ISRPREEMPT R 0 表示挂起中断下一步将变为活跃状态(用于调试)
22 ISRPENDING R 0 外部中断挂起(除了用于错误的 NMI 等系统异常)
21 : 12 VECTPENDING R 0 挂起的 ISR 编号
11 RETTOBASE R 0 当处理器在执行异常处理时置 1,若中断返回且没有其他异常挂起则会返回线程
9 : 0 VECTACTIVE R 0 当前执行的中断服务程序

该寄存器中的多个位域可被调试器使用以确定系统异常的状态。多数情况下,只有挂起位可用于应用开发。

3. 向量表偏移寄存器(VTOR)

​ 注意不同版本的 Cortex-M3 和 Cortex-M4 处理器的 VTOR 的定义可能会有些区别,两者的 VTOR 寄存器地址都为 0xE000ED0C,可由 CMSISCore 符号 SCB->VTOR 访问。

对于 Cortex-M4 处理器或 Cortex-M3 版本 r2pl(或之后)VTOR 的定义:

  • Cortex-M4 或 Cortex-M3 r2pl 中的向量表偏移寄存器
名称 类型 复位值 描述
31 : 7 TBLOFF R/W 0 向量表偏移数值

对于 Cortex-M3 版本 r0p0~r2p0,VTOR 的定义:

  • Cortex-M3 r2p0 或之前版本中的向量表偏移寄存器
名称 类型 复位值 描述
31 : 30 保留 - - 未实现,保持为 0
29 TBLBASE R/W 0 表格基地址位于代码(0)或 RAM(1)
28 : 7 TBLOFF R/W 0 代码区域或 RAM 区域的表格偏移数值

Cortex-M 处理器的版本可由 SCB 中的 CPUID 寄存器确定。

4. 应用中断和复位控制寄存器(AIRCR)

AIRCR 寄存器用于:

  • 控制异常/中断优先级管理中的优先级分组。
  • 提供系统的端信息(可被软件或调试器使用)。
  • 提供自复位特性。

  • 应用中断和复 位控制寄存器(SCB->A1RCR,地址 0xE000ED0C)
类型 复位值 描述
31 : 16 VECTKEY R/W - 访问键值。写这个寄存器时必须要将 0x05FA 写入,否则写会被忽略,高半字的读回值为 0xFA05
15 ENDIANESS R - 1 表示系统为大端,0 则表示系统为小端,复位后才能更改
10 : 8 PRIGROUP R/W 0 优先级分组
2 SYSRESETREQ W - 请求芯片控制逻辑产生一次复位
1 VECTCLRACTIVE W - 清除异常的所有活跃状态信息。一般用于调试或 OS 中,以便系统可以从系统错误中恢复过来(复位更安全)
0 VECTRESET W - 复位 Cortex-M3/M4 处理器(调试逻辑除 外),但不会复位处理器外的电路,用于调试操作,且不能和 SYSRESETREQ 同时使用

​ 多数情况下,可以使用 CMSIS-Core 函数 NVIC_SetPriorityGrouping 和 NVIC_GetPriorityGrouping 来访问 PRIGROUP 域。

​ VECTRESET 和 VECTCLRACTIVE 位域是为调试器设计的,尽管软件可以利用 VECTRESET 触发一次处理器复位,不过由于它不会复位外设等系统中的其他部分,因此多数应用程序是不大会用到它的。若想产生一次系统复位 ,多数情况下(取决于芯片设计和应用复位需求)应该使用 SYSRESETREQ。

​ 有一点需要注意,VECTRESET 和 VECTCLRACTIVE 位域不应同时置位,非要这么做的话会导致一些 Cortex-M3/M4 设备的复位电路出错,这是因为 VECTRESET 信号会复位SYSRESETREQ。

​ 根据微控制器的复位电路设计 ,将 1 写入 SYSRESETREQ 后 ,处理器可能会在复位实际产生前继续执行几条指令。因此 ,通常要在系统复位请求后加上一个死循环

5. 系统处理优先级寄存器(SCB->SHP[0~11])

​ SCB->SHP[0] 到 SCB->SHP[11] 的位域定义和中断优先级寄存器的定义相同,不同之处在于它们是用于系统异常的。这些寄存器并未全部实现,可以使用 CMSIS-Core 中的函数 NVIC_SetPriority 和 NVIC_GetPriority 来调整或访问系统异常的优先级。

  • 系统处理优先级寄存器(SCB->SHP[0~11])
地址 名称 类型 复位值 描述
0xE000ED18 SCB->SHP[0] R/W 0(8位) 存储器管理错误的优先级
0xE000ED19 SCB->SHP[1] R/W 0(8位) 总线错误的优先级
0xE000ED1A SCB->SHP[2] R/W 0(8位) 使用错误的优先级
0xE000ED1B SCB->SHP[3] - - -(未实现)
0xE000ED1C SCB->SHP[4] - - -(未实现)
0xE000ED1D SCB->SHP[5] - - -(未实现)
0xE000ED1E SCB->SHP[6] - - -(未实现)
0xE000ED1F SCB->SHP[7] R/W 0(8位) SVC 的优先级
0xE000ED20 SCB->SHP[8] R/W 0(8位) 调试监控的优先级
0xE000ED21 SCB->SHP[9] - - -
0xE000ED22 SCB->SHP[10] R/W 0(8位) PendSV 的优先级
0xE000ED23 SCB->SHP[11] R/W 0(8位) SysTick 的优先级

6. 系统处理控制和状态寄存器(SCB->SHCSR)

​ 使用错误、存储器管理(MemManage)错误和总线错误异常的使能由系统处理控制和状态寄存器(0xE000ED24)控制。错误的挂起状态和多数系统异常的活跃状态也可以从这个寄存器中得到。

  • 系统处理控制和状态寄存器(SCB->SHCSR,地址 0xE000ED24)
名称 类型 复位值 描述
18 USGFAULTENA R/W 0 使用错误处理使能
17 BUSFAULTENA R/W 0 总线错误处理使能
16 MEMFAULTENA R/W 0 存储器管理错误处理使能
15 SVCALLPENDED R/W 0 SVC 挂起,SVC 已启动但被更高优先级异常抢占
14 BUSFAULTPENDED R/W 0 总线错误挂起,总线错误已启动但被更高优先级异常抢占
13 MEMFAULTPENDED R/W 0 存储器管理错误挂起,存储器管理错误已启动但被更高优先级异常抢占
12 USGFAULTPENDED R/W 0 使用错误挂起,使用错误已启动但被更高优先级异常抢占
11 SYSTICKACT R/W 0 若 SYSTICK 异常活跃则读出为 1
10 PENDSVACT R/W 0 若 PendSV 异常活跃则读出为 1
8 MONITORACT R/W 0 若调试监控异常活跃则读出为 1
7 SVCALLACT R/W 0 若 SVC 异常活跃则读出为 1
3 USGFAULTACT R/W 0 若使用错误异常活跃则读出为 1
1 BUSFAULTACT R/W 0 若总线错误异常活跃则读出为 1
0 MEMFAULTACT R/W 0 若存储器管理异常活跃则读出为 1

​ 多数情况下,该寄存器仅用于应用代码使能可配置的错误处理(MemManage 错误、总线错误和使用错误)。

​ 在写这个寄存器时应该多加小心,确保系统异常的活跃状态位不会被意外修改。例如,要使能总线错误异常,应该使用一次读一修改一写的操作:

SCB->SHCSR|= 1<<17; //使能总线错误异常

​ 不然,若一个已经被激活的系统异常的活跃状态被意外清除,当系统异常处理产生异常退出时就会出现错误异常。

十、用于异常或中断屏蔽的特殊寄存器细节

1. PRIMASK

​ 在许多应用中,可能都需要暂时禁止所有中断以执行一些时序关键的任务,此时可以使用 PRIMASK 寄存器。PRIMASK 寄存器只能在特权状态访问

PRIMASK 用于禁止除 NMI 和 HardFault 外的所有异常,它实际上是将当前优先级改为 0(最高的可编程等级)。如用 C 编程,可以使用 CMSIS-Core 提供的函数来设置和清除PRIMASK:

void _enable_irq();						//清除 PRIMASK
void _disable_irq();					//设置 PRIMASK
void _set_PRIMASK(uint32_t priMask);	//设置 PRIMASK 为特定值
uint32_t _get_PRIMASK(void);			//读取 PRIMASK 的数值

对于汇编编程,可以利用 CPS(修改处理器状态)指令修改 PRIMASK 寄存器的数值。

CPSIE I;清除 PRIMASK(使能中断)
CPSID I;设置 PRIMASK(禁止中断)

PRIMASK 寄存器还可通过 MRS 和 MSR 指令访问。例如:

MOVS R0,#1
MSR PRIMASK,R0		;将 1 写入 PRIMASK 禁止所有中断

以及:

MOVS R0,#0
MSR PRIMASK,R0		;将 0 写入 PRIMASK 以使能中断

当 PRIMASK 置位时,所有的错误事件都会触发 HardFault 异常,而不论相应的可配置错误异常(如 MemManage、总线错误和使用错误)是否使能。

2. FAULTMASK

​ 从行为来说,FAULTMASK 和 PRIMASK 很类似 ,只是它实际上会将当前优先级修改为 -1,这样甚至是 HardFault 处理也会被屏蔽。当 FAULTMASK 置位时 ,只有 NMI 异常处理才能执行。

​ 从用法来说,FAULTMASK 用于将配置的错误处理(如 MemManage、总线错误和使用错误)的优先级提升到 -1,这样这些处理就可以使用 HardFault 的一些特殊特性。其中包括:

  • 旁路 MPU
  • 忽略用于设备/存储器探测的数据总线错误

将当前优先级提升到 -1 后,FAULTMASK 可在可配置的错误处理执行期间,阻止其他异常或中断处理的执行。

FAULTMASK 寄存器只能在特权状态访问,不过不能在 NMI 和 HardFault 处理中设置。若在 C 编程中使用符合 CMSIS 的设备驱动,则可以使用下面的 CMSIS-Core 函数来设置清除 FAULTMASK:

void _enable_fault_irq(void);			//清除 FAULTMASK
void _disable_fault_irq(void);			//设置 FAULTMASK 以禁止中断
void _set_FAULTMASK(uint32_t faultMask);
uint32_t _get_FAULTMASK(void);

对于汇编语言用户,可以利用 CPS 指令修改 FAULTMASK 的当前状态:

CPSIE F;清除 FAULTMASK
CPSID F;设置 FAULTMASK

还可以利用 MRS 和 MSR 指令访问 FAULTMASK 寄存器:

MOVS R0,#1
MSR FAULTMASK,R0	;将 1 写入 FAULTMASK 禁止所有中断

以及:

MOVS R0,#0
MSR FAULTMASK,R0	;将 0 写入 FAULTMASK 使能中断

FAULTMASK 会在退出异常处理时被自动清除,从 NMI 处理中退出时除外。由于这个特点,FAULTMASK 就有了一个很有趣的用法:若要在低优先级的异常处理中触发一个高优先级的异常(NMI除外),但想在低优先级处理完成后再进行高优先级的处理,可以:

  • 设置 FAULTMASK 禁止所有中断和异常(NMI异常除外)
  • 设置高优先级中断或异常的挂起状态
  • 退出处理

​ 由于在 FAULTMASK 置位时,挂起的高优先级异常处理无法执行,高优先级的异常就会在 FAULTMASK 被清除前继续保持挂起状态,低优先级处理完成后才会将其清除。因此,可以强制让高优先级处理在低优先级处理结束后开始执行。

3. BASEPRI

​ 有些情况下,可能只想禁止优先级低于某特定等级的中断,此时,就可以使用 BASEPRI寄存器。要实现这个目的,只需简单地将所需的屏蔽优先级写入 BASEPRI 寄存器。例如,若要屏蔽优先级小于等于 0x60 的所有异常,则可以将这个数值写入 BASEPRI:

_set_BASEPRI(0x60);	//利用 CMSIS-Core 函数禁止优先级在 0x60 OxFF 间的中断

若使用汇编编程,可将同一操作写为:

MOVS R0,#0x60
MSR BASEPRI,R0				;禁止优先级在 0x60 OxFE 间的中断

还可以读回 BASEPRI 的数值:

x = _get_BASEPRI(void);		//读出 BASEPR工 的数值

或者用汇编实现:

MRS R0,BASEPRI

要取消屏蔽,只需将 BASEPRI 寄存器写 0:

_set_BASEPRI(0x0);			//取消 BASEPRI 屏蔽

或用汇编实现:

MOVS R0, # 0x0
MSR BASEPRI,R0				;取消 BASEPRI 屏蔽

​ BASEPRI 寄存器还可通过另一个名称访问,也就是 BASEPRI_MAX。它们实际上是同一个寄存器,不过当用这 个名称访问时,会得到一个条件写操作。BASEPRI 和 BASEPRI_MAX 在硬件上是一个寄存器,不过在汇编代码中的编码不同。在使用 BASEPRI_MAX 时,处理器会自动比较当前值和新的数值,不过只有新的优先级更高时才会允许修改。例如,考虑下面的指令序列:

MOVS R0,#0x60
MSR BASEPRI_MAX,R0	;禁止优先级为 0x60,0x61 等的中断
MOVS R0z #0xF0
MSR BASEPRI_MAX,RO	;由于数值低于 0x60,本次写操作不起作用
MOVS R0z #0x40
MSR BASEPRI_MAX,R0	;本次写操作会将屏蔽数值修改为 0x40

​ 要修改为更低的屏蔽值或禁止屏蔽,应该使用寄存器名 BASEPRI。BASEPRI/BASEPRI_MAX 寄存器无法在非特权状态设置

​ 与其他的优先级寄存器类似,BASEPRI 寄存器的格式受实际的优先级寄存器宽度影响。例如,若优先级寄存器只实现了 3 位,BASEPRI 可被设置为 0x00、0x20、0x40、…、0xC0 和 0xE0。

十一、设置中断的步骤

1. 简单情况

​ 在多数应用中,包括向量表在内的程序代码位于 Flash 等只读存储器中,而且在运行过程中无须修改向量表。这样可以只依赖于存储在 ROM 中的向量表,无须进行向量表重定位。要设置中断,只需执行以下步骤:

  1. 设置优先级分组。优先级分组默认为 0(优先级寄存器中只有第 0 位用于子优先级),这一步是可选的。
  2. 设置中断的优先级。中断的优先级默认为 0(最高的可编程优先级),这一步也是可选的。
  3. 在 NVIC 或外设中使能中断。

下面的例子为设置中断的步骤:

//将优先级分组设置为 5
NVIC_SetPriorityGrouping(5);
//将 Timer0_JRQn 优先级设置为 0xC0(4 位优先级)
NVIC_SetPriority(Timer0_IRQn, 0xC);	//CMSIS 函数将其移至 0xC0
//使能 NVIC 中的 Timer 0 中断
NVIC_EnableIRQ(Timer0_IRQn);

​ 向量表默认位于存储器的开头处(地址 0)。不过,有些微控制器具有 Boot loader,而且在存储在 Flash 中的程序开始执行前,Boot loader 可能已经将向量表重定位到了 Flash 存储器的开头(软件开发人员定义的向量表)。在这种情况下,由于 Boot loader 已经设置了 VTOR 且重定位了向量表,就不用自己处理了。

​ 若存在大量的嵌套中断,除了使能中断外,还应该确保栈空间足够。由于在处理模式中,中断处理总是使用主栈指针(MSP),主栈应该有足够应对最坏情况的栈空间(最大数量的嵌套中断/异常)。计算栈空间时应该考虑中断处理使用的栈以及每级栈帧使用的栈。

2. 向量表重定位时的情况

​ 若需要将向量表重定位,如到 SRAM 中以便能在应用的不同阶段更新部分异常向量,则需要多执行几步:

  1. 系统启动时,需要设置优先级分组,这一步是可选的。优先级分组默认为 0(优先级寄存器的第 0 位用于子优先级,第 7~1 位则用于抢占优先级)。
  2. 若需要将向量表重定位到 SRAM,则复制当前向量表到 SRAM 中的新位置。
  3. 设置向量表偏移寄存器(VTOR)指向新的向量表。
  4. 若有必要,更新异常向量。
  5. 设置所需中断的优先级。
  6. 使能中断。

向量表重定位的实例:

  • 复制向量表到 SRAM 开头处(0x20000000)的代码实例
//注意,下面的存储器屏障指令的使用是基于架构建议的,
//Cortex - M3 和 Cortex- M4 并非强制要求
//字访问的宏定义
#define HW32_REG(ADDRESS)	(*((volatile unsigned long *)(ADDRESS)))
#define VTOR_NEW_ADDR	0x20000000
int i;	//循环变量
//在设置 VTOR 前首先将向量表复制到 SRAM
for (i=0; i<48; i++){			//假定异常的最大数量为 48
	//将每个向量表入口从 Flash 复制到 SRAM
	HW32_REG((VTOR_NEW_ADDR + (i<<2))) = HW32_REG((i<<2);)
}
_DMB();	//数据存储器屏障,确保到存储器的写操作结束
SCB->VTOR = VTOR_NEW_ADDR;	//将 VTOR 设置为新的向量表位置
_DSB();	//数据同步屏除,确保接下来的所有指令都使用新配置

将向量表复制到 SRAM 后,可以利用下面的代码更新异常向量:

void new_timerO_handler(void);	//新的 Timer 0 中断处理
unsigned int vect_addr;
//计算异常向量的船址
//假定向量即将被替换的中断为 TimerO_IRQn
vect_addr = SCB-> VTOR + ((((int) TimerO_IRQn) + 16)<<2);
//将向量表的地址更新为 new_timer0_handler()
HW32_REG(vect_addr) = (unsigned int) new_timerO_handler;

十二、软件中断

​ 可以利用软件代码触发异常或中断,之所以要这么做,最常见的原因为,允许多任务环境中处于非特权状态的应用任务,可以访问一些需要在特权状态下才能执行的系统服务。根据要触发的异常或中断的类型,应该使用不同的方法。

​ 若要触发一个中断(异常类型 16 或之上),最简单的方法为使用 CMSIS-Core 函数 NVIC_ SetPendingIRQ:

NVIC_SetPendingIRQ(Timer0_IRQn);
//Timer0_IRQn 为定义在设备相关头文件中的枚举

​ 上面的代码设置了中断的挂起状态,若中断已使能且优先级高于当前等级,中断就会被触发。注意,即便优先级高于当前等级 ,中断处理也会延迟几个时钟周期。若要在下一步操作前执行中断处理,则需要插入存储器屏障指令

NVIC_SetPendingIRQ(Timer0_IRQn);
_DSB(); 	//使能传输已完成
_ISB(); 	//确保写的副作用可见

​ 这方面的更多细节可以参考 ARM 应用笔记 321“ARM Cortex-M 存储器屏障指令编程指南”(参考文献 9)。

​ 除了使用 CMSIS-Core 函数,还可以写中断设置挂起寄存器或软件触发中断寄存器。 不过该代码的可移植性会降低,在用于其他 Cortex-M 处理器上时可能需要修改。

​ 若要触发 SVC 异常,则需要执行 SVC 指令。用 C 语言实现的方法是和编译器相关的,内联/嵌入汇编的方法一般用于 OS 环境。

​ 对于其他如 NMI、PendSV 以及 SysTick 等异常 ,可以设置中断控制和状态寄存器中的挂起状态来进行触发。与中断类似,设置这些异常的挂起状态并不总能保证会立即执行。

​ 剩下的系统异常为错误异常,不应用作软件中断。

posted @ 2024-05-05 22:36  一只心耳  阅读(810)  评论(0编辑  收藏  举报