FreeRTOS学习笔记(2、同步与互斥通信、队列、队列集的使用)

前言


这是第二弹,由于CSDN长度的限制,所以把FreeRTOS学习分为几部分来发,这是第二部分


主要包括同步与互斥通信、队列、队列集的使用


往期学习笔记链接


第一弹FreeRTOS学习笔记(1、FreeRTOS初识、任务的创建以及任务状态理论、调度算法等)
第二弹: FreeRTOS学习笔记(2、同步与互斥通信、队列、队列集的使用)
第三弹: FreeRTOS学习笔记(3、信号量、互斥量的使用)
第四弹: FreeRTOS学习笔记(4、事件组、任务通知)
第五弹: FreeRTOS学习笔记(5、定时器、中断管理、调试与优化)


学习工程


所有学习工程
oufen / FreeRTOS学习
都在我的Gitee工程当中,大家可以参考学习


同步互斥与通信

同步、互斥与通信

举个例子
多任务系统当作是一个团队,里面的每一个task就相当于团队里的一个人
团队成员之间要协调工作进度 ———-> 同步
争用会议室 ———-> 互斥
沟通 ———-> 通信

任务通知(task notification)、队列(queue)、事件组(event group)、信号
量(semaphoe)、互斥量(mutex)等


  1. 等就是同步

taskA正在做某些事情,taskB必须等待taskA完成后才能继续往下做,这就是同步,步调一致


  1. 互斥

对于某些资源,同一时间只能有一个task使用,这个task1必须独占的使用它
一个task1使用过了之后,另一个task2就不能使用

但是这个task1使用完后,提醒task2,你可以使用了,这就是使用同步来实现互斥



互斥的理解

有taskA和taskB两个函数
taskA先行占用,使用厕所
在taskA执行的过程中,taskB又来执行,发现厕所有人使用,进入阻塞状态,blocked
taskA使用完厕所后,发出提醒,告诉taskB
taskB从blocked状态,转换为running状态,使用厕所,然后离开


使用同步来实现互斥


实现同步或互斥的方法

  • 任务通知(task notification)

  • 队列(queue)

  • 事件组(event group)

  • 信号量(semaphoe)

  • 互斥量(mutex)

同步操作的例子

task2等待task1计算完成
task2在task1计算的过程中和task1竞争CPU资源

两个task相互竞争消耗CPU的资源约4s左右

问题在于,task2既然在等待,为什么还要抢占CPU的资源


注释掉task2
这里只消耗了2s,task2没有和task1进行抢占CPU资源


循环检测某个变量来实现同步的方法,有很大的缺陷,会很大的占用CPU资源

RTOS实现同步的话,task不仅要等待某件事情发生,而且在等待的过程中当前task同时进入阻塞状态blocked或者休眠状态suspended

异步


互斥操作的例子

互斥独占的使用串口

多任务系统中使用全局变量来实现互斥,是有隐患的

使用一个总的函数来创建task3和task4

单纯的使用全局变量

通信

在freeRTOS中实现通信并不复杂

在task1中计算出变量后,在task2中就可以访问这个变量 ,这就是通信
通过全局变量来实现通信

复杂的地方在于如何实现同步和互斥

freeRTOS的解决方案

要保证

  • 正确性

  • 效率

    • taskA在使用CPU资源,taskB在等待的过程中应该阻塞或者休眠

    • 等待者要进入休眠或阻塞状态

解决方案

  • 队列(先进先出)

    • 理解队列就是一个流水线或者是一个传送带

  • 时间组(事件的组合)

    • 每一个bit表示一个事件

    • 生产者做完了某件事情就把某位设置为1

    • 消费者可以等待某个事件或某几个事件或若干个事件中的某一个事件,多对多的关系

    • 多个生产者,多个消费者

  • 信号量

    • 队列中传递的是数据

    • 信号量中传递的是计数值

    • taskA生产者完成后,就让计数值+1

    • taskC消费者消费,就让计数值-1

  • 互斥量

    • 信号量中传递的计数值只设置为1或者0

    • 使用互斥量来保护某些临界资源

    • 同一时间只能有一个task来使用CPU资源

    • 还有可能发生优先级反转,(优先级继承)

  • 任务通知

    • 多对1的关系

    • 左边的task通知taskC

    • 可以通知数值,事件等

队列 Queue

队列的基本知识

  • 如何创建、清除、删除队列

  • 队列中消息如何保存

  • 如何向队列发送(写入)数据、如何从队列中读取数据、如何覆盖队列的数据

队列是先进先出的

可以认为队列就是一个常规操作,是一个流水线

写数据时放入尾部,读数据时从头部开始读


