【freertos】007-系统节拍和系统延时管理实现细节

前言

本章节的时钟系统节拍主要分析FreeRTOS内核相关及北向接口层,南向接口层不分析。
本章节的系统延时主要分析任务系统延时实现。

原文:李柱明博客:https://www.cnblogs.com/lizhuming/p/16085130.html
笔记手码。
相关代码仓库:李柱明 gitee

7.1 系统节拍配置

FreeRTOS的系统时钟节拍可以在配置文件FreeRTOSConfig.h里面设置:#define configTICK_RATE_HZ( ( TickType_t ) 1000 )

7.2 系统时钟节拍的原理

系统时钟节拍不仅仅只记录系统运行时长,还涉及到系统的时间管理,任务延时等等。

系统节拍数:

系统会通过南向接口层实现定时回调,维护一个全局变量xTickCount

每次定时回调会将变量xTickCount加1。

这个变量xTickCount就是系统时基节拍数。

获取时钟节拍数其实也就是返回该值。

注意:

系统节拍数不是每个tick都在实时累加的,在调度器挂起的情况下,触发产生的tick会记录下来,在恢复调度器后按挂起调度器产生的tick数逐个跑回xTaskIncrementTick(),快进模拟。

7.3 系统节拍中的处理:xTaskIncrementTick()

时钟节拍分析就按这个函数分析就好。

每当系统节拍定时器中断时,南向接口层都会调用该函数来实现系统节拍需要处理的代码。
系统节拍xTaskIncrementTick()这个函数的主要内容:

  1. 调度器没有被挂起:

    1. 计算系统节拍。
    2. 如果当前节拍已经大于或等于下一个需要解锁任务的节拍,就检索延时链表,解锁任务。如果被解锁的任务中有更高优先级的任务,需要触发调度。
    3. 如果当前任务优先级对应的就绪链表有其他任务,且开启了时间片调度,切换任务,触发调度。
  2. 如果调度器被挂起,则只记录节拍递增的次数,恢复调度器时按记录补回来。

小笔记:上述就会存在一个问题,如果就绪链表中有比当前任务优先级更加高的任务,是有可能不会被监测到的,所以内核在解锁一个比当前任务优先级更加高的任务,需要主动触发一次任务调度。

7.3.1 调度器正常

uxSchedulerSuspended这个变量记录调度器运行状态:

  • pdFALSE表示调度器正常,没有被挂起。
  • pdTRUE表示调度器被挂起。

7.3.1.1 系统节拍数统计

调度器正常的情况下,xTickCount加1。

7.3.1.2 延时列表

先看下面几条链表的源码。

/* Lists for ready and blocked tasks. --------------------
 * xDelayedTaskList1 and xDelayedTaskList2 could be moved to function scope but
 * doing so breaks some kernel aware debuggers and debuggers that rely on removing
 * the static qualifier. */
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; /*< Prioritised ready tasks. */
PRIVILEGED_DATA static List_t xDelayedTaskList1;                         /*< Delayed tasks. */
PRIVILEGED_DATA static List_t xDelayedTaskList2;                         /*< Delayed tasks (two lists are used - one for delays that have overflowed the current tick count. */
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;              /*< Points to the delayed task list currently being used. */
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;      /*< Points to the delayed task list currently being used to hold tasks that have overflowed the current tick count. */
PRIVILEGED_DATA static List_t xPendingReadyList;                         /*< Tasks that have been readied while the scheduler was suspended.  They will be moved to the ready list when the scheduler is resumed. */

需要注意的是,延时链表其实只有两条:

  • xDelayedTaskList1
  • xDelayedTaskList2

pxDelayedTaskListpxOverflowDelayedTaskList只是链表指针,分别指向当前正在使用的延时列表和溢出列表。

为什么需要两条延时列表?

为了解决系统节拍溢出问题。

如当系统节拍未溢出,pxDelayedTaskList指向xDelayedTaskList1pxOverflowDelayedTaskList指向xDelayedTaskList2时;

任务需要唤醒的时间在未溢出范围内,记录到pxDelayedTaskList指向的xDelayedTaskList1

任务需要唤醒的时间在超出溢出范围,记录到pxOverflowDelayedTaskList指向的xDelayedTaskList2

当系统节拍溢出时,会做如下处理:

  • pxDelayedTaskList更新指向xDelayedTaskList2
  • pxOverflowDelayedTaskList更新指向xDelayedTaskList1

这样就实现了pxDelayedTaskList始终指向未溢出的任务延时列表。

7.3.1.3 系统节拍溢出处理

对于嵌入式系统而已,xTickCount系统节拍占位也就8、32、64或者更大,但是也有溢出的时候,所以需要做溢出处理。

