Diving into the STM32 HAL-----Interrupts

        硬件管理就是处理异步事件。其中大部分来自硬件外围设备。例如,计时器达到配置的 period 值,或者 UART 在数据到达时发出警告。

        中断是一个异步事件,它会导致按优先级停止执行当前代码(中断越重要,其优先级越高;这将导致优先级较低的中断被挂起)。为中断提供服务的代码称为 Interrupt Service Routine (ISR)。

        中断可以由硬件和软件本身产生。ARM 架构区分了两种类型:由硬件发起的中断,由软件发起的异常(例如,访问无效的内存位置)。在 ARM 术语中,中断是一种异常。Cortex-M 处理器提供了一个专门用于异常管理的单元。这称为嵌套向量中断控制器 (NVIC)。

1、NVIC中断控制器

        NVIC 是基于 Cortex-M 的微控制器内部的专用硬件单元,负责异常处理。下图是NVIC 单元、处理器内核和外设之间的关系。必须区分两种类型的外设:CortexM 内核外部同时也是STM32 MCU 内部的外设(例如定时器、UARTS 等),以及 MCU 外部的外设(中断源是 MCU I/O)。

       ARM 区分源自 CPU 内核内部的系统异常和来自外部外设的硬件异常,也称为中断请求 (IRQ)。程序员只需要使用特定的 ISR 管理异常(通常使用 C 语言编写)。处理器知道在哪里找到这些例程,这要归功于一个包含 Interrupt Service Routines 内存中地址的间接表。这个表通常称为矢量表,每个 STM32 微控制器都定义了自己的表。异常类型如下:

        向量表包含处理程序例程的地址(实际上是一个间接表),Cortex-M 内核需要一种方法来在内存中查找向量表。按照惯例,在所有基于 Cortex-M 的处理器中,向量表都从硬件地址 0x0000 0000 开始。如果我们的固件设计为矢量表驻留在内部闪存中(一种非常常见的情况),那么矢量表将从所有 STM32 MCU 中的 0x0800 0000 地址开始放置。然而,在第 1 章中,我们看到当 CPU 启动时,0x0800 0000 地址会自动别名到 0x0000 0000。

2、使能中断

        当 STM32 MCU 启动时,默认情况下仅启用 Reset、NMI 和 Hard Fault 异常。其余的异常和外设中断被禁用,并且必须根据请求启用它们。
        为了启用 IRQ,CubeHAL 提供了以下功能:

void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);void HAL_NVIC_DisableIRQ(IRQn_Type IRQn);

        其中 IRQn_Type 是为该特定 MCU 定义的所有异常和中断的枚举。IRQn_Type 枚举是 ST 驱动程序 HAL 的一部分,它在 stm32XXxx.h头文件中定义。

        需要注意的是,上面两个函数在 NVIC 控制器级别启用/禁用中断。从前面中可以看出,连接到该线路的外设断言了一条中断线路。例如,USART2 外设断言与 NVIC 控制器内部的 USART2_IRQn 中断线相对应的中断线。这意味着必须正确配置单个外设才能在中断模式下工作。大多数 STM32 外设都设计为在中断模式下工作。通过使用特定的 HAL 例程,我们可以在外设级别启用中断。例如,使用 HAL_USART_Transmit_IT(), 我们在中断模式下隐式配置 USART 外设。显然,还需要通过调用 HAL_NVIC_EnableIRQ() 在 NVIC 级别启用相应的中断。

2.1、中断线

        如下图,所有 Px0 引脚都连接到 EXTI0,所有 Px10 引脚都连接到 EXTI10,所有 Px15 引脚都连接到 EXTI15。但是,EXTI 10 和 15 线路在 NVIC 内共享相同的 IRQ(因此由相同的 ISR 提供服务)。

例如,PC13按键手动控制PB5 LED闪烁:

int main(void) {GPIO_InitTypeDef GPIO_InitStruct = {0};/* MCU Configuration----------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* Configure the system clock */SystemClock_Config();/* GPIOA and GPIOC Configuration----------------------------------------------*//* GPIO Ports Clock Enable */__HAL_RCC_GPIOC_CLK_ENABLE();__HAL_RCC_GPIOB_CLK_ENABLE();/*Configure GPIO pin : PC13 */GPIO_InitStruct.Pin = GPIO_PIN_13;GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;GPIO_InitStruct.Pull = GPIO_PULLDOWN;HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);/*Configure GPIO pin : PB5 */GPIO_InitStruct.Pin = GPIO_PIN_5;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);/* Configure GPIO pin Output Level */HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET);/* EXTI interrupt init*/HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);while (1);
}void EXTI15_10_IRQHandler(void) {if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_13) != RESET) {__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_13);HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);}
}

        首先,GPIO PC13 配置为每次从低电平变为高电平时触发中断。这是通过设置 GPIO 来实现的。Mode 设置为 GPIO_MODE_IT_RISING。

        接下来,启用与 Px13 引脚关联的 EXTI 线的中断,即 EXTI15_10_IRQn。

        最后,定义函数 void EXTI15_10_IRQHandler(),这是与向量表中 EXTI15_10  IRQ 关联的 ISR 例程。ISR 的内容非常简单。由于 EXTI15_10 线连接到不同的引脚,检查 PC13 是否是触发中断的引脚。如果是这样,切换 PB5 I/O 并清除与 EXTI 线关联的pending处理位。

        幸运的是,ST HAL 提供了一种抽象机制,除非我们需要处理它们,否则我们无需处理所有这些细节。前面的例子可以按以下方式重写:

