在使用 FreeRTOS 的过程中我们通常会在一个任务函数中使用延时函数对这个任务延时, 当执行延时函数的时候就会进行任务切换,并且此任务就会进入阻塞态,直到延时完成,任务重新进入就绪态。延时函数属于 FreeRTOS 的时间管理,本章我们就来学习一些FreeRTOS 的这个时间管理过程,看看在调用延时函数以后究竟发生了什么?任务是如何进入阻塞态的,在延时完成以后任务又是如何从阻塞态恢复到就绪态的。
FreeRTOS延时函数
学习过 UCOSIII 的朋友应该知道,在 UCOSIII 中延时函数 OSTimeDly()可以设置为三种模式:相对模式、周期模式和绝对模式。在 FreeRTOS 中延时函数也有相对模式和绝对模式,不过在FreeRTOS 中不同的模式用的函数不同,其中函数 vTaskDelay()是相对模式(相对延时函数),函数vTaskDelayUntil()是绝对模式(绝对延时函数)。
函数vTaskDelay()
函数 vTaskDelay()在文件 tasks.c 中有定义,要使用此函数的话宏 INCLUDE_vTaskDelay 必须为 1,函数代码如下:
(1)、延时时间由参数 xTicksToDelay 来确定,为要延时的时间节拍数,延时时间肯定要大于 0。否则的话相当于直接调用函数 portYIELD()进行任务切换。
(2)、调用函数 vTaskSuspendAll()挂起任务调度器。
(3) 、调用函数prvAddCurrentTaskToDelayedList()将要延时的任务添加到延时列表pxDelayedTaskList或者pxOverflowDelayedTaskList中 。后面会具体分析函数 prvAddCurrentTaskToDelayedList()。
(4)、调用函数 xTaskResumeAll()恢复任务调度器。
(5)、如果函数 xTaskResumeAll()没有进行任务调度的话那么在这里就得进行任务调度。
(6)、调用函数 portYIELD_WITHIN_API()进行一次任务调度。
函数 prvAddCurrentTaskToDelayedList()用于将当前任务添加到等待列表中,函数在文件 tasks.c 中有定义,具体过程自行查阅源码和正点视频。
vTaskDelay();函数延时的单位是tick,我们一个tick是1ms,所以n-tick就是n-ms,因为tick数量没有上限,所以计时时间也没有上限。
函数vTaskDelayUntil()
函数 vTaskDelayUntil()会阻塞任务,阻塞时间是一个绝对时间,那些需要按照一定的频率运行的任务可以使用函数 vTaskDelayUntil()
相对延时和绝对延时有何区别?
相对延时:
指每次延时都是从调用函数vTaskDelay()开始,延时指定的时间后结束,延时结束后,不一定就能马上轮到该任务运行,可能系统此时还在运行其他任务。相对模式的延时时间不精确,根据情况不同延时时间会变化。
绝对延时:
指每隔指定的时间,执行一次调用vTaskDelayUntil()函数的任务,不管中间有什么情况,只要时间一到,就必须要回来执行任务,它保证延时时间严格,不过中断也会打断该函数,从而导致延时时间有细微变化,只是相对于vTaskDelay()来说,会更加精准。
系统延时和自定义延时的对比
什么时候使用系统自带的延时函数,什么时候使用自定义的延时函数?
我能想到的就是rtos提供的延时函数可以阻塞任务,属于任务延时,但是自己写的延时函数属于任务的一部分,会一直执行,属于系统级的延时,一般是底层驱动的us级使用吧。ms级别的应该可以直接使用rtos提供的延时函数。
那具体是什么样的呢?得去查阅资料。
HAL_Delay是由ST提供的STM32 Cube HAL库中的一个函数,通常用于在STM32微控制器上实现简单的延时。HAL_Delay函数使用系统时钟来进行延时,并且在延时期间会阻塞整个处理器,也就是说,它会使处理器暂时停止执行其他任务和代码。
Rtos提供的延时还有个问题,那就是只能提供tick整数倍的时间,也就是说最小只能提供1ms的定时,无法提供更短时间的,也就是us级别的延时。如果需要使用us级别的延时,就只能使用自定义的延时函数。另外,如果不希望发生任务切换,也可以使用自定义延时,比如初始化时的延时,就能使用自定义的。
实际开发中,二者都应该被提供。
参考:
RTOS系统延时与普通软延时的特点与区别_rtos的延时会变化-CSDN博客
其中最关键的其实就是,rtos提供的延时函数会引发任务切换,而自定义延时函数是真正的阻塞延时。
总结就是,初始化和底层驱动时序的场景下使用自定义延时函数,如果希望实现任务切换,就可以在任务中使用rtos提供的延时函数。
在这种情况下,相对延时用的会比较多。
FreeRTOS提供了两个系统延时函数:相对延时函数vTaskDelay()和绝对延时vTaskDelayUntil()。
这两个延时函数和自己实现的延时函数不同,这两个延时函数一旦被调用,当前任务会立马进入阻塞状态,而自己写的延时函数(以for循环等形式实现的软件延时)会被当做有效任务而一直执行。
也就是,rtos提供的在延时期间能干别的活,但是自己写的延时函数没法干别的活。
参考:
vTaskDelay()和vTaskDelayUntil()-CSDN博客
再想想,其实只要不是微秒级别的延时,都能使用rtos提供的,底层只要延时时间准确就行,初始化时还没有开启任务调度,所以只会延时,不会发生任务调度,再说了,就算发生了任务调度也没关系,延时期间还能干活,不就和裸机中的中断延时是一样的思路嘛。
所以,除了底层us级别的延时,都能使用rtos提供的延时函数。
注意,延时函数需要在开启任务调度器之后,才能开始使用,这么说吧,开启任务调度就相当于freertos的初始化,只有开启了,systick等才能被初始化,然后才能使用延时函数。
这样来看,任务创建之前的初始化阶段,只能使用自己写的延时函数了。
可以说,vTaskDelay和vTaskDelayUntil更多是为任务而生的。
除非有延时的需要,否则一般情况下我们不会调用延时函数来让任务阻塞,虽然正点在学习freertos的时候会这样来做,来实际中基本不会有。通常是等待队列或者信号量没有等到才会阻塞任务,即不满足运行条件。
FreeRTOS系统时钟节拍
不管是什么系统,运行都需要有个系统时钟节拍,前面已经提到多次了,xTickCount 就是FreeRTOS 的系统时钟节拍计数器。每个滴答定时器中断中 xTickCount 就会加一,xTickCount 的具体操作过程是在函数 xTaskIncrementTick()中进行的,此函数在文件 tasks.c 中有定义。
我们之前已经在滴答定时器的中断里调用了函数xPortSysTickHandler
该函数内部就调用了函数xTaskIncrementTick()
该函数很长,可自行查阅源码,实现流程如下所述:
(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)、任务延时时间还未到,而且 xItemValue 已经保存了下一个要唤醒的任务的唤醒时间。
(12)、任务延时时间到了,所以将任务先从延时列表中移除。
(13)、检查任务是否还等待某个事件,比如等待信号量、队列等。如果还在等待的话就将任务从相应的事件列表中移除。因为超时时间到了!
(14)、将任务从相应的事件列表中移除。
(15)、任务延时时间到了,并且任务已经从延时列表或者事件列表中已经移除。所以这里需要将任务添加到就绪列表中。
(16)、延时时间到的任务优先级高于正在运行的任务优先级,所以需要进行任务切换了,标 记 xSwitchRequired 为 pdTRUE,表示需要进行任务切换。
(17)、如果使能了时间片调度的话,还要处理跟时间片调度有关的工作。
(18)、如果使能了时间片钩子函数的话就执行时间片钩子函数 vApplicationTickHook(),函数的具体内容由用户自行编写。
(19)、如果调用函数 vTaskSuspendAll()挂起了任务调度器的话在每个滴答定时器中断就不会更新 xTickCount 了。取而代之的是用 uxPendedTicks 来记录调度器挂起过程中的时钟节拍数。这样在调用函数 xTaskResumeAll()恢复任务调度器的时候就会调用 uxPendedTicks次函数xTaskIncrementTick(),这样 xTickCount 就会恢复,并且那些应该取消阻塞的任务都会取消阻塞。
由(16)项描述可知,抢占式任务调度是在这里面实现的。
更多内容可后续补充。