xTickCount系统节拍溢出处理是调用taskSWITCH_DELAYED_LISTS()实现

  • 交换延时列表指针和溢出延时列表指针;

  • 溢出次数记录xNumOfOverflows

  • 调用prvResetNextTaskUnblockTime()更新下一次解除阻塞的时间到xNextTaskUnblockTime

    • 如果延时列表为空,说明没有任务因为延时阻塞。把下次需要唤醒的时间更新为最大值。说明未来不需要检查延时列表。

    • 如果延时列表不为空,说明有任务等待唤醒。从延时列表的第一个任务节点中把节点值取出来,该值就是延时列表中未来最近有任务需要唤醒的时间。

      • freertos内核链表采用的是非通用双向循环链表,节点结构体如下代码所示。其中xItemValue可由用户自定义赋值,在freertos延时列表中,用于记录当前任务需要唤醒的时间节拍值。
      • 学习freertos内核链表的可以参考:李柱明-双向非通用链表

freertos内核链表节点结构体:

struct xLIST_ITEM

{
    listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE           /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
    configLIST_VOLATILE TickType_t xItemValue;          /*< The value being listed.  In most cases this is used to sort the list in ascending order. */
    struct xLIST_ITEM * configLIST_VOLATILE pxNext;     /*< Pointer to the next ListItem_t in the list. */
    struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*< Pointer to the previous ListItem_t in the list. */
    void * pvOwner;                                     /*< Pointer to the object (normally a TCB) that contains the list item.  There is therefore a two way link between the object containing the list item and the list item itself. */
    struct xLIST * configLIST_VOLATILE pxContainer;     /*< Pointer to the list in which this list item is placed (if any). */
    listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE          /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
};
typedef struct xLIST_ITEM ListItem_t;                   /* For some reason lint wants this as two separate definitions. */

xNextTaskUnblockTime变量就是表示当前系统未来最近一次延时列表任务中有任务需要唤醒的时间。

利用这个变量就不需要在每次tick到了都检查下延时列表是否需要解除阻塞,节省CPU开销。

7.3.1.4 任务唤醒处理

系统节拍溢出处理完后,检查是否需要唤醒任务。

如:

if( xConstTickCount >= xNextTaskUnblockTime )
{
   /* 延时的任务到期,需要被唤醒 */
}

进入上面代码逻辑分支以后,循环以下内容:

如果延时列表为空,则把xNextTaskUnblockTime更新到最大值。

如果延时列表不为空,则从延时列表中把任务句柄拿出来,分析:

  • 如果该任务需要唤醒的时间比系统节拍时间早,则

    • 把该任务从延时列表移除,重新插入到就绪列表;
    • 如果是因为事件阻塞,还要把该任务从事件列表中删除;
    • 如果解除阻塞的任务优先级比当前运行的任务优先级高,就标记触发任务调度xSwitchRequired = pdTRUE;
  • 如果该唤醒时间在未来,更新这个时间到xNextTaskUnblockTime,且退出遍历延时列表。

7.3.1.5 时间片处理

处理完任务阻塞后,便开始处理时间片的问题。

freertos的时间片不是真正意义的时间片,不能随意设置时间片多少个tick,只能默认一个tick。其实现就看这里代码就知道了。伪时间片。

每次tick都会检查是否有其他任务共享当前优先级,有就标记需要任务切换。

/* 如果有其它任务与当前任务共享一个优先级,则这些任务共享处理器(时间片) */
#if ( (configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
    if(listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
    {
        xSwitchRequired = pdTRUE;
    }
    else
    {
       mtCOVERAGE_TEST_MARKER();
    }
}

7.3.1.6 tick钩子

时间片处理完,可以执行tick钩子函数了。

需要注意的是,tick钩子函数vApplicationTickHook()是在系统滴答中跑的,所以这个函数内容要短小,不能大量使用堆栈,且只能调用以”FromISR" 或 "FROM_ISR”结尾的API函数。

另外,在代码中也能看到,在uxPendedTicks值为0才会执行tick钩子,这是因为不论调度器是否挂起,都会执行vApplicationTickHook()

而在调度器挂起期间,tick钩子也在执行,所以在补回时钟节拍的处理就不在执行tick钩子。

上述的uxPendedTicks值,是记录调度器挂起期间产生的tick数。

7.3.1.7 xYieldPending

该变量为了实现自动切换而萌生。

在函数xTaskIncrementTick()内,xSwitchRequired为返回值,为真,在外部调用会触发任务切换。

但是函数中xYieldPending变量也会触发xSwitchRequired为真。

我们需要了解xYieldPending这个变量的含义。

