探究为什么FreeRTOS 有些API不能在中断服务函数中调用,转而需要调用带ISR的版本

  用了好久的FreeRTOS以前只是知道,如果在中断服务程序中调用某一些FreeRTOS的API函数时需要注意,如果有ISR版本的一定要调用,末尾带ISR的函数,并且要调用系统的API函数,中断服务程序的中断优先级不能高于配置宏(configMAX_SYSCALL_INTERRUPT_PRIORITY)的值这是为什么呢。刚好今天受台风影响只能在家里窝着,所以就想着趁有时间看看这一部分的内容,研究一下为什么,那么废话不多说开干。

   找了几个函数简化一些安全检查的内容再把一些宏函数替换后对比观察了下内容如下:

TickType_t xTaskGetTickCount( void )
{
    TickType_t xTicks;
    {
        xTicks = xTickCount;
    }
    return xTicks;
}
/*-----------------------------------------------------------*/

TickType_t xTaskGetTickCountFromISR( void )
{
    TickType_t xReturn;
    UBaseType_t uxSavedInterruptStatus;
    portASSERT_IF_INTERRUPT_PRIORITY_INVALID();

    uxSavedInterruptStatus = portTICK_TYPE_SET_INTERRUPT_MASK_FROM_ISR();
    {
        xReturn = xTickCount;
    }
    portTICK_TYPE_CLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );

    return xReturn;
}

其中的函数

分别解析下

  • portASSERT_IF_INTERRUPT_PRIORITY_INVALID这是一个宏对应的真实函数为vPortValidateInterruptPriority,具体内容如下,这个函数解释了为什么不能在优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY配置宏的中端中调用系统API函数了。
void vPortValidateInterruptPriority( void )
{
    uint32_t ulCurrentInterrupt;
    uint8_t ucCurrentPriority;
    //参考内核指南,这个命令是获取当前的中断号
    ulCurrentInterrupt = vPortGetIPSR();
    /*portFIRST_USER_INTERRUPT_NUMBER 是一个和芯片相关的用户中断号
    在M3、M4的芯片上就是15以后是外部中断的中断号所以这里配置成16
    判断是不是在外部中断中调用的API函数,如果是执行if里的内容*/
    if( ulCurrentInterrupt >= portFIRST_USER_INTERRUPT_NUMBER )
    {
        /*根据中断服务函数的中断号获取当前中断的优先级设置*/
        ucCurrentPriority = pcInterruptPriorityRegisters[ ulCurrentInterrupt ];
        /*如果当前执行的中断优先级数字小于配置ucMaxSysCallPriority这个值实际上是
        configMAX_SYSCALL_INTERRUPT_PRIORITY,也就是当前中断优先级高于配置最高优先级
        断言将会失败,程序将停止在这里*/
        configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );
    }
    /*当前中断优先级分组组大于配置分组(也就是表示抢占优先级的位数少于配置)则断言失败,程序停止*/
    configASSERT( ( portAIRCR_REG & portPRIORITY_GROUP_MASK ) <= ulMaxPRIGROUPValue );
}
  • portTICK_TYPE_SET_INTERRUPT_MASK_FROM_ISR 和 portTICK_TYPE_CLEAR_INTERRUPT_MASK_FROM_ISR

这两个函数实际上是设置内核的中断屏蔽寄存器(BASEPRI),但是在xTaskGetTickCountFromISR函数中就是这样设计的,在另一个函数中也可以看出来,比如任务挂起恢复函数xTaskResumeFromISR同样简化后看

BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume )
{
    BaseType_t xYieldRequired = pdFALSE;
    TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume;
    UBaseType_t uxSavedInterruptStatus;

    configASSERT( xTaskToResume );

    portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
    //上面的内容同获取当前时钟SysTick相同,不说了
    /*portSET_INTERRUPT_MASK_FROM_ISR就是将中断掩蔽寄存器配置成系统调用最高中断优先级
    进而掩蔽中断优先级低于系统调用最高优先级*/
    uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
    {
        /*任务是否是挂起态*/
        if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE )
        {
            /*调试代码*/
            traceTASK_RESUME_FROM_ISR( pxTCB );

            /*调度器为运行态*/
            if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
            {
                /*恢复的任务比中断之前的任务优先级高,则将xYieldRequired设置,提示需要任务切换保证系统的实时性*/
                if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
                {
                    xYieldRequired = pdTRUE;
                }
                else
                {
                    
                }
                /*任务的一些状态等修改,并将任务加入就绪列表等待调度器调度*/
                ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
                prvAddTaskToReadyList( pxTCB );
            }
            else
            {
                /*调度器被暂停,此时只需要将任务加入挂起就绪列表,等任务调度器恢复后加入到就绪任务列表*/
                vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
            }
        }
        else
        {
        }
    }
    /*取消中断屏蔽*/
    portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
    /*返回是否需要任务切换状态*/
    return xYieldRequired;
}

