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通过调用下面方法创建任务。
//pxTaskCode:任务函数 void 函数名(void *pvParameters)
//pcName:任务名称,小于configMAX_TASK_NAME_LEN,建议小于16
//usStackDepth:堆栈大小(整数)
//pvParameters:给任务传递的参数
//uxPriority:任务优先级(整数)
//pxCreatedTask:任务的句柄,后期用他操作任务
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对象
TCB_t* pxNewTCB;
//返回值
BaseType_t xReturn;
StackType_t* pxStack;
//调用heap申请内存,申请栈的空间
pxStack = pvPortMalloc((((size_t)usStackDepth) * sizeof(StackType_t)));
//栈申请成功就申请TCB
if (pxStack != NULL)
{
//调用heap申请内存,申请TCB空间
pxNewTCB = (TCB_t*)pvPortMalloc(sizeof(TCB_t));
if (pxNewTCB != NULL)
{
//TCB指向栈底
pxNewTCB->pxStack = pxStack;
}
else
{
//TCB申请失败就是否栈空间
vPortFree(pxStack);
}
}
else
{
pxNewTCB = NULL;
}
//申请TCB和栈成功后的操作
if (pxNewTCB != NULL)
{
//初始化新创建的任务
prvInitialiseNewTask(pxTaskCode, pcName, (uint32_t)usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB);
//把任务加入就绪列表
prvAddNewTaskToReadyList(pxNewTCB);
//返回创建成功1
xReturn = pdPASS;
}
else
{
//返回申请空间失败-1
xReturn = -1;
}
return xReturn;
}
该方法先给任务申请栈内存
//调用heap申请内存,申请栈的空间
pxStack = pvPortMalloc((((size_t)usStackDepth) * sizeof(StackType_t)));
申请栈空间成功后再申请TCB内存,让TCB的栈底指针指向栈内存开始地址。
//调用heap申请内存,申请TCB空间
pxNewTCB = (TCB_t*)pvPortMalloc(sizeof(TCB_t));
if (pxNewTCB != NULL)
{
//TCB指向栈底
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;
//栈向下生长,算的栈结束地址
//得到栈顶地址。空栈时候指向栈的开始位置。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));
//将pxNewTCB设置为从ListItem_t返回的链接。这样我们就可以得到
//从列表中的通用项返回包含 TCB
listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem), pxNewTCB);
//事件列表始终按优先顺序排列。按最大优先级减去优先级保存
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);
//delay终止标志默认为FALSE
pxNewTCB->ucDelayAborted = pdFALSE;
//初始化 TCB 堆栈,使其看起来好像任务已经在运行,
//但已被调度程序中断。 返回地址已设置
//到任务功能的开始。 堆栈初始化后
//栈顶变量被更新。
pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters);
//传入了任务句柄,就把TCB设置到任务句柄
if (pxCreatedTask != NULL)
{
//设置tcb到句柄
*pxCreatedTask = (TaskHandle_t)pxNewTCB;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
初始化任务逻辑里先按申请栈大小通过栈开始地址加大小算到栈底位置。(因为ARM栈向下生长),申请的栈内存返回的地址是内存低地址。所以要换算出栈底地址。如果是向上生长的栈不用计算,申请的地址就是栈底。
//栈向下生长,算的栈结束地址
//得到栈顶地址。空栈时候指向栈的开始位置。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));
//将pxNewTCB设置为从ListItem_t返回的链接。这样我们就可以得到
//从列表中的通用项返回包含 TCB
listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem), pxNewTCB);
//事件列表始终按优先顺序排列。按最大优先级减去优先级保存
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);
//delay终止标志默认为FALSE
pxNewTCB->ucDelayAborted = pdFALSE;
然后模拟ARM硬件自动入栈寄存器操作。把栈按硬件压栈顺序依次压入PSR,PC,LR,R12,R3,R2,R1,R0的初始值。然后再按照OS上下文切换压栈其他寄存器顺序压栈腾出相应空间。确保任务被调度时候出栈特定数量值后正好是任务栈的初始状态。模拟入栈得根据芯片文档芯片自动入栈的顺序和OS入栈其他寄存器顺序定,该操作不可少。否则OS第一次调度任务时候按正常出栈就直接溢出了。因为切换任务逻辑理解为任务都压栈了任务上下文。最后把任务TCB结构体地址设置到返回的TCB句柄。
//初始化 TCB 堆栈,使其看起来好像任务已经在运行,
//但已被调度程序中断。 返回地址已设置
//到任务功能的开始。 堆栈初始化后
//栈顶变量被更新。
pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters);
//传入了任务句柄,就把TCB设置到任务句柄
if (pxCreatedTask != NULL)
{
//设置tcb到句柄
*pxCreatedTask = (TaskHandle_t)pxNewTCB;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
模拟入栈逻辑如下。和芯片文档和OS入栈顺序对应。
//模拟硬件初始化栈
//pxTopOfStack:
//pxCode:
//pvParameters:
StackType_t* pxPortInitialiseStack(StackType_t* pxTopOfStack,
TaskFunction_t pxCode,
void* pvParameters)
{
//模拟堆栈帧,因为它将由上下文切换创建中断。
//PSR:状态寄存器
//PC:程序计数器
//LR:链接寄存器
//依次压入PSR,PC,LR,R12,R3,R2,R1,R0
pxTopOfStack--;
//入栈初始状态
*pxTopOfStack = portINITIAL_XPSR;
pxTopOfStack--;
//PC赋值成为了执行任务的函数的入口
//为了严格遵守 Cortex-M 规范,任务起始地址应清除第0位,因为它是在退出ISR时加载到PC中的。
*pxTopOfStack = ((StackType_t)pxCode) & portSTART_ADDRESS_MASK;
pxTopOfStack--;
//入栈链接到错误跳转
*pxTopOfStack = (StackType_t)prvTaskExitError;
//空出位置
pxTopOfStack -= 5;
//任务参数放入R0对应位置
*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运行状态为内核态。任务运行状态是用户态。内核态运行堆栈和用户态堆栈通过双堆栈指针分开。