FreeRTOS列举一个简单的任务控制块结构体:
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 栈顶 */
ListItem_t xStateListItem; /* 任务节点 */
StackType_t *pxStack; /* 任务栈起始地址 */
char pcTaskName[ configMAX_TASK_NAME_LEN ]; /* 任务名称,长度默认16 */
} tskTCB;
typedef tskTCB TCB_t;
在多任务系统中,任务的执行是由系统调度的,系统为每一个任务都额外定义了一个任务控制块,存放任务所有信息,例如任务的栈指针、任务名称、任务形式等。
静态任务创建函数:
typedef void * TaskHandle_t;
typedef void ( *TaskFunction_t )( void * )
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
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_t *pxNewTCB;
TaskHandle_t xReturn;
if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )
{
pxNewTCB = ( TCB_t * ) pxTaskBuffer;
pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;
/* 创建新的任务 */
prvInitialiseNewTask( pxTaskCode, /* 任务入口 */
pcName, /* 任务名称,字符串形式 */
ulStackDepth, /* 任务栈大小,单位为字 */
pvParameters, /* 任务形参 */
&xReturn, /* 任务句柄 */
pxNewTCB); /* 任务控制块指针 */
}
else
{
xReturn = NULL;
}
/* 返回任务句柄,如果任务创建成功,此时xReturn应该指向任务控制块 */
return xReturn;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
在FreeRTOS中,任务的创建可采用两种方法,一种是使用动态创建,另一种是使用静态创建。动态创建:任务控制块和栈的内存是创建任务时动态分配的,任务删除时,内存可以释放;静态创建:任务控制块和栈的内存需要事先定义好,是静态的内存,任务删除时,内存不能释放。
可以看到xTaskCreateStatic函数做了两个动作:
1.设置任务控制块栈内存起始地址,并返回任务控制块指针
2.调用prvInitialiseNewTask函数来创建新任务,接下来分析prvInitialiseNewTask函数做了什么
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 );
/* 向下做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;
}
}
函数解析:
1)栈顶地址向下做8字节对齐,在Cortex-M3(Cortex-M4或Cortex-M7)内核的单片机中,因为总线宽度时32位的,通常只要栈保持4字节对齐即可,那么此处为何需要8字节?难道有哪些操作是64位?确实有,那就是浮点运算,所以要8字节对齐(但是目前还没有涉及浮点运算,此处的设置只是为了后续能够兼容浮点运算)。如果栈顶指针是8字节对齐的,在进行向下8字节对齐时,指针不会移动;如果不是8字节对齐的,在做向下8字节对齐时,就会空出几个字节,如pxTopOfStack是33时,明显不能被8整除,进行向下8字节对齐就是32,那么就会空出一个字节不能使用。进行8字节对齐,只需要将地址低三位清零(1000值等于8,所以x000形式代表地址8字节对齐)。
2)初始化TCB中的xStateListItem节点,即初始化该节点所在的链表为空,表士节点还没有插入任何链表
3)调用pxPortInitialiseStack()函数初始化任务栈,并更新栈顶指针,任务第一次运行的环境参数就存在任务栈中。
继续深入分析pxPortInitialiseStack()函数:
#define portINITIAL_XPSR ( 0x01000000 )
#define portSTART_ADDRESS_MASK ( ( StackType_t ) 0xfffffffeUL )
static void prvTaskExitError( void )
{
/* 函数停止在这里 */
for(;;);
}
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;
}
1)异常发生时,CPU自动从栈中加载到CPU寄存器的内存,包括8个寄存器,分别是r0、r1、r2、r3、r12、r14、r15和xPSR的位24,且顺序不能改变
2)任务返回地址概念,通常任务时不会返回的,如果返回了就跳转到prvTaskExitError,该函数是一个无限循环
3)r12、r3、r2、r1默认初始化为0;异常发生时,需要手动加载到CPU寄存器的内容,总共有8个,分别为r4、r5、r6、r7、r8、r9、r10、r11,默认初始化为0
4)返回栈顶指针,任务第一次运行时,就是从这个栈顶指针开始手动加载8个字的内容到CPU寄存器:r4、r5、r6、r7、r8、r9、r10、r11,当推出异常时,栈中剩下的8个字的内容会自动加载到CPU寄存器:r0、r1、r2、r3、r12、r14、r15和xPSR的位24,此时PC指针就指向了任务入口地址,从而成功跳转到第一个任务。