FreeRTOS消息队列

队列简介 

 更详细的操作入下图所示:

传输数据的方法

FreeRTOS中的队列传输使用的是拷贝:把数据、把变量的值复制进队列里

FreeRTOS 使用拷贝值的方法,这更简单:
(1) 局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据。
(2)无需分配 buffer 来保存数据,队列中有 buffer
(3)局部变量可以马上再次使用
(4)发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
(5)如果数据实在太大,你还是可以使用队列传输它的地址
(6)队列的空间有 FreeRTOS 内核分配,无需任务操心
(7)对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把数据复制出队列。

队列函数

使用队列的流程: 创建队列 写队列 读队列 删除队列

创建

队列的创建有两种方法:动态分配内存、静态分配内存。

函数原型如下:
动态分配:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );

静态分配:

QueueHandle_t xQueueCreateStatic ( UBaseType_t uxQueueLength , UBaseType_t uxItemSize , uint8_t * pucQueueStorageBuffer , StaticQueue_t * pxQueueBuffer );

复位

队列刚被创建时,里面没有数据;使用过程中可以调用 xQueueReset() 把队列恢复为初始状态,此函数原型为:

/* pxQueue : 复位哪个队列;

* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);

删除 

删除队列的函数为 vQueueDelete() ,只能删除使用动态方法创建的队列,它会释放内存。原型如下:
void vQueueDelete( QueueHandle_t xQueue );

写队列 

可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
任务中往尾部写入:
/* 等同于xQueueSendToBack
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend(
 QueueHandle_t xQueue,
 const void *pvItemToQueue,
 TickType_t xTicksToWait
 );

在ISR中往尾部写入:

/* 
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
 QueueHandle_t xQueue,
 const void *pvItemToQueue,
 BaseType_t *pxHigherPriorityTaskWoken
 );
这些函数用到的参数是类似的,统一说明如下:

+

读队列

使用 xQueueReceive() 函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在 ISR 中使用。函数原型如下:
BaseType_t xQueueReceive( QueueHandle_t xQueue,
 void * const pvBuffer,
 TickType_t xTicksToWait );
BaseType_t xQueueReceiveFromISR(
 QueueHandle_t xQueue,
 void *pvBuffer,
 BaseType_t *pxTaskWoken
 );
参数说明如下:
           

查询

可以查询队列中有多少个数据、有多少空余空间。函数原型如下:
/*
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
/*
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );

覆盖/偷看

当队列长度为 1 时,可以使用 xQueueOverwrite() xQueueOverwriteFromISR()来覆盖数据。
注意,队列长度必须为 1。当队列满时,这些函数会覆盖里面的数据,这也以为着这些函数不会被阻塞。
函数原型如下:
/* 覆盖队列
* xQueue: 写哪个队列
* pvItemToQueue: 数据地址
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueueOverwrite(
 QueueHandle_t xQueue,
 const void * pvItemToQueue
 );
BaseType_t xQueueOverwriteFromISR(
 QueueHandle_t xQueue,
 const void * pvItemToQueue,
 BaseType_t *pxHigherPriorityTaskWoken
 );
如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,要留给后来人。那么可以使用" 窥视 " ,也就是 xQueuePeek() xQueuePeekFromISR() 。这些函数会从队列中复制出数据,但是不移除数据。这也意味着,如果队列中没有数据,那么" 偷看 " 时会导致阻塞;一旦队列中有数据,以后每次" 偷看 "都会成功。 函数原型如下:
/* 偷看队列
* xQueue: 偷看哪个队列
* pvItemToQueue: 数据地址, 用来保存复制出来的数据
* xTicksToWait: 没有数据的话阻塞一会
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueuePeek(
 QueueHandle_t xQueue,
 void * const pvBuffer,
 TickType_t xTicksToWait
 );
BaseType_t xQueuePeekFromISR(
 QueueHandle_t xQueue,
 void *pvBuffer,
 );

队列的阻塞访问(Key!!!)

只要知道队列的句柄,谁都可以读、写该队列。任务、ISR 都可读、写队列。可以多个任务读写队列。
任务读写队列时,简单地说:如果读写不成功,则阻塞;可以指定超时时间。口语化地说,就是可以定个闹钟:如果能读写了就马上进入就绪态,否则就阻塞直到超时。 某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。既然读取队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?
优先级最高的任务
如果大家的优先级相同,那等待时间最久的任务会进入就绪态
跟读队列类似,一个任务要写队列时,如果队列满了,该任务也可以进入阻塞状态:还可以指定阻塞的时间。如果队列有空间了,则该阻塞的任务会变为就绪态。如果一直都没有空间,则时间到之后它也会进入就绪态。既然写队列的任务个数没有限制,那么当多个任务写" 满队列 " 时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的空间。当队列中有空间时,哪个任务会进入就绪态?
优先级最高的任务
如果大家的优先级相同,那等待时间最久的任务会进入就绪态
这里要补充一点,进入阻塞态就是把任务写到阻塞链表里面,就绪态就是把任务写入到就绪链表里面。

队列进阶使用(队列集)

假设有 2 个输入设备:红外遥控器、旋转编码器,它们的驱动程序应该专注于“产生硬件数据”,不应该跟“业务有任何联系”。比如:红外遥控器驱动程序里,它只应该把键值记录下来、写入某个队列,它不应该把键值转换为游戏的控制键。在红外遥控器的驱动程序里,不应该有游戏相关的代码,这样,切换使用场景时,这个驱动程序还可以继续使用。 相当于多一个队列进行中转,一个队列对硬件数据进行处理,并把自己写进另一个队列,另一个队列会根据自己队列里面的”数据“,来对硬件数据转换成自己需要的数据。
队列集的本质也是队列,只不过里面存放的是“队列句柄”。 使用过程如下:
a. 创建队列 A,它的长度是 n1
b. 创建队列 B,它的长度是 n2
c. 创建队列集 S,它的长度是“n1+n2”
d. 把队列 A、B 加入队列集 S
e. 这样,写队列 A 的时候,会顺便把队列 A 的句柄写入队列集 S
f. 这样,写队列 B 的时候,会顺便把队列 B 的句柄写入队列集 S
g. InputTask 先读取队列集 S,它的返回值是一个队列句柄,这样就可以知道哪个队列有
有数据了;然后 InputTask 再读取这个队列句柄得到数据。

创建队列集

QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength )

把队列加入队列集

BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,
 QueueSetHandle_t xQueueSet );

读取队列集

QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,
 TickType_t const xTicksToWait );

队列的使用示例

这里我推荐b站韦东山老师的课程,他也有讲解FreeRTOS的使用,大家要去看队列的使用示例,可以去看韦老师的课程,他确实讲的很好,我这里只是教内部机制和原理,只要会内部机制和使用原理,那么,读者可以随便找个以前写过的代码,加入队列,就可以验证自己是否成功使用到队列,

队列的内部机制核心

核心是:关中断、环形缓冲区、链表!!! 

 怎么互斥访问数据 

简单粗暴:关中断

在queue.c中:

 这里为什么要用 taskENTER_CRITICAL();来关闭中断以避免冲突呢?FreeRTOS中有这么多任务,如果说,有两个任务想要同时进行写队列,而且前一个任务还没有写完队列就被下一个写队列的任务切换,那么这样子的话,整个RTOS会乱套,所以写队列之前要关闭调度。

 

函数 BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, const BaseType_t xCopyPosition ) 是 FreeRTOS 中用于向队列发送数据的函数。让我们逐个解释其参数和功能:

1.xQueue:
这是一个队列句柄(QueueHandle_t),用于标识将要发送数据的目标队列。队列句柄是FreeRTOS 中管理队列的标识符,用于区分不同的队列实例。
2.pvItemToQueue:

这是一个指向将要发送到队列中的数据项的指针。它是一个 const void * 类型,即指向常量数据的指针,因为该函数并不修改实际的数据内容,只是将数据项发送到队列。

3.xTicksToWait:

这是发送数据时的超时时间,以时钟节拍(TickType_t)为单位。如果队列已满,并且在指定的超时时间内仍未能发送数据到队列,则函数将会阻塞任务或者返回一个错误码(取决于具体的使用方式)。

4.xCopyPosition:
这个参数指示了数据项在发送到队列时的复制策略。它的类型是 BaseType_t,通常用作一个标志或者枚举值,表示数据项的复制方式。具体的取值和含义可以根据实际的 FreeRTOS 配置和使用场景而有所不同。

函数功能概述:

xQueueGenericSend 函数的作用是向指定的队列 xQueue 发送一个数据项 pvItemToQueue。如果队列已满,且指定的 xTicksToWait 时间内无法发送数据项到队列,函数将会根据超时策略进行处理(可能阻塞任务或者返回一个错误码)。发送数据项的方式(复制策略)由 xCopyPosition 参数控制,确保数据项能够安全地发送到队列中,不会被意外修改或损坏。在 FreeRTOS 中,队列的发送操作是一个重要的任务间通信机制,允许一个任务将数据发送给另一个任务,实现任务间的同步和数据传递。

怎么传递数据 

使用环形缓冲区传递数据。

环形缓冲区(ring buffer)也称作循环缓冲区(cyclic buffer)、圆形队列(circular queue)、圆形缓冲区(circular buffer)。环形缓冲区并不是指物理意义上的一个首尾相连成“环”的缓冲区,而是逻辑意义上的一个环,因为内存空间是线性结构,所以实际上环形缓冲区仍是一段有长度的内存空间,是一个先进先出功能的缓冲区,具备实现通信进程对该缓冲区的互斥访问功能。

 

 环形缓冲区实际在内存空间内示意图:

 

环形缓冲区的长度是固定的,在使用该缓冲区时,不需要将所有的数据清除,只需要调整指向该缓冲区的pHead、pValidWrite和pTail指针位置即可。pValidWrite指针最先指向pHead指针位置(环形缓冲区开头位置),数据从pValidWrite指针处开始存储,每存储一个数据,pValidWrite指针位置向后移动一个长度 ,随着数据的添加,pValidWrite指针随移动数据长度大小个位置。当pValidWrite指向pTail尾部指针,pValidWrite重新指向pHead指针位置(折行处理),并且覆盖原先位置数据内容直到数据存储完毕。

实现原理:

一般构建一个环形缓冲区需要一段连续的内存空间以及4个指针:
pHead指针:指向内存空间中的首地址;
pTail指针:指向内存空间的尾地址;
pValidRead:指向内存空间存储数据的起始位置(读指针);
pValidWrite:指向内存空间存储数据的结尾位置(写指针)。

当申请完内存以及指针定义完毕后,环形缓冲区说明及使用如下:

 

1.该段内存空间的长度是Len = pTail-pHead;
2.pValidRead是读数据的起始位置,当读取完N数据之后要移动N个单位长度的偏移,当有addlen长度的数据要存入到环形缓冲区,若addlen + pValidWrite > pTail时,pValidWrite将存入len1 = pTail - pValidWrite个数据长度,然后pValidWrite回到pHead位置,将剩下的len2 = addlen - len1个数据从pHead开始存储并覆盖到原来的数据内容。
3.pValidWrite是写数据的起始位置,当存入N个数据之后要移动N个单位长度的偏移,pValidRead是读数据的起始位置,当读取N个数据之后要移动N个单位长度的偏移。当要addlen长度的数据要从环形缓冲区读取,若addlen + pValidRead > pTail时,pValidRead 将读取len1 = pTail - pValidRead 个数据长度,然后pValidRead 回到pHead位置,将剩下的len2 = addlen - len1个数据从pHead开始读取完毕。

 我们该怎么知道这个buff是空的还是满的呢?还有说还有空位?(我们这里假设指针类型和buff类型是一样的)。

 判断环形buff为空:

pValidRead==pValidWrite。我们在创建环形buff的时候,读指针和写指针所指向的位置是一样的,只要当读指针的值等于写指针的值,那么这个buff为空,所以意味着这个队列读不了,读了会阻塞。

判断环形buff为满:

pValidRead==pValidWrite+1;当下一个写位置要等于读位置的时候,那么这个buff已经满了。所以意味这个队列写不了,写了会阻塞。 

 写入队列就判断buff是不是满的,如果不是满的,直接写buff->pValidWrite+=1;

读出队列就是判断buff是不是空的,如果不是空的,直接读buff-> pValidRead+=1;

写队列 

调用过程:

xQueueSendToBack
    xQueueGenericSend
        /* 如果不成功: */
        vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );
			// 1. 当前任务记录在队列的链表里:  pxQueue->xTasksWaitingToSend
			vListInsert( pxEventList, &( pxCurrentTCB->xEventListItem ) );

			// 2. 把当前任务从ready list放到delayed list
			prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );

 xEventListItem是为了当队列被读之后,有空位,可以通过这个链表来找回这个任务,通知他可以写队列了,来唤醒它。

DelayedList是为了当队列超时之后,通过这个链表来对这个任务进行唤醒。

一个任务写队列时,如果队列已经满了,它会被挂起,何时被唤醒?

超时:

  • 任务写队列不成功时,它会被挂起:从ready list移到delayed list中
  • 在delayed list中,按照"超时时间"排序
  • 系统Tick中断不断发生,在Tick中断里判断delayed list中的任务时间到没?时间到后就唤醒它

别的任务读队列:

         

读队列:

读队列也是一样的,读不到就被阻塞,等待超时或者等待有人写队列来去唤醒它。 

总结:

以上就是FreeRTOS中关于消息队列的知识,我从浅到深讲解了消息队列,不局限于单纯的去调用API函数使用消息队列,要对其内部机制有一定的了解,这才是我们学习者该做到的事情, 

  • 31
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值