带中断保护的API函数(后缀FromISR),都会有一个参数pxHigherPriorityTaskWoken

如果这些API函数导致一个任务解锁,且该任务的优先级高于当前运行任务,这些API会标记*pxHigherPriorityTaskWoken = pdTRUE;,然后再退出字段前,老版本的FreeRTOS需要手动触发一次任务调度。

如在中断中跑:

BaseType_txHigherPriorityTaskWoken = pdFALSE;  
/* 收到一帧数据,向命令行解释器任务发送通知 */ 
vTaskNotifyGiveFromISR(xCmdAnalyzeHandle,&xHigherPriorityTaskWoken); 
/* 是否需要强制上下文切换 */ 
portYIELD_FROM_ISR(xHigherPriorityTaskWoken );

从FreeRTOSV7.3.0起,pxHigherPriorityTaskWoken成为一个可选参数,并可以设置为NULL。

转而使用xYieldPending来实现带中断保护的API函数解锁一个更高优先级任务后,标记该变量为pdTRUE,实现任务自动进行切换。

变量xYieldPendingpdTRUE,会在下一次系统节拍中断服务函数中,触发一次任务切换。代码便是:

if( xYieldPending != pdFALSE )
{
    xSwitchRequired = pdTRUE;
}

但是实际实现启用该功能是在在V9.0以及以上版本。

小结一下pxHigherPriorityTaskWokenxYieldPending

  • 在带中断保护的API中解锁了更高优先级的任务,需要在这些API内部标记一些变量来触发任务切换。这些变量有pxHigherPriorityTaskWokenxYieldPending

  • pxHigherPriorityTaskWoken

    • 手动切换标记。
    • 局部变量。
    • 如果带中断保护的API解锁了更高优先级的任务,会标记pxHigherPriorityTaskWokenpdTRUE,用户根据这个变量调用portYIELD_FROM_ISR()来实现手动切换任务。
  • xYieldPending

    • 自动切换标记。
    • 全家变量。
    • 如果标记为pdTRUE,在执行xTaskIncrementTick()时钟节拍处理时,调度器正常的情况下回触发一次任务切换。

带中断保护API内部参考代码:

if( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) 
{ 
     /*如果解除阻塞的任务优先级大于当前任务优先级,则设置上下文切换标识,等退出函数后手动切换上下文,或者在系统节拍中断服务程序中自动切换上下文*/ 
     if(pxHigherPriorityTaskWoken != NULL ) 
     { 
           *pxHigherPriorityTaskWoken= pdTRUE;    /* 设置手动切换标志*/ 
     } 
   
     xYieldPending= pdTRUE;                 /* 设置自动切换标志*/  
}

7.3.2 调度器挂起

如果调度器挂起,正在执行的任务会一直继续执行,内核不再调度,直到该任务调用xTaskResumeAll()恢复调度器。

在调度器挂起期间不会进行任务切换,但是其中产生的系统节拍都会记录在变量uxPendedTicks中。

在恢复调度器后,会在xTaskResumeAll()函数内调用uxPendedTicksxTaskIncrementTick()实现逐个补回时钟节拍处理。

7.4 系统节拍相关API

获取系统节拍:xTaskGetTickCount

作用:用于普通任务中,用于获取系统当前运行的时钟节拍数。

原型:

volatile TickType_t xTaskGetTickCount( void );

参数:无。

返回:返回当前运行的时钟节拍数。

7.4.1 获取系统节拍中断保护调用:xTaskGetTickCountFromISR()

作用:用于中断中,用于获取系统当前运行的时钟节拍数。

原型:

volatile TickType_t xTaskGetTickCountFromISR( void );

7.4.2 系统节拍API 实战

当前配置是configTICK_RATE_HZ是1000,即是1ms触发一次系统节拍。

7.5 系统延时API相关

系统提供两个延时API:

  • 相对延时函数vTaskDelay()
  • 绝对延时函数vTaskDelayUntil();
  • 终止延时函数xTaskAbortDelay()

7.6 相对延时:vTaskDelay()

7.6.1 API使用

函数原型:

void vTaskDelay(const TickType_t xTicksToDelay );

函数说明:

  • vTaskDelay()用于相对延时,是指每次延时都是从任务执行函数vTaskDelay()开始,延时指定的时间结束。
  • xTicksToDelay参数用于设置延迟的时钟节拍个数。
  • 延时的最大值宏在portmacro.h中有定义:#define portMAX_DELAY (TickType_t )0xffffffffUL

图中N就是参数xTicksToDelay

7.6.2 相对延时实现原理

