FreeRTOS task 源码解析

[toc]


FreeRTOS 本质上就是有很多的 List 组成,所以学习之前最好要对 FreeRTOS 中的链表要有所了解,可以参考:FreeRTOS 列表 List 源码解析

源码都在 task.c 中

一、基本结构和变量

1、TCB_t

首先来看一下一个任务的结构:

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
typedef struct tskTaskControlBlock    
{
volatile StackType_t * pxTopOfStack; /*< 指向任务堆栈中最后放置的项目位置。这必须是TCB结构中的第一个成员,具体原因在后面讲 PendSV 中断的时候会提到 */

/* MPU 相关,暂时不用管 */
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings; /*< The MPU settings are defined as part of the port layer. THIS MUST BE THE SECOND MEMBER OF THE TCB STRUCT. */
#endif

ListItem_t xStateListItem; /*< 表示该任务的状态(就绪、阻塞、挂起),不同的状态会挂接在不同的状态链表下 */
ListItem_t xEventListItem; /*< 用于从事件列表中引用任务,会挂接到不同事件链表下 */
UBaseType_t uxPriority; /*< 任务的优先级。0 是最低优先级 */
StackType_t * pxStack; /*< 指向堆栈起始位置,这只是单纯的一个分配空间的地址,可以用来检测堆栈是否溢出 */
char pcTaskName[ configMAX_TASK_NAME_LEN ]; /*< 任务名,仅用于调试(仅允许用于字符串和单个字符) */

/* 指向栈尾,可以用来检测堆栈是否溢出 */
#if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
StackType_t * pxEndOfStack; /*< Points to the highest valid address for the stack. */
#endif

/* 记录临界段的嵌套层数 */
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting; /*< Holds the critical section nesting depth for ports that do not maintain their own count in the port layer. */
#endif

/* 跟踪调试用的变量 */
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber; /*< 存储一个每次创建TCB时递增的数字。它允许调试器确定何时删除一个任务并重新创建它 */
UBaseType_t uxTaskNumber; /*< 存储一个专门供第三方跟踪代码使用的数字 */
#endif

/* 任务优先级被临时提高时,保存任务原本的优先级 */
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; /*< 最后分配给任务的优先级 - 用于优先级继承机制 */
UBaseType_t uxMutexesHeld;
#endif

/* 任务的一个标签值,可以由用户自定义它的意义,例如可以传入一个函数指针可以用来做 Hook 函数调用 */
#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

/* 支持NEWLIB 的一个变量 */
#if ( configUSE_NEWLIB_REENTRANT == 1 )

/* 分配一个特定于此任务的 Newlib reent 结构。
* 注意,Newlib 的支持是应广大用户需求而添加的,但并未由 FreeRTOS 的维护者本人使用。
* FreeRTOS 对于由此产生的 Newlib 操作不承担责任。用户必须熟悉 Newlib,并提供全系统所需的相关实现。
* 请注意(在撰写时),当前的 Newlib 设计实现了一个需要锁的全系统 malloc()。 */
struct _reent xNewLib_reent;
#endif

/* 任务通知功能需要用到的变量 */
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; /* 任务通知的值 */
volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; /* 任务通知的状态 */
#endif

/* 用来标记这个任务的栈是不是静态分配的 */
#if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
uint8_t ucStaticallyAllocated; /*< 如果任务是静态分配的,则设置为 pdTRUE,以确保不会尝试释放该内存 */
#endif

/* 延时是否被打断 */
#if ( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif

/* 错误标识 */
#if ( configUSE_POSIX_ERRNO == 1 )
int iTaskErrno;
#endif
} tskTCB;

typedef tskTCB TCB_t;

任务结构体被声明为 TCB_t,也就是 Task Control Block(任务控制块),熟悉这个任务控制块的结构有助于我们对后续源码的理解。

2、状态链表

FreeRTOS 中的任务一共有四种状态分别是运行状态(Running State)、就绪状态(Ready State)、阻塞状态(Blocked State)、挂起状态(Suspended State),其含义可以简单理解为:

  • 运行状态:正在执行的任务。
  • 就绪状态:等待获得执行权的任务。
  • 阻塞状态:直到某些条件达成才会重新进入就绪态等待获得执行权,否则不会执行的任务。
  • 挂起状态:除非被主动恢复,否则永远不会执行。

Task状态转换图

  • 这四种状态分别对应着 pxCurrentTCBpxReadyTasksListspxDelayedTaskListxSuspendedTaskList 这四个变量。除运行状态外,任务处于其它状态时,都是通过将任务 TCB 中的 xStateListItem 挂到相应的链表下来表示的。

因此,FreeRTOS 中任务状态的切换本质上就是把任务项挂接到对应的链表下。

从源码中可以看到 pxReadyTasksListspxDelayedTaskListxSuspendedTaskList 这四个变量的类型是链表数组,每一个下标就表示一个优先级,这样就把同一优先级的多个任务放在了一起,不同优先级是由不同的链表项连接。

进行任务切换的时候,调度器首先选择最高优先级的任务进行切换,而且具有相同优先级的任务会轮流执行。高优先级的任务未执行完低优先级的任务无法执行,因为低优先级无法抢占高优先级。

2.1 pxCurrentTCB

1
2
/* 始终指向当前运行的任务 */
PRIVILEGED_DATA TCB_t * volatile pxCurrentTCB = NULL;

当前运行的任务只可能有一个,因此 pxCurrentTCB 只是单个 TCB_t 指针。

2.2 pxReadyTasksLists

1
2
3
4
#define configMAX_PRIORITIES ( 10 )

/* 由链表组成的数组,每一个成员都是由处于就绪态而又有着相同任务优先级的任务组成的的链表. */
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

除此之外,还有一个变量 uxTopReadyPriority。其的定义如下

1
2
3
#define tskIDLE_PRIORITY ( ( UBaseType_t ) 0U )

PRIVILEGED_DATA static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;

uxTopReadyPriority 存储的是有任务挂接的最高优先级。pxReadyTasksListspxCurrentTCBuxTopReadyPriority 三者之间的关系可由以下的图来表示:

Task 状态转换图

当使用时间片时,pxCurrentTCB 会在有任务挂接的最高优先级链表中遍历,以实现它们对处理器资源的分时共享。

2.3 pxDelayedTaskList

延时链表的作用不仅是用来处理任务的延时,任务的阻塞也是由它进行实现的。

1
2
3
4
PRIVILEGED_DATA static List_t xDelayedTaskList1;                         /*< 延时任务队列 */
PRIVILEGED_DATA static List_t xDelayedTaskList2; /*< 延时任务队列 (使用两个列表:一个用于已溢出当前tick计数的延迟 */
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList; /*< 指向当前正在使用的延时任务列表 */
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList; /*< 指向当前正在使用的延时任务列表,用于保存已溢出当前tick计数的任务 */

