一:队列的读写
对于队列,我们只要理解它就是一个环形缓冲区,然后还可以去休眠、唤醒,就可以了。
1.写数据的时候,如果没有空间自己就休眠
2.读数据的时候,如果没有数据自己就休眠
3.写数据成功之后,如果有其他任务在等待数据,就把它唤醒
4.读数据成功之后,如果有其他任务在等待空间,就把它唤醒
比环形缓冲区多了:休眠和唤醒的操作。
我们再来回顾一下怎么去创建一个队列:
这里,故意把这个程度改为4,来看看会发生什么事。
我们要关注的是这个图片里面黄色的那两个变量;
一个是写位置:pcWriteTo
,另一个是读位置:pcReadFrom
。
怎么写数据呢?假设初始情况和写了一个数据后,分别如下:
1.写到哪里去?pcWriteTo
2.写完之后,pcWriteTo
指向下一个位置
那怎么读数据?假设初始情况和写了一个数据后,分别如下:
1.pcReadFrom
先调整为指向下一个位置
2.再从pcReadFrom
读出一个元素
读出数据之后,数据还保存在队列里, 但是指针位置变了,所以之前的那些数据并不会再次读出来,也就相当于之前的数据被废弃了。
读和写的时候,都是使用memcpy
,那么复制多长的数据呢?
创建队列的时候就指定有每一个元素有多长。
二:队列的休眠和唤醒
下面我们来看看休眠和唤醒的操作。
写队列、读队列的操作是很类似的。
休眠的时间可以设置成:0、portMAX_DELAY、某个值
0的话就表示不休眠:成就成,不成就拉倒。
通过返回值来判断是否成功:
上一次课我们讲了写队列的操作,今天我们来讲读队列。
其实大家英文好的话,看这个代码,它的注释都很完善:
他会判断一下时间到了没有。
时间没到,并且队列是空的,那显然得不到数据,那就休息一下。
怎么休息呢?
1.当前任务,要把自己放到队列的某个链表去
2.当前任务,把自己从ready list
放到delay list
假设想去读数据,但是没有数据,则会被放到队列的xTasksWaitingToReceive
上。
为什么要把当前任务放到队列的xTasksWaitingToReceive
链表?
这里是登记一下,等待的数据来了可以唤醒这个任务。
这里会涉及三个链表:
1.我在等待数据,那别人怎么知道你在等待数据?就需要把自己放到队列的xTasksWaitingToReceive
链表
2.我要休眠,怎么休眠?把自己从ready list
放到delay list
再强调一下,超时时间,不影响排队的位置:
三:队列的常规应用
队列的常规应用:
1.写到队列的尾部
2.从队列的头部读到数据
就是先写到队列的数据,会被先读出来,FIFO,先进先出,这是常规用法。
我们还可以覆盖地写:
1.队列长度是1,也就是里面只会有一个元素
2.写了第1个数据之后,还可以继续写第2个数据、第3个数据
本来,如果队列已经满了,是无法再写入新的数据的,但是可以用另外一个函数:
这函数就是覆盖的写:要注意,你能使用这个函数,前提是队列的长度只有1。
既然是覆盖的写:那就是原来里面的数据会被覆盖。
从上面图可以看出来,一般的队列操作时,队列没有满才可以写数据。
但是如果使用了xQueueOverwrite
,即使队列满了也可以写数据。
我们看到了覆盖:写的时候比较特殊,
还有一个操作:读的时候比较特殊。
读的时候,本来应该是这样的,先移动读位置,读到数据:
对于xQueuePeek
,他是这样的:
这两个特殊的写操作、读操作,组合起来就可以得到一个“邮箱”。
我觉得这个邮箱取名并不好,也许是因为“橱窗”会更好。
他的适用场景是这样:
A、B、C、D 4个学习委员,去买报纸。
只要有一个人能够买到报纸,全班所有的同学都可以写“实事作文”。
ABCD这些任务,就可以调用:xQueueOverwrite
。
其他同学,就可以调用:xQueuePeek
。
在这个场景里面, A买到了报纸,其他同学都可以看到这个报纸,B买到了报纸,其他同学都可以看到这个报纸。
我们根本不在乎是谁买到了报纸,谁买到都可以,这个报纸是共享的,谁都可以看到。
四:队列的使用
我们来看看队列在什么情况下使用。
这就是队列的使用场景,左边生产数据,右边消费数据。
在我们的项目里,就可以使用队列,
我们用环形缓冲区的地方,就可以改成使用队列。
我们在使用这些函数时,要注意使用的位置。
这些函数有两个版本:
1.在任务里面使用
2.在中断函数里面使用(有FromISR
后缀)
五:晚课学员提问
1. 问: 老师,多个队列休眠,放入xTaskWaitingToReceive
队列里面时候,在队列里面的排序是在哪里?
答: 在队列里面如果有多个任务都在等待数据,谁排在最前面?
1.谁先来排队,谁就排在前面
2.谁的优先级更高,他就可以插队
列表项,里面有一个xItemValue
,会先根据这个词在链表里面找到一个位置,再把它插到链表里去。
总之在链表里面会根据优先级来排放那些任务,如果优先级相同,就会放到同优先级的任务的后面。
举个例子:
在上面的图里,有7个任务在等待数据。
这时候,有一个优先级为3的任务也来等待数据,它在哪里排队?
那如果再有一个优先级为2的任务来等待数据,他插在哪里?
使用这种方法,就可以保证:
1.优先级高的任务,排前面
2.优先级相同的任务,按照函数调用时间来排
2. 问: freeRTOS 如果在同一优先级的任务,是不是有没抢断了?只能链表时间片来轮询?
答: 没错,同优先级的任务,只能轮询。
3. 问: 各种不同的输入,输入的数据大小不一样,怎么应对这种情况呢?
答: 我们创建队列的时候就指定了元素的大小,我们去读写队列时,都是使用memcpy
。
所以,假设数据源有A和B。
A本来只需要写一个字节, B需要写100个字节。
你偏要使用同一个队列来处理A和B提供的数据,那就只能牺牲一些效率,浪费一些空间。
对于A,即使只需要写一个字节,也需要那么memcpy
100个字节
4. 问: 老师,任务的优先级怎么配置比较合适,比如有三个task,task1,task2,task3。task1负责采集数据发送给task2处理,task2处理完数据后发给task3进行对外发送。频率都是100HZ。
答: 他们有依赖关系,也就是说有前后关系,所以优先级不重要。
如果:task2出来的过程总,允许task1采集数据,那么,task1的优先级更高。
在实际的开发过程中:task1 > task2 > task3
task1优先级最高,确保了数据不会丢失,
task2 > task3 : task2处理完之后,task3才能处理
5. 问: xQueueReceive
的最开头,有个判断时间到没到,这个是怎么判断的,从哪里算是start的时间?
答: 不是在开头判断时间,队列中没有数据才判断时间。
6. 问:
答:
中断函数要考虑一点:要尽快执行。
我们假设在中断里面写队列:
1.写入了数据
2.导致一个优先级非常高的任务从阻塞变为了就绪
3.会马上调度吗?
4.不会,我的中断都还没执行完呢
5.怎么做?记录下来:
6.等中断处理完了,才去触发调度
为什么要这么做呢?
我们反过来假设:在中断里面,没处理完中断就要去调度、切换任务。
1.如果这个中断函数里面有两个循环,第1个循环会去切换任务A,第2个循环要去切换任务B
2.应该把它汇聚起来,只去执行一次切换:只在最后时刻切换任务B
你切换任务A,中断高于任务,没有用,还不如等到中断处理完的时候,再去挑出优先级最高的任务B,这样只需要切换一次。
7. 问: 老师,3个任务的优先级那个,可不可以这样思考:如果读任务的优先级高,那么队列中就只能写入一个数据了,所以一般都要写任务优先级高,这样队列才有可能写满。
答: 还是要具体分析。
1.不想让数据丢失,写任务优先级就要高
2.数据丢失没关系,一旦得到数据,就要全力处理:这个时候写任务的优先级可以调低
对于第2种情况,其实挺普遍的。
我们在屏幕上按下某个按钮之后,他就卡死了,实际上是在进行数据的处理。
在处理数据的过程中,你再去点击屏幕也没有任何作用,处理完数据之后,你之前点击屏幕那些动作也没有任何作用。
8. 问: 中断可以不能被任务打断,是不是可以因为中断没有TCB(调度器只认识TCB),无法保存现场和恢复现场?但是中断可以被中断打断,是因为中断可以使用主堆栈保存现场和恢复现场(不同中断服务函数之间使用的主堆栈是连续的)?老师可以这样理解吗?
答: 中断可以被打断,中断不可以阻塞。
不是这个原因,这是由硬件决定的。
任务运行的时候,一旦发生了中断, CPU就一定会去执行中断
在执行中断的过程中,有什么理由暂停中断的处理、去执行任务呢?
9. 问: 中断里调度会发生什么?
答:
中断里调度,只会去设置pxCurrentTCB
,并不会运行任务。
我们反过来假设:在中断里面,没处理完中断就要去调度、切换任务。
如果这个中断函数里面有3个循环,
第1个循环会去切换任务A,pxCurrentTCB
= task A
第2个循环要去切换任务B, pxCurrentTCB
= task B
第3个循环要去切换任务C, pxCurrentTCB
= task C
前面两个循环毫无意义,你去设置pxCurrentTCB
也没有用,中断没执行完都不会去执行任务。
所以,在中断里调用xQueueSendToBackFromISR
时,只会设置一个变量,表示说“需要调度”
等中断处理完,再设置pxCurrentTCB
等于最高优先级的任务。