FreeRTOS中任务创建函数xTaskCreate()的解析
函数 xTaskCreate()
此函数用于使用动态的方式创建任务,任务的任务控制块以及任务的栈空间所需的内存,均由 FreeRTOS 从 FreeRTOS 管理的堆中分配,若使用此函数,需要在 FreeRTOSConfig.h 文件中将宏configSUPPORT_DYNAMIC_ALLOCATION 配置为 1。此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。函数原型如下所示:
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, /* 指向任务函数的指针,类型为 void (*TaskFunction_t)( void * ) */
const char *const pcName, /* 任务名,最大长度为 configMAX_TASK_NAME_LEN */
const uint16_t usStackDepth, /* 任务堆栈大小,单位:字(注意,单位不是字节) */
void *const pvParameters, /* 传递给任务函数的参数,若无则填 NULL */
UBaseType_t uxPriority, /* 任务优先级,最大值为(configMAX_PRIORITIES-1) */
TaskHandle_t *const pxCreatedTask); /* 任务句柄,任务成功创建后,pxCreatedTask会保存新任务的任务控制块 */
函数 xTaskCreate()的返回值为pdPASS则表示创建任务成功,返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY则表示任务创建失败(这个错误码实际上是-1)。
要使用该函数的前提是 configSUPPORT_DYNAMIC_ALLOCATION 支持动态内存分配的宏为1,默认情况下是为1的。
以下是该函数的解析:
BaseType_t xTaskCreate(TaskFunction_t pxTaskCode,
const char *const pcName,
const uint16_t usStackDepth,
void *const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *const pxCreatedTask)
{
TCB_t *pxNewTCB;
BaseType_t xReturn;
/* 判断任务堆栈是向上还是向下的增长方式,如果栈是向上增长的就先分配
任务控制块内存再分配任务堆栈的内存,反之如果是向下增长的就先分配任务
栈空间再分配任务控制块内存,宏 portSTACK_GROWTH 用于定义栈的生长方向,
STM32 的栈是向下生长的,因此宏 portSTACK_GROWTH 定义为-1 */
#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)
{
/* Could not allocate the stack. Delete the allocated TCB. */
vPortFree(pxNewTCB);
pxNewTCB = NULL;
}
}
}
/* 否则堆栈是向下增长的,就先分配堆栈内存再分配任务控制块内存,
这样就能确保任务控制块在任务堆栈内存的上方,防止被被任务执行时的堆栈操作所破坏 */
#else /* portSTACK_GROWTH */
{
StackType_t *pxStack;
/* 分配任务堆栈空间 */
pxStack = (StackType_t *)pvPortMalloc((((size_t)usStackDepth) * sizeof(StackType_t)));
if (pxStack != NULL)
{
/* 分配任务控制块内存 */
pxNewTCB = (TCB_t *)pvPortMalloc(sizeof(TCB_t));
if (pxNewTCB != NULL)
{
/* Store the stack location in the TCB. */
pxNewTCB->pxStack = pxStack;
}
else
{
/* The stack cannot be used as the TCB was not created. Free
it again. */
vPortFree(pxStack);
}
}
else
{
pxNewTCB = NULL;
}
}
#endif /* portSTACK_GROWTH */
if (pxNewTCB != NULL)
{
/* 若支持静态分配内存以及支持动态分配内存的宏都为 1
或者MPU(内存保护单元)宏为1(是否启用 MPU 来提供任务保护和隔离)*/
#if (tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0)
{
/* 在同时满足静态、动态创建任务的情况下,标记该任务是动态创建的,
以便之后删除任务时内存释放的操作 */
pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
/* 初始化新任务 */
prvInitialiseNewTask(pxTaskCode, pcName, (uint32_t)usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL);
/* 添加新任务到就绪列表当中 */
prvAddNewTaskToReadyList(pxNewTCB);
/* 返回pdPASS表示创建任务成功 */
xReturn = pdPASS;
}
else
{
xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}
return xReturn;
}
1.该函数首先定义一个任务控制块的指针变量 pxNewTCB 用于保存该新创建的任务的信息和一个返回值 xReturn 用以指示任务创建成功与否。
2.随后进行判断栈的生长方向,根据不同的生长方向先后使用 pvPortMalloc 内存分配函数(这是FreeRTOS内部自己实现的内存分配函数)分配任务控制块的内存与任务堆栈的内存。
3.如果内存分配成功就对新任务进行初始化并将新任务添加到就绪列表当中。
4.最后返回 xReturn 指示任务创建成功与否。
其中需要注意初始化新任务函数和将新任务添加到就绪列表中的函数。
函数 prvInitialiseNewTask()
该函数是用来创建任务时初始化任务控制块中的成员变量的。
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, /* 任务控制块 */
const MemoryRegion_t *const xRegions) /* MPU 相关 */
{
StackType_t *pxTopOfStack;
UBaseType_t x;
/* 是否启用MPU(内存管理单元) */
#if (portUSING_MPU_WRAPPERS == 1)
/* Should the task be created in privileged mode?
任务应该在特权模式下创建吗? */
BaseType_t xRunPrivileged;
/* 在特权模式下创建任务 */
if ((uxPriority & portPRIVILEGE_BIT) != 0U)
{
xRunPrivileged = pdTRUE;
}
else
{
xRunPrivileged = pdFALSE;
}
/* 将特权位清0 */
uxPriority &= ~portPRIVILEGE_BIT;
#endif /* portUSING_MPU_WRAPPERS == 1 */
/* Avoid dependency on memset() if it is not required. */
/* 是否启用相关功能 */
#if ((configCHECK_FOR_STACK_OVERFLOW > 1) || (configUSE_TRACE_FACILITY == 1) || (INCLUDE_uxTaskGetStackHighWaterMark == 1))
{
/* Fill the stack with a known value to assist debugging. */
/* 启用功能则进行内存块修改,将堆栈指针 pxNewTCB->pxStack
所指向的堆栈区域初始化为指定的值,tskSTACK_FILL_BYTE(0xA5)。
这可以用于在任务创建时清理堆栈,用已知值填充堆栈以帮助调试
memset 是对已分配的内存块进行修改的函数,不负责内存的分配和释放 */
(void)memset(pxNewTCB->pxStack, (int)tskSTACK_FILL_BYTE, (size_t)ulStackDepth * sizeof(StackType_t));
}
#endif
/* 如果堆栈是向下增长的模式 */
#if (portSTACK_GROWTH < 0)
{
/* 获取任务栈顶指针 */
pxTopOfStack = (StackType_t *)(((portPOINTER_SIZE_TYPE)pxTopOfStack) & (~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK)));
/* 检查计算得到的堆栈顶部的对齐是否正确 */
configASSERT((((portPOINTER_SIZE_TYPE)pxTopOfStack & (portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK) == 0UL));
}
/*else堆栈是向上增长的*/
#else
{
/* 获取任务栈栈顶地址 */
pxTopOfStack = pxNewTCB->pxStack;
/* 检查计算得到的堆栈顶部的对齐是否正确 */
configASSERT((((portPOINTER_SIZE_TYPE)pxNewTCB->pxStack & (portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK) == 0UL));
// 计算栈顶地址最后位置
pxNewTCB->pxEndOfStack = pxNewTCB->pxStack + (ulStackDepth - (uint32_t)1);
}
#endif /* portSTACK_GROWTH */
/* 将任务名字存储到任务控制块TCB中 */
for (x = (UBaseType_t)0; x < (UBaseType_t)configMAX_TASK_NAME_LEN; x++)
{
pxNewTCB->pcTaskName[x] = pcName[x];
if (pcName[x] == 0x00) /* 如果遇到字符串结束符号\0就跳出循环,因为已经复制任务名字结束了 */
{
break;
}
else
{
mtCOVERAGE_TEST_MARKER(); /* 用于测试代码覆盖率,并不影响实际功能 */
}
}
/* 在任务名成员变量末尾加上'\0' */
pxNewTCB->pcTaskName[configMAX_TASK_NAME_LEN - 1] = '\0';
/* uxPriority 被用作数组索引,因此必须确保它不会太大
检测任务优先级是否超过了最大的允许范围,超过了就对其进行限制 */
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));
/* 初始化任务状态列表项的拥有者为新的任务控制块 */
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);
/* 判断是否允许任务临界区的嵌套深度的层数保存在任务控制块的字段当中,允许则清零层数 uxCriticalNesting */
#if (portCRITICAL_NESTING_IN_TCB == 1)
{
/* 任务单独临界区嵌套计数器初始化为 0 */
pxNewTCB->uxCriticalNesting = (UBaseType_t)0U;
}
#endif /* portCRITICAL_NESTING_IN_TCB */
/* 判断是否使用应用任务标签,允许则先使标签为NULL */
#if (configUSE_APPLICATION_TASK_TAG == 1)
{
pxNewTCB->pxTaskTag = NULL;
}
#endif /* configUSE_APPLICATION_TASK_TAG */
/* 运行时间统计功能 */
#if (configGENERATE_RUN_TIME_STATS == 1)
{
/* 计数清零 */
pxNewTCB->ulRunTimeCounter = 0UL;
}
#endif /* configGENERATE_RUN_TIME_STATS */
/* 判断是否使用 MPU。MPU的目的是提供一种方便且安全的方式来管理任务的内存访问权限
从而增强系统的可靠性和安全性 */
#if (portUSING_MPU_WRAPPERS == 1)
{
vPortStoreTaskMPUSettings(&(pxNewTCB->xMPUSettings), xRegions, pxNewTCB->pxStack, ulStackDepth);
}
#else
{
/* Avoid compiler warning about unreferenced parameter. */
(void)xRegions;
}
#endif
/* 判断是否进行初始化线程本地存储指针的数量和相应的操作 */
#if (configNUM_THREAD_LOCAL_STORAGE_POINTERS != 0)
{
for (x = 0; x < (UBaseType_t)configNUM_THREAD_LOCAL_STORAGE_POINTERS; x++)
{
pxNewTCB->pvThreadLocalStoragePointers[x] = NULL;
}
}
#endif
/* 任务通知功能 */
#if (configUSE_TASK_NOTIFICATIONS == 1)
{
pxNewTCB->ulNotifiedValue = 0; // 设置为0是为了在新任务开始时没有之前的通知状态,为任务的通知机制提供一个干净的起点
pxNewTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION; // 表明此时任务不处于等待任务通知的状态
}
#endif
/* 是否启用Newlib,使用Newlib可以确保每个任务拥有自己的独立状态,能够正确使用 Newlib 提供的库函数 */
#if (configUSE_NEWLIB_REENTRANT == 1)
{
/* Initialise this task's Newlib reent structure. */
_REENT_INIT_PTR((&(pxNewTCB->xNewLib_reent)));
}
#endif
/* 是否启用中断 延时等待功能,启用中断延时等待功能即可以在任务在进行delay延时还没结束的时候,此时可
能有一个紧急事件的到来,需要立即执行,所以中断延时等待功能就会立即打断此刻延时,任务不再是阻塞态
立即恢复到可运行状态 */
/* 注意:中断延时等待功能通常只能被任务本身使用来打断自己的延时函数的执行
而无法被中断本身使用来打断正在运行的中断 */
#if (INCLUDE_xTaskAbortDelay == 1)
{
pxNewTCB->ucDelayAborted = pdFALSE; // 表示任务目前没有中断延时等待
}
#endif
#if (portUSING_MPU_WRAPPERS == 1)
{
pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters, xRunPrivileged);
}
#else /* portUSING_MPU_WRAPPERS */
{
/* 配置栈顶指针 */
pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters);
}
#endif /* portUSING_MPU_WRAPPERS */
if ((void *)pxCreatedTask != NULL)
{
/* 将新任务的任务句柄赋值给 pxCreatedTask 这个参数,调用者就能通过任务句柄修改优先级,删除任务等 */
*pxCreatedTask = (TaskHandle_t)pxNewTCB;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
1.先获取了任务栈顶指针,为方便后续任务栈的初始化进行操作。
2.将任务名字存储到任务控制块当中,这通常用于调试使用。
3.检测任务优先级是否超过设定范围,若超过则对其进行边界控制。
4.初始化任务的状态、事件列表项,初始化任务状态、事件列表项的所属者为当前任务控制块。
5.初始化任务栈栈顶指针。
6.最后将新的任务控制块赋值给形参pxCreatedTask。
该函数内部还通过调用 pxPortInitialiseStack() 函数初始化任务栈,就是往任务的栈中写入一些重要的信息,这些信息会在任务切换的时候被弹出到 CPU 寄存器中,以恢复任务的上下文信息,这些信息就包括 xPSR 寄存器的初始值、任务的函数地址(PC 寄存器)、任务错误退出函数地址(LR 寄存器)、任务函数的传入参数(R0 寄存器)以及为 R1~R12 寄存器预留空间,若使用了浮点单元,那么还会有EXC_RETURN 的值。同时该函数会返回更新后的栈顶指针。针对 ARM Cortex-M3 和针对 ARM Cortex-M4 和 ARM Cortex-M7 内核的函数 pxPortInitialiseStack() 稍有不同,原因在于 ARM Cortex-M4 和 ARM Cortex-M7 内核具有浮点单元,因此在任务栈中还需保存浮点寄存器的值。
函数 pxPortInitialiseStack()
StackType_t *pxPortInitialiseStack(StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters)
{
/* 将栈顶指针递减一个单位,以便为接下来存储的数据腾出空间,这一步是为了在向下生长的堆栈分配正确的空间 */
pxTopOfStack--;
*pxTopOfStack = portINITIAL_XPSR; /* xPSR 该值表示 程序状态寄存器xPSR 的初始值 */
pxTopOfStack--;
/* 将任务函数的地址存储到目前栈顶指针位置 */
*pxTopOfStack = ((StackType_t)pxCode) & portSTART_ADDRESS_MASK; /* PC */
pxTopOfStack--;
/* 将prvTaskExitError的值存储到栈顶指针所指向的位置,表示任务退出时跳转的地址LR */
*pxTopOfStack = (StackType_t)prvTaskExitError; /* LR */
/* 递减5个单位,为接下来的寄存器值腾出空间 */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
/* 将任务参数 pvParameters 存储到栈顶指针所指向的位置,表示第一个通用寄存器 R0 的初始值 */
*pxTopOfStack = (StackType_t)pvParameters; /* R0 */
/* 递减8个单位,为后续寄存器值腾出空间 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
/* 返回更新好的栈顶指针,这样任务的堆栈在初始化过程中被正确的设置为了各个寄存器的初始值和其他必要的消息
以便在上下文发生时能够正确的保存和恢复任务的状态(出栈入栈)*/
return pxTopOfStack;
}
可以看到 pxPortInitialiseStack()函数主要是对栈指针进行一些必要的初始化与更新,最后返回更新好的栈顶指针并保存在新创建的任务控制块中,便于在上下文切换发生时能够正确的保存与恢复状态。
/* 配置栈顶指针 */
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
函数 pxPortInitialiseStack()初始化后的任务栈如下图所示:
函数 prvAddNewTaskToReadyList()
函数 prvAddNewTaskToReadList()用于将新建的任务添加到就绪态任务列表中,具体的代码如下所示:
static void prvAddNewTaskToReadyList(TCB_t *pxNewTCB)
{
/* 进入临界区,确保在操作就绪态任务列表时,中断不会访问列表 */
taskENTER_CRITICAL();
{
/* 就绪列表中当前任务数量加1 */
uxCurrentNumberOfTasks++;
/* 此全局变量用于指示当前系统中处于就绪态任务中优先级最高的任务
如果为空,即表示当前创建的任务为系统中的唯一的就绪任务 */
if (pxCurrentTCB == NULL)
{
/* 没有其他就绪任务,或者所有其他任务都在挂起状态,那么优先级最高的就绪态任务就为当前任务 */
pxCurrentTCB = pxNewTCB;
/* 判断是否是第一个被创建的任务或者在没有其他活动任务的情况下创建的新任务,那么就执行
初始化任务列表,仅在第一个任务被创建时执行一次 */
if (uxCurrentNumberOfTasks == (UBaseType_t)1)
{
/* 第一次创建任务就初始化一些任务列表,比如延时任务列表、挂起任务列表、等待删除任务列表等等 */
prvInitialiseTaskLists();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* 不是第一次创建的任务 */
else
{
/* 如果调度器没有在运行,就判断当前任务优先级是否低于新创建的任务的任务优先级
如果低于, 当任务调度器为运行时将 pxCurrentTCB 更新为优先级最高的就绪态任务 */
if (xSchedulerRunning == pdFALSE)
{
if (pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority)
{
pxCurrentTCB = pxNewTCB;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* 这个是为新创建的任务分配一个唯一的任务号,每新创建一个任务都会++以确保每个任务都有一个唯一的标识号
与uxCurrentNumberOfTasks不同,它是该列表中实际的任务数量 */
uxTaskNumber++;
/* 可视化跟踪 */
#if (configUSE_TRACE_FACILITY == 1)
{
/* Add a counter into the TCB for tracing only. */
/* 将该标识号保存的任务控制块中以便可视化跟踪使用,其实就是任务编号 */
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)
{
/* 触发任务调度器进行任务切换,实际就是触发PensSV中断进行任务切换 */
taskYIELD_IF_USING_PREEMPTION();
}
else
{
/* 测试代码覆盖率的工具 */
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
总结
总的来说 xTaskCreate() 函数的流程就是:
1.根据堆栈生长方式为任务控制块(实际上就是一个结构体,里面保存着当前任务的各个信息)、任务运行时的栈大小进行分配空间。其中STM32的栈是向下增长的所以就先分配任务栈内存然后再分配任务控制块内存。
2.接着就调用 prvInitialiseNewTask() 函数对新创建的任务进行初始化,主要是对任务控制块中的成员进行初始化,以及初始化任务的状态列表项、事件列表项,内部再调用 pxPortInitialiseStack() 函数初始化任务栈指针,主要是更新栈指针为任务进行上下文切换时任务状态的保存与恢复做准备。
3.随后就是调用 prvAddNewTaskToReadyList() 函数将任务添加到就绪列表项中,当前任务就处于了就绪状态,若当前没有比它更高优先级的任务它将会被执行。