FreeRTOS源码解析:prvInitialiseNewTask函数深度剖析
该函数所在位置FreeRTOS/task.c
1、函数名及参数分析
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 )
不得不说FreeRTOS源码的函数命名很耿直,读函数名就能知道这个函数是要干什么的。前缀prv
表示private,表示这个函数有static修饰。Initialise New Task翻译过来就是初始化一个新的任务,也就是这个函数主要的功能了。
pxTaskCode
是任务函数,前缀p
表示pointer,前缀x
表示stdint.h这个头文件中没有定义的数据类型;
pcName
是字符串,可以给新建的任务取个名,c
表示char;
ulStackDepth
是栈深度,也就是栈大小,ul
表示unsigned long;
pvParameters
是任务函数的参数,v
表示void;
uxPriority
是任务优先级,其底层是unsigned long类型,数值不应超过FreeRTOSConfig.h中配置的configMAX_PRIORITIES
这个宏,如果超过了,在这个函数中会自动把这个任务的优先级设置为configMAX_PRIORITIES-1
;
pxNewTCB
是Task Control Block;
xRegions
是内存区域的指针。
2、函数主体分析
StackType_t * pxTopOfStack;
UBaseType_t x;
上来先定义两个局部变量,StackType_t
本质是u32
类型,该变量是指向栈顶端的指针;UBaseType_t
本质是unsigned long
类型,用来遍历pcName
中的每一个字符(在后面的分析中会看到)。
#if ( portUSING_MPU_WRAPPERS == 1 )
BaseType_t xRunPrivileged;
if( ( uxPriority & portPRIVILEGE_BIT ) != 0U )
{
xRunPrivileged = pdTRUE;
}
else
{
xRunPrivileged = pdFALSE;
}
uxPriority &= ~portPRIVILEGE_BIT;
#endif /* portUSING_MPU_WRAPPERS == 1 */
#if
宏判断,若portable.h
中,portUSING_MPU_WRAPPERS
这个宏为1,即打开了内存保护单元(Memory Protection Unit),才会编译这段代码。
xRunPrivileged
是指示该任务是否应该运行在特权模式的flag。在FreeRTOS.h
中定义了portPRIVILEGE_BIT
为0,所以不管uxPriority
是多少,相与的结果都是0,所以上面的代码进入else的部分,这部分将xRunPrivileged
设置为false。portPRIVILEGE_BIT
取反,二进制表现为全1,与uxPriority
相与,结果还是uxPriority
本身。
/* 如用户未要求,不依赖memset函数。 */
#if ( tskSET_NEW_STACKS_TO_KNOWN_VALUE == 1 )
(void) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) ulStackDepth * sizeof( StackType_t ) );
#endif /* tskSET_NEW_STACKS_TO_KNOWN_VALUE */
#if
宏判断,如果tskSET_NEW_STACKS_TO_KNOWN_VALUE
这个宏(新分配的栈写入特定值)置为1,则编译下面的代码。
memset
函数,向pxStack
指向的地址处写入tskSTACK_FILL_BYTE
(其值为0xa5)。官方说这个操作是用于辅助debug的,个人猜测,可以通过测试内存中有多少控件被写入了0xa5这个数值,来判断任务创建时,栈有没有被正确初始化。
#if ( portSTACK_GROWTH < 0 )// 栈是向下增长的
{
pxTopOfStack = &( pxNewTCB->pxStack[ ulStackDepth - ( uint32_t ) 1 ] );// 获取栈顶地址
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 ) );
#if ( configRECORD_STACK_HIGH_ADDRESS == 1 )// 记录栈的高地址flag打开
{
/* 记录栈的高地址,官方说这一步可用于辅助 debug */
pxNewTCB->pxEndOfStack = pxTopOfStack;
}
#endif /* configRECORD_STACK_HIGH_ADDRESS */
}
#else /* portSTACK_GROWTH 栈是向上增长的 */
{
/* 获取栈顶地址 */
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 */
上面这部分代码计算栈顶地址。
最外层的宏判断根据portSTACK_GROWTH
确定栈是向上增长还是向下增长,分别编译不同的代码段。
portBYTE_ALIGNMENT_MASK
定义为0x0007。值得注意的是第4行代码,在将栈顶地址和对齐掩码做对位相与之前,对两个变量分别做了强制类型转换,体现了代码的严谨。
/* 把任务名存到TCB里 */
if( pcName != NULL )
{
for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
{
pxNewTCB->pcTaskName[ x ] = pcName[ x ];// 逐个字符保存任务名字
if( pcName[ x ] == ( char ) 0x00 ) // 如果遇到0x00说明到了字符串结尾
{
break; // 此时跳出循环
}
else // 没遇到0x00,说明没到结尾
{
mtCOVERAGE_TEST_MARKER(); // 测试代码
}
}
/* 确保这个表示任务名字的字符串以0x00结尾,看,很严谨吧。 */
pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';
}
else // pcName是空指针,说明没有传入任务命名
{
mtCOVERAGE_TEST_MARKER(); // 测试代码
}
还记得prvInitialiseNewTask
函数接收的参数pcName
,和函数最开始定义的变量x
吗?
if
判断pcName
是否为空,非空,则遍历这个变量里的每一个字符,将其写入pxNewTCB
结构体的pcTaskName
成员变量。
值得一提的是mtCOVERAGE_TEST_MARKER();
这个代码段,在FreeRTOS的源代码中出现了好多次。它被定义在FreeRTOS.h
中:
#ifndef mtCOVERAGE_TEST_MARKER
#define mtCOVERAGE_TEST_MARKER()
#endif
这里仅仅是定义了mtCOVERAGE_TEST_MARKER()
这么一个宏。因此代码段中每次出现mtCOVERAGE_TEST_MARKER()
,都会被替换为空。那如果,我在这个宏定义的后面添加上我想要测试的代码呢?例如:
#define mtCOVERAGE_TEST_MARKER() do{ \
led_toggle(); \
printf("flag\n"); \
} while(0U)
那么每次在代码中出现mtCOVERAGE_TEST_MARKER()
宏,都会被替换成我想要测试的代码,这是一种常用的debug的方式。你学会了吗~
下面继续看源码。
/* 官方:用作数组索引,所以该值不能太大 */
configASSERT( uxPriority < configMAX_PRIORITIES ); // 检查任务优先级不大于配置的最大优先级
if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES )// 如果任务优先级大于等于配置的最大优先级
{
// 把任务优先级设置为配置的最大优先级-1
uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;
}
else // 任务优先级没有大于配置的最大优先级
{
mtCOVERAGE_TEST_MARKER(); // 测试代码
}
上面这部分源码就是分析函数参数时提到的,如果传入的任务优先级大于config文件里配置的最大可用优先级数,则把优先级设置为最大可用优先级数-1。
pxNewTCB->uxPriority = uxPriority; // 把作为参数传入的优先级保存到TCB结构体中
#if ( configUSE_MUTEXES == 1 ) // 配置了使用互斥锁
{
pxNewTCB->uxBasePriority = uxPriority;// 作为参数传入的优先级保存到TCB中的“基本优先级”成员变量。至于这个成员变量在使用互斥锁时如何产生作用,还有待我探索,欢迎大佬们指教。
}
#endif /* configUSE_MUTEXES */
vListInitialiseItem( &( pxNewTCB->xStateListItem ) );// 把状态列表的container设置为NULL
vListInitialiseItem( &( pxNewTCB->xEventListItem ) );// 把事件列表的container设置为NULL
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );// 把状态列表的owner设置为NewTCB,也就是让状态列表知道自己的爹是谁。
/* 官方注释:事件列表按照优先级排序 */
listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority ); // 把事件列表的ItemValue值设置为configMAX_PRIORITIES - uxPriority
/* 为什么要设置成最大值-当前值的形式呢?因为事件列表是按照优先级排序的,优先级高的任务应该排在前面,所以当前值越大,也就是优先级越高,最大值-当前值得到的索引就越小,而越小的索引在数组结构中是越靠前的。 */
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB );// 让事件列表知道自己的爹是NewTCB。
#if ( portUSING_MPU_WRAPPERS == 1 )// 使用了内存保护单元
{
vPortStoreTaskMPUSettings( &( pxNewTCB->xMPUSettings ), xRegions, pxNewTCB->pxStack, ulStackDepth );// 没找到函数的定义,但是看名字能知道函数作用是保存任务的MPU设定
}
#else
{
/* 官方注释:避免编译器提示变量定义了但未使用 */
( void ) xRegions;
}
#endif
#if ( ( configUSE_NEWLIB_REENTRANT == 1 ) || ( configUSE_C_RUNTIME_TLS_SUPPORT == 1 ) )
{
/* Allocate and initialize memory for the task's TLS Block. */
configINIT_TLS_BLOCK( pxNewTCB->xTLSBlock );
}
#endif
/* 官方注释:初始化TCB任务,让任务看起来像是正在运行,但被调度器打断了。
* 返回地址设置为任务函数的起始地址。一旦任务被初始化,栈顶端的变量将被更新。 */
#if ( portUSING_MPU_WRAPPERS == 1 )// 用MPU
{
/* 官方注释:要是接口可以检测到栈溢出,那就把栈结束的地址也传给栈初始函数好了。 */
#if ( portHAS_STACK_OVERFLOW_CHECKING == 1 )// 检测栈溢出的宏置1
{
#if ( portSTACK_GROWTH < 0 )// 栈向下增长
{
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxNewTCB->pxStack, pxTaskCode, pvParameters, xRunPrivileged );// 初始化栈,保存栈顶地址
}
#else /* portSTACK_GROWTH 栈向上增长 */
{
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxNewTCB->pxEndOfStack, pxTaskCode, pvParameters, xRunPrivileged );// 初始化栈,保存栈顶地址,注意观察跟上面传参的不同
}
#endif /* portSTACK_GROWTH */
}
#else /* portHAS_STACK_OVERFLOW_CHECKING 检测栈溢出的宏置0 */
{
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters, xRunPrivileged );// 跟上面传参的区别在于少了传入栈结束的地址。
}
#endif /* portHAS_STACK_OVERFLOW_CHECKING */
}
#else /* portUSING_MPU_WRAPPERS 没用MPU */
{
/* 这一部分跟上面使用了MPU的代码大同小异,就不重复注释了 */
#if ( portHAS_STACK_OVERFLOW_CHECKING == 1 )
{
#if ( portSTACK_GROWTH < 0 )// 栈向下增长
{
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxNewTCB->pxStack, pxTaskCode, pvParameters );
}
#else /* portSTACK_GROWTH 栈向上增长 */
{
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxNewTCB->pxEndOfStack, pxTaskCode, pvParameters );
}
#endif /* portSTACK_GROWTH */
}
#else /* portHAS_STACK_OVERFLOW_CHECKING 没开启栈溢出检测 */
{
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
}
#endif /* portHAS_STACK_OVERFLOW_CHECKING */
}
#endif /* portUSING_MPU_WRAPPERS */
if( pxCreatedTask != NULL )// 传入的句柄参数非空
{
/* 官方注释:用匿名方式传出TCB句柄。通过TCB可以修改任务的优先级、删除和创建任务。 */
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
}
else
{
mtCOVERAGE_TEST_MARKER(); // 测试代码
}
总结一下
prvInitialiseNewTask
这个函数的主要作用是初始化并根据传入参数配置了pxNewTCB这个结构体,然后把这个结构体与传入的句柄相连接,让用户可以通过句柄来动态配置pxNewTCB结构体,进而配置这个任务。
作者:wong_xian