0%

freertos 任务调度

任务创建

1
2
3
4
5
6
BaseType_t xTaskCreate(	TaskFunction_t pxTaskCode,  //指向任务函数的入口。任务永远不会返回(位于死循环内)
const char * const pcName, // 任务描述。主要用于调试。字符串的最大长度(包括字符串结束字符)由宏configMAX_TASK_NAME_LEN指定
const configSTACK_DEPTH_TYPE usStackDepth, //任务栈大小
void * const pvParameters, // 传给任务入口的参数
UBaseType_t uxPriority, //任务初始优先级
TaskHandle_t * const pxCreatedTask ) // 用于回传一个句柄(ID),创建任务后可以使用这个句柄引用任务。

任务结构体的定义:

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
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; // 指向当前栈的栈顶 , pxTopOfStack 总是指向最后一个入栈的项目

ListItem_t xStateListItem; // 状态列表项, ready blocked suspended 状态轮转
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; /* 任务栈的 结束地址*/
#endif

#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting; /* 任务嵌套深度*/
#endif

#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; /*< The priority last assigned to the task - used by the priority inheritance mechanism. 用于任务优先级继承的机制*/
UBaseType_t uxMutexesHeld;
#endif

#if( configUSE_TASK_NOTIFICATIONS == 1 ) // 任务通知机制, 用于简洁的任务通信
volatile uint32_t ulNotifiedValue;
volatile uint8_t ucNotifyState;
#endif

/* See the comments above the definition of
tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE. */
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
uint8_t ucStaticallyAllocated; /* true 标识任务栈是静态分配的, 不需要free*/
#endif

} tskTCB;

这个函数就是创建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
{
StackType_t *pxStack;
/* malloc 一个栈 , 也可以通过别的函数静态指定 */
pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) );
// 指针pxStack指向堆栈的起始位置 任务创建时会分配指定数目的任务堆栈,申请堆栈内存函数返回的指针就被赋给该变量
if( pxStack != NULL )
{
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); /*lint !e961 MISRA exception as the casts are only redundant for some paths. */

if( pxNewTCB != NULL )
{
pxNewTCB->pxStack = pxStack;
}
}
}

if( pxNewTCB != NULL )
{
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
{
/* Tasks can be created statically or dynamically, so note this
task was created dynamically in case it is later deleted. */
pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
// 挂入ready列表中
prvAddNewTaskToReadyList( pxNewTCB );
xReturn = pdPASS;
}
else
{
//false
xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}
return xReturn;
  • pxTopOfStack指向当前堆栈栈顶,随着进栈出栈,pxTopOfStack指向的位置是会变化的;
  • pxStack指向当前堆栈的起始位置,一经分配后,堆栈起始位置就固定了,不会被改变了。
  • 那么为什么需要pxStack变量呢,这是因为随着任务的运行,堆栈可能会溢出,在堆栈向下增长的系统中,这个变量可用于检查堆栈是否溢出;
  • 如果在堆栈向上增长的系统中,要想确定堆栈是否溢出,还需要另外一个变量pxEndOfStack来辅助诊断是否堆栈溢出.

嵌入式系统上大多都是向上增长的, 因此需要pxEndOfStack来检测是否溢出

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
static void prvInitialiseNewTask( 	TaskFunction_t pxTaskCode,
const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask,
TCB_t *pxNewTCB,
const MemoryRegion_t * const xRegions )
{

/* Avoid dependency on memset() if it is not required. */
#if( tskSET_NEW_STACKS_TO_KNOWN_VALUE == 1 )
{
// 填固定值用于debug
( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) ulStackDepth * sizeof( StackType_t ) );
}
#endif /* tskSET_NEW_STACKS_TO_KNOWN_VALUE */

/* Calculate the top of stack address. This depends on whether the stack
grows from high memory to low (as per the 80x86) or vice versa.
portSTACK_GROWTH is used to make the result positive or negative as required
by the port. */
#if( portSTACK_GROWTH < 0 )
{
... 栈是向下增长的, 省略
}
#else /* portSTACK_GROWTH */
{
pxTopOfStack = pxNewTCB->pxStack;
// 需要 pxEndOfStack 判断是否溢出
pxNewTCB->pxEndOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
}
#endif /* portSTACK_GROWTH */

/* Store the task name in the TCB. */ //任务描述相关
for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
{
pxNewTCB->pcTaskName[ x ] = pcName[ x ];
}
pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';

// 检查优先级是否超出范围, 归位
if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES )
{
uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;
}

