FreeRTOS中断管理

目录

  • 1.中断
  • 2.为什么使用两套API
    • 2.1 为什么
    • 2.2 优势
    • 2.3 第三方库
  • 3.两套API列表
  • 4.pxHigherPriorityTaskWoken参数和portYIELD_FROM_ISR宏
  • 5.中断处理拆分
  • 6.屏蔽中断
    • 6.1 核心原理
    • 6.2 在任务中屏蔽中断
    • 6.3 在中断服务程序(ISR)中屏蔽中断
    • 6.4 高优先级中断不允许调用FreeRTOS的API
  • 7. 暂停调度器(Scheduler Suspension)
  • 8. 与其他资源管理机制对比

img

ISR设计铁律:短、快、无阻塞。

1.中断

在RTOS中,中断是硬件事件的实时响应机制。当中断发生时,CPU会立即暂停当前任务,转而执行中断服务例程(ISR)。中断处理的典型流程:

  • 硬件触发:外部事件(如按键、定时器溢出)触发中断信号。
  • 保存上下文:CPU自动保存当前任务的状态(如寄存器值、程序计数器)。
  • 跳转到ISR:根据中断向量表,执行对应的中断服务函数。
  • 执行ISR:快速处理关键操作(如清除中断标志、记录事件)。
  • 恢复上下文:恢复被中断任务的现场,继续执行原任务或调度更高优先级任务。

发生中断就得去处理,并且是越快越好,因此在freertos中:

  • ISR必须高效:避免阻塞,确保实时性。
  • 将耗时操作移至任务中执行(ISR仅做预处理)。其实这里就有点像Linux将中断分为了上半部分和下半部分中断去进行处理
  • 中断优先级高于所有任务:即使是最低优先级的中断,其执行权也高于最高优先级的任务。
  • 任务只能在无中断时运行:中断会抢占任务,但任务不能抢占中断。
处理阶段 执行环境 操作内容 时间要求
ISR 中断上下文 清除中断标志、记录事件、触发任务 微秒级(短)
任务 任务上下文 复杂数据处理、业务逻辑、通信 毫秒级(可长)

ISR虽然是软件实现的,但是却和硬件密切相关,何时调用、调用哪个ISR都是由硬件决定。

2.为什么使用两套API

2.1 为什么

在 FreeRTOS 中,任务(Task)和中断服务例程(ISR)对操作系统的资源(如队列、信号量、定时器等)有不同的使用要求。这种差异源于 任务和中断的本质区别,需要设计两套独立的 API 来满足不同的需求,上面介绍中断的时候也说了,中断要避免阻塞,确保实时性,归根到底还是由于对阻塞行为的不同处理才需要设计两套API。

// 任务中向队列发送数据,若队列满则阻塞等待 100 ticks
xQueueSend(xQueue, &data, 100 / portTICK_PERIOD_MS);

以上面这个函数,对于任务来说, 当任务调用某个 API(如向队列发送数据)时,如果资源不可用(如队列已满),任务可以选择 阻塞等待(Block),直到资源可用或超时。 这能提高 CPU 利用率,任务主动让出 CPU,调度器可运行其他任务,这就适用于非实时要求性较高的场景。

// 错误!在 ISR 中使用任务 API,可能导致不可预知行为
void ISR_Handler() {
    xQueueSend(xQueue, &data, 0); // 阻塞参数无意义,但函数内部可能包含阻塞逻辑
}

而如果中断的处理函数中使用了任务版本的API,就像上面的代码,这可能会导致了一些不好的结果,毕竟 ISR 运行在中断上下文中,无法被调度器管理,阻塞或是其他情况会导致系统崩溃,比如看看xQueueSend函数的内部实现,以阻塞参数为0为前提,只展示部分代码:

#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) \
    xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
