目录
3.3.1 xQueueSend()、xQueueSendToBack()、xQueueSendToFront()和xQueueOverwrite()
3.3.2 xQueueGenericSend() - 解读
3.4.1xQueueReceive()和xQueuePeek()
一. 为什么使用消息队列?
在编写项目时,常常会遇到一个任务和另一个任务进行“沟通交流”的情况,在没有操作系统时,全局变量可以解决这个问题,但是如果在使用操作系统的应用中用全部变量来传递信息就会涉及到“资源管理”的问题,而且全局变量数据无保护,导致数据不安全,当多个任务同时对该变量操作时,数据易受损,无法追踪全局变量被谁使用或被谁更改。 FreeRTOS对此提供一个叫做“队列”的机制。
二. 认识消息队列
2.1 消息队列的简介
队列是为了任务与任务、任务与中断之间的通信而准备的,可以在任务与任务、任务与中断之间传递消息,队列中可以存储有限的、大小固定的数据项目。任务与任务、任务与中断之间要交流的数据保存在队列中,叫做队列项目。队列所能保存的最大数据项目数量叫做队列的长度,创建队列的时候会指定数据项目的大小和队列的长度。由于队列用来传递消息的,所以也称为消息队列。FreeRTOS 中的信号量的也是依据队列实现的!
2.2 消息队列的特点
2.2.1 数据入队出队方式
先进先出(FIFO):
往队列发送数据(入队)是发送到队列的尾部的。
从队列提取数据(出队)是从队列的头部提取的。
后进先出(LIFO)
2.2.2 数据传递方式
值传递:
传递的是数据
引用传递:
传递的是消息指针
注意
引用传递的优点
可以大大减小数据传送到队列的时间。
引用传递的缺点
采用引用传递的消息就必须保持可见性,也就是消息内容必须有效,如此的话,例如函数的局部变量就存在会被随时删除的情况
值传递的优点在数据发送到队列后,原先存储数据的缓冲区可以被删除或者覆写,这样的话缓冲区就可以一直被重复使用。
值传递的缺点
不过对于网络信息传递的情况往往需要传送大量的信息,那么势必会消耗很多时间
2.2.3 多任务访问
队列不属于某个任务,任何任务和中断都可以向队列发送/读取消息
2.2.4 出队、入队阻塞
出队阻塞:
当任务从一个队列中读取消息的时候可以指定一个阻塞时间,这个阻塞时间就是当任务从队列中读取消息无效的时候任务阻塞的时间,这个阻塞时间单位是时钟节拍数。
比如 任务 A 从 队列B 中读取数据,如果此时 队列B 是空的,说明没有数据,任务A 这时候来读取的话肯定是获取不到任何东西,
任务A有三种选择:
1.不读数据直接结束这个读取的过程;
2.等待一段时间,也就是所谓的阻塞时间,在这段时间内读取到队列的数据就结束,反之,则等待阻塞时间到了之后就从延时列表进入就绪列表;
3.设置等待时间的为最大值portMAX_DELAY,也就是如果没有读取到数据就一直进入阻塞态等待,直到接收到数据为止。当一个任务向队列发送消息的话也可以设置阻塞时间。
比如任务C向消息队列D发送消息,但是此时队列D是满的,那肯定是发送失败的。此时任务C就会遇到和上面任务A一样的问题,这两种情况的处理过程是类似的。
疑问
当多个任务写入消息给一个“满队列”时,这些任务都会进入阻塞状态,也就是说有多个任务在等待同一 个队列的空间。那当队列中有空间时,哪个任务会进入就绪态?
答:
1.优先级最高的任务
2.如果大家的优先级相同,那等待时间最久的任务会进入就绪态
三.消息队列的相关函数
3.1队列结构体-解释
typedef struct QueueDefinition
{
int8_t *pcHead; /*指向队列存储区的开始地址*/
int8_t *pcTail; /*指向队列存储区的结束地址*/
int8_t *pcWriteTo; /*指向存储区中下一个空闲区域*/
union
{
int8_t *pcReadFrom;
UBaseType_t uxRecursiveCallCount;
} u;
//等待发送任务列表,那些因为队列满导致入队失败而进入阻塞态的任务就会挂到此列表上。
List_t xTasksWaitingToSend;
//等待接收任务列表,那些因为队列空导致出队失败而进入阻塞态的任务就会挂到此列表上。
List_t xTasksWaitingToReceive;
//队列中当前队列项数量,也就是消息数
volatile UBaseType_t uxMessagesWaiting;
//创建队列时指定的队列长度,也就是队列中最大允许的队列项(消息)数量
UBaseType_t uxLength;
//创建队列时指定的每个队列项(消息)最大长度,单位:字节
UBaseType_t uxItemSize;
//当队列上锁以后用来统计从队列中接收到的队列项数量,也就是出队的队列项数量,当队列没有上锁的话此字段为 queueUNLOCKED
volatile int8_t cRxLock;
//当队列上锁以后用来统计发送到队列中的队列项数量,也就是入队的队列项数量,当队列没有上锁的话此字段为 queueUNLOCKED
volatile int8_t cTxLock;
/*以下代码省略*/
} xQUEUE;
3.2 队列创建
函数 | 描述 |
xQueueCreate() | 动态方式创建队列 |
xQueueCreateStatic() | 静态方式创建队列 |
区别:
队列所需的内存空间由 FreeRTOS 从 FreeRTOS 管理的堆中分配,而静态创建需要用户自行分配内存。
3.2.1动态方式创建队列
#define xQueueCreate( uxQueueLength, uxItemSize )
xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
QueueHandle_t xQueueGenericCreate(
/*要创建队列的长度*/ const UBaseType_t uxQueueLength,
/*队列中每个消息长度的大小*/ const UBaseType_t uxItemSize,
/*队列的类型,默认为消息队列*/ const uint8_t ucQueueType
)
队列类型 | |
queueQUEUE_TYPE_BASE | 队列 |
queueQUEUE_TYPE_SET | 队列集 |
queueQUEUE_TYPE_MUTEX | 互斥信号量 |
queueQUEUE_TYPE_COUNTING_SEMAPHORE | 计数型信号量 |
queueQUEUE_TYPE_BINARY_SEMAPHORE | 二值信号量 |
queueQUEUE_TYPE_RECURSIVE_MUTEX | 递归互斥信号量 |
3.3往队列发送消息
函数 | 描述 |
xQueueSend() | 往队列的尾部写入消息 |
xQueueSendToBack() | 同 xQueueSend() |
xQueueSendToFront() | 往队列的头部写入消息 |
xQueueOverwrite() | 覆写队列消息(只用于队列长度为 1 的情况) |
xQueueSendFromISR() | 在中断中往队列的尾部写入消息 |
xQueueSendToBackFromISR() | 同 xQueueSendFromISR() |
xQueueSendToFrontFromISR() | 在中断中往队列的头部写入消息 |
xQueueOverwriteFromISR() | 在中断中覆写队列消息(只用于队列长度为 1 的情况) |
3.3.1 xQueueSend()、xQueueSendToBack()、xQueueSendToFront()和xQueueOverwrite()
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait )
xQueueGenericSend( ( xQueue ),
( pvItemToQueue ),
( xTicksToWait ),
( queueSEND_TO_BACK )
)
#define xQueueSendToBack ( xQueue, pvItemToQueue, xTicksToWait )
xQueueGenericSend( ( xQueue ),
( pvItemToQueue ),
( xTicksToWait ),
( queueSEND_TO_BACK )
)
#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait )
xQueueGenericSend( ( xQueue ),
( pvItemToQueue ),
( xTicksToWait ),
( queueSEND_TO_FRONT )
)
#define xQueueOverwrite( xQueue, pvItemToQueue )
xQueueGenericSend( ( xQueue ),
( pvItemToQueue ),
( 0 ),
( queueOVERWRITE )
)
这四个函数都是用于向队列中发送消息的,这三个函数本质都是宏。
这四个函数最后都是调用的同一个函数:xQueueGenericSend()
参数 描述 xQueue 队列句柄,指明要向哪个队列发送数据 pvItemToQueue 指向要发送的消息,发送时候会将这个消息拷贝到队列中 xTicksToWait 阻塞时间,此参数指示当队列满的时候任务进入阻塞态等待队列空闲的最大时间
3.3.2 xQueueGenericSend() - 解读
BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, const BaseType_t xCopyPosition )
{
BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;
TimeOut_t xTimeOut;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
configASSERT( pxQueue );
configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );
#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
{
configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );
}
#endif
for( ;; )
{
/*进入临界区*/
taskENTER_CRITICAL();
{
/*
查询队列现在是否还有剩余存储空间,如果采用覆写方式入队的话
那就不用在乎队列是不是满的啦。
*/
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )//(1)
{
traceQUEUE_SEND( pxQueue );
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );//(2)
#if ( configUSE_QUEUE_SETS == 1 )
{
/*省略此代码*/
}
#else
{
/*检查是否有任务由于等待消息而进入阻塞态 */
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )//(3)
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )//(4)
{
/*解除阻塞态的任务优先级最高,因此要进行一次任务切换*/
queueYIELD_IF_USING_PREEMPTION();//(5)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else if( xYieldRequired != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
taskEXIT_CRITICAL();
return pdPASS;//(6)
}
else
{
if( xTicksToWait == ( TickType_t ) 0 )//(7)
{
/*队列是满的,并且没有设置阻塞时间的话就直接返回*/
taskEXIT_CRITICAL();
traceQUEUE_SEND_FAILED( pxQueue );
return errQUEUE_FULL;//(8)
}
else if( xEntryTimeSet == pdFALSE )//(9)
{
/*队列是满的并且指定了任务阻塞时间的话就初始化时间结构体*/
vTaskSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
else
{
/*时间结构体已经初始化过了*/
mtCOVERAGE_TEST_MARKER();
}
}
}
/*退出临界区*/
taskEXIT_CRITICAL();
vTaskSuspendAll();//(10)
prvLockQueue( pxQueue );//(11)
/*更新时间壮态,检查是否有超时产生*/
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )//(12)
{
if( prvIsQueueFull( pxQueue ) != pdFALSE )//(13)
{
traceBLOCKING_ON_QUEUE_SEND( pxQueue );
vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );//(14)
prvUnlockQueue( pxQueue );//(15)
if( xTaskResumeAll() == pdFALSE )//(16)
{
portYIELD_WITHIN_API();
}
}
else
{
/*重试一次*/
prvUnlockQueue( pxQueue );//(17)
( void ) xTaskResumeAll();
}
}
else
{
/*超时产生*/
prvUnlockQueue( pxQueue );//(18)
( void ) xTaskResumeAll();
traceQUEUE_SEND_FAILED( pxQueue );
return errQUEUE_FULL;//(19)
}
}
}
(1) 向队列发送数据,先检查一下队列是不是满的,如果是满的话就不能发送的。
当队列未满或者是覆写入队的话就可以将消息入队了。
(2) 调用函数 prvCopyDataToQueue()将消息拷贝到队列中。
入队方式分为后向入队、前向入队和覆写入队,他们的具体实现就是在函数 prvCopyDataToQueue()中完成的。
如果选择后向入队 queueSEND_TO_BACK 的话就将消息拷贝到队列结构体成员 pcWriteTo所指向的队列项,拷贝成功以后 pcWriteTo 增加 uxItemSize 个字节,指向下一个队列项目。
当选择前向入队 queueSEND_TO_FRONT 或者 queueOVERWRITE 的话就将消息拷贝到 u.pcReadFrom 所指向的队列项目,同样的需要调整 u.pcReadFrom 的位置。
当向队列写入一个消息以后队列中统计当前消息数量的成员uxMessagesWaiting 就会加一,但是选择覆写入队 queueOVERWRITE 的话还会将 uxMessagesWaiting 减一。
(3) 检查是否有任务由于请求队列消息而阻塞,阻塞的任务会挂在队列的xTasksWaitingToReceive列表上。
(4) 任务由于请求消息而阻塞,因为在(2)中已将向队列中发送了一条消息了,所以调用函数 xTaskRemoveFromEventList()将阻塞的任务从列表 xTasksWaitingToReceive 上移除,并且把这个任务添加到就绪列表中。如果调度器上锁的话,这些任务就会挂到列表 xPendingReadyList 上。如果取消阻塞的任务优先级比当前正在运行的任务优先级高还要标记需要进行任务切换。当函数 xTaskRemoveFromEventList()返回值为 pdTRUE 的话就需要进行任务切换。
(5) 进行任务切换。
(6) 返回 pdPASS,标记入队成功
(7) (2)到(6)都是非常理想的效果,即消息队列未满,入队没有任何障碍。但是队列满了以后呢?首先判断设置的阻塞时间是否为 0,如果为 0 的话就说明没有阻塞时间。
(8) 由(7)得知阻塞时间为 0,那就直接返回 errQUEUE_FULL,标记队列已满就可以了。
(9) 如果阻塞时间不为 0 并且时间结构体还没有初始化的话就初始化一次超时结构体变量,调用函数 vTaskSetTimeOutState()完成超时结构体变量 xTimeOut 的初始化。其实就是记录当前的系统时钟节拍计数器的值 xTickCount 和溢出次数 xNumOfOverflows。
(10) 任务调度器上锁,代码执行到这里说明当前的状况是队列已满了,而且设置了不为 0的阻塞时间。那么接下来就要对任务采取相应的措施了,比如将任务加入到队列的
xTasksWaitingToSend 列表中。
(11) 调用函数 prvLockQueue()给队列上锁,其实就是将队列中的成员变量 cRxLock 和
cTxLock 设置为 queueLOCKED_UNMODIFIED。
(12) 调用函数 xTaskCheckForTimeOut()更新超时结构体变量 xTimeOut,并且检查阻塞时间是否到了。
(13) 阻塞时间还没到,那就检查队列是否还是满的。
(14) 经过(12)和(13)得出阻塞时间没到,而且队列依旧是满的,那就调用函数vTaskPlaceOnEventList()将任务添加到队列的 xTasksWaitingToSend 列表中和延时列表中,并且将任务从就绪列表中移除 。 若阻塞时间为portMAX_DELAY 并且宏INCLUDE_vTaskSuspend为1的话,函数vTaskPlaceOnEventList()会将任务添加到列表xSuspendedTaskList 上。
(15) 操作完成,调用函数 prvUnlockQueue()解锁队列。
(16) 调用函数 xTaskResumeAll()恢复任务调度器
(17) 阻塞时间还没到,但是队列现在有空闲的队列项,那么就重新执行(15)和(16)。
(18) 相比于第(12)步,阻塞时间到了!那么任务就不用添加到那些列表中了,那就解锁队列,恢复任务调度器。
(19) 返回 errQUEUE_FULL,表示队列满了。
3.4从队列读取消息
函数 | 描述 |
xQueueReceive() | 从队列头部读取消息,并删除消息 |
xQueuePeek() | 从队列头部读取消息 |
xQueueReceiveFromISR() | 在中断中从队列头部读取消息,并删除消息 |
xQueuePeekFromISR() | 在中断中从队列头部读取消息 |
3.4.1xQueueReceive()和xQueuePeek()
#define xQueueReceive( xQueue, pvBuffer, xTicksToWait )
xQueueGenericReceive( ( xQueue ),
( pvBuffer ),
( xTicksToWait ),
( pdFALSE )
)
#define xQueuePeek( xQueue, pvBuffer, xTicksToWait )
xQueueGenericReceive( ( xQueue ),
( pvBuffer ),
( xTicksToWait ),
( pdTRUE )
)
这两个函数都是用于从队列中读取消息的,这两个函数本质都是宏。
这两个函数最后都是调用的同一个函数:xQueueGenericReceive()
参数 描述 xQueue 队列句柄,指明要读取哪个队列的数据 pvBuffer 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中 xTicksToWait 阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最大时间