可以看到这里有两个 xDelayedTaskListxDelayedTaskList1xDelayedTaskList1。这是由于 pxDelayedTaskList 要处理和时间相关的信息,所以需要考虑到系统的 systick 溢出的处理。为了解决这一繁琐的问题,FreeRTOS 设计了两个延时链表和两个延时链表指针来处理溢出问题。

如下图,xDelayedTaskList1xDelayedTaskList2 是两个实际链表,其中任务的排列顺序是按==退出阻塞时间==排序的,也就是链表的第一个成员任务是将最早退出阻塞,而最后一个成员任务是最后退出阻塞的。当系统的 systick 溢出时,pxDelayedTaskListpxOverflowDelayedTaskList 指向的链表地址也会随之交换一次,实现对溢出的处理。对于溢出的处理在后面会结合源码分析。以下是四个变量之间的关系:

Task 状态转换图

与延时任务链表变量为 xNextTaskUnblockTime。其定义如下:

1
2
/* 存储的是下一个任务进行解除阻塞操作的时间,用来判断在何时进行解除阻塞操作 */
PRIVILEGED_DATA static volatile TickType_t xNextTaskUnblockTime = ( TickType_t ) 0U;

2.4 xSuspendedTaskList

1
2
/*< 已被挂起的任务 */
PRIVILEGED_DATA static List_t xSuspendedTaskList;

3、任务调度器相关

3.1 xSchedulerRunning

1
2
/* 表示任务调度器是否已经运行(挂起的任务调度器也算在运行状态) */
PRIVILEGED_DATA static volatile BaseType_t xSchedulerRunning = pdFALSE;

3.2 uxSchedulerSuspended

1
2
3
4
/* 在调度器挂起期间,上下文切换将被挂起。此外,如果调度器已挂起,中断不得操作 TCB 的 xStateListItem,
* 或任何可以从 xStateListItem 引用的列表。如果在中断需要挂起调度器时解除阻塞任务,则将任务的事件列表项移入 xPendingReadyList,
* 以便调度器恢复时内核将任务从待就绪列表移入实际就绪列表。待就绪列表本身只能在临界区中访问 */
PRIVILEGED_DATA static volatile UBaseType_t uxSchedulerSuspended = ( UBaseType_t ) pdFALSE;

uxSchedulerSuspended 的作用是记录任务调度器被挂起的次数,当这个变量为 0(dFALSE)时,任务调度器不被挂起,任务切换正常执行,当这个变量大于 0 时代表任务调度器被挂起的次数。如果执行挂起任务调度器操作该变量值会增加,如果执行恢复任务调度器操作,该变量值会减一,直到它为 0 时才会真正的执行实际的调度器恢复操作,这样可以有效的提高执行效率。

3.3 xPendedTicks

1
PRIVILEGED_DATA static volatile TickType_t xPendedTicks = ( TickType_t ) 0U;

任务调度器在被挂起期间,系统的时间,仍然是需要增加的。挂起期间漏掉的 systick 数目便会被存储在这个变量中,以用于恢复调度器时补上漏掉的 systick。

3.4 xPendingReadyList

1
2
/* 在调度器挂起期间已就绪的任务。调度器恢复时,它们将被移到就绪列表中 */
PRIVILEGED_DATA static List_t xPendingReadyList;

这个链表中挂接的是在任务调度器挂起期间解除阻塞条件得到满足的阻塞任务,在任务调度器恢复工作后,这些任务会被移动到就绪链表组中,变为就绪状态。

4、任务删除相关

4.1 xTasksWaitingTermination

1
2
/* 已被删除但内存尚未释放的任务 */
PRIVILEGED_DATA static List_t xTasksWaitingTermination;

当任务自己删除自己时,其是不能立刻自己释放自己所占用的内存等资源的,其需要将自己挂接到 xTasksWaitingTermination 这个链表下,然后让 IdleTask 来回收其所占用的资源。

4.2 uxDeletedTasksWaitingCleanUp

1
2
/* 等待IdleTask 处理的自己删除自己的任务的数目 */
PRIVILEGED_DATA static volatile UBaseType_t uxDeletedTasksWaitingCleanUp = ( UBaseType_t ) 0U;

4.3 xIdleTaskHandle

1
2
/* 这个任务句柄指向 IdleTask(任务调度器在启动时便自动创建的空闲任务),用于回收内存等操作 */
PRIVILEGED_DATA static TaskHandle_t xIdleTaskHandle = NULL;

TaskHandle_t 本质上是指向任务 TCB 的指针,IdleTask 是任务调度器在启动时便自动创建的空闲任务,用于回收内存等操作,这个任务句柄指向 IdleTask

5、系统信息相关

5.1 xTickCount

1
2
/* 存储systick 的值,用来给系统提供时间信息 */
PRIVILEGED_DATA static volatile TickType_t xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;

5.2 xNumOfOverflows

1
2
/* 保存了xTickCount 溢出的次数 */
PRIVILEGED_DATA static volatile BaseType_t xNumOfOverflows = ( BaseType_t ) 0;

5.3 uxTaskNumber

1
2
/* 存储当前任务的数目 */
PRIVILEGED_DATA static UBaseType_t uxTaskNumber = ( UBaseType_t ) 0U;

每创建一个任务,这个值便会增加一次,为每个任务生成一个唯一的序号,供调试工具使用。注意与 uxCurrentNumberOfTasks 区分。

5.4 uxCurrentNumberOfTasks

1
2
/* 存储当前任务的数目 */
PRIVILEGED_DATA static volatile UBaseType_t uxCurrentNumberOfTasks = ( UBaseType_t ) 0U;

二、任务的创建和删除

1、任务的创建

FreeRTOS 提供了以下4 种任务创建函数:

  • xTaskCreateStatic():以静态内存分配的方式创建任务,也就是在编译时便要分配好 TCB 等所需要内存。
  • xTaskCreateRestrictedStatic():以静态内存分配的方式创建任务,需要 MPU。
  • xTaskCreate():以动态内存分配方式创建任务,需要提供 portMolloc() 函数的实现,在程序实际运行时分配 TCB 等所需要内存。
  • xTaskCreateRestricted():以动态内存分配方式创建任务,需要 MPU。

这里只讲 xTaskCreate(),其它函数有需要了解的请自行阅读源码。

1.1 xTaskCreate()

Task 状态转换图

