一、两套API

FreeRTOS 的所有内核对象操作(队列、信号量、任务通知等)都提供两套 API,分别用于任务上下文中断上下文

特性 任务级 API (普通版) 中断级 API (FromISR 后缀)
函数示例 xQueueSend() xQueueSendFromISR()
调用场景 任务函数中 中断服务程序 (ISR) 中
阻塞/等待 支持。若资源不可用,可阻塞等待 (xTicksToWait)。 不支持。必须立即返回,不能休眠。
调度行为 直接调度。若唤醒高优先级任务,立即触发上下文切换。 延迟调度。仅标记需要切换,不直接执行切换。
关键参数 xTicksToWait (等待时间) pxHigherPriorityTaskWoken (输出标记)

中断上下文的特殊性

  • 不可阻塞/休眠:中断服务程序必须尽快执行,中断版本的 API 没有等待时间参数,总是立即返回成功或失败。

  • 可能唤醒更高优先级的任务:当在中断中向队列发送数据时,可能唤醒一个因等待该队列而阻塞的任务。如果这个被唤醒的任务优先级比当前被中断的任务高,理论上应该进行任务切换。

  • 但中断内不宜立即切换:如果每唤醒一个高优先级任务就立即切换,而中断中可能多次调用 API(比如多次发送数据),就会导致多次无意义的切换,浪费 CPU 时间。更好的做法是:只记录“需要切换”的标志,在中断全部处理完毕、即将退出时,再进行一次统一的切换

以队列为例:

  • pxHigherPriorityTaskWoken:中断版本多了一个输出参数。这是一个指向 BaseType_t 的指针。在函数内部,如果因为本次操作唤醒了一个更高优先级的任务,就会将此变量设置为 pdTRUE。调用者需要根据这个值决定是否在中断退出时触发任务切换。

void GPIO_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint32_t data;

    // 读取按键数据...
    data = read_gpio();

    // 发送到队列,可能唤醒等待的任务
    xQueueSendFromISR(xQueue, &data, &xHigherPriorityTaskWoken);

    // 如果还有其他操作,可以继续调用其他 FromISR 函数,传入同一个标志变量
    // xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);

    // 最后,如果需要切换,则触发 PendSV
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

二、两类中断

FreeRTOS 将中断优先级划分为两个区域,通过一个阈值进行分割。这个阈值由宏 configMAX_SYSCALL_INTERRUPT_PRIORITY 定义(在 FreeRTOSConfig.h 中)。

ARM Cortex-M (M3/M4/M7) 的中断优先级数值越小,优先级越高(0 最高,255 最低)。

区域 优先级范围 名称 能否调用 FreeRTOS API? 用途
高优先级区 0 ~ MaxSysCall - 1
(数值更小)
不受管制的中断 ❌ 绝对禁止 极高实时性要求,完全不依赖 OS。如:电机急停、看门狗喂食等自己定义的业务。
低优先级区 MaxSysCall ~ 255
(数值更大)
受管制的中断 ✅ 允许调用 普通业务中断。如:GPIO 按键、UART 接收、定时器等自己定义的业务。

目的:

1.保证高优先级中断的实时性:高优先级中断通常处理时间关键事件(如电机控制、紧急报警),不应该被操作系统内部的临界区(关中断)延迟。将它们隔离在 API 之外,确保它们总能被及时响应。

2.简化临界区保护:当 FreeRTOS 需要保护临界区时,它只需要关掉“允许调用 API 的那部分中断”,而不必关所有中断。这样高优先级中断依然可以响应,系统的实时性得到保障。

1.关中断策略:只关一类中断

FreeRTOS 的临界区保护函数 taskENTER_CRITICAL() / taskEXIT_CRITICAL() 以及内部使用的关中断操作,并不是关掉所有中断,而是只关掉优先级低于或等于阈值的中断

临时说明:

  • BASEPRI 是一个寄存器,它定义了一个屏蔽阈值

  • 当 BASEPRI 被设置为某个非零值 X 时,所有中断优先级数值大于等于 X 的中断都会被屏蔽(也就是屏蔽低优先级,保留高优先级)。

  • 但有一个例外:优先级数值为 0 的中断(最高逻辑优先级)永远无法被 BASEPRI 屏蔽。这正是为什么 configMAX_SYSCALL_INTERRUPT_PRIORITY 不能设为 0 的原因。

下面为taskENTER_CRITICAL() / taskEXIT_CRITICAL()核心源码

//进入临界区
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
    uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
    __asm
    {
        msr basepri, ulNewBASEPRI   /* 将 configMAX_SYSCALL_INTERRUPT_PRIORITY 写入 BASEPRI */
        dsb                          /* 数据同步屏障,确保写操作完成 */
        isb                          /* 指令同步屏障,确保之后指令使用新状态 */
    }
}

//退出临界区 ulBASEPRI  = 0 注意0是清除屏蔽,而不是只让优先级0运行
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
    __asm
    {
        msr basepri, ulBASEPRI       /* 直接写入 BASEPRI,无屏障 */
    }
}

在 ISR 中调用 xQueueSendFromISR() 时,如果该 ISR 属于允许使用 API 的类别,那么它同样会受到临界区关中断的影响(即当它调用 API 时,可能被其他同级中断打断?不,因为 BASEPRI 设置会屏蔽同级,所以 API 内部是安全的)。

2.中断优先级检查机制:防止误用 API

为了确保高优先级中断不会误调用 FreeRTOS API,FreeRTOS 在 FromISR 函数内部通常会插入一个检查宏:portASSERT_IF_INTERRUPT_PRIORITY_INVALID(),如果检查不通过就会断言。

注:在 FreeRTOS 中,configASSERT 是一种断言机制,用于在调试阶段捕获代码中的逻辑错误。当断言条件为假(即参数为 0)时,程序会进入死循环,方便开发者定位问题。

下面为该宏的核心代码

void vPortValidateInterruptPriority( void )
{
    uint32_t ulCurrentInterrupt;   /* 当前正在执行的中断号 */
    uint8_t ucCurrentPriority;      /* 当前中断的优先级数值(硬件格式) */

    /* 1. 获取当前中断号:通过读取 IPSR 寄存器得到正在处理的中断编号 */
    ulCurrentInterrupt = vPortGetIPSR();

    /* 2. 判断是否为用户中断(可编程中断),排除系统异常(如 HardFault 等) */
    if( ulCurrentInterrupt >= portFIRST_USER_INTERRUPT_NUMBER )
    {
        /* 3. 读取该中断的优先级数值(从 NVIC 优先级寄存器映射数组中获取) */
        ucCurrentPriority = pcInterruptPriorityRegisters[ ulCurrentInterrupt ];

        /* 4. 核心断言:检查当前中断的优先级是否允许调用 FreeRTOS API*/
        configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );
    }

    /* 5. 额外检查:中断优先级分组是否正确(所有位必须分配给抢占优先级)*/
    configASSERT( ( portAIRCR_REG & portPRIORITY_GROUP_MASK ) <= ulMaxPRIGROUPValue );
}

PendSV 和 SysTick 优先级最低:确保这两个内核中断的优先级是最低的,以避免阻塞其他中断

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