在前一小节中,任务操作里面的延时就是直接让 C P U CPU CPU干等着,啥也不干,这样会极大的浪费 C P U CPU CPU的资源。这一小节即将要讲到的阻塞延时就是当任务有延时需要的时候让 C P U CPU CPU不要干等着,而是去执行其它的任务,充分利用 C P U CPU CPU资源,就算此时没有任何任务需要执行那我们就可以执行我们专门建立的一个空闲任务,这个空闲任务可以上什么都不干或者进入低功耗模式来节能,当相应任务的延时到期的时候再转去执行对应的任务,这样的话 C P U CPU CPU的利用率将大大提高。那么我们首先来建立一个空闲任务,这个任务的建立和在前一小节中任务1和任务2的建立基本没有区别,只是这个任务需要插入到就绪列表数组中索引为0的链表中,这是因为空闲任务在系统中的优先级是最低的,只有当前没有任务执行或任务需要延迟的时候采取执行它,就绪列表数组的索引号就代表着挂到该元素对应的链表里面的任务的优先级,索引号越大,优先级越高。空闲任务对应的任务控制块,任务栈以及任务对应的函数的定义和空闲任务的创建如下所示。
TaskHandle_t IdleTask_Handle;
#define IDLETASK_STACK_SIZE 20
StackType_t IdleTaskStack[IDLETASK_STACK_SIZE];
TCB_t IdleTaskTCB;
/*
*****************************************************************************
Idle Task
*****************************************************************************
*/
void IdleTask_Entry( void *p_arg )
{
while(1);
}
IdleTask_Handle = xTaskCreateStatic( (TaskFunction_t)IdleTask_Entry,
(char *)"IdleTask",
(uint32_t)IDLETASK_STACK_SIZE ,
(void *) NULL,
(StackType_t *)IdleTaskStack,
(TCB_t *)&IdleTaskTCB );
vListInsertEnd( &( pxReadyTasksLists[0] ), &( ((TCB_t *)(&IdleTaskTCB))->xStateListItem ) );
这里需要注意的是在
F
r
e
e
R
T
O
S
FreeRTOS
FreeRTOS的源码中空闲任务的创建是在源文件
t
a
s
k
s
.
c
tasks.c
tasks.c中的
v
T
a
s
k
S
t
a
r
t
S
c
h
e
d
u
l
e
r
vTaskStartScheduler
vTaskStartScheduler接口里面创建的,而不是像我这里这样,我这里只是为了讲解方便(野火的讲解中空闲任务的创建也是在源文件
t
a
s
k
s
.
c
tasks.c
tasks.c中的
v
T
a
s
k
S
t
a
r
t
S
c
h
e
d
u
l
e
r
vTaskStartScheduler
vTaskStartScheduler接口里面创建的),
F
r
e
e
R
T
O
S
FreeRTOS
FreeRTOS的源码中空闲任务对应的函数名也不是我这里的名字而是
p
r
v
I
d
l
e
T
a
s
k
prvIdleTask
prvIdleTask,不同的配置之下
F
r
e
e
R
T
O
S
FreeRTOS
FreeRTOS的源码中空闲任务的创建接口可能也不是这里的静态创建接口
x
T
a
s
k
C
r
e
a
t
e
S
t
a
t
i
c
xTaskCreateStatic
xTaskCreateStatic(使用用户定义的
R
A
M
RAM
RAM空间)而是使用动态创建接口
x
T
a
s
k
C
r
e
a
t
e
xTaskCreate
xTaskCreate(使用动态分配的
R
A
M
RAM
RAM空间)。
空闲任务创建完毕之后,下一步我们要来看一下阻塞延时如何实现。为了实现阻塞延时这里的任务控制块里面加了一个元素
x
T
i
c
k
s
T
o
D
e
l
a
y
xTicksToDelay
xTicksToDelay,但是要注意的是在
F
r
e
e
R
T
O
S
FreeRTOS
FreeRTOS的源码中任务控制块中是没有这个元素的,这里只不过是为了讲解的便利,在
F
r
e
e
R
T
O
S
FreeRTOS
FreeRTOS的源码中阻塞延时的实现应该用到了延时列表(后面的章节会讲到)。这个元素的值表示当前任务需要延时的时间单位的个数,时间单位是配置的
S
y
s
T
i
c
k
SysTick
SysTick的周期,只有当任务对应的任务控制块里面的元素
x
T
i
c
k
s
T
o
D
e
l
a
y
xTicksToDelay
xTicksToDelay的值为0的时候才会再次恢复执行。下面我们再来看一下阻塞延时对应的接口
v
T
a
s
k
D
e
l
a
y
vTaskDelay
vTaskDelay,如下所示。这个接口比较简单,只是简单的将当前正在执行的任务的任务控制块的元素
x
T
i
c
k
s
T
o
D
e
l
a
y
xTicksToDelay
xTicksToDelay的值赋值为需要延时的
S
y
s
T
i
c
k
SysTick
SysTick的周期的个数,然后进行一次任务切换因为这里已经打算对当前任务进行延时了,那肯定要切换到其它需要运行的任务或空闲任务。相应的这里相对于前一小节任务1和任务2对应的函数也有所改变,任务对应的函数中使用接口
v
T
a
s
k
D
e
l
a
y
vTaskDelay
vTaskDelay进行延时操作且不再在里面调用任务切换接口。
void vTaskDelay( const TickType_t xTicksToDelay )
{
TCB_t *pxTCB = NULL;
pxTCB = pxCurrentTCB;
pxTCB->xTicksToDelay = xTicksToDelay;
taskYIELD();
}
/*
*****************************************************************************
Task 1
*****************************************************************************
*/
void Task1_Entry( void *p_arg )
{
while(1)
{
flag1 = 1;
vTaskDelay(2);
flag1 = 0;
vTaskDelay(2);
}
}
/*
*****************************************************************************
Task 2
*****************************************************************************
*/
void Task2_Entry( void *p_arg )
{
while(1)
{
flag2 = 1;
vTaskDelay(2);
flag2 = 0;
vTaskDelay(2);
}
}
有了前面的逻辑在前一小节中的保存上文之后寻找最高优先级的接口 v T a s k S w i t c h C o n t e x t vTaskSwitchContext vTaskSwitchContext(相对于源码中的定义,这里做了大量精简,在 t a s k s . c tasks.c tasks.c这个文件中定义。虽然这里也没有实现优先级)的逻辑也得变一下。
- 如果当前任务是空闲任务,则先看任务1需不需要再延时,如果不需要就切换到任务1执行,如果需要就看任务2需不需要再延时,如果不需要就切换到任务2执行,如果需要就继续执行空闲任务。
- 如果当前任务是任务1,则先看任务2需不需要再延时,如果不需要就切换到任务2执行,如果需要就看任务1需不需要再延时,如果需要就切换到空闲任务,如果不需要就继续执行任务1。
- 如果当前任务是任务2,则先看任务1需不需要再延时,如果不需要就切换到任务1执行,如果需要就看任务2需不需要再延时,如果需要就切换到空闲任务,如果不需要就继续执行任务2。
void vTaskSwitchContext( void )
{
if( pxCurrentTCB == &IdleTaskTCB )
{
if(Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task1TCB;
}
else if(Task2TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task2TCB;
}
else
{
return;
}
}
else
{
if(pxCurrentTCB == &Task1TCB)
{
if(Task2TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task2TCB;
}
else if(pxCurrentTCB->xTicksToDelay != 0)
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
return;
}
}
else if(pxCurrentTCB == &Task2TCB)
{
if(Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task1TCB;
}
else if(pxCurrentTCB->xTicksToDelay != 0)
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
return;
}
}
}
}
前面我们提到任务控制块里面加了一个元素 x T i c k s T o D e l a y xTicksToDelay xTicksToDelay,它表示当前任务需要延时的时间单位的个数,时间单位是配置的 S y s T i c k SysTick SysTick的周期,只有当任务对应的任务控制块里面的元素 x T i c k s T o D e l a y xTicksToDelay xTicksToDelay的值为0的时候表示延时结束。既然延时的单位是 S y s T i c k SysTick SysTick的周期,那么每一个任务的任务控制块里面的元素 x T i c k s T o D e l a y xTicksToDelay xTicksToDelay只要不为0,在每进一次 S y s T i c k SysTick SysTick中断函数的时候都应该进行减一的操作。我们先看 S y s T i c k SysTick SysTick的初始化的接口 v P o r t S e t u p T i m e r I n t e r r u p t vPortSetupTimerInterrupt vPortSetupTimerInterrupt,这个接口在 F r e e R T O S FreeRTOS FreeRTOS源码中的 p o r t . c port.c port.c文件中定义。这个接口没啥好说的,只是使能了 S y s T i c k SysTick SysTick中断且中断的周期为10ms。我们前面说过这一部分的代码例子是不依赖于任何的硬件环境的,所以具体的系统时钟为多少,我们直接看工程里面的 s y s t e m A R M C M 3. c system_ARMCM3.c systemARMCM3.c文件里面的宏 S Y S T E M C L O C K SYSTEM_CLOCK SYSTEMCLOCK定义的值( 25000000 H Z 25000000HZ 25000000HZ),且按照这个值去配置, S y s T i c k SysTick SysTick直接使用系统时钟而没有使用系统时钟除8的时钟。 S y s T i c k SysTick SysTick初始化的接口在 x P o r t S t a r t S c h e d u l e r xPortStartScheduler xPortStartScheduler接口中调用。
/*
* Setup the SysTick timer to generate the tick interrupts at the required
* frequency.
*/
#if ( configOVERRIDE_DEFAULT_TICK_CONFIGURATION == 0 )
__weak void vPortSetupTimerInterrupt( void )
{
/* Calculate the constants required to configure the tick interrupt. */
#if ( configUSE_TICKLESS_IDLE == 1 )
{
ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
}
#endif /* configUSE_TICKLESS_IDLE */
/* Stop and clear the SysTick. */
portNVIC_SYSTICK_CTRL_REG = 0UL;
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
/* Configure SysTick to interrupt at the requested rate. */
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT_CONFIG | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}
#endif /* configOVERRIDE_DEFAULT_TICK_CONFIGURATION */
/*-----------------------------------------------------------*/
下面来看一下 S y s T i c k SysTick SysTick的中断函数 x P o r t S y s T i c k H a n d l e r xPortSysTickHandler xPortSysTickHandler,该函数在 F r e e R T O S FreeRTOS FreeRTOS源码中的 p o r t . c port.c port.c文件中定义(这里有修改)。 S y s T i c k SysTick SysTick的中断函数调用接口 x T a s k I n c r e m e n t T i c k xTaskIncrementTick xTaskIncrementTick对处于就绪列表中的每一个任务,如果其任务控制块的 x T i c k s T o D e l a y xTicksToDelay xTicksToDelay元素的值不为0则做减一操作,调用之后会进行触发任务的切换操作,因为此时某些任务的延时已经结束或者某些任务开始了延时需要重新恢复执行。接口 x T a s k I n c r e m e n t T i c k xTaskIncrementTick xTaskIncrementTick在 F r e e R T O S FreeRTOS FreeRTOS源码中的 t s a k s . c tsaks.c tsaks.c文件中定义(这里有大量精简修改)。这里 S y s T i c k SysTick SysTick的中断函数在调用接口 x T a s k I n c r e m e n t T i c k xTaskIncrementTick xTaskIncrementTick的时候将 x T a s k I n c r e m e n t T i c k xTaskIncrementTick xTaskIncrementTick当做临界段处理,通过调用 v P o r t R a i s e B A S E P R I vPortRaiseBASEPRI vPortRaiseBASEPRI和 v P o r t C l e a r B A S E P R I F r o m I S R vPortClearBASEPRIFromISR vPortClearBASEPRIFromISR来进出和退出临界段。
void xPortSysTickHandler( void )
{
/* The SysTick runs at the lowest interrupt priority, so when this interrupt
* executes all interrupts must be unmasked. There is therefore no need to
* save and then restore the interrupt mask value as its value is already
* known - therefore the slightly faster vPortRaiseBASEPRI() function is used
* in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
vPortRaiseBASEPRI();
{
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
/* A context switch is required. Context switching is performed in
* the PendSV interrupt. Pend the PendSV interrupt. */
//portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
portYIELD();
}
}
vPortClearBASEPRIFromISR();
}
BaseType_t xTaskIncrementTick( void )
{
TCB_t *pxTCB = NULL;
BaseType_t i = 0;
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
for(i=0; i<configMAX_PRIORITIES; i++)
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) );
if(pxTCB->xTicksToDelay > 0)
{
pxTCB->xTicksToDelay --;
}
}
return pdTRUE;
}
好了为了实现空闲任务和阻塞延时,在前一小节上需要做的修改都已经介绍完,下面我们来实际跑跑代码看看,和上一小节一样这里还是使用 K E I L − M D K KEIL−MDK KEIL−MDK自带的 L o g i c A n a l y z e r Logic\quad Analyzer LogicAnalyzer来看结果,结果如图1所示。这里就好像是两个任务在同时执行一样。这是因为这里的延时没有让 C P U CPU CPU干等待而是转而去执行需要执行的其它任务或空闲任务。相关工程代码在这里。