本文测试环境:硬件基于STM32F103RET6,FreeRTOS版本基于FreeRTOSv10.2.1,开发环境基于MDK529
FreeRTOS任务的创建有两种方法,一种是静态创建,另一种是动态创建。其实队列,信号量等都有静态和动态方式创建。
静态创建:需要的内存由程序员指定。在编译期间就已经确定。好处就是一定可以创建成功(如果内存不足,程序编译报错)。坏处就是内存在编译期间就指定出去,指定出去的内存不能回收,无法重复利用。
动态创建:程序在运行期间由内存申请函数在堆中申请内存。好处就是内存可以重复利用。举个例子,A任务删除以后,操作系统回收了A任务的内存,在某一时刻又要去创建B任务,此时操作系统就可以把回收来的内存再次分配出去,供B任务使用。坏处就是在创建的时候有可能分配不到内存,造成创建失败。
具体使用哪种创建方式要视情况而定,安全要求高的场合就使用静态方式。内存小的MCU就使用动态方式创建。
本文着重讨论动态方式创建。
创建任务函数
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask ) /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
{
TCB_t *pxNewTCB;
BaseType_t xReturn;
/* If the stack grows down then allocate the stack then the TCB so the stack
does not grow into the TCB. Likewise if the stack grows up then allocate
the TCB then the stack. */
#if( portSTACK_GROWTH > 0 )//判断堆栈的增长方向 >0向上增长
{
/* Allocate space for the TCB. Where the memory comes from depends on
the implementation of the port malloc function and whether or not static
allocation is being used. */
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );
if( pxNewTCB != NULL )
{
/* Allocate space for the stack used by the task being created.
The base of the stack memory stored in the TCB so the task can
be deleted later if required. */
pxNewTCB->pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
if( pxNewTCB->pxStack == NULL )
{
/* Could not allocate the stack. Delete the allocated TCB. */
vPortFree( pxNewTCB );
pxNewTCB = NULL;
}
}
}
#else /* portSTACK_GROWTH */
{
StackType_t *pxStack;
/* Allocate space for the stack used by the task being created. */
//为任务申请堆栈内存
pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
if( pxStack != NULL )
{
/* Allocate space for the TCB. */
//申请任务控制块内存
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); /*lint !e961 MISRA exception as the casts are only redundant for some paths. */
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 )//判断任务堆栈和任务控制块内存是否都申请成功
{
#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 */
//初始化一个新的任务 主要是初始化TCB中各个成员变量的值
prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
prvAddNewTaskToReadyList( pxNewTCB );//添加新的任务到就绪列表中
xReturn = pdPASS;
}
else
{
xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}
return xReturn;
}
创建流程:
1.申请Stack和TCB所需要的内存
2.初始化一个新的任务
3.将任务添加到就绪列表中
先来看第一步内存申请。申请内存的方式根据MCU的堆栈生长方向分为两种情况。向上生长:先申请TCB,再去申请Stack;向下生长:先申请Stack。再去申请TCB。为什么要这样呢?我们看下图:
箭头表示堆栈的生长方向。一般情况下程序员要保证任务在运行过程中堆栈不溢出。如果程序在运行过程中堆栈确实是溢出了,那么在向上生长方式中,先分配TCB再分配Stack,这样堆栈溢出就不会伤及TCB了。向下生长方式同理,也不会伤及TCB。这种分配方式只是让代码变得健壮(千万不要认为这样堆栈就可以溢出了)。
申请到的堆栈起始地址赋值到TCB的Stack成员中,记录下堆栈的起始地址。
再看初始化任务:
初始化任务就是给TCB的成员赋值。
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 ) /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
{
StackType_t *pxTopOfStack;
UBaseType_t x;
#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;
}
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. */
//如果使能了上边的功能就将任务堆栈全部内存设置为一个指定的数字
//以后判断任务堆栈是否溢出就可以通过判断栈底指针的数据是否是被改写
( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) ulStackDepth * sizeof( StackType_t ) );
}
#endif /* ( ( configCHECK_FOR_STACK_OVERFLOW > 1 ) || ( ( configUSE_TRACE_FACILITY == 1 ) || ( INCLUDE_uxTaskGetStackHighWaterMark == 1 ) ) ) */
/* 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 )
{
pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );//计算出栈顶地址
//栈顶地址内存对齐 目的是MCU能快速处理
pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) ); /*lint !e923 MISRA exception. Avoiding casts between pointers and integers is not practical. Size differences accounted for using portPOINTER_SIZE_TYPE type. */
/* Check the alignment of the calculated top of stack is correct. */
configASSERT( ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack & ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) == 0UL ) );
}
#else /* portSTACK_GROWTH */
{
pxTopOfStack = pxNewTCB->pxStack;
/* Check the alignment of the stack buffer is correct. */
configASSERT( ( ( ( portPOINTER_SIZE_TYPE ) pxNewTCB->pxStack & ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) == 0UL ) );
/* The other extreme of the stack space is required if stack checking is
performed. */
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 ];
/* Don't copy all configMAX_TASK_NAME_LEN if the string is shorter than
configMAX_TASK_NAME_LEN characters just in case the memory after the
string is not accessible (extremely unlikely). */
if( pcName[ x ] == 0x00 )
{
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* Ensure the name string is terminated in the case that the string length
was greater or equal to configMAX_TASK_NAME_LEN. */
pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';//任务名字结尾处添加 '\0'字符
/* This is used as an array index so must ensure it's not too large. First
remove the privilege bit if one is present. */
//如果传入的任务优先级大于系统最大的优先级 那么将任务优先级设置到最大优先级
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 ) );//初始化状态列表项 pvContainer = NULL 该节点不挂载到任何列表中
vListInitialiseItem( &( pxNewTCB->xEventListItem ) );//初始化事件列表项
/* Set the pxNewTCB as a link back from the ListItem_t. This is so we can get
back to the containing TCB from a generic item in a list. */
//设置状态列表项
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB ); //pvOwner指向了pxNewTCB 拥有xStateListItem节点的内核对象是pxNewTCB
/* Event lists are always in priority order. */
listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB ); //pvOwner指向了pxNewTCB 拥有xEventListItem节点的内核对象是pxNewTCB
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
{
pxNewTCB->uxCriticalNesting = ( UBaseType_t ) 0U;
}
#endif /* portCRITICAL_NESTING_IN_TCB */
#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 */
#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;
pxNewTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
}
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
/* Initialise this task's Newlib reent structure. */
_REENT_INIT_PTR( ( &( pxNewTCB->xNewLib_reent ) ) );
}
#endif
#if( INCLUDE_xTaskAbortDelay == 1 )
{
pxNewTCB->ucDelayAborted = pdFALSE;
}
#endif
/* Initialize the TCB stack to look as if the task was already running,
but had been interrupted by the scheduler. The return address is set
to the start of the task function. Once the stack has been initialised
the top of stack variable is updated. */
#if( portUSING_MPU_WRAPPERS == 1 )
{
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters, xRunPrivileged );
}
#else /* portUSING_MPU_WRAPPERS */
{
//pxNewTCB->pxTopOfStack 表示的是当前任务栈顶指针所指的地方 是动态的,并不是上方计算出来的
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );//初始化任务堆栈,返回栈顶指针的值
}
#endif /* portUSING_MPU_WRAPPERS */
if( ( void * ) pxCreatedTask != NULL )
{
/* Pass the handle out in an anonymous way. The handle can be used to
change the created task's priority, delete the created task, etc.*/
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;//将任务控制块的地址赋值给任务句柄
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
函数中重要的代码都已给出注释。
listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );
为TCB的事件列表项赋值。在FreeRTOS中,优先级数字越大优先级越高;列表排序是从小到大排序。通过最大优先级数减去优先级就可以实现高优先级排在列表前边,低优先级排在列表后边。如果多个不同优先级的任务在等待同一个事件,当事件发生时,由于高优先级任务排在列表前边,就可以实现高优先级任务优先响应事件。
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
初始化任务堆栈,返回栈顶指针的值
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. */
//STM32 栈是向下生长的 所以栈顶指针是减减
pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */ //xPSR和51单片机中的PSW是一个意思 都是程序状态寄存器
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
//注意此处不是将pvParameters放入R0寄存器 而是将pvParameters放入任务堆栈中,等下次任务恢复
//的时候将pvParameters恢复到R0寄存器中 此函数中其他代码同理
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return pxTopOfStack;
}
把要赋到CPU寄存器的值保存到堆栈中。在任务切换的时候从堆栈中把对应寄存器的值恢复到CPU寄存器中(恢复现场)。
细心的大兄弟可能发现cortex-M3内核不止这几个寄存器,这里为什么只有这几个寄存器?其实这写寄存器是自动恢复的。这里涉及到内核架构知识,我对内核架构并不懂。不敢多说,误导别人就不好了。
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;//将任务控制块的地址赋值给任务句柄
通过上边的代码可以看出任务句柄指向了任务控制块的地址。所以在实际使用中我们拥有了任务句柄,就等于拥有了任务控制块,就可以操作任务了。
创建任务的最后一个函数void prvAddNewTaskToReadyList( TCB_t *pxNewTCB )添加一个新的任务到就序列表中。
static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB )
{
/* Ensure interrupts don't access the task lists while the lists are being
updated. */
taskENTER_CRITICAL();
{
uxCurrentNumberOfTasks++;//记录系统中任务的个数 创建++ 删除--
//如果正在创建第一个任务,以前系统没有任务存在 此时pxCurrentTCB是NULL
if( pxCurrentTCB == NULL )
{
/* There are no other tasks, or all the other tasks are in
the suspended state - make this the current task. */
pxCurrentTCB = pxNewTCB;
//判断一下系统中任务的个数,如果任务的个数是1,那么就是第一个创建的任务,
//而不是所有任务都挂起的情况 因为所有任务都挂起时pxCurrentTCB也指向了NULL
if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 )
{
/* This is the first task to be created so do the preliminary
initialisation required. We will not recover if this call
fails, but we will report the failure. */
//如果创建的是第一个任务初始化FreeRTOS内核列表
prvInitialiseTaskLists();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else//系统中有其他任务
{
/* If the scheduler is not already running, make this task the
current task if it is the highest priority task to be created
so far. */
//判断一下任务调度器是否在运行
if( xSchedulerRunning == pdFALSE )
{
//新创建的任务优先级是否大于系统中正在运行任务的优先级
if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority )
{
pxCurrentTCB = pxNewTCB;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
uxTaskNumber++;//任务号++ 创建的时候++,删除的时候不--
#if ( configUSE_TRACE_FACILITY == 1 )
{
/* Add a counter into the TCB for tracing only. */
pxNewTCB->uxTCBNumber = uxTaskNumber;//为任务标号赋值,创建一个任务uxTaskNumber加加一次
}
#endif /* configUSE_TRACE_FACILITY */
traceTASK_CREATE( pxNewTCB );
prvAddTaskToReadyList( pxNewTCB );
portSETUP_TCB( pxNewTCB );
}
taskEXIT_CRITICAL();
if( xSchedulerRunning != pdFALSE )//如果任务调度器是开启的
{
/* If the created task is of a higher priority than the current task
then it should run now. */
if( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority )//新创建任务的优先级是否高于当前运行任务的优先级
{
taskYIELD_IF_USING_PREEMPTION();//进行一次任务切换
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
变量uxCurrentNumberOfTasks代表了系统中目前共有多少个任务,该变量在任务创建的时候加加,删除的时候减减。
如果系统在正在创建第一个任务就调用prvInitialiseTaskLists()函数初始化列表。函数内部初始化的列表包括就序列表,延时列表,挂起列表等等。
uxTaskNumber变量是每创建一个任务就加加一次,pxNewTCB->uxTCBNumber = uxTaskNumber; 因此每个任务控制块中的uxTCBNumber成员的值都是独一无二的。利用uxTCBNumber成员变量可以调试跟踪任务。
最后就是prvAddTaskToReadyList( );将任务真正的添加到就绪列表中。当添加到就绪列表中后,任务调度器就会在合适的时机调度任务。该函数是一个宏。
#define prvAddTaskToReadyList( pxTCB ) \
taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); \
vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \
可以看到该宏完成了两件事:
1.将对应任务的优先级标志位置1(这里默认是使用硬件方式确定就绪任务)。
taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );
#define taskRECORD_READY_PRIORITY( uxPriority ) portRECORD_READY_PRIORITY( uxPriority, uxTopReadyPriority )
#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )
最终就是将uxTopReadyPriority变量中的第uxPriority位置1.
2.将任务的状态列表项插入到就绪列表的末尾(任务的状态是通过状态列表项确定的)。就绪时将任务状态列表项挂载到就绪列表中,挂起时将任务状态列表项挂载到挂起列表中。
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
定义就绪列表数组,FreeRTOS每个优先级下都有一条就绪列表。而每个列表挂载的任务个数不限制,因此FreeRTOS中也就没有最大任务个数这回事了。
举个例子:如果configMAX_PRIORITIES 定义为32,那么系统中就有32条就序列表。每个列表下可以挂载N个任务。