创建任务的时候,我们就把它添加到对应就绪链表数组下的对应优先级下的链表的结尾,当我们运行一个任务(同一优先级时)的时候,它会先从链表的最后一项开始运行(因为 pxCurrentTCB 指向它),也就是先运行 3 号任务,然后是 1 号任务,最后是 2 号任务。

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
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,  /* 指向任务函数的函数指针 */
const char * const pcName, /* 任务的名称 */
const configSTACK_DEPTH_TYPE usStackDepth, /* 栈的深度,这里的栈的单位不是byte 而是根据平台的位数决定的,8 位,16 位,32
位分别对应1,2,3,4byte */
void * const pvParameters, /* 传入任务的参数 */
UBaseType_t uxPriority, /* 任务的优先级。数值越大,任务的优先级越高 */
TaskHandle_t * const pxCreatedTask ) /* 创建的任务的句柄,本质就是一个指向创建任务TCB 的指针 */
{
TCB_t * pxNewTCB;
BaseType_t xReturn;

/* 如果堆栈向下增长,则先分配堆栈再分配 TCB,以防止堆栈增长到 TCB 中。
* 如果堆栈向上增长,则先分配 TCB 再分配堆栈 */
#if ( portSTACK_GROWTH > 0 )
{
/* 栈向上生长 */

/* 为 TCB 分配空间 */
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );

if( pxNewTCB != NULL )
{
/* 为栈分配空间 */
pxNewTCB->pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) );

if( pxNewTCB->pxStack == NULL )
{
/* 无法分配堆栈。删除已分配的 TCB */
vPortFree( pxNewTCB );
pxNewTCB = NULL;
}
}
}
#else /* portSTACK_GROWTH */
{
/* 栈向下生长 */

StackType_t * pxStack;

/* 为正在创建的任务分配堆栈空间 - pvPortMalloc 见 porttable/MemMang/heap_4.c */
pxStack = pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /* pvPortMalloc() 返回的所有值至少具有 MCU 堆栈所需的对齐方式,并且此分配是堆栈 */

if( pxStack != NULL )
{
/* 为 TCB 分配空间 */
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); /* pvPortMalloc() 返回的所有值至少具有 MCU 堆栈所需的对齐方式,且 TCB_t 的第一个成员始终是指向任务堆栈的指针 */

if( pxNewTCB != NULL )
{
/* 将堆栈位置存储在 TCB 中 */
pxNewTCB->pxStack = pxStack;
}
else
{
/* 由于 TCB 未创建,堆栈无法使用。再次释放它 */
vPortFree( pxStack );
}
}
else
{
pxNewTCB = NULL;
}
}
#endif /* portSTACK_GROWTH */

if( pxNewTCB != NULL )
{
#if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
{
/* 任务可以静态或动态创建,因此注意此任务是以动态方式创建的,以便稍后删除时参考 */
pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
}
#endif /* tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE */

/** 初始化新创建的任务 **/
prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
prvAddNewTaskToReadyList( pxNewTCB ); /** 将新创建的任务添加到就绪列表 */
xReturn = pdPASS;
}
else
{
xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}

return xReturn;
}

代码内容很简单,大致做了这几件事:

  • 初始化栈 - pvPortMallocpxNewTCB->pxStack = pxStack;
  • 为任务分配内存空间、填充 TCB 结构体 - pvPortMallocprvInitialiseNewTask
  • 将 TCB 加入到就绪列表中,并根据优先级进行任务切换 - prvAddNewTaskToReadyList

1.2 prvInitialiseNewTask

xTaskCreate 函数中调用了 prvInitialiseNewTask 函数来填充 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
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
106
107
108
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask,
TCB_t * pxNewTCB, /* TCB 地址 */
const MemoryRegion_t * const xRegions ) /* MPU 相关暂时不讨论 */
{
StackType_t * pxTopOfStack;
UBaseType_t x;

/* 如果不需要,避免依赖 memset() */
#if ( tskSET_NEW_STACKS_TO_KNOWN_VALUE == 1 )
{
/* 用已知值填充堆栈以协助调试 */
( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) ulStackDepth * sizeof( StackType_t ) );
}
#endif /* tskSET_NEW_STACKS_TO_KNOWN_VALUE */

/* 计算堆栈顶部地址。这取决于堆栈是从高内存向低内存增长(如 80x86)还是相反。
* portSTACK_GROWTH 用于根据端口的需要使结果为正或负 */
{
/* 栈向下生长 */

pxTopOfStack = &( pxNewTCB->pxStack[ ulStackDepth - ( uint32_t ) 1 ] );
pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) ); // 调用入口按 8 字节对齐

/* 检查计算出的堆栈顶部对齐是否正确 */
configASSERT( ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack & ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) == 0UL ) );
}

/* 将任务名称存入 TCB */
if( pcName != NULL )
{
for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
{
pxNewTCB->pcTaskName[ x ] = pcName[ x ];

/* 如果字符串短于 configMAX_TASK_NAME_LEN 个字符,则不要复制所有 configMAX_TASK_NAME_LEN,
* 以防字符串后的内存不可访问(极其不可能) */
if( pcName[ x ] == ( char ) 0x00 )
{
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}

/* 确保在字符串长度大于或等于 configMAX_TASK_NAME_LEN 的情况下,名称字符串以空字符终止 */
pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';
}
else
{
/* 任务未给定名称,因此确保在读取时有一个空字符终止符 */
pxNewTCB->pcTaskName[ 0 ] = 0x00;
}

/* 这用作数组索引,因此必须确保它不过大。首先移除特权位(如果存在) */
if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES )
{
uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;
}
else
{
mtCOVERAGE_TEST_MARKER();
}

pxNewTCB->uxPriority = uxPriority;
#if ( configUSE_MUTEXES == 1 )
{
pxNewTCB->uxBasePriority = uxPriority;
pxNewTCB->uxMutexesHeld = 0;
}
#endif /* configUSE_MUTEXES */

/** 初始化列表项 - 任务状态列表项和事件列表项 */
vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
vListInitialiseItem( &( pxNewTCB->xEventListItem ) );

/* 将 pxNewTCB 设置为从 ListItem_t 返回的链接。这样我们就可以从列表中的通用项返回到包含的 TCB */
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );

/* 事件列表始终按优先级顺序排列 */
listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB );

#if ( configUSE_TASK_NOTIFICATIONS == 1 )
{
memset( ( void * ) &( pxNewTCB->ulNotifiedValue[ 0 ] ), 0x00, sizeof( pxNewTCB->ulNotifiedValue ) );
memset( ( void * ) &( pxNewTCB->ucNotifyState[ 0 ] ), 0x00, sizeof( pxNewTCB->ucNotifyState ) );
}
#endif

pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );

if( pxCreatedTask != NULL )
{
/* 以匿名方式传递句柄。该句柄可用于更改已创建任务的优先级、删除已创建的任务等 */
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}

执行过程大致如下:

  • 将栈值设定为特定值,以用于栈最高使用大小检测等功能
    • ( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) ulStackDepth * sizeof( StackType_t ) );
  • 计算栈顶指针、栈底指针
    • pxTopOfStack = &( pxNewTCB->pxStack[ ulStackDepth - ( uint32_t ) 1 ] );
    • pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
  • 复制任务名、写入优先级等相关 TCB 结构体成员赋初值
    • pxNewTCB->pcTaskName[ x ] = pcName[ x ];
  • 初始化链表项
  • 对栈进行初始化
    • pxPortInitialiseStack

