前面本喵讲解了和任务相关的FreeRTOS源码,进行再来介绍一下用于任务间通信的几种数据结构源码。
队列(Queue)、队列集(Queue Set)、信号量(Semaphore)、互斥量(Mutex)、递归互斥量,这5种机制的核心都是通用队列(xQueueGenericCreate):
上面函数都调用了xQueueGenericCreate
,创建一个通用队列。这个函数原型为:
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
const uint8_t ucQueueType );
参数含有如下:
参数 | 含义 |
---|---|
uxQueueLength | 队列长度,对于信号量、互斥量,这个参数为1 |
uxItemSize | 队列中数据长度, 对于队列:这个参数由用户设置; 对于队列集:这个参数是 sizeof(Queue_t *) ;对于信号量、互斥量:这个参数是0 |
ucQueueType | 队列类型,分别是: #define queueQUEUE_TYPE_BASE ( uint8_t ) 0U #define queueQUEUE_TYPE_SET ( uint8_t ) 0U #define queueQUEUE_TYPE_MUTEX ( uint8_t ) 1U #define queueQUEUE_TYPE_COUNTING_SEMAPHORE (uint8_t )2U#define queueQUEUE_TYPE_BINARY_SEMAPHORE ( uint8_t )3U #define queueQUEUE_TYPE_RECURSIVE_MUTEX ( uint8_t)4U |
根据类型创建不同的类型结构。
如上图所示队列结构体xQUEUE
定义。这个结构体可以用来实现队列、队列集、信号量、互斥量:
pcWriteTo
、xQueue里的pcReadFrom
用来维护环形缓冲区:队列/队列集用来读写数据。xTasksWaitingToSend
:用来管理"想发送数据,但是没有空间,因此阻塞"的任务,信号量、互斥量不会用到它。xTasksWaitingToReceive
:用来管理"想读取数据,但是没有数据,因此阻塞"的任务。uxMessagesWaiting
:队列/队列集用来记录有多少个有效数据,信号量/互斥量用来记录数值。
如上图所示,创建不同结构的函数虽然不同,但是最终调用的都是xQueueGenericCreate
函数,只是传入的参数不同。
这里创建的是可以存放数据的队列。
如上图代码所示,使用xQueueCreate
创建队列,在调用时传入队列长度和每一项大小两个参数,该函数又是xQueueGenericCreate
的封装,本质上就是在创建一个通用队列,只是它的类型是queueQUEUE_TYPE_BASE
表明这是一个用来存放数据的队列。
如上图创建通用队列xQueueGenericCreate
函数,会使用pvPortMalloc
在堆区上开辟一段空间,这段空间包括通用队列结构体Queue_t
和uxQueueLength * uxItemSize
个字节用来存放数据的的空间。
然后再让pucQueueStroage
跳过sizeof(Queue_t)
个字节,指向存放数据的起始位置,再调用prvInitialiseNewQueue
来初始新化队列:
如上图所示prvInitialiseNewQueue
函数,先让Queue_t
中的pcHead
环形缓冲区头指针指向存放数据的起始位置,然后再给uxLength
和uxItemSize
成员赋值,再调用xQueueGenericReset
来复位通用队列。
如上图所示xQueueGenericReset
函数,关闭中断后进入临界区,让环形缓冲区的尾指针pcTail
指向存放数据的末尾位置,让pcWriteTo
写指针指向存放数据的起始位置,并且给记录队列中有效数据个数的uxMessageWaiting
变量赋值为0。
最重要的时,让读环形缓冲区的指针pcReadFrom
指向存放数据的最后一个数据所在位置,并不指向头,而是指向上一次读数据的位置。
然后就是调用vListInitialise
来初始化队列结构体Queue_t
中的两个链表,一个用来管理因写数据而阻塞的任务,另一个用来管理因读数据而阻塞的任务。最后恢复中断出临界区。
如上图所示便是队列创建好后的示意图,总的来说,创建过程分为如下几步:
Queue_t
队列头和环形缓冲区。pcHead
和写指针pcWrite
指向存放数据空间的起始位置。QueuePointers_t
中的尾指针pcTail
指向数据存满后的最后位置,读指针pcReadFrom
指向存放最后一个数据的位置。xTasksWaitingToSend
和xTasksWaitingToReceive
两个链表。Queue_t
中其他成员赋予合适的值。
如上图,使用xQueueSned
函数向队列中写数据,最后会调用通用写数据函数xQueueGenericSend
,在调用时会指定写数据的位置queueSEND_TO_BACK
。
队列有空:
如上图代码所示,当队列中有空位置时,也就是uxMessageWaiting < uxLength
,调用prvCopyDataToQueue
将数据复制到环形缓冲区中。
如上图prvCopyDataToQueue
函数,使用memcpy
将要写入队列的数据复制到队列中,然后更新pcWriteTo
写指针,如果和尾指针pcTail
相同,则让其指向pcHead
来维持环状。最后再让有效数据个数uxMessageWiting
加一。
然后再使用listLIST_IS_EMPTY
来判断一下管理读取数据链表中是否有任务在等待,如果有,则调用xTaskRemoveFromEventList
将其移除。然后写数据成功返回。
如上图xTaskRemoveFromEventList
函数,首先从等待读取数据的链表中选出要唤醒任务的TCB,然后将TCB从该链表(事件链表)中移除。
如果此时调度器是开着的,则将唤醒的任务放入到就绪链表中,如果是调度器是关着的,则将唤醒的任务放入到xPendingReadyList
链表中,待调度器打开后从该链表中将TCB放入就绪链表中。
如果唤醒的任务优先级高于现在正在执行的任务,则发起调度。
队列没空:
如上图xQueueSend
函数中队列没空位置的处理代码,如果时间xTicksToWait
为0,说明不愿意等待,则立刻返回errQUEUE_FULL
表示队列满了,无法写入数据。
如果愿意等待,则调用vTaskInternalSetTimeOutState
设置一下时间:
如上图所示,就是记录一下当前的系统时间,方便后面进行超时唤醒。
在记录完时间以后立刻恢复中断,然后再关闭调度器,因为关闭中断的代价太大了,能关闭调度器就不关中断。
检查一下该任务等待是否超时,如果没有超时则再确认一下队列中真的没空。
- 因为在恢复中断后,虽然调度器关了,但是该函数
xQueueSend
随时可能被中断打断,如果打断了,中断函数执行一定时间后再次轮到该函数执行,很有可能就超时了,也有可能中断函数会从队列中读取数据,此时队列就不空了。
然后再调用vTaskPlaceOnEventList
将这个写数据的任务放入到等待写数据的链表中。
如上图vTaskPlaceOnEventList
函数所示,在该函数中,除了将放入到等待写入数据的事件链表中外,还要将自己放入到等待超时时间到来的延时状态链表中。
重新开启调度器。
总的来说,向队列中写数据分为如下几步:
如果队列不满,则将要写的数据复制到队列中,并且从等待读数据的链表xTasksWaitingToReceive
唤醒一个任务(如果有任务在等待的话)。
如果队列满了,则看该任务是否愿意等待:
xTasksWaitingToSend
中,并且根据超时时间也将其放入延时链表xDelayList
中。队列中有数据:
如上图xQueueReceive
读取数据的函数,先得到该队列中有效数据个数,如果大于0说明队列中有数据,此时调用prvCopyDataFromQueue
函数从队列中复制一个数据到目标地址。
然后调用listLIST_IS_EMPTY
判断等待写数据的链表xTasksWaitingTosend
中是否有任务,如果有,则此时队列中有空位置,调用xTaskRemoveFromEventList
唤醒一个任务。
最后读数据成功返回。
队列没有数据:
如上图xQueueReceive
函数中部分代码所示,当队列中没有数据时,会先判断该任务是否愿意等待数据到来,如果不愿意,则直接错误返回,表示队列为空。
如果愿意等待,则设置一下超时时间,在恢复中断,关闭调度器后,再次确认是否超时和队列是否为空,原因和前面写数据时一样。
然后将自己的TCB放入到等待读取数据的链表xTasksWaitingToReceive
中,然后主动发起一次调度。
总的来说,读数据和写数据非常类似,主要分为如下几步:
回答一个问题,为什么前面本喵讲解的这些代码都在一个for
循环中呢?
如上图所示,无论是xQueueSend
还是xQueueReceive
,所有对队列的操作和判断都是在这个死循环for
中。
如上图代码所示,在将任务放入到事件链表中后会发起一次调度,此时任务本身就处于阻塞状态了。
当被唤醒时,有两种可能:
无论哪种情况下被唤醒,该任务都是从阻塞处恢复执行。因为是for
循环,所以该任务会重新对队列进行一遍前面的判断和操作,在重新判断和操作的过程中:
如上图,在重新判断和操作的过程中,在正常读取数据或者写入数据后,执行return pdPASS
成功返回。
如上图所示,超时唤醒后,执行return errQUEUE_XXX
错误返回。
队列集的核心,就是队列。
如上图,调用xQueueCreateSet
创建队列集的本质就是调用xQueueGenericCreate
来创建通用队列,只是队列类型是queueQUEUE_TYPE_SET
,表示这是一个队列集。
如上图,其他和创建普通队列一样,只是在prvInitialiseNewQueue
中初始化新队列时,将属于队列集的pxQueueSetContainer
设置为NULL
。
如上图所示,队列集创建好后,和队列结构几乎相同,只是队列集中每个数据存放的都是Queue_t*
队列指针,而且多了一个struct QueueDefinition * pxQueueSetContainer
成员,且其初始值是NULL
。
- 如果使用队列集的话,普通队列中也会多出
pxQueueSetContainer
成员,且初始值是NULL
。
将队列添加到队列集中:
如上图xQueueAddToSet
函数所示,只是让要添加到队列集中的队列里的pxQueueSetContainer
成员指向队列集xQueueSet
。
也就是说,被添加到的队列集中的队列,都可以通过pxQueueSetContainer
指针找到队列集xQueueSet
。
写队列集:
并没有专门的任务来写队列集,写队列集只是写队列时顺带手的事:
上图所示,在xQueueGenericSend
中向队列写数据时,如果使用了队列集,则在调用prvCopyDataToQueue
将要写的数据复制到队列中后,再调用prvNotifyQueueSetContainer
将队列的地址写入到pxQueueSetContainer
指向的队列集中。
如上图prvNotifyQueueSetContainer
函数,当队列集中有空位置时,将本次写队列的队列地址复制到队列集中,然后判断一下队列集中等待读取数据的链表中是否有任务在等待,如果有则唤醒。
- 队列集中存放的是队列的句柄。
- 每当有任务向队列中写数据时,顺手会将要写的队列句柄写到队列集中。
只有这样才能在向队列集中写数据时有足够的空间,FreeRTOS对于写队列集也没有阻塞的机制。
对队列集:
如上图,调用xQueueSelectFromSetFromISR
读取队列集时,会调用xQueueReceive
来读取。
传参时也没有特别传什么参数,所以和普通队列的读取是一样的:
xTasksWaitingToReceive
中阻塞等待。读取队列集成功后返回的是队列集中存放的某个队列句柄:
如上图,再使用xQueueReceive
从返回的队列句柄指向的队列中读取具体的数据。
无论是普通队列,还是队列集,都是对通用队列的进一步封装,所以说,通用队列才是核心。