在嵌入式实时系统中,中断是处理异步事件的核心机制。FreeRTOS 提供了完善的中断管理机制,帮助开发者编写安全、高效的中断服务程序(ISR),并与任务进行同步。本文将通俗易懂地讲解 FreeRTOS 中断管理的核心概念,并给出基于 STM32 标准外设库 的完整代码示例。
1. 中断优先级管理
1.1 数值优先级与逻辑优先级
在 Cortex-M 内核中,中断优先级的数值越小,逻辑优先级越高。FreeRTOS 通过配置项 configMAX_SYSCALL_INTERRUPT_PRIORITY 来划分"受 FreeRTOS 管理"的中断和"不受管理"的中断。
- 受管理的中断 :优先级数值 大于等于
configMAX_SYSCALL_INTERRUPT_PRIORITY的中断,可以在 ISR 中调用 FreeRTOS 的 API(带FromISR后缀)。 - 不受管理的中断 :优先级数值 小于
configMAX_SYSCALL_INTERRUPT_PRIORITY的中断,禁止调用任何 FreeRTOS API,以保证实时性。
在 FreeRTOSConfig.h 中通常这样配置:
c
#define configPRIO_BITS 4 // 使用 4 位优先级
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 0xf // 最低优先级
#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) )
通俗理解:优先级数值 ≥5 的中断(逻辑优先级较低)可以安全调用 FreeRTOS API,而优先级数值 <5 的中断(逻辑优先级很高,需要极快响应)则不能调用,以免破坏系统实时性。
1.2 设置中断优先级
使用标准库设置 NVIC 时,确保中断优先级满足上述要求。例如,配置 EXTI0 中断:
c
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 5; // 优先级数值 ≥5
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
2. 临界区与中断安全
2.1 任务级临界区
在任务代码中,使用 taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() 保护共享资源,防止被其他任务或中断打断。但在临界区内不能调用会导致阻塞的 API,且会关闭受管理的中断。
c
taskENTER_CRITICAL();
// 访问共享资源
taskEXIT_CRITICAL();
2.2 中断级临界区
在 ISR 中,应使用 taskENTER_CRITICAL_FROM_ISR() 和 taskEXIT_CRITICAL_FROM_ISR(),它们会保存并恢复中断状态,避免嵌套中断导致的问题。
c
UBaseType_t uxSavedInterruptStatus;
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
// 访问共享资源
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
注意:临界区应尽量短小,避免长时间关闭中断影响系统实时性。
3. 中断安全的 API
FreeRTOS 提供了一系列带 FromISR 后缀的 API,专门用于中断服务程序中。这些 API 不会阻塞,且需要传递一个 pxHigherPriorityTaskWoken 指针,用于指示是否有更高优先级任务被唤醒。
常用 API 示例:
xSemaphoreGiveFromISR( semaphore, &xHigherPriorityTaskWoken )xQueueSendFromISR( queue, &data, &xHigherPriorityTaskWoken )vTaskNotifyGiveFromISR( taskHandle, &xHigherPriorityTaskWoken )
在 ISR 结束时,如果 xHigherPriorityTaskWoken 为 pdTRUE,则需要进行上下文切换(通常调用 portYIELD_FROM_ISR())。
c
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
4. 延迟中断处理(Deferred Interrupt Processing)
中断服务程序应尽可能短小,将耗时操作交给任务处理。常用的方法是通过 二值信号量 或 任务通知 唤醒一个高优先级任务,由该任务完成实际工作。
4.1 二值信号量同步
创建二值信号量:
c
SemaphoreHandle_t xBinarySemaphore;
xBinarySemaphore = xSemaphoreCreateBinary();
中断服务程序:
c
void EXTI0_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 给出信号量,唤醒等待的任务
xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
EXTI_ClearITPendingBit(EXTI_Line0);
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
任务代码:
c
void vProcessingTask(void *pvParameters)
{
for (;;)
{
// 等待信号量,阻塞直到中断发生
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE)
{
// 处理中断事件(例如读取传感器数据、发送消息等)
ProcessInterruptEvent();
}
}
}
4.2 计数信号量
当需要记录中断发生的次数时,使用计数信号量。创建时指定最大计数值和初始值:
c
SemaphoreHandle_t xCountingSemaphore;
xCountingSemaphore = xSemaphoreCreateCounting(10, 0); // 最大计数10,初始0
ISR 中每次 xSemaphoreGiveFromISR() 会使计数加 1,任务中 xSemaphoreTake() 会使计数减 1,从而处理所有累积的事件。
4.3 任务通知(轻量级替代)
任务通知比信号量更高效,占用更少 RAM。每个任务都有一个通知值,可以当作二值或计数信号量使用。
ISR 中:
c
void EXTI0_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 给指定任务发送通知
vTaskNotifyGiveFromISR(xTaskHandle, &xHigherPriorityTaskWoken);
EXTI_ClearITPendingBit(EXTI_Line0);
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
任务中:
c
void vProcessingTask(void *pvParameters)
{
for (;;)
{
// 等待通知,进入阻塞
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 处理中断事件
ProcessInterruptEvent();
}
}
5. 完整代码示例(STM32 标准库 + FreeRTOS)
以下示例演示:使用外部中断 EXTI0(按键触发)唤醒一个任务,任务中打印消息或执行其他操作。
5.1 全局变量与任务句柄
c
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
SemaphoreHandle_t xBinarySemaphore;
// 任务函数声明
void vProcessingTask(void *pvParameters);
5.2 硬件初始化(GPIO + EXTI + NVIC)
c
void EXTI0_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 使能 GPIOA 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 使能 AFIO 时钟(用于外部中断)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// 配置 PA0 为浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 连接 EXTI0 到 PA0
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
// 配置 EXTI0
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; // 上升沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
// 配置 NVIC,优先级必须 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY 对应的数值
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 5;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
5.3 中断服务程序
c
void EXTI0_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 给出二值信号量
xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
EXTI_ClearITPendingBit(EXTI_Line0);
}
// 如果唤醒了更高优先级任务,则请求上下文切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
5.4 任务函数
c
void vProcessingTask(void *pvParameters)
{
for (;;)
{
// 等待信号量
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE)
{
// 处理中断事件
// 例如:切换 LED 状态、发送串口数据等
GPIO_WriteBit(GPIOB, GPIO_Pin_5, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_5)));
}
}
}
5.5 主函数初始化
c
int main(void)
{
// 初始化硬件
EXTI0_Init();
// 创建二值信号量
xBinarySemaphore = xSemaphoreCreateBinary();
if (xBinarySemaphore != NULL)
{
// 创建处理任务
xTaskCreate(vProcessingTask, "Processing", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
}
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会执行到这里
for (;;);
}
6. 总结
- FreeRTOS 中断管理的关键是 优先级划分 和 中断安全 API 的使用。
- ISR 应尽量简短,通过 二值信号量 、计数信号量 或 任务通知 将处理延迟到任务中。
- 在 ISR 中使用
FromISRAPI 时,务必传递pxHigherPriorityTaskWoken并在结束后调用portYIELD_FROM_ISR()。 - 临界区分为任务级和中断级,需根据上下文选择合适版本。
掌握这些技巧,可以构建稳定、高效的 FreeRTOS 中断处理系统。希望本文能帮助你理解并应用 FreeRTOS 的中断管理。
参考资料
- FreeRTOS 官方文档:https://www.freertos.org/
- STM32 标准外设库用户手册