0%

前几章中都有涉及FreeRTOS的列表相关内容,在学习过程中,我们也能看出来列表与任务的状态(就绪、延时、堵塞)与任务调度息息相关,本文就针对任务延时进行列表运行机制的进一步学习

参考:FreeRTOS高级篇9—-FreeRTOS系统延时分析_研究是为了理解的博客-CSDN博客_pdms_to_ticks

相对延时

函数名 参数 返回值
vTaskDelay( const TickType_t xTicksToDelay ) 系统节拍个数

再次提醒一下,FreeRTOS中的延时参数都是以FreeRTOS自身的系统节拍为单位,系统节拍可以在在配置文件FreeRTOSConfig.h,改变宏configTICK_RATE_HZ的值;cubemx中的配置项也有相关的配置处,单位为Hz

函数体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void vTaskDelay( const TickType_t xTicksToDelay )
{
BaseType_t xAlreadyYielded = pdFALSE;

/* 如果延时时间为0,则不会将当前任务加入延时列表 */
if( xTicksToDelay > ( TickType_t ) 0U )
{
configASSERT( uxSchedulerSuspended == 0 );//禁用中断
vTaskSuspendAll();//将调度器挂起
{
/* 将当前任务从就绪列表中移除,并根据当前系统节拍计数器值计算唤醒时间,然后将任务加入延时列表 */
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
}
xAlreadyYielded = xTaskResumeAll();//恢复调度器运行
}

/* 强制执行一次上下文切换*/
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
}

其中,关键函数为prvAddCurrentTaskToDelayedList():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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

/* 将列表项移出当前列表(就绪列表),pxCurrentTCB作为全局变量指向当前任务TCB,uxListRemove()返回列表的成员个数 */
if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
/* The current task must be in a ready list, so there is no need to
check, and the port reset macro can be called directly. */
portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}

#if ( INCLUDE_vTaskSuspend == 1 )
{
if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
{
/*如果设置的延时参数等于portMAX_DELAY(0xffffffff),则视为堵塞而不是延时 */
vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* 计算唤醒时间 */
xTimeToWake = xConstTickCount + xTicksToWait;

/* 将当前任务TCB的状态列表项值设置为唤醒时间 */
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );

if( xTimeToWake < xConstTickCount )
{
/* Wake time has overflowed. Place this item in the overflow
list. */
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* 使用的是插入函数,因此延时列表中列表项会根据唤醒时间的大小进行排序 */
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );

/* If the task entering the blocked state was placed at the
head of the list of blocked tasks then xNextTaskUnblockTime
needs to be updated too. */
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
}


/* Avoid compiler warning when INCLUDE_vTaskSuspend is not 1. */
( void ) xCanBlockIndefinitely;
}
#endif /* INCLUDE_vTaskSuspend */
}

通过对上述代码解释了列表的具体使用方法:

  • 若要将当前任务延时或挂起,可以通过全局变量pxCurrentTCB找到该任务TCB,再操作该TCB的状态列表项

  • 当任务延时或挂起时,状态列表项的列表项值xItemValue会用于存放唤醒(延时结束)的时间,由于列表插入函数会自动根据xItemValue的值进行排序,所以唤醒时间越短的列表项会越排在列表前面

  • FreeRTOS使用了两个延时列表:xDelayedTaskList1和xDelayedTaskList2,并使用两个列表指针类型变量pxDelayedTaskList和pxOverflowDelayedTaskList分别指向上面的延时列表1和延时列表2

    pxOverflowDelayedTaskList用于解决计时溢出的问题:

    tasks.c中定义了很多局部静态变量,其中有一个变量xTickCount用于记录系统节拍中断次数(可以理解为记录当前时间),当xTicksToDelay达到4294967295后再增加,就会溢出变成0。 如果内核判断出xTickCount+ xTicksToDelay溢出,就将当前任务挂接到列表指针pxOverflowDelayedTaskList指向的列表中

相对延时的缺点

由于相对延时是在每次调用vTaskDelay之后才开启延时,如果调用延时的任务在运行过程中发生中断,那通过相对延时进行的周期任务就会因为该中断而被影响导致不能发生中断。

因此,为了能最大程度的使相对延时达到精准的效果,最好将使用相对延时的任务优先级设置为最高

绝对延时

函数名 参数 返回值
vTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement )
上一次记录的时间;
周期循环时间

函数体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
{
TickType_t xTimeToWake;
BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;

vTaskSuspendAll();
{
/* 保存系统节拍中断次数计数器 */
const TickType_t xConstTickCount = xTickCount;

/* 计算任务下次唤醒时间(以系统节拍中断次数表示) */
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;

/* *pxPreviousWakeTime中保存的是上次唤醒时间,唤醒后需要一定时间执行任务主体代码,如果上次唤醒时间大于当前时间,说明节拍计数器溢出了 */
if( xConstTickCount < *pxPreviousWakeTime )
{
/*只有当周期性延时时间大于任务主体代码执行时间,才会将任务挂接到延时列表.*/
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
}
else
{
/* 也都是保证周期性延时时间大于任务主体代码执行时间 */
if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
}

/* 更新唤醒时间,为下一次调用本函数做准备. */
*pxPreviousWakeTime = xTimeToWake;

if( xShouldDelay != pdFALSE )
{
/* 将本任务加入延时列表,注意阻塞时间并不是以当前时间为参考,因此减去了当前系统节拍中断计数器值*/
prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
}
}
xAlreadyYielded = xTaskResumeAll();

/* 强制执行一次上下文切换 */
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
}