原理:原理就是通过当前时间点和延时时长这两个值算出未来需要唤醒的时间,记录当前任务未来唤醒的时间点,然后把当前任务从就绪链表移到延时链表。

未来唤醒时间 = 当前时间 + 延时时间。

xTimeToWake = xConstTickCount + xTicksToWait;

7.6.3 实现细节

7.6.3.1 传入参数为0

传入参数为0时,不会把当前任务进行阻塞。

但是会触发一次任务调度。

7.6.3.2 挂起调度器

进入延时函数,在挂起调度器前会检查下当前当前是否已经挂起调度器了,如果硬件挂起调度器了还调用阻塞的相关API,系统会挂掉。

/* 如果调度器挂起了,那就没得玩了!!! */
configASSERT( uxSchedulerSuspended == 0 );

如果当前调度器没有被挂起,那可以进入延时处理,先挂起调度器,防止在迁移任务时被其它任务打断。

/* 挂起调度器 */
vTaskSuspendAll();

7.6.3.3 计算出未来唤醒时间

计算出未来唤醒时间点,这个就是相对延时和绝对延时的主要区别。

相对延时,未来唤醒时间点xTimeToWake是当前系统节拍加上xTicksToWait需要延时的节拍数。

然后把这个值记录到当前任务状态节点里面的节点值xItemValue里,用于插入延时列表排序使用。

xTimeToWake = xConstTickCount + xTicksToWait;
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );

7.6.3.4 迁移任务到延时链表

从就绪链表迁移到延时链表时,调用prvAddCurrentTaskToDelayedList()实现。

如果启用了终止延时功能,先pxCurrentTCB->ucDelayAborted把这个标志位复位,因为要出现进入延时了。

先把任务从就绪链表中移除。

移除后,如果当前任务同等优先级没有其它任务了,需要处理下就绪任务优先级位图:

  • 如果开启了优先级优化功能:需要把这个优先级在图表uxTopReadyPriority中对应的位清除。
  • 如果没有开启优先级优化功能:我认为也应该更新uxTopReadyPriority这个值,让系统知道当前就绪任务最高优先级已经不是当前任务的优先级值了。但是freertos并没有这样做。
  • 优先级优化功能可以查看我前面章节说的前导零指令。
/* 把当前任务先从就绪链表中移除。 */
if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
    /* 如果开启了优先级优化功能:需要把这个优先级在图表`uxTopReadyPriority`中对应的位清除。
        如果没有开启优先级优化功能,这个宏为空的,不处理。 */
    portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority ); 
}

如果计算出未来唤醒时间点溢出了,就把当前任务插入到溢出延时链表,到系统节拍溢出时就换使用该链表作为延时链表的。

如果未来唤醒时间点没有溢出,就插入当前延时链表,等待唤醒。如果唤醒时间比当前所有延时任务需要唤醒的时间还要早,那就更新下系统当前未来最近需要唤醒的时间值。

if( xTimeToWake < xConstTickCount )
{
    /* 唤醒时间点的系统节拍溢出,就插入到溢出延时列表中。 */
    vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
    /* 唤醒时间的系统节拍没有溢出,就插入当前延时链表。 */
    vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );

    /* 如果唤醒时间比当前所有延时任务需要唤醒的时间还要早,那就更新下系统当前未来最近需要唤醒的时间值。 */
    if( xTimeToWake < xNextTaskUnblockTime )
    {
        xNextTaskUnblockTime = xTimeToWake;
    }
}

7.6.3.5 强制任务调度

恢复调度器后,如果在恢复调度器时没有触发过任务调度,那必须进行一次触发任务调度,要不然本任务会继续往下跑,不符合设计逻辑。

/* 恢复调度器 */
xAlreadyYielded = xTaskResumeAll();

if( xAlreadyYielded == pdFALSE )
{
    /* 强制调度 */
    portYIELD_WITHIN_API();
}

7.7 绝对延时:vTaskDelayUntil()

7.7.1 API使用

函数原型:

BaseType_t vTaskDelayUntil( TickType_t *pxPreviousWakeTime, const TickType_t xTimeIncrement ); 

函数说明:

  • vTaskDelayUntil()用于绝对延时,也叫周期性延时。想象下精度不高的定时器。
  • pxPreviousWakeTime参数是存储任务上次处于非阻塞状态时刻的变量地址。
  • xTimeIncrement参数用于设置周期性延时的时钟节拍个数。
  • 返回:pdFALSE 说明延时失败。
  • 使用此函数需要在FreeRTOSConfig.h配置文件中开启:#defineINCLUDE_vTaskDelayUntil 1
  • 需要保证周期性延时比任务主体运行时间长。
  • 相对延时的意思是延时配置的N个节拍后恢复当前任务为就绪态。
  • 绝对延时的意思是延时配置的N个节拍后该任务跑回到当前绝对延时函数。