int main(void) {GPIO_InitTypeDef GPIO_InitStruct = {0};/* MCU Configuration----------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* Configure the system clock */SystemClock_Config();/* GPIOA and GPIOC Configuration----------------------------------------------*//* GPIO Ports Clock Enable */__HAL_RCC_GPIOC_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();/*Configure GPIO pin : PC13 */GPIO_InitStruct.Pin = GPIO_PIN_12 | GPIO_PIN_13;GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;GPIO_InitStruct.Pull = GPIO_PULLDOWN;HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);/*Configure GPIO pin : PB5 */GPIO_InitStruct.Pin = GPIO_PIN_5;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);/* Configure GPIO pin Output Level */HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET);/* EXTI interrupt init*/HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);while (1);
}void EXTI15_10_IRQHandler(void) {HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_12);HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);
}void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {if(GPIO_Pin == GPIO_PIN_13)HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, SET);else if(GPIO_Pin == GPIO_PIN_12)HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, RESET);
}

        这次我们将引脚 PC13 和 PC12 都配置为中断源。当调用 EXTI15_10_IRQHandler() ISR 时,我们将控制权转移到 HAL 内的 HAL_GPIO_EXTI_IRQHandler() 函数。这将为我们执行所有与中断相关的活动,并且它将调用 HAL_GPIO_EXTI_Callback() 例程,该例程传递生成 IRQ 的实际 GPIO(请记住,PC12 和 PC13 连接到同一条外线中断线)。

        下图清楚地显示了从 IRQ生成的调用序列。HAL 中几乎所有的 IRQ 处理程序例程都使用这种机制。

2.2、使用 CubeMX 启用中断

        CubeMX 会自动将已启用的 ISR 添加到 Core/Src/stm32XXxx_it.c 文件中,并负责启用 IRQ。此外,它还为我们添加了要调用的相应 HAL 处理程序例程,如下所示:

/**
* @brief This function handles EXTI line[15:10] interrupts.
*/
void EXTI15_10_IRQHandler(void) {/* USER CODE BEGIN EXTI15_10_IRQn 0 *//* USER CODE END EXTI15_10_IRQn 0 */HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);/* USER CODE BEGIN EXTI15_10_IRQn 1 *//* USER CODE END EXTI15_10_IRQn 1 */
}

我们只需要在应用程序代码中添加相应的回调函数(例如 HAL_GPIO_EXTI_Callback() 例程)。

        注意:stm32XX_hal_cortex.c 模块清楚地展示了 ST HAL 和 CMSIS 包之间的交互,因为它完全依赖官方的 ARM 包来处理底层的 Cortex-M NVIC 控制器。每个 HAL_NVIC_xxx() 函数都是相应 CMSIS NVIC_xxx() 函数的包装。这意味着我们可以使用 CMSIS API 对 NVIC 控制器进行编程。

3、中断生命周期

        一旦处理了中断,了解它们的生命周期就非常重要了。尽管 Cortex-M 内核会自动为我们完成大部分工作,但我们必须注意在中断管理过程中可能造成混淆的某些方面。

        中断可以:

        1. 禁用 (默认行为) 或启用:我们调用 HAL_NVIC_EnableIRQ()/HAL_NVIC_DisableIRQ()函数启用/禁用它;

        2. 处于待处理状态pending(请求正在等待送达)或未待处理not pending;

        3. 处于 Active (正在服务) 或 Inactive 状态。

        我们已经在上一段中看到过第一种情况。现在,研究发生中断时会发生什么很重要。当中断触发时,它会被标记为 pending (挂起),直到处理器可以处理它。如果当前没有其他中断正在处理,则处理器会自动清除其 pending状态,处理器几乎立即开始为其提供服务。

        特别注意:按理解,这里的pending bit是NVIC给出的ISR pending bit,与后面说的IRQ pending bit不是一个。

        上图显示了其工作原理。中断 A 在时间 t0 触发,由于 CPU 没有为另一个中断提供服务,因此其pending bit位被清除,其执行立即开始(中断变为活动状态)。在时间 t1 触发 B 中断,但在这里我们假设它的优先级低于 A。因此,它将处于待处理状态pending,直到 A ISR 结束其操作。发生这种情况时,pending位会自动清除,ISR 变为活动状态。

        在这里,重要的是要理解“立即”这个词,我们并不是说中断执行立即开始。如果没有其他中断正在运行,Cortex-M3/4/7/33 内核在 12 个 CPU 周期内提供中断,而 Cortex-M0 在 15 个周期内提供中断,Cortex-M0+ 在 16 个周期内提供中断。

        上图显示了另一个重要情况。在这里,我们发现中断A触发,CPU 可以立即为它提供服务。中断B在A提供服务时触发,因此它保持待处理状态pending,直到 A 完成。发生这种情况时,中断B的 pending bit 将被清除,并且它变为活动状态。但是,一段时间后,中断A再次触发,由于它具有更高的优先级,因此中断B被暂停(变为非活动状态),并立即开始执行A。完成此操作后,中断B将再次变为活动状态,并完成其作业。

        NVIC 为程序员提供了高度的灵活性。如上图所示中断在执行过程中可以强制再次触发,只需再次设置其pending bit 位。Cortex-M 架构的设计目的是,如果一个中断触发,而处理器已经在为另一个中断提供服务,则无需恢复之前入栈的应用程序即可对其进行服务。这种技术称为尾链,它可以加快中断管理速度并降低功耗。

        同样,当中断处于 pending 状态时,可以通过清除其 pending bit 来取消中断的执行,如下图所示。

        当中断发生时,大多数 STM32 外设都会置位一个连接到 NVIC 的特定信号,该信号映射到外设存储器中的某一特定位---Interrupt Request 位。外设的Interrupt Request 位将保持高电平,直到应用程序代码手动清除它。例如,在前面必须使用宏 __HAL_GPIO_EXTI_CLEAR_IT() 显式清除 EXTI中断线 IRQ 挂起位。如果我们不清除该位,将触发新的中断,直到它被清除。

        上图清楚地显示了外设IRQ pending状态和 ISR pending状态之间的关系。信号 I/O 是驱动 I/O 的外部外设(例如,连接到引脚的触摸开关)。当信号电平发生变化时,连接到该 I/O 的 EXTI 中断线会生成一个 IRQ 并置位相应的Interrupt Request位。因此,NVIC 会生成中断。当处理器开始为 ISR 提供服务时,ISR 挂起位会自动清除,但外设 IRQ 挂起位(Interrupt Request)将保持高电平,直到应用程序代码清除它。

        上图显示了另一种情况。这里我们强制执行 ISR 设置其 pending bit。由于此时不涉及外设,因此无需清除相应的 IRQ pending bit。由于 IRQ pending位的存在取决于外设,因此使用 ST HAL 函数来管理中断总是合适的,将所有底层细节留给 HAL 实现。但是,请记住,为避免丢失重要的中断,在开始处理 ISR 时就清除外设的 IRQ 挂起状态位是一种很好的做法。处理器内核不跟踪多个中断(它不会对中断进行排队),因此如果我们在 ISR 末尾清除外设pending 位,我们可能会丢失在中间触发的重要 IRQ。

        要查看中断是否处于待处理状态pending(即已触发但未运行),我们可以使用 HAL 函数:

uint32_t HAL_NVIC_GetPendingIRQ(IRQn_Type IRQn);

如果 IRQ not pending,则返回 0,否则返回 1。

        要设置 IRQ 的pending bit,我们可以使用 HAL 函数:

void HAL_NVIC_SetPendingIRQ(IRQn_Type IRQn);

这将导致中断触发,因为它本应该是由硬件生成的。Cortex-M 处理器的一个显着特点是,可以在一个中断的 ISR 例程内触发另一个中断。

        相反,要清除 IRQ 的pending bit,我们可以使用函数:

void HAL_NVIC_ClearPendingIRQ(IRQn_Type IRQn);

同样,可以在为一个ISR 内部取消另一处理处于pending状态的中断的执行。

        要检查 ISR 是否处于活动状态(IRQ 正在服务),我们可以使用以下函数:

uint32_t HAL_NVIC_GetActive(IRQn_Type IRQn);

如果 IRQ 处于活动状态,则返回 1,否则返回 0。

4、中断优先级

        ARM Cortex-M 架构的一个显著特点是能够对中断进行优先级排序(具有固定优先级的前三个软件异常除外)。中断优先级允许定义两件事:在并发中断的情况下定义首先执行的 ISR;高优先级的 ISR抢占。Cortex-M0/0+ 和 Cortex-M3/4/7/33 内核之间的 NVIC 优先级机制有很大不同。

4.1、Cortex-M0/0+

        基于 Cortex-M0/0+ 的微控制器具有更简单的中断优先级机制。这意味着 STM32F0/G0/L0 MCU 的行为与其他 STM32 微控制器不同。如果要在 STM32 系列之间移植代码,则必须特别注意。在 Cortex-M0/0+ 内核中,每个中断的优先级通过称为 IPR 的 8 位寄存器定义。在 ARMv6-M 内核架构中,仅使用该寄存器的 4 位,允许多达 16 个不同的优先级。然而,在实践中,实现这些内核的 STM32 MCU 仅使用该寄存器的两个高位,将所有其他位视为零。

        图         上图显示了IPR。这意味着我们只有四个最大优先级:0x00、0x40、0x80 0xC0。此数字越低,优先级越高。也就是说,优先级等于 0x40 的 IRQ 的优先级高于优先级级别等于 0xC0的 IRQ。如果两个中断同时触发,则优先级较高的中断将首先得到服务。如果处理器已经在为中断提供服务,并且触发了更高优先级的中断,则当前中断将挂起,控制权将传递到更高优先级的中断。完成此操作后,如果在此期间没有其他优先级更高的中断发生,则执行将返回到上一个中断。这种机制称为中断抢占。

        上图显示了中断抢占的示例。A 是在时间 t0 触发的优先级较低的 IRQ。ISR 开始执行,但具有较高优先级(较低优先级数字)的 IRQ B 在时间 t1 触发,并且 A ISR 的执行已停止。当 B 完成其作业时,A ISR 的执行将继续,直到完成。这种由中断优先级引起的 “嵌套” 机制导致了 NVIC 控制器的名称,即 Nested Vectored Interrupt Controller。

        Cortex-M0/0+ 与 Cortex-3/4/7/33 内核相比有一个重要的区别。中断优先级是静态的。这意味着一旦启用中断,它的优先级就不能再改变,直到我们再次禁用 IRQ。

        CubeHAL 提供了以下函数来为 IRQ 分配优先级:

void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);

        HAL_NVIC_SetPriority() 函数接受我们将要配置的 IRQn 和 PreemptPriority,这是我们将要分配给 IRQ 的抢占优先级。CMSIS API 以及 CubeHAL 库的设计使得 PreemptPriority 的优先级编号范围为 0 到 4。该值在内部自动转移到最高有效位。这简化了将代码移植到具有不同优先级位数的其他 MCU 的过程(这就是为什么芯片供应商仅使用 IPR 寄存器的左侧部分的原因)。

        HAL_NVIC_SetPriority() 函数还接受附加参数 SubPriority,该参数在 CubeF0 和 CubeL0 HAL 中被忽略,因为底层 Cortex-M 处理器不支持中断子优先级。在这里,ST决定对基于 Cortex-M3/4/7/33 的处理器使用相同 API。他们决定这样做可能是为了简化不同 STM32 MCU 之间的移植代码。奇怪的是,他们决定定义相应的函数来检索 IRQ 的优先级,方式如下:

