CPU有这些寄存器。R0-R12为通用寄存器。R13为栈顶指针,在OS时候中断函数的R13使用MSP的指针(内核态)。非中断里面使用PSP指针(用户态)。
正是有双堆栈指针可以保证OS切换任务不会盖用户程序的堆栈状态。对于OS和任务都是C语言编译的逻辑。OS可不知道任务在要切换时候运行的指令,对于OS来说也不需要关心任务执行的指令。只要确保以下就可以保证任务切换出去后再切换回来时候保持不变。
1.剥夺任务CPU后运行其他指令不会覆盖任务用的内存空间。TCB、栈空间等。
2.任务切换回来的时候能把任务切换前的CPU寄存器状态恢复。包括R13的PSP指针。
保证以上两条就可以进行CPU调度,让CPU在多个任务之间来回切换。
要保证内存不被其他程序乱覆盖,首先通过公共方法申请和释放内存地址。申请的本质就是内存管理方法把没人用的内存地址给程序返回使用。内存管理方法通过空闲内存链表管理空闲内存。基于申请内存程序不做越界访问就不会破坏别人的数据。
要保证任务后面重新运行时候能接着之前停止时候运行。那么就要确保CPU的寄存器和之前状态一样。那么就必须把切换之前的寄存器值保存起来供恢复时候使用。既然任务要让出CPU,那么把寄存器值依次压入任务自己的栈就行了。停止的任务栈里保存自己停止时候的CPU状态,恢复时候出栈恢复CPU寄存器值。出栈完成后任务的栈正好也是停止任务时候的栈内存状态。
任务内存布局类似下图(OS内存管理占用一大片内存给不同任务分配,启动汇编文件时候申请一片给裸机用。MSP栈执行在该片区域):
由于ARM栈是向下生长的。即PUSH后栈顶像低地址移动,POP时候像高地址移动。因为OS要管理任务,栈向下生长可能会堆栈溢出。所以先申请栈内存,再申请TCB结构体内存。为的就是让堆栈溢出时候盖的也是别的任务的空间。还能通过当前任务TCB检测是否堆栈溢出。先后并没有严格要求。
对应具体一个任务,运行时候不断基于PSP指针入栈出栈,基于栈执行指令。让出CPU时候中断逻辑基于MSP栈控制执行逻辑。这时候PSP没变。在中断里面把寄存器和PSP压入任务自己栈。
向下生长的栈入栈出栈示意图。
FreeRTOS通过调用下面方法创建任务。
BaseType_t xTaskCreate(TaskFunction_t pxTaskCode,
const char* const pcName,
const configSTACK_DEPTH_TYPE usStackDepth,
void* const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t* const pxCreatedTask)
{
TCB_t* pxNewTCB;
BaseType_t xReturn;
StackType_t* pxStack;
pxStack = pvPortMalloc((((size_t)usStackDepth) * sizeof(StackType_t)));
if (pxStack != NULL)
{
pxNewTCB = (TCB_t*)pvPortMalloc(sizeof(TCB_t));
if (pxNewTCB != NULL)
{
pxNewTCB->pxStack = pxStack;
}
else
{
vPortFree(pxStack);
}
}
else
{
pxNewTCB = NULL;
}
if (pxNewTCB != NULL)
{
prvInitialiseNewTask(pxTaskCode, pcName, (uint32_t)usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB);
prvAddNewTaskToReadyList(pxNewTCB);
xReturn = pdPASS;
}
else
{
xReturn = -1;
}
return xReturn;
}
该方法先给任务申请栈内存
pxStack = pvPortMalloc((((size_t)usStackDepth) * sizeof(StackType_t)));
申请栈空间成功后再申请TCB内存,让TCB的栈底指针指向栈内存开始地址。
pxNewTCB = (TCB_t*)pvPortMalloc(sizeof(TCB_t));
if (pxNewTCB != NULL)
{
pxNewTCB->pxStack = pxStack;
}
内存申请成功后就初始化新创建的任务
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)
{
StackType_t* pxTopOfStack;
UBaseType_t x;
pxTopOfStack = &(pxNewTCB->pxStack[ulStackDepth - (uint32_t)1]);
pxTopOfStack = (StackType_t*)(((portPOINTER_SIZE_TYPE)pxTopOfStack) & (~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK)));
if (pcName != NULL)
{
for (x = (UBaseType_t)0; x < (UBaseType_t)16; x++)
{
pxNewTCB->pcTaskName[x] = pcName[x];
if (pcName[x] == (char)0x00)
{
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
pxNewTCB->pcTaskName[16 - 1] = '\0';
}
else
{
pxNewTCB->pcTaskName[0] = 0x00;
}
if (uxPriority >= (UBaseType_t)configMAX_PRIORITIES)
{
uxPriority = (UBaseType_t)configMAX_PRIORITIES - (UBaseType_t)1U;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
pxNewTCB->uxPriority = uxPriority;
pxNewTCB->uxBasePriority = uxPriority;
pxNewTCB->uxMutexesHeld = 0;
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);
pxNewTCB->ucDelayAborted = pdFALSE;
pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters);
if (pxCreatedTask != NULL)
{
*pxCreatedTask = (TaskHandle_t)pxNewTCB;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
初始化任务逻辑里先按申请栈大小通过栈开始地址加大小算到栈底位置。(因为ARM栈向下生长),申请的栈内存返回的地址是内存低地址。所以要换算出栈底地址。如果是向上生长的栈不用计算,申请的地址就是栈底。
pxTopOfStack = &(pxNewTCB->pxStack[ulStackDepth - (uint32_t)1]);
pxTopOfStack = (StackType_t*)(((portPOINTER_SIZE_TYPE)pxTopOfStack) & (~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK)));
然后保存任务优先级和名称
if (pcName != NULL)
{
for (x = (UBaseType_t)0; x < (UBaseType_t)16; x++)
{
pxNewTCB->pcTaskName[x] = pcName[x];
if (pcName[x] == (char)0x00)
{
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
pxNewTCB->pcTaskName[16 - 1] = '\0';
}
else
{
pxNewTCB->pcTaskName[0] = 0x00;
}
if (uxPriority >= (UBaseType_t)configMAX_PRIORITIES)
{
uxPriority = (UBaseType_t)configMAX_PRIORITIES - (UBaseType_t)1U;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
pxNewTCB->uxPriority = uxPriority;
pxNewTCB->uxBasePriority = uxPriority;
pxNewTCB->uxMutexesHeld = 0;
然后初始化任务状态和事件列表项
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);
pxNewTCB->ucDelayAborted = pdFALSE;
然后模拟ARM硬件自动入栈寄存器操作。把栈按硬件压栈顺序依次压入PSR,PC,LR,R12,R3,R2,R1,R0的初始值。然后再按照OS上下文切换压栈其他寄存器顺序压栈腾出相应空间。确保任务被调度时候出栈特定数量值后正好是任务栈的初始状态。模拟入栈得根据芯片文档芯片自动入栈的顺序和OS入栈其他寄存器顺序定,该操作不可少。否则OS第一次调度任务时候按正常出栈就直接溢出了。因为切换任务逻辑理解为任务都压栈了任务上下文。最后把任务TCB结构体地址设置到返回的TCB句柄。
pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters);
if (pxCreatedTask != NULL)
{
*pxCreatedTask = (TaskHandle_t)pxNewTCB;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
模拟入栈逻辑如下。和芯片文档和OS入栈顺序对应。
StackType_t* pxPortInitialiseStack(StackType_t* pxTopOfStack,
TaskFunction_t pxCode,
void* pvParameters)
{
pxTopOfStack--;
*pxTopOfStack = portINITIAL_XPSR;
pxTopOfStack--;
*pxTopOfStack = ((StackType_t)pxCode) & portSTART_ADDRESS_MASK;
pxTopOfStack--;
*pxTopOfStack = (StackType_t)prvTaskExitError;
pxTopOfStack -= 5;
*pxTopOfStack = (StackType_t)pvParameters;
pxTopOfStack -= 8;
return pxTopOfStack;
}
任务堆栈初始化好之后就把任务状态列表项加入就绪列表。这样任务就处于创建好等待调度的状态了。启动调度器之后就按优先级从就绪列表取任务出栈上下文执行任务的逻辑了。同时后续可以通过TCB结构体句柄操作任务。
prvAddNewTaskToReadyList(pxNewTCB);
这就是FreeRTOS创建任务的过程,大体归纳为以下:
1.申请栈内存
2.申请TCB结构体内存
3.计算栈底地址设置到TCB栈底指针
4.模拟硬件入栈寄存器和OS入栈寄存器
5.把TCB状态列表项挂入就绪列表等待调度
理解双堆栈指针(MSP和PSP)。中断函数调度逻辑执行在MSP上,在保存任务上下文时候不会因为调度逻辑本身破坏程序状态。MSP运行状态为内核态。任务运行状态是用户态。内核态运行堆栈和用户态堆栈通过双堆栈指针分开。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)