依照FreeRTOS用户的操作顺序,我们一般先创建任务,分配好任务的调用栈,再去开启任务调度器进行任务调度。所以,这篇文章就从FreeRTOS的创建任务开始,细致整理一下FreeRTOS。如有错误,请大佬指正!
那么任务创建分为动态创建和静态创建两种,这里就以动态创建(因为使用最多)为例,分析一下FreeRTOS的源码,看看这里面都在做什么事情。
首先进入xTaskCreate(...)API内部,第一步,会去使用pvPortMalloc
(类似malloc一样的申请内存函数)申请到一片名为pxStack
的内存部分,这部分就作为这个任务的调用栈,用来保存任务内部函数的局部变量、函数调用信息、任务的上下文切换等。
pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) );
第二步,申请一块内存为pxNexTCB
,用来存储任务信息(比如这个任务的调用栈地址、当前任务在什么列表中等)。
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );
具体TCB存储着什么任务信息,需要去TCB_这个结构体中去详细看,这里简单列在下面:
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; //任务栈栈顶,必须为TCB第一个成员
#if ( portUSING_MPU_WRAPPERS == 1 )
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 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
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;
#if ( configUSE_NEWLIB_REENTRANT == 1 )
struct _reent xNewLib_reent;
#endif
//任务通知值:ulNotifiedValue
#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;
没有标注的是作者没有使用过,没有了解过或是比较少用的一些,如果需要了解更深入,自行百度一下,这里作者实力有限。
第三步,对新任务进行初始化,也就是调用了这个函数prvInitialiseNewTask(...)
,在这个函数内部除了对任务的一些优先级、名字、任务句柄等赋值任务的事件、状态列表项初始化 ,最重要的是,在其内部有这么一个函数pxPortInitialiseStack(...)
,这个函数的源码如下(ps:这个是我在M4内核开发例程代码找到的源码,我在vivado SDK中的源码中A9的不太一样):
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
/* Simulate the stack frame as it would be created by a context switch
interrupt. */
/* Offset added to account for the way the MCU uses the stack on entry/exit
of interrupts, and to ensure alignment. */
pxTopOfStack--;
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */
/* Save code space by skipping register initialisation. */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
/* A save method is being used that requires each task to maintain its
own exec return value. */
pxTopOfStack--;
*pxTopOfStack = portINITIAL_EXEC_RETURN;
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return pxTopOfStack;
}
这里至少清楚,为什么说这个申请来的内存是作为任务的调用栈,因为这些信息都是cpu运作该任务结束后保存下来的现场,这里给这些地方先做清理也就是初始化,并且这里有个细节为pxCode
在PC寄存器上,也就是说这个pxCode
参数是这个任务的任务函数,这里就可以知道任务地址了。
那么这里就说一下,处理器内部寄存器组的结构:
R0-R7:Low通用寄存器,可以被任何指令访问
R8-R12:High通用寄存器,不能被某些thumb指令访问
R13:stacker pointer,记录当前调用栈的地址,简称为sp,那么sp又分成msp和psp,psp一般为正常线程正在执行的指令所在地址,msp则记录关于中断的....
R14:linker register,俗称LR,这个寄存器的作用用来保证程序在结束函数调用后返回到原来的代码执行处
R15:pc,存储执行指令的地址
第四步,做的就是将这个任务添加至就绪队列中,prvAddNewTaskToReadyList(...)
也就是执行当前这个API。在这里面,最开始会检查是不是第一个任务,因为涉及到FreeRTOS各个列表需要初始化,如果需要初始化则需要执行prvInitialiseTaskLists();,在这里我们就可以观察到FreeRTOS的一些基础列表了,源码如下:
static void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
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 pxOverflowDelayedTaskList
using list2. */
pxDelayedTaskList = &xDelayedTaskList1;
pxOverflowDelayedTaskList = &xDelayedTaskList2;
}
这里观察到 32个优先级对应的就序列表(优先级一般设置成32),两个延时列表,一个xPendingReadyList列表(存储那些已经准备好可以执行,但由于某些原因,比如优先级更高的任务正在执行,而还没有被调度执行的任务),一个任务悬挂suspend列表,一个xTasksWaitingTermination列表(管理等待终止任务的角色)。
如果创建的任务已经不只是1个了,并且任务调度器处于停止调度状态,会先将当前新添加任务的优先级和之前正在执行的任务进行优先级的对比,如果当前新添加的任务优先级高,则抢占取代,使用PendSV中断切换任务。之后会执行prvAddTaskToReadyList( pxNewTCB );
,该API源码如下:
#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 )
这里其实就是根据当前任务的优先级,将任务分配到pxReadyTasksLists[n]
列表中,当然这种就序列表共有32个,每个优先级都有对应的就序列表。从这里可以发现到从TCB到任务列表的一系列关系,对应关系为:任务TCB的xStateListItem
为当前任务的状态列表项,记录了改项前后的列表项信息、所在列表信息、及所属TCB信息,那么xStateListItem
中的pvOwner
指针指向TCB,pvContainer
指向所属列表。
至此,创建任务的部分就结束了。下期将会整理任务调度器开启调度的部分。欢迎大家指正错误,相互学习!之后我会将自己的学习笔记通过链接的方式放在文章上,对部分细节会更加细致。