图中N就是参数xTimeIncrement ,其中黄色延时部分需要延时多少是vTaskDelayUntil()实现的。

7.7.2 绝对延时实现原理

原理:实现周期延时的原理就是,通过上次唤醒的时间点、当前时间点和延时周期三个值算出剩下需要延时的时间,得出未来需要唤醒当前任务的时间,然后把当前任务从就绪链表迁移到延时链表。

未来唤醒时间 = 上次唤醒时间 + 周期。

xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;

7.7.3 实现细节

7.7.3.1 参数检查

指针不能为空,周期值不能为0,调度器没有被挂起。

configASSERT( pxPreviousWakeTime );
configASSERT( ( xTimeIncrement > 0U ) );
configASSERT( uxSchedulerSuspended == 0 );

7.7.3.2 挂起调度器

需要注意的是,在调用该函数时,调度器必须是正常的。

如果当前调度器没有被挂起,那可以进入延时处理,先挂起调度器,防止在迁移任务时被其它任务打断。

7.7.3.3 未来唤醒时间

能把任务从就绪链表迁移到延时链表就绪阻塞的主要条件是唤醒时间在未来。

先算出未来唤醒时间:

xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;

7.7.3.4 溢出处理

如果当前时间对比上次唤醒的时间已经溢出了,那只有未来唤醒的时间值比当前的时间值还大,才能就绪阻塞处理。

这种情况如下图:

代码如下:

if( xConstTickCount < *pxPreviousWakeTime )
{
    /* 只有当周期性延时时间大于任务主体代码执行时间,即是唤醒时间在未来,才会将任务挂接到延时链表 */
    if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
    {
        xShouldDelay = pdTRUE;
    }
}

如果当前时间对比上次唤醒时间没有溢出过,需要考虑两种情况:

  • 未来时间唤醒时间已经溢出。
  • 未来时间唤醒时间没有溢出。

对于未来时间没有溢出,就是下图:

如果未来唤醒时间比上次唤醒的时间还小,便可说明唤醒时间在未来,这种判断代码就是:

/* 当前时间没有溢出的情况下,未来唤醒时间小于上次唤醒时间,可以说明未来唤醒时间在未来。 */
if( xConstTickCount >= *pxPreviousWakeTime && xTimeToWake < *pxPreviousWakeTime)
{
    xShouldDelay = pdTRUE;
}

而对于未来时间也没有溢出的情况如下图:

对于这种情况,未来唤醒时间值比当前时间值大,当前时间值又比上次唤醒时间值大,也可以说明唤醒时间在未来。

/* 当前时间没有溢出的情况下, 唤醒时间比当前时间还大,可以说明未来唤醒时间在未来。 */
if( xConstTickCount >= *pxPreviousWakeTime && xTimeToWake > xConstTickCount)
{
    xShouldDelay = pdTRUE;
}

小结下,只需要证明到实际时空时间值是:上次唤醒 < 当前时间 < 未来唤醒。即可说明当前任务主体运行时间比周期时间小,可以进行延时阻塞。

7.7.3.5 迁移到延时链表

参考相对延时的迁移到延时链表章节。

需要注意的是,传入prvAddCurrentTaskToDelayedList()的参数应该是相对延时值,而不是未来唤醒时间。

7.7.3.6 强制任务调度

恢复调度器后,如果在恢复调度器时没有触发过任务调度,那必须进行一次触发任务调度,要不然本任务会继续往下跑,不符合设计逻辑。

/* 恢复调度器 */
xAlreadyYielded = xTaskResumeAll();

if( xAlreadyYielded == pdFALSE )
{
    /* 强制调度 */
    portYIELD_WITHIN_API();
}

7.8 终止任务阻塞:xTaskAbortDelay()

使用该功能前需要在FreeRTOSConfig.h文件中配置宏INCLUDE_xTaskAbortDelay为1来使用该功能。

7.8.1 API 使用

函数原型:

BaseType_t xTaskAbortDelay( TaskHandle_t xTask );

函数说明:

  • xTaskAbortDelay()函数用于解除任务的阻塞状态,将任务插入就绪链表中。

  • xTask :任务句柄。

  • 返回:

    • pdPASS:任务解除阻塞成功。
    • pdFAIL或其它:没有解除任务阻塞还在任务不在阻塞状态。

7.8.2 实现细节

7.8.2.1 参数检查

主要检查任务句柄值是否有效。

/* 如果传入的任务句柄是NULL,直接断言 */
configASSERT( pxTCB );

7.8.2.2 挂起调度器

