FreeRTOS 中断配置和临界段 ,是整个 FreeRTOS 移植和使用中最关键、最容易出错的部分之一。我们从 Cortex-M 硬件机制讲起,逐层深入到 FreeRTOS 的配置宏、开关中断实现、临界段保护,最后通过实验验证。
一、Cortex-M 中断基础(硬件层面)
要理解 FreeRTOS 的中断配置,首先得掌握 Cortex-M 内核(如 STM32)的中断管理机制。
1. NVIC 与中断优先级
- NVIC(嵌套向量中断控制器) 管理所有外部中断和系统异常。
- 每个中断有一个 8 位优先级寄存器 ,但实际使用多少位由芯片厂商决定。STM32 只使用了高 4 位 ,因此有 16 个优先级(0~15),数值越小优先级越高。
- 优先级分组(PRIGROUP) 将优先级分为"抢占优先级"和"子优先级"两部分。FreeRTOS 强制要求使用分组 4,即所有 4 位全部作为抢占优先级,没有子优先级。这样做的目的是简化中断管理,避免子优先级带来的复杂性。
ST 的 HAL 库在
HAL_Init()中默认设置的就是NVIC_PRIORITYGROUP_4(见图 5.1.2),所以使用 HAL 库时通常不需要修改。但如果从其他非 FreeRTOS 例程移植代码,务必检查并统一为分组 4。
2. 三个关键的中断屏蔽寄存器
| 寄存器 | 作用 | FreeRTOS 使用情况 |
|---|---|---|
| PRIMASK | 屏蔽除 NMI 和 HardFault 外的所有中断 | μC/OS 常用,FreeRTOS 不用 |
| FAULTMASK | 比 PRIMASK 更强,连 HardFault 都能屏蔽 | FreeRTOS 不用 |
| BASEPRI | 屏蔽优先级低于某个阈值的中断(更精细的控制) | FreeRTOS 使用,这是核心 |
重点理解 BASEPRI :向它写入一个数值(比如 5),则所有优先级数值大于等于 5 的中断都会被屏蔽,而优先级为 0~4 的高优先级中断不受影响。这种"选择性屏蔽"非常适合 RTOS------既要保护临界区,又不能耽误高优先级实时中断。
二、FreeRTOS 中断配置宏(软件层面)
FreeRTOS 通过 FreeRTOSConfig.h 中的几个宏来配置中断行为。这些宏容易混淆,我们逐一理清:
| 宏名称 | 示例值(STM32) | 含义与用途 |
|---|---|---|
configPRIO_BITS |
4 | MCU 实际使用的优先级位数。STM32 用 4 位,所以为 4。 |
configLIBRARY_LOWEST_INTERRUPT_PRIORITY |
15 | 用户视角的最低优先级(即数值 15)。 |
configKERNEL_INTERRUPT_PRIORITY |
15 << (8-4) = 0xF0 |
写入寄存器时使用的值。因为优先级只存在高 4 位,需要左移 4 位。用于设置 PendSV 和 SysTick 的中断优先级(设为最低)。 |
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY |
5 | 用户视角的"系统调用最大优先级阈值"。优先级数值小于 5 的中断不受 FreeRTOS 屏蔽。 |
configMAX_SYSCALL_INTERRUPT_PRIORITY |
5 << 4 = 0x50 |
写入 BASEPRI 寄存器时使用的值。用于开关中断时屏蔽"低于此优先级"的中断。 |
关键结论(图 5.1.3 的核心):
- 优先级 0~4 :高于阈值,不会被 FreeRTOS 关中断屏蔽。这些中断绝对实时 ,但不能在 ISR 中调用任何 FreeRTOS API 函数(因为 API 可能被屏蔽,导致不可预知)。
- 优先级 5~15 :低于或等于阈值,会被 FreeRTOS 关中断屏蔽。这些中断可以安全调用
FromISR版本的 API(如xQueueSendFromISR)。- PendSV 和 SysTick:被设为最低优先级 15,确保所有外设中断都能优先于内核服务。
为什么优先级值要左移 4 位?
因为 STM32 只使用了优先级寄存器的高 4 位,低 4 位不存在(读回 0)。所以写入寄存器时,必须将优先级值左移 4 位,让它对齐到高 4 位。例如,优先级 5 在寄存器中实际写入的是 0x50,优先级 15 写入的是 0xF0。
PendSV 和 SysTick 优先级设置详解
从 xPortStartScheduler() 源码可以看到,FreeRTOS 通过直接操作地址 0xE000ED20(即 portNVIC_SYSPRI2_REG)一次性写入 32 位数据,其中:
- SysTick 优先级对应最高 8 位(左移 24 位)
- PendSV 优先级对应次高 8 位(左移 16 位)
两者都被设置为最低优先级(15),确保任务切换发生在所有外设中断之后。
三、FreeRTOS 开关中断的实现机制
有了上面的基础,开关中断的实现就一目了然了。
1. 关中断:portDISABLE_INTERRUPTS()
实际执行的是:
c
ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; // 0x50
msr basepri, ulNewBASEPRI
效果:所有优先级 ≥ 5 的中断被屏蔽,优先级 0~4 的中断继续响应。
2. 开中断:portENABLE_INTERRUPTS()
实际执行的是:
c
vPortSetBASEPRI(0); // 向 BASEPRI 写入 0
效果:取消所有屏蔽,所有中断恢复正常。
为什么不用 PRIMASK? 因为 BASEPRI 更灵活,允许高优先级中断(如电机控制、紧急停机的 IO 中断)在临界区中依然得到及时响应,这是 RTOS 实时性的重要保障。
四、临界段代码保护(应用层面)
临界段是"必须完整执行、不能被中断打断"的代码区域。FreeRTOS 提供了两种保护方式,区别在于调用上下文。
1. 任务级临界段:taskENTER_CRITICAL() / taskEXIT_CRITICAL()
- 在任务代码中使用(不能在中断中使用)。
- 支持嵌套 :通过全局变量
uxCriticalNesting记录嵌套深度。只有最外层的EXIT才会真正开中断,内层的EXIT只减少计数。这在复杂函数调用中非常有用,避免过早开中断破坏内层临界区的保护。 - 使用原则 :临界区代码要尽可能短,因为关中断会影响系统实时性(即使只屏蔽低优先级中断)。
典型用法:
c
taskENTER_CRITICAL();
// 操作共享资源(如全局变量、外设配置)
total_num += 0.01f;
printf("total_num = %.4f\n", total_num);
taskEXIT_CRITICAL();
2. 中断级临界段:taskENTER_CRITICAL_FROM_ISR() / taskEXIT_CRITICAL_FROM_ISR()
- 在中断服务函数中使用 (且该中断的优先级必须 低于
configMAX_SYSCALL_INTERRUPT_PRIORITY)。 - 用法有区别 :进入函数时会返回当前 BASEPRI 的值 ,退出时必须将返回值作为参数传入,以恢复之前的中断屏蔽状态。这保证了嵌套中断场景下,中断屏蔽状态能正确还原。
- 为什么返回值很重要:假如中断 A 被更高优先级的中断 B 抢占,B 中又调用了临界段,那么退出 B 时必须恢复到 A 的中断屏蔽状态,而不是简单打开中断。返回值机制确保了这个恢复过程的正确性。
正确用法:
c
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
uint32_t status_value;
status_value = taskENTER_CRITICAL_FROM_ISR(); // 保存当前 BASEPRI
// 操作共享资源
taskEXIT_CRITICAL_FROM_ISR(status_value); // 恢复保存的 BASEPRI
}
五、中断测试实验深度解析(验证理论)
实验设计
- TIM3:优先级设为 4(高于阈值),每秒打印 "TIM3 output..."。
- TIM5:优先级设为 5(等于阈值),每秒打印 "TIM5 output..."。
- 任务
interrupt_task:运行 5 次后,调用portDISABLE_INTERRUPTS()关闭中断,延时 5 秒,再调用portENABLE_INTERRUPTS()打开中断。
实验结果与结论
- 关中断前:两个定时器都正常输出。
- 关中断期间(5 秒) :
- TIM3(优先级 4)继续输出 ------ 因为它不受 BASEPRI 屏蔽。
- TIM5(优先级 5)停止输出 ------ 因为它被 BASEPRI 屏蔽了。
- 重新开中断后:TIM5 恢复输出。
这个实验完美验证了 :
configMAX_SYSCALL_INTERRUPT_PRIORITY这个阈值确实将中断分成了"不可屏蔽的高实时通道"和"可屏蔽的普通通道"。这为实际项目中选择中断优先级提供了清晰的依据。
六、核心要点总结与实践建议
| 要点 | 说明 |
|---|---|
| 优先级分组必须是 4 | 在 HAL_Init() 中已设置,不要改动。 |
阈值 configMAX_SYSCALL_INTERRUPT_PRIORITY 是分水岭 |
高于它的中断可以响应但不能调用 API ;低于或等于它的中断可以调用 API,但会被临界段屏蔽。 |
| PendSV 和 SysTick 优先级必须最低 | FreeRTOS 内核依赖它们进行任务切换和时间管理。 |
| 临界区代码要极短 | 进入临界区会关中断(即使只关一部分),过长会影响系统实时性。 |
| 任务级与中断级临界段函数不可混用 | 在任务中用 taskENTER_CRITICAL(),在 ISR 中用 taskENTER_CRITICAL_FROM_ISR(),并注意返回值传递。 |
| 中断优先级设置要避开临界区 | 如果你有一个极端重要的中断(比如故障保护),把它设为 0~4,并确保它的 ISR 不调用任何 FreeRTOS API。 |
七、与 SVC 和内核态/用户态的关联
- 在标准 FreeRTOS(无 MPU)中:所有中断和任务都运行在特权态,SVC 只用于启动第一个任务。开关中断(BASEPRI 操作)是全局的,影响所有低于阈值的任务和中断。
- 在启用 MPU 的 FreeRTOS 中 :用户态任务通过 SVC 陷入内核态调用 API。此时,中断配置逻辑不变,但
configMAX_SYSCALL_INTERRUPT_PRIORITY阈值同样决定了哪些中断可以"穿透"临界区,哪些会被屏蔽。高优先级中断(0~4)即使在用户态任务执行时也能响应,但它们的 ISR 仍然不能调用 FreeRTOS API(因为可能访问内核数据,触发 MPU 错误)。