Toriyung

导航

freertos-内部机制:整理笔记

三种现场保存

  任务切换:由于任务之间没有联系,所以需要保存所有寄存器数据

  硬件中断:硬件自动保存一部分寄存器,而还有一部分在中断时会被破坏,需要手动软件保存

  调用子函数:R0,R1等寄存器本身就是保存数据用来传递给子函数,所以没有保存的必要

 

创建函数详解

  TCB结构体:内含有函数,函数参数,栈深等,在结构体源码中可找到对应。如指定的栈深,函数会在默认指定划分的巨大空间(文件-memmang,hc_heap)中划分出来,起始地址保存在结构体中的pxStack;而传入的函数则是自动将地址保存在pc寄存器,函数参数保存在r0..等寄存器。

 

任务调度

freertos共有三种链表,若干个(按最高优先级来算)ready链表,1个delay链表,1个suspend链表

 创建好任务时,根据优先级选择对应链表,选择队尾加入,调用时,从高优先度到低优先度遍历,优先高优先度,当同一优先度时,先运行队尾,然后从头开始运行,当任务延时时或者任务阻塞时,进入delay链表

 

 

礼让

当存在优先级为0的任务时,空闲任务会进行礼让(先运行很短的时间),源代码如下,当判断0优先级链表长度大于1时,即除了空闲任务还有其他同级任务,则礼让

 

