【FreeRTOS源码学习笔记】任务延时列表

【FreeRTOS源码学习笔记】任务延时列表


前言

在之前,为了实现任务的阻塞延时,在任务控制块中内置了一个延时变量 xTicks-ToDelay。每当任务需要延时的时候,就初始化 xTicksToDelay 需要延时的时间,然后将任务挂起,这里的挂起只是将任务在优先级位图表 uxTopReadyPriority 中对应的位清零,并不会将任务从就绪列表中删除。当每次时基中断(SysTick 中断)来临时,就扫描就绪列表中每个任务的 xTicksToDelay,如果 xTicksToDelay 大于 0,则递减一次,然后判断 xTicksToDelay 是否为 0,如果为 0 则表示延时时间到,将该任务就绪(即将任务在优先级位图表 uxTopReadyPriority 中对应的位置位),然后进行任务切换。这种延时的缺点是,在每个时基中断中需要对所有任务都扫描一遍,比较费时,优点是容易理解。之所以先这样讲解,是为了慢慢地过渡到对 FreeRTOS 任务延时列表的介绍。


一、任务延时列表的工作原理

在 FreeRTOS 中,有一个任务延时列表(实际上有两个,为了方便讲解原理,我们假设将二者合并为一个,其实两个延时列表的作用是一样的),当任务需要延时,则先将任务挂起,即先将任务从就绪列表中删除,然后插入任务延时列表,同时更新下一个任务的解锁时刻变量 xNextTaskUnblockTime 的值。

xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时的值 xTicksToDelay。当系统时基计数器 xTickCount 的值与 xNextTaskUnblockTime 相等时,就表示有任务延时到期了,需要将该任务就绪。与 RT-Thread 和 μC/OS 在解锁延时任务时要扫描定时器列表这种时间不确定的方法相比,FreeRTOS 中的 xNextTaskUnblockTime 全局变量设计得非常巧妙。

任务延时列表维护着一条双向链表,每个节点代表了正在延时的任务,节点按照延时时间大小做升序排列。当每次时基中断(SysTick 中断)来临时,就用系统时基计数器的值 xTickCount 与下一个任务的解锁时刻变量 xNextTaskUnblockTime 的值相比较,如果相等,则表示有任务延时到期,需要将该任务就绪,否则只是单纯地更新系统时基计数器 xTickCount 的值,然后进行任务切换。


二、实现任务延时列表

2.1 定义任务延时列表

任务延时列表在 task.c 中定义。

c 复制代码
1 static List_t xDelayedTaskList1;                        (1)
2 static List_t xDelayedTaskList2;                        (2)
3 static List_t * volatile pxDelayedTaskList;             (3)
4 static List_t * volatile pxOverflowDelayedTaskList;     (4)

( 1 )( 2 ):FreeRTOS 定义了两个任务延时列表,当系统时基计数器 xTickCount 没有溢出时,用一个列表,当 xTickCount 溢出后,用另外一个列表。

( 3 ):任务延时列表指针,指向 xTickCount 没有溢出时使用的列表。

( 4 ):任务延时列表指针,指向 xTickCount 溢出时使用的列表。


2.2 任务延时列表初始化

任务延时列表属于任务列表的一种,在 prvInitialiseTaskLists() 函数中初始化。

c 复制代码
1 /* 初始化任务相关的列表 */
2 void prvInitialiseTaskLists( void )
3 {
4     UBaseType_t uxPriority;
5 
6     /* 初始化就绪列表 */
7     for ( uxPriority = ( UBaseType_t ) 0U;
8          uxPriority < ( UBaseType_t ) configMAX_PRIORITIES;
9          uxPriority++ )
10    {
11        vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
12    }
13 
14    vListInitialise( &xDelayedTaskList1 );
15    vListInitialise( &xDelayedTaskList2 );
16 
17    pxDelayedTaskList = &xDelayedTaskList1;
18    pxOverflowDelayedTaskList = &xDelayedTaskList2;
19 }

