FreeRTOS 手动移植教程(八):中断管理 —— 优先级、临界区与任务通知
前面几篇文章中,我们已经多次在中断里使用了
FromISR函数,但并未系统梳理中断优先级与 FreeRTOS 的配合规则。本篇将深入讨论这些规则,并介绍临界区的正确使用方法。同时,我们还会引入一种更轻量级的任务通信机制——任务通知,它可以在某些场景下替代信号量或队列,进一步提升效率。最后通过实验,在按键中断中用任务通知直接唤醒任务。
一、为什么中断管理如此重要?
在 FreeRTOS 中,中断是系统实时性的关键。一方面,中断需要快速响应硬件事件;另一方面,中断可能唤醒高优先级任务,这些任务需要在中断退出后立即执行。如果中断优先级配置不当,轻则导致 API 调用失败(进入断言死循环),重则破坏内核数据结构,造成系统崩溃。
1.1 回顾FreeRTOSConfig.h中的关键宏
#defineconfigPRIO_BITS4#defineconfigLIBRARY_LOWEST_INTERRUPT_PRIORITY0xf#defineconfigLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY5#defineconfigKERNEL_INTERRUPT_PRIORITY(0xf<<(8-4))#defineconfigMAX_SYSCALL_INTERRUPT_PRIORITY(5<<(8-4))这些宏定义了中断优先级与 FreeRTOS 的协作边界:
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY= 5:
优先级数值在 5 ~ 15 之间的中断,可以安全调用 FreeRTOS 的FromISR系列 API。- 优先级 0 ~ 4 的中断:
完全不被打扰,但绝不能调用任何 RTOS 函数。它们通常留给极度紧急的硬件事件(如掉电检测)。
1.2 NVIC 优先级分组必须匹配
我们已在每个工程的main()开头放置了:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);// 4 位抢占优先级这让所有中断优先级都只用抢占优先级(0~15),不再分子优先级。只有这样,上述宏对应的数值才能正确生效。如果分组不对,整个中断管理策略将完全失效,可能导致难以调试的死机。
1.3 中断如何引发任务切换
中断服务函数中调用xSemaphoreGiveFromISR、xQueueSendFromISR等函数时,可能会使更高优先级任务就绪。此时这些函数会返回一个xHigherPriorityTaskWoken标志。中断退出前,必须用portYIELD_FROM_ISR触发 PendSV,让内核切换到高优先级任务。忽略这个标志会导致任务延迟到下一次 SysTick 才运行,破坏实时性。
二、临界区——短暂的“关门”操作
2.1 什么是临界区
当一段代码操作了多个任务或中断共享的变量时,如果不加以保护,可能会在执行到一半时被中断打乱,造成数据损坏。FreeRTOS 提供了临界区宏,用于短暂地关闭和恢复中断:
taskENTER_CRITICAL();// ... 受保护的代码,此时内核不会切换任务,且可屏蔽的中断被禁用 ...taskEXIT_CRITICAL();在 Cortex-M3 中,这两个宏通过操作BASEPRI寄存器实现,关闭优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断,从而达到保护目的。
2.2 使用注意事项
- 临界区应尽可能短:长时间关中断会破坏系统的实时性,甚至导致中断丢失。只保护必不可少的几条指令。
- 临界区不能嵌套调用
FromISRAPI:因为临界区内部中断已被屏蔽,若强行调用会导致断言失败。 - 不影响高优先级中断:优先级高于
configMAX_SYSCALL_INTERRUPT_PRIORITY的中断仍然可以响应,这是 Cortex-M3 的特点,允许紧急事件穿透临界区。
典型场景:多任务同时向一个链表添加节点,或修改一个全局变量。例如:
taskENTER_CRITICAL();global_flags|=0x01;// 原子性修改taskEXIT_CRITICAL();三、任务通知——轻量级任务间通信
3.1 为什么需要任务通知?
前面我们使用的二值信号量、计数信号量、队列都需要提前创建内核对象,并占用一定 RAM。FreeRTOS 从 V8.2.0 开始为每个任务内置了一个通知状态,它可以用作轻量级的二值/计数信号量或简单事件标志,完全无需创建任何对象,速度更快,内存开销更小。
3.2 关键 API
| 功能 | API 名称 | 说明 |
|---|---|---|
| 发送通知(任务) | xTaskNotifyGive | 目标任务的通知值加 1 |
| 发送通知(中断) | vTaskNotifyGiveFromISR | 中断中给目标任务通知值加 1 |
| 获取通知(任务) | ulTaskNotifyTake | 清零通知值并返回原值,可阻塞等待 |
| 发送带数据的通知 | xTaskNotify/xTaskNotifyFromISR | 可发送指定值、设置位、覆盖等 |
| 等待通知(带数据) | xTaskNotifyWait | 可接收完整 32 位数据,并清零通知状态 |
我们本章主要使用最简单的**“Give / Take”**模式,它类似二值信号量的行为。
3.3 使用限制
- 只能由一个任务接收:任务通知的目标是特定的任务,不能像队列那样被多个任务阻塞接收。如果多个任务需要等待同一事件,任务通知不适用,此时仍需信号量或队列。
- 通知值可累加:多次 Give 会累积,Take 时一次性清零并返回累加值,类似计数信号量。
四、实验:按键中断使用任务通知唤醒任务
4.1 设计思路
将之前“二值信号量”章节的实验改用任务通知实现:PA0 按键触发中断,在中断中调用vTaskNotifyGiveFromISR直接给 LED 任务发送通知;LED 任务使用ulTaskNotifyTake阻塞等待通知,获取后翻转 LED。
4.2 硬件与配置
沿用 PA0 按键、PC13 LED。BSP 文件bsp_led.c、bsp_exti.c保持不变。中断服务函数在stm32f10x_it.c中实现。
4.3 中断服务函数
// stm32f10x_it.c 中的 EXTI0_IRQHandler#include"stm32f10x_it.h"#include"FreeRTOS.h"#include"task.h"externTaskHandle_t xLedTaskHandle;// 在 main.c 中定义voidEXTI0_IRQHandler(void){BaseType_t xHigherPriorityTaskWoken=pdFALSE;if(EXTI_GetITStatus(EXTI_Line0)!=RESET){/* 直接给 LED 任务发送通知,类似二值信号量的 Give */vTaskNotifyGiveFromISR(xLedTaskHandle,&xHigherPriorityTaskWoken);EXTI_ClearITPendingBit(EXTI_Line0);}portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}4.4 main.c 任务实现
#include"stm32f10x.h"#include"FreeRTOS.h"#include"task.h"#include"bsp_led.h"#include"bsp_exti.h"TaskHandle_t xLedTaskHandle=NULL;/* LED 任务:等待任务通知,收到后翻转 LED */voidvLedTask(void*pvParameters){while(1){/* ulTaskNotifyTake(pdTRUE, portMAX_DELAY): - pdTRUE:获取后将通知值清零 - portMAX_DELAY:无限等待,直到通知值 > 0 */ulTaskNotifyTake(pdTRUE,portMAX_DELAY);LED3_Toggle();}}intmain(void){NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);LED_InitAll();EXTI0_Init();// PA0 按键中断/* 创建 LED 任务,保存其句柄,以便中断中发送通知 */xTaskCreate(vLedTask,"Led",128,NULL,1,&xLedTaskHandle);vTaskStartScheduler();while(1);}4.5 实验现象
- 上电后 LED 保持熄灭;
- 每按一次 PA0 按键,LED 翻转一次。
- 整个流程不依赖任何信号量或队列,代码更简洁,RAM 占用更低。
4.6 与二值信号量的对比
| 特性 | 二值信号量 | 任务通知 |
|---|---|---|
| 创建对象 | 需要 | 不需要 |
| 发送方 | 任意任务或中断 | 知道目标任务的句柄 |
| 接收方 | 任意任务可同时等待同一信号量 | 仅目标任务可接收 |
| 内存开销 | 需分配信号量控制块 | 无额外开销 |
| 适用场景 | 多对一、多对多同步 | 单对单同步,轻量级事件通知 |
在“一个中断唤醒一个特定任务”的简单场景中,任务通知是最优选择。
五、临界区与任务通知的配合
有时我们需要在任务中访问共享变量,同时又要保证不被中断破坏。例如,记录按键次数并显示。我们可以用临界区保护计数器:
volatileuint32_tkey_count=0;voidvLedTask(void*pvParameters){while(1){ulTaskNotifyTake(pdTRUE,portMAX_DELAY);/* 临界区保护对 key_count 的修改 */taskENTER_CRITICAL();key_count++;taskEXIT_CRITICAL();LED3_Toggle();}}而在中断中我们只做最简单的通知,避免在中断中执行耗时操作。
六、常见错误与调试
- 忘记
portYIELD_FROM_ISR:如果中断唤醒了更高优先级任务却没有调用该宏,任务会被延迟。 - 在临界区内调用阻塞 API:会导致任务永远挂起(因为调度器被锁定),典型症状是系统卡死。
- 中断优先级超出
configMAX_SYSCALL_INTERRUPT_PRIORITY:在 0~4 优先级中断中调用 RTOS API 将进入configASSERT死循环。 - 任务通知的接收者没有清空计数器:如果使用
xTaskNotifyWait而不清空,可能反复接收到旧数据。使用ulTaskNotifyTake(pdTRUE, ...)可安全清零。
七、总结
本篇系统地梳理了 FreeRTOS 的中断管理规则,并引入了两个重要技术:
- 临界区:通过短暂屏蔽部分中断,保护共享数据;
- 任务通知:零内存开销的任务间通信方式,尤其适用于中断到任务的单对单唤醒。
通过按键中断的实验,我们体会到了任务通知的简洁高效。在实际项目中,应根据同步场景合理选择信号量、队列或任务通知,以达到资源与性能的最佳平衡。
下一篇文章,我们将进入实时性与调试技巧篇,讨论如何检测任务栈溢出、分析 CPU 利用率,以及使用configASSERT定位早期错误。
下一篇:FreeRTOS 调试与优化 —— 栈溢出检测、CPU 利用率与断言。
