• 【freertos】007-系统节拍和系统延时管理实现细节


    前言

    本章节的时钟系统节拍主要分析FreeRTOS内核相关及北向接口层,南向接口层不分析。
    本章节的系统延时主要分析任务系统延时实现。

    原文:李柱明博客:https://www.cnblogs.com/lizhuming/p/16085130.html
    笔记手码。
    相关代码仓库:李柱明 gitee

    7.1 系统节拍配置

    FreeRTOS的系统时钟节拍可以在配置文件FreeRTOSConfig.h里面设置:#define configTICK_RATE_HZ( ( TickType_t ) 1000 )

    7.2 系统时钟节拍的原理

    系统时钟节拍不仅仅只记录系统运行时长,还涉及到系统的时间管理,任务延时等等。

    系统节拍数:

    系统会通过南向接口层实现定时回调,维护一个全局变量xTickCount

    每次定时回调会将变量xTickCount加1。

    这个变量xTickCount就是系统时基节拍数。

    获取时钟节拍数其实也就是返回该值。

    注意:

    系统节拍数不是每个tick都在实时累加的,在调度器挂起的情况下,触发产生的tick会记录下来,在恢复调度器后按挂起调度器产生的tick数逐个跑回xTaskIncrementTick(),快进模拟。

    7.3 系统节拍中的处理:xTaskIncrementTick()

    时钟节拍分析就按这个函数分析就好。

    每当系统节拍定时器中断时,南向接口层都会调用该函数来实现系统节拍需要处理的代码。
    系统节拍xTaskIncrementTick()这个函数的主要内容:

    1. 调度器没有被挂起:

      1. 计算系统节拍。
      2. 如果当前节拍已经大于或等于下一个需要解锁任务的节拍,就检索延时链表,解锁任务。如果被解锁的任务中有更高优先级的任务,需要触发调度。
      3. 如果当前任务优先级对应的就绪链表有其他任务,且开启了时间片调度,切换任务,触发调度。
    2. 如果调度器被挂起,则只记录节拍递增的次数,恢复调度器时按记录补回来。

    小笔记:上述就会存在一个问题,如果就绪链表中有比当前任务优先级更加高的任务,是有可能不会被监测到的,所以内核在解锁一个比当前任务优先级更加高的任务,需要主动触发一次任务调度。

    7.3.1 调度器正常

    uxSchedulerSuspended这个变量记录调度器运行状态:

    • pdFALSE表示调度器正常,没有被挂起。
    • pdTRUE表示调度器被挂起。

    7.3.1.1 系统节拍数统计

    调度器正常的情况下,xTickCount加1。

    7.3.1.2 延时列表

    先看下面几条链表的源码。

    需要注意的是,延时链表其实只有两条:

    • xDelayedTaskList1
    • xDelayedTaskList2

    pxDelayedTaskListpxOverflowDelayedTaskList只是链表指针,分别指向当前正在使用的延时列表和溢出列表。

    为什么需要两条延时列表?

    为了解决系统节拍溢出问题。

    如当系统节拍未溢出,pxDelayedTaskList指向xDelayedTaskList1pxOverflowDelayedTaskList指向xDelayedTaskList2时;

    任务需要唤醒的时间在未溢出范围内,记录到pxDelayedTaskList指向的xDelayedTaskList1

    任务需要唤醒的时间在超出溢出范围,记录到pxOverflowDelayedTaskList指向的xDelayedTaskList2

    当系统节拍溢出时,会做如下处理:

    • pxDelayedTaskList更新指向xDelayedTaskList2
    • pxOverflowDelayedTaskList更新指向xDelayedTaskList1

    这样就实现了pxDelayedTaskList始终指向未溢出的任务延时列表。

    7.3.1.3 系统节拍溢出处理

    对于嵌入式系统而已,xTickCount系统节拍占位也就8、32、64或者更大,但是也有溢出的时候,所以需要做溢出处理。

    xTickCount系统节拍溢出处理是调用taskSWITCH_DELAYED_LISTS()实现

    • 交换延时列表指针和溢出延时列表指针;

    • 溢出次数记录xNumOfOverflows

    • 调用prvResetNextTaskUnblockTime()更新下一次解除阻塞的时间到xNextTaskUnblockTime

      • 如果延时列表为空,说明没有任务因为延时阻塞。把下次需要唤醒的时间更新为最大值。说明未来不需要检查延时列表。

      • 如果延时列表不为空,说明有任务等待唤醒。从延时列表的第一个任务节点中把节点值取出来,该值就是延时列表中未来最近有任务需要唤醒的时间。

        • freertos内核链表采用的是非通用双向循环链表,节点结构体如下代码所示。其中xItemValue可由用户自定义赋值,在freertos延时列表中,用于记录当前任务需要唤醒的时间节拍值。
        • 学习freertos内核链表的可以参考:李柱明-双向非通用链表

    freertos内核链表节点结构体:

    xNextTaskUnblockTime变量就是表示当前系统未来最近一次延时列表任务中有任务需要唤醒的时间。

    利用这个变量就不需要在每次tick到了都检查下延时列表是否需要解除阻塞,节省CPU开销。

    7.3.1.4 任务唤醒处理

    系统节拍溢出处理完后,检查是否需要唤醒任务。

    如:

    进入上面代码逻辑分支以后,循环以下内容:

    如果延时列表为空,则把xNextTaskUnblockTime更新到最大值。

    如果延时列表不为空,则从延时列表中把任务句柄拿出来,分析:

    • 如果该任务需要唤醒的时间比系统节拍时间早,则

      • 把该任务从延时列表移除,重新插入到就绪列表;
      • 如果是因为事件阻塞,还要把该任务从事件列表中删除;
      • 如果解除阻塞的任务优先级比当前运行的任务优先级高,就标记触发任务调度xSwitchRequired = pdTRUE;
    • 如果该唤醒时间在未来,更新这个时间到xNextTaskUnblockTime,且退出遍历延时列表。

    7.3.1.5 时间片处理

    处理完任务阻塞后,便开始处理时间片的问题。

    freertos的时间片不是真正意义的时间片,不能随意设置时间片多少个tick,只能默认一个tick。其实现就看这里代码就知道了。伪时间片。

    每次tick都会检查是否有其他任务共享当前优先级,有就标记需要任务切换。

    7.3.1.6 tick钩子

    时间片处理完,可以执行tick钩子函数了。

    需要注意的是,tick钩子函数vApplicationTickHook()是在系统滴答中跑的,所以这个函数内容要短小,不能大量使用堆栈,且只能调用以”FromISR" 或 "FROM_ISR”结尾的API函数。

    另外,在代码中也能看到,在uxPendedTicks值为0才会执行tick钩子,这是因为不论调度器是否挂起,都会执行vApplicationTickHook()

    而在调度器挂起期间,tick钩子也在执行,所以在补回时钟节拍的处理就不在执行tick钩子。

    上述的uxPendedTicks值,是记录调度器挂起期间产生的tick数。

    7.3.1.7 xYieldPending

    该变量为了实现自动切换而萌生。

    在函数xTaskIncrementTick()内,xSwitchRequired为返回值,为真,在外部调用会触发任务切换。

    但是函数中xYieldPending变量也会触发xSwitchRequired为真。

    我们需要了解xYieldPending这个变量的含义。

    带中断保护的API函数(后缀FromISR),都会有一个参数pxHigherPriorityTaskWoken

    如果这些API函数导致一个任务解锁,且该任务的优先级高于当前运行任务,这些API会标记*pxHigherPriorityTaskWoken = pdTRUE;,然后再退出字段前,老版本的FreeRTOS需要手动触发一次任务调度。

    如在中断中跑:

    从FreeRTOSV7.3.0起,pxHigherPriorityTaskWoken成为一个可选参数,并可以设置为NULL。

    转而使用xYieldPending来实现带中断保护的API函数解锁一个更高优先级任务后,标记该变量为pdTRUE,实现任务自动进行切换。

    变量xYieldPendingpdTRUE,会在下一次系统节拍中断服务函数中,触发一次任务切换。代码便是:

    但是实际实现启用该功能是在在V9.0以及以上版本。

    小结一下pxHigherPriorityTaskWokenxYieldPending

    • 在带中断保护的API中解锁了更高优先级的任务,需要在这些API内部标记一些变量来触发任务切换。这些变量有pxHigherPriorityTaskWokenxYieldPending

    • pxHigherPriorityTaskWoken

      • 手动切换标记。
      • 局部变量。
      • 如果带中断保护的API解锁了更高优先级的任务,会标记pxHigherPriorityTaskWokenpdTRUE,用户根据这个变量调用portYIELD_FROM_ISR()来实现手动切换任务。
    • xYieldPending

      • 自动切换标记。
      • 全家变量。
      • 如果标记为pdTRUE,在执行xTaskIncrementTick()时钟节拍处理时,调度器正常的情况下回触发一次任务切换。

    带中断保护API内部参考代码:

    7.3.2 调度器挂起

    如果调度器挂起,正在执行的任务会一直继续执行,内核不再调度,直到该任务调用xTaskResumeAll()恢复调度器。

    在调度器挂起期间不会进行任务切换,但是其中产生的系统节拍都会记录在变量uxPendedTicks中。

    在恢复调度器后,会在xTaskResumeAll()函数内调用uxPendedTicksxTaskIncrementTick()实现逐个补回时钟节拍处理。

    7.4 系统节拍相关API

    获取系统节拍:xTaskGetTickCount

    作用:用于普通任务中,用于获取系统当前运行的时钟节拍数。

    原型:

    参数:无。

    返回:返回当前运行的时钟节拍数。

    7.4.1 获取系统节拍中断保护调用:xTaskGetTickCountFromISR()

    作用:用于中断中,用于获取系统当前运行的时钟节拍数。

    原型:

    7.4.2 系统节拍API 实战

    当前配置是configTICK_RATE_HZ是1000,即是1ms触发一次系统节拍。

    7.5 系统延时API相关

    系统提供两个延时API:

    • 相对延时函数vTaskDelay()
    • 绝对延时函数vTaskDelayUntil();
    • 终止延时函数xTaskAbortDelay()

    7.6 相对延时:vTaskDelay()

    7.6.1 API使用

    函数原型:

    函数说明:

    • vTaskDelay()用于相对延时,是指每次延时都是从任务执行函数vTaskDelay()开始,延时指定的时间结束。
    • xTicksToDelay参数用于设置延迟的时钟节拍个数。
    • 延时的最大值宏在portmacro.h中有定义:#define portMAX_DELAY (TickType_t )0xffffffffUL

    图中N就是参数xTicksToDelay

    7.6.2 相对延时实现原理

    原理:原理就是通过当前时间点和延时时长这两个值算出未来需要唤醒的时间,记录当前任务未来唤醒的时间点,然后把当前任务从就绪链表移到延时链表。

    未来唤醒时间 = 当前时间 + 延时时间。

    7.6.3 实现细节

    7.6.3.1 传入参数为0

    传入参数为0时,不会把当前任务进行阻塞。

    但是会触发一次任务调度。

    7.6.3.2 挂起调度器

    进入延时函数,在挂起调度器前会检查下当前当前是否已经挂起调度器了,如果硬件挂起调度器了还调用阻塞的相关API,系统会挂掉。

    如果当前调度器没有被挂起,那可以进入延时处理,先挂起调度器,防止在迁移任务时被其它任务打断。

    7.6.3.3 计算出未来唤醒时间

    计算出未来唤醒时间点,这个就是相对延时和绝对延时的主要区别。

    相对延时,未来唤醒时间点xTimeToWake是当前系统节拍加上xTicksToWait需要延时的节拍数。

    然后把这个值记录到当前任务状态节点里面的节点值xItemValue里,用于插入延时列表排序使用。

    7.6.3.4 迁移任务到延时链表

    从就绪链表迁移到延时链表时,调用prvAddCurrentTaskToDelayedList()实现。

    如果启用了终止延时功能,先pxCurrentTCB->ucDelayAborted把这个标志位复位,因为要出现进入延时了。

    先把任务从就绪链表中移除。

    移除后,如果当前任务同等优先级没有其它任务了,需要处理下就绪任务优先级位图:

    • 如果开启了优先级优化功能:需要把这个优先级在图表uxTopReadyPriority中对应的位清除。
    • 如果没有开启优先级优化功能:我认为也应该更新uxTopReadyPriority这个值,让系统知道当前就绪任务最高优先级已经不是当前任务的优先级值了。但是freertos并没有这样做。
    • 优先级优化功能可以查看我前面章节说的前导零指令。

    如果计算出未来唤醒时间点溢出了,就把当前任务插入到溢出延时链表,到系统节拍溢出时就换使用该链表作为延时链表的。

    如果未来唤醒时间点没有溢出,就插入当前延时链表,等待唤醒。如果唤醒时间比当前所有延时任务需要唤醒的时间还要早,那就更新下系统当前未来最近需要唤醒的时间值。

    7.6.3.5 强制任务调度

    恢复调度器后,如果在恢复调度器时没有触发过任务调度,那必须进行一次触发任务调度,要不然本任务会继续往下跑,不符合设计逻辑。

    7.7 绝对延时:vTaskDelayUntil()

    7.7.1 API使用

    函数原型:

    函数说明:

    • vTaskDelayUntil()用于绝对延时,也叫周期性延时。想象下精度不高的定时器。
    • pxPreviousWakeTime参数是存储任务上次处于非阻塞状态时刻的变量地址。
    • xTimeIncrement参数用于设置周期性延时的时钟节拍个数。
    • 返回:pdFALSE 说明延时失败。
    • 使用此函数需要在FreeRTOSConfig.h配置文件中开启:#defineINCLUDE_vTaskDelayUntil 1
    • 需要保证周期性延时比任务主体运行时间长。
    • 相对延时的意思是延时配置的N个节拍后恢复当前任务为就绪态。
    • 绝对延时的意思是延时配置的N个节拍后该任务跑回到当前绝对延时函数。

    图中N就是参数xTimeIncrement ,其中黄色延时部分需要延时多少是vTaskDelayUntil()实现的。

    7.7.2 绝对延时实现原理

    原理:实现周期延时的原理就是,通过上次唤醒的时间点、当前时间点和延时周期三个值算出剩下需要延时的时间,得出未来需要唤醒当前任务的时间,然后把当前任务从就绪链表迁移到延时链表。

    未来唤醒时间 = 上次唤醒时间 + 周期。

    7.7.3 实现细节

    7.7.3.1 参数检查

    指针不能为空,周期值不能为0,调度器没有被挂起。

    7.7.3.2 挂起调度器

    需要注意的是,在调用该函数时,调度器必须是正常的。

    如果当前调度器没有被挂起,那可以进入延时处理,先挂起调度器,防止在迁移任务时被其它任务打断。

    7.7.3.3 未来唤醒时间

    能把任务从就绪链表迁移到延时链表就绪阻塞的主要条件是唤醒时间在未来。

    先算出未来唤醒时间:

    7.7.3.4 溢出处理

    如果当前时间对比上次唤醒的时间已经溢出了,那只有未来唤醒的时间值比当前的时间值还大,才能就绪阻塞处理。

    这种情况如下图:

    代码如下:

    如果当前时间对比上次唤醒时间没有溢出过,需要考虑两种情况:

    • 未来时间唤醒时间已经溢出。
    • 未来时间唤醒时间没有溢出。

    对于未来时间没有溢出,就是下图:

    如果未来唤醒时间比上次唤醒的时间还小,便可说明唤醒时间在未来,这种判断代码就是:

    而对于未来时间也没有溢出的情况如下图:

    对于这种情况,未来唤醒时间值比当前时间值大,当前时间值又比上次唤醒时间值大,也可以说明唤醒时间在未来。

    小结下,只需要证明到实际时空时间值是:上次唤醒 < 当前时间 < 未来唤醒。即可说明当前任务主体运行时间比周期时间小,可以进行延时阻塞。

    7.7.3.5 迁移到延时链表

    参考相对延时的迁移到延时链表章节。

    需要注意的是,传入prvAddCurrentTaskToDelayedList()的参数应该是相对延时值,而不是未来唤醒时间。

    7.7.3.6 强制任务调度

    恢复调度器后,如果在恢复调度器时没有触发过任务调度,那必须进行一次触发任务调度,要不然本任务会继续往下跑,不符合设计逻辑。

    7.8 终止任务阻塞:xTaskAbortDelay()

    使用该功能前需要在FreeRTOSConfig.h文件中配置宏INCLUDE_xTaskAbortDelay为1来使用该功能。

    7.8.1 API 使用

    函数原型:

    函数说明:

    • xTaskAbortDelay()函数用于解除任务的阻塞状态,将任务插入就绪链表中。

    • xTask :任务句柄。

    • 返回:

      • pdPASS:任务解除阻塞成功。
      • pdFAIL或其它:没有解除任务阻塞还在任务不在阻塞状态。

    7.8.2 实现细节

    7.8.2.1 参数检查

    主要检查任务句柄值是否有效。

    7.8.2.2 挂起调度器

    挂起调度器,防止任务被切走处理。

    7.8.2.3 获取任务状态

    通过API eTaskGetState()获取任务状态是否处于阻塞态。有以下情况可以判断任务处于阻塞态:

    1. 任务处于延时链表或者处于延时溢出链表。
    2. 任务处于挂起态,但是在等待某个事件,也属于阻塞态。
    3. 处于挂起态,也没有在等待事件,但是在等待任务通知,也属于阻塞态。

    这部分看下该API源码即可。

    如果不在阻塞态,可以xTaskAbortDelay()函数直接返回pdFAIL

    7.8.2.4 解除任务状态并重新插入就绪链表

    解除任务所有状态,在阻塞态时,其实就是先把任务迁出对应的任务状态链表。

    然后加入临界处理因为事件而阻塞的问题,进入临界处理是因为部分中断回调也会接触到任务事件链表。

    如果任务是因为事件而阻塞的,需要从事件链表中移除,解除阻塞,并且标记上强制解除阻塞标记。

    处理完事件链表后,可以将其重新插入到就绪链表。

    7.8.2.5 恢复调度器

    把阻塞的任务成功迁入到就绪链表后,如果开启了抢占式调度,如果解除阻塞的任务优先级大于当前在跑的任务优先级,需要任务切换。

    通过xYieldPending = pdTRUE;标记在恢复调度器时进行任务切换。这个是一个确保。

    在恢复调度器API xTaskResumeAll()里面,后面章节会有分析过这个API,有兴趣的同学可以往后翻。

    在这个API里面,恢复调度器也会逐个恢复系统节拍,然后在最后检查xYieldPending变量是否需要触发任务切换。

    7.9 系统延时实战

    代码地址:李柱明 gitee

    • 找到release分支中的 freertos_on_linux_task_delay 文件夹,拉下来,直接make。

    创建三个任务说明相对延时、绝对延时和解除阻塞:

    运行成功:

    附件

    系统节拍统计:xTaskIncrementTick()

    系统节拍溢出处理:taskSWITCH_DELAYED_LISTS()

    相对延时:vTaskDelay()

    添加当前任务到延时列表:prvAddCurrentTaskToDelayedList()

    绝对延时:xTaskDelayUntil()

    解除任务阻塞:xTaskAbortDelay()


    __EOF__

  • 本文作者: 李柱明
  • 本文链接: https://www.cnblogs.com/lizhuming/p/16085130.html
  • 关于博主: 嵌入式从业者。RTOS、Linux ...
  • 版权声明: 版权归博主所有
  • 声援博主: 学习笔记分享
  • 相关阅读:
    Mybatis 懒加载使用及源码分析
    内容安全实验——实验一 硬盘分区恢复实践
    光模块对网络延迟的影响如何?
    批量剪辑视频怎么做?附保姆级教程,新手小白也能3分钟50+短视频。
    八、【Vue-Router】编程式路由导航
    一周快速入门Python之day01
    Transforms的使用2(ToTensor类)
    个人微信号管理工具哪个好?
    1012 数字分类【PAT (Basic Level) Practice (中文)】
    大数据1星笔试题_220621
  • 原文地址:https://www.cnblogs.com/lizhuming/p/16085130.html