这是第二弹,由于CSDN长度的限制,所以把FreeRTOS学习分为几部分来发,这是第二部分
主要包括同步与互斥通信、队列、队列集的使用
等
第一弹
:FreeRTOS学习笔记(1、FreeRTOS初识、任务的创建以及任务状态理论、调度算法等)
同步、互斥与通信
举个例子
多任务系统当作是一个团队,里面的每一个task就相当于团队里的一个人
团队成员之间要协调工作进度 -------> 同步
争用会议室 -------> 互斥
沟通 -------> 通信
任务通知(task notification)、队列(queue)、事件组(event group)、信号
量(semaphoe)、互斥量(mutex)等
taskA正在做某些事情,taskB必须等待taskA完成后才能继续往下做,这就是同步,步调一致
对于某些资源,同一时间只能有一个task使用,这个task1必须独占的使用它
一个task1使用过了之后,另一个task2就不能使用
但是这个task1使用完后,提醒task2,你可以使用了,这就是使用同步来实现互斥
有taskA和taskB两个函数
taskA先行占用,使用厕所
在taskA执行的过程中,taskB又来执行,发现厕所有人使用,进入阻塞状态,blocked
taskA使用完厕所后,发出提醒,告诉taskB
taskB从blocked状态,转换为running状态,使用厕所,然后离开
使用同步来实现互斥
task2等待task1计算完成
task2在task1计算的过程中和task1竞争CPU资源
两个task相互竞争消耗CPU的资源约4s左右
注释掉task2
这里只消耗了2s,task2没有和task1进行抢占CPU资源
循环检测某个变量来实现同步的方法,有很大的缺陷,会很大的占用CPU资源
RTOS实现同步的话,task不仅要等待某件事情发生,而且在等待的过程中当前task同时进入阻塞状态blocked或者休眠状态suspended
互斥独占的使用串口
多任务系统中使用全局变量来实现互斥,是有隐患的
使用一个总的函数来创建task3和task4
单纯的使用全局变量
在freeRTOS中实现通信并不复杂
在task1中计算出变量后,在task2中就可以访问这个变量 ,这就是通信
通过全局变量来实现通信
复杂的地方在于如何实现同步和互斥
要保证
解决方案
队列是先进先出的
可以认为队列就是一个常规操作,是一个流水线
写数据时放入尾部,读数据时从头部开始读
左边是工人,右边是消费者
工人生产好商品之后,将商品放入到传送带上去
当队列中有数据时,消费者就可以从队列中去读取数据
队列里如果有多个数据,得到的是最先放入队列中的数据
在队列中存放数据时,可以分为头部head和尾部tail
常规的做法是生产好数据后放入尾部tail
消费者从头部head读数据
把新数据放入head头部的话,新的数据不会覆盖原来的头部head数据
是把原来的数据往后挪一下,队列中会把原来头部head的数据往后挪一下,然后新数据插进来
这些数据是使用环形缓冲区来管理的,所以挪动一个数据并不复杂,效率很高
Queue,队列
每个队列的容量不一样,有一个指针指向一个真正用来存放数据的缓冲区
一开始这个队列中没有数据,消费者在等待时应该进入阻塞状态
如何进入阻塞状态,可以先修改自己的状态
但是当队列中有数据时应该能够找到消费者,将其唤醒
所以Queue结构体中,应该有一个List链表,存放等待数据的任务
假如队列被填满了数据,生产者还想往队列中填数据,如果不想覆盖数据的话,就应该等待
所以Queue结构体中应该有一个链表List2,等待写数据,空间的任务
多个任务可以读写队列,只要知道队列的句柄就行
任务,ISR都可以读写队列
能够读写了就进入就绪Ready状态,否则就阻塞直到超时
如果队列有数据了,则该任务立马变为就绪Ready状态
如果一直没有数据,则超时时间到了之后,也会进入就绪Ready状态
当多个任务在等待同一个队列的数据时,当队列中有数据,哪一个task会进入就绪Ready状态
- 优先级最高的task
- 如果大家的优先级都一样,等待时间最久的task将进入就绪Ready态
使用队列的流程
队列的创建有两种方法
队列的本质是环形缓冲区
想去创建队列,首先要去创建一个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
一般,编译器会对系统做个优化,使得MCU不从内存中读取数据,而是从缓存,或者寄存器中读取,因此,我们必须加voaltile修饰,保证编译器对这个变量不做任何优化.
一般编译器会通过volatile来避免关键的变量编译时被优化,比如说从寄存器变量,每次使用这个变量时都会从寄存器中读取,而不是优化后的(可能是拷贝内存中的数据),确保读出的数据稳定。
读取volatile类型的变量时总会返回最新写入的值。
volatile只会干一件事情,告诉编译器别对我这个变量作什么优化,按照我写的代码编译就行,避免多线程问题,你写蠢代码也会比编译出来的。
什么情况下一定要将变量定义为volatile?
- 寄存器变量
- 方法外部的被中断历程使用的全局变量
- 方法外部的被线程使用的全局变量
让task1计算完成后,将累加值sum写入队列中,task2去读取队列,当队列中有数据的时候打印出来,队列中没有数据的话,进入阻塞状态
这样task2在等待的过程中就不会参与CPU的调度
一旦tsak1将累加值写入队列中后,task2从阻塞状态进入就绪状态,从而运行态,读取队列中的数据
步骤:
想让task3和task4实现独占的使用串口,使用队列来实现互斥
向队列中传入地址即可
在队列中我们传入的是值,将值拷贝进队列中
这个值可以是数据,也可以是地址
使用地址去访问数据时,数据存放在RAM中,要注意这几点
从多个队列中获得数据,就是队列集
比如有鼠标、按键、以及触摸屏都可以产生数据,并且都可以放入自己的队列当中
应用程序App,支持3种输入设备,这个时候就需要读取这三个队列,等待这三个队列
任意一个队列有数据,都可以唤醒App,让其继续工作
队列集也是一个队列
之前的队列里面放的是数据,而队列集里放的是队列
假设鼠标、按键、以及触摸屏都创建了三个队列,如果程序想同时创建这三个队列
那么应该创建一个队列集 Queue Set
否则在A、B、C都满的情况下,队列集没有空间存放所有的handle
队列的handle会指向队列集
这个时候Queue Set当中就有数据了
Read Queue Set 函数将会返回某一个队列Queue
读取Queue Set一次,返回一个队列后,只能读取Queue一次
创建两个task
task1往Queue1中写入数据
tsak2往Queue2中写入数据
task3使用Queue Set队列集监测这两个队列
/*队列集的长度应该是 队列A的长度+队列B的长度*
xQueueSetHandle = xQueueCreateSet(4); /
注意这是建立联系,并不是放到Queue Set中
/* 3、把两个Queue和Queue Set建立联系*/
xQueueAddToSet(xQueueHandle1, xQueueSetHandle);
xQueueAddToSet(xQueueHandle2, xQueueSetHandle);
task1和task2分别往队列中写入数据
task3来监测Queue Set,看哪一个Queue有数据,哪一个有数据,就把数据读出来
task1把数据写入Queue1,同时会把Queue1队列的handle,放入Queue Set中
task3在等待Queue Set, Queue1有数据,返回handle,读取handle的数据,打印-1
task2同理
#define configUSE_QUEUE_SETS 1 /*Queue Set 函数开关*/
task3 Queue Set监测队列Queue1 和 Queue2,如果队列中有数据,获取队列的handle,从而读Queue的数据,进而打印数据
队列集可以去监测多个队列,可以从多个队列中去挑出有数据的队列,然后去读队列,进而去读队列中的数据