pxPortInitialiseStack 函数会按处理器规则填充任务私有栈的值,将任务的私有栈“伪装”成已经被调度过一次的样子。

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
StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,
TaskFunction_t pxCode,
void * pvParameters )
{
/* 模拟上下文切换中断创建的堆栈帧 */

/* 这里空出一个存储地址是为了符合MCU 进出中断的方式 */
pxTopOfStack--;

/* 栈中寄存器 xPSR 被初始为 0x01000000 ,其中 bit24 被置 1,表示使用 Thumb 指令 */
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
/* 将任务函数地址压入栈中程序 PC(R15),当该第一次切换任务时,
* 硬件的 PC 指针将指向该函数,也就是会从头执行这个任务 */
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */ /* 保证地址对齐 */
pxTopOfStack--;
/* 正常任务是死循环,不会使用 LR 进行返回,这里赋为错误处理函数地址,出错时会进入该函数 */
*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */

/* 跳过 R12 ,R3 ,R2,R1 不用初始化,节省代码空间 */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */

/* 使用一种要求每个任务维护自己的 exec 返回值的保存方法 */
pxTopOfStack--;
*pxTopOfStack = portINITIAL_EXC_RETURN;

pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */

return pxTopOfStack;
}

注意看,这里初始化栈的时候,把 LR 的值设为了 prvTaskExitError

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void prvTaskExitError( void )
{
/* 实现任务的函数不能退出或尝试返回给调用者,因为没有东西可以返回。
* 如果任务想要退出,它应该调用 vTaskDelete(NULL)。如果定义了 configASSERT(),
* 则人为地触发一个 assert(),然后在此处停止,以便应用程序编写者可以捕获错误。
*/
configASSERT( uxCriticalNesting == ~0UL );
portDISABLE_INTERRUPTS(); // 进入临界区,禁止中断

/* 发生错误,进入死循环,会一直停在这里 */
for( ; ; )
{
}
}

也就是一个死循环。所以说,如果我们自己写一个任务处理函数的时候,如果不是死循环的话(且没有经过特殊的处理),最终就会执行到这里,所有的任务都无法再执行,也就是你之前遇到死机的可能的原因之一。

当我们想让任务退出的时候,必须要杀死这个任务,这就会用到下面会将到的 vTaskDelete()

1.3 prvAddNewTaskToReadyList

xTaskCreate 函数中,紧接着调用了 prvAddNewTaskToReadyList 来使任务处于就绪态和任务切换:

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
static void prvAddNewTaskToReadyList( TCB_t * pxNewTCB )
{
/* 确保在更新任务列表时,中断不会访问任务列表 */
taskENTER_CRITICAL();
{
uxCurrentNumberOfTasks++; /* 全局变量 - 记录当前任务数 */

if( pxCurrentTCB == NULL )
{
/* 没有其他任务,或者所有其他任务都处于挂起状态 - 将此任务设为当前任务 */
pxCurrentTCB = pxNewTCB;

/* 全局变量 - 当前任务数为 1 */
if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 )
{
/* 这是创建的第一个任务,因此需要进行初步初始化。如果此调用失败,我们将无法恢复,但我们会报告失败 */
prvInitialiseTaskLists();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
/* 如果调度器尚未运行,那么如果此任务是迄今为止创建的优先级最高的任务,则将该任务设置为当前任务 */
if( xSchedulerRunning == pdFALSE )
{
if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority )
{
pxCurrentTCB = pxNewTCB;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}

uxTaskNumber++;

#if ( configUSE_TRACE_FACILITY == 1 )
{
/* 在 TCB 中添加一个计数器,仅用于跟踪 */
pxNewTCB->uxTCBNumber = uxTaskNumber;
}
#endif /* configUSE_TRACE_FACILITY */
traceTASK_CREATE( pxNewTCB );

/* 添加到就绪列表中 */
prvAddTaskToReadyList( pxNewTCB );

portSETUP_TCB( pxNewTCB );
}
taskEXIT_CRITICAL();

if( xSchedulerRunning != pdFALSE )
{
/* 如果创建的任务优先级高于当前任务,则它应该立即运行 */
if( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority )
{
/* 见 port.c vPortGenerateSimulatedInterrupt(),
* 产生一个模拟中断,以便调度器运行 */
taskYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}

该函数大致做了如下工作:

  • 记录当前任务数量
    • uxCurrentNumberOfTasks++;
  • 将任务添加到就绪链表中
    • prvAddTaskToReadyList( pxNewTCB );

将任务插入就绪链表中时采用的宏 prvAddTaskToReadyList() 相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 将由 pxTCB 表示的任务放置到相应的就绪列表中。它被插入到列表的末尾
* 按优先级放到对应的链表下
*/
#define prvAddTaskToReadyList( pxTCB ) \
traceMOVED_TASK_TO_READY_STATE( pxTCB ); \
taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); \
vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \
tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )


/* 提供了一个 port 优化的版本。调用端口定义的宏,记录最高先级的就绪任务的优先级 */
#define taskRECORD_READY_PRIORITY( uxPriority ) portRECORD_READY_PRIORITY( uxPriority, uxTopReadyPriority )

#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )

首先通过 taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); 来获取最高优先级的就绪任务的优先级,然后调用 vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) );,根据优先级,将任务放在对应优先级的就绪列表项后面。

  • 根据新加入的优先级判断是否需要进行一次任务切换
    • taskYIELD_IF_USING_PREEMPTION();

该函数本质上就是 port.c 文件中的 vPortGenerateSimulatedInterrupt() 函数,该函数通过产生一个模拟中断来让调度器进行一次任务切换,

至此,xTaskCreate() 的执行过程就结束了,一个任务就此创建好了。

2、任务删除

2.1 vTaskDelete