BaseType_t xQueueSemaphoreTake( QueueHandle_t xQueue,
                               TickType_t xTicksToWait )
{
    //省略
    for( ; ; )
    {
        taskENTER_CRITICAL();
        {
            /* Semaphores are queues with an item size of 0, and where the
             * number of messages in the queue is the semaphore's count value. */
            const UBaseType_t uxSemaphoreCount = pxQueue->uxMessagesWaiting;

            /* Is there data in the queue now?  To be running the calling task
             * must be the highest priority task wanting to access the queue. */
            if( uxSemaphoreCount > ( UBaseType_t ) 0 )
            {
                traceQUEUE_RECEIVE( pxQueue );

                /* Semaphores are queues with a data size of zero and where the
                 * messages waiting is the semaphore's count.  Reduce the count. */
                pxQueue->uxMessagesWaiting = uxSemaphoreCount - ( UBaseType_t ) 1;

                #if ( configUSE_MUTEXES == 1 )
                {
                    if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
                    {
                        /* Record the information required to implement
                             * priority inheritance should it become necessary. */
                        pxQueue->u.xSemaphore.xMutexHolder = pvTaskIncrementMutexHeldCount();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                #endif /* configUSE_MUTEXES */

                /* Check to see if other tasks are blocked waiting to give the
                 * semaphore, and if so, unblock the highest priority such task. */
                if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
                {
                    if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
                    {
                        queueYIELD_IF_USING_PREEMPTION();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }

                taskEXIT_CRITICAL();
                return pdPASS;
            }
            else
            {
                if( xTicksToWait == ( TickType_t ) 0 )
                {
                    /* For inheritance to have occurred there must have been an
                     * initial timeout, and an adjusted timeout cannot become 0, as
                     * if it were 0 the function would have exited. */
                    #if ( configUSE_MUTEXES == 1 )
                    {
                        configASSERT( xInheritanceOccurred == pdFALSE );
                    }
                    #endif /* configUSE_MUTEXES */

                    /* The semaphore count was 0 and no block time is specified
                     * (or the block time has expired) so exit now. */
                    taskEXIT_CRITICAL();
                    traceQUEUE_RECEIVE_FAILED( pxQueue );
                    return errQUEUE_EMPTY;
                }
                else if( xEntryTimeSet == pdFALSE )
                {
                    /* The semaphore count was 0 and a block time was specified
                     * so configure the timeout structure ready to block. */
                    vTaskInternalSetTimeOutState( &xTimeOut );
                    xEntryTimeSet = pdTRUE;
                }
                else
                {
                    /* Entry time was already set. */
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        }
        taskEXIT_CRITICAL();

    }

即使xTicksToWait为0,会跳过阻塞逻辑: if( xTicksToWait == ( TickType_t ) 0 ), return errQUEUE_EMPTY;返回一个错误,但是这部分代码调用到了如 taskENTER_CRITICAL() 等任务级代码,这些宏不兼容 ISR 环境,可能导致内核状态混乱。而freertos内核其实也没那么傻,假设我们真在中断中调用了,内核内部也有相应的措施,将taskENTER_CRITICAL()继续展开可以看到以下定义:

void vPortEnterCritical( void )
{
    portDISABLE_INTERRUPTS();
    uxCriticalNesting++;

    /* This is not the interrupt safe version of the enter critical function so
     * assert() if it is being called from an interrupt context.  Only API
     * functions that end in "FromISR" can be used in an interrupt.  Only assert if
     * the critical nesting count is 1 to protect against recursive calls if the
     * assert function also uses a critical section. */
    if( uxCriticalNesting == 1 )
    {
        configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
    }
}

portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK == 0,检查 CPU 的 中断活跃状态寄存器,若值非零,表示当前正在执行中断服务程序(ISR),触发断言,进行报错。官方在里面也给了注释了。

因此为了能在中断中去使用像队列、信号量等这些功能,也就提供了另一套api,也就是相比较于任务的多了以FromISR为结尾的函数。

2.2 优势

那为什么不直接使用同一套代码,在代码内部直接添加类似于if else的条件语句判断是任务上下文还是中断上下文不就好了?????

避免运行时上下文判断
如果统一使用一套 API,函数内部需通过 if (is_in_isr()) 判断当前是任务还是 ISR 上下文。这种分支判断会:

  • 增加代码体积:每个 API 函数都需插入判断逻辑。
  • 降低执行效率:额外的判断语句影响性能,尤其在频繁调用的 API(如队列操作)中。
// 假设统一 API(伪代码)
BaseType_t xQueueSend(...) {
    if (is_in_isr()) {
        // ISR 逻辑:无阻塞,直接返回
    } else {
        // 任务逻辑:可能阻塞
    }
}

// FreeRTOS 实际设计:分离为两套 API
BaseType_t xQueueSend(...)        // 任务专用,含阻塞逻辑
BaseType_t xQueueSendFromISR(...) // ISR 专用,无阻塞

对于任务是需要有阻塞参数的,但是对于中断则是不需要的,因此设置两套API还可以简化参数, 如果强行合并 API,参数列表会变得臃肿,且某些参数在特定上下文中无效。

并且某些处理器(如低端 MCU)无法通过软件简单判断当前是否在 ISR 中,需依赖特殊指令或寄存器读取。两套 API 的设计避免了这种硬件依赖,简化了 FreeRTOS 的移植。

2.3 第三方库

当使用第三方库时,如果该库内部调用了 FreeRTOS API,但未区分任务和 ISR 上下文,可能导致潜在风险。可以这么解决:

(1) 推迟中断处理(Deferred Interrupt Processing)

  • ISR 仅记录事件,通过队列或信号量通知一个高优先级任务,由该任务调用第三方库函数。
void ISR_Handler() {
    // 在 ISR 中发送通知到队列
    xQueueSendFromISR(xQueue, &data, &xHigherPriorityTaskWoken);
}

void DeferredTask() {
    while (1) {
        xQueueReceive(xQueue, &data, portMAX_DELAY);
        ThirdParty_Library_SendData(data); // 在任务中调用第三方库
    }
}

(2) 强制使用 FromISR API

  • 修改第三方库:将库内的 FreeRTOS API 调用替换为 FromISR 版本(需谨慎验证)。
// 第三方库原代码
xSemaphoreGive(xSemaphore);

// 修改为
xSemaphoreGiveFromISR(xSemaphore, NULL); // 假设在任务中调用也允许

3.两套API列表

类型 在任务中 在ISR中
队列(queue) xQueueSendToBack xQueueSendToBackFromISR
xQueueSendToFront xQueueSendToFrontFromISR
xQueueReceive xQueueReceiveFromISR
xQueueOverwrite xQueueOverwriteFromISR
xQueuePeek xQueuePeekFromISR
事件组(event group) xEventGroupSetBits xEventGroupSetBitsFromISR
xEventGroupGetBits xEventGroupGetBitsFromISR
任务通知(task notification) xTaskNotifyGive vTaskNotifyGiveFromISR
xTaskNotify xTaskNotifyFromISR
软件定时器(software timer) xTimerStart xTimerStartFromISR
xTimerStop xTimerStopFromISR
xTimerReset xTimerResetFromISR
xTimerChangePeriod xTimerChangePeriodFromISR

4.pxHigherPriorityTaskWoken参数和portYIELD_FROM_ISR宏

ISR版本的函数,除了名字不同,关键就在于pxHigherPriorityTaskWoken参数。先以任务中以队列举个例子,任务A调用 xQueueSendToBack() 写队列,有几种情况发生:

  • 队列满了,任务A阻塞等待,另一个任务B运行
  • 队列没满,任务A成功写入队列,但是它导致另一个任务B被唤醒,任务B的优先级更高:任务B先
  • 运行
  • 队列没满,任务A成功写入队列,即刻返回

可以看到,在任务中调用API函数可能导致任务阻塞、任务切换,这叫做"context switch",上下文切换。这个函数可能很长时间才返回,在函数的内部实现了任务切换,来看一下xQueueSendToBack函数内部的实现就知道了:

#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) \
    xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )

BaseType_t xQueueGenericSend( QueueHandle_t xQueue,
                              const void * const pvItemToQueue,
                              TickType_t xTicksToWait,
                              const BaseType_t xCopyPosition )
{
    BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;
    TimeOut_t xTimeOut;
    Queue_t * const pxQueue = xQueue;

    /*---------------------- 参数合法性检查 ----------------------*/
    configASSERT( pxQueue ); // 队列句柄必须有效
    configASSERT(!( (pvItemToQueue == NULL) && (pxQueue->uxItemSize != 0) )); // 数据指针非空检查
    configASSERT(!( (xCopyPosition == queueOVERWRITE) && (pxQueue->uxLength != 1) )); // 覆盖模式必须为队列长度1
    #if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
        configASSERT(!( (xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED) && (xTicksToWait != 0) )); // 调度器挂起时不可阻塞
    #endif

    /*---------------------- 主循环尝试写入队列 ----------------------*/
    for( ; ; )
    {
        taskENTER_CRITICAL(); // 进入临界区(关中断)
        {
            /* 检查队列是否有空间或允许覆盖写入 */
            if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
            {
                traceQUEUE_SEND( pxQueue ); // 追踪队列发送事件

                /*---------------------- 数据写入队列 ----------------------*/
                #if ( configUSE_QUEUE_SETS == 1 )
                {
                    // 队列集(Queue Set)相关逻辑
                    const UBaseType_t uxPreviousMessagesWaiting = pxQueue->uxMessagesWaiting;
                    xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );

                    if( pxQueue->pxQueueSetContainer != NULL )
                    {
                        if( (xCopyPosition == queueOVERWRITE) && (uxPreviousMessagesWaiting != 0) )
                        {
                            // 覆盖写入且队列非空时,不通知队列集
                            mtCOVERAGE_TEST_MARKER();
                        }
                        else if( prvNotifyQueueSetContainer( pxQueue ) != pdFALSE )
                        {
                            // 队列集通知成功,触发任务切换(高优先级任务就绪)
                            queueYIELD_IF_USING_PREEMPTION(); // 任务切换点1 ⚡
                        }
                    }
                    else
                    {
                        // 检查是否有任务等待接收数据
                        if( listLIST_IS_EMPTY( &(pxQueue->xTasksWaitingToReceive) ) == pdFALSE )
                        {
                            if( xTaskRemoveFromEventList( &(pxQueue->xTasksWaitingToReceive) ) != pdFALSE )
                            {
                                // 唤醒的任务优先级更高,触发任务切换
                                queueYIELD_IF_USING_PREEMPTION(); // 任务切换点2 ⚡
                            }
                        }
                        else if( xYieldRequired != pdFALSE )
                        {
                            // 互斥锁释放顺序异常时的特殊切换
                            queueYIELD_IF_USING_PREEMPTION(); // 任务切换点3 ⚡
                        }
                    }
                }
                #else /* 非队列集模式 */
                {
                    xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
                    if( listLIST_IS_EMPTY( &(pxQueue->xTasksWaitingToReceive) ) == pdFALSE )
                    {
                        if( xTaskRemoveFromEventList( &(pxQueue->xTasksWaitingToReceive) ) != pdFALSE )
                        {
                            queueYIELD_IF_USING_PREEMPTION(); // 任务切换点4 ⚡
                        }
                    }
                    else if( xYieldRequired != pdFALSE )
                    {
                        queueYIELD_IF_USING_PREEMPTION(); // 任务切换点5 ⚡
                    }
                }
                #endif /* configUSE_QUEUE_SETS */

                taskEXIT_CRITICAL(); // 退出临界区(开中断)
                return pdPASS; // 发送成功
            }
            else /* 队列已满 */
            {
                if( xTicksToWait == 0 )
                {
                    taskEXIT_CRITICAL();
                    traceQUEUE_SEND_FAILED( pxQueue );
                    return errQUEUE_FULL; // 不阻塞,直接返回队列满错误
                }
                else if( xEntryTimeSet == pdFALSE )
                {
                    vTaskInternalSetTimeOutState( &xTimeOut ); // 初始化超时计时器
                    xEntryTimeSet = pdTRUE;
                }
            }
        }
        taskEXIT_CRITICAL(); 

        /*---------------------- 队列已满时的阻塞处理 ----------------------*/
        vTaskSuspendAll(); // 挂起调度器
        prvLockQueue( pxQueue ); // 锁定队列(防止并发操作)

        if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
        {
            if( prvIsQueueFull( pxQueue ) != pdFALSE )
            {
                traceBLOCKING_ON_QUEUE_SEND( pxQueue );
                vTaskPlaceOnEventList( &(pxQueue->xTasksWaitingToSend), xTicksToWait ); // 将任务挂起到发送等待列表
                prvUnlockQueue( pxQueue ); 

                if( xTaskResumeAll() == pdFALSE )
                {
                    portYIELD_WITHIN_API(); // 恢复调度器后立即触发切换 ⚡任务切换点6
                }
            }
            else
            {
                prvUnlockQueue( pxQueue );
                ( void ) xTaskResumeAll(); // 队列不再满,重试发送
            }
        }
        else /* 超时处理 */
        {
            prvUnlockQueue( pxQueue );
            ( void ) xTaskResumeAll();
            traceQUEUE_SEND_FAILED( pxQueue );
            return errQUEUE_FULL;
        }
    }
}

进行任务切换的调用:queueYIELD_IF_USING_PREEMPTIONportYIELD_WITHIN_API,可以看出是在函数的内部中去直接调用相关代码实现任务的切换。

但是,对于FormISR版本的函数,是不会在内部去实现任务的切换的,回到前面讲的参数:pxHigherPriorityTaskWoken, 标记是否因操作(如发送队列、释放信号量)导致 更高优先级任务就绪。若为 pdTRUE,表示需要手动触发任务切换。

上面讲到了xQueueSendToBack,而xQueueSendToBackFromISR() 函数也可能导致任务切换,但是不会在函数内部进行切换,而是返回一个参数,也就是pxHigherPriorityTaskWoken,表示传给宏portYIELD_FROM_ISR是否需要切换,函数原型与用法如下:

BaseType_t xHigherPriorityTaskWoken = pdFALSE; // 必须初始化为 pdFALSE

// 在 ISR 中发送数据到队列
xQueueSendFromISR(xQueue, &data, &xHigherPriorityTaskWoken);

// 检查是否需要任务切换
if (xHigherPriorityTaskWoken == pdTRUE) {
    portYIELD_FROM_ISR(pdTRUE); // 触发任务切换
}

为什么不在 FromISR 函数内部直接切换任务?

  • FreeRTOS 的设计哲学是 将任务切换的决策权交给开发者,而非在每次 API 调用时自动切换。
  • 避免多次切换:若 ISR 中多次调用 FromISR API(如循环处理多个数据包),每次调用都切换任务会导致性能浪费。毕竟我中断程序正在执行,即使调用了任务切换函数,那个切换的任务也还是运行不了,还是放在了任务就绪链表,资源仍然被CPU占用,一次任务切换还能接受,那要是多次,就很浪费性能了。
  • 批量处理:通过累积多个操作的切换请求,最后 一次性切换,减少上下文切换次数。
// 错误设计:每次发送数据都切换任务(低效)
void UART_ISR() {
    for (int i=0; i<10; i++) {
        xQueueSendFromISR(..., &xWoken); //xWoken就是BaseType_t * const pxHigherPriorityTaskWoken
        if (xWoken) portYIELD_FROM_ISR(pdTRUE); // 每次循环都可能切换
    }
}

// 正确设计:批量处理完成后切换任务(高效)
void UART_ISR() {
    BaseType_t xWoken = pdFALSE;
    for (int i=0; i<10; i++) {
        xQueueSendFromISR(..., &xWoken);
    }
    portYIELD_FROM_ISR(xWoken); // 仅切换一次
}

再来看下切换任务的宏portYIELD_FROM_ISR, 修改 CPU 的 中断返回地址,使 ISR 退出后直接运行调度器,而非返回被中断的任务。

#define portEND_SWITCHING_ISR( xSwitchRequired )    do { if( xSwitchRequired != pdFALSE ) portYIELD(); } while( 0 )
#define portYIELD_FROM_ISR( x )                     portEND_SWITCHING_ISR( x )

就是判断传进来的pxHigherPriorityTaskWoken参数是否不为pdFALSE,条件成立就调用了portYIELD(),源程序是这样标注portYIELD()的:Scheduler utilities.,调度器实用程序

5.中断处理拆分

当硬件中断处理耗时较长时,直接全程在 ISR(中断服务例程) 中执行会引发以下问题:

  • 实时性下降:低优先级中断无法及时响应。
  • 系统卡顿:用户任务长时间无法执行。
  • 中断嵌套风险:若中断嵌套层数过多,可能导致堆栈溢出或调度混乱。

将中断处理拆分为 快速 ISR延迟任务,通过优先级调度平衡实时性与处理效率。

FreeRTOS中断管理_第1张图片

阶段 1:任务 1 运行,中断发生(t1 → t2)

  • 初始状态

    • 任务 1(绿色):正在运行(如数据计算、UI 刷新)。
    • 任务 2(蓝色):处于阻塞态(等待中断事件)。
  • 中断触发:硬件事件(如按键按下、数据接收完成)触发中断信号。

  • CPU 响应

    • 立即暂停任务 1,保存其上下文(寄存器、程序计数器)。
    • 跳转至中断向量表,执行对应的 ISR(红色)

阶段 2:ISR 快速处理(t2 → t3)

  • ISR 核心职责
    1. 清除中断标志:通知硬件中断已处理,避免重复触发。
    2. 记录关键数据:如读取传感器值、存储接收到的字节。
    3. 触发延迟任务:通过队列、信号量唤醒 任务 2。:
  • 仅处理必要操作,时间控制在微秒级。
  • 禁止调用可能阻塞的 API(如 vTaskDelay)。

示例

void UART_ISR() {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint8_t data = UART_ReadByte(); // 读取数据
    xQueueSendFromISR(xQueue, &data, &xHigherPriorityTaskWoken); // 发送到队列
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 触发任务切换
}

阶段 3:高优先级任务 2 处理中断(t3)

  • 任务 2 设计

    • 优先级高于任务 1:确保中断处理优先于普通任务。
    • 执行耗时操作(如协议解析、复杂计算)。
  • ISR 退出后,调度器选择 最高优先级就绪任务(即任务 2)。

  • 任务 2 从队列中取出数据,完成后续处理。

示例

void vDeferredTask(void *pvParameters) {
    while (1) {
        uint8_t data;
        if (xQueueReceive(xQueue, &data, portMAX_DELAY) == pdPASS) {
            process_complex_data(data); // 耗时操作
        }
    }
}

阶段 4:任务 2 完成,任务 1 恢复(t3 → t4)

  • 任务 2 阻塞:处理完成后,主动阻塞以等待下一中断(如调用 xQueueReceive 阻塞)。

  • 任务 1 恢复:调度器重新选择任务 1 继续执行。

  • 系统状态回归

    • 任务 1 继续运行,任务 2 等待新中断。
    • 中断响应周期完成,系统恢复常态。

至于中断和任务之间的通信:队列、信号量、互斥量、事件组、任务通知等等方法,都可使用。

6.屏蔽中断

6.1 核心原理

FreeRTOS通过操作ARM Cortex-M处理器的 BASEPRI寄存器 实现中断屏蔽。该寄存器允许设置一个优先级阈值:低于或等于该阈值的中断会被屏蔽,而更高优先级的中断仍可触发,但不可调用FreeRTOS API。

#define configMAX_SYSCALL_INTERRUPT_PRIORITY 5  // 示例值,FreeRTOS中默认的是191

假设该值为5,则优先级数值≤5的中断会被屏蔽(优先级数值越小,实际优先级越高)。


6.2 在任务中屏蔽中断

taskENTER_CRITICAL();  // 进入临界区(屏蔽中断)
/* 操作临界资源(如全局变量、硬件寄存器) */
taskEXIT_CRITICAL();   // 退出临界区(恢复中断)

优先级≤configMAX_SYSCALL_INTERRUPT_PRIORITY的中断被屏蔽。 优先级更高的中断(如硬件故障中断)仍可触发,但不允许调用FreeRTOS API(如队列操作、任务切换函数)。

这里再提一下:优先级数值越小,实际优先级越高(例如优先级 0 > 优先级 1)

FreeRTOS中断管理_第2张图片

支持嵌套调用,内部通过计数器记录嵌套深度,仅当深度归零时恢复中断。

中断延迟:低优先级中断无法及时响应,可能影响系统实时性。 代码简洁性:临界区代码必须简短,避免长时间占用(通常建议控制在几十微秒内)。

来看看函数实际上的内部实现:

taskENTER_CRITICAL()
    #define taskENTER_CRITICAL()               portENTER_CRITICAL()
        #define portENTER_CRITICAL()                      vPortEnterCritical()
            portDISABLE_INTERRUPTS();
                #define portDISABLE_INTERRUPTS()                  vPortRaiseBASEPRI()

//最后可以看到:
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
    {
        uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;

        __asm
        {
            /* Set BASEPRI to the max syscall priority to effect a critical
             * section. */
/* *INDENT-OFF* */
            msr basepri, ulNewBASEPRI
            dsb
            isb
/* *INDENT-ON* */
        }
    }
/*----

msr basepri, ulNewBASEPRI就是在basepri寄存器中去屏蔽优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断


6.3 在中断服务程序(ISR)中屏蔽中断

void ISR_Handler(void) {
    UBaseType_t uxSavedStatus;
    uxSavedStatus = taskENTER_CRITICAL_FROM_ISR();  // 进入临界区,保存中断状态
    /* 操作临界资源 */
    taskEXIT_CRITICAL_FROM_ISR(uxSavedStatus);       // 恢复原始中断状态
}

ISR执行时中断可能已被屏蔽(如嵌套中断),因此需保存当前中断状态uxSavedStatus,退出时还原。

使用FROM_ISR后缀的宏,确保在中断上下文中正确处理状态。

ISR内需快速访问共享资源(如读取传感器数据到全局缓冲区)。

  • 低优先级的中断被屏蔽了:优先级低于、等于configMAX_SYSCALL_INTERRUPT_PRIORITY

  • 高优先级的中断可以产生:优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY

    • 但是,这些中断ISR里,不允许使用FreeRTOS的API函数
  • 任务调度依赖于中断、依赖于API函数,所以:这两段代码之间,不会有任务调度产生

6.4 高优先级中断不允许调用FreeRTOS的API

前面提到了屏蔽了中断,它虽然能屏蔽,但是屏蔽的也只能是低优先级中断,高优先级中断是无法屏蔽的,并且也只能在低优先级的中断中去调用ISR版本Freertos的API(FromISR结尾的函数),在高优先级的中断服务程序中是不允许去调用任务的API的

就以xQueueSendToBackFromISR函数为例子:

#define xQueueSendToBackFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) \
    xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )

--->


BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue,
                                     const void * const pvItemToQueue,
                                     BaseType_t * const pxHigherPriorityTaskWoken,
                                     const BaseType_t xCopyPosition )
{
    BaseType_t xReturn;
    UBaseType_t uxSavedInterruptStatus;
    Queue_t * const pxQueue = xQueue;

    configASSERT( pxQueue );
    configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
    configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );

    /* RTOS ports that support interrupt nesting have the concept of a maximum
     * system call (or maximum API call) interrupt priority.  Interrupts that are
     * above the maximum system call priority are kept permanently enabled, even
     * when the RTOS kernel is in a critical section, but cannot make any calls to
     * FreeRTOS API functions.  If configASSERT() is defined in FreeRTOSConfig.h
     * then portASSERT_IF_INTERRUPT_PRIORITY_INVALID() will result in an assertion
     * failure if a FreeRTOS API function is called from an interrupt that has been
     * assigned a priority above the configured maximum system call priority.
     * Only FreeRTOS functions that end in FromISR can be called from interrupts
     * that have been assigned a priority at or (logically) below the maximum
     * system call interrupt priority.  FreeRTOS maintains a separate interrupt
     * safe API to ensure interrupt entry is as fast and as simple as possible.
     * More information (albeit Cortex-M specific) is provided on the following
     * link: https://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html */
    portASSERT_IF_INTERRUPT_PRIORITY_INVALID();

    //省略

}