uint32_t HAL_NVIC_GetPriority(IRQn_Type IRQn);

这与 HAL 中为基于 Cortex-M3/4/7/33 的处理器定义的完全不同。

        以下示例显示了中断优先级机制的工作原理。

uint8_t blink = 0;int main(void) {GPIO_InitTypeDef GPIO_InitStruct;HAL_Init();/* GPIO Ports Clock Enable */__HAL_RCC_GPIOC_CLK_ENABLE();__HAL_RCC_GPIOB_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();/*Configure GPIO pin : PC13 - USER BUTTON */GPIO_InitStruct.Pin = GPIO_PIN_13 ;GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;GPIO_InitStruct.Pull = GPIO_PULLDOWN;HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);/*Configure GPIO pin : PA0 */GPIO_InitStruct.Pin = GPIO_PIN_0 ;GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;GPIO_InitStruct.Pull = GPIO_PULLUP;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);/*Configure GPIO pin : PB5 - LD2 LED */GPIO_InitStruct.Pin = GPIO_PIN_5;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_LOW;HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0x1, 0);HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);HAL_NVIC_SetPriority(EXTI0_IRQn, 0x0, 0);HAL_NVIC_EnableIRQ(EXTI0_IRQn);while(1);
}void EXTI15_10_IRQHandler(void) {HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);
}void EXTI0_IRQHandler(void) {HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2);
}void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {if(GPIO_Pin == GPIO_PIN_13) {blink = 1;while(blink) {HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);for(volatile int i = 0; i < 100000; i++) {/* Busy wait */}}}else {blink = 0;}
}

        这里我们有两个与 EXTI2 和 EXTI13 关联的 IRQ。相应的 ISR 调用 HAL HAL_GPIO_EXTI_IRQHandler(),后者反过来调用 HAL_GPIO_EXTI_Callback() 回调,传递中断中涉及的 GPIO。当按下连接到 PC13 信号的用户按钮时,ISR 开始无限循环,直到闪烁全局变量为 >0。此循环使LED 快速闪烁。当引脚被置为低电平时,EXTI0_IRQHandler() 触发,这会导致 HAL_GPIO_EXTI_IRQHandler() 将 blink 变量设置为 0。EXTI15_10_IRQHandler() 现在可以结束了。每个中断的优先级配置。由于中断优先级在基于 Cortex-M0/0+ 的 MCU 中是静态的,因此我们必须在启用相应的中断之前对其进行设置。

        请注意,这是处理中断的一种非常糟糕的方法。将 MCU 锁定在中断内(ISR中的while)是一种糟糕的编程风格,它是嵌入式编程中万恶之源。不幸的是,这是作者想到的唯一例子,考虑到此时这本书仍然涵盖很少的主题。每个 ISR 都必须设计为尽可能短的持续时间,否则其他基本 ISR 可能会长时间被掩盖,从而丢失来自其他外围设备的重要信息。

        作为练习,尝试使用中断优先级,看看如果两个中断具有相同的优先级会发生什么。

        可能会出现一个很好的问题:为什么不使用 HAL_Delay() 函数而是 busywait?答案很简单,它与中断优先级直接相关。HAL_Delay() 在等待 SysTick 计时器递增全局 uwTick 变量(以 1KHZ 频率递增的无符号 32 位整数)时执行忙等待。然而,正如我们将在后面看到的那样,SysTick 定时器被配置为在中断模式下工作,并且中断优先级由 CubeMX 设置 - 默认情况下 - 尽可能低的优先级(查看TICK_INT_PRIORITY)。通过将 EXTI15_10_IRQn 中断的优先级设置为 1 将导致 SysTick 计时器的 ISR 永远不会触发,直到 blink 变量变为 0 并且 ISR 可以终止。

        您可能会注意到,通常只需触摸电线即可触发中断,即使它没有接地。为什么会这样?基本上有两个原因会导致中断 “意外” 触发。首先,现代微控制器试图最大限度地减少与使用内部上拉/下拉电阻器相关的功率泄漏。因此,这些电阻器的值选择较高(大约 50kΩ)。如果你明白分压器方程,你会发现当上拉/下拉电阻器具有高电阻值时,很容易将 I/O 拉低或拉高。其次,这里我们没有对输入引脚进行足够的去抖。去抖动是将“不稳定”源(例如机械开关)产生的反弹影响降至最低的过程。通常,在硬件或软件中执行去抖动,方法是计算从输入状态的第一个变化开始经过多少时间:在我们的例子中,如果输入保持低电平的时间超过给定的时间(通常在 100 毫秒到 200 毫秒之间就足够了),那么我们可以说输入已经有效地接地。正如我们将在后面看到的那样,我们还可以使用配置为在输入捕获模式下工作的 timer 的一个通道来检测 GPIO 何时更改状态。这使我们能够自动计算从第一个事件开始经过的时间。此外,定时器通道支持集成和可编程的硬件滤波器,这使我们能够减少外部组件的数量,以对 I/O 进行去抖。