写法和基本思路和相对延时区别不大,唯一需要注意的是。绝对延时之所以叫绝对延时,是因为其第一个参数pxPreviousWakeTime除开第一次使用时需获取当前系统时间外,每次调用绝对延时函数时pxPreviousWakeTime内保存的值为上一次调用时计算出的唤醒时间

也就是说,绝对延时的每一次延时的起点都是上一次延时的终点,这也就保证了任务主体即使被中断也不会影响到下一次执行任务主体(前提是任务主体+中断执行的时间小于延时长度,如果超过延时时间,需要重新获取pxPreviousWakeTime为当前系统的最新时间)

本文的学习参考了FreeRTOS高级篇2—-FreeRTOS任务创建分析_研究是为了理解的博客-CSDN博客_

任务TCB分析

基于FreeRTOS的列表学习 | 小董的BLOG (gitee.io)中对列表项分析时,在列表项的结构中有一个无类型指针:

1
void * pvOwner;                                     /*指向一个任务TCB*/

该指针用于指向任务的TCB(Task Control Block)。顾名思义,该结构作为一个用于控制任务的集合。它用于存储任务的状态信息,包括任务运行时的环境。每个任务都有自己的任务TCB。

结构解读

可以在task.c中找到定义TCB的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; //栈顶指针

#if ( portUSING_MPU_WRAPPERS == 1 ) //是否使用MPU
xMPU_SETTINGS xMPUSettings;
#endif

ListItem_t xStateListItem; //状态列表项
ListItem_t xEventListItem; //事件列表项
UBaseType_t uxPriority; //优先级
StackType_t *pxStack; //指向堆栈起始位置
char pcTaskName[ configMAX_TASK_NAME_LEN ]; //任务名字最大长度

#if ( portSTACK_GROWTH > 0 ) //判断堆栈生长方向,>0为向上生长
StackType_t *pxEndOfStack; //需要额外的一个指针来判断堆栈是否溢出
#endif

#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting;
#endif

#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber; //一个数值,每个任务都有一个唯一的值
UBaseType_t uxTaskNumber; //存储一个特定数值
#endif

#if ( configUSE_MUTEXES == 1 ) //是否使用互斥量
UBaseType_t uxBasePriority; //用于保存任务的基础的基础优先级
UBaseType_t uxMutexesHeld;
#endif

#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif

#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif

#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter; //记录任务在运行状态下执行的总时间
#endif

#if ( configUSE_NEWLIB_REENTRANT == 1 )
/* 为任务分配一个Newlibreent结构体变量。Newlib是一个C库函数,并非FreeRTOS维护,FreeRTOS也不对使用结果负责。如果用户使用Newlib,必须熟知Newlib的细节*/
struct _reent xNewLib_reent;
#endif

#if( configUSE_TASK_NOTIFICATIONS == 1 ) //与任务通知相关
volatile uint32_t ulNotifiedValue;
volatile uint8_t ucNotifyState;
#endif

#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
uint8_t ucStaticallyAllocated;
#endif

#if( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif

} tskTCB;
typedef tskTCB TCB_t;

状态列表项与事件列表项

从TCB的结构中可以发现两个定义的列表项,分别是状态列表项和事件列表项,在上一章也略微提到过,列表与列表项用于调度任务、跟踪任务状态。例如:在task.c中,定义了一些静态列表变量,其中有就绪、阻塞、挂起列表,例如当某个任务处于就绪态时,调度器就将这个任务TCB的状态列表项挂接到就绪列表。事件列表项也与之类似,当队列满的情况下,任务因入队操作而阻塞时,就会将事件列表项挂接到队列的等待入队列表上。具体实现方法和规则会在之后学习任务调度时讲到。

uxTCBNumber与uxTaskNumber

这两个变量主要用于调试与可视化追踪。仅当宏configUSE_TRACE_FACILITY(位于FreeRTOSConfig.h中)为1时有效。变量uxTCBNumber在创建任务时由内核自动分配,每个任务对应一个值,后续不再改变。变量uxTaskNumber由API函数vTaskSetTaskNumber()来设置的,数值由函数参数指定。

其定义在task.c中如下,参数为对应任务的引用句柄和要设置的数值

1
2
3
4
5
6
7
8
9
10
void vTaskSetTaskNumber( TaskHandle_t xTask, const UBaseType_t uxHandle )
{
TCB_t *pxTCB;

if( xTask != NULL )
{
pxTCB = ( TCB_t * ) xTask;
pxTCB->uxTaskNumber = uxHandle;
}
}