只摘取关键部分:portASSERT_IF_INTERRUPT_PRIORITY_INVALID()

#define portASSERT_IF_INTERRUPT_PRIORITY_INVALID()    vPortValidateInterruptPriority()

-->
void vPortValidateInterruptPriority( void )
{
    uint32_t ulCurrentInterrupt;  /* 用于保存当前执行的中断号 */
    uint8_t ucCurrentPriority;    /* 用于保存当前中断的优先级 */

    /* 获取当前正在执行的中断号。
     * vPortGetIPSR() 返回 IPSR(中断程序状态寄存器)的值,
     * IPSR 中包含当前中断号,若当前不在中断中,该值通常为 0。 */
    ulCurrentInterrupt = vPortGetIPSR();

    /* 判断当前中断号是否为用户自定义的中断。
     * portFIRST_USER_INTERRUPT_NUMBER 定义了第一个用户中断的编号,
     * 系统中低于该编号的中断由内核或系统使用。 */
    if( ulCurrentInterrupt >= portFIRST_USER_INTERRUPT_NUMBER )
    {
        /* 根据当前中断号查找该中断在中断优先级寄存器数组中的优先级值。
         * pcInterruptPriorityRegisters 是一个数组,每个索引对应一个中断的优先级。 */
        ucCurrentPriority = pcInterruptPriorityRegisters[ ulCurrentInterrupt ];

        /* 下面的断言用于确保使用 FreeRTOS 安全 API 的中断的优先级不能太高。
         *
         * 对于 Cortex-M 架构,数值较低的优先级表示逻辑上较高的优先级。
         * configMAX_SYSCALL_INTERRUPT_PRIORITY 定义了允许调用 FreeRTOS
         * API 的最高中断优先级(数值较高的实际优先级)。
         *
         * 因此,如果一个中断使用了 ISR 安全的 FreeRTOS API,其优先级
         * 必须被设置为大于或等于 ucMaxSysCallPriority(即数值上不低于 configMAX_SYSCALL_INTERRUPT_PRIORITY)。
         *
         * 注意:默认情况下,某些中断可能处于最高优先级(0),
         * 这种情况是非法的,因为它们肯定高于 configMAX_SYSCALL_INTERRUPT_PRIORITY。
         */
        configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );
    }

    /* 验证优先级分组设置:
     *
     * Cortex-M 的中断控制器(NVIC)允许将中断优先级的位分为两部分:
     * 1. 抢占优先级(Pre-emption priority):决定中断抢占关系。
     * 2. 子优先级(Sub-priority):用于相同抢占优先级中确定响应顺序。
     *
     * 为了简化 FreeRTOS 的实现,要求所有的优先级位都应被用作抢占优先级,
     * 而不应分出子优先级。如果存在子优先级,可能会导致 FreeRTOS API 调用时
     * 出现不可预测的行为。
     *
     * portAIRCR_REG 是应用程序中断和复位控制寄存器,
     * portPRIORITY_GROUP_MASK 用于提取其中的优先级分组位。
     * ulMaxPRIGROUPValue 是允许的最大优先级分组值(确保全部位都是抢占优先级)。
     *
     * 如果优先级分组的设置大于 ulMaxPRIGROUPValue,则说明有部分位被当作子优先级,
     * 这会导致系统行为异常,因此断言失败。
     */
    configASSERT( ( portAIRCR_REG & portPRIORITY_GROUP_MASK ) <= ulMaxPRIGROUPValue );
}