4.2、Cortex-M3/4/7/33

        Cortex-M3/4/7/33 中的中断优先机制比基于 Cortex-M0/0+ 的微控制器中的中断优先机制更先进。开发人员具有更高的灵活性,这通常是新手头疼的几个根源。此外,ARM 和 ST 文档中呈现中断优先级的方式有点违反直觉。在 Cortex-M3/4/7/33 内核中,每个中断的优先级是通过 IPR 寄存器定义的。这是 ARMv7-M 内核架构中的 8 位寄存器,最多支持 255 个不同的优先级。然而,在实践中,实现 Cortex-M3/4/7 内核的 STM32 MCU 仅使用该寄存器的四个高位,而基于 Cortex-M33 内核的 STM32 MCU 仅使用该寄存器的三个高位。

        上图清楚地显示了IPR 的内容。这意味着我们只有 16 个最高优先级级别:0x00、0x10、0x20、0x30、0x40、0x50、0x60、0x70、0x80、0x90、0xA0、0xB0、0xC0、0xD0、0xE0、0xF0。此数字越低,优先级越高。也就是说,优先级等于 0x10 的 IRQ 的优先级高于优先级等于 0xA0的 IRQ。如果两个中断同时触发,则优先级较高的中断将首先得到服务。如果处理器已经在为中断提供服务,并且触发了更高优先级的中断,则当前中断将挂起,控制权将传递给更高优先级的中断。完成此操作后,如果在此期间没有其他优先级更高的中断发生,则执行将返回到上一个中断。

        到目前为止,其机制与 Cortex-M0/0+ 基本相同。复杂性在于 IPR 寄存器可以在逻辑上细分为两部分:定义抢占优先级的一系列位和定义子优先级的一系列位。第一个优先级控制 ISR 之间的抢占优先级。如果一个 ISR 的优先级高于另一个 ISR,它将抢占较低优先级 ISR 的执行,以防它触发。子优先级确定在多个待处理 ISR 的情况下将首先执行什么 ISR,但它不会对 ISR 抢占执行操作。

        使中断优先级的理解复杂化的是,在官方文档中,有时抢占优先级也称为组优先级。这会导致很多混淆,因为新手倾向于认为这些位定义了一种访问控制列表 (ACL) 权限。这里,为了简化对这件事的理解,我们只说抢占优先级别。

        上图显示了中断抢占的示例。A 是在时间 t0 触发的优先级最低的 IRQ。ISR 开始执行,但具有较高优先级(较低优先级)的 IRQ B 在时间 t1 触发,并且 A ISR 的执行已停止。一段时间后,C IRQ 在时间 t2 触发,B ISR 停止,C ISR 开始执行。完成此操作后,B ISR 的执行将恢复,直到完成。发生这种情况时,将恢复 A ISR 的执行。这种由中断优先级引起的 “嵌套” 机制导致了 NVIC 控制器的名称,即 Nested Vectored Interrupt Controller。

        上图显示了子优先级如何影响多个待处理 ISR 的执行。这里我们有三个中断,都具有相同的最大优先级。在时间 t0 ,IRQ A 触发并立即进行服务。在 t1 B IRQ 触发时,但由于它与其他 IRQ 具有相同的优先级,因此它处于待处理状态。在时间 t2 时,C IRQ 也会触发,但与之前相同的原因,处理器将其保留为 pending 状态。当 A ISR 完成时,首先服务C IRQ,因为它的子优先级高于 B。只有当 C ISR 完成时,才能服务B IRQ。IPR 位的逻辑细分方式由 SCB->AIRCR 寄存器(系统控制块 (SCB) 寄存器的子组)定义,并且从一开始就强调这种解释 IPR 寄存器内容的方式对所有 ISR 都是全局的,这一点很重要。一旦我们定义了一个优先级方案(在 HAL 中也称为优先级分组),它就与系统中使用的所有中断通用。

        上图显示了 IPR 寄存器的所有五个可能的细分,而下表显示了每个细分方案允许的抢占优先级和子优先级级别的最大数量。如前所述,基于 Cortex-M33 内核的 STM32 MCU 仅使用 IPR 寄存器的三个高位。这意味着最多有 8 个优先级和 3 个优先级组。因此,NVIC_PRIORITYGROUP_4 根本不可用。

        CubeHAL 提供了以下函数来为 IRQ 分配优先级:

void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);

        HAL 库的设计使 PreemptPriority 和 SubPriority 可以配置从 0 到 16 的优先级编号。该值在内部自动转移到最高有效位。这简化了将代码移植到具有不同优先级位数的其他 MCU 的过程(这就是为什么芯片供应商仅使用 IPR 寄存器的左侧部分的原因)。相反,要定义优先级分组,即如何在抢占优先级和子优先级之间细分 IPR 寄存器,可以使用以下函数:

void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);

        其中 PriorityGroup 参数是上表 2 中 NVIC Priority Group 列中的宏之一。以下示例显示了中断优先级机制的工作原理。