pxNewTCB->uxPriority = uxPriority;
#if ( configUSE_MUTEXES == 1 )
{
// 初始优先级 定为 uxPriority
pxNewTCB->uxBasePriority = uxPriority;
pxNewTCB->uxMutexesHeld = 0;
}
#endif /* configUSE_MUTEXES */
// 初始化事件列表 状态列表
vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
vListInitialiseItem( &( pxNewTCB->xEventListItem ) );
// 状态列表owner -> 新创建的任务
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
// 事件列表 xItemValue 为优先级, 并指定onwer
listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB );

#if ( portCRITICAL_NESTING_IN_TCB == 1 )
{
pxNewTCB->uxCriticalNesting = ( UBaseType_t ) 0U;
}
#endif /* portCRITICAL_NESTING_IN_TCB */

// 任务通知机制
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
{
pxNewTCB->ulNotifiedValue = 0;
pxNewTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
}
#endif

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

if( ( void * ) pxCreatedTask != NULL )
{
// 回传任务句柄
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
}
}

pxTopOfStack的处理

这个函数关注点在 pxTopOfStack 段的处理, 估计跟rtthread的处理是相似的,
pxTopOfStack 类似于 rtthread的每个thread结构体的sp成员, 是当前任务栈的游标, sp向上几十个字节压入了必要的寄存器信息(a0 ra 等)以及status cause epc 等协处理器的信息,
在往上就是当前任务entry 执行了多少函数后, 当前函数栈sp递减到的位置.

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
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
/* Ensure byte alignment is maintained when leaving this function. */
pxTopOfStack--;

*pxTopOfStack = (StackType_t) 0xDEADBEEF;
pxTopOfStack--;

*pxTopOfStack = (StackType_t) 0x12345678; /* Word to which the stack pointer will be left pointing after context restore. */
pxTopOfStack--;

*pxTopOfStack = (StackType_t) mips_getcr();
pxTopOfStack--;

*pxTopOfStack = (StackType_t) portINITIAL_SR;/* CP0_STATUS */
pxTopOfStack--;

*pxTopOfStack = (StackType_t) pxCode; /* CP0_EPC */
pxTopOfStack--;

*pxTopOfStack = (StackType_t) portTASK_RETURN_ADDRESS; /* ra */
pxTopOfStack -= 15; //这里-15 是指-15 个指针的空间, 一个指针占4个字节

*pxTopOfStack = (StackType_t) pvParameters; /* Parameters to pass in. */
pxTopOfStack -= 15; //这里-15 是指-15 个指针的空间, 一个指针占4个字节

return pxTopOfStack;
}

二是注意

事件列表项

xEventListItem. xItemValue等于configMAX_PRIORITIES减去任务A的优先级(为1),即5-1=4。
在这里xItemValue不是直接保存任务优先级,而是保存优先级的补数,这意味着xItemValue的值越大,对应的任务优先级越小。
使用宏listGET_OWNER_OF_HEAD_ENTRY获得列表中的第一个列表项的xItemValue值总是最小(因为是升序排列的),也就是优先级最高的任务!

进入临界区

调用taskENTER_CRITICAL()进入临界区,这是一个宏定义,最终进入临界区的代码由移植层提供。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void vTaskEnterCritical( void )
{
// 禁用中断
portDISABLE_INTERRUPTS();
// 调度器未被挂起
if( xSchedulerRunning != pdFALSE )
{
// 嵌套+1
( pxCurrentTCB->uxCriticalNesting )++;
if( pxCurrentTCB->uxCriticalNesting == 1 )
{
// 这个版本不是中断安全的函数, 如果要在中断中使用, 用 taskENTER_CRITICAL_FROM_ISR 函数
portASSERT_IF_IN_ISR();
}
}
}

