队列提供了任务到任务、任务到中断和中断到任务的通信机制。
数据存储
队列可以保存有限数量的固定大小的数据项。一个队列所能容纳的最大条目数称为它的长度。每个数据项的长度和大小都在创建队列时设置。
队列通常用作先进先出(FIFO)缓冲区,其中数据被写入队列的末端,并从队列的头部删除。
图31展示了向用作FIFO的队列写入和读取数据的过程。
有两种方式可以实现队列行为:
**FreeRTOS使用复制队列的方法,**通过复制队列比引用队列更强大和更简单,因为:
多任务访问
队列本身就是对象,任何知道它们存在的任务或ISR都可以访问它们。任意数量的任务都可以写同一个队列,也可以从同一个队列读任意数量的任务。
队列读取阻塞
当任务试图从队列中读取时,它可以选择指定一个“阻塞”时间。如果队列已经为空,则该任务保持在Blocked状态,以等待从队列中获得数据。
处于阻塞状态的任务正在等待队列中的数据可用,当另一个任务或中断将数据放入队列中时,该任务会自动移动到Ready状态。如果指定的块时间在数据可用之前过期,任务也会自动从Blocked状态移动到Ready状态。
队列可以有多个读取者,因此单个队列上可能阻塞多个任务等待数据。在这种情况下,当数据可用时,只有一个任务被解除阻塞。未阻塞的任务将始终是等待数据的最高优先级任务。如果阻塞的任务具有相同的优先级,那么等待数据时间最长的任务被解除阻塞。
队列写阻塞
与从队列读取时一样,任务也可以在写入队列时指定阻塞时间。在这种情况下,阻塞时间是在队列已经满的情况下,任务保持在Blocked状态来等待队列上有可用空间的最长时间。
队列可以有多个写入者,因此一个完整的队列上可能阻塞了多个任务,等待完成发送操作。
在这种情况下,当队列上的空间变为可用时,只有一个任务将被解除阻塞。未阻塞的任务将始终是等待空间的最高优先级任务。如果阻塞的任务具有相同的优先级,那么等待空间最长的任务将被解除阻塞。
多队列阻塞
可以将队列分组到集合中,允许任务进入Blocked状态以等待数据在集合中的任何队列上变为可用。
**xQueueCreate()**API 函数:在使用队列之前,必须显示地创建队列。创建一个队列并返回引用它所创建的队列的QueueHandle_t。
队列由句柄引用,句柄是QueueHandle_t类型的变量。
当创建队列时,FreeRTOS从FreeRTOS堆中分配RAM。RAM用于保存队列数据结构和队列中包含的项。如果要创建的队列没有足够的堆RAM可用,xQueueCreate()返回NULL。
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,UBaseType_t uxItemSize);
在创建队列之后,可以使用xQueueReset()API函数将队列返回到原始的空状态。
xQueueSendToBack()和xQueueSendToFront()
注意:永远不要从中断服务程序中调用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);
**xQueueReceive()**API函数:从队列中读取数据项,然后从队列中删除此项目。
注意:永远不要从中断服务例程调用xQueueReceive()。要使用xQueueReceiveFromISR()
BaseType_t xQueueReceive(QueueHandle_t xQueue,void * const pvBuffer, TickType_t xTicksToWait);
**uxQueueMessagesWaiting()**API函数:查询当前在队列中的项的数量。
永远不要从中断服务例程调用uxQueueMessagesWaiting()。应该使用uxQueueMessagesWaitingFromISR()。
UBaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue);
例10.当从队列接收数据时阻塞
static void vSendrTask(void *pvParameters)
{
int32_t lValueToSend;
BaseType_t xSatus;
/*创建了该任务的两个实例,因此发送到队列的值通过task参数传入—这样,每个实例可以使用不同的值。创建队列是为了保存类型为int32_t的值,因此将参数强制转换为所需的类型。*/
lValueToSend = (int32_t)pvParameters;
/*和大多数任务一样,这个任务是在一个无限循环中实现的。*/
for(;;)
{
/*将值发送到队列。
第一个参数是数据被发送到的队列。队列是在启动调度器之前创建的,因此是在此任务开始执行之前。
第二个参数是要发送的数据的地址,在本例中是lValueToSend的地址。
第三个参数是阻塞时间——在队列已经满的情况下,任务应该保持阻塞状态以等待队列上有可用空间的时间。在这种情况下,没有指定块时间,因为队列永远不应该包含一个以上的项,因此永远不会满。*/
xStatus = xQueueSendToBack(xQueue,&lValueToSend,0);
if(xStatus != pdPASS)
{
/*发送操作无法完成,因为队列已满-这一定是一个错误,因为队列不应该包含超过一个项!* /
vPrintString( "Could not send to the queue.\r\n" );
}
}
}
接收任务指定了100毫秒的阻塞时间,因此将进入阻塞状态等待数据可用。当队列上有数据可用或者超过100毫秒时,它会离开Blocked状态。100毫秒的超时应该永远不会过期,因为有两个任务不断地向队列写入。
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" );
}
/*从队列接收数据。
第一个参数是接收数据的队列。队列是在调度程序启动之前创建的,因此是在此任务第一次运行之前。
第二个参数是缓冲区,接收到的数据将放在其中。在这种情况下,缓冲区只是一个变量的地址,该变量具有容纳接收到的数据所需的大小。最后一个参数是块时间——如果队列已经为空,任务将保持在Blocked状态以等待数据可用的最长时间。*/
xStatus = xQueueReceive(xQueue,&lReceivedValue,xTicksToWait);
if(xStatus == pdPASS)
{
/*从队列中成功接收数据,打印接收的值。*/
vPrintStringAndNumber( "Received = ", lReceivedValue );
}
else{
/*队列等待100ms后仍未收到数据。
这一定是一个错误,因为发送任务是自由运行的,将不断写入队列。*/
vPrintString( "Could not receive from the queue.\r\n" );
}
}
}
在启动调度器之前创建队列和三个任务,创建该队列以保存最多5个int32_t值。
/*声明一个QueueHandle_t类型的变量,用于存储三个任务都要访问的队列的句柄。*/
int main()
{
/*创建队列以保存最多5个值,每个值都足够容纳一个类型为int32_t的变量*/
xQueue = xQueueCreate(5,sizeof(int32_t));
if (xQueue != NULL)
{
/*创建两个将发送到队列的任务实例。task参数用于传递任务将写入队列的值,因此一个任务将持续向队列写入100,而另一个任务将持续向队列写入200。两个任务都在优先级1上创建。*/
xTaskCreate(vSenderTask,"Sender 1",1000, (void *)100, 1,NULL);
xTaskCreate(vSenderTask,"Sender 2",1000, (void *)200, 1,NULL);
/*创建从队列中读取的任务。该任务的优先级为2,因此高于发送方任务的优先级。*/
xTaskCreate(vReceiverTask,"Receiver",1000,NULL,2,NULL);
vTaskStartScheduler();
}
else
{
/*无法创建队列。*/
}
/*如果一切正常,main()将永远不会到达这里,因为调度器现在正在运行任务。如果main()到达这里,那么很可能没有足够的FreeRTOS堆内存用于创建空闲任务。第2章提供了关于堆内存管理的更多信息。*/
for(;;);
}
发送到队列的两个任务具有相同的优先级。这将导致两个发送任务依次向队列发送数据。示例10产生的输出如图32所示。
在FreeRTOS设计中,任务从多个源接收数据是很常见的。接收任务需要知道数据来自哪里,来确定该如何处理数据。
一个简单的设计方案是使用队列来传输结构,结构的字段中包含数据的值和数据的源。
typedef struct{
ID_t eDataID;
int32_t lDataValue;
}Data_t;
例11.发送消息到队列时阻塞,并在队列上发送有结构的消息
/*用于标识数据来源的枚举类型。*/
typedef enum
{
eSender1,
eSender2
}DataSource_t;
/*定义将传递给队列的结构类型。*/
typedef struct{
uint8_t ucValue;
DataSource_t eDataSource;
}Data_t;
/*声明两个Data_t类型的变量,它们将在队列上传递。*/
static const Data_t xStructsToSend[2] =
{
{100,eSender1},
{200,eSender2}
}
static void vSenderTask(void *pvParameters)
{
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS(100);
for(;;)
{
/*发送消息到队列
第二个参数是被发送的结构的地址,地址作为任务参数传入,因此直接使用pvParameters。第三个参数是阻塞时间——如果队列已经满了,任务应该保持阻塞状态以等待队列上有可用空间的时间。之所以指定阻塞时间,是因为发送任务的优先级高于接收任务,因此队列预计将被填满。当两个发送任务都处于Blocked状态时,接收任务将从队列中删除项。*/
xStatus = xQueueSendToBack(xQueue,pvParameters,xTicksToWait);
if(xStatus != PASS)
{
/*发送操作无法完成,即使在等待100ms后。
这一定是一个错误,因为只要两个发送任务都处于Blocked状态,接收任务就应该在队列中腾出空间。*/
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.eDataScource == 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,&(xStrcutsToSend[0]),2,NULL);
xTaskCreate(vSenderTask,"sender2",1000,&(xStrcutsToSend[1]),2,NULL);
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
vTaskStartScheduler();
}
else{
}
for(;;);
}
如果存储在队列中的数据的大小很大,那么最好使用队列来传输指向数据的指针,而不是一个字节一个字节地将数据复制到队列中。传输指针在处理时间和创建队列所需的RAM量方面都更有效。
当使用指针队列时,必须特别小心,以确保:
如何使用队列将指向缓冲区的指针从一个任务发送到另一个任务。