对比普通版的任务恢复函数

void vTaskResume( TaskHandle_t xTaskToResume )
{
    TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume;
    configASSERT( xTaskToResume );
    if( ( pxTCB != NULL ) && ( pxTCB != pxCurrentTCB ) )
    {
        /*设置中断掩蔽,同时将临结嵌套深度增加1*/
        taskENTER_CRITICAL();
        {
            if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE )
            {
                traceTASK_RESUME( pxTCB );

                /*  */
                ( void ) uxListRemove(  &( pxTCB->xStateListItem ) );
                prvAddTaskToReadyList( pxTCB );

                /*任务的一些状态等修改,并将任务加入就绪列表等待调度器调度*/
                if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
                {
                    /*悬起PenSVC中断,退出临结后启动任务切换调度*/
                    taskYIELD_IF_USING_PREEMPTION();
                }
                else
                {
                    
                }
            }
            else
            {
                
            }
        }
        taskEXIT_CRITICAL();
    }
    else
    {
        
    }
}

对比两个函数的实现方式,不同的地方时ISR结尾的函数不会触发任务调度,而是返回需不需要任务调度,这是为什么呢????这就需要去看看,任务调度函数的实现了,这里就语言论述了。

     任务切换由可悬起中断服务函数(PendSV)负责,这是一个汇编函数,其中主要做的事情是将,任务当前的执行状态以PSP进行压栈(保存现场),然后调用vTaskSwitchContext,找到就绪的其他任务(优先级相同或者高于当前任务)然后从任务堆栈中出栈--改写寄存器(偷梁换柱-操作系统的巧妙之处),在返回时处理器会按寄存器中保存的返回地址直接返回,然后新的任务就运行了。这个过程通常是由滴答定时器,按照固定周期触发的,具体的触发过程由滴答定时器中断服务函数置起中断标志位,然后在滴答定时器退出PendSV中断就会执行,进而任务切换,用一个图来说明一下这个过程吧。但是我想不通一点,M3的内核是支持中断咬未操作的,所以应该是下图的这样的一个过程。

探究为什么FreeRTOS 有些API不能在中断服务函数中调用,转而需要调用带ISR的版本

