队列简介
更详细的操作入下图所示:
传输数据的方法
FreeRTOS中的队列传输使用的是拷贝:把数据、把变量的值复制进队列里
队列函数
创建
静态分配:
复位
/* pxQueue : 复位哪个队列;
删除
写队列
/* 等同于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);
+
读队列
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 );
覆盖/偷看
/* 覆盖队列
* 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);
/* 偷看队列
* 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!!!)
队列进阶使用(队列集)
创建队列集
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;
写队列
调用过程:
xQueueSendToBackxQueueGenericSend/* 如果不成功: */vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );// 1. 当前任务记录在队列的链表里: pxQueue->xTasksWaitingToSendvListInsert( pxEventList, &( pxCurrentTCB->xEventListItem ) );// 2. 把当前任务从ready list放到delayed listprvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
xEventListItem是为了当队列被读之后,有空位,可以通过这个链表来找回这个任务,通知他可以写队列了,来唤醒它。
DelayedList是为了当队列超时之后,通过这个链表来对这个任务进行唤醒。
一个任务写队列时,如果队列已经满了,它会被挂起,何时被唤醒?
超时:
- 任务写队列不成功时,它会被挂起:从ready list移到delayed list中
- 在delayed list中,按照"超时时间"排序
- 系统Tick中断不断发生,在Tick中断里判断delayed list中的任务时间到没?时间到后就唤醒它
别的任务读队列:
读队列:
读队列也是一样的,读不到就被阻塞,等待超时或者等待有人写队列来去唤醒它。
总结:
以上就是FreeRTOS中关于消息队列的知识,我从浅到深讲解了消息队列,不局限于单纯的去调用API函数使用消息队列,要对其内部机制有一定的了解,这才是我们学习者该做到的事情,