用了好久的FreeRTOS以前只是知道,如果在中断服务程序中调用某一些FreeRTOS的API函数时需要注意,如果有ISR版本的一定要调用末尾带ISR的函数,并且中断服务程序要调用freeRTOS的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 是一个和具体平台和配置相关的的宏,如果配置了configAssert 宏则这个宏函数指向vPortValidateInterruptPriority这个函数。这个函数也是一个和具体平台相关的函数所以他实现在port.c中。解析如下
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 ); }
这个函数的执行过程大致是(Cortex-m3架构)
- 获取当前活跃的中断号
- 判断是否是内核外的的异常---中断,如果是外部的中断则获取当前中断的优先级
- 如果当前中断的优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY所表示的优先级的话则此处断言失败。
这也就解释了为什么不能在优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY宏的中断中调用系统API函数了。但是为什么freeRTOS实现的时候需要增加这一段代码呢,这才是本文想要探究的问题。。。。。。
为了探究上面的问题这里选择了同一个API函数任务挂起后的恢复的普通版本和ISR的版本进行对比。
普通版本(不可以在中断中调用)
void vTaskResume ( TaskHandle_t xTaskToResume ) { TCB_t* const pxTCB = ( TCB_t* ) xTaskToResume; /* It does not make sense to resume the calling task. */ configASSERT ( xTaskToResume ); /* The parameter cannot be NULL as it is impossible to resume the currently executing task. */ if ( ( pxTCB != NULL ) && ( pxTCB != pxCurrentTCB ) ) { /* 基于cortex-m3 内核实际上是调用的是vPortRaiseBASEPRI 它增加了临界区的嵌套深度的统计变量, 并设置basepri寄存器的值为configMAX_SYSCALL_INTERRUPT_PRIORITY 这实际上就是屏蔽了中断优先级低于这个值 的所有中断。 */ taskENTER_CRITICAL(); { if ( prvTaskIsTaskSuspended ( pxTCB ) != pdFALSE ) { traceTASK_RESUME ( pxTCB ); /* The ready list can be accessed even if the scheduler is suspended because this is inside a critical section. */ /* 将当前任务从之前的状态的list中删除 */ ( void ) uxListRemove ( & ( pxTCB->xStateListItem ) ); /* 将当前任务加入就绪状态的list中,就绪状态的任务将会被调度器调度 */ prvAddTaskToReadyList ( pxTCB ); /* A higher priority task may have just been resumed. */ if ( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) { /* This yield may not cause the task just resumed to run, but will leave the lists in the correct state for the next yield. */ /* 恢复的进程的优先级高于当前任务的优先级,则调用portYIELD() 这个接口的实现就是悬起pensv中断(行为等同systick的中断干的事情) 然后在没有任何中断的时候就会切换任务。 */ taskYIELD_IF_USING_PREEMPTION(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } /* 基于cortex-m3 内核实际上是调用的是vPortExitCritical 减小临界区的嵌套,如果临界区嵌套已经为0了则 设置basepri寄存器的值为0这实际上就是取消了对任何中断的屏蔽。 */ taskEXIT_CRITICAL(); } else { mtCOVERAGE_TEST_MARKER(); } }
中断版本
BaseType_t xTaskResumeFromISR ( TaskHandle_t xTaskToResume ) { BaseType_t xYieldRequired = pdFALSE; TCB_t* const pxTCB = ( TCB_t* ) xTaskToResume; UBaseType_t uxSavedInterruptStatus; configASSERT ( xTaskToResume ); /* 上面分析过了,这里把注释翻译一下,这里是和普通版的第一处区别。 支持中断嵌套的RTOS端口具有最大系统调用(或最大API调用)中断优先级的概念。 即使RTOS内核处于关键区域,超过最大系统调用优先级的中断也会永久启用,但无法 对FreeRTOS API函数进行任何调用。如果在FreeRTOSConfig.h中定义了configASSERT(), 则如果从已分配了高于配置的最大系统调用优先级的优先级的中断调用FreeRTOS API函数, 则portASSERT_IF_INTERRUPT_PRIORITY_INVALID()将导致断言失败。只能从已被分配了 最大系统调用中断优先级或在逻辑上低于优先级的中断中调用以FromISR结尾的FreeRTOS函数。 */ portASSERT_IF_INTERRUPT_PRIORITY_INVALID(); /* 实际调用的是ulPortRaiseBASEPRI 他和vPortRaiseBASEPRI 的区别就是,这个接口会返回 basepri寄存器之前配置值,以用来进行恢复中断屏蔽的设置并不会操作临界区嵌套深度 */ uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); { if ( prvTaskIsTaskSuspended ( pxTCB ) != pdFALSE ) { traceTASK_RESUME_FROM_ISR ( pxTCB ); /* Check the ready lists can be accessed. */ if ( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) { /* Ready lists can be accessed so move the task from the suspended list to the ready list directly. */ if ( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) { /* 这里的处理就是和普通版的函数的区别的第二个位置, */ xYieldRequired = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } /* 同样是将当前唤醒的进程从对应状态的列表中移除,然后将其加入到就绪任务列表中 */ ( void ) uxListRemove ( & ( pxTCB->xStateListItem ) ); prvAddTaskToReadyList ( pxTCB ); } else { /* The delayed or ready lists cannot be accessed so the task is held in the pending ready list until the scheduler is unsuspended. */ /* 如果任务调度器挂起了,则不允许操作就绪列表,则将当前唤醒进程加入到xPendingReadyList */ vListInsertEnd ( & ( xPendingReadyList ), & ( pxTCB->xEventListItem ) ); } } else { mtCOVERAGE_TEST_MARKER(); } } /* 恢复之前的中断掩蔽配置,并不会操作临界区嵌套深度 */ portCLEAR_INTERRUPT_MASK_FROM_ISR ( uxSavedInterruptStatus ); /* 返回师傅需要进行一次任务调度 */ return xYieldRequired; }
对比两个函数的实现方式不同的地方有
1、中断下的接口不操作临界区嵌套深度,仅设置中断屏蔽避免中断嵌套。
2、中断下的接口在遇到需要任务调度的情况下的处理不是引起一次任务调度,而是将是否需要进行任务调度的状态返回。实际使用如下:
/* Resume the suspended task. */ xYieldRequired = xTaskResumeFromISR( xHandle ); if( xYieldRequired == pdTRUE ) { /* A context switch should now be performed so the ISR returns directly to the resumed task. This is because the resumed task had a priority that was equal to or higher than the task that is currently in the Running state. NOTE: The syntax required to perform a context switch from an ISR varies from port to port, and from compiler to compiler. Check the documentation and examples for the port being used to find the syntax required by your application. It is likely that this if() statement can be replaced by a single call to portYIELD_FROM_ISR() [or portEND_SWITCHING_ISR()] using xYieldRequired as the macro parameter: portYIELD_FROM_ISR( xYieldRequired );*/ portYIELD_FROM_ISR(); }
其中 portYIELD_FROM_ISR 的实现和具体的平台硬件相关,在cortex-m3上的实现就是直接使用普通版本的portYIELD()。除此之外通过上述的注释我们也能明白freeRTOS不允许临界区无法屏蔽的中断调用系统API。
ISR结尾的函数不会触发任务调度,而是返回需不需要任务调度,这是为什么呢????这就需要去看看,任务调度函数的实现了,这里就语言论述了。
任务切换由可悬起中断服务函数(PendSV)处理,这是一个汇编函数,其中主要做的事情是将,任务当前的上下文以PSP进行压栈(保存现场),然后调用vTaskSwitchContext,找到就绪的其他任务(优先级相同或者高于当前任务)。然后从新的任务堆栈中出栈(偷梁换柱-操作系统的巧妙之处),在中断返回时处理器会按寄存器中保存的返回地址返回,然后就跑到新的任务上下文了。这个过程通常是由滴答定时器,按照固定周期触发的,具体的触发过程由滴答定时器中断服务函数置起中断标志位,然后在滴答定时器中断处理完成后PendSV中断就会执行,进而任务切换,用一个图来说明一下这个过程吧。但是我想不通一点,M3的内核是支持中断咬尾操作的,所以应该是下图的这样的一个过程。
但是当任务A在运行用到了寄存器R8,但是当滴答定时器中断来临时一部分寄存器被硬件自动压栈保存(不包括R8)①,然后开始执行滴答定时器中断服务函数,一堆处理完成后悬起PendSV中断,这时当滴答定时器准备退出时难道会按中断咬尾的方式直接进行PendSV中断服务函数。从源码看出来PendSV服务函数,会将剩下的寄存器R4-R11等继续入栈(用psp)这里栈指针没有问题,但是,R4-R11可能已经在滴答定时器中断服务程序中被使用过,所以他不是之前的任务堆栈的状态,这样压栈没有问题吗??哎想不通,难道这里不会按中断咬尾处理,先出栈再进PendSV中断,希望知道的人指导一下,不胜感激。。。。(2019-08-10)。后来我找来了吃灰已久的开发板移植了FreeRTOS进行仿真测试,测试发现实际的效果就是后面的处理方式(没有发生中断咬尾)具体原因是什么尚不得知,如果有知道的大佬麻烦留言指导下。(2019-08-17 14:34:43)这个问题先放下继续探究主题。这里的问题其实是自己理解的不透彻导致的,具体的中断服务程序的压栈工作是硬件做了固定的部分(仅压栈一定会使用的寄存器从而减少硬件保存现场的时间),然后之前疑惑的高组寄存器的现场保存的问题是由编译器决定的,因为编译器如何翻译C代码的中断服务函数是自己清楚的,所以能明确自己是否使用了高组寄存器从而决定是否要进行必要的压栈(编译器处理的软件行为),所以前面的问题就不攻自破了,至于说是否发生了中断咬尾操作也就没有任何区别了。
总结
如果在中断中不调用ISR结尾系统API函数而使用普通版本的API接口会发生什么,为什么不能这么使用?通过查看多个类似的ISR结尾的函数和普通版本的区别发现就是普通API接口函数会增加临界区的嵌套且可能会直接调用portYIELD()以触发一次任务调度。 但是如果在中断中调用普通版本的API则可能出现问题,多方查询发现这两种解释是最可能的答案,一是为了保证系统的实时性避免高优先级中断被系统调用掩蔽从而响应延迟;其次是在普通任务中调用的portYIELD函数和中断中调用的portYIELD函数的实现不同因此需要区别对待,但是在cortex-m3上两者实现是相同的。以上两种答案好像都无法合理解释我心中的疑虑,最后找到资料有这样一句“freeRTOS支持中断嵌套,低于configMAX_SYSCALL_INTERRUPT_PRIORITY优先级的中断里才允许调用FreeRTOS 的API函数,而优先级高于这个值的中断则可以像前后台系统下一样正常运行,但是这些中断函数不能调用系统API函数”然后豁然开朗,因为操作系统为了保证操作系统内核的运行稳定性,保证API 执行重要的过程都是原子操作,这样就不会存在系统运行紊乱,比如如果在中断一个可控的中断中进行插入链表的操作,但是又一个优先级优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断发生了并且调用了系统API这样他就有可能打破低优先级中断的链表操作导致内核数据的毁坏,此时系统的运行就会出现紊乱。因此需要将系统的API相关重要操作“原子化“,从而避免系统核心数据操作紊乱。FreeRTOS是支持中断嵌套的,但是低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断之间不会嵌套以保证系统API的操作的”原子性“。简单分析两种情况下的嵌套:
1、先发生了中断优先级为“中等”且低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断M然后发生中断优先级为“高等”且高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断H,这里的中断响应过程大致是发生中断M时的处理过程和前后台中断过程相同,由硬件自动入栈,然后中断执行在这过程中又来了中断H,此时使用MSP将M的执行现场保存(这里保存的有可能还有任务的现场数据---因为有可能M中断仅使用除硬件自动入栈的寄存器外的Rx所以M中断保存现场时仅保存了Rx,进程却使用了Ry但是中断M未使用所以此时的Ry在M中是不需要保存的,但是因为H中断会用到Ry所以H中断会压栈Ry到MSP的栈中此时的Ry是发生M前的任务现场数据)然后开始运行H中断,H完成执行后POP数据到M中断后接着运行M中断服务程序(此时也恢复了Ry)。
2、先发生了中断优先级为“中等”的中断M然后发生中断优先级为“高等”的中断H(M和H都高configMAX_SYSCALL_INTERRUPT_PRIORITY)此时的嵌套和前后台系统下的情况相同。
如果中断的优先级比configMAX_SYSCALL_INTERRUPT_PRIORITY高,则这些中断可以直接触发不会被RTOS延时,如果优先级比其低,则有可能被RTOS延时。
2020-11-28 17:50:03 修改