中断管理

中断是一种由硬件或软件触发的机制,用于打断当前程序的执行流程,转而去处理更高优先级的任务。

中断执行流程

以下是一个典型的中断处理流程,通常以硬件中断为例:

  1. 触发中断
  • 硬件设备产生中断信号,通知处理器有事件需要处理。
  • 处理器检测到中断信号后,当前程序暂停,进入中断响应阶段。
  1. 中断优先级判断
  • 如果有多个中断信号,处理器根据中断优先级选择需要响应的中断。
  • 如果当前有更高优先级的任务正在运行,则可能推迟低优先级中断的响应。
  1. 保存上下文
  • 处理器保存当前的执行上下文(包括程序计数器 PC、通用寄存器、状态寄存器等),以便中断处理完成后可以恢复。
  • 在某些处理器中,这由硬件自动完成,或者需要软件显式完成。
  1. 跳转到中断向量表
  • 处理器根据中断号查找中断向量表(Interrupt Vector Table, IVT),获取对应中断服务例程(ISR)的入口地址。
  • 中断向量表是一个存储各中断服务例程地址的数组。
  1. 执行中断服务例程(ISR)
  • 处理器跳转到中断服务例程的入口地址,执行中断处理代码。
  • 中断服务例程是用户定义的程序,用于处理特定的中断事件。
  1. 中断结束与恢复
  • 中断服务例程执行完成后,调用 IRET(中断返回)或类似指令,恢复保存的上下文。
  • 处理器返回到被中断的程序,继续执行。

ARM 中的中断优先级

ARM Cortex-M 架构自身允许最多 256 个不同的优先级(最多有 8 个 优先级位,因此从 0 到 0xff 的优先级都是可能的),但绝大多数使用 ARM Cortex-M 核心的微控制器仅允许使用其中一部分;

ARM Cortex-M4F 使用 4(低四位) 位优先级寄存器来配置中断的优先等级,共分为两个部分:抢占优先级和子优先级。分配给每个部分的位数是可配置的。抢占优先级定义了一个中断是否可以抢占另一个正在执行的中断。当两个抢占优先级相同的中断同时发生时,子优先级决定首先执行哪个中断。

在 ARM Cortex-M 核心中,优先级的数值越小, 则中断的逻辑优先级越高。

分组 抢占优先级 响应优先级
分组0 0(可取值为0) 4(可取值为0到15)
分组1 1(可取值为0到1) 3(可取值为0到7)
分组2 2(可取值为0到3) 2(可取值为0到3)
分组3 3(可取值为0到7) 1(可取值为0到1)
分组4 4(可取值为0到15) 0(可取值为0)

在使用 FreeRTOS 时,建议将所有优先级位都指定为抢占优先级位, 不保留任何优先级位作为子优先级位。

任何其他配置都会使 configMAX_SYSCALL_interrupt_PRIORITY 设置与分配给各个外设中断之间的直接关系复杂化。

1
nvic_priority_group_set(NVIC_PRIGROUP_PRE4_SUB0);

FreeRTOS 中的中断优先级

FreeRTOS 的中断嵌套方案将可用的中断优先级分为两组:一组将被 RTOS 临界区屏蔽,另一组 永远不会被 RTOS 临界区屏蔽,因此始终处于启用状态。两个组之间的边界由 FreeRTOSConfig.h 中的 configMAX_SYSCALL_INTERRUPT_PRIORITY 设置定义。此设置的最佳值将取决于微控制器中实现的优先级位数量。

临界区是指一段代码,它在执行过程中需要独占访问某些共享资源(如全局变量、外设寄存器等),以防止并发访问导致的数据不一致问题。中断中的临界区通过禁用某些中断来实现,这样可以保证这段代码不被中断打断,从而避免资源访问冲突。

简单来说 FreeRTOS 中的中断分为以下两类:

  1. 非 FreeRTOS 管理的中断(中断优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY):
    • 这些中断不能调用 FreeRTOS 提供的 API 函数
    • 适用于对实时性要求非常高的事件。
    • 它们的优先级设置必须高于 configMAX_SYSCALL_INTERRUPT_PRIORITY(即优先级数值小于)。
  2. FreeRTOS 管理的中断(中断优先级低于等于 configMAX_SYSCALL_INTERRUPT_PRIORITY):
    • 可以调用 FreeRTOS 提供的中断安全 API
    • 中断优先级必须低于或等于 configMAX_SYSCALL_INTERRUPT_PRIORITY(即优先级数值大于或等于),否则可能导致系统行为异常。

