FreeRTOS 中断管理
1. 先理解什么是中断
中断是外设向 CPU 发出的"有事情需要处理"的请求。例如定时器到时、GPIO 检测到边沿时,CPU 会暂时停下正在执行的任务,转去运行对应的中断服务函数(ISR)。处理完后,CPU 再返回被打断的位置继续执行。
任务正在运行
↓ 外设发出中断请求
CPU 暂停任务,保存必要现场
↓
运行 ISR,尽快处理紧急工作
↓
退出 ISR,恢复原现场或切换到更合适的任务
ISR 不是一个 FreeRTOS 任务。它运行在中断上下文中,因此不能随意调用任务上下文 API,也不应做耗时工作。
2. 中断优先级:数值方向和任务相反
在 Cortex-M / STM32 中,中断优先级数值通常是越小越紧急 :优先级 0 比优先级 6 更紧急。FreeRTOS 任务优先级则通常是数值越大越高。
| 对象 | 数值更大意味着 |
|---|---|
| Cortex-M 中断优先级 | 紧急程度更低 |
| FreeRTOS 任务优先级 | 运行优先级更高 |
Cortex-M 的优先级寄存器宽度为 8 位,但芯片实际实现的位数可能少于 8 位。课件中的 STM32 实现了高 4 位,因此可配置 16 个有效优先级等级(0--15)。不能把 STM32 的 4 位实现直接套用到所有 Cortex-M 芯片。
2.1 抢占优先级和子优先级
部分 STM32 HAL/CMSIS 配置把中断优先级位分成:
-
抢占优先级:数值更小的中断可以打断抢占优先级数值更大的中断。
-
子优先级:抢占优先级相同时,多个中断同时等待时用于决定先处理谁;子优先级本身不能让一个中断抢占另一个。
3. 哪些中断可以调用 FreeRTOS API?
这是本章最重要的规则:
只有优先级处于 FreeRTOS 允许范围内的中断,才能调用 FreeRTOS 的 ISR 专用 API。
在 Cortex-M 上,中断数值越小越紧急。FreeRTOS 使用 configMAX_SYSCALL_INTERRUPT_PRIORITY(或相关配置)设定一条边界:比边界更紧急的中断不会被 FreeRTOS 临界区屏蔽,因此不能调用 FreeRTOS API;边界及更低紧急度的中断,才可在遵循移植层要求的前提下调用 ...FromISR() API。
以 STM32 只实现 4 个优先级位、使用逻辑优先级 5 作为边界为例:
| 中断逻辑优先级 | 紧急程度 | 能否调用 FreeRTOS API |
|---|---|---|
| 0--4 | 更紧急 | 不可以 |
| 5--15 | 较低或等于边界 | 可以调用允许的 ISR 专用 API |
这里的 5 是 CMSIS/HAL 常用的未移位逻辑优先级。FreeRTOS 底层配置 configMAX_SYSCALL_INTERRUPT_PRIORITY 时,通常需要填写按芯片优先级位数编码后的寄存器值,例如 4 个有效位时逻辑优先级 5 对应 0x50。许多工程也会定义便于阅读的 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY。务必按项目 FreeRTOSConfig.h 和官方移植层的要求填写,不要把逻辑值 5 与编码值 0x50 混用。
不同厂商 HAL、芯片实现位数和移植版本的宏定义可能不同;以工程配置及对应端口文档为准。优先级设错可能导致断言失败、临界区行为异常或难以复现的系统故障。
4. 为什么高紧急度中断不能调用内核 API?
FreeRTOS 在 Cortex-M 上利用 BASEPRI 实现可管理中断的屏蔽。进入临界区时,内核提高 BASEPRI 阈值,屏蔽一部分较低紧急度的中断;优先级更高的紧急中断仍可立即响应。
因此,高紧急度中断可以用于时限严格的硬件响应,但它可能在内核修改列表、队列或其他内部状态时插入执行。内核无法保证这些操作对它安全,所以这类中断不能调用 FreeRTOS API。
可把中断分成两类理解:
-
高紧急度、内核不可管理:响应快,但 ISR 中不能调用 FreeRTOS API。若要把数据交给任务,可先写入硬件缓冲区或受控的无锁缓冲,再由可调用内核 API 的中断或任务转交。
-
可由内核管理的中断 :优先级数值处于允许范围,可以调用匹配的
...FromISR()API。
5. 中断中调用任务 API 的规则
在 ISR 中需要使用 FreeRTOS 功能时,调用对应的中断版本 API,通常函数名带 FromISR 后缀。例如:
| 任务上下文 API | ISR 中对应 API |
|---|---|
xQueueSend() |
xQueueSendFromISR() |
xSemaphoreGive() |
xSemaphoreGiveFromISR() |
vTaskNotifyGive() |
vTaskNotifyGiveFromISR() |
vTaskResume() |
xTaskResumeFromISR() |
实际可用函数取决于 FreeRTOS 版本和组件配置。不能只因为名称相似就从 ISR 调用普通任务版本。
5.1 请求在中断退出时切换任务
ISR API 通常通过输出参数告诉程序,操作是否唤醒了一个比当前任务更高优先级的任务。ISR 退出前再调用端口提供的切换宏,让调度器决定是否立即切到新任务:
void UART_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 读取并清除 UART 中断状态 */
xQueueSendFromISR(xRxQueue, &receivedByte,
&xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
如果队列操作唤醒了更高优先级任务,通常在 ISR 退出时立即切换;否则先返回原任务。切换发生在中断退出路径中,而不是让 ISR 直接"运行一个任务"。
不同端口可能使用 portEND_SWITCHING_ISR() 等其他宏名,参数类型也可能不同,应以当前移植层提供的写法为准。
6. 临界区和中断屏蔽
临界区用于保护一小段不能被相关中断打断的代码。Cortex-M FreeRTOS 端口通常通过 BASEPRI 屏蔽可管理的中断,而不是无条件屏蔽所有中断。因此更高紧急度的中断仍可能执行。
任务上下文中的典型形式:
taskENTER_CRITICAL();
/* 受保护的短小操作 */
taskEXIT_CRITICAL();
规则:
-
进入和退出应成对,临界区必须尽可能短。
-
不要在临界区中调用会阻塞的 API、延时、等待队列或执行耗时操作。
-
临界区不等同于"所有中断都停了";具体屏蔽范围由端口和配置决定。
-
需要在 ISR 中保护代码时,应使用当前端口明确提供的 ISR 临界区接口;不要在 ISR 中直接照搬任务上下文的进入/退出宏。
-
PRIMASK、FAULTMASK与BASEPRI的作用范围不同。应用层通常优先使用 FreeRTOS 临界区 API,不要随意直接改内核依赖的屏蔽寄存器。
7. Cortex-M 系统异常优先级
课件提到 SHPR1、SHPR2、SHPR3 是系统处理程序优先级寄存器。FreeRTOS Cortex-M 端口通常把 SysTick 和 PendSV 配为最低优先级:
-
SysTick 产生系统节拍,推进延时和时间管理。
-
PendSV 执行任务上下文切换。
把它们放在较低优先级,可让更紧急的外设中断先处理,减少任务切换对高紧急度中断的影响。普通应用一般由 FreeRTOS 端口配置这些异常,不应随意覆盖其优先级。
8. 本章使用要点
-
中断优先级和任务优先级方向相反:前者数值越小越紧急,后者通常数值越大优先级越高。
-
只有处于
configMAX_SYSCALL_INTERRUPT_PRIORITY允许范围内的中断才能调用 FreeRTOS ISR API;更紧急的中断不能调用任何 FreeRTOS API。 -
ISR 要短小。耗时处理放到任务中,ISR 负责确认硬件事件并通知任务。
-
ISR 中只调用明确标记为
FromISR的接口,并按返回值或唤醒标志使用端口切换宏。 -
中断优先级寄存器的有效位数、逻辑优先级和编码值要区分清楚;按芯片和移植层配置,不照抄其他工程的常数。
-
临界区只保护短小的共享数据操作,不能在其中阻塞,也不能假设所有中断都被屏蔽。
9. 本章函数与宏汇总
| 函数或宏 | 中文含义 | 主要用法 |
|---|---|---|
taskENTER_CRITICAL() |
进入任务临界区 | 任务上下文中保护短小的共享数据操作;必须与退出宏配对 |
taskEXIT_CRITICAL() |
退出任务临界区 | 恢复进入临界区前的调度/中断屏蔽状态 |
taskENTER_CRITICAL_FROM_ISR() |
从 ISR 进入临界区 | 仅在移植层支持时用于 ISR;保存宏返回的状态,供退出时恢复 |
taskEXIT_CRITICAL_FROM_ISR() |
从 ISR 退出临界区 | 使用 taskENTER_CRITICAL_FROM_ISR() 返回的状态恢复;具体签名查当前版本 |
xQueueSendFromISR() |
从中断向队列发送数据 | 传入队列、数据地址和唤醒标志;必要时在 ISR 退出时请求切换 |
xSemaphoreGiveFromISR() |
从中断释放信号量 | 常用于 ISR 通知等待任务;检查唤醒标志并在退出时按端口要求切换 |
vTaskNotifyGiveFromISR() |
从中断向任务发送通知 | 传入目标任务句柄和唤醒标志;适合轻量级事件通知 |
xTaskResumeFromISR() |
从中断恢复挂起任务 | 仅在任务确实使用挂起/恢复语义时使用;它不是可靠的事件缓存机制 |
portYIELD_FROM_ISR() |
根据条件请求 ISR 退出切换 | 通常传入 ISR API 设置的高优先级任务唤醒标志;宏名依端口而异 |
portEND_SWITCHING_ISR() |
部分端口的 ISR 切换宏 | 某些移植层使用此名称;按当前端口示例调用 |
HAL_NVIC_SetPriorityGrouping() |
设置 NVIC 优先级分组 | STM32 HAL 初始化时设置抢占/子优先级位分配;FreeRTOS 项目按移植层建议配置 |
HAL_NVIC_SetPriority() |
设置外设中断优先级 | STM32 HAL 中为指定 IRQ 配置逻辑抢占优先级和子优先级;确保其符合 FreeRTOS API 调用范围 |
portDISABLE_INTERRUPTS() / portENABLE_INTERRUPTS() |
移植层屏蔽/允许中断 | 端口相关的底层宏;应用代码通常使用临界区 API,避免不成对调用 |
configMAX_SYSCALL_INTERRUPT_PRIORITY(配置宏) |
FreeRTOS API 可调用中断的优先级边界 | 在 FreeRTOSConfig.h 中按处理器实现位数填写移位后的编码值;更紧急的中断不得调用内核 API |
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY(部分工程配置宏) |
以逻辑优先级表示的 API 边界 | 有些工程用它生成上面的编码值;遵循项目配置,避免混淆逻辑值和寄存器值 |
configPRIO_BITS(配置宏) |
芯片实现的中断优先级位数 | 移植配置使用,必须匹配 MCU 实际实现的优先级位数 |