freertos-刘火良:内核实现
定义习惯
变量
将变量类型缩写当作前缀,如无符号字符uc,字符指针pc,数据结构、任务句柄等用x
函数
返回值类型缩写当作前缀,如无返回v,私有函数加pri前缀
宏定义
宏定义大写,所在头文件名字缩写为前缀,小写。(信号量函数是宏定义,但命名按函数定义)
通用宏定义
链表实现
节点定义
根节点:根节点实际上就是一个链表(或者说链表头),所以其为第一个节点同时也是最后一个节点。属性有:链表上的节点个数(不包括根节点)、索引值(用来遍历节点,指向入口节点)、最后一个节点(本质是精简节点MINIitem):根节点自身item,和普通节点一样有辅助值、前一节点后一节点。
节点:属性为辅助排序值(确定该节点在链表中顺序,作用是确定优先级进行排序)、前一个节点、后一个节点、owner(挂载的TCB结构体)、container(节点所在链表)
节点初始化
根节点初始化:初始化时由于没有其他节点,则前一个节点和后一个节点都指向自身,index索引值指向最后一个节点即同样指向自身,链表上挂载节点数为0,根节点自身排序为最大MAX
节点初始化:节点初始化时不挂载任何链表,即container = NULL
向链表添加节点
链表尾部添加节点
尾部添加涉及到三个节点的属性更改
新节点:新节点的前一个节点为根节点原前一个节点,即NewItem->pxPrevious = pxIndex->pxPrevious;新节点的下一个节点为根节点,即NewItem->pxNext = pxIndex;同时新节点挂载链表为指定链表,即container = list。
根节点原前一个节点:作为根节点原前一个节点,其下一个节点不再是第一个节点即根节点,而是插入的新节点,即pxIndex->pxPrevious->pxNext = NewItem
根节点:根节点前一个节点将变成新节点,即pxIndex->pxPrevious = NewItem;同时链表节点计数器加一
按辅助顺序值添加节点
设置一个临时节点iterator,使用for循环进行遍历,当iterator的下一个节点的辅助值大于要添加的节点的顺序值时,则将新节点位置确定在iterator和iterator->next之间。
创建任务
所谓创建任务,其实就是给TCB对象内属性初始化
首先看TCB结构体
typedef struct tasTaskControlerBlock { volatile StackType_t *pxTopOfStack; //栈顶 ListItem_t xStateListItem; //任务节点(链表项) StackType_t *pxStack; //任务栈起始地址 char pcTaskName[configMAX_TASK_NAME_LEN]; //任务名称 }tskTCB; typedef tskTCB TCB_t;
可以看到TCB结构体内有栈顶、栈起始地址、任务节点即链表项、任务名称。我们要做的就是对这四个值进行赋值初始化
从逻辑框图可以看到,初始化分为三个函数,第一个函数TaskCreate主要是用来初始化TCB和TCB栈,简单来说就是内部创建一个TCB,初始化为传进的TCB;同理TCB栈,然后下面调用属性初始化函数对其进行初始化,最后返回一个TCB指针的指针(二级指针),即句柄TaskHandle_t
TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode, const char * const pcName, const uint32_t ulStackDepth, void * pvParameters, StackType_t puxStackBuffer, TCB_t * const pxTaskBuffer) { /* brief:创建新任务,实质就是给TCB对象内的成员初始化(赋值) param: pxTaskCode:任务函数 pcName:任务名称 ulStackDepth:任务栈深 pvParameters:任务函数传参 puxStackBuffer:任务栈起始地址 pxTaskBuffer:TCB指针 return:任务句柄 */ TCB_t *pxNewTCB; TaskHandle_t xReturn; if((pxTaskBuffer != NULL) && (puxStackBuffer != NULL)) //当TCB存在且任务栈存在才进行创建 { pxNewTCB = (TCB_t * )pxTaskBuffer; //初始化TCB pxNewTCB->pxStack = (StackType_t *)puxStackBuffer; //初始化TCB栈 prvInitialiseNewTask(pxTaskCode,pcName,ulStackDepth,pvParameters,&xReturn,pxNewTCB); // } else { xReturn = NULL; } return xReturn; }
第二个函数InitTask初始化TCB,主要是计算栈顶,对齐,定义任务名称,挂载TCB到任务节点上,伪造现场
static void prvInitialiseNewTask(TaskFunction_t pxTaskCode, const char * const pcName, const uint32_t ulStackDepth, void * pvParameters, TaskHandle_t * const pxCreatedTask, TCB_t *pxNewTCB) { /* brief:初始化新任务 param: pxTaskCode:任务函数 pcName:任务名称 ulStackDepth:任务栈深 pvParameters:任务函数传参 pxCreatedTask:任务句柄,指向TCB pxNewTCB:TCB return:NULL */ StackType_t *pxTopOfStack; UBaseType_t x; //计算栈顶 pxTopOfStack = pxNewTCB->pxStack + (ulStackDepth - (uint32_t)1); pxTopOfStack = (StackType_t *)((uint32_t)pxTopOfStack & ~((uint32_t)0x0007)); //向下对齐八个字节 //初始化任务栈(伪造现场) pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters); //初始化任务名称 for(x = (UBaseType_t)0;x < (UBaseType_t)configMAX_TASK_NAME_LEN;x++) { pxNewTCB->pcTaskName[x] = pcName[x]; if(pcName[x] == 0x00) { break; } } pxNewTCB->pcTaskName[configMAX_TASK_NAME_LEN -1] = '\0'; //最后一个字符为结束符? //初始化任务节点 vListInitialiseItem(&(pxNewTCB->xStateListItem)); //设置节点owner listSET_LIST_ITEM_OWNER((pxNewTCB->xStateListItem),pxNewTCB); //关联owner,令该任务节点挂载TCB //任务句柄指向TCB if((void *) pxCreatedTask != NULL) { *pxCreatedTask = (TaskHandle_t) pxNewTCB; } }
第三个函数InitStack主要就是伪造现场,由于初始化时是第一次对栈进行操作,此时寄存器并没有实际意义的值可以入栈,于是自定义大部分的值到栈中
StackType_t *pxPortInitialiseStack(StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters) { /* 初始化任务栈---伪造现场,按顺序进行填充各寄存器的值(伪) */ pxTopOfStack--; //暂时不明白为什么要先减一 *pxTopOfStack = portINITIAL_XPSR; //PSR寄存器 pxTopOfStack--; *pxTopOfStack = ((StackType_t)pxCode) & portSTART_ADDRESS_MASK; //任务函数入口地址:PC pxTopOfStack--; *pxTopOfStack = (StackType_t) prvTaskExitError; //万一任务返回,直接进入error函数死循环 pxTopOfStack -= 5; //R1~R3,R12,R14 *pxTopOfStack = (StackType_t) pvParameters; //任务参数:r0 pxTopOfStack -= 8; //R4~R11寄存器手动加载,伪造现场时无需管 return pxTopOfStack; }
调度器(vTaskStartScheduler)
rtos的本质就是调度器进行任务的切换,调度器的本质就是一个死循环和中断,通过中断进行当前任务TCB(pxCurrentTCB)的切换,从时间顺序来说,首先启动任务,然后后续就是不断切换任务。下面给出启动任务和切换任务的具体原理
启动任务
pxCurrentTCB指向第一个任务TCB
xPortStartScheduler:设置PendSV和SYSTICK优先级为最低
内嵌汇编 prvStartFirstTask:寻找主栈位置,开启中断,触发SVC中断
SVC中断 vPortSVCHandler:其为宏定义的SVC_Handler。手动出栈使得psp更新到r0位置即r11之后,然后硬件出栈让psp更新到栈顶,此时r15寄存器即PC值为任务1的函数
切换任务
一般情况下,通过触发PendSV中断进行任务切换,由内嵌汇编 xPortPendSVHandler实现,切换任务具体分为三个部分
1. Task1的现场保存
找到psp,在进入PendSV中断时xPSR等寄存器硬件自动压栈,如图1此时psp指向r11,赋值给r0,利用r0手动将r11到r4压栈;
图1 psp指向
找到栈顶存放地址r2,利用其修改栈顶位置为r0,此时r0如图2
图2 r0指向
将Task1的TCB指针和此时r14的值保存到主栈中
2. 进入临界区,切换任务
vTaskSwitchContext:实质上就是关闭其他中断,对“当前任务TCB指针”指向优先级最高的任务TCB,然后恢复其他中断出临界区
3. Task2的现场恢复
由2,从主栈中出栈TCB指针和r14值;根据TCB指针找到栈顶位置,由1可推断,上次Task2保存现场时栈顶位置为r4值所在位置,如图3
图3 栈顶位置
然后手动进行出栈到r4~r11,此时r0位置为

将r0值赋给psp,使用bx指令返回,硬件根据psp对剩下xPSR等寄存器进行出栈
调度器总体框架示意图
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通