左边是工人,右边是消费者
工人生产好商品之后,将商品放入到传送带上去
当队列中有数据时,消费者就可以从队列中去读取数据
队列里如果有多个数据,得到的是最先放入队列中的数据

在队列中存放数据时,可以分为头部head和尾部tail

常规的做法是生产好数据后放入尾部tail
消费者从头部head读数据

把新数据放入head头部的话,新的数据不会覆盖原来的头部head数据

是把原来的数据往后挪一下,队列中会把原来头部head的数据往后挪一下,然后新数据插进来

这些数据是使用环形缓冲区来管理的,所以挪动一个数据并不复杂,效率很高


描述队列

Queue,队列


每个队列的容量不一样,有一个指针指向一个真正用来存放数据的缓冲区

一开始这个队列中没有数据,消费者在等待时应该进入阻塞状态

如何进入阻塞状态,可以先修改自己的状态

但是当队列中有数据时应该能够找到消费者,将其唤醒

所以Queue结构体中,应该有一个List链表,存放等待数据的任务

假如队列被填满了数据,生产者还想往队列中填数据,如果不想覆盖数据的话,就应该等待

所以Queue结构体中应该有一个链表List2,等待写数据,空间的任务


使用队列传输数据

队列的阻塞访问

多个任务可以读写队列,只要知道队列的句柄就行
任务,ISR都可以读写队列

  1. task读写队列时,如果读写不成功,就会进入阻塞Blocked状态,可以指定超时时间

能够读写了就进入就绪Ready状态,否则就阻塞直到超时

  1. 某个task读队列时,如果这个队列没有数据,则该任务进入阻塞Blocked状态,还可以指定阻塞的时间

如果队列有数据了,则该任务立马变为就绪Ready状态
如果一直没有数据,则超时时间到了之后,也会进入就绪Ready状态

  1. 读取队列的task没有限制,多个任务读写空的队列时,这些任务都会进入阻塞状态

当多个任务在等待同一个队列的数据时,当队列中有数据,哪一个task会进入就绪Ready状态

  • 优先级最高的task
  • 如果大家的优先级都一样,等待时间最久的task将进入就绪Ready态

使用队列的流程

  • 创建队列

  • 写队列

  • 读队列

  • 删除队列

创建队列

队列的创建有两种方法

  • 动态分配内存

  • 静态分配内存


队列的结构体xQUEUE



xQUEUE


队列的本质是环形缓冲区
想去创建队列,首先要去创建一个Queue 结构体


创建队列的方法


1、动态创建队列Queue

2、静态创建队列Queue



复位队列



删除队列



写队列

写队列,队列是一个环形缓冲区




队列的长度为
0 - N-1
pcWriteTo指针指向缓冲区的头部head

可以通过pvItemToQueue指针获取队列的数据,大小为ItemSize

拷贝完成后,即写入队列,pcWriteTo指针指向队列的下一个数据,pcWriteTo指针+=ItemSize;

假如队列已经满了,就不应该再写入队列,否则会覆盖之前的老数据
这个时候就可以指定一个等待时间xTicksToWait,如果这个等待时间是0的话,就表示不等待,无法写队列时,会立马返回
不是0的话,就会把调用这个写队列函数的task放入xTasksWaitingToSend链表中来,进入阻塞状态
以后队列中有空间后,再把它唤醒

当写指针写入队列的最后一个数据后,指针跳转到队列的头部,从尾部tail跳转到head中


读队列



无法读出数据时,将会放到队列的xTasksWaitingToReceive链表中,进入阻塞状态,当别人task写这个队列时唤醒


指针pcHead指向数据的首地址,这个不会改变
改变的是pcReadFrom,上一次读取的位置,指向
pcReadFrom+=ItemSize,如果读取超过了队列的大小
pcReadFrom将会重新指向头部,从而读出第0个数据



写数据和读数据时,如果写入的数据满了或者读数据时没有数据,进入阻塞状态,等待
唤醒


唤醒的是最高优先级的Task
如果优先级都相同的话,唤醒的是等待时间最长的Task


查询队列

查询队列中有多少个数据、有多少剩余空间



队列的基本使用

1、使用队列实现同步


Volatile关键字的作用


一般,编译器会对系统做个优化,使得MCU不从内存中读取数据,而是从缓存,或者寄存器中读取,因此,我们必须加voaltile修饰,保证编译器对这个变量不做任何优化.


一般编译器会通过volatile来避免关键的变量编译时被优化,比如说从寄存器变量,每次使用这个变量时都会从寄存器中读取,而不是优化后的(可能是拷贝内存中的数据),确保读出的数据稳定。