队列(queue)

  当多个任务对同一数据(或寄存器)进行操作时,可能会出现一个任务未完成操作就被另一个任务抢占的情况,为了防止这种情况出现,需要引入互斥的思想,即等待任务完成后再进行下一个任务,rtos中使用的是队列(queue)的方式。

  裸机的循环判断,在每一个循环中,除了任务1会运行外,需要每次都进行任务2的判断,这样浪费了cpu资源;而队列则是将任务2先放入队列链表,当任务1完成,唤醒任务2,这样就大大提高了任务1的速度,提高了cpu效率,所以本质上,队列的互斥是以关闭中断,使其中唯一一个任务运行的原理进行的

  队列结构

    实现上述为任务2的操作,使用到了关闭中断的操作,即关闭让任务进行切换而产生的中断。于是队列结构大致为

    关中断、环形缓冲区、链表、开中断        (其中环形缓冲区是指当写或读操作到最后一个位置时重新返回第一个位置,如此循环得名)

  创建队列

    xQueueCreate()  参数:队列的长度(即多少个格子),元素的大小(即格子的大小)

  读/写操作

    xQueueReceive()/xQueueSend()    参数:队列句柄,接收数据存放数组/发送数据存放数组,超时唤醒时间(最大延时时间)

    

 

      如上图为环形缓冲区,当进行读操作任务时,读操作任务将自己放进等待读操作队列(waitingReceiveQueue),同时进入休眠(从ready链表中加入delay链表),当写操作任务将数据写进缓冲区时,会唤醒读操作任务:删去等待读操作队列(waitingReceiveQueue)第一个任务(也就是刚刚进入休眠的读操作任务),同时从delay中拉出读操作加入ready,如果没有空间了于是将自己放进等待写操作队列(waitingSendQueue),同时进入休眠(从ready链表中加入delay链表;然后读操作任务判断队列是否有元素,有的话进行读取并移位,同时元素个数减一,因为读取了数据,于是将写操作任务同时将写操作任务从delay中移除并加入ready,直到元素个数为0,重新将自己放进等待读操作队列(waitingReceiveQueue),同时进入休眠。

上图表达的就是大概的流程,需要注意的是,写和读的操作是交替进行的,在读取后有空间和写入后有数据时互相唤醒的一个循环流程

超时唤醒:从函数中可以看到还有个最大延时时间,即某个任务没有其他任务唤醒时,超过了最大延时时间也会自动唤醒

 

信号量(semaphore)

  临界资源:多个线程每次只能有一个线程占据临界资源进行调度

  临界区:多个线程每次只能有一个线程进入临界区进行对临界资源的调度,产生中断抢占优先级时进入临界区

  当多个任务对同一个临界资源进行调度,为了防止资源被破坏,需要顺序调度,于是需要使用一个“量”进行争夺(take),take到了量的任务才能对临界资源进行调度,这个量称之为信号量,信号量指的是计数值,代码上来看是特殊的队列。顾名思义核心是“量”,操作过程就是对信号量进行增减,当信号量>0时,则表示可以进行争夺,当信号量=0时,则表示已经没有空闲量可以争夺,此时任务阻塞等待。

  创建信号量

    xSemaphoreCreateBinary():创建二进制信号量,也可以是其他进制,二进制-表示只能是1或0,即只能争夺一次。其源码其实就是创建一个只有队列头,没有buffer的队列。

  增/减信号量

    xSemaphoreTake()

    过程和队列差不多。增的过程就是被唤醒后信号量加一,然后查看waiting队列中是否有等待的任务,有的话唤醒(删除队列首个任务并delay→ready),减的过程类似。

 

互斥量(mutex)

  由信号量知识得知,任务需要对资源进行争夺,但有这么一种情况

  -存在三个任务,优先级A>B>C

  -已知A和C任务需要对同一资源调度,B不需要

  当C先进行调度,获得信号量,运行到中途,B抢占运行,此时C挂起,而下面A要抢占,但由于B抢占运行,使得C没有运行完,信号量没有释放,于是A也无法调度资源运行,也挂起,直到B结束,A才能调度执行。

  从宏观现象看,以上过程表现出了B>A>C的现象,称之为优先级反转

  优先级继承

    从前面可以知道,优先级反转不是一个好现象,问题就出在C的优先级太低,于是有种方法,将A的优先级继承给C,此时C的优先级大于B,可以先执行,执行完后释放信号量,A获得信号量,A就可以先于B执行,这就是优先级继承。

    xSemaphoreCreateMutex()

    原理:将互斥量持有者从低优先级ready链表中移除后放到高优先级ready链表。当任务结束,需要解除继承,重新回到低优先级ready链表

  而互斥量,本质上就是拥有优先级继承的信号量

 

事件组

  当有许多个事件被等待或者可以被触发的时候,可以使用事件组。比如任务1等待事件1或事件2,此时他进入等待链表,同时从ready→delay;此时负责设置事件的任务2写入了事件1,同时其判断等待链表中是否有满足条件的(等待事件1),如果有,则将其从等待链表删除,且将其从delay→ready。

  xEventGroupCreate()  创建事件组,其创建一个队列,每一个bit位代表一种事件。 

  xEventGroupWaitBits()  等待事件。  参数:事件组,等待的事件(一或多个),等到事件后是否清除事件,是否等待全部事件

    内部机制:关闭调度器,等待事件,若没有直接返回错误;或进入等待列表,同时从ready→delay;直到等待到

  xEventGroupSetBits()  设置事件。 参数:设置的事件

    内部机制:设置某些bit位,并从等待链表判断是否有满足条件的,如果有,则将其从等待链表删除,且将其从delay→ready

  Q:为什么只关闭调度器?

  A:任务切换有中断和调度器两种方式,当关闭中断时,调度器和中断都被关闭;当关闭调度器时,中断不会受影响,事件组中任务中断是通过写入回调函数到队列,进而唤醒守护任务,守护任务再调用这个回调函数,而不是真正通过中断任务进行设置,这是因为如果设置事件并唤醒等待链表时,其所需时间无法确认(因为满足事件的等待任务数量未知),这与rtos的实时性相悖,需要避免 

  补充知识:

 

    守护任务:进行定时器超时处理或队列数据处理,其效率严重依赖于优先级

 

任务通知

  当任务B在等待数据时,任务A写入数据了,此时需要将B唤醒,于是有了任务通知。

  和之前的事件组等不同的是,他们A和B之间数据通信是通过中间的结构体,B等待时在结构体中挂上等待链表,A写入后再结构体中的等待链表唤醒,不直接产生接触(即没有特定任务目标);而任务通知则是在B中写入状态,在B中唤醒(即有特定任务目标),如此一来效率更高。由于没有中间结构体,所以无需初始化结构体。

  状态:

  

  xTaskNotify()  给特定任务发通知。参数:任务句柄,数据,数据的作用(作为信号量还是事件组标记等)

    内部机制:写入数据后,将对方任务状态置为RECEIVED

  xTaskNotifyWait()  接收通知。参数:入口处是否清空通知值,出口处是否清空通知值,取出通知值(不取则为NULL)

    内部机制:初始化状态为NOT_WAITING,进行状态判断,如果没有数据,改为WAITING,当被发送任务改为RECEIVED,则进行数据读取

 

软件定时器

  内部机制:创建定时器,即初始化定时器结构体。当启动定时器,则把定时器加入队列,如果守护任务处于就绪状态,则处理该命令---将定时器加入链表。然后等到该定时器超时,产生中断,守护任务处理定时器的回调函数。

  函数  

    xTimerCreate()   构造定时器。参数:名字,周期,一次性/自动加载(循环),ID(一般取0),回调函数          *调用前需要配置下宏定义

    xTimerStart()    启动定时器,本质上是构造一个消息(命令)写入队列。参数:定时器句柄,等待时间(因为队列可能满了,此时需要等待)

  调用配置

  

 

 

中断管理

  FreeRTOS中,有两套API,普通任务用一套中断用一套,如下图。

  

  在普通任务可以唤醒其他任务;而中断“过程”中不进行唤醒,这是因为唤醒了其他任务,而优先级仍没有中断高,无法执行,这样浪费了资源,所以可以返回一个标记值,在中断结束之前根据标记值统一进行唤醒。

  

  中断开启状态下,产生中断时,无论中断优先级多低,都比最高普通任务优先级高。

  两类中断

    ARM中按优先级将中断分为两类,其中相对高优先级的中断一般不能再被中断,因为可能引起错误

   

 

 

  常见优先级

    中断的优先级始终大于任务,而中断内又分不同优先度,常见的systick和PendSV在RTOS中一般设置为最低优先度。

    *关于PendSV:

      OS在任务过程中,如果出现ISR打断,由于任务调度需要耗费时间可能会导致ISR的处理被推迟,实时性差,从而将整个OS任务调度过程拆为OS→PendSV,PendSV→调度。(参考:https://blog.csdn.net/weixin_44322983/article/details/121322101)

      

      当systick中断产生时,首先OS→PendSV,此时PendSV保存现场,然后PendSV→ISR,中断执行完后再有ISR→PendSV,PendSV→任务。(参考:https://blog.csdn.net/u012351051/article/details/124789418)

       

 

   多任务切换的实质:任务都有对应自己的栈,切换时进保存现场和恢复现场(各寄存器的值)

  手动实现:

   创建任务:由于任务切换需要保存和恢复现场,而第一次或者还未运行过的任务,何来的现场(寄存器值)?所以在创建任务时,先伪造一次现场,同时保存现场。由于保存需要栈,和任务函数,所以创建任务时需要传入栈、栈大小、函数等。

       保存现场的过程就是对寄存器值和返回地址等存入栈中,同时记录本任务栈的位置(方便下次切换过来调用),还有任务数量加一等标志。其中返回地址是在切换任务过程中,调用任务函数使用到的,所以返回地址指向函数。

   启动任务:恢复现场时r0为任务的栈地址,软件将栈中R4-R11的值重新读回R4-R11寄存器;r1为保存的LR特殊值(EXC_RETURN),使用BX LR会产生硬件中断自动将剩余寄存器值读回,读回后LR值更新为函数地址,所以恢复现场后自动跳转到函数。

   切换任务:创建全局变量count记录任务总数;创建全局变量task记录当前任务。判断当task小于count时task加一即指向下一个任务,当大于等于count时task重置为0即回到开头。

 

posted on 2022-10-26 14:10  Toriyung  阅读(347)  评论(0编辑  收藏  举报