FreeRTOS 中断管理:优先级、底层原理与 API 实践


本文以常见的 ARM Cortex-M3/M4/M7 + 单核 FreeRTOS 为主,解释 FreeRTOS 如何划分中断优先级、怎样利用 BASEPRI 保护内 核,以及在 ISR 中调用 API 时应遵守哪些规则。

内容导航


核心结论

FreeRTOS 的中断管理可以浓缩成四句话:

  1. FreeRTOS 不接管 NVIC,只规定哪些中断可以访问内核。
  2. Cortex-M3/M4/M7 端口通常使用 BASEPRI,只屏蔽一部分中断,而不是全局关中断。
  3. 只有名称以 FromISR 结尾、并被文档明确允许的 API 才能在 ISR 中调用。
  4. ISR 唤醒任务后,通过 PendSV 延迟执行上下文切换,不会直接在 ISR 内切换任务。

最重要的安全规则
能够绕过 FreeRTOS 临界区的高优先级中断,绝不能调用任何 FreeRTOS API,包括 FromISR API。


中断优先级分层

假设 MCU 实现 4 个 NVIC 优先级位,并配置:

#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY  5

此时逻辑优先级范围是 0~15

NVIC 优先级 逻辑紧迫性 FreeRTOS API FreeRTOS 临界区内 典型用途
0~4 最高 全部禁止 仍可响应 紧急保护、高速采样、极低延迟控制
5 安全边界 FromISR 被暂时屏蔽 最高紧迫性的 RTOS 感知中断
6~15 较低 FromISR 被暂时屏蔽 UART、DMA、CAN、普通定时器
15 最低 FromISR 被暂时屏蔽 PendSV、SysTick 通常位于此级别
高紧迫性

NVIC 0  ─┐
         │ 0~4:不受 FreeRTOS BASEPRI 临界区屏蔽
NVIC 4  ─┘       禁止调用任何 FreeRTOS API

NVIC 5  ─── configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
            可调用 FromISR API 的最高紧迫性边界

NVIC 6  ─┐
         │ 6~15:允许调用 FromISR API
NVIC 15 ─┘        FreeRTOS 临界区内会被暂时屏蔽

低紧迫性

两套优先级方向相反

对象 数值规律
NVIC 中断优先级 数值越小,优先级越高
FreeRTOS 任务优先级 数值越大,优先级越高

因此,不能直接比较 NVIC 优先级和任务优先级。


优先级数值与寄存器编码

Cortex-M 的优先级寄存器宽度为 8 位,但芯片通常只实现最高若干位。以常见的 4 位实现为例:

#define __NVIC_PRIO_BITS  4

逻辑优先级需要写入寄存器高 4 位:

逻辑优先级 5   = 0101
硬件寄存器值   = 0101 0000 = 0x50

逻辑优先级 15  = 1111
硬件寄存器值   = 1111 0000 = 0xF0

典型配置如下:

#define configPRIO_BITS                                  4
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY          15
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY     5

#define configKERNEL_INTERRUPT_PRIORITY                  \
    (configLIBRARY_LOWEST_INTERRUPT_PRIORITY <<          \
     (8 - configPRIO_BITS))

#define configMAX_SYSCALL_INTERRUPT_PRIORITY             \
    (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY <<     \
     (8 - configPRIO_BITS))

在这个例子中:

configKERNEL_INTERRUPT_PRIORITY      = 0xF0
configMAX_SYSCALL_INTERRUPT_PRIORITY = 0x50

CMSIS 和 STM32 HAL 通常接收未移位的逻辑值

HAL_NVIC_SetPriority(DMA1_Stream0_IRQn, 5, 0);
NVIC_SetPriority(DMA1_Stream0_IRQn, 5);

configMAX_SYSCALL_INTERRUPT_PRIORITY 使用已经移位的硬件格式。混淆 50x50 是最常见的配置错误之一。

优先级分组

建议把所有优先级位都用于抢占优先级,不保留子优先级。STM32 HAL 常见配置为:

HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

重点不是厂商常量的名字,而是最终形成:

4 位抢占优先级 + 0 位子优先级

BASEPRI 与内核临界区

Cortex-M3/M4/M7 具有 BASEPRI 寄存器。FreeRTOS 将它设置为 configMAX_SYSCALL_INTERRUPT_PRIORITY,从而只屏蔽一个优先级区间。

当:

BASEPRI = 0x50

其效果是:

优先级 0~4   → 仍可抢占
优先级 5~15  → 暂时不能进入
NMI/HardFault → 不受 BASEPRI 控制

任务进入 FreeRTOS 临界区时,底层流程大致为:

flowchart TD
    A["任务调用 FreeRTOS API"] --> B["设置 BASEPRI 阈值"]
    B --> C["屏蔽优先级 5~15"]
    C --> D["安全修改队列、任务链表等内核数据"]
    D --> E["恢复 BASEPRI"]
    E --> F["受管理中断重新响应"]
    C -. "0~4 仍可抢占,但禁止访问内核" .-> G["高紧迫性 ISR"]

这套机制成立的前提是:优先级 0~4 的 ISR 不接触 FreeRTOS 内核对象。否则它可能在内核链表更新到一半时抢占并再次修改同一结构,最终造成链表损坏、随机断言或 HardFault。

三种保护机制不要混淆

操作 任务切换 优先级 5~15 优先级 0~4
taskENTER_CRITICAL() 暂时不能发生 被屏蔽 仍可响应
vTaskSuspendAll() 暂停调度 仍可响应 仍可响应
PRIMASK 全局关中断 不能发生 被屏蔽 也被屏蔽

vTaskSuspendAll() 不是中断临界区,不能用来保护任务与 ISR 共享的数据。

另外,taskENTER_CRITICAL() 也防不住优先级 0~4 的 ISR。高优先级 ISR 与任务共享数据时,需要使用原子操作、无锁结构、屏蔽指定 IRQ 或专门的分层通信方案。


从 ISR 到任务切换的完整流程

ISR 通过队列、信号量或任务通知唤醒任务时,切换过程如下:

sequenceDiagram
    participant HW as 外设
    participant ISR as 中断服务程序
    participant K as FreeRTOS 内核
    participant PSV as PendSV
    participant T as 高优先级任务

    HW->>ISR: 产生中断
    ISR->>ISR: 读取状态并清除中断标志
    ISR->>K: 调用 xQueueSendFromISR 等
    K->>K: 将等待任务移入就绪态
    K-->>ISR: xHigherPriorityTaskWoken = pdTRUE
    ISR->>PSV: portYIELD_FROM_ISR 设置 PendSV pending
    ISR-->>HW: 退出中断
    PSV->>K: 保存上下文并选择最高优先级任务
    K->>T: 恢复任务上下文

PendSV 通常是最低中断优先级,因此不会在普通外设 ISR 中间强行切换任务。它会在更高优先级中断退出后执行,并负责:

  1. 保存当前任务寄存器和栈顶;
  2. 调用调度器选择最高优先级就绪任务;
  3. 恢复新任务上下文;
  4. 通过异常返回直接进入新任务。

portYIELD_FROM_ISR() 只是请求一次调度。如果省略它,被唤醒的任务往往要等到下一次 SysTick 或其他调度点才运行。


FromISR API 选择

使用场景 推荐 API 说明
一个 ISR 唤醒一个任务 vTaskNotifyGiveFromISR() 轻量、高效
传递 32 位值或事件位 xTaskNotifyFromISR() 支持覆盖、递增、按位或
传递结构体或消息 xQueueSendFromISR() 将数据复制到队列
释放二值/计数信号量 xSemaphoreGiveFromISR() 不能用于 mutex
获取二值/计数信号量 xSemaphoreTakeFromISR() 不能用于 mutex
传输连续字节流 xStreamBufferSendFromISR() 常用于 UART/DMA
传输变长消息 xMessageBufferSendFromISR() 保留消息边界
设置事件组位 xEventGroupSetBitsFromISR() 操作可能延迟到 Timer Task
控制软件定时器 xTimerStartFromISR() 向定时器命令队列投递命令
获取系统 tick xTaskGetTickCountFromISR() ISR 专用版本

API 使用原则

  • 只能调用文档明确允许在 ISR 中使用的函数。
  • 普通 API 即使传入零阻塞时间,也不能代替 FromISR 版本。
  • ISR 不能阻塞,资源不足时 API 会立即失败。
  • 必须检查队列满、缓冲区满、定时器命令队列满等失败情况。
  • Mutex 具有任务所有权和优先级继承语义,不能在 ISR 中获取或释放。
  • 避免在 ISR 中调用 printf()malloc()free()HAL_Delay()vTaskDelay()
  • 大结构体通过队列传递会增加 ISR 延迟,必要时传递预分配缓冲区的指针或索引。

Event Group 的特殊性

xEventGroupSetBitsFromISR() 可能唤醒数量不确定的任务,因此 FreeRTOS 会把实际设置操作投递给 Timer/Daemon Task。使用时还要关注:

  • configUSE_TIMERS
  • INCLUDE_xTimerPendFunctionCall
  • configTIMER_QUEUE_LENGTH
  • configTIMER_TASK_PRIORITY
  • API 的 pdPASS / pdFAIL 返回值。

标准 ISR 示例