挂起调度器,防止任务被切走处理。

7.8.2.3 获取任务状态

通过API eTaskGetState()获取任务状态是否处于阻塞态。有以下情况可以判断任务处于阻塞态:

  1. 任务处于延时链表或者处于延时溢出链表。
  2. 任务处于挂起态,但是在等待某个事件,也属于阻塞态。
  3. 处于挂起态,也没有在等待事件,但是在等待任务通知,也属于阻塞态。

这部分看下该API源码即可。

如果不在阻塞态,可以xTaskAbortDelay()函数直接返回pdFAIL

7.8.2.4 解除任务状态并重新插入就绪链表

解除任务所有状态,在阻塞态时,其实就是先把任务迁出对应的任务状态链表。

( void ) uxListRemove( &( pxTCB->xStateListItem ) );

然后加入临界处理因为事件而阻塞的问题,进入临界处理是因为部分中断回调也会接触到任务事件链表。

如果任务是因为事件而阻塞的,需要从事件链表中移除,解除阻塞,并且标记上强制解除阻塞标记。

/* 进入临界 */
taskENTER_CRITICAL();
{
    /* 因为事件而阻塞 */
    if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
    {
        /* 移除任务的事件 */
        ( void ) uxListRemove( &( pxTCB->xEventListItem ) );

        /* 强制解除阻塞标志 */
        pxTCB->ucDelayAborted = pdTRUE;
    }
}
/* 退出临界 */
taskEXIT_CRITICAL();

处理完事件链表后,可以将其重新插入到就绪链表。

/* 重新加入就行链表 */
prvAddTaskToReadyList( pxTCB );

7.8.2.5 恢复调度器

把阻塞的任务成功迁入到就绪链表后,如果开启了抢占式调度,如果解除阻塞的任务优先级大于当前在跑的任务优先级,需要任务切换。

通过xYieldPending = pdTRUE;标记在恢复调度器时进行任务切换。这个是一个确保。

在恢复调度器API xTaskResumeAll()里面,后面章节会有分析过这个API,有兴趣的同学可以往后翻。

在这个API里面,恢复调度器也会逐个恢复系统节拍,然后在最后检查xYieldPending变量是否需要触发任务切换。

7.9 系统延时实战

代码地址:李柱明 gitee

  • 找到release分支中的 freertos_on_linux_task_delay 文件夹,拉下来,直接make。

创建三个任务说明相对延时、绝对延时和解除阻塞:

/** @brief lzmStaticTestTack
  * @details 
  * @param 
  * @retval 
  * @author lizhuming
  */
static void lzmStaticTestTask(void* parameter)
{
    int tick_cnt = 0;

	/* task init */
    printf("start lzmStaticTestTask\r\n");

    for(;;)
    {
        vTaskDelay(500); /* 假设任务主体需要 500 个节拍运行 */

        tick_cnt = xTaskGetTickCount();
        printf("delay task tick_cnt befor sleep [1][%d]\r\n", tick_cnt); /* 阻塞前 */
        vTaskDelay(1000);
        tick_cnt = xTaskGetTickCount();
        printf("delay task after wake up [1][%d]\r\n", tick_cnt); /* 唤醒后 */
    }
}

/** @brief lzmTestTask
  * @details 
  * @param 
  * @retval 
  * @author lizhuming
  */
static void lzmTestTask(void* parameter)
{
    int tick_cnt = 0;
    TickType_t pervious_wake_time = 0;

	/* task init */
    printf("start lzmTestTask\r\n");

    tick_cnt = xTaskGetTickCount();
    pervious_wake_time = tick_cnt;

    for(;;)
    {
        tick_cnt = xTaskGetTickCount();
        printf("delayunitil task tick_cnt [2][%d]\r\n", tick_cnt); /* 观测下是否按1000个tick的周期跑 */
    
        vTaskDelay(500); /* 假设任务主体需要 500 个节拍运行 */
        xTaskAbortDelay(lzmAbortDelayTaskHandle); /* 解除其他任务阻塞 */

        vTaskDelayUntil(&pervious_wake_time, 1000); /* 周期1000个tick */
    }
}

/** @brief lzmAbortDelayTask
  * @details 
  * @param 
  * @retval 
  * @author lizhuming
  */
static void lzmAbortDelayTask(void* parameter)
{
    int tick_cnt = 0;

	/* task init */
    printf("start lzmAbortDelayTask\r\n");

    tick_cnt = xTaskGetTickCount();

    for(;;)
    {
        vTaskDelay(portMAX_DELAY); /* 永久阻塞 */

        tick_cnt = xTaskGetTickCount();
        printf("unblock tick_cnt [3][%d]\r\n", tick_cnt); /* 如果被解除阻塞一次,就打印一次 */
    }
}