任务创建的内部流程

假设当前创建了一个任务如下:

1
2
static TaskHandle_t xHandle;
xTaskCreate(vTask_A,”Task A”,120,NULL,1,&xHandle);

当这个语句执行后,任务A被创建并加入就绪任务列表,我们这章的主要目的,就是看看这个语句在执行过程中,发生了什么事情。

1、创建任务堆栈和TCB所用空间

注:原博客中也就是接下来讲到的函数在我使用的基于STM32的FreeRTOS中并不存在,但是可以在对xTaskCreate的定义下找到基本类似的操作(可能是版本原因),不过其并不是用函数封装的,但是功能基本一致。为了方便学习就使用原博客中的函数来理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static TCB_t *prvAllocateTCBAndStack( const uint16_t usStackDepth, StackType_t * const puxStackBuffer, TCB_t * const pxTaskBuffer )
{
TCB_t *pxNewTCB;
StackType_t *pxStack;

/* 分配堆栈空间*/
pxStack = ( StackType_t * ) pvPortMallocAligned( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ), puxStackBuffer );
if( pxStack != NULL )
{
/* 分配TCB空间 */
pxNewTCB = ( TCB_t * ) pvPortMallocAligned( sizeof( TCB_t ), pxTaskBuffer );

if( pxNewTCB != NULL )
{
/* 将堆栈起始位置存入TCB*/
pxNewTCB->pxStack = pxStack;
}
else
{
/* 如果TCB分配失败,释放之前申请的堆栈空间 */
if( puxStackBuffer == NULL )
{
vPortFree( pxStack );
}
}
}
else
{
pxNewTCB = NULL;
}

if( pxNewTCB != NULL )
{
/* 如果需要,使用固定值填充堆栈 */
#if( ( configCHECK_FOR_STACK_OVERFLOW> 1 ) || ( configUSE_TRACE_FACILITY == 1 ) || ( INCLUDE_uxTaskGetStackHighWaterMark== 1 ) )
{
/* 仅用于调试 */
( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) usStackDepth * sizeof( StackType_t ) );
}
#endif
}

return pxNewTCB;
}

基本就是一些分配内存的逻辑与操作,还是比较好理解的。

2、初始化TCB必要的字段

这些操作在我的版本中都是用预编译判断执行的,没有使用函数封装,要找到对应操作实在是有点眼花,姑且看到大部分操作都是在task.c中对xTaskCreate的定义的后面,这里为了方便理解还是使用原博主的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
static void prvInitialiseTCBVariables( 
TCB_t * const pxTCB,
const char * const pcName,
UBaseType_t uxPriority,
const MemoryRegion_t * const xRegions,
const uint16_t usStackDepth
)
{
UBaseType_t x;

/* 将任务描述存入TCB */
for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
{
pxTCB->pcTaskName[ x ] = pcName[ x ];
if( pcName[ x ] == 0x00 )
{
break;
}
}
/* 确保字符串有结束 */
pxTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';

/* 调整优先级,宏configMAX_PRIORITIES的值在FreeRTOSConfig.h中设置 */
if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES )
{
uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;
}

pxTCB->uxPriority = uxPriority;
#if ( configUSE_MUTEXES == 1 ) /*使用互斥量*/
{
pxTCB->uxBasePriority = uxPriority;
pxTCB->uxMutexesHeld = 0;
}
#endif /* configUSE_MUTEXES */

/*初始化列表项*/
vListInitialiseItem( &( pxTCB->xStateListItem ) );
vListInitialiseItem( &( pxTCB->xEventListItem ) );

/* 设置列表项xStateListItem的成员pvOwner指向当前任务控制块 */
listSET_LIST_ITEM_OWNER( &( pxTCB->xStateListItem ), pxTCB );

/* 设置列表项xEventListItem的成员xItemValue*/
listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );
/* 设置列表项xEventListItem的成员pvOwner指向当前任务控制块 */
listSET_LIST_ITEM_OWNER( &( pxTCB->xEventListItem ), pxTCB );

#if ( portCRITICAL_NESTING_IN_TCB ==1 ) /*使能临界区嵌套功能*/
{
pxTCB->uxCriticalNesting = ( UBaseType_t ) 0U;
}
#endif /* portCRITICAL_NESTING_IN_TCB */

#if ( configUSE_APPLICATION_TASK_TAG == 1 ) /*使能任务标签功能*/
{
pxTCB->pxTaskTag = NULL;
}
#endif /* configUSE_APPLICATION_TASK_TAG */

#if ( configGENERATE_RUN_TIME_STATS == 1 ) /*使能事件统计功能*/
{
pxTCB->ulRunTimeCounter = 0UL;
}
#endif /* configGENERATE_RUN_TIME_STATS */