void DMA1_Stream0_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    Sample_t xSample;

    if (DMA_TransferComplete()) {
        /* 按外设手册要求及时清除中断标志。 */
        DMA_ClearTransferCompleteFlag();

        xSample.value = ReadDmaResult();

        if (xQueueSendFromISR(
                xSampleQueue,
                &xSample,
                &xHigherPriorityTaskWoken) != pdPASS) {
            /* ISR 中只做轻量错误记录。 */
            droppedSampleCount++;
        }
    }

    /* 整个 ISR 末尾统一请求调度。 */
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

使用时注意:

  1. xHigherPriorityTaskWoken 每次进入 ISR 都初始化为 pdFALSE
  2. 同一个 ISR 中的多个 FromISR 调用可以共用该变量;
  3. 通常在 ISR 末尾只调用一次 portYIELD_FROM_ISR()
  4. 在启用 IRQ 前创建好队列、任务和其他内核对象;
  5. 如果队列传递的是指针,必须保证目标缓冲区在任务处理完成前有效。

高优先级 ISR 如何与任务通信

优先级 0~4 的 ISR 不能直接调用 FreeRTOS。推荐使用两级处理:

flowchart LR
    A["优先级 0~4 的极速 ISR"] --> B["读取硬件并写入预分配缓冲区"]
    B --> C["更新原子索引或状态"]
    C --> D["触发优先级 ≥5 的软件/网关中断"]
    D --> E["调用 FromISR API"]
    E --> F["唤醒处理任务"]

可选方案包括:

  • 单生产者、单消费者无锁环形缓冲区;
  • 原子标志加任务轮询;
  • 高优先级 ISR 触发低优先级软件 IRQ;
  • 在极短时间内屏蔽指定外设 IRQ。

仅使用 volatile 不能保证复合操作的原子性,也不能自动解决缓存一致性和内存顺序问题。


常见错误与排查清单

高频错误

  1. 中断保留默认优先级 0,却调用了 FromISR API;
  2. 把逻辑优先级 5 和寄存器值 0x50 混用;
  3. configPRIO_BITS 与芯片实际实现不一致;
  4. 优先级分组中保留了子优先级;
  5. 在 ISR 中调用普通任务 API;
  6. 在优先级 0~4 的 ISR 中调用 FromISR API;
  7. 唤醒任务后遗漏 portYIELD_FROM_ISR()
  8. 队列或任务句柄创建前就启用了外设 IRQ;
  9. 忽略 FromISR API 的失败返回值;
  10. ISR 过长,包含打印、动态内存或大块数据复制。

建议启用断言

开发阶段应启用 configASSERT()。Cortex-M 端口可以借此检查:

  • 当前 ISR 优先级是否允许访问内核;
  • configMAX_SYSCALL_INTERRUPT_PRIORITY 是否有效;
  • NVIC 优先级分组是否合理;
  • 普通临界区 API 是否被错误地用于 ISR。

出现断言时可先检查:

uint32_t priority = NVIC_GetPriority(MyIRQn);
uint32_t grouping = NVIC_GetPriorityGrouping();

“零中断延迟”并不准确

优先级 0~4 只是不受 FreeRTOS BASEPRI 临界区影响,仍可能被以下因素延迟:

  • 更高或同级 ISR;
  • PRIMASK 全局关中断;
  • 异常进入和寄存器压栈;
  • Flash wait state、总线竞争;
  • FPU lazy stacking;
  • Cache、MPU、TrustZone 或芯片勘误。

更准确的说法是:不受 FreeRTOS 内核临界区影响的高紧迫性中断


平台差异与参考资料

上述 BASEPRI 分层主要适用于具有 BASEPRI 的 Cortex-M3/M4/M7/M33。

Cortex-M0/M0+ 没有 BASEPRI,其 FreeRTOS 端口通常需要更广泛地屏蔽中断,不能直接套用“高优先级中断始终不受内核影响”的结论。Cortex-A/R、RISC-V、Xtensa、SMP 和 TrustZone 端口也应以各自 portable 目录的实现为准。

官方参考资料


一页速记

1. NVIC 数值越小,中断优先级越高。
2. FreeRTOS 任务优先级数值越大,任务优先级越高。
3. 优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的 ISR 禁止调用内核。
4. 允许访问内核的 ISR 也只能调用 FromISR API。
5. xHigherPriorityTaskWoken 每次进入 ISR 初始化为 pdFALSE。
6. ISR 末尾通过 portYIELD_FROM_ISR 请求 PendSV 调度。
7. ISR 不阻塞、不使用 mutex、不打印、不动态分配大块内存。
8. 检查所有 FromISR API 的失败返回值。
9. taskENTER_CRITICAL 无法防住不受 BASEPRI 管理的高优先级 ISR。
10. 开发阶段启用 configASSERT,优先排查默认优先级 0 和移位错误。