就绪列表是一个数组,每个数组元素就是一个链表,所以用循环来进行初始化。

延时列表就是一个链表,所以直接初始化即可。


2.3 初始化 xNextTaskUnblockTime

xNextTaskUnblockTime 是一个在 task.c 中定义的静态变量,用于表示下一个任务的解锁时刻。xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时的值 xTicksToDelay。当系统时基计数器的值 xTickCount 的值与 xNextTaskUnblockTime 相等时,就表示有任务延时到期了,需要将该任务就绪。

xNextTaskUnblockTime 在 vTaskStartScheduler() 函数中初始化为 portMAX_DELAY(portMAX_DELAY 是一个 portmacro.h 中定义的宏,默认为 0xffffffffUL)

c 复制代码
1 void vTaskStartScheduler( void )
2 {
3     /*========================== 创建空闲任务 start ==========================*/
4     TCB_t *pxIdleTaskTCBBuffer = NULL;
5     StackType_t *pxIdleTaskStackBuffer = NULL;
6     uint32_t ulIdleTaskStackSize;
7 
8     /* 获取空闲任务的内存:任务栈和任务 TCB */
9     vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,
10                                  &pxIdleTaskStackBuffer,
11                                  &ulIdleTaskStackSize );
12 
13    xIdleTaskHandle =
14    xTaskCreateStatic( (TaskFunction_t)prvIdleTask,
15                      (char *)"IDLE",
16                      (uint32_t)ulIdleTaskStackSize,
17                      (void *) NULL,
18                      (UBaseType_t) tskIDLE_PRIORITY,
19                      (StackType_t *)pxIdleTaskStackBuffer,
20                      (TCB_t *)pxIdleTaskTCBBuffer );
21    /*========================== 创建空闲任务 end ==========================*/
22 
23    xNextTaskUnblockTime = portMAX_DELAY;  //--新增--//
24 
25    xTickCount = ( TickType_t ) 0U;
26 
27    /* 启动调度器 */
28    if ( xPortStartScheduler() != pdFALSE )
29    {
30        /* 调度器启动成功,则不会返回,即不会来到这里 */
31    }
32 }

为什么要初始化成一个极大的值?

表示"当前没有任何任务处于阻塞延时状态"

防止滴答中断误判(避免无意义的开销):

假设初始化为 0。系统刚开始运行,xTickCount 也是 0(或很小)。那么在第一次 Tick 中断时,xTickCount >= xNextTaskUnblockTime 直接成立。调度器每次中断都会去检查延时列表(Delayed List),但列表其实是空的。这不仅浪费CPU周期,还增加了系统出错的概率。设为极大值,就可以直接跳过这个检查,提升效率。

后面提到的prvAddCurrentTaskToDelayedList() 函数会比较大小然后更新 xNextTaskUnblockTime。


三、修改代码以支持任务延时列表

接下来我们在之前代码的基础上,继续迭代修改,从而实现支持任务延时列表。

3.1 修改 vTaskDelay() 函数

c 复制代码
1 void vTaskDelay( const TickType_t xTicksToDelay )
2 {
3     TCB_t *pxTCB = NULL;
4 
5     /* 获取当前任务的 TCB */
6     pxTCB = pxCurrentTCB;
7 
8     /* 设置延时时间 */
9     //pxTCB->xTicksToDelay = xTicksToDelay;                          (1)
10
11    /* 将任务插入延时列表 */
12    prvAddCurrentTaskToDelayedList( xTicksToDelay );               (2)
13
14    /* 任务切换 */
15    taskYIELD();
16 }

( 1 ):从本章开始,添加了任务的延时列表,延时时不用再依赖任务 TCB 中内置的延时变量 xTicksToDelay。

( 2 ):将任务插入延时列表。函数 prvAddCurrentTaskToDelayedList() 在 task.c 中定义,具体实现如下。

prvAddCurrentTaskToDelayedList() 函数