#if ( portUSING_MPU_WRAPPERS == 1 ) /*使用MPU功能*/
{
vPortStoreTaskMPUSettings( &( pxTCB->xMPUSettings ), xRegions, pxTCB->pxStack, usStackDepth );
}
#else /* portUSING_MPU_WRAPPERS */
{
( void ) xRegions;
( void ) usStackDepth;
}
#endif /* portUSING_MPU_WRAPPERS */

#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS != 0 )/*使能线程本地存储指针*/
{
for( x = 0; x < ( UBaseType_t )configNUM_THREAD_LOCAL_STORAGE_POINTERS; x++ )
{
pxTCB->pvThreadLocalStoragePointers[ x ] = NULL;
}
}
#endif

#if ( configUSE_TASK_NOTIFICATIONS == 1 ) /*使能任务通知功能*/
{
pxTCB->ulNotifiedValue = 0;
pxTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
}
#endif

#if ( configUSE_NEWLIB_REENTRANT == 1 ) /*使用Newlib*/
{
_REENT_INIT_PTR( ( &( pxTCB->xNewLib_reent ) ) );
}
#endif

#if( INCLUDE_xTaskAbortDelay == 1 )
{
pxTCB->ucDelayAborted = pdFALSE;
}
#endif
}

其实大部分也就是对应TCB结构体中各个成员的初始化,其中有几点需要着重讲一下:

对两个列表项的初始化:

  • 对状态列表项的初始化

    使成员pvOwner指向对应的任务TCB,其余成员不对其操作

  • 对事件列表项的初始化

    之前讲到过事件列表项值会存储任务的优先级相关信息,其存储方式比较特别:

    1
    2
    3
    4
    /* 设置列表项xEventListItem的成员xItemValue*/
    listSET_LIST_ITEM_VALUE(
    &( pxTCB->xEventListItem ),
    ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );

    可以看出,事件列表项的xItemValue中存储的值为用户设置的最大优先级-当前任务的优先级,也就是说,ItemValue的值越小,当前任务的优先级越高

    再回想一下列表项插入列表的函数vListInsert():

    可以发现xItemValue越小的值越是排在列表的前面,这也就解释了这样做的原因,其实也就是优先级越高的任务越在列表前面

任务通知