确保所有使用 FreeRTOS API 的中断均设置在允许的优先级范围内,并且系统中断优先级的分组配置符合 FreeRTOS 的要求。这有助于避免优先级反转和不可预知的中断响应问题。

configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );这个便是关键,其实就是要求所处中断服务程序的优先级必须在限定范围内(低优先级部分),也就是优先级数值高于configMAX_SYSCALL_INTERRUPT_PRIORITY(freertos默认设置的是191)

FreeRTOS中断管理_第3张图片

也就是说如上图,调用该xQueueSendToBackFromISR函数的中断服务程序的优先级,必须在0xBF(191)往下部分的优先级范围内,191往上的部分,属于高优先级,不允许调用FreeRTOS的API,否则configASSERT就会触发断言,这个断言可以设置成死循环来表示程序出错或是崩溃

显然,之前我们经常讲的屏蔽中断,屏蔽的其实都是低优先级中断。通过屏蔽低优先级中断可以防止任务的切换,而任务切换依赖的是tick中断去触发调度,因此tick中断就是低优先级中断的其中一种

7. 暂停调度器(Scheduler Suspension)

如果有别的任务来跟你竞争临界资源,你可以把中断关掉:这当然可以禁止别的任务运行,但是这代价太大了。它会影响到中断的处理。