c 复制代码
1 static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait )
2 {
3     TickType_t xTimeToWake;
4 
5     /* 获取系统时基计数器 xTickCount 的值 */                                    (1)
6     const TickType_t xConstTickCount = xTickCount;
7 
8     /* 将任务从就绪列表中移除 */                                                (2)
9     if ( uxListRemove( &( pxCurrentTCB->xStateListItem ) )
10        == ( UBaseType_t ) 0 )
11    {
12        /* 将任务在优先级位图中对应的位清除 */
13        portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority,
14                                 uxTopReadyPriority );
15    }
16 
17    /* 计算任务延时到期时,系统时基计数器 xTickCount 的值是多少 */                 (3)
18    xTimeToWake = xConstTickCount + xTicksToWait;
19 
20    /* 将延时到期的值设置为节点的排序值 */                                        (4)
21    listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ),
22                            xTimeToWake );
23 
24    /* 溢出 */                                                                    (5)
25    if ( xTimeToWake < xConstTickCount )
26    {
27        vListInsert( pxOverflowDelayedTaskList,
28                    &( pxCurrentTCB->xStateListItem ) );
29    }
30    else/* 没有溢出 */
31    {
32        vListInsert( pxDelayedTaskList,
33                    &( pxCurrentTCB->xStateListItem ) );                        (6)
34
35        /* 更新下一个任务解锁时刻变量 xNextTaskUnblockTime 的值 */                   (7)
36        if ( xTimeToWake < xNextTaskUnblockTime )
37        {
38            xNextTaskUnblockTime = xTimeToWake;
39        }
40    }
41
42 }

(1):获取系统时基计数器 xTickCount 的值,xTickCount 是一个在 task.c 中定义的全局变量,用于记录 SysTick 的中断次数。

(2):调用函数 uxListRemove() 将任务从就绪列表中移除,uxListRemove() 会返回当前链表下节点的个数,如果为 0,则表示当前链表下没有任务就绪,那么将调用函数 portRESET_READY_PRIORITY() 将任务在优先级位图表 uxTopReadyPriority 中对应的位清除。因为 FreeRTOS 支持同一个优先级下可以有多个任务,所以在清除优先级位图表 uxTopReadyPriority 中对应的位时要判断该优先级下的就绪列表是否还有其他任务。到目前为止,我们还没有实现支持同一个优先级下有多个任务的功能,这个功能我们将在第 10 章实现。

(3):计算任务延时到期时,系统时基计数器 xTickCount 的值是多少。

(4):将任务延时到期的值设置为节点的排序值。将任务插入延时列表时就是根据这个值来做升序排列的,最先延时到期的任务排在最前面。

(5):xTimeToWake 溢出,将任务插入溢出任务延时列表。溢出是什么意思?xTimeToWake 等于系统时基计数器 xTickCount 的值加上任务需要延时的时间 xTicks-ToWait。例如,如果当前 xTickCount 的值等于 0xffffffffUL,xTicksToWait 等于 0x03,那么 xTimeToWake = 0xffffffffUL + 0x03 = 1,显然得出的值比任务需要延时的时间 0x03 还小,这肯定不正常,说明溢出了,这个时候需要将任务插入溢出任务延时列表。

(6):xTimeToWake 没有溢出,则将任务插入正常任务延时列表。

(7):更新下一个任务解锁时刻变量 xNextTaskUnblockTime 的值。这一步很重要,在 xTaskIncrementTick() 函数中,我们只需要让系统时基计数器 xTickCount 与 xNextTaskUnblockTime 的值先进行比较,就可以知道延时最快结束的任务是否到期。

3.2 修改 xTaskIncrementTick() 函数

对 xTaskIncrementTick() 函数的改动较大,具体参见代码清单 9-6 。

代码清单 9-6 xTaskIncrementTick() 函数

