从裸机到 RTOS 的临界区迁移:Cortex-M 的 BASEPRI 与 FreeRTOS 中断优先级陷阱

1. 裸机时代的临界区:简单粗暴的关中断

在裸机开发中,保护共享资源(如全局变量、环形缓冲区)最直接的方法就是关中断:

__disable_irq();
// 访问共享资源
__enable_irq();

这种方式简单有效,但代价是屏蔽所有中断,包括那些对实时性要求极高的中断(如电机控制、通信接收)。在裸机系统中,由于任务逻辑简单,通常可以接受。

然而,当系统迁移到 FreeRTOS 后,如果继续使用 __disable_irq() 保护临界区,会带来两个严重问题:

  • 破坏 RTOS 的调度实时性:FreeRTOS 依赖 SysTick 和 PendSV 进行任务切换,关中断会延迟任务切换,导致系统响应变慢。
  • 与 RTOS 的中断优先级管理冲突:FreeRTOS 要求中断优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断不能调用任何 FreeRTOS API,而 __disable_irq() 会无差别屏蔽所有中断,可能让本应快速响应的中断被阻塞。

2. Cortex-M 的 BASEPRI:更精细的中断屏蔽

Cortex-M 内核提供了一个专门用于中断优先级屏蔽的寄存器:BASEPRI。它允许你屏蔽优先级低于(数值大于)某个阈值的中断,而高于该阈值的中断不受影响。

  • BASEPRI = 0:不屏蔽任何中断(默认)。
  • BASEPRI = N:屏蔽所有优先级数值 ≥ N 的中断(即优先级低于 N 的中断)。

例如,设置 BASEPRI = 0x50,则优先级为 0x60、0x70 的中断被屏蔽,而优先级 0x40、0x30 的中断仍可响应。

FreeRTOS 正是利用 BASEPRI 实现临界区:在临界区内,将 BASEPRI 设置为 configMAX_SYSCALL_INTERRUPT_PRIORITY,从而屏蔽所有可能调用 FreeRTOS API 的中断,但保留更高优先级(数值更小)的中断用于紧急事件。

3. FreeRTOS 的临界区实现与中断优先级陷阱

3.1 关键宏定义

FreeRTOSConfig.h 中,必须正确配置:

#define configPRIO_BITS                  4  // STM32 通常为 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))
  • configMAX_SYSCALL_INTERRUPT_PRIORITY:允许调用 FreeRTOS API 的最高中断优先级(数值最小)。
  • 任何优先级高于此值的中断(数值更小),绝不能调用 FreeRTOS API,否则会导致断言失败或系统崩溃。

3.2 陷阱:裸机中断优先级未适配

裸机代码中,中断优先级可能随意设置,例如将串口中断设为最高优先级 0。迁移到 FreeRTOS 后,如果该中断服务程序(ISR)中调用了 xQueueSendFromISR(),就会触发 configASSERT() 失败,因为优先级 0 高于 configMAX_SYSCALL_INTERRUPT_PRIORITY

正确做法:所有需要调用 FreeRTOS API 的中断,其优先级数值必须 ≥ configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY(即优先级低于该阈值)。

3.3 陷阱:临界区嵌套与 BASEPRI 恢复

FreeRTOS 的 taskENTER_CRITICAL()taskEXIT_CRITICAL() 支持嵌套,内部通过保存和恢复 BASEPRI 实现。但如果在临界区内直接修改 BASEPRI 或调用 __disable_irq(),会破坏嵌套逻辑,导致退出临界区后中断屏蔽状态错误。

4. 迁移实战:从裸机临界区到 FreeRTOS 临界区

4.1 裸机代码示例

// 裸机:保护共享缓冲区
void UART_SendData(uint8_t *data, uint32_t len) {
    __disable_irq();
    for (uint32_t i = 0; i < len; i++) {
        while (!(USART1->SR & USART_SR_TXE));
        USART1->DR = data[i];
    }
    __enable_irq();
}

4.2 迁移到 FreeRTOS

// FreeRTOS:使用临界区宏
void UART_SendData(uint8_t *data, uint32_t len) {
    taskENTER_CRITICAL();
    for (uint32_t i = 0; i < len; i++) {
        while (!(USART1->SR & USART_SR_TXE));
        USART1->DR = data[i];
    }
    taskEXIT_CRITICAL();
}

如果共享资源可能被中断访问,应使用 taskENTER_CRITICAL_FROM_ISR()taskEXIT_CRITICAL_FROM_ISR()

4.3 中断优先级配置示例

// 配置 USART1 中断优先级为 6(低于 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY=5)
HAL_NVIC_SetPriority(USART1_IRQn, 6, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);

// 在 ISR 中安全调用 FreeRTOS API
void USART1_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // 处理中断...
    xQueueSendFromISR(xQueue, &data, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

5. 注意事项与避坑指南

  • 优先级分组:Cortex-M 的优先级分组必须设置为所有位都用于抢占优先级(NVIC_PRIORITYGROUP_4),否则 FreeRTOS 的优先级判断会出错。
  • 不要混用关中断:在 FreeRTOS 中,禁止使用 __disable_irq() 保护临界区,应统一使用 taskENTER_CRITICAL()
  • ISR 中 API 选择:中断服务程序中只能使用带 FromISR 后缀的 API,且必须检查返回值。
  • 临界区尽量短:临界区内不要调用阻塞函数或耗时操作,否则影响系统实时性。
  • 调试技巧:若系统进入 configASSERT(),优先检查中断优先级是否高于 configMAX_SYSCALL_INTERRUPT_PRIORITY

6. 总结

从裸机迁移到 FreeRTOS,临界区的保护方式必须从“关总中断”转变为“基于 BASEPRI 的优先级屏蔽”。正确配置中断优先级,使用 RTOS 提供的临界区宏,才能兼顾系统实时性与安全性。理解 BASEPRI 的工作原理,是避开 FreeRTOS 中断优先级陷阱的关键。