数值优先级与逻辑优先级设置之间的反转关系

在 ARM Cortex-M 核心中,优先级的数值越小, 则中断的逻辑优先级越高

在 FreeRTOS 中以 “FromISR” 结尾的 FreeRTOS 函数是中断安全的,但前提是调用这些函数的中断的逻辑优先级不高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 定义的优先级;

因此,对于任何使用一个 RTOS API 函数的中断服务程序,必须为其手动设置为一个数值优先级, 这个值必须等于或大于 configMAX_SYSCALL_INTERRUPT_PRIORITY 设定的值。这确保了中断的逻辑优先级等于或小于 configMAX_SYSCALL_INTRUPT_PRIORITY 设置。

FreeRTOS 规定,调用 “FromISR” 函数的中断必须满足:

  • 逻辑优先级 ≤ configMAX_SYSCALL_INTERRUPT_PRIORITY
  • 数值优先级 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY

由于不同的 Cortex-M 微控制器使用的优先级位数可能有所不同(通常少于 8 位),为了统一处理这些差异,FreeRTOS 将硬件原生的优先级值映射到一个统一的 8 位优先级范围。这种映射机制确保了在不同 Cortex-M 微控制器上实现一致的中断优先级管理。configKERNEL_INTERRUPT_PRIORITY 和 configMAX_SYSCALL_INTERRUPT_PRIORITY

对于仅实现 configKERNEL_INTERRUPT_PRIORITY 的移植:

configKERNEL_INTERRUPT_PRIORITY 设置 RTOS 内核自身使用的中断优先级。(SysTick 也需要一个中断优先级)

调用 API 函数的中断也必须在此优先级下执行。不调用 API 函数的中断可以在更高的优先级下执行,因此其执行不会因 RTOS 内核活动而延迟 (在硬件本身的限制范围内)。

如果微处理器本身与 FreeRTOS 一致,则可以不需要使用 configMAX_SYSCALL_INTERRUPT_PRIORITY 抽象一层;

对于同时实现 configKERNEL_INTERRUPT_PRIORITY 和 configMAX_SYSCALL_INTERRUPT_PRIORITY 的移植:

configKERNEL_INTERRUPT_PRIORITY 设置 RTOS 内核自身使用的中断优先级。

configMAX_SYSCALL_INTERRUPT_PRIORITY 设置可以调用中断安全 FreeRTOS API 函数的最高中断优先级。

宏介绍

1
2
3
4
5
6
7
8
9
10
11
// 定义 MCU 最多支持几位优先级 Cortex-M 是 4 位,0~15
#define configPRIO_BITS 4
// 定义最低优先级的值
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 0xf
// 定义可以被 FreeRTOS 管理的最高优先级中断,
// 这里定义为5,也就是说优先级高于5的中断FreeRTOS是不可管理的,低于5的才可管理。
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
// 设置 RTOS 内核使用的中断优先级(最低)
#define configKERNEL_INTERRUPT_PRIORITY (configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
// 设置可以调用中断安全 FreeRTOS API 函数的中断的最高逻辑优先级(数值最小,逻辑优先级最高)。
#define configMAX_SYSCALL_INTERRUPT_PRIORITY (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))

RTOS 内核的核心功能依赖于硬件的 SysTick 和其他可能需要的中断服务(例如上下文切换)。

中断优先级与任务优先级

freertosrwzdyxj

实现

FreeRTOS 主要用于帮我们管理中断的关闭与开启

1
2
3
4
// 屏蔽中断
portENABLE_INTERRUPTS();
// 恢复中断
portDISABLE_INTERRUPTS();

FreeRTOS 的中断屏蔽与恢复是通过设置 BASEPRI 寄存器的值来实现的(写 0 屏蔽,写 1 恢复)

BASEPRI 是 ARM Cortex-M 处理器架构中的一个 8 位寄存器,逻辑为:

  • 当 BASEPRI 设置为 0 时,所有中断都可以响应(没有屏蔽)。
  • 当 BASEPRI 设置为某个非零值时,优先级低于该值的中断将被屏蔽。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include "main.h"

TaskHandle_t start_handler;
TaskHandle_t task_key_handler;

