FreeRTOS基础篇(六)——中断管理

前言:
   本篇文章介绍FreeRTOS的移植方法,以stm32f103举例。

1 介绍

   中断 是硬件或软件发出的信号,通知CPU暂停当前任务,去处理更紧急的事件(如定时器溢出、串口接收数据等)。处理中断的函数称为 中断服务程序(ISR, Interrupt Service Routine) 。中断处理应尽可能快,避免阻塞系统,因此中断内要处理的东西应该尽可能精简。
  FreeRTOS对中断进行了优化,使其与任务调度协同工作。主要特点包括:

  • 可屏蔽中断(Maskable Interrupts)
    • FreeRTOS在进入关键区(如修改内核数据结构)时会临时关闭中断,防止竞态条件。
    • 使用 taskENTER_CRITICAL()taskEXIT_CRITICAL() 来保护临界区。
  • 中断优先级管理
    • 在Cortex-M系列MCU上,FreeRTOS将中断分为两类:
      • 可由FreeRTOS管理的中断:优先级较低,可以在ISR中调用FreeRTOS API。
      • 高优先级中断(不被RTOS延迟):优先级很高,不能调用大多数FreeRTOS函数。
  • 从中断唤醒任务
    • ISR可以通过特定的API向任务发送信号(如队列、信号量),并请求任务切换(pend a context switch)。

2 API

  为了在ISR中安全操作,FreeRTOS提供了FromISR版本的API,例如:

功能 任务中使用 中断中使用
发送消息到队列 xQueueSend() xQueueSendFromISR()
接收消息从队列 xQueueReceive() xQueueReceiveFromISR()
给二值/计数信号量 xSemaphoreGive() xSemaphoreGiveFromISR()
获取互斥信号量 xSemaphoreTake() 不允许在 ISR 中获取

  总之,中断中不允许使用可能导致阻塞的API,所以单独提供了ISR版本的API接口。

3 中断优先级配置(Cortex-M)

3.1 configMAX_SYSCALL_INTERRUPT_PRIORITY

  这个宏定义了一个逻辑优先级边界,划分为 可被内核屏蔽的中断不受内核影响的中断 的边界优先级。举例更能说明清楚,假设 configMAX_SYSCALL_INTERRUPT_PRIORITY 值为5,则可推理出:

  • “等于或低于逻辑优先级 5”的中断,其优先级数值是:5, 6, 7, …, 15。这些中断可以被内核屏蔽。
  • “高于逻辑优先级 5”的中断,其优先级数值是:0, 1, 2, 3, 4。这些中断永远不能被内核屏蔽,拥有最高实时性。

  逻辑优先级等于或低于此值(即优先级数值 ≥ 此值)的中断,才能安全调用FreeRTOS API,优先级0~4的中断不能调用 FromISR API,它们“凌驾于RTOS之上”。

3.2 为什么“高于”此边界(数值更小)调用 API 不安全?

  • 它不能被屏蔽:由于它的逻辑优先级“高于” configMAX_SYSCALL_INTERRUPT_PRIORITY 设定的边界,当 FreeRTOS 进入临界区时,BASEPRI 寄存器无法屏蔽这类中断。它随时可以抢占正在运行的内核代码或任务。
  • 引发数据竞争:如果在这个高优先级的 ISR 中调用了 xQueueSendFromISR 这类 API,而恰好在它执行时,一个低优先级的任务或中断也正在操作同一个队列,就会发生数据竞争。内核用于保护数据的临界区机制对这类中断无效,因为屏蔽不了它。
  • 导致系统崩溃:这种未受保护的并发访问可能会破坏链表、计数器等内核数据结构,导致队列丢失数据、任务状态错乱,甚至系统死锁或崩溃。
中断逻辑优先级 中断优先级数值 与边界 (5) 的关系 能否被内核屏蔽? 可否安全调用 FreeRTOS API? 典型用途
极高 0, 1, 2, 3, 4 高于边界 不能 绝对不可以 电机紧急制动、看门狗、高速ADC采样
中/低(可管理) 5, 6, 7, …, 15 等于或低于边界 可以 可以(且必须用 FromISR 结尾的API) 串口接收完成、定时器、按键检测

3.3 configMAX_SYSCALL_INTERRUPT_PRIORITY范围

  configMAX_SYSCALL_INTERRUPT_PRIORITY的数值并没有一个固定的、全局通用的范围。它的有效数值完全取决于你使用的具体Cortex-M微控制器硬件。这个宏配置的是一个经过位对齐后的“硬件值”,其范围上限取决于芯片厂家实现了多少位中断优先级,比如STM32通常是4位优先级,那么此时FreeRTOS中断的有效范围是0~15(0最高,15最低)。如果芯片只支持2位优先级,那么有效范围是0~3。STM32的Cortex-M内核采用 数值越小,优先级越高 的逻辑,通常使用4位,可表示 16级 (0-15) 逻辑优先级。