读取volatile类型的变量时总会返回最新写入的值


volatile只会干一件事情,告诉编译器别对我这个变量作什么优化,按照我写的代码编译就行,避免多线程问题,你写蠢代码也会比编译出来的。




什么情况下一定要将变量定义为volatile?


  • 寄存器变量
  • 方法外部的被中断历程使用的全局变量
  • 方法外部的被线程使用的全局变量

让task1计算完成后,将累加值sum写入队列中,task2去读取队列,当队列中有数据的时候打印出来,队列中没有数据的话,进入阻塞状态

这样task2在等待的过程中就不会参与CPU的调度

一旦tsak1将累加值写入队列中后,task2从阻塞状态进入就绪状态,从而运行态,读取队列中的数据


步骤:

  1. 创建队列
    1. 指定队列的长度(队列中有多少个成员)
    2. 指定队列中数据的大小(队列中数据(成员)的大小)
  2. task1向队列中写入数据
  3. task2从队列中读出数据


使用队列就实现了同步




2、使用队列实现互斥

想让task3和task4实现独占的使用串口,使用队列来实现互斥

给串口加锁来实现互斥



队列的示例

  • 分辨数据源
  • 传输大块的数据
  • 邮箱

分辨数据源



传输大块数据

向队列中传入地址即可

在队列中我们传入的是值,将值拷贝进队列中
这个值可以是数据,也可以是地址



使用地址去访问数据时,数据存放在RAM中,要注意这几点

  • RAM被称为共享内存,要确保不能同时修改RAM,在写队列时只有发送者修改这个RAM,在读队列时只能由接收者访问这块RAM
  • RAM要保持可用,这块RAM应该是全局变量,或者是动态分配的内存
  • 对于动态分配的内存,要确保其不能提前释放,要等接收者使用完后在释放

邮箱


队列集 Queue Set

从多个队列中获得数据,就是队列集

比如有鼠标、按键、以及触摸屏都可以产生数据,并且都可以放入自己的队列当中

应用程序App,支持3种输入设备,这个时候就需要读取这三个队列,等待这三个队列

任意一个队列有数据,都可以唤醒App,让其继续工作

队列集也是一个队列
之前的队列里面放的是数据,而队列集里放的是队列

假设鼠标、按键、以及触摸屏都创建了三个队列,如果程序想同时创建这三个队列

那么应该创建一个队列集 Queue Set


1、队列集长度
  1. 这个队列集的长度应该是:队列A的长度+队列B的长度+队列C的长度

否则在A、B、C都满的情况下,队列集没有空间存放所有的handle



2、队列集建立联系
  1. 队列集和队列建立联系

队列的handle会指向队列集

3、产生数据,写入队列,队列handle传入队列集
  1. 按下触摸屏touch,touch产生数据,这个数据存放到touch Queue的队列当中(xQueueSend),这个函数的内部还会把handle放入Queue Set当中


这个时候Queue Set当中就有数据了


4、读队列集
  1. 读Queue Set

Read Queue Set 函数将会返回某一个队列Queue

5、读队列
  1. 读Queue

读取Queue Set一次,返回一个队列后,只能读取Queue一次



具体步骤



队列集的使用

创建两个task
task1往Queue1中写入数据
tsak2往Queue2中写入数据

task3使用Queue Set队列集监测这两个队列


1、创建两个队列Queue
2、创建Queue Set

/_队列集的长度应该是 队列A的长度+队列B的长度_
xQueueSetHandle = xQueueCreateSet(4); /

3、把两个Queue 添加进Queue Set中(建立联系)

注意这是建立联系,并不是放到Queue Set中


/_ 3、把两个Queue和Queue Set建立联系_/
    xQueueAddToSet(xQueueHandle1, xQueueSetHandle);
    xQueueAddToSet(xQueueHandle2, xQueueSetHandle);

4、创建三个task

task1和task2分别往队列中写入数据
task3来监测Queue Set,看哪一个Queue有数据,哪一个有数据,就把数据读出来



task1把数据写入Queue1,同时会把Queue1队列的handle,放入Queue Set中

task3在等待Queue Set, Queue1有数据,返回handle,读取handle的数据,打印-1

task2同理


使用队列集需要配置FreeRTOS_Config.h文件

#define configUSE_QUEUE_SETS 1 /_Queue Set 函数开关_/


task3 Queue Set监测队列Queue1 和 Queue2,如果队列中有数据,获取队列的handle,从而读Queue的数据,进而打印数据





队列集可以去监测多个队列,可以从多个队列中去挑出有数据的队列,然后去读队列,进而去读队列中的数据