freertos-刘火良:延时列表(链表)
前面几章的学习中,任务从创建后一直位于就绪列表中,延时、优先级等操作全部在就绪列表进行,这是不太方便的。根据任务的几个状态知,还需要一个延时列表,当任务进入延时状态时,则移出就绪列表进入延时列表。当有了延时列表后,前面的几个操作需要相应进行修改
1. 定义延时列表
就绪列表为一个带有多条链表的数组,而延时列表仅为一个链表结构,如图示意
需要注意的是,由于可能存在唤醒时间超过数值类型的最大值,如本实验定义的TickType_t类型最大为0xffff_ffff,假如唤醒时间超过了这个值,则需要进入另一个列表(相当于用多个列表弥补不足),所以定义两个延时列表,代码如下
static List_t xDelayedTaskList1; //时基计数器未溢出时用的列表 static List_t xDelayedTaskList2; //时基计数器溢出时用的列表 static List_t *volatile pxDelayedTaskList = NULL; //指向xDelayedTaskList1 static List_t *volatile pxOverflowDelayedTaskList = NULL; //指向xDelayedTaskList2
2. 初始化延时列表
由上面定义可知,初始化要做的事就是进行指针的指向,同时链表必然需要进行链表的初始化。所以在最初的只初始化了就绪列表的函数中,添加初始化延时列表
void prvInitialiseTaskLists(void) { /* brief:初始化链表组 */ UBaseType_t uxPriority; for(uxPriority = (UBaseType_t) 0U; uxPriority < (UBaseType_t) configMAX_PRIORITIES; uxPriority++) //遍历每个元素(链表),进行链表初始化 { vListInitialise( &(pxReadyTasksLists[uxPriority])); } // 初始化延时列表并定好指针指向 vListInitialise( &xDelayedTaskList1); vListInitialise( &xDelayedTaskList2); pxDelayedTaskList = &xDelayedTaskList1; pxOverflowDelayedTaskList = &xDelayedTaskList2; }
3. 下一个唤醒时刻xNextTaskUnblockTime
与前面的延时和扫描方式不同,本次使用的就是通过对比当前时刻和唤醒时刻,当两者一致时,就唤醒任务,所以用到变量xNextTaskUnblockTime
其初始化在调度任务函数vTaskStartScheduler中进行,将其初始化为最大延时时间。同时调度任务函数中初始化当前时刻xTickCount为0
void vTaskStartScheduler(void) { /* 启动任务调度(含有空闲任务创建) */ TaskHandle_t xIdleTaskHandle; //空闲任务句柄 TCB_t *pxIdleTaskTCBBuffer = NULL; //空闲任务TCB的指针 StackType_t *pxIdleTaskStackBuffer = NULL; //空闲任务栈指针 uint32_t ulIdleTaskStackSize; //空闲任务栈大小 extern TickType_t xTickCount; vApplicationGetIdleTaskMemory(&pxIdleTaskTCBBuffer,&pxIdleTaskStackBuffer,&ulIdleTaskStackSize); //获取空闲任务信息,注意用二重指针 //创建空闲任务 xIdleTaskHandle = xTaskCreateStatic((TaskFunction_t)prvIdleTask,(char *)"IDLE", (uint32_t)ulIdleTaskStackSize,(void *)NULL, (UBaseType_t)tskIDLE_PRIORITY, (StackType_t *)pxIdleTaskStackBuffer,(TCB_t *)pxIdleTaskTCBBuffer); xNextTaskUnblockTime = portMAX_DELAY; //初始化为最大延时时间 xTickCount = (TickType_t) 0U; if(xPortStartScheduler() != pdFALSE) { //调度器启动失败则进入这里 } }
4. TaskDelay
以往的方法是通过对任务TCB附加一个TickToDelay的延时,并对所有任务的其进行扫描递减,这样子大大增加了CPU的使用负担;而本次是对任务TCB的辅助排序值进行赋值,然后直接放入延时列表,定一个唤醒时刻,当时间到时再进行列表任务的判断取出,这样子,只有部分满足条件的任务需要进行比对而无需比对所有任务。
TaskDelay函数将延时任务交给prvAddCurrentTaskToDelayedList函数,最后触发PendSV中断
void vTaskDelay(const TickType_t xTicksToDelay) { TCB_t *pxTCB = NULL; //设置当前任务的延时计数 pxTCB = pxCurrentTCB; //加入延时列表 prvAddCurrentTaskToDelayedList(xTicksToDelay); //任务切换 taskYIELD(); }
prvAddCurrentTaskToDelayedList函数完成三件事:将任务移出就绪列表;将任务加入延时列表;给任务装填排序值(即唤醒时刻)
static void prvAddCurrentTaskToDelayedList(TickType_t xTickToWait) { /* brief:将当前任务移出就绪列表,加入延时列表 */ TickType_t xTimeToWake; extern TickType_t xTickCount; const TickType_t xConstTickCount = xTickCount; //移出就绪列表 //同时如果当前任务所在优先级就绪链表上没有节点了,消除这一优先级 if(uxListRemove(&(pxCurrentTCB->xStateListItem)) == (UBaseType_t) 0) { taskRESET_READY_PRIORITY(pxCurrentTCB->uxPriority); //注意这里和书上不同,书上用的是portRESET,这里用的是宏定义 } //计算当前任务唤醒时刻 xTimeToWake = xConstTickCount + xTickToWait; //将唤醒时间作为排序值 listSET_LIST_ITEM_VALUE(&(pxCurrentTCB->xStateListItem), xTimeToWake); //如果唤醒时间溢出则加入溢出列表(溢出指的是唤醒时刻比当前时刻还早,明显就是出错了);如果没有溢出则加入正常延时列表 if(xTimeToWake < xConstTickCount) { vListInsert(pxOverflowDelayedTaskList, &(pxCurrentTCB->xStateListItem)); } else { vListInsert(pxDelayedTaskList, &(pxCurrentTCB->xStateListItem)); //更新唤醒时刻值,如果这个任务的唤醒时刻比当前下一个唤醒时刻快,则表示下一个唤醒时刻应改成这个任务的唤醒时刻 if(xTimeToWake < xNextTaskUnblockTime) { xNextTaskUnblockTime = xTimeToWake; } } }
5. xTaskIncrementTick
和之前的递减计数值进行扫描不同,本次是进行系统计数值的递增,每次中断产生则计数值加一,同时判断其是否到达唤醒时刻。同时判断是否计数值溢出,如果溢出,则要进行延时列表切换和计数值重置(因为溢出实质上是计数超过了程序变量能记录的值,所以要用另一个列表装下对应任务和将数值重置)
void xTaskIncrementTick(void) { /* 每次中断进行计数加一,并判断是否到达唤醒时刻 */ TCB_t *pxTCB = NULL; TickType_t xItemValue; extern TickType_t xTickCount; const TickType_t xConstTickCount = xTickCount + 1; xTickCount = xConstTickCount; //如果计时溢出(回到0)则切换列表和重设唤醒时刻 if(xConstTickCount == (TickType_t) 0U) { taskSWITCH_DELAYED_LISTS(); } //判断是否到达唤醒时刻 if(xConstTickCount >= xNextTaskUnblockTime) { for(;;) { //如果延时列表已经空了,则不用进行循环判断了 if(listLIST_IS_EMPTY(pxDelayedTaskList) != pdFALSE) { xNextTaskUnblockTime = portMAX_DELAY; //因为没有延时任务了,自然也没有下一个唤醒时刻了,所以初始化为最大时间 break; } else { pxTCB = (TCB_t *)listGET_OWNER_OF_HEAD_ENTRY(pxDelayedTaskList); //获取延时列表第一个任务,即最近一个将要唤醒的任务 xItemValue = listGET_LIST_ITEM_VALUE(&(pxTCB->xStateListItem)); //得到延时时刻
//这个任务唤醒时间还没到,则进行唤醒时间更新 if(xConstTickCount < xItemValue) { xNextTaskUnblockTime = xItemValue; break; } //从延时列表中删除 (void)uxListRemove(&(pxTCB->xStateListItem)); //加入就绪列表 prvAddTaskToReadyList(pxTCB); } } } portYIELD(); }
其中taskSWITCH_DELAYED_LISTS为宏定义,完成的事情有:1. 列表对调;2. 溢出次数加1(暂时不知道有什么作用);3. 重置唤醒时刻
#define taskSWITCH_DELAYED_LISTS() \ { \ /* 唤醒时间溢出,需要用另外一个列表辅助,此时切换为另外一个列表 */ \ List_t *pxTemp = NULL; \ /* 两个列表对调 */ \ pxTemp = pxDelayedTaskList; \ pxDelayedTaskList = pxOverflowDelayedTaskList; \ pxOverflowDelayedTaskList = pxTemp; \ /* 溢出次数加一 */ \ xNumOfOverflows++; \ /* NextTime已经溢出了,任务列表也切换了,所以更新NextTime */ \ prvResetNextTaskUnblockTime(); \ }
其中prvResetNextTaskUnblockTime根据情况:如果延时列表没有任务,则重置为最大值;如果延时列表有任务,则设置为最近一个任务的唤醒时刻
static void prvResetNextTaskUnblockTime(void) { TCB_t *pxTCB; //如果延时列表空了,则唤醒时间重置为最大 if(listLIST_IS_EMPTY(pxDelayedTaskList) != pdFALSE) { xNextTaskUnblockTime = portMAX_DELAY; } //如果列表还没空,则更新唤醒时间 else { (pxTCB) = (TCB_t *)listGET_OWNER_OF_HEAD_ENTRY(pxDelayedTaskList); xNextTaskUnblockTime = listGET_LIST_ITEM_VALUE(&((pxTCB)->xStateListItem)); } }
6. taskRESET_READY_PRIORITY
本次引入了延时列表,则就绪列表有可能出现空白(即没有任务)的情况,所以,这次可以实现:当某一优先级的就绪链表为空白时,才可以进行相对应的优先级位图置零
#define taskRECORD_READY_PRIORITY( uxPriority ) \ { \ /* 如果对应优先级链表为空则在优先级位图置0 */ \ if(listCURRENT_LIST_LENGTH(&(pxReadyTasksLists[uxPriority])) == (UBaseType_t) 0) \ { \ portRECORD_READY_PRIORITY( uxPriority, (uxTopReadyPriority)); \ } \ }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通