4 PendSV和SysTick

  FreeRTOS 利用 Cortex-M 内建的两个异常来实现调度和时间管理:

异常 用途
SysTick 提供周期性时钟节拍(tick),驱动 RTOS 时间管理和延时。
PendSV 负责上下文切换。当需要从 ISR 返回到更高优先级任务时触发。

  假设初始状态是低优先级任务A正在运行,我们来追踪接下来发生的每一步:

  • 中断来临,CPU强制转场。
    • 时刻 T0:任务A的代码正在执行。
    • 时刻 T1:一个硬件中断B发生(例如,定时器时间到)。CPU立刻保存A的当前执行现场(将关键寄存器压入任务A的栈),然后跳转到中断B的服务程序开始执行。此时,任务A被“中断挂起”。
  • 中断服务程序中的操作。
    • 时刻 T2:在中断B的代码中,调用了 xQueueSendFromISR() 发送数据,并成功唤醒了高优先级任务C。函数会设置一个标志位 xHigherPriorityTaskWoken = pdTRUE。
  • 关键决策:触发“延迟切换”
    • 时刻 T3:中断B的服务程序执行完毕。在它最后的 portYIELD_FROM_ISR() 函数中,检测到标志位为TRUE。于是,它并不直接切换,而是向CPU“预约”了一个名为 PendSV(可挂起的系统调用)的异常。
    • 时刻 T4:CPU从中断模式退出。按照规则,它需要恢复之前被中断的现场。于是,CPU将之前保存的寄存器从任务A的栈中弹出,自然而然地回到了任务A的代码中继续执行。此时,中断B在逻辑上已经结束。
  • “延迟切换”生效,调度器执行切换
    • 时刻 T5:由于在第3步“预约”了 PendSV 异常,并且它的优先级被设为最低,所以CPU在执行完中断返回后,立刻又进入了 PendSV 异常的处理程序。这里才是真正的任务切换现场。
    • 时刻 T6:在 PendSV 中,调度器开始工作:
      • 将任务A的完整上下文(所有寄存器)保存到任务A的任务控制块(TCB)。
      • 从任务C的任务控制块(TCB) 中恢复任务C的完整上下文。
    • 时刻 T7:PendSV 异常处理完毕,执行返回。由于上下文已被替换,CPU的返回地址和所有寄存器都变成了任务C的,因此CPU开始执行任务C的代码。任务A则被放入就绪列表,等待未来再次被调度。
  • 任务C执行完毕,系统可能再次调度A
    • 任务C会一直执行,直到它主动阻塞(例如调用 vTaskDelay 或等待队列),或另一个更高优先级的任务被唤醒。
    • 当任务C阻塞后,调度器会从就绪列表中找出当前最高优先级的就绪任务。如果此时任务A仍然是最高优先级的就绪任务,那么调度器就会切换回任务A,从它上次被切换出去的地方(即时刻T5之后,T6之前的那条指令)继续执行。

  FreeRTOS采用了一个巧妙的设计: portYIELD_FROM_ISR() 这个函数实际上只是简单地手动触发了一个名为PendSV(可挂起的系统调用)的异常。你可以把这个异常看作一个 延迟切换 的请求。

注意: 不要手动修改PendSV或SysTick的优先级。通常PendSV设为最低优先级,以便多个中断可以合并一次调度。

3 运用

  Cortex-M为例,典型中断处理流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义一个队列,用于传递数据给任务。
QueueHandle_t xQueue;

// 中断服务程序。
void example_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
char cReceivedChar;

// 中断处理。
// ...

// 将数据发送到队列(ISR 版本)。
xQueueSendFromISR(xQueue, &cReceivedChar, &xHigherPriorityTaskWoken); # 唤醒了某个任务。

// 如果有更高优先级的任务被唤醒,则请求上下文切换。
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
  • xHigherPriorityTaskWoken:是一个输出参数,表示是否有任务因本次操作变为就绪态且优先级高于当前任务。
  • portYIELD_FROM_ISR():如果需要切换任务,它会触发 PendSV 异常(Cortex-M 中用于任务切换的机制)。

将使用 FreeRTOS API 的中断设置为 >= configMAX_SYSCALL_INTERRUPT_PRIORITY(数值上更大,实际优先级更低)。

  FreeRTOS的设计目标之一是低中断延迟。以下是影响中断响应的因素:

因素 影响 建议
关中断时间长 延迟中断响应 尽量减少临界区代码
高优先级任务频繁运行 抢占调度开销 合理分配任务优先级
使用 configMAX_SYSCALL_INTERRUPT_PRIORITY 不当 可能导致死锁或不可预测行为 严格遵守规则