1
2
3
4
#if ( configUSE_TASK_NOTIFICATIONS == 1 )   /*使能任务通知功能*/
{
pxTCB->ulNotifiedValue = 0;
pxTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;

FreeRTOS任务间的交互方法 | 小董的BLOG (gitee.io)中将任务通知的流程时也使用到了使用该值的函数:

1
2
3
4
BaseType_t xTaskNotifyWait( uint32_tulBits ToClearOnEntry,
uint32_tulBits ToClearOnExit,
uint32_t* pulNotificationValue,
TickType_t xTicksToWait );

其实pulNotificationValue就是拷贝自TCB中的ulNotifiedValue值,该值在任务通知发送函数将ulValue的值拷贝进ulNotifiedValue,在接收时用户可以通过pulNotificationValue判断任务通知发送的值。在初始化时,其为0,因此发送的任务值不能为0

3、初始化任务堆栈

调用函数pxPortInitialiseStack()初始化任务堆栈,并将最新的栈顶指针赋值给任务TCB的pxTopOfStack字段。

具体与寄存器的调用有关,目前不深读。

4、进入临界区

调用taskENTER_CRITICAL()进入临界区,进入临界区后的代码段将不能被打断,比如有的外设的初始化需要严格的时序,初始化过程中不能被打断。FreeRTOS 在进入临界区代码的时候需要关闭中断,当处理完临界区代码以后再打开中断。

FreeRTOS 与 临界区代 码 保 护 有 关 的 函 数 有 4 个 : taskENTER_CRITICAL() 、taskEXIT_CRITICAL() 、taskENTER_CRITICAL_FROM_ISR() 和taskEXIT_CRITICAL_FROM_ISR(),这四个函数其实是宏定义,在 task.h 文件中有定义。 这四个函数的区别是前两个是任务级的临界段代码保护,后两个是中断级的临界段代码保护。

5、改变跟踪任务的变量

在task.c中定义了一些静态私有变量用于跟踪任务的数量、状态等信息,每当有任务发生变化时这些对应的变量也会发生变化。

其中变量uxCurrentNumberOfTasks表示当前任务的总数量,每创建一个任务,这个变量都会增加1。

6、第一次运行的必要初始化

如果当前创建的任务为第一个任务,则会调用函数prvInitialiseTaskLists()对列表进行初始化,在这之前,task.c中定义了这些静态类型的列表变量:(List_t类型变量在上一章已经讲过)

1
2
3
4
5
6
7
8
9
10
11
12
PRIVILEGED_DATAstatic List_t pxReadyTasksLists[ configMAX_PRIORITIES ];/*按照优先级排序的就绪态任务*/
PRIVILEGED_DATAstatic List_t xDelayedTaskList1; /*延时的任务 */
PRIVILEGED_DATAstatic List_t xDelayedTaskList2; /*延时的任务 */
PRIVILEGED_DATAstatic List_t xPendingReadyList; /*任务已就绪,但调度器被挂起 */

#if (INCLUDE_vTaskDelete == 1 )
PRIVILEGED_DATA static List_t xTasksWaitingTermination;/*任务已经被删除,但内存尚未释放*/
#endif

#if (INCLUDE_vTaskSuspend == 1 )
PRIVILEGED_DATA static List_t xSuspendedTaskList; /*当前挂起的任务*/
#endif

初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static void prvInitialiseTaskLists( void )
{
UBaseType_tuxPriority;

for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
{
vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
}

vListInitialise( &xDelayedTaskList1 );
vListInitialise( &xDelayedTaskList2 );
vListInitialise( &xPendingReadyList );

#if ( INCLUDE_vTaskDelete == 1 )
{
vListInitialise( &xTasksWaitingTermination );
}
#endif /* INCLUDE_vTaskDelete */

#if ( INCLUDE_vTaskSuspend == 1 )
{
vListInitialise( &xSuspendedTaskList );
}
#endif /* INCLUDE_vTaskSuspend */

/* Start with pxDelayedTaskList using list1 and the pxOverflowDelayedTaskListusing list2. */
pxDelayedTaskList = &xDelayedTaskList1;
pxOverflowDelayedTaskList = &xDelayedTaskList2;
}

其中主要调用的函数vListInitialise()在讲列表时已经讲过,其作用就是初始化列表成员的一些指针的初始指向,为后续接入列表项做准备。

7、更新任务TCB指针

tasks.c中定义了一个任务TCB指针型变量:

  • PRIVILEGED_DATA TCB_t * volatile pxCurrentTCB= NULL;

    这是一个全局变量,在tasks.c中只定义了这一个全局变量。这个变量用来指向当前正在运行的任务TCB。

当调度器还没有开启时(程序刚开始运行时,可能会先创建几个任务,之后才会启动调度器),如果新创建的任务优先级大于pxCurrentTCB指向的任务优先级,则设置pxCurrentTCB指向当前新创建的任务TCB。

1
2
3
4
5
6
7
8
9
10
11
if( xSchedulerRunning == pdFALSE )
{
if( pxCurrentTCB->uxPriority <= uxPriority )
{
pxCurrentTCB = pxNewTCB;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}

这项操作可以确保pxCurrentTCB始终指向优先级最高的就绪任务。

8、将新创建的任务加入就绪列表数组

新创建的任务会被直接加入到就绪列表中,因为它不会一被创建就被阻塞或挂起。观察在task.c中定义的几个静态类型的列表可以发现就绪列表是一个结构体数组:

1
2
PRIVILEGED_DATAstatic List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
/*按照优先级排序的就绪态任务*/

因此当前新创建的任务会根据该任务TCB中存储的优先级决定存入的列表下标。比如我们新创建的任务优先级为1,则这个任务被加入到列表pxReadyTasksLists[1]中。

并且由于同一个列表中任务的优先级是相同的,因此每次加入列表时会直接将当前任务放入列表尾:

1
2
3
4
#define prvAddTaskToReadyList( pxTCB )                        
taskRECORD_READY_PRIORITY( ( pxTCB)->uxPriority );
vListInsertEnd( &( pxReadyTasksLists[ (pxTCB )->uxPriority ] ),
&( ( pxTCB )->xStateListItem ) );

调用一个宏将任务放入列表。可以看到,加入列表的为任务的状态列表项,但最终调用宏的参数直接是任务TCB,因此可能用户不用关心何时操作状态列表项,何时操作事件列表项?(我的猜测,因为目前还不太清楚二者的明显区别)

9、退出临界区

到此,一个任务的创建基本结束,也就可以退出临界区了(本身进入临界区的原因就是让任务的创建过程不被打断)

10、新任务进入调度

如果调度器已开启,新任务便会根据其优先级由调度器进行相关调度。

任务创建过程总结

  • 为当前任务分配内存创建堆栈,为任务TCB创建空间
  • 初始化TCB,主要是使TCB成员中的两个列表项指向当前TCB和改变事件列表项的列表项值
  • 初始化任务的堆栈
  • 进入临界区,之后的进程不能被打断
  • 改变一些存储任务信息的变量(主要是全局的信息,并不是指某一个任务的信息。比如任务的总数量等)
  • 如果当前为第一个任务,则初始化各列表
  • 更新当前的任务TCB指针(该指针专用于指向当前TCB)
  • 将新任务加入就绪列表数组中,根据优先级决定加入的数组下标
  • 退出临界区
  • 新任务也加入调度过程中

本文参考了FreeRTOS高级篇1—-FreeRTOS列表和列表项研究是为了理解的博客-CSDN博客freertos list进行学习

列表与列表项

FreeRTOS内核为了调度任务之间的关系,大量地使用了列表和列表项的数据结果来跟踪任务当前的状态。例如,处于挂起、延时、就绪的任务都会被挂接到各自的列表中

FreeRTOS中使用指针实现列表的功能,类似于链式列表。一个列表下有多个列表项,且每个列表项中都有一个指针指向列表。

需要注意的是,列表和之前讲到任务交互时使用的队列是两个东西!

FreeRTOS列表项的构成

全功能版列表项

我们可以在list.h中找到freertos对列表项的定义

1
2
3
4
5
6
7
8
9
10
11
struct xLIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /*用于检测列表项数据是否完整*/
configLIST_VOLATILE TickType_t xItemValue; /*列表项值*/
struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*指向列表中下一个列表项*/
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*指向列表中上一个列表项*/
void * pvOwner; /*指向一个任务TCB*/
void * configLIST_VOLATILE pvContainer; /*指向包含该列表项的列表 */
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE /*用于检测列表项数据是否完整*/
};
typedef struct xLIST_ITEM ListItem_t;
  • listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE 和listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE 这两个宏定义用于检查列表项的数据是否完整

    当使能projdefs.h中的宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES时,这两个宏会被替换成两个已知的数值用于检测完整性。

  • xItemValue为列表项值,通常是一个被跟踪的任务优先级或是一个调度事件的计数器值。

    例如在队列的使用中,如果一个任务因为在等待从队列中读取数据而进入阻塞态,则该任务的事件列表项中的列表项值会用于保存该任务的优先级有关信息;该任务的状态列表项的列表项值会用于保存阻塞时间的有关信息(之前讲过,当有多个任务等待读取队列时,读取顺序与优先级和阻塞时间相关)。至于什么是事件列表项和状态列表项,之后的文章会讲到。

    由于列表项值应是随时都可能更新的,因此该变量被configLIST_VOLATILE修饰,该宏被映射为C的关键字volatile

  • pxNext和pxPrevious为两个结构体指针,用于指向上下列表项,实现类似于双向链表的结构,对结构体指针还不太懂的话可以参考数据结构_链表 | 小董的BLOG (gitee.io)中的一些解释。

  • pvOwner用于指向一个任务的TCB(Task Control Block),之后的文章会讲到。

  • pvContainer用于指向包含该列表项的列表

迷你版列表项

list.h中还有一个迷你版列表项的定义:

1
2
3
4
5
6
7
8
struct xMINI_LIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE //检测完整性
configLIST_VOLATILE TickType_t xItemValue; //列表项值
struct xLIST_ITEM * configLIST_VOLATILE pxNext; //指向下一列表项
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; //指向前一列表项
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;

迷你版列表项的作用下一节讲列表时会讲到。

初始化列表项

列表项的初始比较简单,只要确保列表项不在任何列表中即可。

1
2
3
4
5
6
7
8
void vListInitialiseItem( ListItem_t * const pxItem )
{
pxItem->pvContainer = NULL;

/*设置为已知值,用于检测列表项数据是否完整*/
listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE(pxItem );
listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE(pxItem );
}

FreeRTOS的列表构成

列表的定义

1
2
3
4
5
6
7
8
typedef struct xLIST
{
listFIRST_LIST_INTEGRITY_CHECK_VALUE //检测完整性
configLIST_VOLATILE UBaseType_t uxNumberOfItems;
ListItem_t * configLIST_VOLATILE pxIndex;
MiniListItem_t xListEnd;
listSECOND_LIST_INTEGRITY_CHECK_VALUE //检测完整性
} List_t;
  • uxNumberOfItems用于表示该列表中挂接的列表项数目,0表示列表为空。

  • pxIndex为一个类型为完全体列表项的结构体指针,用于遍历列表项。

  • 列表项xListEnd用于标记列表结束,因为该列表项只用于标记列表的结束,因此不需要完全体列表项的所有功能,因此使用了迷你版列表项的类型。

    xListEnd.xItemValue被初始化为一个常数,其值与硬件架构相关,为0xFFFF(16位架构)或0xFFFFFFFF(32位架构)。

列表的初始化

可以在list.c中找到初始化函数vListInitialise()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void vListInitialise( List_t * const pxList )
{
/*列表索引指向列表项xListEnd*/
pxList->pxIndex = ( ListItem_t * )&( pxList->xListEnd );
/* 设置为最大可能值为0xffff */
pxList->xListEnd.xItemValue =portMAX_DELAY;

/* 列表项xListEnd的pxNext和pxPrevious指针指向了它自己 */
pxList->xListEnd.pxNext = (ListItem_t * ) &( pxList->xListEnd );
pxList->xListEnd.pxPrevious= ( ListItem_t * ) &( pxList->xListEnd );
pxList->uxNumberOfItems = ( UBaseType_t) 0U;

/* 设置为已知值,用于检测列表数据是否完整*/
listSET_LIST_INTEGRITY_CHECK_1_VALUE(pxList );
listSET_LIST_INTEGRITY_CHECK_2_VALUE(pxList );
}
  • 如果宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES设置为1,则使能列表项数据完整性检查,则宏listSET_LIST_INTEGRITY_CHECK_1_VALUE()和listSET_LIST_INTEGRITY_CHECK_2_VALUE被一个已知值代替,默认为0x5a5a(16位架构)或者0x5a5a5a5a(32位架构)。

  • 按照上述初始化后就应该是下图的情况:

    此时pxIndex、pxList->xListEnd.pxNext、pxList->xListEnd.pxPrevious都指向了 pxList->xListEnd(也可以说是xListEnd这个结构体的首元素)

将列表项插入列表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t *pxIterator; //函数内定义一个列表项指针,通过列表项值的比较,找到当前输入列表项需插入的位置
const TickType_t xValueOfInsertion = pxNewListItem->xItemValue; //定义一个变量存放当前输入列表项的列表项值

listTEST_LIST_INTEGRITY( pxList ); //检测完整性
listTEST_LIST_ITEM_INTEGRITY( pxNewListItem ); //检测完整性

/*开始为输入列表项寻找合适的插入位置*/
if( xValueOfInsertion == portMAX_DELAY )
{
pxIterator = pxList->xListEnd.pxPrevious;
}
else
{
for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd );
pxIterator->pxNext->xItemValue <= xValueOfInsertion;
pxIterator = pxIterator->pxNext )
{
/* There is nothing to do here, just iterating to the wanted
insertion position. */
}
}
/******************************/

