FreeRTOS-内核控制函数
- FreeRTOS中有一些内核函数,一般来说这些内核函数在应用层不会使用,但是内核控制函数是理解FreeRTOS中断的基础。接下来我们逐一分析这些内核函数。
taskYIELD()
- 该函数的作用是进行任务切换,这是一个宏定义,实际上调用了portYIELD()。portYIELD函数定义如下:
#define portYIELD() \
{ \
\
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
\
\
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
- 该函数中实际进行任务切换的是portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT。我们深入来看一下portNVIC_INT_CTRL_REG 的定义:
#define portNVIC_INT_CTRL_REG ( * ( ( volatile uint32_t * ) 0xe000ed04 ) )
- 看到这里可能会有点懵,因为这里涉及到Cortex-M3内核的一部分知识,《cortex-M3权威指南》提到这一部分如下表所示:
- 该宏定义对应的地址是0xe000ed04,这证号和中断控制及状态寄存器ICSR地址对应上,上图中我们仅截取了一部分,在分析这里时只需要知道这一部分就行了。接下来我们再看一下portNVIC_PENDSVSET_BIT的定义:
#define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )
- 到这里其实这段代码的意思就很清晰了,其实就是将ICSR的第28位置1。那么置1和进行任务切换有什么关系呢?我们在描述中可见这时会将pendSV悬起,那么pendSV又是什么?
- pendSV是可悬起的系统调用,当软件将PendSV悬起寄存器写入1时,会执行一个任务切换,如果当前任务优先级高于其他任务,则会延缓切换,否则直接切换。
- 到这里我们将上面的过程串起来就是:当调用taskYIELD()函数时,实际上就是想pendSV悬起寄存器里写入1,然后硬件自动进行任务切换。
taskENTER_CRITICAL()
- 当有一部分代码希望不被FreeRTOS所管理的高优先级任务打断时,可以调用该函数进入临界区,进入临界区后将会屏蔽操作系统所能管理的任务切换和中断。该函数其实是调用了void vPortEnterCritical( void ),函数定义如下:
void vPortEnterCritical( void )
{
portDISABLE_INTERRUPTS();
uxCriticalNesting++;
if( uxCriticalNesting == 1 )
{
configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}
}
- 该函数定义主要就两句,一个是调用了portDISABLE_INTERRUPTS()函数,另外就是让全局变量uxCriticalNesting自加一。我们先来分析一下portDISABLE_INTERRUPTS()函数,其定义如下(portDISABLE_INTERRUPTS()是一个宏定义,真正的函数是vPortRaiseBASEPRI):
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
msr basepri, ulNewBASEPRI
dsb
isb
}
}
-
这是一段汇编代码,其实主要就是将configMAX_SYSCALL_INTERRUPT_PRIORITY这个宏赋值给basepri寄存器,我们先来看看basepri寄存器是干什么的,在CM3权威指南中有如下说明: -
该寄存器被称为中断可屏蔽器,作用是用于屏蔽优先级低于某个数值的中断,从而保证对时间要求严格的任务能够按照预期来完成任务。并且上表中的所有寄存器是CM3的特殊功能寄存器,只能由指令MSR\MRS访问,所以代码中的msr指令可以理解为专门为这些特殊功能寄存器设置的赋值命令。那么如何正确的赋值才能达到我们预期的屏蔽效果呢?这是一个比较复杂的问题,下面一步一步来分析。 -
要解决这个问题,就必须要从中断优先级分组说起,CM3中划分为两种优先级,一种是抢占优先级,另一种是子优先级(或响应优先级)。CM3支持高达256级可编程优先级,相应地由一个8位的寄存器用于表达这256级的优先级。但是CM最多支持128级抢占优先级,而不是256级,这是因为内核在出厂时厂家就规定了表达抢占优先级的位只能在[7:1]这个区间内,因此抢占优先级最高为128级,相应优先级最高为256级,这是CM3中的优先级定义。下图是优先级分组表。 -
但是单片机在设计的时候并不会将这256个优先级全部使用,因为如果全部使用的话芯片架构将会变得很复杂,因此,stm32仅仅拿出了这8位中的高4位来表达它的优先级,低4位则无效。因此,stm32最高支持16级的优先级。那么如何来确定优先级的高低呢?实际上这同使用库函数编程的时候判断方法是相同的,因stm32为例,假定分组位置为3,全部设置为抢占优先级,如果往寄存器中写入0001 xxxx表达的优先级要比往寄存器中写入0011 xxxx表达的优先级高。知道了这些以外还需要明确一点的是,设置BASEPRI寄存器屏蔽优先级的方法和设置中断优先级的方法是相同的,因为这里只有4位来表示中断优先级,因此要屏蔽优先级不高于5的中断(注意CM3内核中始终认为优先级数字为0的那一个是最高优先级,优先级数字越大优先级越低),只需要以下指令即可:
msr basepri 5<<4
-
到这里们就知道了CM3中断优先级分组和设置中断优先级以及屏蔽中断优先级的原则了,那么回到代码中继续分析宏configMAX_SYSCALL_INTERRUPT_PRIORITY 其定义如下:
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
#define configPRIO_BITS __NVIC_PRIO_BITS
#define __NVIC_PRIO_BITS 4
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY (5<<(8-4))
- 这不就相当于将那些优先级不高于5的中断给屏蔽了吗?所以从这里也可以看出,我配置的FreeRTOS最高可以管理优先级为5的中断,那些更高优先级的中断如1,2等FreeRTOS是管理不了的。到这里其实还是有一个问题,那就是,我们这里只是屏蔽了优先级不高于5的中断,而更高优先级的任务不是同样会抢占CPU使用权吗,这样不也就会导致任务切换吗?实际上,在FreeRTOS中,任务的切换也是由中断产生的而这个,在前面我们已经分析了只需要将pendSV中的悬起寄写入1即可产生中断,从而触发任务切换,另外就是SysTick这个系统时钟,当这个系统时钟产生中断时,同样可以发生任务切换。这是任务切换的两种途径,既然任务切换只能由这两种途径发生,那么如果我们进入临界区的时候也将这两个中断给屏蔽掉,那么将不再产生任务切换,也就没有高优先级任务抢占的发生了,这样不就解决问题了吗?所以下面我们来看一下这两个中断的中断优先级是多少,在port.c文件和FreeRTOSConfig.h中找到了这两个中断的优先级有关的宏,
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15
#define configPRIO_BITS __NVIC_PRIO_BITS
#define __NVIC_PRIO_BITS 4
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )
#define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )
- 从上面大致看出,这两个优先级很有可能会被设置为最低,如果真是这样,那么,nice!我们的问题已经解决了,但是如果不是,那么将会很头疼。接下来就深入分析一下这两个中断的优先级是否真的为最低。我们再回到CM3权威指南中找到了两个优先级设置的寄存器,表格如下:
- 从上面可以看出我们只需要设置PRI_14和PRI_15这两个寄存器就可以设置其中断优先级。我们再看看代码中是不是这样设置的,找到这两个中断优先级的引用的地方,发现在启动任务调度器中有这两个语句:
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
portNVIC_SYSPRI2_REG定义如下:
#define portNVIC_SYSPRI2_REG ( * ( ( volatile uint32_t * ) 0xe000ed20 ) )
- 发现它的对应地址是0xE000ED20,而PendSV对应的是0xE000ED22,SysTick对应的是0xE000ED23,似乎不对呀?但是不要忘了,portNVIC_PENDSV_PRI对应的宏定义是有2字节的移位的,这样经过移位之后刚好对应0xE000ED22,而且其中断优先级为15,即最低的。SysTick的优先级也是这么设置的。
#define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )
- 到这里进入中断临界区的深入分析已经结束了,看似简单的调用和赋值其实蕴含了很多CM3核心的知识。
- 再总结一下过程:当要进入临界区的时候,会将系统所能管理的中断全部屏蔽掉,相应的触发任务切换的中断也会屏蔽掉,从而不会受到高优先级任务抢占和中断的影响,确保了对时间要求严格的任务如期进行。但是同样的,有一些更高优先级(不由FreeRTOS管理的)的中断触发的话是会影响到任务运行的。
taskEXIT_CRITICAL()
- 既然有进入临界区,那么必然也会有退出临界区,退出临界区和进入临界区背后的原理是相同的,下面是退出临界区的函数定义(实际上调用了vPortExitCritical()函数):
void vPortExitCritical( void )
{
configASSERT( uxCriticalNesting );
uxCriticalNesting--;
if( uxCriticalNesting == 0 )
{
portENABLE_INTERRUPTS();
}
}
- 退出临界区的代码很简单,首先uxCriticalNesting自减1,uxCriticalNesting的作用就是记录进入临界区的嵌套次数,每进入一次就自加1,退出1次就自减1,知道uxCriticalNesting=0是恢复所有中断。下面来看一下portENABLE_INTERRUPTS()函数。
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
msr basepri, ulBASEPRI
}
}
- 由此可见,只是将0赋值给basepri中断屏蔽寄存器,在CM3权威指南中提到,如果往basepri中写0,那么所有被屏蔽的中断都将被接触,从而完全退出了临界区,原理同进入一样,这里就不仔细分析了。
taskENTER_CRITICAL_FROM_ISR()
- 中断级进入临界区和任务级进入临界区非常相似,从下面的函数定义中就可以发现:
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
mrs ulReturn, basepri
msr basepri, ulNewBASEPRI
dsb
isb
}
return ulReturn;
}
- 中断级的进入临界区只是将中断屏蔽寄存器的值取出来,作为函数返回值返回了,其他并没有区别。所以这里不再赘述了。
taskEXIT_CRITICAL_FROM_ISR()
taskDISABLE_INTERRUPTS()
- 同样是调用vPortRaiseBASEPRI()函数,将FreeRTOS所能管理的中断全部屏蔽。
taskENABLE_INTERRUPTS()
- 同样是调用vPortSetBASEPRI( 0 )函数,将FreeRTOS所能管理的中断全部开启。
vTaskSupendAll()
- 该函数用于挂起任务调度器,当任务调度器挂起之后,任务调度器相当于暂停工作了,不会产生任务调度。任务调度器挂起操作很简单,函数定义如下:
void vTaskSuspendAll( void )
{
++uxSchedulerSuspended;
}
- 代码仅有1行,就是将uxSchedulerSuspended这个全局变量自加1,任务挂起可以嵌套,当uxSchedulerSuspended=0时,才表示任务调度器解挂,这一点在讲述任务调度器解挂操作会分析。那么uxSchedulerSuspended变量自加1后会对任务切换等产生什么影响呢?下面是截取的相关函数:
BaseType_t xTaskIncrementTick( void )
{
···
traceTASK_INCREMENT_TICK( xTickCount );
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
if( xConstTickCount == ( TickType_t ) 0U )
{
taskSWITCH_DELAYED_LISTS();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
···
}
···
}
- 从上面代码中可以看出,只有当uxSchedulerSuspended = 0时,即任务调度器没有被挂起时,滴答定时器计数变量才会自加1,也就相当于关闭了滴答定时器计数。下面一段代码是关于任务切换的。
void vTaskSwitchContext( void )
{
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
{
xYieldPending = pdTRUE;
}
else
{
xYieldPending = pdFALSE;
traceTASK_SWITCHED_OUT();
#if ( configGENERATE_RUN_TIME_STATS == 1 )
{
#ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
#else
ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
#endif
if( ulTotalRunTime > ulTaskSwitchedInTime )
{
pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
ulTaskSwitchedInTime = ulTotalRunTime;
}
#endif
taskCHECK_FOR_STACK_OVERFLOW();
taskSELECT_HIGHEST_PRIORITY_TASK();
traceTASK_SWITCHED_IN();
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif
}
}
- 从上面代码中可见,调度器挂起的时候不会进行任务切换,这样就相当于直接关掉了任务切换。
- 所以 uxSchedulerSuspended 变量是在函数中通过判断来实现关闭任务切换和滴答定时器的计数。
xTaskResumeAll()
- 任务调度器的解挂就是判断uxSchedulerSuspended是否为0,如果为0则进行解挂操作。系统中调用多少次vTaskSupendAll()就需要调用多少次xTaskResumeAll()才能达到任务调度器解挂的操作。下面来深入分析该函数的源码。
BaseType_t xTaskResumeAll( void )
{
TCB_t *pxTCB = NULL;
BaseType_t xAlreadyYielded = pdFALSE;
configASSERT( uxSchedulerSuspended );
taskENTER_CRITICAL();
{
--uxSchedulerSuspended;----(1)
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )----(2)
{
if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U )----(3)
{
while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE )----(4)
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &xPendingReadyList ) );----(5)
( void ) uxListRemove( &( pxTCB->xEventListItem ) );----(6)
( void ) uxListRemove( &( pxTCB->xStateListItem ) );----(7)
prvAddTaskToReadyList( pxTCB );----(8)
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )----(9)
{
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
if( pxTCB != NULL )----(10)
{
prvResetNextTaskUnblockTime();
}
{
UBaseType_t uxPendedCounts = uxPendedTicks;
if( uxPendedCounts > ( UBaseType_t ) 0U )----(11)
{
do
{
if( xTaskIncrementTick() != pdFALSE )----(12)
{
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
--uxPendedCounts;----(13)
} while( uxPendedCounts > ( UBaseType_t ) 0U );----(14)
uxPendedTicks = 0;----(15)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
if( xYieldPending != pdFALSE )----(16)
{
#if( configUSE_PREEMPTION != 0 )
{
xAlreadyYielded = pdTRUE;
}
#endif
taskYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
return xAlreadyYielded;
}
- xTaskResumeAll()函数主要完成两个任务,一个是任务列表的变换,另一个是时间补偿。下面分别来深入分析这两部分。
- 更改任务列表主要是因为,当任务调度器被挂起的时候,如果有中断导致任务调度,但这时候任务调度器处于挂起状态,无法进行任务切换,所以该任务就将被分配到xPendingReadyList中,等到任务调度器解除挂起后,立刻恢复到就绪列表中。所以在代码中描述为:
(1)、uxSchedulerSuspended自减1 (2)、如果任务调度器解挂 (3)、判断系统中任务数量,如果系统中没有任务则不需要继续进行了。 (4)、循环直到将xPendingReadyList中的任务全部移动到就绪列表中 (5)、获取处于xPendingReadyList中的任务控制块 (6)、任务控制块中有两个列表项,一个是事件列表,另一个是状态列表,如果要将任务添加到就绪列表中,就需要将这两个列表全部移除。所以该句就是将任务控制块中的事件列表移除。 (7)、相应的,该句是将任务控制块中的状态列表移除。 (8)、将该任务添加到就绪列表中 (9)、判断刚添加到就绪列表中的任务优先级是否大于正在运行的任务的任务优先级,大于则进行任务切换 (10)、重新设置下一个任务解除阻塞的时间节点,即距现在最近的那个任务节点 - 由于任务调度器挂起后,滴答定时器的计数器不再工作,所以中间有一段时间没有记录时间点,所以第二部分的所要干的就是将这一部分时间补偿回来。虽然滴答定时器的计数器不工作了,但是这时候另一个计数器uxPendedCounts 会代替xTickCount继续计数。这样只需要重复调用uxPendedCounts 次xTaskIncrementTick()函数就可以将这段时间补偿回来。下面来分析这部分代码。
(11)、判断uxPendedCounts计数次数是否大于0,只有在大于0的时候才有能正常补偿时间 (12)、调用xTaskIncrementTick()进行一次时间补偿,并判断是否需要进行任务切换,该函数通过返回pdTRUE来表示需要进行任务切换 (13)、uxPendedCounts自减1 (14)、循环直到时间完全补偿 (15)、判断是否需要进行任务切换
到这里我们就将FreeRTOS的一些内核控制函数讲述完了,FreeRTOS内核控制函数相对来说比较难,而且不好理解,关系错综复杂。
|