我们通过调用 vTaskDelete() 函数来删除一个任务,该函数有两个使用场景:

  1. 任务自己删除自己(传入参数为 NULL)
  2. 当前任务删除其它任务(传入任务句柄)
    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
    void vTaskDelete( TaskHandle_t xTaskToDelete )
    {
    TCB_t * pxTCB;

    /** 在临界区中操作 */
    taskENTER_CRITICAL();
    {
    /* 获取 TCB,如果为 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();
    }

    /* 同时增加 uxTaskNumber,以便内核感知的调试器可以检测到任务列表需要重新生成。
    * 这是在 portPRE_TASK_DELETE_HOOK() 之前完成的,因为在 Windows 端口上,该宏不会返回。 */
    uxTaskNumber++;

    if( pxTCB == pxCurrentTCB )
    {
    /* 一个任务正在删除自己。这不能在任务内部完成,因为需要切换到另一个任务。
    * 将任务放入终止列表中。空闲任务将检查终止列表,并释放调度器为已删除任务的 TCB 和堆栈分配的任何内存。 */
    vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) );

    /* 增加 `ucTasksDeleted` 变量,以便空闲任务知道有一个已删除的任务,
    * 因此应该检查 `xTasksWaitingTermination` 列表。 */
    ++uxDeletedTasksWaitingCleanUp;

    /* 在调用 `portPRE_TASK_DELETE_HOOK()` 之前调用删除钩子,
    * 因为在 Win32 端口上,`portPRE_TASK_DELETE_HOOK()` 不会返回。 */
    traceTASK_DELETE( pxTCB );

    /* 预删除钩子主要用于 Windows 模拟器,在该模拟器中会执行特定的 Windows 清理操作,
    * 之后无法从这个任务中让出执行权 - 因此使用 `xYieldPending` 来标记需要进行上下文切换。
    * 关闭当前正在运行的线程 */
    portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending );
    }
    else
    {
    --uxCurrentNumberOfTasks;
    traceTASK_DELETE( pxTCB );

    /* 重置下一个预期的解除阻塞时间,以防它指的是刚刚被删除的任务 */
    prvResetNextTaskUnblockTime();
    }
    }
    taskEXIT_CRITICAL();

    /* 如果任务不是在删除自己,那么在临界区之外调用 `prvDeleteTCB`。如果任务在删除自己,
    * 那么 `prvDeleteTCB` 是从 `prvCheckTasksWaitingTermination` 调用的,
    * 而 `prvCheckTasksWaitingTermination` 又是从空闲任务调用的 */
    if( pxTCB != pxCurrentTCB )
    {
    prvDeleteTCB( pxTCB );
    }

    /* 如果刚刚被删除的任务是当前正在运行的任务,则强制进行重新调度 */
    if( xSchedulerRunning != pdFALSE )
    {
    if( pxTCB == pxCurrentTCB )
    {
    configASSERT( uxSchedulerSuspended == 0 );
    portYIELD_WITHIN_API(); /* 生成一个模拟中断,以便调度器运行 */
    }
    else
    {
    mtCOVERAGE_TEST_MARKER();
    }
    }
    }

我们首先通过如下宏来判断传入的是 NULL 还是任务句柄:

1
#define prvGetTCBFromHandle(pxHandle) ( ( ( pxHandle ) == NULL ) ? pxCurrentTCB : ( pxHandle ) )

当我们用 vTaskDelete() 来删除其它任务时,所需要进行的工作步骤如下:

  1. 将待删除任务从相关的状态链表中删除,设置相关参数,保证被删除的任务不会再次获得处理器使用权。
    • uxListRemove( &( pxTCB->xStateListItem ) )
  2. 将待删除任务从其相关的事件链表中删除,设置相关参数,保证被删除的任务不会再次获得处理器使用权。
    • uxListRemove( &( pxTCB->xEventListItem ) )
  3. 更改当前任务数目。
    • --uxCurrentNumberOfTasks;
  4. 直接释放内存空间。
    • prvDeleteTCB( pxTCB );
1
2
3
4
5
6
7
8
9
10
11
static void prvDeleteTCB( TCB_t * pxTCB )
{
/* 这个调用是专门为 TriCore 端口所需的。它必须在 `vPortFree()` 调用之上。
* 这个调用也被那些希望静态分配和清理 RAM 的端口/演示程序使用。 */
portCLEAN_UP_TCB( pxTCB );

/* 任务只能被动态分配- 释放堆栈和 TCB
* 见 portable/MemMang/heap_4.c */
vPortFree( pxTCB->pxStack );
vPortFree( pxTCB );
}

portCLEAN_UP_TCB 本质上就是 port.c 中的函数 vPortDeleteThread

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
void vPortDeleteThread( void *pvTaskToDelete )
{
ThreadState_t *pxThreadState;
uint32_t ulErrorCode;

/* 消除编译器警告 */
( void ) ulErrorCode;

/* 获取线程状态 */
pxThreadState = ( ThreadState_t * ) ( *( size_t *) pvTaskToDelete );

/* 检查所指定的 pxThreadState 的 pvThread 句柄是否有效。如果无效,说明线程可能已被关闭 */
if( pxThreadState->pvThread != NULL )
{
/* 等待获取 pvInterruptEventMutex 互斥量,以确保在进行线程删除时,其他可能的线程不会干扰 */
WaitForSingleObject( pvInterruptEventMutex, INFINITE );

/* 强制终止指定的线程,并检查返回值是否有误 */
ulErrorCode = TerminateThread( pxThreadState->pvThread, 0 );
configASSERT( ulErrorCode );

/* 关闭 pxThreadState->pvThread 句柄,释放相应资源 */
ulErrorCode = CloseHandle( pxThreadState->pvThread );
configASSERT( ulErrorCode );

/* 释放互斥量 */
ReleaseMutex( pvInterruptEventMutex );
}
}
  1. 重新计算下个任务解除阻塞的时间。
    • prvResetNextTaskUnblockTime();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void prvResetNextTaskUnblockTime( void )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
/* 新的当前延迟列表为空。将 `xNextTaskUnblockTime` 设置为最大可能值,
* 以便在延迟列表中有项目之前,`if( xTickCount >= xNextTaskUnblockTime )` 测试极不可能通过。 */
xNextTaskUnblockTime = portMAX_DELAY;
}
else
{
/* 新的当前延迟列表不为空,获取延迟列表头部项目的值。这是延迟列表头部任务应从阻塞状态中移除的时间 */
xNextTaskUnblockTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxDelayedTaskList );
}
}

// list.h
#define listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxList ) ( ( ( pxList )->xListEnd ).pxNext->xItemValue )

FreeRTOS 系统中所有的阻塞都是由将任务按解除阻塞时间升序挂接到延时任务链表 pxDelayedTaskList 中实现的,因此 prvResetNextTaskUnblockTime() 实际上只是读取 pxDelayedTaskList 下的第一个任务解除阻塞的时间,将其赋值给 xNextTaskUnblockTime 而已,如果 pxDelayedTaskList 为空,那么 xNextTaskUnblockTime 将会被赋值为 portMAX_DELAY

当任务是自己删除自己时,上述步骤的第4 步将有所变化。当前任务仍在运行中,此时直接释放其占用的内存可能导致运行错误,因此需要等待其退出运行状态时才能安全的对其占用的内存进行释放。此时上述的步骤 4 替换为以下两步:

  • 将待删除任务挂接到待终止任务链表 xTasksWaitingTermination
    • vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) );
  • 增加删除待清理任务数 uxDeletedTasksWaitingCleanUp
    • ++uxDeletedTasksWaitingCleanUp;

在前面讲创建任务的时候,提到会创建一个空闲任务,空闲任务就会来释放掉这个任务所申请的内存(TCB、栈等),相当于 Linux 下的 init 守护进程。但是空闲任务的优先级是 0,如果就绪列表一直不为空,那空闲任务该如何得到执行?那就是用 vTaskDelay,它会把任务从就绪链表移动到延迟列表,让出 CPU 资源,这样空闲任务就可以得到执行。

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 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();
}

