1.任务的基本概念
从系统的角度来看,任务是竞争系统资源的最小运行单元。FreeRTOS是一个支持多任务的操作系统。在FreeRTOS中,任务可以使用或等待CPU、使用内存空间等系统资源,并独立于其他运行,任何数量的任务可以共享同一个优先级,如果宏configUSE_TIME_SLICING定义为1,处于就绪态的多个相同优先级任务将会以时间片切换的方式共享处理器。如果configUSE_TIME_SLICING定义为0,处于就绪态的多个相同优先级任务不会在时间tick中断执行后进行切换。
FreeRTOS的任务可以认为是一系列独立任务的集合。每个任务在自己的环境中运行。在任何时刻,只有一个任务得到运行,FreeRTOS调度器决定运行那个任务。调度器会不停的启动、停止每一个任务,宏观上看去所有任务都在同时执行。对于任务本身,不需要堆调度器的活动进行了解,在任务切入切出时保存上下文环境(寄存器值、堆栈内容)是调度器主要的职责。为了实现这点,每个FreeRTOS任务都要有自己的栈空间,或是静态分配,在创建任务前认为分配空间,或是动态分配,由FreeRTOS在创建任务的时候自动分配。在任务切出时,它的执行环境会被保存在该任务的栈空间中,当任务再次运行时,就能从堆栈中正确的恢复上次的运行环境,任务越多,需要的堆栈空间就越大,一个系统能运行多少个任务,理论上时无限个,而实际上取决于系统的可用SRAM。
FreeRTOS中可以给用户提供多个任务单独享有的堆栈空间,系统可以决定任务的状态,决定任务是否可以运行,同时还能运用内核的IPC通信资源,实现任务之间的通信,帮助用户管理业务程序流程。
FreeRTOS中的任务是抢占式任务调度机制,高优先级任务可以打断低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度。同时FreeRTOS也支持时间片轮转调度方式,只不过时间片的调度是不允许抢占任务的CPU使用权。
任务通常会运行在一个死循环中,也不会退出,如果一个任务不再需要,可以调用FreeRTOS中的任务删除API函数接口显式将其删除。
2.任务调度器的基本概念
FreeRTOS中提供的任务调度器是基于优先级的全抢占式调度:在系统中除了中断处理函数、调度器上锁部分代码和禁止中断的代码是不可以抢占的之外,系统的其他部分都是可以抢占的。系统理论上可以支持无数个优先级0~N,0为最低优先级,分配给空闲任务使用。如果使能了configUSE_PORT_OPTIMISED_TASK_SELECTION ,一般可以强制限定最大可用优先级数目为32。在系统资源比较紧张的环境下,可选择只支持8个。在系统中,当有比当前任务优先级更高的任务就绪时,当前任务将立刻退出,高优先级任务抢占处理器运行,运行完后,在切换到之前任务。
FreeRTOS内核中采用两种方法寻找最高优先级任务,第一张是通用方法,在就绪链表中查找从高优先级往低查找uxTopPriority。在创建任务的时候,我们都会给任务分配一个优先级,而创建的时候已经将优先级进行排序,查找到的第一个uxTopPriority就是我们需要的任务。第二种方法就是,利用计算前导零指令CLZ,直接在uxTopReadyPriority 这个32为变量得出uxTopPriority,这样就知道哪一个优先级任务能够运行,这种调度算法比普通方法更快捷。
FreeRTOS内核中允许创建相同优先级任务。相同优先级的任务采用时间片轮转方式进行调度(分时调度),时间片轮转调度仅在当前系统中无更高优先级就绪任务存在的情况下才有效。为保证系统的实时性,系统尽最大可能地保证高优先级任务运行。任务调度的原则是一旦任务状态发生了改变,并且当前运行的任务优先级小于优先级队列组中任务最高优先级时,立刻进行任务切换。(除非当前系统处于中断处理程序或者禁止任务切换的状态)
3.任务状态迁移
FreeRTOS系统中每一个任务都有多种运行状态,一共有四种状态:运行态、阻塞态、挂起态、就绪态。任务在被创建的时候未使用阻塞状态和挂起状态。它们只是在就绪状态或者运行状态。如图下所示:
3.1 the Blocked State
阻塞(Blocked):等待时间的任务被称为“Blocked ”状态,这是“Not Running”状态的一个子状态。任务可以通过等待两种不同类型的时间来进入阻塞状态:
- 时域时间。相对延迟时间或者一个绝对的延迟时间。一个任务可以通过等待10ms来进入阻塞状态。举个例子,调用vTaskDelay()函数,来进入阻塞状态。
vTaskDelay(20);/* 延时20个tick */
- 同步事件,该事件来源于其他任务或者中断。,一个任务可能进入阻塞状态来等待队列中的数据到来。同步事件涵盖了各种各样的事件类型。FreeRTOS 队列、二进制信号量、计数信号量、互斥量、循环互斥量、事件组、任务通知都可以用来创建同步事件。举个例子,在队列接收函数里,一直等待队列数据。如果没有数据到来,该任务会一直阻塞在接收队列数据这里。
xReturn = xQueueReceive( Test_Queue, /* 消息队列的句柄 */
&r_queue, /* 发送的消息内容 */
portMAX_DELAY); /* 等待时间 一直等 */
3.2 the Suspended State
‘Suspended’也是‘Not Running‘的一个子状态,任务在挂起的时候是无法被调度器利用的。任务进入挂起状态需要调用vTaskSuspend()函数或者vTaskSuspendAll()函数,任务退出挂起状态,需要调用vTaskResume()或者xTaskResumeFromISR() 函数,大多数应用不会用到挂起状态。
3.3 the Ready State
任务在不运行状态,但是又不处于阻塞状态和挂起状态被称作就绪状态。任务可能会运行,准备去运行,但是现在不在运行状态。
4.常用的任务函数讲解
4.1 任务挂起函数
4.1.1 vTaskSuspend()
挂起指定任务。不管该任务具有什么优先级,被挂起的任务不会得到CPU的使用权。
任务可以通过调用 vTaskSuspend()函数可以将处于任何状态的任务挂起,被挂起的任务得到CPU的使用权,也不会参与调度,它相对调度器是不可见的,除非它从挂起状态解除。任务挂起是我们常用的一个函数。下面是其源码
#if ( INCLUDE_vTaskSuspend == 1 )
void vTaskSuspend( TaskHandle_t xTaskToSuspend )
{
TCB_t *pxTCB;
/*进入临界段,禁止中断*/
taskENTER_CRITICAL();
{
/* 如果在此次传递NULL,那么它正在挂起的任务是正在运行的任务*/
pxTCB = prvGetTCBFromHandle( xTaskToSuspend );
traceTASK_SUSPEND( pxTCB );
/* 从就绪/阻塞列表中删除任务,并放到挂起列表中,如果就绪/阻塞列表中删除任务后,没有任务后,重新复位任务的优先级,
找到最高优先级任务 */
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
taskRESET_READY_PRIORITY( pxTCB->uxPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 如果任务在等待事件中,也从等待事件列表中 */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/*将任务添加到挂起列表中*/
vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) );
}
/*退出临界段,开启中断*/
taskEXIT_CRITICAL();
if( xSchedulerRunning != pdFALSE )
{
/* 重置下一个任务的解除阻塞事件.重新计算一下多长时间执行下一个任务。如果下个任务的解锁,是刚好被挂起的任务。那么
prvResetNextTaskUnblockTime函数里的NextTaskUnblockTime不正确,需要重新从延时列表中获取。*/
taskENTER_CRITICAL();
{
prvResetNextTaskUnblockTime();
}
taskEXIT_CRITICAL();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( pxTCB == pxCurrentTCB )
{
if( xSchedulerRunning != pdFALSE )
{
/* 当前的任务已经被挂起. */
configASSERT( uxSchedulerSuspended == 0 );
/*调度器正在运行,立即切换到下一个任务*/
portYIELD_WITHIN_API();
}
else
{
/* 如果调度器没有在运行,而当前任务已经被挂起,必须调整pxCurrentTCB 指向其他任务。判断是否系统中所有任务都被挂起
如果是,则将当前任务控制块指向空,直到新任务创建。实际上不会出现这种情况,因为空闲任务是不允许被挂起和阻塞的 .
如果不是,切换到其他任务*/
if( listCURRENT_LIST_LENGTH( &xSuspendedTaskList ) == uxCurrentNumberOfTasks )
{
pxCurrentTCB = NULL;
}
else
{
vTaskSwitchContext();
}
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* INCLUDE_vTaskSuspend */
任务可以调用vTaskSuspend()这个函数来挂起任务自身,但是在挂起自身的时候会进行一次任务上下文切换,需要挂起自身就将vTaskSuspend(NULL),就将传进来的参数设置为NULL。无论是什么任务都可以被挂起,只要调用**vTaskSuspend()**函数,就会挂起其他任务或者自身任务。
任务的挂起和恢复函数在很多时候都是有用的,比如我们想暂停某个任务运行一段时间,但是我们又需要其恢复的时候继续工作,我们无法删除任务,删除任务后,任何的所有信息是不可能恢复的,里面的资源被系统释放掉。挂起任务就不会,调用挂起任务函数,仅仅只是将任务进入挂起态,其内部资源都会保留下来,同时不参与系统的任务调度,当调用恢复函数的时候,整个任务立即从挂起态进入就绪态,并且参与任务的调度,如果该任务的优先级是当前就绪态优先级最高的任务,那么立即会按照挂起前的任务状态继续执行该任务,从而达到我们需要的效果。注意,是继续执行,挂起任务之前是什么状态,恢复后继续执行。
4.1.2 vTaskSuspendAll()
这个函数是将所有任务都挂起,将调度器锁定,挂起所有任务也就是挂起任务调度器。调度器被挂起后不能进行上下文的切换。如果中断需要进行上下文切换,这个任务将会被挂起,在调度器恢复后才能执行任务。调取器恢复可以调用**xTaskResumeAll()函数。调用了多少次 vTaskSuspendAll(),就要调用xTaskResumeAll()**函数恢复多少次。源码如下:
void vTaskSuspendAll( void )
{
/* A critical section is not required as the variable is of type
* BaseType_t. Please read Richard Barry's reply in the following link to a
* post in the FreeRTOS support forum before reporting this as a bug! -
* https://goo.gl/wu4acr */
/* portSOFTWARE_BARRIER() is only implemented for emulated/simulated ports that
* do not otherwise exhibit real time behaviour. */
portSOFTWARE_BARRIER();
/* The scheduler is suspended if uxSchedulerSuspended is non-zero. An increment
* is used to allow calls to vTaskSuspendAll() to nest. */
++uxSchedulerSuspended;
/* Enforces ordering for ports and optimised compilers that may otherwise place
* the above increment elsewhere. */
portMEMORY_BARRIER();
}
4.2 任务恢复函数
4.2.1 vTaskResume()
任务恢复函数就是将挂起的任务重新进入就绪状态,恢复的任务会保留挂起前的状态信息,在恢复的时候根据挂起时的状态继续运行。如果被恢复任务在所有就绪任务中,处于最高优先级列表中的第一位,那么系统将会进行上下文的切换。源码如下:
#if ( INCLUDE_vTaskSuspend == 1 )
void vTaskResume( TaskHandle_t xTaskToResume )
{
TCB_t * const pxTCB = xTaskToResume;
/* 检查恢复的任务是否被挂起,如果没有挂起,恢复没有任何意义. */
configASSERT( xTaskToResume );
/* 这个参数不能空,也不能指向当前任务控制块,当前任务正在被运行,不需要恢复。 */
if( ( pxTCB != pxCurrentTCB ) && ( pxTCB != NULL ) )
{
/*进入临界区*/
taskENTER_CRITICAL();
{
if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE )
{
traceTASK_RESUME( pxTCB );
/* 由于处于临界区,即使任务被挂起,我们也可以将任务从挂起列表中删除 */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
/*将任务添加到就绪列表中*/
prvAddTaskToReadyList( pxTCB );
/* 如果刚刚恢复的任务的优先级比正在执行的任务优先级高. */
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
/* 此函数不会导致刚恢复的任务开始执行,但是会进行一次任务切换*/
taskYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/*退出临界区*/
taskEXIT_CRITICAL();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* INCLUDE_vTaskSuspend */
**vTaskResume()函数用于恢复挂起的任务。无论任务在挂起的时候调用过多少次vTaskSuspend()**函数,也只需要调用一次 **vTaskResume()**函数就将任务恢复。无论调用多少次的 **vTaskResume()**函数,也只在任务时挂起态的时候进行恢复。
4.2.2 xTaskResumeFromISR()
xTaskResumeFromISR()函数与vTaskResume()函数一样都是用于恢复被挂起的任务。区别就是xTaskResumeFromISR()函数专门用于中断服务程序中。要 想 使 用 该 函 数 必 须 在 FreeRTOSConfig.h 中 把 INCLUDE_vTaskSuspend 和INCLUDE_vTaskResumeFromISR 都定义为 1 才有效。任务还没有处于挂起态的时候,调用
xTaskResumeFromISR()函数是没有任何意义的。源码如下:
#if ( ( INCLUDE_xTaskResumeFromISR == 1 ) && ( INCLUDE_vTaskSuspend == 1 ) )
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume )
{
BaseType_t xYieldRequired = pdFALSE;
TCB_t * const pxTCB = xTaskToResume;
UBaseType_t uxSavedInterruptStatus;
configASSERT( xTaskToResume );
/*调用 portSET_INTERRUPT_MASK_FROM_ISR()函数设置 basepri寄存器用于屏蔽系统可管理的中断,防止被处理被其他中断打断,
当 basepri 设置为configMAX_SYSCALL_INTERRUPT_PRIORITY 的时候(该宏在 FreeRTOSConfig.h 中定义,现在配置为 5),会
让系统不响应比该优先级低的中断,而优先级比之更高的中断则不受影响。就是说当这个宏定义配置为 5 的时候,中断优先级数值在
0、 1、 2、 3、 4 的这些中断是不受 FreeRTOS 管理的,不可被屏蔽,而中断优先级在 5 到 15 的中断是受到系统管理,可用被
屏蔽的。*/
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE )
{
traceTASK_RESUME_FROM_ISR( pxTCB );
/* 检查就绪列表是否可以访问,检查调度器是否被挂起. */
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
/* 如果刚刚恢复的任务优先级比当前任务的优先级高,则进行一次任务切换. */
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xYieldRequired = pdTRUE;
/* 如果用户没有使用返回值从portYIELD_FROM_ISR函数来实现上下文切换,标记现在在挂起 */
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/*任务从挂起列表中移除*/
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
/*任务添加到就绪列表中*/
prvAddTaskToReadyList( pxTCB );
}
else
{
/* 延时或者就绪列比奥无法被访问因此任务处于调度器被挂起,将任务添加到待处理的就绪列表中,等待调度器被恢复后
进行任务的处理 */
vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/*调用 portCLEAR_INTERRUPT_MASK_FROM_ISR()函数清除basepri 的设置,恢复屏蔽的中断*/
portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
/*返回 xYieldRequired 结果,在外部选择是否进行任务切换。*/
return xYieldRequired;
}
#endif /* ( ( INCLUDE_xTaskResumeFromISR == 1 ) && ( INCLUDE_vTaskSuspend == 1 ) ) */
使用 xTaskResumeFromISR()的时候需要注意:
- 当函数的返回值为 pdTRUE 时: 恢复运行的任务的优先级等于或高于正在运行的任 务 , 表 明 在 中 断 服 务 函 数 退 出 后 必 须 进 行 一 次 上 下 文 切 换 , 使 用**portYIELD_FROM_ISR()**进行上下文切换。当函数的返回值为 pdFALSE 时: 恢复运行的任务的优先级低于当前正在运行的任务,表明在中断服务函数退出后不需要进行上下文切换。
- xTaskResumeFromISR() 被认为是一个危险的函数,因为它的调用并非是固定的,中断随时来临。所以xTaskResumeFromISR()不能用于任务和中断间的同步,如果中断恰巧在任务被挂起之前到达,会导致一次中断丢失。(任务没有挂起,调用xTaskResumeFromISR没有意义的,只能等下一次中断)。这种情况下,可以使用信号量或者任务通知来同步就可以避免这种情况发生。
4.2.3 xTaskResumeAll()
调用了 **vTaskSuspendAll()**函数将调度器挂起,想要恢复调度器的时候我们就需要调用 **xTaskResumeAll()**函数,源码如下:
BaseType_t xTaskResumeAll( void )
{
TCB_t * pxTCB = NULL;
BaseType_t xAlreadyYielded = pdFALSE;
/* 如果这个uxSchedulerSuspended 值为0,表示所有任务没有挂起,恢复所有任务没有意义. */
configASSERT( uxSchedulerSuspended );
/* 进入临界区. */
taskENTER_CRITICAL();
{
/*uxSchedulerSuspended变量减一 */
--uxSchedulerSuspended;
/*如果调度器没有挂起,可以将待处理的就绪任务从待处理就绪列表移动到适当的列表中*/
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U )
{
/*当待处理的就绪列表xPendingReadyList 不是空的 */
while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE )
{
/*获取当前列表的当前节点的下一个节点*/
pxTCB = listGET_OWNER_OF_HEAD_ENTRY( ( &xPendingReadyList ) );
/*从待处理的就绪列表中移除*/
listREMOVE_ITEM( &( pxTCB->xEventListItem ) );
portMEMORY_BARRIER();
listREMOVE_ITEM( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
/* 如果移动的任务优先级高于或等于当前任务的优先级,需要进行一次任务切换 */
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
if( pxTCB != NULL )
{
/*如果调度器在挂起的时候任务被解除阻塞,这可能会阻止重新计算下一个解除阻塞时间。调用prvResetNextTaskUnblockTime()
函数将从延时列表中获取下一个要解除阻塞的任务 */
prvResetNextTaskUnblockTime();
}
/* 如果调度器被挂起的时候产生了滴答计数器,它们必须被处理。这个确保滴答计数器没有滑动,
也保证任何延时任务在正确的时间恢复*/
{
TickType_t xPendedCounts = xPendedTicks; /* Non-volatile copy. */
if( xPendedCounts > ( TickType_t ) 0U )
{
do
{
/*调用 xTaskIncrementTick()函数查找是否有待进行切换的任务,如果有则应该进行任务切换*/
if( xTaskIncrementTick() != pdFALSE )
{
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
--xPendedCounts;
} while( xPendedCounts > ( TickType_t ) 0U );
xPendedTicks = 0;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/*如果需要任务切换,调用taskYIELD_IF_USING_PREEMPTION()函数进行一次任务切换*/
if( xYieldPending != pdFALSE )
{
#if ( configUSE_PREEMPTION != 0 )
{
xAlreadyYielded = pdTRUE;
}
#endif
taskYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/*退出临界区*/
taskEXIT_CRITICAL();
return xAlreadyYielded;
}
4.3 任务删除函数vTaskDelete()
vTaskDelete()用于删除一个任务。当一个任务删除另外一个任务时, 形参为要删除任务创建时返回的任务句柄,如果是删除自身, 则形参为 NULL。 要想使用该函数必须在FreeRTOSConfig.h 中把 INCLUDE_vTaskDelete 定义为 1,删除的任务将从所有就绪,阻塞,挂起和事件列表中删除,任务删除函数vTaskDelete()源码如下:
#if ( INCLUDE_vTaskDelete == 1 )
void vTaskDelete( TaskHandle_t xTaskToDelete )
{
TCB_t * pxTCB;
/*进入临界区*/
taskENTER_CRITICAL();
{
/* 获取任务控制块,如果参数为NULL则删除任务自身 */
pxTCB = prvGetTCBFromHandle( xTaskToDelete );
/* 将任务从延时或者就绪列表中移除. */
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
/*清楚任务的就绪优先级变量标志位*/
taskRESET_READY_PRIORITY( pxTCB->uxPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 如果当前任务在等待事件,将任务从事件列表中移除 */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* Increment the uxTaskNumber also so kernel aware debuggers can
* detect that the task lists need re-generating. This is done before
* portPRE_TASK_DELETE_HOOK() as in the Windows port that macro will
* not return. */
uxTaskNumber++;
if( pxTCB == pxCurrentTCB )
{
/* 任务正在删除自己。这不能在任务本身完成,因为需要上下文切换到另一个任务。将任务放在结束列表中。
空闲任务会检查结束列表并释放掉删除的任务控制块和已删除任务的堆栈的任何内存。 */
vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) );
/* 增加uxDeletedTasksWaitingCleanUp变量,记录有多少个任务需要释放内存,以便空闲任务知道有一个已
删除的任务,然后进行内存释放。空闲任务会检查结束列表xTasksWaitingTermination */
++uxDeletedTasksWaitingCleanUp;
/* Call the delete hook before portPRE_TASK_DELETE_HOOK() as
* portPRE_TASK_DELETE_HOOK() does not return in the Win32 port. */
traceTASK_DELETE( pxTCB );
/*任务删除钩子函数. */
portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending );
}
else
{
--uxCurrentNumberOfTasks;
traceTASK_DELETE( pxTCB );
/* 重置下一个任务的解除阻塞事件.重新计算一下还需要多长事件执行下一个任务,如果下一个任务解锁,
刚好是被删除的任务,这是不正确的。因为删除的任务对调度器是不可见的,调度器无法对删除的任务进
行调度,所以重新从延时列表中获取下一个阻塞的任务。它是从延时列表的头部来获取的任务 TCB,延时
列表是按延时时间排序的*/
prvResetNextTaskUnblockTime();
}
}
/*退出临界区*/
taskEXIT_CRITICAL();
/* 如果任务不是删除它本身,从外部临界区调用prvDeleteTCB,删除任务控制块。如果任务是删除它本身,
prvDeleteTCB从空闲任务中prvCheckTasksWaitingTermination函数调用prvDeleteTCB,删除任务控制块。 */
if( pxTCB != pxCurrentTCB )
{
prvDeleteTCB( pxTCB );
}
/* 如果删除的任务是当前的任务,则需要进行下一次上下文切换. */
if( xSchedulerRunning != pdFALSE )
{
if( pxTCB == pxCurrentTCB )
{
configASSERT( uxSchedulerSuspended == 0 );
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
#endif /* INCLUDE_vTaskDelete */
删除任务,只会自动释放内核本身分配给任务的内存。应用程序分配给任务的内存或任何其他资源必须是删除任务时由应用程序显式释放。在某个任务中申请了一大块内存,但是没释放就把任务删除,这块内存在任务删除之后不会自动释放的,所有我们应该在删除任务之前就把任务中的这些资源释放掉,然后再进行删除,否则很容易造成内存泄漏。
4.4 任务延时函数
4.4.1 vTaskDelay()
void vTaskDelay( const TickType_t xTicksToDelay )
vTaskDelay()函数在我们任务中用的非常多,每个任务都是死循环,并且必须要有阻塞的情况,否则低优先级的任务就无法执行。使用vTaskDelay()函数之前,必须在FreeRTOSConfig.h中把INCLUDE_vTaskDelay定义为1来使能。
vTaskDelay()用于阻塞延时,调用该函数后,任务将进入阻塞状态,进入阻塞状态的任务将让出CPU资源。延时的时长由xTicksToDelay 决定,单位是系统节拍周期。比如系统节拍周期是1ms,调用vTaskDelay(1)的延时时间为1ms.系统节拍周期由configTICK_RATE_HZ定义设置,设置为1000,系统节拍周期为1ms;设置为100,系统节拍周期为10ms,依次类推。
vTaskDelay()延时是相对性的延时,它指定的延时时间是从调用vTaskDelay()函数后开始计算的,经过指定的时间后延时结束。比如vTaskDelay(100),从调用vTaskDelay()结束后,任务进入阻塞状态,经过100个系统时钟节拍周期后,任务解除阻塞。vTaskDelay()不适用与周期性执行任务的场合。此外,其他任务和中断活动会影响到vTaskDelay()的调用(比如调用前高优先级任务抢占了当前任务,比如在解除阻塞后,仍在执行高优先级任务)。vTaskDelay()源码如下:
#if ( INCLUDE_vTaskDelay == 1 )
void vTaskDelay( const TickType_t xTicksToDelay )
{
BaseType_t xAlreadyYielded = pdFALSE;
/* 延时要大于0,否则强制执行切换任务. */
if( xTicksToDelay > ( TickType_t ) 0U )
{
configASSERT( uxSchedulerSuspended == 0 );
/*挂起任务调度器*/
vTaskSuspendAll();
{
traceTASK_DELAY();
/*将当前任务当道延时列表里. */
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
}
/*恢复任务调度器*/
xAlreadyYielded = xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 强制切换任务. */
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* INCLUDE_vTaskDelay */
4.4.2 vTaskDelayUntil()
在FreeRTOS中,除了相对延时函数还有绝对延时函数vTaskDelayUntil(),这个绝对延时常用于较精确的周期运行任务,比如我有一个任务,希望它以固定的频率定期执行,而不受外部的影响,任务从上一次运行开始到下一次运行开始的时间间隔是绝对的,而不是相对的。
vTaskDelayUntil() 与 vTaskDelay () 一 样都 是 用 来 实 现 任 务 的 周 期 性 延 时 。 但vTaskDelay ()的延时是相对的,是不确定的,它的延时是等 vTaskDelay ()调用完毕后开始计算的。 并且 vTaskDelay ()延时的时间到了之后,如果有高优先级的任务或者中断正在执行,被延时阻塞的任务并不会马上解除阻塞,所有每次执行任务的周期并不完全确定。而vTaskDelayUntil()延时是绝对的,适用于周期性执行的任务。 当(*pxPreviousWakeTime +xTimeIncrement)时间到达后, vTaskDelayUntil()函数立刻返回,如果任务是最高优先级的,
那么任务会立马解除阻塞, 所以说 vTaskDelayUntil()函数的延时是绝对性的.源码如下:
#if ( INCLUDE_xTaskDelayUntil == 1 )
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement )
{
TickType_t xTimeToWake;
BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;
configASSERT( pxPreviousWakeTime );
configASSERT( ( xTimeIncrement > 0U ) );
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll();
{
/* 开始获取进行延时的时间点. */
const TickType_t xConstTickCount = xTickCount;
/* 计算延时到达时间,也就是唤醒任务的时间.pxPreviousWakeTime 是上次唤醒时间 */
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
/*如果唤醒时间大于当前时间,说明节拍计数器溢出了*/
if( xConstTickCount < *pxPreviousWakeTime )
{
/* 如果唤醒的时间小于上次唤醒时间,并且唤醒时间大于开始计时时间,相当于没有溢出,保证周期性延时时间大于任务主体代码
执行时间 */
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
/* 如果只是唤醒时间溢出或者没溢出,保证延时时间大于任务主体代码的执行时间. */
if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* 更新上一次唤醒时间. */
*pxPreviousWakeTime = xTimeToWake;
if( xShouldDelay != pdFALSE )
{
traceTASK_DELAY_UNTIL( xTimeToWake );
/* prvAddCurrentTaskToDelayedList 需要的是阻塞时间,不是唤醒时间,所以是下一次唤醒时间减去当前时间 */
prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
xAlreadyYielded = xTaskResumeAll();
/* 强制执行一次上下文切换. */
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
return xShouldDelay;
}
#endif /* INCLUDE_xTaskDelayUntil */
4.5 任务设计要点
在设计嵌入式系统的时候,任务的优先级信息,任务与中断的处理,任务的运行时间、逻辑、状态等都要清楚。在设计的时候,要考虑任务运行的上下文环境、任务的执行时间合理。
FreeRTOS中程序运行的上下文包括:
- 中断服务函数
- 普通任务
- 空闲任务
4.5.1 中断服务函数
中断服务函数是一种需要特别注意的上下文环境,它运行在非任务的执行环境下(一般为芯片的一种特殊运行模式(也称作特权模式)),在这个上下文环境中不能使用挂起当前任务的操作,也不允许调用任何会阻塞运行的API接口。中断服务程序最好保持精简短小,快进快出,一般在中断服务函数中只做标记事件的发生,然后通知任务,让对应的任务去执行相关的处理,因为中断服务函数的优先级高于任何优先级的任务,如果中断处理时间过长,将会导致整个系统的任务无法正常运行。在设计的时候必须考虑中断的频率、中断的处理时间等因素,配合对应中断处理任务的工作。
4.5.2 任务
任务看似没有限制程序执行的因素,似乎所有操作都可以执行。但是作为一个优先级明确的实时系统,如果一个任务中的程序出现死循环(没有阻塞机制的任务循环体),比这个任务优先级低的任务都无法执行,也包括空闲任务。因为死循环的时候,任务不会主动让出CPU,低优先级的任务是不可能得到CPU的使用权的,而高优先级的任务就可以抢占CPU。如果一个任务只有就绪态而无阻塞态,势必会影响到其他低优先级任务的执行。在进行任务设计时,保证任务在不活跃的时候,任务进入阻塞态交出CPU使用权。需要我们明确什么情况下任务进入阻塞态,保证低优先级任务可以正常运行。
4.5.3 空闲任务
空闲任务(idle任务)是FreeRTOS系统中没有其他工作进行时自动进入的系统任务。因为处理器总是需要代码来执行—所以至少要有一个任务处于运行态。FreeRTOS为了保证这一点,在调用**vTaskStartScheduler()**时,调度器会自动创建一个空闲任务。空闲任务时一个非常短小的循环。用户可以通过空闲任务的钩子方式,在空闲任务上钩入自己的功能函数。通常这个空闲任务钩子能够完成一些额外的功能,例如系统运行状态指示,系统省电模式等。除了空闲任务钩子, FreeRTOS 系统还把空闲任务用于一些其他的功能,比
如当系统删除一个任务或一个动态任务运行结束时, 在执行删除任务的时候,并不会释放任务的内存空间,只会将任务添加到结束列表中, 真正的系统资源回收工作在空闲任务完成,空闲任务是唯一一个不允许出现阻塞情况的任务,因为 FreeRTOS 需要保证系统永远都有一个可运行的任务。
&emsp;对于空闲任务钩子挂接的空闲钩子函数,应满足以下条件:
- 永远不会挂起空闲任务
- 不应该陷入死循环,需要留出部分时间用于系统处理系统资源回收
4.5.4 任务的执行时间
任务的执行时间一般是指两个方面,一是任务从开始到结束的时间,二是任务的周期。
在系统设计的时候,我们都需要考虑。例如,对于事件A对于的服务任务Ta,系统要求的实时响应指标是10ms,而Ta的最大运行时间是1ms,那么10ms就是Ta的周期,1ms是任务的运行时间。任务Ta只要在10ms内完成对事件A的响应即可。系统中还存在着以50ms为周期的另一任务Tb,它每次运行的最大时间长度是100us.在这种情况下,即使把任务Tb的优先级抬到比Ta的优先级更高的位置,对系统实时性指标也没啥影响,因为即使在 Ta 的运行过程中, Tb 抢占了 Ta 的资源,等到 Tb 执行完毕,消耗的时间也只不过是 100us,还是在事件 A 规定的响应时间内(10ms), Ta 能够安全完成对事件 A 的响应。但是假如系统中还存在任务 Tc,其运行时间为 20ms,假如将 Tc
的优先级设置比 Ta 更高,那么在 Ta 运行的时候,突然间被 Tc 打断,等到 Tc 执行完毕,那 Ta 已经错过对事件 A(10ms)的响应了,这是不允许的。所以在我们设计的时候,必须考虑任务的时间,一般来说处理时间更短的任务优先级应设置更高一些。
4.6 参考例程
4.6.1 任务挂起与恢复
#include "stm32f4xx.h"
#include "./led/bsp_led.h"
#include "FreeRTOS.h"
#include "task.h"
#include "bsp_key.h"
/* 空闲任务任务堆栈 */
static StackType_t Idle_Task_Stack[configMINIMAL_STACK_SIZE];
/* 定时器任务堆栈 */
static StackType_t Timer_Task_Stack[configTIMER_TASK_STACK_DEPTH];
/* 空闲任务控制块 */
static StaticTask_t Idle_Task_TCB;
/* 定时器任务控制块 */
static StaticTask_t Timer_Task_TCB;
/* LED1 任务堆栈 */
static StackType_t LED1_Task_Stack[128];
/*LED1任务控制块 */
static StaticTask_t LED1_Task_TCB;
/* LED1 任务句柄 */
static TaskHandle_t LED1_Task_Handle;
/* LED2 任务句柄 */
TaskHandle_t LED2_TaskHandle = NULL;
TaskHandle_t KEY_TaskHandle = NULL;
void LED2_Task()
{
while(1)
{
LED2( ON );
vTaskDelay(500);
LED2( OFF );
vTaskDelay(500);
}
}
static void KEY_Task(void* parameter)
{
while(1)
{
if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
{
vTaskSuspend(LED2_TaskHandle);
}
if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
{
vTaskResume(LED2_TaskHandle);
}
vTaskDelay(20);
}
}
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
BaseType_t xReturn = pdPASS;
/* LED 端口初始化 */
LED_GPIO_Config();
Key_GPIO_Config();
xReturn = xTaskCreate( (TaskFunction_t )KEY_Task,
(const char* )"KEY_Task",
(uint32_t )128,
(void* )NULL,
(UBaseType_t )4,
(TaskHandle_t* )&KEY_TaskHandle);
//create led2 task
xReturn = xTaskCreate( LED2_Task,
"LED2_Task",
128,
NULL,
3,
&LED2_TaskHandle);
vTaskStartScheduler();
}
/**
**********************************************************************
* @brief 获取空闲任务的任务堆栈和任务控制块内存
* ppxTimerTaskTCBBuffer : 任务控制块内存
* ppxTimerTaskStackBuffer : 任务堆栈内存
* pulTimerTaskStackSize : 任务堆栈大小
* @author fire
* @version V1.0
* @date 2018-xx-xx
**********************************************************************
*/
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize)
{
*ppxIdleTaskTCBBuffer=&Idle_Task_TCB;/* 任务控制块内存 */
*ppxIdleTaskStackBuffer=Idle_Task_Stack;/* 任务堆栈内存 */
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;/* 任务堆栈大小 */
}
/**
*********************************************************************
* @brief 获取定时器任务的任务堆栈和任务控制块内存
* ppxTimerTaskTCBBuffer : 任务控制块内存
* ppxTimerTaskStackBuffer : 任务堆栈内存
* pulTimerTaskStackSize : 任务堆栈大小
* @author fire
* @version V1.0
* @date 2018-xx-xx
**********************************************************************
*/
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
StackType_t **ppxTimerTaskStackBuffer,
uint32_t *pulTimerTaskStackSize)
{
*ppxTimerTaskTCBBuffer=&Timer_Task_TCB;/* 任务控制块内存 */
*ppxTimerTaskStackBuffer=Timer_Task_Stack;/* 任务堆栈内存 */
*pulTimerTaskStackSize=configTIMER_TASK_STACK_DEPTH;/* 任务堆栈大小 */
}
4.6.2 任务的两种延时
#include "stm32f4xx.h"
#include "./led/bsp_led.h"
#include "FreeRTOS.h"
#include "task.h"
/* 空闲任务任务堆栈 */
static StackType_t Idle_Task_Stack[configMINIMAL_STACK_SIZE];
/* 定时器任务堆栈 */
static StackType_t Timer_Task_Stack[configTIMER_TASK_STACK_DEPTH];
/* 空闲任务控制块 */
static StaticTask_t Idle_Task_TCB;
/* 定时器任务控制块 */
static StaticTask_t Timer_Task_TCB;
/* LED1 任务堆栈 */
static StackType_t LED1_Task_Stack[128];
/*LED1任务控制块 */
static StaticTask_t LED1_Task_TCB;
/* LED1 任务句柄 */
static TaskHandle_t LED1_Task_Handle;
/* LED2 任务句柄 */
TaskHandle_t LED2_TaskHandle = NULL;
void LED1_Task()
{
TickType_t xLastWakeTime;
xLastWakeTime = xTaskGetTickCount();
while(1)
{
LED1( ON );
vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 1000 ) );
LED1( OFF );
vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 1000 ) );
}
}
void LED2_Task()
{
while(1)
{
LED2( ON );
vTaskDelay(500);
LED2( OFF );
vTaskDelay(500);
}
}
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
BaseType_t xReturn = pdPASS;
/* LED 端口初始化 */
LED_GPIO_Config();
//create led1 task
/* 创建LED_Task任务 */
LED1_Task_Handle = xTaskCreateStatic( (TaskFunction_t )LED1_Task,
(const char* )"LED1_Task",
(uint32_t )128,
(void* )NULL,
(UBaseType_t )4,
(StackType_t* )LED1_Task_Stack,
(StaticTask_t* )&LED1_Task_TCB);
//create led2 task
xReturn = xTaskCreate( LED2_Task,
"LED2_Task",
128,
NULL,
4,
&LED2_TaskHandle);
vTaskStartScheduler();
}
/**
**********************************************************************
* @brief 获取空闲任务的任务堆栈和任务控制块内存
* ppxTimerTaskTCBBuffer : 任务控制块内存
* ppxTimerTaskStackBuffer : 任务堆栈内存
* pulTimerTaskStackSize : 任务堆栈大小
* @author fire
* @version V1.0
* @date 2018-xx-xx
**********************************************************************
*/
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize)
{
*ppxIdleTaskTCBBuffer=&Idle_Task_TCB;/* 任务控制块内存 */
*ppxIdleTaskStackBuffer=Idle_Task_Stack;/* 任务堆栈内存 */
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;/* 任务堆栈大小 */
}
/**
*********************************************************************
* @brief 获取定时器任务的任务堆栈和任务控制块内存
* ppxTimerTaskTCBBuffer : 任务控制块内存
* ppxTimerTaskStackBuffer : 任务堆栈内存
* pulTimerTaskStackSize : 任务堆栈大小
* @author fire
* @version V1.0
* @date 2018-xx-xx
**********************************************************************
*/
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
StackType_t **ppxTimerTaskStackBuffer,
uint32_t *pulTimerTaskStackSize)
{
*ppxTimerTaskTCBBuffer=&Timer_Task_TCB;/* 任务控制块内存 */
*ppxTimerTaskStackBuffer=Timer_Task_Stack;/* 任务堆栈内存 */
*pulTimerTaskStackSize=configTIMER_TASK_STACK_DEPTH;/* 任务堆栈大小 */
}
4.6.3 任务的删除
#include "stm32f4xx.h"
#include "./led/bsp_led.h"
#include "FreeRTOS.h"
#include "task.h"
/* 空闲任务任务堆栈 */
static StackType_t Idle_Task_Stack[configMINIMAL_STACK_SIZE];
/* 定时器任务堆栈 */
static StackType_t Timer_Task_Stack[configTIMER_TASK_STACK_DEPTH];
/* 空闲任务控制块 */
static StaticTask_t Idle_Task_TCB;
/* 定时器任务控制块 */
static StaticTask_t Timer_Task_TCB;
/* LED1 任务堆栈 */
static StackType_t LED1_Task_Stack[128];
/*LED1任务控制块 */
static StaticTask_t LED1_Task_TCB;
/* LED1 任务句柄 */
static TaskHandle_t LED1_Task_Handle;
/* LED2 任务句柄 */
TaskHandle_t LED2_TaskHandle = NULL;
void LED1_Task()
{
TickType_t xLastWakeTime;
xLastWakeTime = xTaskGetTickCount();
while(1)
{
vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 10000 ) );
vTaskDelete(LED2_TaskHandle);
}
}
void LED2_Task()
{
while(1)
{
LED2( ON );
vTaskDelay(500);
LED2( OFF );
vTaskDelay(500);
}
}
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
BaseType_t xReturn = pdPASS;
/* LED 端口初始化 */
LED_GPIO_Config();
//create led1 task
/* 创建LED_Task任务 */
LED1_Task_Handle = xTaskCreateStatic( (TaskFunction_t )LED1_Task,
(const char* )"LED1_Task",
(uint32_t )128,
(void* )NULL,
(UBaseType_t )4,
(StackType_t* )LED1_Task_Stack,
(StaticTask_t* )&LED1_Task_TCB);
//create led2 task
xReturn = xTaskCreate( LED2_Task,
"LED2_Task",
128,
NULL,
4,
&LED2_TaskHandle);
vTaskStartScheduler();
}
/**
**********************************************************************
* @brief 获取空闲任务的任务堆栈和任务控制块内存
* ppxTimerTaskTCBBuffer : 任务控制块内存
* ppxTimerTaskStackBuffer : 任务堆栈内存
* pulTimerTaskStackSize : 任务堆栈大小
* @author fire
* @version V1.0
* @date 2018-xx-xx
**********************************************************************
*/
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize)
{
*ppxIdleTaskTCBBuffer=&Idle_Task_TCB;/* 任务控制块内存 */
*ppxIdleTaskStackBuffer=Idle_Task_Stack;/* 任务堆栈内存 */
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;/* 任务堆栈大小 */
}
/**
*********************************************************************
* @brief 获取定时器任务的任务堆栈和任务控制块内存
* ppxTimerTaskTCBBuffer : 任务控制块内存
* ppxTimerTaskStackBuffer : 任务堆栈内存
* pulTimerTaskStackSize : 任务堆栈大小
* @author fire
* @version V1.0
* @date 2018-xx-xx
**********************************************************************
*/
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
StackType_t **ppxTimerTaskStackBuffer,
uint32_t *pulTimerTaskStackSize)
{
*ppxTimerTaskTCBBuffer=&Timer_Task_TCB;/* 任务控制块内存 */
*ppxTimerTaskStackBuffer=Timer_Task_Stack;/* 任务堆栈内存 */
*pulTimerTaskStackSize=configTIMER_TASK_STACK_DEPTH;/* 任务堆栈大小 */
}