“队列”(Queue)提供了任务与任务之间通信的机制。在这样的场景:一个或多个其他的任务产生数据,主任务要依次处理数据,队列就显得非常有用了。
参考资料:《Mastering the FreeRTOS Real Time Kernel》-Chapter 4 Queue Management
FreeRTOS全解析-5.队列(Queue)
目录
1.队列的特征
1.1数据存储
1.2读取队列时阻塞
1.3写入队列时阻塞
1.4在多个队列上阻塞
2.使用队列
2.1创建队列
2.2往队列发送数据
2.3从队列接收数据
2.4查询队列中的项数
2.5例子
3.任务接收不同数据
4.当数据非常大,或者大小不定时
4.1用队列传输大数据
4.2用队列传输大小不定数据
5.从多个队列中接收数据
1.队列的特征
1.1数据存储
队列可以容纳有限数量的固定大小的数据项。一个队列可以容纳的最大项目数称为它的“长度”。每个数据项的长度和大小都在创建队列时设置。
队列通常用作先进先出(FIFO)缓冲区,其中数据被写入队列的末尾(尾部),并从队列的前端(头部)删除。
如图,创建一个可以包含五个整数的队列,刚开始是空的。
TaskA往队列尾部写入数据,因为队列是空的,尾部其实也就是头部。
TaskA继续往队列尾部写入数据,这次的数据是20,排在第一个数据10后面。
TaskB从队列头部读取数据,也就是读到了Task第一次写入的数:10.
TaskB把10取走后,第二个数据20就变成了当前队列里的第一个数据。
有两种方式可以实现队列:
1.按复制,复制队列是指将发送到队列的数据复制一份到队列中。
2. 按引用,引用队列意味着队列中只保存指向发送到队列的数据的指针,而不是数据本身。
FreeRTOS使用复制队列的方法。通过复制排队比通过引用排队更强大,使用起来更简单,因为:
1.栈中的变量可以直接发送到队列,即使栈变量会在声明它的函数退出后将不存在。
2.不用预先为数据分配空间。
3.发送任务发送完数据后可以立即重用这个变量。
4.发送任务和接收任务是完全分离的——应用程序设计人员不需要关心哪个任务“拥有”数据,或者哪个任务负责发布数据。
5.复制队列可以同时使用引用功能。例如,当队列中的数据的大小使得将数据复制到队列中不切实际时,则可以将指向数据的指针复制到队列中。
6.RTOS完全负责分配用于存储数据的内存。
7.在内存保护的系统中,任务可以访问的RAM将受到限制。在这种情况下,只有当发送和接收任务都可以访问存储数据的RAM时,才可以使用引用队列。复制队列没有这种限制;内核总是以完全权限运行,允许使用队列跨内存保护边界传递数据。
1.2读取队列时阻塞
当一个任务试图从队列中读取数据时,它可以指定一个“阻塞”时间。
队列为空的时候无法读取,这个时候就会阻塞,当另一个任务或中断将数据放入队列中时,处于阻塞态的任务将自动移动到就绪态。如果指定的阻塞时间到了,还是没有数据,任务也将自动移到就绪态。
可能存在多个任务读取队列,因此单个队列上可能阻塞了多个等待数据的任务。在这种情况下,当有数据时,只有一个任务将被解除阻塞。被解除阻塞的任务将始终是等待数据的任务中优先级最高的那个任务。如果阻塞的任务具有相同的优先级,则等待数据时间最长的任务将被解除阻塞。
1.3写入队列时阻塞
就像从队列中读取一样,任务在写入队列时可以指定阻塞时间。在这种情况下,发生阻塞是因为队列是满的,没有可以写入的空间。
可能有多个任务试图写入队列,因此一个队列上可能阻塞了多个等待完成发送(写入)操作的任务。在这种情况下,当队列有空间时,只有一个任务将被解除阻塞。被解除阻塞的任务将始终是等待空间的任务中最高优先级的任务。如果阻塞的任务具有相同的优先级,则等待空间时间最长的任务将被解除阻塞。
1.4在多个队列上阻塞
可以将多个队列编组成集合,允许任务进入阻塞状态以等待在集合中的任何一个队列可以使用,这个后面讲。
2.使用队列
2.1创建队列
在使用队列之前,必须用xQueueCreate() 函数创建一个队列并返回一个QueueHandle_t。这个类型的变量被称作队列句柄,用于使用队列。
FreeRTOS V9.0.0还包括xQueueCreateStatic()函数,它在编译时静态分配创建队列所需的内存:FreeRTOS在创建队列时从FreeRTOS堆中分配RAM。RAM用于保存队列数据结构和队列中包含的项。如果没有足够的堆RAM供创建的队列使用,xQueueCreate()将返回NULL。
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,
UBaseType_t uxitemsize);
参数 | 作用 |
uxQueueLength | 创建的队列可以容纳的最大项数。 |
uxitemsize | 队列中的单个数据项的字节大小。 |
返回值 | 如果返回NULL,则不能创建队列,因为没有足够的堆内存供FreeRTOS分配队列数据结构和存储区域。返回非NULL值表示队列已成功创建。返回值应该存储为所创建队列的句柄。 |
创建队列后,可以使用xQueueReset() API函数将队列返回到原始的空状态。
2.2往队列发送数据
xQueueSendToBack()用于将数据发送到队列的后面(尾部),xQueueSendToFront()用于将数据发送到队列的前面(头部)。还有个函数xQueueSend()等价于xQueueSendToBack(),他们完全一样。
注意:不要在中断服务程序中调用xQueueSendToFront()或xQueueSendToBack()。在中断中使用时用xQueueSendToFrontFromISR()和xQueueSendToBackFromISR()来代替它们。在讲中断时细讲。
BaseType_t xQueueSendToFront( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait );
BaseType_t xQueueSendToBack( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait );
参数 | 作用 |
xQueue | 数据被发送(写入)到的队列的句柄。 |
pvItemToQueue | 指向要复制到队列中的数据的指针。 |
xTicksToWait | 如果队列已满,任务进入阻塞态以等待队列有空间的时间(等多久)。 (阻塞时间以Tick周期指定,因此它表示的绝对时间依赖于Tick频率。宏pdMS_TO_TICKS()可用于将以毫秒为单位指定的时间转换为以tick为单位指定的时间。) 如果xTicksToWait为0且队列已满,xQueueSendToFront()和xQueueSendToBack()都将立即返回。 设置xTicksToWait为宏portMAX_DELAY将导致任务无限期地等待(没有超时),前提是在FreeRTOSConfig.h中将INCLUDE_vTaskSuspend设置为1。 |
返回值 | 有两个可能的返回值: 1.pdPASS 只有当数据成功发送到队列时才返回pdPASS。 2. errQUEUE_FULL 如果由于队列已满而无法将数据写入队列,则返回errQUEUE_FULL。 |
2.3从队列接收数据
xQueueReceive()用于从队列中接收(读取)一个项。队列里的项(数据)被任务给接收走后,队列就会把这个项(数据)删除。
注意:不要在中断服务程序中调用xQueueReceive()。中断安全的xQueueReceiveFromISR() 函数在讲中断时细讲。
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
参数 | 作用 |
xQueue | 接收(读取)数据的队列句柄。 |
pvBuffer | 指向接收到的数据将被复制到其中的内存的指针。就是你要复制到哪里去。 |
xTicksToWait | 队列为空时,任务阻塞以等待队列中有数据的时间。等它有数据,等多久。 (阻塞时间以Tick周期指定,因此它表示的绝对时间依赖于Tick频率。宏pdMS_TO_TICKS()可用于将以毫秒为单位指定的时间转换为以tick为单位指定的时间。) 如果xTicksToWait为零,那么如果队列已经为空,xQueueReceive()将立即返回。 将xTicksToWait设置为portMAX_DELAY将导致任务无限期地等待(没有超时)。(要在FreeRTOSConfig.h中将INCLUDE_vTaskSuspend设置为1) |
返回值 | 有两个可能的返回值: 1.pdPASS 只有成功从队列中读取数据时才返回pdPASS。 2. errQUEUE_EMPTY 如果由于队列已经为空而无法从队列中读取数据,则返回errQUEUE_EMPTY。 |
2.4查询队列中的项数
uxQueueMessagesWaiting()用于查询当前在队列中的项的数量。
注意:永远不要从中断服务例程中调用uxQueueMessagesWaiting()。应该使用中断安全的uxQueueMessagesWaitingFromISR()来代替它。
UBaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue);
2.5例子
static void vSenderTask( void *pvParameters )
{
int32_t lValueToSend;
BaseType_t xStatus;
lValueToSend = ( int32_t ) pvParameters;
for( ;; ) {
xStatus = xQueueSendToBack( xQueue, &lValueToSend, 0 );
if( xStatus != pdPASS ) {
vPrintString( "Could not send to the queue.\r\n" );
}
}
}
static void vReceiverTask( void *pvParameters )
{
int32_t lReceivedValue;
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 );
for( ;; ) {
if( uxQueueMessagesWaiting( xQueue ) != 0 ) {
vPrintString( "Queue should have been empty!\r\n" );
}
xStatus = xQueueReceive( xQueue, &lReceivedValue, xTicksToWait );
if( xStatus == pdPASS ) {
vPrintStringAndNumber( "Received = ", lReceivedValue );
} else {
vPrintString( "Could not receive from the queue.\r\n" );
}
}
}
QueueHandle_t xQueue;
int main( void )
{
xQueue = xQueueCreate( 5, sizeof( int32_t ) );
if( xQueue != NULL ) {
xTaskCreate( vSenderTask, "Sender1", 1000, ( void * ) 100, 1, NULL );
xTaskCreate( vSenderTask, "Sender2", 1000, ( void * ) 200, 1, NULL );
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );
vTaskStartScheduler();
} else {
}
for( ;; );
}
这个例子创建了3个任务Sender1:一直往队列发送100,Sender2:一直往队列发送200,Receiver从队列接收数据。
由于接收任务优先级高,所以一旦队列中有数据,接收任务就会启动,所以队列里最多只有一个数据。两个发送任务优先级是一样的,所以会轮着发送。结果是这样的:
3.任务接收不同数据
在FreeRTOS设计中,一个任务从多个源接收数据是很常见的。接收任务需要知道数据来自哪里,以确定数据应该如何处理。
如上图,把数据打包成数据结构Data_t。结构成员有两个,一个为枚举类型表示不同的数据来源,另一个为数据值。
CAN总线任务发送到队列里的Data_t型数据中的eDataID为eMotorSpeed表示这是电机转速。
HMI任务发送到队列里的Data_t型数据中的eDataID为eSpeedSetPiont表示这是新设定的点的值。
像这样封装出数据结构,就可以给你的主任务足够的信息,剩下的就是在主任务中编写分辨功能了。
例子:
typedef enum
{
eSender1,
eSender2
} DataSource_t;
typedef struct
{
uint8_t ucValue;
DataSource_t eDataSource;
} Data_t;
static const Data_t xStructsToSend[ 2 ] =
{
{ 100, eSender1 }, /* Used by Sender1. */
{ 200, eSender2 } /* Used by Sender2. */
};
static void vSenderTask( void *pvParameters )
{
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 );
for( ;; ) {
xStatus = xQueueSendToBack( xQueue, pvParameters, xTicksToWait );
if( xStatus != pdPASS ) {
vPrintString( "Could not send to the queue.\r\n" );
}
}
}
static void vReceiverTask( void *pvParameters )
{
Data_t xReceivedStructure;
BaseType_t xStatus;
for( ;; ) {
if( uxQueueMessagesWaiting( xQueue ) != 3 )
vPrintString( "Queue should have been full!\r\n" );
xStatus = xQueueReceive( xQueue, &xReceivedStructure, 0 );
if( xStatus == pdPASS ) {
if( xReceivedStructure.eDataSource == eSender1 ) {
vPrintStringAndNumber( "From Sender 1 = ", xReceivedStructure.ucValue );
} else {
vPrintStringAndNumber( "From Sender 2 = ", xReceivedStructure.ucValue );
}
} else {
vPrintString( "Could not receive from the queue.\r\n" );
}
}
}
int main( void )
{
xQueue = xQueueCreate( 3, sizeof( Data_t ) );
if( xQueue != NULL ) {
xTaskCreate( vSenderTask, "Sender1", 1000, &( xStructsToSend[ 0 ] ), 2, NULL );
xTaskCreate( vSenderTask, "Sender2", 1000, &( xStructsToSend[ 1 ] ), 2, NULL );
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
vTaskStartScheduler();
} else {
}
for( ;; );
}
主函数创建了元素个数为3的队列,元素的类型为Data_t,定义了两个发送任务,发送数据不同,发送的数据中标识了不同的源。
这个例子和前面的例子还有一点不同:发送任务的优先级高于接收任务,也就是说一旦可以发送,就会抢占接收任务,因此队列不会存在空位,总是满的。
为什么Sender1接连发送了4个数据?
因为在这个例子里,任务很简单,数据发送特别快,在一个tick内就可以发好几次,队列没有满,tick时间没有到,就不会发生任务切换。
Sender1一直发直到队列满,Sender1阻塞,Sender2尝试发送,但是队列已经满,Sender2也进入阻塞。直到接收任务Receiver接收走一个数据,Sender1和2才有机会解除阻塞,但是Sender1和2优先级相同,就看谁阻塞久,就先启动谁,所以又启动Sender1,这就导致了接连有4个Sender1。
Receiver又接收走一个数据,这个时候就是Sender2阻塞更久了,所以Sender2就开始发送,如此往复。
4.当数据非常大,或者大小不定时
4.1用队列传输大数据
如果存储在队列中的数据很大,那么最好使用队列来传输指向数据的指针,而不是逐个字节地将数据本身复制到队列中或复制出队列。传输指针在处理时间和创建队列所需的RAM量方面更加高效。但是在对指针进行排队时,必须非常小心,以确保:
1. 指针指向的RAM空间的所有者必需清晰
当通过指针在两个任务之间共享内存时,必须确保两者不会同时修改内存内容,也不会采取可能导致内存内容无效或不一致的任何其他操作。理想情况下,在指针进入队列前,只有发送任务被允许访问指针指向的内存。在指针从队列出去后,只有接收任务可以访问指针指向的内存
2. 指针所指向的RAM要保持有效。
如果所指向的内存是动态分配的,或者是从内存池中获得的的预分配缓冲区,则应该有一个任务负责释放内存。在释放内存之后,任何任务都不应该尝试访问内存。
指针绝不能用来访问已分配到任务栈上的数据。在栈帧改变后,数据将无效。
例子:
QueueHandle_t xPointerQueue;
xPointerQueue = xQueueCreate( 5, sizeof( char * ) );
void vStringSendingTask( void *pvParameters )
{
char *pcStringToSend;
const size_t xMaxStringLength = 50;
BaseType_t xStringNumber = 0;
for( ;; ) {
pcStringToSend = ( char * ) prvGetBuffer( xMaxStringLength );
snprintf( pcStringToSend, xMaxStringLength, "String number %d\r\n", xStringNumber );
xStringNumber++;
xQueueSend( xPointerQueue,&pcStringToSend, portMAX_DELAY );
}
}
void vStringReceivingTask( void *pvParameters )
{
char *pcReceivedString;
for( ;; ) {
xQueueReceive( xPointerQueue, &pcReceivedString,portMAX_DELAY );
vPrintString( pcReceivedString );
prvReleaseBuffer( pcReceivedString );
}
}
创建了一个最多可以容纳5个指针的队列。
在发送任务vStringSendingTask中分配一个缓冲区,向缓冲区写入一个字符串,然后向队列发送一个指向缓冲区的指针。
在接收任务vStringReceivingTask中从队列接收一个指向缓冲区的指针,然后打印缓冲区中包含的字符串,打印完就释放缓冲区。
4.2用队列传输大小不定数据
前面的部分演示了两种设计模式:向队列发送结构,和向队列发送指针。
把这两个结合起来,任务可以使用单个队列从任何数据源接收任何数据类型。
FreeRTOS+TCP TCP/IP栈的实现就是这样的一个真实例子:
TCP/IP栈在自己的任务中运行,必须处理来自许多不同源的事件。不同的事件类型与不同类型和长度的数据相关联。所有发生在TCP/IP任务之外的事件都由IPStackEvent_t类型的结构描述,并以队列的形式发送给TCP/IP任务。
IPStackEvent_t结构体的pvData成员是一个指针,可以用来直接保存一个值,也可以指向一个缓冲区。
typedef enum
{
eNetworkDownEvent = 0,
eNetworkRxEvent,
eTCPAcceptEvent,
} eIPEvent_t;
typedef struct IP_TASK_COMMANDS
{
eIPEvent_t eEventType;
void *pvData;
} IPStackEvent_t;
上面代码中的三种eIPEvent_t为:
eNetworkRxEvent:从网络收到一个数据包。从网络接收到的数据以结构体IPStackEvent_t的形式发送到TCP/IP任务。IPStackEvent_t的eEventType成员被设置为eNetworkRxEvent,IPStackEvent_t的pvData成员用于指向包含接收数据的缓冲区。伪代码如下:
void vSendRxDataToTheTCPTask( NetworkBufferDescriptor_t *pxRxedData )
{
IPStackEvent_t xEventStruct;
xEventStruct.eEventType = eNetworkRxEvent;
xEventStruct.pvData = ( void * ) pxRxedData;
xSendEventStructToIPTask( &xEventStruct );
}
eTCPAcceptEvent:一个套接字正接受或等待来自客户端的连接。任务以结构体IPStackEvent_t的形式发送接收事件到TCP/IP任务。IPStackEvent_t的eEventType成员被设置为eTCPAcceptEvent,IPStackEvent_t的pvData成员被设置为接受连接的套接字的句柄。伪代码如下:
void vSendAcceptRequestToTheTCPTask( Socket_t xSocket )
{
IPStackEvent_t xEventStruct;
xEventStruct.eEventType = eTCPAcceptEvent;
xEventStruct.pvData = ( void * ) xSocket;
xSendEventStructToIPTask( &xEventStruct );
}
eNetworkDownEvent:网络需要连接或重新连接也就是断网事件。网络接口以结构体IPStackEvent_t的形式发送断网事件到TCP/IP任务。IPStackEvent_t的eEventType成员被设置为eNetworkDownEvent。断网事件与任何数据都没有关联,因此该结构的pvData成员没有被使用。伪代码如下;
void vSendNetworkDownEventToTheTCPTask( Socket_t xSocket )
{
IPStackEvent_t xEventStruct;
xEventStruct.eEventType = eNetworkDownEvent;
xEventStruct.pvData = NULL;
xSendEventStructToIPTask( &xEventStruct );
}
处理事件代码如下:
IPStackEvent_t xReceivedEvent;
xReceivedEvent.eEventType = eNoEvent;
xQueueReceive( xNetworkEventQueue, &xReceivedEvent, xNextIPSleep );
switch( xReceivedEvent.eEventType )
{
case eNetworkDownEvent :
prvProcessNetworkDownEvent();
break;
case eNetworkRxEvent:
prvHandleEthernetPacket( ( NetworkBufferDescriptor_t * )( xReceivedEvent.pvData ) );
break;
case eTCPAcceptEvent:
xSocket = ( FreeRTOS_Socket_t * ) ( xReceivedEvent.pvData );
xTCPCheckNewClient( pxSocket );
break;
}
根据IPStackEvent_t的eEventType成员对IPStackEvent_t的pvData成员进行不同的处理。
5.从多个队列中接收数据
上一节说明了如何用一个队列来接收不同大小、不同含义以及来自不同来源的数据。但是有时候受到一些因素限制,必须为某些数据源使用单独的队列。例如,集成到设计中的第三方代码中可能存在专用队列。在这种情况下,可以使用“队列集”。
使用队列集,不如使用结构体的单个队列实现相同功能的设计更简洁、效率更低。因此,建议仅在设计限制,必须使用时才使用队列集。
队列集顾名思义就是把几个队列打包成一个集合。
当队列集中的某个队列接收到数据时,接收到数据的队列的句柄会被发送到队列集。当有一个任务读取队列集时,这个队列句柄就会被返回给这个任务。
显然队列句柄承担了上文中的结构体中枚举变量的标识作用。
使用队列集步骤:
1创建队列集。启用队列集功能要将FreeRTOSConfig.h中的configUSE_QUEUE_SETS设置为1。
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength );
2. 向队列集中添加队列。信号量也可以添加到队列集。信号量以后讲。
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet );
3.从队列集中读取数据,以确定集合中的哪些队列包含数据。
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,
const TickType_t xTicksToWait );
根据返回的句柄判断是来自哪个队列的数据,就可以实现对不同数据的不同处理了。
队列集基本上用不到,这几个函数就不详解了,看参数类型也知道怎么用。
往期精彩:
FreeRTOS全解析-4.调度器的三种调度算法
FreeRTOS全解析-3.任务(task)
STM32F4移植FreeRTOS
嵌入式C语言几个重点(const、static、voliatile、位运算)文章来源:https://www.toymoban.com/news/detail-778426.html
从Linux内核中学习高级C语言宏技巧文章来源地址https://www.toymoban.com/news/detail-778426.html
到了这里,关于彻底理解FreeRTOS中的队列(Queue)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!