/* Force a reschedule if xTaskResumeAll has not already done so, we may
* have put ourselves to sleep. */
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}

三、任务切换

任务切换的目的是保证当前具有最高优先级的就绪任务获得处理器的使用权。在进行任务切换时,首先要找到具有最高优先级的就绪任务,如果该任务不是当前正在运行的任务,需要先保存当前运行任务的堆栈,并将具有最高优先级的就绪任务堆栈恢复到处理器的堆栈中进行运行。

1、vTaskSwitchContext

通过 vTaskSwitchContext 可以实现任务上下文切换:

删去了不必要的宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void vTaskSwitchContext( void )
{
/* 检查调度器是否被挂起 */
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
{
/* 调度器已经被挂起,不允许上下文切换 */
xYieldPending = pdTRUE;
}
else
{
/* 调度器未被挂起,允许上下文切换 */
xYieldPending = pdFALSE;
traceTASK_SWITCHED_OUT();

/* 进行堆栈溢出检查,确保当前任务没有溢出 */
taskCHECK_FOR_STACK_OVERFLOW();

/* 调用函数选择下一个要运行的任务,依据任务的优先级进行调度 */
taskSELECT_HIGHEST_PRIORITY_TASK();
traceTASK_SWITCHED_IN();
}
}

1.1 taskSELECT_HIGHEST_PRIORITY_TASK

调用 taskSELECT_HIGHEST_PRIORITY_TASK 可以根据当前就绪列表中任务的最高优先级 uxTopReadyPriority 获得要运行任务:

1
2
3
4
5
6
7
8
9
10
11
12
/* 优化后版本 - 寻找拥有最高优先级的就绪任务 
* 这里不在使用数值大小来表示最高优先级,而是使用每一位表示是否有该优先级的任务处于就
* 绪态,对于cortex -m3有 32 位,如 0000 0000 0000 0000 0000 0000 0000 0001 表示第0级有就绪态的任务 */
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
\
/* 查找包含就绪任务队列中的优先级最高的任务 */ \
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
} /* taskSELECT_HIGHEST_PRIORITY_TASK() */

其中出现的宏定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 获取在就绪优先级位图中最高的优先级 
* bsr(Bit Scan Reverse,位扫描反向)指令,目的是查找 uxReadyPriorities 中最高有效位(即最高优先级)*/
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) \
__asm volatile( "bsr %1, %0\n\t" \
:"=r"(uxTopPriority) : "rm"(uxReadyPriorities) : "cc" )

#define listCURRENT_LIST_LENGTH( pxList ) ( ( pxList )->uxNumberOfItems )

#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \
{ \
List_t * const pxConstList = ( pxList ); \
/* Increment the index to the next item and return the item, ensuring */ \
/* we don't return the marker used at the end of the list. */ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
{ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
} \
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \
}

2、进入任务切换的方式

FreeRTOS 进入任务切换的方式有以下两种

  1. xPortSysTickHandler() 中断中进入,也就是在系统 Systick 增加时,根据情况进入任务切换。
  2. 手动调用 portYIELD_WITHIN_API()taskYIELD_IF_USING_PREEMPTION()(在启用抢占模式的情况下其和 portYIELD_WITHIN_API 一样,非抢占模式下,其没有任何作用)直接进行一次任务切换。

2.1 xPortSysTickHandler

xPortSysTickHandler 其实就是 SysTick_Handler,在 FreeRTOSConfig.h 文件中有:

1
2
3
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler

portable/RVDS/ARM_CM4F/port.c 实现了这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void xPortSysTickHandler( void )
{
/* SysTick运行在最低的中断优先级,因此当这个中断执行时,
* 所有中断都必须被取消屏蔽。因此,不需要保存然后恢复中断掩码值,
* 因为其值已经已知 - 因此使用稍微快一些的 vPortRaiseBASEPRI()函数来
* 代替 portSET_INTERRUPT_MASK_FROM_ISR()
*/
vPortRaiseBASEPRI();
{
/* 增加滴答数
* 这里并不是每次进入系统滴答中断都会进行上下文切换,只有有任务从阻塞状态退出
* 或者在时间片轮询模式中有相同的优先级的任务,才会进行上下文切换 */
if( xTaskIncrementTick() != pdFALSE )
{
/* 需要进行任务切换。此时,代码将 PendSV 中断设置为待处理,
* 这样在中断结束后,系统会进行上下文切换 */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}

/* 清除在处理 SysTick 中断时设置的优先级 */
vPortClearBASEPRIFromISR();
}

xTaskIncrementTick() 函数的主要功能是在在任务调度器工作时修改 Systick 的值,并根据 Systick 值的变化判断是否需要进行一次任务切换动作;在任务调度器被挂起时,其会记录任务调度器挂起期间漏掉的 Systick 数,一旦任务调度器恢复运行,任务调度器会补上漏掉的 Systick 和相应的任务切换动作在任务调度器工作时,当以下两种情况发生时,xTaskIncrementTick() 将返回 pdTRUE,以触发一次 PendSV 中断,以进行任务切换动作:

  1. 当前时刻有任务需要退出阻塞状态
  2. 启用时间片模式,当前优先级下有多个任务,需要共享使用权。

2.2 portYIELD_WITHIN_API

这个 API 在前面讲 vTaskCreatevTaskDelete 的时候已经见过了(可能名称不一样,因为又用 #define 封装了几次),这里列出源码(port.c):

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
void vPortGenerateSimulatedInterrupt( uint32_t ulInterruptNumber )
{
ThreadState_t *pxThreadState = ( ThreadState_t *) *( ( size_t * ) pxCurrentTCB ); // 获取当前任务的线程状态

configASSERT( xPortRunning ); // 确保调度器在运行

/* 判断要生成的中断编号是否在最大中断数范围内,并保证互斥量不为空 */
if( ( ulInterruptNumber < portMAX_INTERRUPTS ) && ( pvInterruptEventMutex != NULL ) )
{
WaitForSingleObject( pvInterruptEventMutex, INFINITE ); /* 等待获取互斥锁 */
/**************************************************************************************/
ulPendingInterrupts |= ( 1 << ulInterruptNumber ); /* 设置挂起的中断 */

/* 模拟的中断现在处于挂起状态,但如果此调用处于临界区中,则不要立即处理它。
* 由于等待互斥锁的调用是累积的,因此有可能处于临界区中。
* 如果在临界区中,那么当临界区嵌套计数减少到零时,事件将被设置 */
if( ulCriticalNesting == portNO_CRITICAL_NESTING )
{
SetEvent( pvInterruptEvent );

/* 准备等待一个事件 - 确保事件尚未被信号通知 */
ResetEvent( pxThreadState->pvYieldEvent );
}
/**************************************************************************************/
ReleaseMutex( pvInterruptEventMutex ); /* 释放互斥锁 */
if( ulCriticalNesting == portNO_CRITICAL_NESTING )
{
/* 有一个中断被挂起,所以确保阻塞以允许它执行。
* 在大多数情况下,(模拟的) 中断将在到达下一行之前已经执行
* 所以这只是为了确保万无一失 */
WaitForSingleObject( pxThreadState->pvYieldEvent, INFINITE );
}
}
}

2.3 xPortPendSVHandler

前面也看到了,当触发 PendSV 中断的时候,就会调用 xPortPendSVHandler,也就是 PendSV_Handler,下面是它的实现(port.c),通过它我们就可以清楚任务是如何进行上下文切换的。

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
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;

/* DMB
数据存储器隔离。DMB 指令保证仅当所有在它前面的存储器访问操作
都执行完毕后,才提交(commit)在它后面的存储器访问操作。

DSB
数据同步隔离。比 DMB 严格:仅当所有在它前面的存储器访问操作都执行完毕后,
才执行在它后面的指令(亦即任何指令都要等待存储器访问操作)

ISB
指令同步隔离。最严格:它会清洗流水线,以保证所有它前面的指令都执
行完毕之后,才执行它后面的指令。
*/

/* step1 保存当前任务现场*/
/* =================================================================*/

/* *INDENT-OFF* */
PRESERVE8 /* 字节对齐 */

/* PendSV 中断产生时,硬件自动将xPSR ,PC(R15),LR(R14),R12 ,R3-R0 使用 PSP 压入任务
* 堆栈中,进入中断后硬件会强制使用MSP 指针,此时LR(R14)的值将会被自动被更新为
* 特殊的 EXC_RETURN */
mrs r0, psp /* 保存进程堆栈指针到R0 */
isb
/* Get the location of the current TCB. */
ldr r3, =pxCurrentTCB /* 读取当前TCB 块的地址到R3 */
ldr r2, [ r3 ] /* 将当前任务栈顶地址放到 R2 中,这也是为什么强调栈顶指针一定得是 TCB 块的第一个成员的原因 */

/* 不用管 */
/* Is the task using the FPU context? If so, push high vfp registers. */
tst r14, #0x10
it eq
vstmdbeq r0!, {s16-s31}

/* 将 R4 到 R11 通用寄存器的值压入栈保存 */
stmdb r0!, {r4-r11, r14}

/* 将 R0 的值写入以 R2 为地址的内存中,也就是保存当前的栈顶地址到 TCB 的第一个成员,也就是栈顶指针 */
str r0, [ r2 ]

/* 将 R3,R14 临时压栈,这里的 SP 其实使用的是 MSP ,这里进行压栈保护的原因是 bl 指令会自动更改 R14 值用于返回 */
stmdb sp!, {r0, r3}

/* 屏蔽 configMAX_SYSCALL_INTERRUPT_PRIORITY 以下优先级的中断 */
mov r0, # configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0

/* step2 恢复待切换任务的现场*/
/* =================================================================*/

dsb
isb
bl vTaskSwitchContext /* 这里调用 vTaskSwitchContext 函数来获取下一个要执行任务控制块 */

/* 取消中断屏蔽 */
mov r0, # 0
msr basepri, r0

/* 将 R0、R3 出栈,这里 R3 相当于是 pxCurrentTCB 内存的值,所以此时 R3 值已经更新为下一个要执行的任务 TCB 地址了 */
ldmia sp!, {r0, r3}

/* The first item in pxCurrentTCB is the task top of stack. */
ldr r1, [ r3 ]
ldr r0, [ r1 ] /* 把新任务的栈顶指针放到R0里 */

/* Pop the core registers. */
ldmia r0!, {r4-r11, r14} /* 将新任务的 R4-R11、R14 出栈 */

/* Is the task using the FPU context? If so, pop the high vfp registers
* too. */
tst r14, # 0x10
it eq
vldmiaeq r0!, {s16-s31}

/* step3 更改PSP 指针值*/
/* =================================================================*/

msr psp, r0 /* 将新的栈顶地址放入到进程堆栈指针PSP */
isb
#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
#if WORKAROUND_PMU_CM001 == 1
push { r14 }
pop { pc }
nop
#endif
#endif

/* 异常发生时,R14 中保存异常返回标志,包括返回后进入线程模式还是处理器模
* 式、使用 PSP 堆栈指针还是 MSP 堆栈指针,当调用 bx r14 指令后,硬件会知道要从异常返
* 回,然后出栈,这个时候堆栈指针 PSP 已经指向了新任务堆栈的正确位置,当新任务的运
* 行地址被出栈到 PC 寄存器后,新的任务也会被执行 */
bx r14
/* *INDENT-ON* */
}

注意,在任务中使用的是 PSP,而处理器复位后默认使用的是 MSP 指针。这是因为任务调度器在启动时会调用 prvStartFirstTask() 函数,这个函数也是一段汇编代码,它的主要工作就是复位 MSP,开中断和异常,并且触发一次 SVC 中断,进行第一次任务的加载,其内容如下:

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
__asm void prvStartFirstTask( void )
{
/* *INDENT-OFF* */
PRESERVE8

/* 使用NVIC偏移寄存器来定位堆栈 */
ldr r0, =0xE000ED08 /* 向量表偏移量寄存器的起始地址存储着 MSP 的初始值 */
ldr r0, [ r0 ]
ldr r0, [ r0 ]
/* 将主堆栈指针(msp)设置回堆栈的起始位置 */
msr msp, r0 /* 复位MSP */

/* 清除指示 FPU 正在使用的位,以防在调度器启动之前使用了FPU——
* 否则会导致在SVC堆栈中为FPU寄存器的延迟保存不必要地留下空间 */
mov r0, #0
msr control, r0
/* 使能全局中断和异常 */
cpsie i
cpsie f
dsb
isb
/* 触发 SVC 中断来启动第一个任务 */
svc 0
nop
nop
/* *INDENT-ON* */
}

SVC 异常服务函数里的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__asm void vPortSVCHandler( void )
{
/* *INDENT-OFF* */
PRESERVE8

/* 获取当前TCB的位置 */
ldr r3, = pxCurrentTCB
ldr r1, [ r3 ]
ldr r0, [ r1 ]
/* Pop the core registers. */
ldmia r0 !, {r4-r11,r14}
msr psp, r0
isb
mov r0, # 0
msr basepri, r0
bx r14
/* *INDENT-ON* */
}

在最后一步, SVC 异常服务函数修改了 r14 的值,正是修改该值使得处理器在退出中断后运行任务函数时进入线程模式并使用 PSP 栈指针。

四、任务调度器

1、启动

1.1 vTaskStartScheduler()

FreeRTOS 中任务调度器的启动由 vTaskStartScheduler() 函数实现,此函数被调用后,OS 将接手处理器的管理权,它主要有以下几个步骤:

  • 创建空闲任务、定时器任务。
  • 初始化下一次解除阻塞时间,系统 tick 初始值,运行状态等变量。
  • 调用 xPortStartScheduler() 函数启动调度器
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
void vTaskStartScheduler( void )
{
BaseType_t xReturn;

/* 1. 添加空闲任务(设置为最低优先级) */
/******************************************************/
{
/* 空闲任务由正在使用动态分配的RAM创建 */
xReturn = xTaskCreate( prvIdleTask,
configIDLE_TASK_NAME,
configMINIMAL_STACK_SIZE,
( void * ) NULL,
portPRIVILEGE_BIT, /* 实际上是 ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),但 tskIDLE_PRIORITY 为零 */
&xIdleTaskHandle ); /* MISRA 异常,这是合理的,因为它不是对所有支持的编译器都冗余的显式转换 */
}

/* 2. 添加定时器任务 */
/******************************************************/

#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
/* 创建定时器任务 */
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TIMERS */

/* 3. 启动调度器 */
/******************************************************/

if( xReturn == pdPASS )
{
/* 只有在定义了用户可定义的宏 FREERTOS_TASKS_C_ADDITIONS_INIT 时,
* 才应调用 freertos_tasks_c_additions_init(),因为这是该函数唯一调用的宏 */
#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
{
freertos_tasks_c_additions_init();
}
#endif

/* 在此处关闭中断,以确保在调用 xPortStartScheduler() 之前或期间不会发生滴答。
* 已创建任务的堆栈包含一个中断已开启的状态字,因此当第一个任务开始运行时,中断将自动重新启用 */
portDISABLE_INTERRUPTS();

xNextTaskUnblockTime = portMAX_DELAY; /* 初始化下一次解除阻塞时间,因为当前任务是首次使用没有要延迟的任务了,所以设为最大 portMAX_DELAY */
xSchedulerRunning = pdTRUE; /* 设置标志任务调度器已启动 */
xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT; /* 初始化系统 tick 初始值 */

/* 如果定义了 configGENERATE_RUN_TIME_STATS,则必须定义以下宏以配置用于生成运行
* 时间计数器时间基准的定时器/计数器。注意:如果 configGENERATE_RUN_TIME_STATS 设置为 0 并且以下行无法构建,
* 请确保在您的 FreeRTOSConfig.h 文件中没有定义 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()。 */
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

traceTASK_SWITCHED_IN();

/* 设置定时器滴答是硬件特定的,因此位于可移植接口中 */
if( xPortStartScheduler() != pdFALSE )
{
/* 如果调度器正在运行,该函数将不会返回,因此不应到达此处 */
}
else
{
/* 只有当任务调用 xTaskEndScheduler() 时才会到达此处 */
}
}
else
{
/* 只有当内核无法启动时才会到达此行,因为没有足够的堆内存来创建空闲任务或定时器任务 */
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}

( void ) xIdleTaskHandle;

( void ) uxTopUsedPriority;
}

1.2 xPortStartScheduler

这个函数是与平台相关的,根据 arm-cm3 的移植文件来看(在目录 portable/RVDS/ARM_CM4F 下),它主要的工作是设置上下文切换中断和Systick 中断,启动定时器为系统提供 Systick,最终调用 prvStartFirstTask() (前面已经介绍过)来启动第一个任务。

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
BaseType_t xPortStartScheduler( void )
{
/* configMAX_SYSCALL_INTERRUPT_PRIORITY 不能为 0
* See https://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html */
configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );

/* This port can be used on all revisions of the Cortex-M7 core other than
* the r0p1 parts. r0p1 parts should use the port from the
* /source/portable/GCC/ARM_CM7/r0p1 directory. */
configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );

/* 1. 中断配置 */
/*************************************************************/

#if ( configASSERT_DEFINED == 1 )
{
volatile uint32_t ulOriginalPriority; /* 存储原始的中断优先级 */
/* 指向第一个用户中断优先级寄存器的指针 */
volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
volatile uint8_t ucMaxPriorityValue; /* 存储最大优先级值 */

/* 读取并保存当前的中断优先级寄存器值 */
ulOriginalPriority = *pucFirstUserPriorityRegister;

/* 将寄存器设置为最大8位值(0xFF) */
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;

/* 读取返回的值以查看有多少位保持不变 */
ucMaxPriorityValue = *pucFirstUserPriorityRegister;

/* 确保内核中断优先级设置为最低优先级 */
configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );

/* 计算可用于系统调用的最大优先级 */
ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

/* 初始化最大优先级组值 */
ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;

/* 确定最大优先级的位数 */
while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
{
ulMaxPRIGROUPValue--;
ucMaxPriorityValue <<= ( uint8_t ) 0x01;
}

#ifdef __NVIC_PRIO_BITS
{
/* Check the CMSIS configuration that defines the number of
* priority bits matches the number of priority bits actually queried
* from the hardware. */
configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
}
#endif

#ifdef configPRIO_BITS
{
/* 检查定义优先级位数的 FreeRTOS 配置是否与从硬件实际查询的优先级位数相匹配 */
configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
}
#endif

/* 将优先级组值移回其在 AIRCR 寄存器内的位置 */
ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

/* 将中断优先级寄存器恢复到原始值 */
*pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif /* conifgASSERT_DEFINED */

/* 2. PendSV 和 SysTick 中断优先级配置 */
/*************************************************************/

/* 将 PendSV 和 SysTick 设置为最低优先级的中断 */
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

/* 3. 定时器中断配置 */
/*************************************************************/

/* 启动生成 tick ISR 的定时器。这里已经禁用了中断 */
vPortSetupTimerInterrupt();

/* 初始化临界区嵌套计数,为第一个任务做准备 */
uxCriticalNesting = 0;

/* 确保启用VFP - 无论如何都应该启用 */
prvEnableVFP();

/* 总是使用延迟保存 */
*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;

/* 4. 启动第一个任务 */
/*************************************************************/

/* 开始第一个任务 */
prvStartFirstTask();

/* 不应该执行到这里 */
return 0;
}

2、结束

2.1 vTaskEndScheduler

任务调度器的关闭由 vTaskEndScheduler() 函数实现,此函数调用后 OS 将停止工作。它的实现就非常简单了,只有三行:

1
2
3
4
5
6
7
8
9
void vTaskEndScheduler( void )
{
/* 停止调度器中断并调用可移植调度器结束例程,以便在需要时可以恢复原始的 ISR。
* port 层必须确保中断使能位保持在正确的状态 */

portDISABLE_INTERRUPTS(); /* 关闭中断 */
xSchedulerRunning = pdFALSE; /* 设置标志表示任务调度器已停止 */
vPortEndScheduler(); /* 停止调度器 */
}

FreeRTOS task 源码解析
http://example.com/2024/11/11/FreeRTOS-task-源码解析/
作者
Yu xin
发布于
2024年11月11日
许可协议