与离开临界区呼应, pxCurrentTCB->uxCriticalNesting--, uxCriticalNesting减到0后,启用中断.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void vTaskExitCritical( void )
{
// 调度器未被挂起的情况下
if( xSchedulerRunning != pdFALSE )
{
if( pxCurrentTCB->uxCriticalNesting > 0U )
{
( pxCurrentTCB->uxCriticalNesting )--;

if( pxCurrentTCB->uxCriticalNesting == 0U )
{
portENABLE_INTERRUPTS();
}
}
}
}

注意这里的启用/关闭中断仅仅是把timer的systick 中断禁用了, 全局中断并没有关

1
#define portENABLE_INTERRUPTS()     mips_bissr( SR_HINT5 )

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
static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB )
{
// 进入临界区, list要更新时,不能因systick中断切出
taskENTER_CRITICAL();
{
uxCurrentNumberOfTasks++;
// 没有正在运行的任务时
if( pxCurrentTCB == NULL )
{
pxCurrentTCB = pxNewTCB;
// 该任务是第一个任务
if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 )
{
// 调用函数prvInitialiseTaskLists()初始化全局任务列表
prvInitialiseTaskLists();
}
}
else
{
// 有正在运行的任务, 但是调度器被挂起了, 新任务优先级大于正在运行的任务, 更新pxCurrentTCB为新任务 1. ---->
if( xSchedulerRunning == pdFALSE )
{
if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority )
{
pxCurrentTCB = pxNewTCB;
}
}
}
uxTaskNumber++;
// 调用prvAddTaskToReadyList(pxNewTCB)将创建的任务TCB加入到就绪列表数组中,任务的优先级确定了加入到就绪列表数组的哪个下标。比如我们新创建的任务优先级为1,则这个任务被加入到列表pxReadyTasksLists[1]中。
prvAddTaskToReadyList( pxNewTCB );
portSETUP_TCB( pxNewTCB );
}
taskEXIT_CRITICAL();
// 任务调度器未被挂起, 如果新任务优先级大于当前任务优先级, 直接调度新任务执行, 这里跟1. ---->的处理逻辑是一样的, 调度器挂起了,只能更新当前任务,那么当调度器运行的时候,会自动调度更新后的当前任务 2. ----->
if( xSchedulerRunning != pdFALSE )
{
// 调度
if( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority )
{
// 强制进行一次上下文切换,切换后,新创建的任务将获得CPU控制权
taskYIELD_IF_USING_PREEMPTION();
}
}
}

为第一次运行做必要的初始化

如果这是第一个任务(uxCurrentNumberOfTasks等于1),则调用函数prvInitialiseTaskLists()初始化任务列表。
FreeRTOS使用列表来跟踪任务,在tasks.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
static void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
{
vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) ); // 初始化readyLists readyList的数组, 优先级作index, 数组的每个元素是一个列表
}

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 pxOverflowDelayedTaskList
using list2. */
pxDelayedTaskList = &xDelayedTaskList1; // mips count未溢出的挂到这个列表下
pxOverflowDelayedTaskList = &xDelayedTaskList2; // count 溢出 UINT_MAX 后挂到这个列表下
}

全局当前任务指针变动

1
PRIVILEGED_DATA TCB_t * volatile pxCurrentTCB= NULL;

这个变量用来指向当前正在运行的任务TCB,我们需要多了解一下这个变量。FreeRTOS的核心是确保处于优先级最高的就绪任务获得CPU运行权。
任务切换就是找到优先级最高的就绪任务,而找出的这个最高优先级任务的TCB,就被赋给变量pxCurrentTCB。

任务调度