/*此时pxIterator为当前pxNewListItem要插入的位置*/
pxNewListItem->pxNext = pxIterator->pxNext;
pxNewListItem->pxNext->pxPrevious = pxNewListItem;
pxNewListItem->pxPrevious = pxIterator;
pxIterator->pxNext = pxNewListItem;

/*将列表项接入列表,列表项数目+1*/
pxNewListItem->pvContainer = ( void * ) pxList;
( pxList->uxNumberOfItems )++;
}

这个函数中比较难以理解的地方就是关于寻找列表项插入位置的判断和循环部分,接下来将一步一步讲解:

当列表内无列表项时(当前插入的列表项为第一个列表项)

假设该列表项值为50

if判断

不满足条件,跳过

for循环

  • 初始化pxIterator = ( ListItem_t * ) &( pxList->xListEnd );后,此时情况为:

    此时pxIterator成员与pxList->xListEnd的成员一一对应

  • 条件判断pxIterator->pxNext->xItemValue <= xValueOfInsertion;

    这里需要注意:列表初始化时,xListEnd的成员pxNext,pxPrevious是指向xListEnd的

    因此pxIterator->pxNext->xItemValue的值就是0xFFFF,是大于50的,因此不满足条件,跳出循环

赋值

  • pxNewListItem->pxNext = pxIterator->pxNext;

    pxIterator->pxNext指向pxList->xListEnd.pxnext,而pxList->xListEnd.pxnext又指向本身xListEnd;因此,最终pxNewListItem->pxNext指向了pxList->xListEnd

  • pxNewListItem->pxNext->pxPrevious = pxNewListItem;

    上一步中pxNewListItem->pxNext指向了pxList->xListEnd,因此pxNewListItem->pxNext->pxPrevious等同于pxList->xListEnd.pxPrevious,而pxList->xListEnd.pxPrevious之前是指向pxList->xListEnd本身的,因此这一步最终的结果相当于将pxList->xListEnd.pxPrevious改变指向,指向pxNewListItem

  • pxNewListItem->pxPrevious = pxIterator;

    由于pxIterator本身就是指向pxList->xListEnd的,因此这一步相当于:

    pxNewListItem->pxPrevious = pxList->xListEnd

  • pxIterator->pxNext = pxNewListItem;

    pxIterator->pxNext指向pxList->xListEnd.pxnext,此时pxList->xListEnd.pxnext本来是指向自己(pxList->xListEnd)的,因此这一步最终是:

    将pxList->xListEnd.pxnext改变指向,指向pxNewListItem

  • 综上,最终的结果为:

    上图省略了pvContainer和pxIndex的指向和uxNumberOfItems的变化

