本篇文章记录我学习有关FreeRTOS的时间管理知识,希望我的分享对你有所帮助。
目录
2、prvAddacurrentTaskToDelayedList()
一、FreeRTOS之延时函数
1、vTaskDelay()
在讲述FreeRTOS的延时函数之前,我们可以联想一下与之相关的UCOS中也有类似的延时函数。在UCOSIII中延时函数OSTimeDly()可以设置为三种模式:相对模式、周期模式和绝对模式。在 FreeRTOS中延时函数也有相对模式和绝对模式,不过在FreeRTOS中不同的模式用的函数不同。
其中函数vTaskDelay()是相对模式(相对延时函数),函数 vTaskDelayUntil()是绝对模式(绝对延时函数)。函数vTaskDelay()在文件 tasks.c中有定义,要使用此函数的话宏 INCLUDE_vTaskDelay必须为1,函数代码如下:
(1)、延时时间由参数xTicksToDelay来确定,为要延时的时间节拍数,延时时间肯定要大于0。否则的话相当于直接调用函数portYIELD()进行任务切换。
(2)、调用函数vTaskSuspendAll()挂起任务调度器。
(3)、调用函数prvAddCurrentTaskToDelayedList()将要延时的任务添加到延时列表pxDelayedTaskList或者pxOverflowDelayedTaskList()中。
(4)、调用函数xTaskResumeAll()恢复任务调度器。
(5)、如果函数xTaskResumeAll()没有进行任务调度的话那么在这里就得进行任务调度。
(6)、调用函数portYIELD_WITHIN_API()进行一次任务调度。
2、prvAddacurrentTaskToDelayedList()
(1)、读取进入函数prvAddCurrentTaskToDelayedList()的时间点并保存在xConstTickCount中,后面计算任务唤醒时间点的时候要用到。xTickCount是时钟节拍计数器,每个滴答定时器中断xTickCount都会加一。
(2)、要将当前正在运行的任务添加到延时列表中,肯定要先将当前任务从就绪列表中移除。(3)、将当前任务从就绪列表中移除以后还要取消任务在uxTopReadyPriority 中的就绪标记。也就是将uxTopReadyPriority 中对应的bit清零。
(4)、延时时间为最大值portMAX_DELAY,并且 xCanBlockIndefinitely不为pdFALSE (xCanBlockIndefinitely 不为 pdFALSE的话表示允许阻塞任务) 的话直接将当前任务添加到挂起列表中,任务就不用添加到延时列表中。
(5)、将当前任务添加到挂起列表xSuspendedTaskList 的末尾。
(6)、计算任务唤醒时间点,也就是(1)中获取到的进入函数prvAddCurrentTaskToDelayedList()的时间值xConstTickCount加上延时时间值xTicksToWait。
(7)、将计算到的任务唤醒时间点值 xTimeToWake 写入到任务列表中状态列表项的相应字段中。(8)、计算得到的任务唤醒时间点小于xConstTickCount,说明发生了溢出。全局变量xTickCount是 TickType_t类型的,这是个32位的数据类型,因此在用xTickCount计算任务唤醒时间点xTimeToWake的时候的肯定会出现溢出的现象。FreeRTOS针对此现象专门做了处理,在FreeROTS 中定义了两个延时列表xDelayedTaskListl和xDelayedTaskList2,并且也定义了两个指针pxDelayedTaskList和 pxOverflowDelayedTaskList来访问这两个列表,在初始化列表函数prvInitialiseTaskLists()中指针 pxDelayedTaskList指向了列表xDelayedTaskListl ,指针pxOverflowDelayedTaskList 指向了列表 xDelayedTaskList2。这样发生溢出的话就将任务添加到pxOverflowDelayedTaskList所指向的列表中,如果没有溢出的话就添加到pxDelayedTaskList所指向的列表中。
(9)、如果发生了溢出的话就将当前任务添加到pxOverflowDelayedTaskList所指向的列表中
(10)、如果没有发生溢出的话就将当前任务添加到pxDelayedTaskList所指向的列表中。(11)、xNextTaskUnblockTime是个全局变量,保存着距离下一个要取消阻塞的任务最小时间点值。当xTimeToWake小于xNextTaskUnblockTime 的话说明有个更小的时间点来了。
(12)、更新xNextTaskUnblockTime为xTimeToWake。
3、函数vTaskDelayUntil()
函数vTaskDelayUntil()会阻塞任务,阻塞时间是一个绝对时间,那些需要按照一定的频率运行的任务可以使用函数vTaskDelayUntil()。此函数再文件tasks.c中有如下定义:
参数:
pxPreviousWakeTime:上一次任务延时结束被唤醒的时间点,任务中第一次调用函数vTaskDelayUntil 的话需要将pxPreviousWakeTime初始化进入任务的while()循环体的时间点值。在以后的运行中函数vTaskDelayUntil()会自动更新pxPreviousWakeTime。xTimeIncrement:任务需要延时的时间节拍数(相对于pxPreviousWakeTime本次延时的节拍数)。
(1)、挂起任务调度器。
(2)、记录进入函数vTaskDelayUntil()的时间点值,并保存在xConstTickCount 中。
(3)、根据延时时间xTimeIncrement来计算任务下一次要唤醒的时间点,并保存在xTimeToWake中。可以看出这个延时时间是相对于pxPreviousWakeTime的,也就是上一次任务被唤醒的时间点。pxPreviousWakeTime、xTimeToWake、xTimeIncrement和 xConstTickCount的关系如图所示:
如上图中 (1)为任务主体,也就是任务真正要做的工作,(2)是任务函数中调用vTaskDelayUntil()对任务进行延时,(3)为其他任务在运行。任务的延时时间是xTimeIncrement这个延时时间是相对于pxPreviousWakeTime的,可以看出任务总的执行时间一定要小于任务的延时时间xTimeIncrement!也就是说如果使用vTaskDelayUntil()的话任务相当于任务的执行周期永远都是 xTimeIncrement,而任务一定要在这个时间内执行完成。这样就保证了任务永远按照一定的频率运行了,这个延时值就是绝对延时时间,因此函数 vTaskDelayUntil()也叫做绝对延时函数。
(4)、根据上图可以看出,理论上xConstTickCount要大于pxPreviousWakeTime的,但是也有一种情况会导致xConstTickCount小于pxPreviousWakeTime,那就是xConstTickCount溢出了!
(5)、既然xConstTickCount都溢出了,那么计算得到的任务唤醒时间点肯定也是要溢出的并且xTimeToWake肯定也是要大于xConstTickCount的。这种情况如图所示:(6)、如果满足(5)条件的话就将pdTRUE赋值给xShouldDelay,标记允许延时。
(7)、还有其他两种情况,一:只有xTimeToWake溢出,二:都没有溢出。只有xTimeToWake溢出的话如图所示:都不溢出的话就如图(3)所示,这两种情况都允许进行延时。
(8)、将pdTRUE赋值给xShouldDelay,标记允许延时。
(9)、更新 pxPreviousWakeTime 的值,更新为xTimeToWake,为本函数的下一次执行做准备。
(10)、经过前面的判断,允许进行任务延时。
(11)、调用函数prvAddCurrentTaskToDelayedList()进行延时。函数的第一个参数是设置任务的阻塞时间,前面我们已经计算出了任务下一次唤醒时间点了,那么任务还需要阻塞的时间就是下一次唤醒时间点xTimeToWake减去当前的时间xConstTickCount。而在函数vTaskDelay()中只是简单的将这参数设置为xTicksToDelay。
(12)、调用函数xTaskResumeAll()恢复任务调度器。函数vTaskDelayUntil()的使用方法如下:其实使用函数vTaskDelayUntil()延时的任务也不一定就能周期性的运行,使用函数vTaskDelayUntil()只能保证你按照一定的周期取消阻塞,进入就绪态。如果有更高优先级或者中断的话你还是得等待其他的高优先级任务或者中断服务函数运行完成才能轮到你。这个绝对延时只是相对vTaskDelay()这个简单的延时函数而言的。
二、FreeRTOS之系统时钟节拍
不管是什么系统,运行都需要有个系统时钟节拍,前面已经提到多次了,xTickCount 就是FreeRTOS的系统时钟节拍计数器。每个滴答定时器中断中xTickCount就会加一, xTickCount的具体操作过程是在函数xTaskIncrementTick()中进行的,此函数在文件 tasks.c中有定义,如下(注:这里引用正点原子开源的相关文档说明一下):
(1)、判断任务调度器是否被挂起。
(2)、将时钟节拍计数器xTickCount加一,并将结果保存在xConstTickCount中,下一行程序会将xConstTickCount赋值给xTickCount,相当于给xTickCount加一。
(3)、xConstTickCount为0,说明发生了溢出!
(4)、如果发生了溢出的话使用函数 taskSWITCH_DELAYED_LISTS 将延时列表指针
pxDelayedTaskList 和溢出列表指针 pxOverflowDelayedTaskList 所指向的列表进行交换,函数taskSWITCH_DELAYED_LISTS()本质上是个宏,在文件 tasks.c中有定义,将这两个指针所指向的列表交换以后还需要更新xNextTaskUnblockTime的值。
(5)、变量 xNextTaskUnblockTime 保存着下一个要解除阻塞的任务的时间点值,如果xConstTickCount大于 xNextTaskUnblockTime的话就说明有任务需要解除阻塞了。
(6)、判断延时列表是否为空。
(7)、如果延时列表为空的话就将xNextTaskUnblockTime设置为portMAX_DELAY。(8)、延时列表不为空,获取延时列表第一个列表项对应的任务控制块。
(9)、获取(8)中获取到的任务控制块中的状态列表项值。
(10)、任务控制块中的状态列表项值保存了任务的唤醒时间点,如果这个唤醒时间点值大于当前的系统时钟(时钟节拍计数器值),说明任务的延时时间还未到。
(11)、任务延时时间还未到,而且 xltemValue已经保存了下一个要唤醒的任务的唤醒时间点,所以需要用xItemValue来更新xNextTaskUnblockTime。
(12)、任务延时时间到了,所以将任务先从延时列表中移除。
(13)、检查任务是否还等待某个事件,比如等待信号量、队列等。如果还在等待的话就任务从相应的事件列表中移除。因为超时时间到了!
(14)、将任务从相应的事件列表中移除。
(15)、任务延时时间到了,并且任务已经从延时列表或者事件列表中已经移除。所以这里需要将任务添加到就绪列表中。
(16)、延时时间到的任务优先级高于正在运行的任务优先级,所以需要进行任务切换了,标记xSwitchRequired为pdTRUE,表示需要进行任务切换。
(17)、如果使能了时间片调度的话,还要处理跟时间片调度有关的工作。
(18)、如果使能了时间片钩子函数的话就执行时间片钩子函数 vApplicationTickHook(),函数的具体内容由用户自行编写。
(19)、如果调用函数vTaskSuspendAll()挂起了任务调度器的话在每个滴答定时器中断就不不会更新xTickCount了。取而代之的是用uxPendedTicks 来记录调度器挂起过程中的时钟节拍数。这样在调用函数 xTaskResumeAll()恢复任务调度器的时候就会调用uxPendedTicks 次函数xTaskIncrementTick(),这样xTickCount就会恢复,并且那些应该取消阻塞的任务都会取消阻塞。函数xTaskResumeAll()中相应的处理代码如下:
(20)、uxPendedTicks是个全局变量,在文件tasks.c中有定义,任务调度器挂起以后此变量用来记录时钟节拍数。
(21)、有时候调用其他的API函数会使用变量xYieldPending来标记是否需要进行上下文切换。
(22)、返回xSwitchRequired 的值,xSwitchRequired 保存了是否进行任务切换的信息,如果为pdTRUE 的话就需要进行任务切换,pdFALSE的话就不需要进行任务切换。函数xPortSysTickHandler()中调用xTaskIncrementTick()的时候就会判断返回值,并且根据返回值决定是否进行任务切换。