c 复制代码
1 void xTaskIncrementTick( void )
2 {
3     TCB_t * pxTCB;
4     TickType_t xItemValue;
5 
6     const TickType_t xConstTickCount = xTickCount + 1;                (1)
7     xTickCount = xConstTickCount;
8 
9     /* 如果 xConstTickCount 溢出,则切换延时列表 */                     (2)
10    if ( xConstTickCount == ( TickType_t ) 0U )
11    {
12        taskSWITCH_DELAYED_LISTS();
13    }
14 
15    /* 最近的延时任务延时到期 */                                        (3)
16    if ( xConstTickCount >= xNextTaskUnblockTime )
17    {
18        for ( ; ; )
19        {
20            if ( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )   (4)
21            {
22                /* 延时列表为空,设置 xNextTaskUnblockTime 为可能的最大值 */
23                xNextTaskUnblockTime = portMAX_DELAY;
24                break;
25            }
26            else/* 延时列表不为空 */                                      (5)
27            {
28                pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
29                xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
30 
31                /* 直到将延时列表中所有延时到期的任务移除才跳出 for 循环 */  (6)
32                if ( xConstTickCount < xItemValue )
33                {
34                    xNextTaskUnblockTime = xItemValue;
35                    break;
36                }
37 
38                /* 将任务从延时列表中移除,解除等待状态 */                  (7)
39                ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
40 
41                /* 将解除等待的任务添加到就绪列表 */                        (8)
42                prvAddTaskToReadyList( pxTCB );
43            }
44        }
45    }/* xConstTickCount >= xNextTaskUnblockTime */
46 
47     /* 任务切换 */
48     portYIELD();                                   (10)
49 }

代码清单 9-6 ( 1 ):更新系统时基计数器 xTickCount 的值。

代码清单 9-6 ( 2 ):如果系统时基计数器 xTickCount 溢出,则切换延时列表。taskSWITCH_DELAYED_LISTS() 函数在 task.c 中定义,具体实现参见代码清单 9-7。

代码清单 9-7 taskSWITCH_DELAYED_LISTS() 函数

c 复制代码
1 #define taskSWITCH_DELAYED_LISTS() \
2 { \
3     List_t *pxTemp; \
4     pxTemp = pxDelayedTaskList; \                          (1)
5     pxDelayedTaskList = pxOverflowDelayedTaskList; \
6     pxOverflowDelayedTaskList = pxTemp; \
7     xNumOfOverflows++; \
8     prvResetNextTaskUnblockTime(); \                       (2)
9 }

代码清单 9-7 ( 1 ):切换延时列表,实际就是更换 pxDelayedTaskList 和 pxOverflowDelayedTaskList 这两个指针的指向。

代码清单 9-7 ( 2 ):复位 xNextTaskUnblockTime 的值。prvResetNextTaskUnblockTime() 函数在 task.c 中定义,具体实现参见代码清单 9-8。

代码清单 9-8 prvResetNextTaskUnblockTime() 函数

