FreeRTOS 与 RT-Thread 和 μC/OS 一样,都支持时间片的功能。所谓时间片就是同一个优先级下可以有多个任务,每个任务轮流地享有相同的 CPU 时间,享有 CPU 的时间我们叫时间片。
在 RTOS 中,最小的时间单位为一个 tick,即 SysTick 的中断周期,RT-Thread 和 μC/OS可以指定时间片的大小为多个 tick,但是 FreeRTOS不一样,时间片只能是一个 tick。与其说 FreeRTOS 支持时间片,倒不如说它的时间片就是正常的任务调度。
先来做一个时间片的测试:
代码如下:
可以看到,任务1和任务2都是绝对的死循环,没有手动切换任务,也没有堵塞延时(堵塞延时中也会切换任务)。那么,可以想到的是,一旦开始调度,首先是最高优先级的任务3执行,然后任务3进入堵塞延时,开始执行任务1。知道任务3定时时间到期,再执行任务3,然后堵塞延时时继续执行任务1。好像我们在同一优先级下,只能执行头结点的任务啊,任务2如何执行?
/* 软件延时 */
void delay (uint32_t count)
{
for(; count!=0; count--);
}
/* 任务1 */
void Task1_Entry( void *p_arg )
{
for( ;; )
{
flag1 = 1;
//vTaskDelay( 1 );
delay (100);
flag1 = 0;
delay (100);
//vTaskDelay( 1 );
}
}
/* 任务2 */
void Task2_Entry( void *p_arg )
{
for( ;; )
{
flag2 = 1;
//vTaskDelay( 1 );
delay (100);
flag2 = 0;
delay (100);
//vTaskDelay( 1 );
}
}
void Task3_Entry( void *p_arg )
{
for( ;; )
{
flag3 = 1;
vTaskDelay( 1 );
//delay (100);
flag3 = 0;
vTaskDelay( 1 );
//delay (100);
}
}
为了解决这个问题,我们先看一下实验现象。实验现象如下:
可以看到,任务3执行(然后堵塞延时1tick),立马切换到了任务1,然后在下一个tick切换到了任务2,循环往复。为什么??
合理猜测一下,在同一优先级列表下,任务控制块的指针每次都会指向下一个列表项,这样,就能实现遍历同一优先级下的多任务函数(执行顺序:任务1 -> 任务2 -> ....)。
FreeRTOS时间片的原理:
之所以在同一个优先级下可以有多个任务, 最终还是得益于taskRESET_READY_PRIORITY()taskSELECT_HIGHEST_PRIORITY_TASK()这两个宏定义的实现方法。
1 taskSELECT_HIGHEST_PRIORITY_TASK() 寻找就绪任务的最高优先级
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
\
/* 寻找包含就绪任务的最高优先级的队列 */ \
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
/* 获取优先级最高的就绪任务的TCB,然后更新到pxCurrentTCB */ \
/* 关键就是这个宏listGET_OWNER_OF_NEXT_ENTRY,它并不是获取链表下的第一个节点的 OWNER, \
而且用于获取下一个节点的 OWNER,假设当前链表有 N 个节点,当第 N 次调用该函数时, \
pxIndex 则指向第 N个节点,即每调用一次,节点遍历指针 pxIndex 则会向后移动一次,用于指向下一个节点 */ \
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
} /* taskSELECT_HIGHEST_PRIORITY_TASK() */
listGET_OWNER_OF_NEXT_ENTRY()函数的妙处在于它并不是获取链表下的第一个节点的 OWNER,而且用于获取下一个节点的 OWNER。有下一个那么就会有上一个的说法,怎么理解?假设当前链表有 N 个节点,当第 N 次调用该函数时,pxIndex 则指向第 N个节点,即每调用一次,节点遍历指针 pxIndex 则会向后移动一次,用于指向下一个节点。
/* 获取链表节点的OWNER,即TCB */
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \
{ \
List_t * const pxConstList = ( pxList ); \
/* 节点索引指向链表第一个节点调整节点索引指针,指向下一个节点,
如果当前链表有N个节点,当第N次调用该函数时,pxInedex则指向第N个节点 */\
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
/* 当前链表为空 */ \
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
{ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
} \
/* 获取节点的OWNER,即TCB */ \
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \
}
本实验中,优先级 2 下有两个任务,当系统第一次切换到优先级为 2 的任务(包含了任务 1 和任务 2,因为它们的优先级相同)时,pxIndex 指向任务 1,任务 1 得到执行。当任务 1 执行完毕,系统重新切换到优先级为 2 的任务时,这个时候 pxIndex指向任务 2,任务 2得到执行,任务 1 和任务 2 轮流执行,享有相同的 CPU时间,即所谓的时间片。
本实验中,任务1和任务2的主体都是无限循环,那如果任务1和任务2 都会调用将自己挂起的函数(实际运用中,任务体都不能是无限循环的,必须调用能将自己挂起的函数),比如 vTaskDelay()。调用能将任务挂起的函数中,都会先将任务从就绪列表删除,然后将任务在优先级位图表 uxTopReadyPriority 中 应的位清零,由taskRESET_READY_PRIORITY()函数来实现。
#define taskRESET_READY_PRIORITY( uxPriority ) \
{ \
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 ) \
{ \
portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) ); \
} \
}
taskRESET_READY_PRIORITY() 函数的妙处在于清除优先级位图表uxTopReadyPriority 中相应的位时候,会先判断当前优先级链表下是否还有其它任务,如果有则不清零。假设当前实验中,任务 1 会调用 vTaskDelay(),会将自己挂起,只能是将任
务 1从就绪列表删除,不能将任务 1 在优先级位图表 uxTopReadyPriority中对应的位清 0,因为该优先级下还有任务 2,否则任务 2 将得不到执行。
修改代码:
1 修改systick中断服务函数:
这是原来的中断服务函数,更新时基函数不带返回值:
/*
*************************************************************************
* SysTick中断服务函数
*************************************************************************
*/
void xPortSysTickHandler( void )
{
/* 关中断 */
vPortRaiseBASEPRI();
/* 更新系统时基 */
xTaskIncrementTick();
/* 开中断 */
vPortClearBASEPRIFromISR();
}
这是新的中断服务函数,当xTaskIncrementTick()函数返回为真时才进行任务切换。区别在于,原来的 xTaskIncrementTick()是不带返回值的,执行到最后会调用 taskYIELD()执行任务切换。
void xPortSysTickHandler( void )
{
/* 关中断 */
vPortRaiseBASEPRI();
/* 更新系统时基 */
if( xTaskIncrementTick() != pdFALSE )
{
/* 任务切换,即触发PendSV */
//portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
taskYIELD();
}
/* 开中断 */
vPortClearBASEPRIFromISR();
}
2 修改时基切换 xTaskIncrementTick()函数
因为之前的时基函数,每次tick都换切换任务,而现在是当xTaskIncrementTick()函数返回为真时才进行任务切换。
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
/* 如果xConstTickCount溢出,则切换延时列表 */
if( xConstTickCount == ( TickType_t ) 0U )
{
taskSWITCH_DELAYED_LISTS();
}
/* 最近的延时任务延时到期 */
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ;; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
/* 延时列表为空,设置xNextTaskUnblockTime为可能的最大值 */
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
else /* 延时列表不为空 */
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
/* 直到将延时列表中所有延时到期的任务移除才跳出for循环 */
if( xConstTickCount < xItemValue )
{
xNextTaskUnblockTime = xItemValue;
break;
}
/* 将任务从延时列表移除,消除等待状态 */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
/* 将解除等待的任务添加到就绪列表 */
prvAddTaskToReadyList( pxTCB );
/* 表示有任务就绪且就绪任务的优先级比当前优先级高时,需要执行一次任务切换,
即将 xSwitchRequired 的值置为 pdTRUE。而原来,我们是在执行完 xTaskIncrementTick()函数的时候,
不管是否有任务就绪,不管就绪的任务的优先级是否比当前任务优先级高都执行一次任务切换。
如果就绪任务的优先级比当前优先级高,那么执行一次任务切换与加了这段代码实现的功能
是一样的。如果没有任务就绪呢?就不需要执行任务切换,这样与之前的实现方法相比就
省了一次任务切换的时间。虽然说没有更高优先级的任务就绪,执行任务切换的时候还是
会运行原来的任务,但这是以多花一次任务切换的时间为代价的。
*/
// 简而言之,就是延时到期提取的任务优先级提当前运行的还要高,就切换任务
// 否则,只是执行上面的添加就绪列表操作,灯带高优先级任务执行完成再自己切换
#if ( configUSE_PREEMPTION == 1 )
{
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
}
#endif /* configUSE_PREEMPTION */
}
}
}/* xConstTickCount >= xNextTaskUnblockTime */
/* 这部分代码与时间片功能相关
当前优先级列表下不止一个任务时就执行一次任务切换,即将 xSwitchRequired 置为 pdTRUE 即可 */
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) )
> ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
}
#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
/* 任务切换 */
//portYIELD();
}
增加的部分1,主要就是为了避免无意义的任务切换。也就是说,当有任务就绪时,就将任务添加到就绪列表,而只有当就绪列表的任务优先级很高时,才进行任务切换,否则不要去浪费切换任务的时间。
/* 表示有任务就绪且就绪任务的优先级比当前优先级高时,需要执行一次任务切换,
即将 xSwitchRequired 的值置为 pdTRUE。而原来,我们是在执行完 xTaskIncrementTick()函数的时候,不管是否有任务就绪,
不管就绪的任务的优先级是否比当前任务优先级高都执行一次任务切换。
如果就绪任务的优先级比当前优先级高,那么执行一次任务切换与加了这段代码实现的功能
是一样的。如果没有任务就绪呢?就不需要执行任务切换,这样与之前的实现方法相比就
省了一次任务切换的时间。虽然说没有更高优先级的任务就绪,执行任务切换的时候还是
会运行原来的任务,但这是以多花一次任务切换的时间为代价的。*/
// 简而言之,就是延时到期提取的任务优先级提当前运行的还要高,就切换任务
// 否则,只是执行上面的添加就绪列表操作,灯带高优先级任务执行完成再自己切换
#if ( configUSE_PREEMPTION == 1 )
{
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
}
#endif
增加的部分2,时间片
/* 这部分代码与时间片功能相关
当前优先级列表下不止一个任务时就执行一次任务切换,即将 xSwitchRequired 置为 pdTRUE 即可 */
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) )
> ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
}
#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
其实 FreeRTOS 的这种时间片功能不能说是真正意义的时间片,因为它不能随意的设置时间为多少个 tick,而是默认一个 tick,然后默认在每个 tick 中断周期中进行任务切换而已。