前面几篇文章中,我们已经多次在中断里使用了
FromISR函数,但并未系统梳理中断优先级与 FreeRTOS 的配合规则。本篇将深入讨论这些规则,并介绍临界区 的正确使用方法。同时,我们还会引入一种更轻量级的任务通信机制------任务通知,它可以在某些场景下替代信号量或队列,进一步提升效率。最后通过实验,在按键中断中用任务通知直接唤醒任务。

一、为什么中断管理如此重要?
在 FreeRTOS 中,中断是系统实时性的关键。一方面,中断需要快速响应硬件事件;另一方面,中断可能唤醒高优先级任务,这些任务需要在中断退出后立即执行。如果中断优先级配置不当,轻则导致 API 调用失败(进入断言死循环),重则破坏内核数据结构,造成系统崩溃。
1.1 回顾 FreeRTOSConfig.h 中的关键宏
c
#define configPRIO_BITS 4
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 0xf
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
#define configKERNEL_INTERRUPT_PRIORITY ( 0xf << (8 - 4) )
#define configMAX_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() 开头放置了:
c
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); // 4 位抢占优先级
这让所有中断优先级都只用抢占优先级(0~15),不再分子优先级。只有这样,上述宏对应的数值才能正确生效。如果分组不对,整个中断管理策略将完全失效,可能导致难以调试的死机。
1.3 中断如何引发任务切换
中断服务函数中调用 xSemaphoreGiveFromISR、xQueueSendFromISR 等函数时,可能会使更高优先级任务就绪。此时这些函数会返回一个 xHigherPriorityTaskWoken 标志。中断退出前,必须用 portYIELD_FROM_ISR 触发 PendSV,让内核切换到高优先级任务。忽略这个标志会导致任务延迟到下一次 SysTick 才运行,破坏实时性。
二、临界区------短暂的"关门"操作
2.1 什么是临界区
当一段代码操作了多个任务或中断共享的变量时,如果不加以保护,可能会在执行到一半时被中断打乱,造成数据损坏。FreeRTOS 提供了临界区宏,用于短暂地关闭和恢复中断:
c
taskENTER_CRITICAL();
// ... 受保护的代码,此时内核不会切换任务,且可屏蔽的中断被禁用 ...
taskEXIT_CRITICAL();
在 Cortex-M3 中,这两个宏通过操作 BASEPRI 寄存器实现,关闭优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断,从而达到保护目的。
2.2 使用注意事项
- 临界区应尽可能短:长时间关中断会破坏系统的实时性,甚至导致中断丢失。只保护必不可少的几条指令。
- 临界区不能嵌套调用
FromISRAPI:因为临界区内部中断已被屏蔽,若强行调用会导致断言失败。 - 不影响高优先级中断 :优先级高于
configMAX_SYSCALL_INTERRUPT_PRIORITY的中断仍然可以响应,这是 Cortex-M3 的特点,允许紧急事件穿透临界区。
典型场景:多任务同时向一个链表添加节点,或修改一个全局变量。例如:
c
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 中断服务函数
c
// stm32f10x_it.c 中的 EXTI0_IRQHandler
#include "stm32f10x_it.h"
#include "FreeRTOS.h"
#include "task.h"
extern TaskHandle_t xLedTaskHandle; // 在 main.c 中定义
void EXTI0_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 任务实现
c
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "bsp_led.h"
#include "bsp_exti.h"
TaskHandle_t xLedTaskHandle = NULL;
/* LED 任务:等待任务通知,收到后翻转 LED */
void vLedTask(void *pvParameters)
{
while (1)
{
/* ulTaskNotifyTake(pdTRUE, portMAX_DELAY):
- pdTRUE:获取后将通知值清零
- portMAX_DELAY:无限等待,直到通知值 > 0 */
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
LED3_Toggle();
}
}
int main(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 与二值信号量的对比
| 特性 | 二值信号量 | 任务通知 |
|---|---|---|
| 创建对象 | 需要 | 不需要 |
| 发送方 | 任意任务或中断 | 知道目标任务的句柄 |
| 接收方 | 任意任务可同时等待同一信号量 | 仅目标任务可接收 |
| 内存开销 | 需分配信号量控制块 | 无额外开销 |
| 适用场景 | 多对一、多对多同步 | 单对单同步,轻量级事件通知 |
在"一个中断唤醒一个特定任务"的简单场景中,任务通知是最优选择。
五、临界区与任务通知的配合
有时我们需要在任务中访问共享变量,同时又要保证不被中断破坏。例如,记录按键次数并显示。我们可以用临界区保护计数器:
c
volatile uint32_t key_count = 0;
void vLedTask(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 利用率与断言。