但是当任务A在运行用到了寄存器R8但是,当滴答定时器中断来临时,一部分寄存器被硬件自动压栈保存(不包括R8)①,然后开始执行滴答定时器中断服务函数,一堆处理完成后悬起PendSV中断,这时当滴答定时器准备退出时难道会按中断咬尾的方式直接进行PendSV中断服务函数。从源码看出来PendSV服务函数,会将剩下的寄存器R4-R11等继续入栈(用psp),栈指针没有问题,但是,R4-R11可能已经在滴答定时器终端服务程序中被使用过,所以他不是之前的任务堆栈的状态,这样压栈没有问题吗??哎想不通,难道这里不会按中断咬尾处理,先出栈在进PendSV中断,希望知道的人指导一下,不胜感激。。。。(2019-08-10)。后来我找来了吃灰已久的开发板移植了FreeRTOS进行仿真测试,测试发现实际的效果就是后面的处理方式(没有发生中断咬尾)具体原因是什么尚不得知,如果有知道的大佬麻烦留言指导下。(2019-08-17 14:34:43)这个问题先放下继续探究主题。

  如果在中断中不调用ISR结尾或者在优先级高于系统调用优先级的中端中调用系统API函数会发生什么,为什么不能这么使用,通过查看多个类似的ISR结尾的函数和普通版本的区别发现,就是在中断中可调用的函数会增加设置中断掩蔽寄存器的操作,增加这一操作对于芯片而言就是防止一些低于configMAX_SYSCALL_INTERRUPT_PRIORITY优先级(在M4上就是优先级数字大于这个值)且高于当前中断优先级(中断优先级数字小于当前中断优先级)的中断打断当前中断中正在执行的系统调用。但是对于操作系统软件而言会有什么问题呢,从FreeRTOS的用户指导手册发现,ISR结尾的类似的函数都应该在中断中调用,并且一般情况下(uxQueueMessagesWaiting 除外)这类函数都会设置中断掩蔽寄存器,同时有时还返回是否需要立刻执行任务切换。又根据给出的使用实例系统调用返回后还是在中断服务函数中,此时需要手动检查是否需要开始一次任务切换,而非ISR结尾的函数则直接在API函数中调用portYIELD()触发一次任务调度,这就奇怪了,好像除了调用位置之外没有什么区别,最后多方查询发现这一种解释是最可能的答案,这是为了保证操作系统的实时性,因为如果有更高优先级的任务就绪了就要让高优先级的任务最快的速度运行起来,其次是在普通任务中调用的portYIELD函数和中断中调用的portYIELD函数的实现不同因此需要区别对待。以上两种答案好像都无法合理解释我心中的疑虑。最后找到资料有这样一句话“freeRTOS支持中断嵌套,低于configMAX_SYSCALL_INTERRUPT_PRIORITY优先级的中断里才允许调用FreeRTOS 的API函数,而优先级高于这个值的中断则可以像前后台系统下一样正常运行,但是这些中断函数不能调用系统API函数”,然后豁然开朗,因为操作系统为了保证操作系统内核的运行稳定性,保证API 执行重要的过程都是原子操作,这样就不会存在系统运行紊乱,比如如果在中断中释放信号量,但是这个中断又会被其他低于configMAX_SYSCALL_INTERRUPT_PRIORITY但是高于当前中断的中断程序打断,此时系统的运行过程就出现了紊乱,可能会导致任务优先级翻转的问题。因此需要将系统的API相关重要操作“原子”化,从而避免系统运行紊乱。FreeRTOS是支持中断嵌套的,分几种情况分析,不全,意思到就行。

  1. 任务A正在执行中,发生了中断优先级为“中等”的中断M然后发生中断优先级为“高等”的中断H(M和H都高于configMAX_SYSCALL_INTERRUPT_PRIORITY)

此时发生中断M,此时的处理过程和前后台中断过程相同,由硬件自动入栈,然后中断执行,然后又来了中断H,此时使用MSP将M的执行现场保存,然后开始运行H中断,完成后POP后接着运行M中断服务程序。

  1. 滴答定时器悬起了PendSV中断后发生了新的中断Q(优先级高于PendSV)。

    如果此中断在PendSV压栈完成后尚未取指,则次中断会按晚到中断处理先运行Q然后运行完后再以中断咬尾的方式运行PendSV,如果PendSV已经取指则按正常嵌套处理;如果新发生的中断优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY并且它还调用了系统 API函数如果此时PendSV中断函数已经完成了中断掩蔽寄存器的设置,则先执行PendSV后执行这个中断Q,反之中断Q会先执行(嵌套)然后执行PendSV。这里如果这个后来的中断Q服务历程调用的API会使某个高优先级的任务就绪的话,那么两种情况下的任务调度结果是不同的,差了一个系统时间片。

最后纠自己的①处的理解错误,R8具体压不压栈是由编译器做掉了,如果你发生中断以前用到了R8就会执行压栈操作,否则不会但是这个过程确实令人费解的是,中断是随机来的,代码编译时,已经确定是压栈还是不压栈了,也就是说不想函数调用之间有明显的关系可以进行判断。最后再次纠正自己,一次(手动狗头),具体用不用高位寄存器是根据中断服务函数来判断的,然后就不用管中断之前是否使用过了。Ok到此我觉得自己的问题解决了,这个帖子前后经历了一个周算是完成了,然后突然发现今天自己迈向25岁了,耳边响起了许巍的歌..........

 

 

 

 

 

探究为什么FreeRTOS 有些API不能在中断服务函数中调用,转而需要调用带ISR的版本

上一篇:Windows10右键添加“在此处打开命令窗口”


下一篇:(三十七)c#Winform自定义控件-有标题的面板