如果只是禁止别的任务来跟你竞争,不需要关中断,暂停调度器就可以了:在这期间,中断还是可以发生、处理。

通过全局变量uxSchedulerSuspended控制调度器状态:

  • 暂停时:禁止任务切换,但不关闭中断,允许中断正常响应。
  • 恢复时:检查是否有更高优先级任务就绪,可能触发一次上下文切换。
vTaskSuspendAll();      // 暂停调度器
/* 长时间操作(如处理大数据块) */
if (xTaskResumeAll() == pdTRUE) {
    // 若有更高优先级任务就绪,可能立即切换任务
}

支持嵌套调用,恢复时需所有嵌套层级均调用xTaskResumeAll()。但是耗时操作:如文件系统操作、大块内存拷贝(需避免任务切换但允许中断响应)。

不可中断的API调用:暂停期间无法调用vTaskDelay()等依赖调度的函数。 实时性影响:长时间暂停可能导致高优先级任务饥饿。


8. 与其他资源管理机制对比

机制 中断影响 任务切换 适用场景
互斥量 不屏蔽 允许 公平竞争,通用资源共享
屏蔽中断 部分屏蔽 禁止 极短时间操作(<微秒级)
暂停调度器 不屏蔽 禁止 较长时间操作(允许中断响应)
守护任务 不屏蔽 允许 集中管理资源(如统一外设访问)

你可能感兴趣的:(freeRtos,FreeRTOS,嵌入式,中断)