调度器启动过程分析
任务创建完成后,静态变量指针pxCurrentTCB指向优先级最高的就绪任务。但此时任务并不能运行,因为接下来还有关键的一步:启动FreeRTOS调度器。
调度器是FreeRTOS操作系统的核心,主要负责任务切换,即找出最高优先级的就绪任务,并使之获得CPU运行权。调度器并非自动运行的,需要人为启动它。
vTaskStartScheduler()用于启动调度器,它会创建一个空闲任务、初始化一些静态变量,最主要的,它会初始化系统节拍定时器并设置好相应的中断,然后启动第一个任务。

mips portRESTORE_CONTEXT

接下来看一下 portRESTORE_CONTEXT 怎么调度到函数entry处执行的

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
.macro  portRESTORE_CONTEXT
la s6, uxInterruptNesting # uxInterruptNesting初始值为1, 表示中断嵌套深度
lw s6, (s6)
addiu s6, s6, -1
bne s6, zero, 1f
nop
la s6, uxSavedTaskStackPointer # uxInterruptNesting传入为1的时候,才会走这里, 这个值保存的是pxCurrentTCB任务块信息,第一项保存pxTopofStack
lw s5, (s6) # s5 <- pxCurrentTCB的第一项pxTopofStack uxInterruptNesting传入为1的时候,才会走这里, 取出第一项给s5

/* Restore the context. */
1: lw s6, 8(s5) # 如果传入uxInterruptNesting 不为1, s5在这个函数段里没有更新, 应该是上次中断后遗留的值
mtlo s6 # hi <- [s5+8] 第三项 HI和LO没有编号寄存器,它们仅用于存储不适合单个寄存器的操作结果(例如,将两个32位整数相乘可能会导致64位整数,因此溢出会进入HI)
lw s6, 12(s5)
mthi s6 # lo <- [s5+12] 第四项
lw $1, 16(s5) # $1 是at 寄存器
lw s6, 44(s5) # s5 + 44 11
lw v0, 52(s5)
lw v1, 56(s5)
lw a0, 60(s5)
lw a1, 64(s5)
lw a2, 68(s5)
lw a3, 72(s5)
lw t0, 76(s5)
lw t1, 80(s5)
lw t2, 84(s5)
lw t3, 88(s5)
lw t4, 92(s5)
lw t5, 96(s5)
lw t6, 100(s5)
lw t7, 104(s5)
lw t8, 108(s5)
lw t9, 112(s5)
lw s8, 116(s5)
lw ra, 120(s5)

/* Protect access to the k registers, and others. */
di # 禁用中断, ei 是启用中断. 这里把中断整体屏蔽了,会改status 第0
ehb

/* Decrement the nesting count. */
la k0, uxInterruptNesting
lw k1, (k0)
addiu k1, k1, -1
sw k1, 0(k0) # uxInterruptNesting = uxInterruptNesting - 1

lw k0, portSTATUS_STACK_LOCATION(s5) # s5 + 128 32 # 这个地方正好跟pxPortInitialiseStack 对应上,
lw k1, portEPC_STACK_LOCATION(s5) # s5 + 124 31

/* Leave the stack in its original state. First load sp from s5, then
restore s5 from the stack. */
add sp, zero, s5 # 恢复任务entry 的 sp, sp pxTopOfStack 为基准, 往下增长, 恢复s5, s5存在 pxTopOfStack + 40 (10)的位置
lw s5, 40(sp)
addiu sp, sp, portCONTEXT_SIZE # sp = pTopofStack + 132

mtc0 k0, C0_STATUS # 恢复cp0_status, 这个地方连带着把中断也给恢复了, 如第一次启动任务,status初始为portINITIAL_SR, 是启用了中断的,
# 如果不是第一次启动任务,而是任务执行过程中切出去了, 现在把它切回来,那切出去前的中断应该也是没有屏蔽的
mtc0 k1, C0_EPC # 恢复cp0_epc
ehb
eret # 从cp0_epc处执行, 即调度到pxCurrentTCB的entry处执行, 同时任务堆栈的sp 更新为了 pTopofStack + 132 的地址
nop

.endm