写在前面
本文是对FreeRTOS中任务句柄、任务控制块TCB、任务栈、任务、就绪表详解。
一、裸机和RTOS中函数存储位置详解
- 左图为裸机开发时 RAM 的使用情况,右图是使用了 FreeRTOS 后 RAM 的使用情况(图片来自野火)。
- 无论是裸机开发还是FreeRTOS,程序都需要存放在RAM中以便执行。不过,在裸机开发环境下,程序员需要手动管理和分配内存,而在FreeRTOS中,操作系统会自动管理内存。
二、什么是任务句柄
任务句柄(Task Handle)是在 FreeRTOS 中用于标识和引用任务的数据类型。每个创建的任务都会分配一个唯一的任务句柄,通过该句柄可以对任务进行操作和管理。
任务句柄是一个指向任务控制块(Task Control Block,TCB)的指针。任务控制块是 FreeRTOS 中用于描述和管理任务的数据结构,包含了任务的状态、优先级、堆栈等信息。
使用任务句柄,可以通过 FreeRTOS 提供的 API 函数对任务进行操作,例如挂起(suspend)、恢复(resume)、删除(delete)任务,或者查询任务的状态等。另外,任务句柄还可以用于任务通信和同步的机制,例如向任务发送信号量或消息。
在创建任务时,通过调用 FreeRTOS 提供的任务创建函数(例如 xTaskCreate()
)可以获取到相应任务的句柄。你可以将该句柄保存在一个变量中,以便后续对该任务进行操作或引用。
例如,以下示例演示了如何创建一个任务并获取其句柄:
// 创建任务
TaskHandle_t xTaskHandle;
xTaskCreate(taskFunction, "Task", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, &xTaskHandle);
// 使用任务句柄进行操作
vTaskSuspend(xTaskHandle); // 挂起任务
vTaskResume(xTaskHandle); // 恢复任务
vTaskDelete(xTaskHandle); // 删除任务
在上述示例中,xTaskCreate()
函数创建了一个名为 “Task” 的任务,并将该任务的任务句柄保存在 xTaskHandle
变量中。然后,我们可以使用任务句柄对任务进行挂起、恢复和删除操作。
任务句柄提供了一种有效的方式来管理和操作 FreeRTOS 中的任务。通过使用任务句柄,可以方便地对任务进行控制和监视。
三、概念图解
四、函数详解
1.任务创建 static void prvInitialiseNewTask(…)
- 这个函数用于创建新的任务,其中的 “prv” 表示该函数是一个私有函数,只用于内部处理和初始化新任务的操作。对于外部使用者来说,应该使用公开的 API 函数来创建和管理任务,而不是直接调用 “prvInitialiseNewTask”。
- 这个函数被 TaskHandle_t xTaskCreateStatic() 函数调用
- 函数源代码如下:
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode, /* 任务入口 */
const char * const pcName, /* 任务名称,字符串形式 */
const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */
void * const pvParameters, /* 任务形参 */
TaskHandle_t * const pxCreatedTask, /* 任务句柄 */
TCB_t *pxNewTCB ) /* 任务控制块指针 */
{
StackType_t *pxTopOfStack;
UBaseType_t x;
/* 获取栈顶地址 */
pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
//pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
/* 向下做8字节对齐 */
pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );
/* 将任务的名字存储在TCB中 */
for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
{
pxNewTCB->pcTaskName[ x ] = pcName[ x ];
if( pcName[ x ] == 0x00 )
{
break;
}
}
/* 任务名字的长度不能超过configMAX_TASK_NAME_LEN */
pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';
/* 初始化TCB中的xStateListItem节点 */
vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
/* 设置xStateListItem节点的拥有者 */
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
/* 初始化任务栈 */
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
/* 让任务句柄指向任务控制块 */
if( ( void * ) pxCreatedTask != NULL )
{
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
}
}
- 获取栈顶地址:
pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
栈顶 = 栈起始地址 + 栈大小 -1
- 获取到的栈顶地址需要做 8 字节对齐
pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );
~
是按位取反运算符。它会反转操作数的每一位,将所有的0变为1,将所有的1变为0。
在给定的代码中,~
运算符用于创建一个掩码,该掩码在对齐操作中用于清除特定位的值。
( ~( ( uint32_t ) 0x0007 ) )
在这里,0x0007
是一个表示二进制数 0000 0111
的十六进制数,它具有最低的3位都是1,其他位都是0。通过 ~
运算符对 0x0007
进行按位取反,得到的掩码就是所有最低的3位都是0,其他位都是1。
这样,当掩码与 pxTopOfStack
进行按位与操作时,最低的3位将被清零,而其他位将保持不变,pxTopOfStack 变量就被更新为按照8字节对齐的地址。
通常,在某些特定的编程环境中,需要按照特定的内存对齐要求来访问数据。这段代码将 pxTopOfStack 指针变量按照8字节对齐,以满足特定的对齐要求。
2.初始化任务栈 StackType_t *pxPortInitialiseStack(…)
通过栈顶指针对整个栈进行初始化,分为自动加载内容和手动加载内容。
- 代码如下:
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
/* 异常发生时,自动加载到CPU寄存器的内容 */
pxTopOfStack--;
*pxTopOfStack = portINITIAL_XPSR; /* xPSR的bit24必须置1 */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC,即任务入口函数 */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR,函数返回地址 */
pxTopOfStack -= 5; /* R12, R3, R2 and R1 默认初始化为0 */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0,任务形参 */
/* 异常发生时,手动加载到CPU寄存器的内容 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4默认初始化为0 */
/* 返回栈顶指针,此时pxTopOfStack指向空闲栈 */
return pxTopOfStack;
}
3.初始化任务就绪列表
任务就绪列表的定义
任务就绪列表(Task Ready List)是用于存储当前准备就绪状态的任务的数据结构。
任务就绪列表是一个由多个优先级队列组成的数据结构,其中每个优先级队列维护了相同优先级的就绪任务。通过任务就绪列表,操作系统可以快速找到具有最高优先级的就绪任务,并将其调度到正在运行的任务。
当一个任务变为就绪状态时,它将被插入到适当的就绪列表中,而当一个任务被调度执行时,它将从就绪列表中被移除。
每个列表中存储相同优先级的任务,最大支持256个优先级,也就是最大有256个列表。
- 定义5个优先级的任务就绪列表的代码:
#define configMAX_PRIORITIES ( 5 ) //最大列表数量
/* 任务就绪列表 */
List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; //定义了5个任务就绪列表
任务就绪列表的初始化
循环调用列表初始化函数 vListInitialise()
进行初始化即可。
- 代码如下:
/* 初始化任务相关的列表 */
void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
{
vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) ); //初始化每个就绪列表
}
}
五、任务创建与初始化方法
1.定义任务栈大小,并定义任务栈存放任务上下文
//定义任务栈
#define TASK1_STACK_SIZE 20
StackType_t Task1Stack[TASK1_STACK_SIZE];
2.定义任务控制块TCB
//定义任务控制块
TCB_t Task1TCB;
3.定义任务句柄(用于指向TCB)
//定义任务句柄
TaskHandle_t Task1_Handle;
4.定义任务函数并声明
void Task1_Entry( void *p_arg );
//定义任务函数(无限循环不返回)
void Task1_Entry(void *p_arg)
{
for(;;){
//此处书写任务代码
}
}
5.在main函数中,初始化所有的任务就绪列表
prvInitialiseTaskLists(); //初始化所有的任务就绪列表
6.在main函数中,创建任务,并使任务句柄指向TCB
//任务创建函数的函数原型:
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, /* 任务入口 */
const char * const pcName, /* 任务名称,字符串形式 */
const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */
void * const pvParameters, /* 任务形参 */
StackType_t * const puxStackBuffer, /* 任务栈起始地址 */
TCB_t * const pxTaskBuffer ); /* 任务控制块指针 */
//创建任务,并使任务句柄指向TCB
Task1_Handle = xTaskCreateStatic(Task1_Entry,
"Task1",
TASK1_STACK_SIZE,
NULL,
Task1Stack,
&Task1TCB);
7.将任务控制块中的任务项插入一个就绪列表中
vListInsert(&pxReadyTasksLists[1], &Task1TCB.xStateListItem);
后记
如果您觉得本文写得不错,可以点个赞激励一下作者!
如果您发现本文的问题,欢迎在评论区或者私信共同探讨!
共勉!