运行成功:

附件

系统节拍统计:xTaskIncrementTick()

BaseType_t xTaskIncrementTick( void )
{
    TCB_t * pxTCB;
    TickType_t xItemValue;
    BaseType_t xSwitchRequired = pdFALSE;
 
    /* 每当系统节拍定时器中断发生,移植层都会调用该函数.函数将系统节拍中断计数器加1,然后检查新的系统节拍中断计数器值是否解除某个任务.*/
    if(uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
    {   /* 调度器正常 */
        const TickType_txConstTickCount = xTickCount + 1;
 
        /* 系统节拍中断计数器加1,如果计数器溢出(为0),交换延时列表指针和溢出延时列表指针 */
        xTickCount = 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 ) );
 
                    if( xConstTickCount < xItemValue )
                    {
                        /* 任务还未到解除阻塞时间?将当前任务唤醒时间设置为下次解除阻塞时间. */
                       xNextTaskUnblockTime = xItemValue;
                        break;
                    }
 
                    /* 从阻塞列表中删除到期任务 */
                    ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
 
                    /* 是因为等待事件而阻塞?是的话将到期任务从事件列表中删除 */
                    if(listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
                    {
                        ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
                    }
 
                    /* 将解除阻塞的任务放入就绪列表 */
                   prvAddTaskToReadyList( pxTCB );
 
                    #if (  configUSE_PREEMPTION == 1 )
                    {
                        /* 使能了抢占式内核.如果解除阻塞的任务优先级大于当前任务,触发一次上下文切换标志 */
                        if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
                        {
                            xSwitchRequired= pdTRUE;
                        }
                    }
                    #endif /*configUSE_PREEMPTION */
                }
            }
        }
 
        /* 如果有其它任务与当前任务共享一个优先级,则这些任务共享处理器(时间片) */
        #if ( (configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
        {
            if(listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
            {
                xSwitchRequired = pdTRUE;
            }
            else
            {
               mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif /* ( (configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
 
        #if (configUSE_TICK_HOOK == 1 )
        {
            /* 调用时间片钩子函数*/
            if( uxPendedTicks == ( UBaseType_t ) 0U )
            {
                vApplicationTickHook();
            }
        }
        #endif /*configUSE_TICK_HOOK */

    #if (configUSE_PREEMPTION == 1 )
        {   /* 如果在中断中调用的API函数唤醒了更高优先级的任务,并且API函数的参数pxHigherPriorityTaskWoken为NULL时,变量xYieldPending用于上下文切换标志 */
            if( xYieldPending!= pdFALSE )
            {
                xSwitchRequired = pdTRUE;
            }
        }
        #endif /*configUSE_PREEMPTION */

    }
    else
    {   /* 调度器挂起状态,变量uxPendedTicks用于统计调度器挂起期间,系统节拍中断次数.
           当调用恢复调度器函数时,会执行uxPendedTicks次本函数(xTaskIncrementTick()):
           恢复系统节拍中断计数器,如果有任务阻塞到期,则删除阻塞状态 */
        ++uxPendedTicks;
 
        /* 调用时间片钩子函数*/
        #if (configUSE_TICK_HOOK == 1 )
        {
            vApplicationTickHook();
        }
        #endif
    }
 
    return xSwitchRequired;
}

系统节拍溢出处理:taskSWITCH_DELAYED_LISTS()

/* pxDelayedTaskList和pxOverflowDelayedTaskList在tick计数溢出时切换 */
#define taskSWITCH_DELAYED_LISTS()                                                \
{                                                                             \
    List_t * pxTemp;                                                          \
                                                                                \
    /* 当列表被切换时,延迟的任务列表应该为空 */ \
    configASSERT( ( listLIST_IS_EMPTY( pxDelayedTaskList ) ) );               \
                                                                                \
    pxTemp = pxDelayedTaskList;                                               \
    pxDelayedTaskList = pxOverflowDelayedTaskList;                            \
    pxOverflowDelayedTaskList = pxTemp;                                       \
    xNumOfOverflows++;                                                        \
    prvResetNextTaskUnblockTime();                                            \
}

static void prvResetNextTaskUnblockTime( void )
{
    if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
    {
        /* 如果延时列表为空,说明没有任务因为延时阻塞。把下次需要唤醒的时间更新为最大值。 */
        xNextTaskUnblockTime = portMAX_DELAY;
    }
    else
    {
        /* 如果延时列表不为空,说明有任务等待唤醒。从延时列表中的第一个任务节点中把节点值取出来,该值就是延时列表中未来最近有任务需要唤醒的时间。 */
        xNextTaskUnblockTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxDelayedTaskList );
    }
}