void SystemClock_Config(void);
uint8_t blink=0;int main(void) {GPIO_InitTypeDef GPIO_InitStruct = {0};/* MCU Configuration----------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* Configure the system clock */SystemClock_Config();/* GPIOA, GPIOB and GPIOC Configuration----------------------------------------------*//* GPIO Ports Clock Enable */__HAL_RCC_GPIOA_CLK_ENABLE();__HAL_RCC_GPIOC_CLK_ENABLE();__HAL_RCC_GPIOB_CLK_ENABLE();/*Configure GPIO pin : PC13 */GPIO_InitStruct.Pin = GPIO_PIN_13;GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;GPIO_InitStruct.Pull = GPIO_PULLDOWN;HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);/*Configure GPIO pin : PA0 */GPIO_InitStruct.Pin = GPIO_PIN_0;GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;GPIO_InitStruct.Pull = GPIO_PULLUP;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);/*Configure GPIO pin : PA5 */GPIO_InitStruct.Pin = GPIO_PIN_5;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);/* Configure GPIO pin Output Level */HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET);/* EXTI interrupt init*/HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0x1, 0x0);HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);HAL_NVIC_SetPriority(EXTI0_IRQn, 0x0, 0x0);HAL_NVIC_EnableIRQ(EXTI0_IRQn);while (1);
}void EXTI15_10_IRQHandler(void) {HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);
}void EXTI0_IRQHandler(void) {HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {if (GPIO_Pin == GPIO_PIN_13) {blink = 1;while (blink) {HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);for (volatile int i = 0; i < 100000; i++) {/* Busy wait */}}} else {blink = 0;}
}

        重要的是要注意一些基本的事情。首先,与基于 Cortex-M0/0+ 的微控制器不同,Cortex-M3/4/7/33 内核允许动态改变中断的优先级,即使已经开启了中断。其次,当优先级分组动态降低时,必须小心。

        让我们考虑以下示例。假设我们有三个 ISR,具有三个递减的优先级(优先级在括号内指定):A(0x0)、B(0x10)、C(0x20)。假设我们在优先级分组等于 NVIC_PRIORITYGROUP_4 时定义了这些优先级。如果我们将其降低到 NVIC_PRIORITYGROUP_1 级别,则当前的抢占级别将被解释为次级优先级。这将导致中断服务程序 A、B 和 C 具有相同的抢占级别(即 0x0),并且无法抢占它们。例如,查看下图,我们可以看到当优先级分组从 4 降低到 1 时,ISR C 的优先级会发生什么变化。当优先级分组设置为 4 时,C ISR 的优先级仅比最大优先级低两级,即 0(下一个最高级别是 0x10,这是 B 的优先级)。这意味着 C 可以被 A 和 B 抢占。但是,如果我们将优先级分组降低到 1,则 C 的优先级变为 0x0(只有位 7 充当优先级),其余位由 NVIC 控制器解释为次优先级。这可能导致以下情况: 1、所有中断将无法相互抢占;2、如果触发了 C 中断,并且 CPU 没有为另一个中断提供服务,则立即为 C 提供服务;3、如果 CPU 正在为 C ISR 提供服务,然后在短时间内触发 A 和 B,则 CPU 将在完成服务 C 后为 A 和 B 提供服务;4、如果 CPU 正在为另一个 ISR 提供服务,如果 C 触发,然后在片刻后触发 A 和 B,则首先为 A 提供服务,然后是 B,然后是 C。

        为了获得中断的优先级,HAL 定义了以下函数:

void HAL_NVIC_GetPriority(IRQn_Type IRQn, uint32_t PriorityGroup, uint32_t* pPreemptPriority, uint32_t* pSubPriority);

        这个函数与 HAL_NVIC_SetPriority() 不同:这里我们还必须指定 PriorityGroup,而 HAL_NVIC_SetPriority() 函数在内部计算它。

        可以使用以下函数获取当前的优先级分组:

uint32_t HAL_NVIC_GetPriorityGrouping(void);

4.3、在 CubeMX 中设置中断优先级

        CubeMX 还可用于设置 IRQ 优先级和优先级分组架构。此配置是通过 Configuration 视图完成的,单击 NVIC 按钮。此时将显示可启用的 ISR 列表,如下图所示。

        使用 Priority Group 组合框,我们可以设置优先级分组架构,然后为每个中断分配单独的优先级和子优先级。CubeMX 将自动生成相应的 C 代码,以在 MX_GPIO_Init() 函数中设置 IRQ 优先级。相反,全局优先级分组架构是在 HAL_MspInit() 函数中配置的。

5、中断重新触发

        让我们假设重新编排上面示例 ,使其使用引脚 PC12 而不是 PA0。在这种情况下,由于 EXTI12 和 EXTI13 共享相同的 IRQ,因此LED永远不会停止闪烁。由于在 Cortex-M 处理器中实现优先级机制的方式(即,具有给定优先级的异常不能被具有相同优先级的另一个异常抢占),异常和中断不是可重入的。因此,它们不能递归调用。但是,在大多数情况下,我们的代码可以重新编排以解决此限制。在以下示例中,闪烁代码在 main() 函数内执行,仅让 ISR 负责设置全局 blink 变量。

void SystemClock_Config(void);
uint8_t blink=0;int main(void) {GPIO_InitTypeDef GPIO_InitStruct = {0};/* MCU Configuration----------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* Configure the system clock */SystemClock_Config();/* GPIOA, GPIOB and GPIOC Configuration----------------------------------------------*//* GPIO Ports Clock Enable */__HAL_RCC_GPIOA_CLK_ENABLE();__HAL_RCC_GPIOC_CLK_ENABLE();__HAL_RCC_GPIOB_CLK_ENABLE();/*Configure GPIO pin : PC12 & PC13 */GPIO_InitStruct.Pin = GPIO_PIN_12 | GPIO_PIN_13;GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;GPIO_InitStruct.Pull = GPIO_PULLDOWN;HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);/*Configure GPIO pin : PB5 */GPIO_InitStruct.Pin = GPIO_PIN_5;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);/* Configure GPIO pin Output Level */HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET);/* EXTI interrupt init*/HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_1);HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0x0, 0);while (1) {if(blink) {HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);for(int i = 0; i < 100000; i++);}}
}void EXTI15_10_IRQHandler(void) {HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_12);HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);
}void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {if(GPIO_Pin == GPIO_PIN_13)blink = 1;elseblink = 0;
}

