FreeRTOS 手动移植教程(八):中断管理 —— 优先级、临界区与任务通知

前面几篇文章中,我们已经多次在中断里使用了 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 中断如何引发任务切换

中断服务函数中调用 xSemaphoreGiveFromISRxQueueSendFromISR 等函数时,可能会使更高优先级任务就绪。此时这些函数会返回一个 xHigherPriorityTaskWoken 标志。中断退出前,必须用 portYIELD_FROM_ISR 触发 PendSV,让内核切换到高优先级任务。忽略这个标志会导致任务延迟到下一次 SysTick 才运行,破坏实时性。


二、临界区------短暂的"关门"操作

2.1 什么是临界区

当一段代码操作了多个任务或中断共享的变量时,如果不加以保护,可能会在执行到一半时被中断打乱,造成数据损坏。FreeRTOS 提供了临界区宏,用于短暂地关闭和恢复中断:

c 复制代码
taskENTER_CRITICAL();
// ... 受保护的代码,此时内核不会切换任务,且可屏蔽的中断被禁用 ...
taskEXIT_CRITICAL();

在 Cortex-M3 中,这两个宏通过操作 BASEPRI 寄存器实现,关闭优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断,从而达到保护目的。

2.2 使用注意事项

  • 临界区应尽可能短:长时间关中断会破坏系统的实时性,甚至导致中断丢失。只保护必不可少的几条指令。
  • 临界区不能嵌套调用 FromISR API:因为临界区内部中断已被屏蔽,若强行调用会导致断言失败。
  • 不影响高优先级中断 :优先级高于 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.cbsp_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();
    }
}

而在中断中我们只做最简单的通知,避免在中断中执行耗时操作。


六、常见错误与调试

  1. 忘记 portYIELD_FROM_ISR:如果中断唤醒了更高优先级任务却没有调用该宏,任务会被延迟。
  2. 在临界区内调用阻塞 API:会导致任务永远挂起(因为调度器被锁定),典型症状是系统卡死。
  3. 中断优先级超出 configMAX_SYSCALL_INTERRUPT_PRIORITY :在 0~4 优先级中断中调用 RTOS API 将进入 configASSERT 死循环。
  4. 任务通知的接收者没有清空计数器 :如果使用 xTaskNotifyWait 而不清空,可能反复接收到旧数据。使用 ulTaskNotifyTake(pdTRUE, ...) 可安全清零。

七、总结

本篇系统地梳理了 FreeRTOS 的中断管理规则,并引入了两个重要技术:

  • 临界区:通过短暂屏蔽部分中断,保护共享数据;
  • 任务通知:零内存开销的任务间通信方式,尤其适用于中断到任务的单对单唤醒。

通过按键中断的实验,我们体会到了任务通知的简洁高效。在实际项目中,应根据同步场景合理选择信号量、队列或任务通知,以达到资源与性能的最佳平衡。

下一篇文章,我们将进入实时性与调试技巧篇,讨论如何检测任务栈溢出、分析 CPU 利用率,以及使用 configASSERT 定位早期错误。


下一篇:FreeRTOS 调试与优化 ------ 栈溢出检测、CPU 利用率与断言。

相关推荐
搁浅小泽1 小时前
电子器件常见的失效模式及对应的失效原因分析
单片机·嵌入式硬件
振南的单片机世界2 小时前
AFIO重映射:USART1_TX从PA9搬PB6,救活一版PCB
stm32·单片机·嵌入式硬件
不做无法实现的梦~2 小时前
Ubuntu 22.04 下使用 CMSIS-DAP 编译和烧录 STM32
linux·stm32·ubuntu
破晓单片机2 小时前
009、STM32单片机分享:智能窗帘系统
stm32·单片机·嵌入式硬件
清风6666663 小时前
基于单片机的智慧城市垃圾桶系统设计
单片机·毕业设计·智慧城市·课程设计·期末大作业
凡科建站3 小时前
单片机IO不够?ULN2003A救急方案
单片机·嵌入式硬件
二十画~书生3 小时前
3款阻容降压电源电路设计详解
经验分享·单片机·嵌入式硬件·硬件工程
点灯小铭3 小时前
基于单片机的智能一体化自动咖啡机设计
数据库·单片机·毕业设计·课程设计·期末大作业
纳祥科技3 小时前
音频ADC芯片基础解析:为什么计算机需要它来理解真实世界?
网络·单片机·音视频·智能音箱