相对延时:vTaskDelay()

void vTaskDelay( const TickType_t xTicksToDelay )
{
    BaseType_t xAlreadyYielded = pdFALSE;

    /* 如果延时输入的参数为0,那只是为了触发一次调度。
        如果输入的参数不为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();
    }
}

添加当前任务到延时列表:prvAddCurrentTaskToDelayedList()

static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely )
{
    TickType_t xTimeToWake;
    const TickType_t xConstTickCount = xTickCount;

    #if ( INCLUDE_xTaskAbortDelay == 1 )
        {
            /* 先把解除延时阻塞的标志位复位。 */
            pxCurrentTCB->ucDelayAborted = pdFALSE;
        }
    #endif

    /* 把当前任务先从就绪链表中移除。 */
    if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
    {
        /* 如果当前任务同等优先级没有其它任务了,就需要把这个优先级在图表 uxTopReadyPriority 中对应的位清除 */
        portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority ); 
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
    {
        /* 如果延时为最大值,且允许无限期阻塞。那直接插入到挂起列表中。 */
        listINSERT_END( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
    }
    else
    {
        /* 相对延时,算出未来需要唤醒的时间点。 */
        xTimeToWake = xConstTickCount + xTicksToWait;

        /* 把当前唤醒值配置到节点内部值里面,插入链表时排序用。 */
        listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );

        if( xTimeToWake < xConstTickCount )
        {
            /* 唤醒时间点的系统节拍溢出,就插入到溢出延时列表中。 */
            vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
        }
        else
        {
            /* 唤醒时间的系统节拍没有溢出,就插入当前延时链表。 */
            vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );

            /* 如果开启了优先级优化功能:需要把这个优先级在图表`uxTopReadyPriority`中对应的位清除。
            如果没有开启优先级优化功能,这个宏为空的,不处理。 */
            if( xTimeToWake < xNextTaskUnblockTime )
            {
                xNextTaskUnblockTime = xTimeToWake;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
    }
}

绝对延时:xTaskDelayUntil()

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;

        /* 算出未来唤醒时间点 */
        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( xTimeToWake - xConstTickCount, pdFALSE );
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    /* 恢复调度器。 */
    xAlreadyYielded = xTaskResumeAll();

    /* 如果在恢复调度器时,内部没有触发任务调度,那这里需要强制触发调度,要不然本任务就会继续跑,不符合期待。 */
    if( xAlreadyYielded == pdFALSE )
    {
        portYIELD_WITHIN_API();
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    return xShouldDelay;
}

解除任务阻塞:xTaskAbortDelay()

BaseType_t xTaskAbortDelay( TaskHandle_t xTask )
{
    TCB_t * pxTCB = xTask;
    BaseType_t xReturn;

    /* 如果传入的任务句柄是NULL,直接断言 */
    configASSERT( pxTCB );

    /* 挂起调度器 */
    vTaskSuspendAll();
    {
        /* 获取任务状态,如果当前为阻塞态,才能解除阻塞嘛 */
        if( eTaskGetState( xTask ) == eBlocked )
        {
            xReturn = pdPASS;

            /* 移除任务所有状态,迁出对应的任务状态链表 */
            ( void ) uxListRemove( &( pxTCB->xStateListItem ) );

            /* 进入临界,处理因为事件而阻塞的问题。
                进入临界处理是因为部分中断回调也会接触到任务事件链表。
                进入临界算是给任务事件链表“上锁”吧*/
            taskENTER_CRITICAL();
            {
                /* 因为事件而阻塞 */
                if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
                {
                    /* 移除任务的事件 */
                    ( void ) uxListRemove( &( pxTCB->xEventListItem ) );

                    /* 强制解除阻塞标志 */
                    pxTCB->ucDelayAborted = pdTRUE;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            /* 退出临界 */
            taskEXIT_CRITICAL();

            /* 重新加入就行链表 */
            prvAddTaskToReadyList( pxTCB );

            #if ( configUSE_PREEMPTION == 1 )
                {
                    /* 如果解除阻塞的任务优先级大于当前在跑的任务优先级,需要任务切换 */
                    if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
                    {
                        /* 标记在恢复调度器时进行任务切换 */
                        xYieldPending = pdTRUE;
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
            #endif /* configUSE_PREEMPTION */
        }
        else
        {
            xReturn = pdFAIL;
        }
    }
    /* 恢复调度器 */
    ( void ) xTaskResumeAll();

    return xReturn;
}
posted @ 2022-04-01 09:23  李柱明  阅读(4528)  评论(0编辑  收藏  举报