6、 一次屏蔽所有中断或优先屏蔽所有中断

        有时我们希望确保我们的代码没有被抢占以允许执行中断或更多特权代码。也就是说,我们希望确保我们的代码是线程安全的。基于 Cortex-M 的处理器允许暂时屏蔽所有中断和异常的执行,而无需一一禁用。两个名为 PRIMASK 和 FAULTMASK 的特殊寄存器分别允许禁用所有中断和异常。

        即使这些寄存器是 32 位宽的,也只使用第一位来启用/禁用中断和异常。ARM 汇编指令 CPSID i 通过将 PRIMASK 位设置为 1 来禁用所有中断,而 CPSIE i 指令通过将 PRIMASK 设置为零来启用它们。相反,指令 CPSID f 通过将 FAULTMASK 位设置为 1 来禁用所有异常(NMI 异常除外),而 CPSIE f 指令则启用它们。

        CMSIS-Core 包提供了几个宏,我们可以使用它们来执行这些操作:__disable_irq() 和 __enable_irq() 自动设置和清除 PRIMASK。任何关键任务都可以放在这两个宏之间,如下所示:

...
__disable_irq();
/* 所有具有可配置优先级的异常都将被暂时禁用。你可以在这里放置关键代码 */
...
__enable_irq();

        但是,请记住,作为一般规则,中断必须仅在非常短的时间内被屏蔽,否则您可能会丢失重要的中断。请记住,中断不会排队。

        我们可以使用的另一个宏是 __set_PRIMASK(x) 值,其中 x 是 PRIMASK 寄存器的内容(0 或 1)。宏 __get_PRIMARK() 返回 PRIMASK 寄存器的内容。相反,宏 __set_FAULTMASK(x) 和 __get_FAULTMASK() 允许操作 FAULTMASK 寄存器。

        需要注意的是,一旦 PRIMASK 寄存器再次设置为零,所有挂起的中断都根据其优先级进行处理: PRIMASK 导致中断挂起位被设置,但 ISR 没有得到处理。这就是为什么我们说中断被屏蔽而不是禁用的原因。一旦清除 PRIMASK,就会开始为中断提供服务。

        Cortex-M3/4/7/33 内核允许根据优先级选择性地屏蔽中断。BASEPRI 寄存器在优先级级别屏蔽异常或中断。BASEPRI 寄存器的宽度与 IPR 寄存器相同,在基于 Cortex-M3/4/7 内核的 STM32 MCU 中持续 4 位,在基于 Cortex-M33 的 STM32 MCU 中持续 3 位。当 BASEPRI 设置为 0 时,它将被禁用。当它设置为非零值时,它会阻止具有相同或较低优先级的异常(包括中断),同时仍允许处理器接受具有较高优先级的异常。例如,如果 BASEPRI 寄存器设置为 0x60,则所有优先级介于 0x60~0xFF之间的中断都将被禁用。请记住,在 Cortex-M 内核中,优先级数字越高,中断优先级级别越低。__set_BASEPRI(x) 宏允许设置 BASEPRI 寄存器的内容:再次记住,HAL 会自动将优先级转移到 MSB 位。因此,如果我们想禁用所有优先级高于 2 的中断,那么我们必须将值 0x20 传递给 __set_BASEPRI()宏。或者,我们可以使用以下代码:__set_BASEPRI(2 << (8 - __NVIC_PRIO_BITS));

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/463197.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux操作系统进程

linux操作系统是对下的软硬件进行管理&#xff0c;为了能够对上提供稳定&#xff0c;快速&#xff0c;安全的服务而诞生的软件。 广义上的操作系统是包含搭载在操作系统上的软件和函数库等文件的。 狭义上的操作系统就是操作系统内核&#xff0c;进行进程管理&#xff0c;文件…

js 获取当前时间与前一个月时间

// 获取当前时间的毫秒数 var currentTimeMillis new Date().getTime();// 获取前一个月的Date对象 var dateLastMonth new Date(); dateLastMonth.setMonth(dateLastMonth.getMonth() - 1);// 获取前一个月的毫秒数 var timeMillisLastMonth dateLastMonth.getTime();conso…

php内置服务停止shell小工具,用来停止指定的端口的php内置服务进程

最近vscode总是喜欢闪退&#xff0c;这导致了上面启动的php内置服务变成了无法管理状态&#xff0c;所以就有了这个工具来停止相关的PHP内置服务进程. 将下面的代码保存到本地合适的位置&#xff0c;并命名为 stop.sh #!/bin/bash # Author: tekintian # Date: 2024-11-02 …

服务器文件访问协议

服务器文件访问协议 摘要NFS、CIFS、SMB概述SMBWindows SMBLinux SambaPython SMB NFS 摘要 本篇博客参考网上文档和博客&#xff0c;对基于网络的服务器/主机的文件访问、共享协议进行简要总结&#xff0c;完整内容将会不断更新&#xff0c;以便加深理解和记忆 NFS、CIFS、S…

Leetcode - 周赛421

目录 一&#xff0c;3334. 数组的最大因子得分 二&#xff0c;3335. 字符串转换后的长度 I 三&#xff0c;3336. 最大公约数相等的子序列数量 四&#xff0c;3337. 字符串转换后的长度 II 一&#xff0c;3334. 数组的最大因子得分 暴力方法就不演示&#xff0c;这里介绍一个…

【java】java的基本程序设计结构06-运算符