c 复制代码
1 static void prvResetNextTaskUnblockTime( void )
2 {
3     TCB_t *pxTCB;
4 
5     if ( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
6     {
7         /* 当前延时列表为空,则设置 xNextTaskUnblockTime 等于最大值 */
8         xNextTaskUnblockTime = portMAX_DELAY;
9     }
10    else
11    {
12        /* 当前列表不为空,表示有任务在延时,则获取当前列表下第一个节点的排序值
13        然后将该节点的排序值更新到 xNextTaskUnblockTime */
14        ( pxTCB ) = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
15        xNextTaskUnblockTime = listGET_LIST_ITEM_VALUE( &( ( pxTCB )->xStateListItem ) );
16    }
17 }

代码清单 9-6 ( 3 ):有任务延时到期,则进入下面的 for 循环,一一将这些延时到期的任务从延时列表中移除。

代码清单 9-6(4):延时列表为空,则将 xNextTaskUnblockTime 设置为最大值,然后跳出 for 循环。

代码清单 9-6(5):延时列表不为空,则需要将延时列表中延时到期的任务删除,并将它们添加到就绪列表。

代码清单 9-6(6):取出延时列表第一个节点的排序辅助值。

代码清单 9-6(7):直到将延时列表中所有延时到期的任务移除才跳出 for 循环。延时列表中有可能存在多个延时相等的任务。

代码清单 9-6(8):将任务从延时列表中移除,解除等待状态。

代码清单 9-6(9):将解除等待的任务添加到就绪列表。

代码清单 9-6(10):执行一次任务切换。


3.3 修改 taskRESET_READY_PRIORITY() 函数

在没有添加任务延时列表之前,与任务相关的列表只有一个,就是就绪列表,无论任务处于延时状态还是就绪状态,都只能通过扫描就绪列表来找到任务的 TCB,从而实现系统调度。所以在第 8 章中实现 taskRESET_READY_PRIORITY() 函数时,不用先判断当前优先级下就绪列表中的链表的节点是否为 0,而是直接把任务在优先级位图表 uxTopReadyPriority 中对应的位清零。因为当前优先级下就绪列表中的链表的节点不可能为 0,目前我们还没有添加其他列表来存放任务的 TCB,只有一个就绪列表。

但是从本章开始,我们额外添加了延时列表,当任务要延时时,将任务从就绪列表移除,然后添加到延时列表,同时将任务在优先级位图表 uxTopReadyPriority 中对应的位清除。当清除任务在优先级位图表 uxTopReadyPriority 中对应的位时,与之前不同的是需要判断就绪列表 pxReadyTasksLists\[\] 在当前优先级下对应的链表的节点是否为 0,只有当该链表下没有任务时,才真正地将任务在优先级位图表 uxTopReadyPriority 中对应的位清零。

taskRESET_READY_PRIORITY() 函数的具体修改如下。

taskRESET_READY_PRIORITY() 函数

c 复制代码
1 #if 1 /* 本章的实现方法 */
2 #define taskRESET_READY_PRIORITY( uxPriority ) \
3 { \
4     if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 ) \
5     { \
6         portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) ); \
7     } \
8 }
9 #else/* 之前的实现方法 */
10    #define taskRESET_READY_PRIORITY( uxPriority ) \
11    { \
12        portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) ); \
13    }
14 #endif

main函数无改动


四、易错点

1.FreeRTOS 的任务延时列表并非简单的"按时间排队",双延时链表的存在是为了让溢出判断在插入时刻一次性完成,而不是在每一次 Tick 中断中做全表扫描。

2.xNextTaskUnblockTime更新:

  • 插入新延时任务时:如果 xTimeToWake < xNextTaskUnblockTime,则更新。

  • 唤醒到期任务后:通常从链表头部取新的最早唤醒时间;如果链表为空,则设为 portMAX_DELAY。

  • 链表切换时:taskSWITCH_DELAYED_LISTS 中调用 prvResetNextTaskUnblockTime() 重新取值。

相关推荐
2035去旅行13 分钟前
bootcmd 详细讲解
linux·嵌入式·arm·soc
蓝天居士23 分钟前
EtherCAT从入门到精通(7)
机器人·嵌入式·ethercat
楚山之石2 小时前
【JCO】mpMRI 先行替代 TURBT 初始分期:BladderPath 随机试验让 MIBC 患者提前 45 天得到正确治疗
人工智能·学习·mri·医学影像
橘色的喵2 小时前
coact:让嵌入式事件处理更可控
freertos·rt-thread·事件驱动·coact
蓝天居士3 小时前
EtherCAT从入门到精通(10)
机器人·嵌入式·ethercat
Peter-OK5 小时前
PostgreSQL学习笔记(入门-进阶-精通说明)
笔记·学习·postgresql
承渊政道5 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
不脱发的程序猿6 小时前
GD32串口数据收进内存之后DMA如何处理?
单片机·嵌入式硬件·dma·嵌入式·串口通信·gd32
橙橙笔记7 小时前
具身智能的学习 - 完成一个机械臂 第1节
python·学习·机械臂·具身智能