void task_key(void *pvParameters) {
uint32_t flag = 0;
FlagStatus pre_state = RESET;
while(1) {
FlagStatus state = gpio_input_bit_get(GPIOA, GPIO_PIN_0);
if(SET == state && pre_state == RESET) {
// 当前高电平, 上一次为低电平,按下
pre_state = state;
if(flag == 0) {
printf("disable \r\n");
// 关闭中断
portDISABLE_INTERRUPTS();
} else if(flag == 1) {
// 开启中断
printf("enable \r\n");
portENABLE_INTERRUPTS();
}
flag++;
if(flag > 1) flag = 0;
} else if(RESET == state && pre_state == SET) {
// 当前高电平, 上一次为低电平,抬起
pre_state = state;
}
delay_1ms(20);
}
}

void Usart0_recv(uint8_t *data, uint32_t len) {
printf("recv: %s\r\n", data);
}

static void GPIO_config() {
// 时钟初始化
rcu_periph_clock_enable(RCU_GPIOA);
// 配置GPIO模式
gpio_mode_set(GPIOA, GPIO_MODE_INPUT, GPIO_PUPD_PULLDOWN, GPIO_PIN_0);
}

#define PRESCALER 10000
#define FREQ 1

static void TIMER_config() {
// 时钟配置
rcu_periph_clock_enable(RCU_TIMER5);

// 复位定时器
timer_deinit(TIMER5);

rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4);
timer_parameter_struct tps;
timer_struct_para_init(&tps);
tps.prescaler = PRESCALER - 1; // 分频系数 240 000 000
tps.period = SystemCoreClock / PRESCALER / FREQ - 1; // 周期计数值 1Hz

timer_init(TIMER5, &tps);
nvic_irq_enable(TIMER5_DAC_IRQn, 5, 0);
timer_interrupt_enable(TIMER5, TIMER_INT_UP);
timer_enable(TIMER5);
}

void TIMER5_DAC_IRQHandler(void) {
if(SET == timer_interrupt_flag_get(TIMER5, TIMER_INT_UP)) {
printf("timer\r\n");
}
//清除中断标志位
timer_interrupt_flag_clear(TIMER5,TIMER_INT_FLAG_UP);
}

void start_task(void *pvParameters) {
GPIO_config();
usart0_init();
TIMER_config();

printf("start\r\n");
taskENTER_CRITICAL();

xTaskCreate(task_key, "task_key", 64, NULL, 2, &task_key_handler);
// 这里不是是 NULL 如果删除的是 start_task 会导致 task_key 也被删除
vTaskDelete(start_handler);
taskEXIT_CRITICAL();
}

int main(void) {
// 配置抢占优先级
nvic_priority_group_set(NVIC_PRIGROUP_PRE4_SUB0);
// 创建任务
xTaskCreate(start_task, "start_task", 128, NULL, 1, &start_handler);
// 开启滴答时钟的
vTaskStartScheduler();
while(1) {}
}

问题

一、vTaskDelay 函数为什么会恢复中断

vTaskDelay 函数会调用 xTaskResumeAll 函数,在 xTaskResumeAll 这个函数中,他会执行 taskENTER_CRITICAL 和 taskEXIT_CRITICAL 函数,这两个函数分别起到了控制中断屏蔽的作用。

  • taskENTER_CRITICAL:这个宏会调用 portDISABLE_INTERRUPTS,通常是通过设置 BASEPRI 寄存器来屏蔽中断。

  • taskEXIT_CRITICAL:这个宏会恢复 BASEPRI 寄存器的值,解除屏蔽低优先级的中断。

二、如果在中断中执行while(1)会怎么样

中断无法结束:中断服务例程的目标是快速处理必要的操作并尽早返回,使得系统可以继续响应其他任务和中断。如果在 ISR 中执行 while(1),中断服务例程会永远处于活动状态,不会退出,这会阻塞中断的返回,系统无法恢复到正常的执行流。

如果系统允许中断嵌套(即优先级较高的中断可以中断较低优先级的中断),那么当 while(1) 导致当前中断服务例程无法退出时,其他待处理的中断会被挂起,直到当前 ISR 完成。这意味着新的中断不会得到及时处理,可能会丢失重要的数据或事件。

如果系统禁用了中断嵌套(通过设置更高的优先级屏蔽),则新的中断在 ISR 执行时将被禁止,导致该中断甚至不会被触发,直到当前 ISR 执行完毕。