第二次,当列表中已有列表项时,且该列表项值大于上一个

假设第二个列表项值为60(第一个为50)

if判断

不满足条件,跳过

for循环

  • 初始化pxIterator = ( ListItem_t * ) &( pxList->xListEnd );后,由于在上一个列表项插入后,pxList.xListEnd中的pxNext和pxPrevious已经改变了指向,因此当前情况为:

  • 条件判断pxIterator->pxNext->xItemValue <= xValueOfInsertion;

    由上图可得知,此时pxIterator->pxNext->xItemValue为50,小于60,条件成立

  • 执行pxIterator = pxIterator->pxNext

    由于pxIterator->pxNext指向pxNewListItem(1),因此当前情况如下:

  • 再次进行条件判断pxIterator->pxNext->xItemValue <= xValueOfInsertion;

    此时pxIterator->pxNext->xItemValue为0xffff,不满足条件,循环结束。

综上,通过该for循环,可以根据每个列表项值的大小进行列表中的位置排序

赋值

这里的所有pxNewListItem在未特别说明的情况下都是指pxNewListItem(2)

  • pxNewListItem->pxNext = pxIterator->pxNext;

    同第一次插入列表,最终pxNewListItem->pxNext指向了pxList->xListEnd

  • pxNewListItem->pxNext->pxPrevious = pxNewListItem;

    同第一次插入列表,将pxList->xListEnd.pxPrevious改变指向,指向pxNewListItem

  • pxNewListItem->pxPrevious = pxIterator;

    pxIterator在for循环中已经指向了pxNewListItem(1),因此这里:

    pxNewListItem->pxPrevious指向pxNewListItem(1)

  • pxIterator->pxNext = pxNewListItem;

    pxIterator->pxNext指向pxNewListItem(1)->pxnext,而pxNewListItem(1)->next最终指向了pxlist.xListEnd,因此这里相当于:

    改变pxNewListItem(1)->pxnext的指向,使其指向pxNewListItem(2)

