根据个人的学习方向,学习FreeRTOS。由于野火小哥把FreeRTOS讲得比较含蓄,打算在本专栏尽量细化一点。作为个人笔记,仅供参考或查阅。
配套资料:FreeRTOS内核实现与应用开发实战指南、野火FreeRTOS配套视频源码、b站野火FreeRTOS视频。搭配来看更佳哟!!!
任务状态的迁移,FreeRTOS系统中的每一个任务都有多种运行状态,请看任务状态迁移图
1:创建任务→就绪态(Ready):任务创建完成后进入就绪态,表明任务已准备就绪,随时可以运行,只等待调度器进行调度。
2:就绪态→运行态(Running):发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态。
3:运行态→就绪态:有更高优先级任务创建或者恢复后,会发生任务调度,此刻就绪列表中最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,依然在就绪列表中,等待最高优先级的任务运行完毕继续运行原来的任务(此处可以看做是 CPU 使用权被更高优先级的任务抢占了)。
4:运行态→阻塞态(Blocked):正在运行的任务发生阻塞(挂起、延时、读信号量等待)时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中当前最高优先级任务。
5:阻塞态→就绪态:阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,
将该任务将再次转换任务状态,由就绪态变成运行态。
6、7、8:就绪态、阻塞态、运行态→挂起态(Suspended):任务可以通过调用 vTaskSuspend() API 函数都可以将处于任何状态的任务挂起,被挂起的任务得不到CPU 的使用权,也不会参与调度,除非它从挂起态中解除。
9:挂起态→就绪态:把一个挂起状态的任务恢复的唯一途径就是调用 vTaskResume() 或 vTaskResumeFromISR() API函数,如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。
vTaskSuspend()挂起指定任务。被挂起的任务绝不会得到CPU的使用权,不管该任务具有什么优先级。相对于调度器而言是不可见的,除非它从挂起态中解除。
vTaskSuspend()如下
#if ( INCLUDE_vTaskSuspend == 1 ) //FreeRTOSConfig.h
void vTaskSuspend( TaskHandle_t xTaskToSuspend )
{
TCB_t *pxTCB;
taskENTER_CRITICAL();
{
//#defineprvGetTCBFromHandle(pxHandle)(((pxHandle)==NULL)?(TCB_t*)pxCurrentTCB:(TCB_t*)(pxHandle))
//如果pxTCB=prvGetTCBFromHandle(NULL);那么它就是正在运行的任务被挂起。
//如果pxTCB=prvGetTCBFromHandle(xTaskToSuspend);相当于pxTCB=(TCB_t*)xTaskToSuspend;那么它就是指定任务被挂起。
pxTCB = prvGetTCBFromHandle( xTaskToSuspend );
traceTASK_SUSPEND( pxTCB );
//从就绪/阻塞列表中删除任务,并在就绪/阻塞列表中把任务的优先级置0
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
taskRESET_READY_PRIORITY( pxTCB->uxPriority ); //根据任务的优先级uxPriority将优先级位图uxTopReadyPriority的某个位置0
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//listLIST_ITEM_CONTAINER返回链表所包含的节点
//如果任务在等待事件,也从等待事件列表中移除
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//插入到挂起列表
vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) );
#if( configUSE_TASK_NOTIFICATIONS == 1 )
{
if( pxTCB->ucNotifyState == taskWAITING_NOTIFICATION )
{
/* The task was blocked to wait for a notification, but is
now suspended, so no notification was received. */
pxTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
}
}
#endif
}
taskEXIT_CRITICAL();
if( xSchedulerRunning != pdFALSE ) //调度器正在运行
{
//重置下一个任务的解除阻塞时间。重新计算一下还要多长时间执行下一个任务。
//如果下个任务的解锁,刚好是被挂起的那个任务,那么变量NextTaskUnblockTime就不对了,所以要重新从延时列表中获取一下。
//因为挂起的任务对调度器而言是不可见的,所以调度器是无法对挂起态的任务进行调度,所以要重新从延时列表中获取下一个要解除阻塞的任务。
taskENTER_CRITICAL();
{
prvResetNextTaskUnblockTime();
}
taskEXIT_CRITICAL();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//如果挂起的是当前任务,并且调度器正在运行,则需要立即切换任务。不然系统的任务就错乱了,这是不允许的。
if( pxTCB == pxCurrentTCB ) //如果挂起的是当前任务
{
if( xSchedulerRunning != pdFALSE ) //调度器正在运行
{
//当前的任务已经被挂起
configASSERT( uxSchedulerSuspended == 0 );
//立即切换任务
portYIELD_WITHIN_API();
}
else
{
//调度器未运行,但pxCurrentTCB指向的任务刚刚被暂停,所以必须调整pxCurrentTCB以指向其他任务。
//PRIVILEGED_DATAstaticvolatileUBaseType_tuxCurrentNumberOfTasks =(UBaseType_t)0U;
if( listCURRENT_LIST_LENGTH( &xSuspendedTaskList ) == uxCurrentNumberOfTasks ) //判断系统所有的任务是不是都被挂起了
{
//事实上并不会发生这种情况,因为空闲任务是不允许被挂起和阻塞的,必须保证系统中无论如何都有一个任务可以运行
//没有其他任务准备就绪,因此将pxCurrentTCB设置回NULL,以便在创建下一个任务时pxCurrentTCB将被设置为指向它,实际上并不会执行到这里
pxCurrentTCB = NULL;
}
else
{
vTaskSwitchContext(); //有其他任务,则切换到其他任务
}
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* INCLUDE_vTaskSuspend */
任务可以调用vTaskSuspend()这个函数来挂起任务自身,但是在挂起自身的时候会进行一次任务上下文切换,需要挂起自身就将xTaskToSuspend设置为NULL传递进来即可。
无论任务是什么状态都可以被挂起,只要调用了vTaskSuspend()这个函数就会挂起成功,不论是挂起其他任务还是挂起任务自身。
任务的挂起与解挂函数在很多时候都是很有用的,比如我们想暂停某个任务运行一段时间,但是我们又需要在其恢复的时候继续工作,那么删除任务是不可能的,因为删除了任务的话,
任务的所有的信息都是不可能恢复的了,删除是完完全全删除了,里面的资源都被系统释放掉,但是挂起任务就不会这样子,调用挂起任务函数,仅仅是将任务进入挂起态,其内部的资源都会保留下来,同时也不会参与系统中任务的调度,当调用恢复函数的时候,整个任务立即从挂起态进入就绪态,并且参与任务的调度,如果该任务的优先级是当前就绪态优先级最高的任务,那么立即会按照挂起前的任务状态继续执行该任务,从而达到我们需要的效果,注意,是继续执行,也就是说,挂起任务之前是什么状态,都会被系统保留下来,在恢复的瞬间,继续执行。
void vTaskSuspendAll(void) //将所有任务都挂起
{
++uxSchedulerSuspended;
}
uxSchedulerSuspended用于记录调度器是否被挂起,默认初始值为pdFALSE,表明调度器是没被挂起的,每调用一次vTaskSuspendAll()函数就将变量加一,用于记录调用了多少次vTaskSuspendAll()函数。
挂起所有任务就是挂起任务调度器。调度器被挂起后则不能进行上下文切换,但是中断还是使能的。当调度器被挂起的时候,如果有中断需要进行上下文切换,那么这个任务将会被挂起,在调度器恢复之后才执行切换任务。
任务挂起vTaskSuspend()函数实例,上述函数官方都封装好的了,我们直接使用。
static TaskHandle_t LED_Task_Handle=NULL;/*LED任务句柄*/
static void KEY_Task(void*parameter)
{
while(1)
{
if(Key_Scan( KEY1_GPIO_PORT , KEY1_GPIO_PIN ) == KEY_ON) /*K1被按下*/
{
printf("挂起LED任务!\n");
vTaskSuspend(LED_Task_Handle); /*挂起LED任务*/
}
vTaskDelay(20); /*延时20个tick*/
}
}
任务解挂就是让挂起的任务重新进入就绪状态,恢复的任务会保留挂起前的状态信息,在恢复的时候根据挂起时的状态继续运行。如果被恢复任务在所有就绪态任务中,处于最高优先级列表的第一位,那么系统将进行任务上下文的切换。
vTaskResume()函数如下
#if ( INCLUDE_vTaskSuspend == 1 )
void vTaskResume( TaskHandle_t xTaskToResume )
{
TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume; //获取xTaskToResume对应的任务控制块
//检查要恢复的任务是否存在
configASSERT( xTaskToResume );
//该任务不能为NULL,同时也无法恢复当前正在执行的任务,因为当前正在运行的任务不需要恢复,只能恢复处于挂起态的任务
if( ( pxTCB != NULL ) && ( pxTCB != pxCurrentTCB ) )
{
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()函数,也只在任务是挂起态的时候才进行恢复。
任务解挂vTaskResume()函数实例,上述函数官方都封装好的了,我们直接使用。
static TaskHandle_t LED_Task_Handle=NULL;/*LED任务句柄*/
static void KEY_Task(void* parameter)
{
while(1)
{
if(Key_Scan( KEY2_GPIO_PORT , KEY2_GPIO_PIN ) == KEY_ON) /*K2被按下*/
{
printf("恢复LED任务!\n");
vTaskResume(LED_Task_Handle); /*恢复LED任务!*/
}
vTaskDelay(20); /*延时20个tick*/
}
}
xTaskResumeFromISR()与vTaskResume()一样都是用于恢复被挂起的任务。不一样的是xTaskResumeFromISR()专门用在中断服务程序中。
无论通过调用一次或多次vTaskSuspend()函数而被挂起的任务,也只需调用一次xTaskResumeFromISR()函数即可解挂。
要想使用该函数必须在FreeRTOSConfig.h中把INCLUDE_vTaskSuspend和INCLUDE_vTaskResumeFromISR都定义为1才有效。
任务还没有处于挂起态的时候,调用xTaskResumeFromISR()函数是没有任何意义的。
xTaskResumeFromISR()函数如下
#if ( ( INCLUDE_xTaskResumeFromISR == 1 ) && ( INCLUDE_vTaskSuspend == 1 ) )
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume )
{
BaseType_t xYieldRequired = pdFALSE;
TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume;
UBaseType_t uxSavedInterruptStatus; //保存关闭中断的状态
//检查要恢复的任务是否存在
configASSERT( xTaskToResume );
//检查中断优先级是否有效
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); //配置BASEPRI寄存器
{
if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE ) //判断任务是否真地被挂起了
{
traceTASK_RESUME_FROM_ISR( pxTCB );
//调度器被挂起
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
//如果恢复的任务优先级比当前任务优先级高
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xYieldRequired = pdTRUE; //切换任务请求标志置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( uxSavedInterruptStatus ); //清除BASEPRI寄存器的配置
return xYieldRequired; //返回切换任务请求标志,在外部选择是否进行任务切换
}
#endif /* ( ( INCLUDE_xTaskResumeFromISR == 1 ) && ( INCLUDE_vTaskSuspend == 1 ) ) */
当函数的返回值为pdTRUE时:恢复运行的任务的优先级等于或高于正在运行的任务,表明在中断服务函数退出后必须进行一次上下文切换,使用portYIELD_FROM_ISR()进行上下文切换。
当函数的返回值为pdFALSE时:恢复运行的任务的优先级低于当前正在运行的任务,表明在中断服务函数退出后不需要进行上下文切换。
xTaskResumeFromISR()通常被认为是一个危险的函数,因为它的调用并非是固定的,中断可能随时来临。所以,xTaskResumeFromISR()不能用于任务和中断间的同步,如果中断恰巧在任务被挂起之前到达,这就会导致一次中断丢失(任务还没有挂起,调用xTaskResumeFromISR()函数是没有意义的,只能等下一次中断)。这种情况下,可以使用信号量或者任务通知来同步就可以避免这种情况。
xTaskResumeFromISR()实例如下
void vAnExampleISR(void)
{
BaseType_t xYieldRequired;
/*恢复被挂起的任务*/
xYieldRequired = xTaskResumeFromISR(xHandle);
if(xYieldRequired == pdTRUE)
{
/*执行上下文切换,ISR返回的时候将运行另外一个任务*/
portYIELD_FROM_ISR();
}
}
xTaskResumeAll()函数如下
BaseType_t xTaskResumeAll( void ) //恢复调度器
{
TCB_t *pxTCB = NULL;
BaseType_t xAlreadyYielded = pdFALSE;
/*如果uxSchedulerSuspended为0,则此函数与先前对vTaskSuspendAll()的调用不匹配,不需要调用xTaskResumeAll()恢复调度器。*/
configASSERT( uxSchedulerSuspended );
taskENTER_CRITICAL();
{
--uxSchedulerSuspended;
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) //调度器没有被挂起,即恢复正常工作
{
if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U )
{
//当待处理就绪列表不为空
while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE )
{
//获取待处理就绪列表的第一个TCB
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &xPendingReadyList ) );
//移除第一个TCB
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB ); //将TCB添加到就绪列表
//如果恢复的TCB优先级高于等于当前任务的优先级
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xYieldPending = pdTRUE; //切换任务请求标志置pdTRUE
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
if( pxTCB != NULL )
{
/*在调度器被挂起时,任务被解除阻塞,这可能阻止了重新计算下一个解除阻塞时间。在这种情况下,重置下一个任务的解除阻塞时间*/
prvResetNextTaskUnblockTime(); //重置下一个任务的解除阻塞时间。
}
//如果在调度器挂起这段时间产生滴答定时器的计时并且在这段时间有任务解除阻塞,由于调度器的挂起导致没法切换任务,当恢复调度器的时候应立即处理这些任务。
//这样确保了滴答定时器的计数不会滑动,并且任何在延时的任务都会在正确的时间恢复
{
UBaseType_t uxPendedCounts = uxPendedTicks; /* Non-volatile copy. */
if( uxPendedCounts > ( UBaseType_t ) 0U )
{
do
{
if( xTaskIncrementTick() != pdFALSE ) //查找是否有待进行切换的任务
{
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
--uxPendedCounts;
} while( uxPendedCounts > ( UBaseType_t ) 0U );
uxPendedTicks = 0;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
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;
}
xTaskResumeAll函数的使用方法很简单,但是要注意,调用了多少次vTaskSuspendAll()函数就必须同样调用多少次xTaskResumeAll()函数。
本节所有函数都封装好的了,可以直接用。
vTaskSuspend()挂起指定任务。
vTaskSuspendAll()挂起任务调度器。
vTaskResume()解挂指定任务。
xTaskResumeAll()解挂任务调度器。
xTaskResumeFromISR()解挂指定任务,专门用在中断服务程序中。