运算符 一、分类 算术运算符关系运算符位运算符逻辑运算符赋值运算符其他运算符 1.1 算术运算符 操作符描述例子加法 - 相加运算符两侧的值A B 等于 30-减法 - 左操作数减去右操作数A – B 等于 -10*乘法 - 相乘操作符两侧的值A * B等于200/除法 - 左操作数除以右操作数B /…

群控系统服务端开发模式-应用开发-菜单功能开发

为什么优先开发菜单&#xff0c;而不是优先开发管理员&#xff1f;查看一下程序草图就明白&#xff0c;还有一个重点就是&#xff0c;管理员需要添加图片&#xff0c;而我还没有封装上传工具及上传目标。 一、添加路由 在根目录下route文件夹下的app.php文件里面&#xff0c;添…

顶点动画-河流的效果

目标是让一个矩形网格面片&#xff0c;通过顶点动画&#xff0c;实现出河流的效果。&#xff08;如下图&#xff09;所谓的河流效果&#xff0c;就是呈现出波浪感&#xff0c;而想要呈现出波浪感&#xff0c;我们必须了解 波长、波动频率、波动幅度 这些关键因素 1、波浪感的关…

线程函数和线程启动的几种不同形式

线程函数和线程启动的几种不同形式 在C中&#xff0c;线程函数和线程启动可以通过多种形式实现。以下是几种常见的形式&#xff0c;并附有相应的示例代码。 1. 使用函数指针启动线程 最基本的方式是使用函数指针来启动线程。 示例代码&#xff1a; #include <iostream&g…

3.1 快速启动Flink集群

文章目录 1. 环境配置2. 本地启动3. 集群启动4. 向集群提交作业4.1 提交作业概述4.2 添加打包插件4.3 将项目打包4.4 在Web UI上提交作业4.5 命令行提交作业 在本实战中&#xff0c;我们将快速启动Apache Flink 1.13.0集群&#xff0c;并在Hadoop集群环境中提交作业。首先&…

讲讲RabbitMQ 性能优化

大家好&#xff0c;我是锋哥。今天分享关于【RabbitMQ 性能优化&#xff1f;】面试题。希望对大家有帮助&#xff1b; 讲讲RabbitMQ 性能优化 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 RabbitMQ 是一个强大的消息代理&#xff0c;广泛用于分布式系统中&#x…

redolog与binlog的写入机制

redo log 事务在执行的过程中&#xff0c;生成的redo log是要先写到redo log buffer中的。redo log buffer里面的内容不需要每次生成后都直接持久化到磁盘。 如果事务执行期间MySQL发生异常重启&#xff0c;那这部分日志就丢了&#xff0c;但是由于没有commit&#xff0c;所以…

推荐一款数学绘图工具:FX Draw Tools

FX Draw Tools是目前最新好用的一款数学绘图工具。该软件界面简洁&#xff0c;使用方便。该软件能够帮助用户快速制作数学图表&#xff0c;从而提高用户的工作效率&#xff0c;轻松完成制图工作&#xff0c;欢迎需要的用户前来下载使用。 功能特色 1. 180和360可以被添加到任何…

《云计算网络技术与应用》实训8-1:OpenvSwitch简单配置练习

1.按《云计算网络技术与应用》实训5-1进行环境配置&#xff0c;安装好OVS 2.开启OVS虚拟交换机 3.创建一个网桥br0 4.查看网桥列表 5.把ens34网卡连接到网桥br0上 6. 查看网桥br0所有端口 7.列出网卡ens34连接的所有网桥列表 8.查看OVS网络状态 9.将网桥br0上连接的网卡ens34删…

Netty 组件介绍 - pipeline

ChannelPipeline为ChannelHandler链提供了容器&#xff0c;并且定义了该链上的入站和出站事件。当initChannel()被调用时&#xff0c;ChannelInitializer将在ChannelPipeline中安装一组自定义的ChannelHandler。他们的执行顺序就是添加顺序。 Server public class Server {pr…

Leetcode 热题100 之 二叉树3

1.二叉树展开为链表 思路分析&#xff1a;迭代法。对于每个节点&#xff0c;我们将其左子树放到右子树的位置。将原来的右子树接到新的右子树&#xff08;也就是原来的左子树&#xff09;的末端。移动到右子节点&#xff0c;继续处理下一节点&#xff0c;直到所有节点都处理完。…

UE5.4 PCG Layered Biomes插件

B站学习链接 官方文档 一、PCGSpawn Preset&#xff1a;负责管理PCG要用到的植被资产有哪些 二、BiomesSettings&#xff1a;设置要使用的植被资产Layer、Spawn参数 1.高度Layer参数&#xff1a; 2.地形Layer&#xff1a;我这里用地形样条线绘制了一块地形Layer 绘制点和…

单个相机矫正畸变

1、通过标定助手获取到内参外参&#xff0c;外参在此无效&#xff0c;只用到了内参 2、然后通过halcon算子进行矫正 参考&#xff1a;超人视觉

Orleans8.2入门测试

微软官方文档&#xff1a;快速入门&#xff1a;使用 ASP.NET Core 生成第一个 Orleans 应用 - .NET | Microsoft Learn 项目及引入的nuget库&#xff1a; 1、接口项目&#xff1b;2、接口实现项目&#xff1b;3、silo项目&#xff1b;4、客户端项目 其中Microsoft.Orleans.St…

电赛入门之软件stm32keil+cubemx

hal库可以帮我们一键生成许多基本配置&#xff0c;就不需要自己写了&#xff0c;用多了hal库就会发现原来用基本库的时候都过的什么苦日子&#xff08;笑 下面我们以f103c8t6&#xff0c;也就是经典的最小核心板来演示 一、配置工程 首先来新建一个工程 这里我们配置rcc和sys&…