综上,当前结果为:

有点乱,也可以参考下图,一样的

不难看出,赋值的最后两步将各个列表项相互链接

第二次,当列表中已有列表项时,但该列表项值小于上一个

假设第二个列表值为30,小于第一个列表值50

if判断

不满足条件,跳过

for循环

  • 初始化pxIterator = ( ListItem_t * ) &( pxList->xListEnd );后,由于在上一个列表项插入后,pxList.xListEnd中的pxNext和pxPrevious已经改变了指向,因此当前情况为:

  • 条件判断pxIterator->pxNext->xItemValue <= xValueOfInsertion;

    由上图可得知,此时pxIterator->pxNext->xItemValue为50,大于30,条件不成立,循环结束

赋值

这里的所有pxNewListItem在未特别说明的情况下都是指pxNewListItem(2)

  • pxNewListItem->pxNext = pxIterator->pxNext;

    不同第一次插入列表!此时pxNewListItem->pxNext最终指向了xNewListItem(1)

  • pxNewListItem->pxNext->pxPrevious = pxNewListItem;

    不同第一次插入列表!此时pxNewListItem->pxNext->pxPrevious 即是:

    pxNewListItem(1)->xPrevious指向了pxNewListItem(2)

  • pxNewListItem->pxPrevious = pxIterator;

    pxIterator是直接指向pxList->xListEnd的,因此:

    pxNewListItem->pxPrevious 直接指向pxList->xListEnd

  • pxIterator->pxNext = pxNewListItem;

    在执行这一步之前,情况如下:

    这里我产生了一个疑问,pxIterator->pxNext是指向pxNewListItem(1),那让pxNewListItem(1)=pxNewListItem是什么意思呢?让上一个列表项指向当前列表项?这明显是不符合列表逻辑的。

    后来在找寻问题的过程中,才发现自己又犯了老错误,pxNewListItem(1)其实也是一个指针变量,他是直接指向列表项1的内存单元的,因此pxIterator->pxNext是直接指向了列表项1的内存单元,因此改变pxIterator->pxNext是直接改变了这个指针的指向,也就是说结果是:

    将pxList->xListEnd.pxNext改变指向,使其指向pxNewListItem(2)

综上,最后结果为:

对比两种情况

第二个列表项值大于第一个的情况:

对比一下,可以看出:

  • pxList->xListEnd.pxNext总是指向列表项值最小的列表项
  • pxList->xListEnd.pxPrevious总是指向列表项值最大的列表项
  • 列表项值小的列表项的pxNext总是指向列表项值大的列表项
  • 列表项值大的列表项的PxPrevious总是指向列表项值小的列表项
  • 首列表项的pxPrevious和尾列表项的pxNext都指向pxList->xListEnd

根据上述的规律,就可以清晰地明白FreeRTOS对列表的插入方式了:

因此,若要访问列表项,要通过pxList->xListEnd.pxNext访问手列表项,通过pxList->xListEnd.pxPrevious访问尾列表项。

实现插入的方法看了之后感觉真的很巧妙,我感觉也只能停留在理解的阶段,要自己写出来感觉难度还是很大。。。但是规律还是很好记住的😋

直接将列表项插入列表尾

原理跟上面的是一样的,如果能理解插入的方法,相信这个理解起来就很ez了,就不多说了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t* const pxIndex = pxList->pxIndex;

/*检查列表和列表项数据的完整性,仅当configASSERT()定义时有效。*/
listTEST_LIST_INTEGRITY( pxList );
listTEST_LIST_ITEM_INTEGRITY(pxNewListItem );

/*向列表中插入新的列表项*/
pxNewListItem->pxNext = pxIndex;
pxNewListItem->pxPrevious =pxIndex->pxPrevious;

mtCOVERAGE_TEST_DELAY();

pxIndex->pxPrevious->pxNext =pxNewListItem;
pxIndex->pxPrevious = pxNewListItem;

pxNewListItem->pvContainer = ( void* ) pxList;

( pxList->uxNumberOfItems )++;
}

总结

在FreeRTOS中,任务的调度与列表息息相关,因此要想学好任务调度的原理,列表是第一大难关!